mirror of
https://github.com/aykhans/slash-e.git
synced 2025-04-16 12:23:12 +00:00
212 lines
8.0 KiB
Go
212 lines
8.0 KiB
Go
package v1
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
"github.com/yourselfhosted/slash/api/auth"
|
|
storepb "github.com/yourselfhosted/slash/proto/gen/store"
|
|
"github.com/yourselfhosted/slash/server/metric"
|
|
"github.com/yourselfhosted/slash/server/service/license"
|
|
"github.com/yourselfhosted/slash/store"
|
|
)
|
|
|
|
type SignInRequest struct {
|
|
Email string `json:"email"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
type SignUpRequest struct {
|
|
Nickname string `json:"nickname"`
|
|
Email string `json:"email"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
func (s *APIV1Service) registerAuthRoutes(g *echo.Group, secret string) {
|
|
g.POST("/auth/signin", func(c echo.Context) error {
|
|
ctx := c.Request().Context()
|
|
signin := &SignInRequest{}
|
|
if err := json.NewDecoder(c.Request().Body).Decode(signin); err != nil {
|
|
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("malformatted signin request, err: %s", err))
|
|
}
|
|
|
|
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
|
Email: &signin.Email,
|
|
})
|
|
if err != nil {
|
|
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to find user by email %s", signin.Email)).SetInternal(err)
|
|
}
|
|
if user == nil {
|
|
return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("user not found with email %s", signin.Email))
|
|
} else if user.RowStatus == store.Archived {
|
|
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("user has been archived with email %s", signin.Email))
|
|
}
|
|
|
|
// Compare the stored hashed password, with the hashed version of the password that was received.
|
|
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(signin.Password)); err != nil {
|
|
return echo.NewHTTPError(http.StatusUnauthorized, "unmatched email and password")
|
|
}
|
|
|
|
accessToken, err := auth.GenerateAccessToken(user.Email, user.ID, time.Now().Add(auth.AccessTokenDuration), []byte(secret))
|
|
if err != nil {
|
|
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to generate tokens, err: %s", err)).SetInternal(err)
|
|
}
|
|
if err := s.UpsertAccessTokenToStore(ctx, user, accessToken); err != nil {
|
|
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert access token, err: %s", err)).SetInternal(err)
|
|
}
|
|
|
|
cookieExp := time.Now().Add(auth.CookieExpDuration)
|
|
setTokenCookie(c, auth.AccessTokenCookieName, accessToken, cookieExp)
|
|
metric.Enqueue("user sign in")
|
|
return c.JSON(http.StatusOK, convertUserFromStore(user))
|
|
})
|
|
|
|
g.POST("/auth/signup", func(c echo.Context) error {
|
|
ctx := c.Request().Context()
|
|
enableSignUpSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{
|
|
Key: storepb.WorkspaceSettingKey_WORKSAPCE_SETTING_ENABLE_SIGNUP,
|
|
})
|
|
if err != nil {
|
|
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to get workspace setting, err: %s", err)).SetInternal(err)
|
|
}
|
|
if enableSignUpSetting != nil && !enableSignUpSetting.GetEnableSignup() {
|
|
return echo.NewHTTPError(http.StatusForbidden, "sign up has been disabled")
|
|
}
|
|
|
|
if !s.LicenseService.IsFeatureEnabled(license.FeatureTypeUnlimitedAccounts) {
|
|
userList, err := s.Store.ListUsers(ctx, &store.FindUser{})
|
|
if err != nil {
|
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to list users").SetInternal(err)
|
|
}
|
|
if len(userList) >= 5 {
|
|
return echo.NewHTTPError(http.StatusBadRequest, "Maximum number of users reached")
|
|
}
|
|
}
|
|
|
|
signup := &SignUpRequest{}
|
|
if err := json.NewDecoder(c.Request().Body).Decode(signup); err != nil {
|
|
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("malformatted signup request, err: %s", err)).SetInternal(err)
|
|
}
|
|
|
|
passwordHash, err := bcrypt.GenerateFromPassword([]byte(signup.Password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return echo.NewHTTPError(http.StatusInternalServerError, "failed to generate password hash").SetInternal(err)
|
|
}
|
|
|
|
create := &store.User{
|
|
Email: signup.Email,
|
|
Nickname: signup.Nickname,
|
|
PasswordHash: string(passwordHash),
|
|
}
|
|
existingUsers, err := s.Store.ListUsers(ctx, &store.FindUser{})
|
|
if err != nil {
|
|
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to find existing users, err: %s", err)).SetInternal(err)
|
|
}
|
|
// The first user to sign up is an admin by default.
|
|
if len(existingUsers) == 0 {
|
|
create.Role = store.RoleAdmin
|
|
} else {
|
|
create.Role = store.RoleUser
|
|
}
|
|
|
|
user, err := s.Store.CreateUser(ctx, create)
|
|
if err != nil {
|
|
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to create user, err: %s", err)).SetInternal(err)
|
|
}
|
|
|
|
accessToken, err := auth.GenerateAccessToken(user.Email, user.ID, time.Now().Add(auth.AccessTokenDuration), []byte(secret))
|
|
if err != nil {
|
|
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to generate tokens, err: %s", err)).SetInternal(err)
|
|
}
|
|
if err := s.UpsertAccessTokenToStore(ctx, user, accessToken); err != nil {
|
|
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert access token, err: %s", err)).SetInternal(err)
|
|
}
|
|
|
|
cookieExp := time.Now().Add(auth.CookieExpDuration)
|
|
setTokenCookie(c, auth.AccessTokenCookieName, accessToken, cookieExp)
|
|
metric.Enqueue("user sign up")
|
|
return c.JSON(http.StatusOK, convertUserFromStore(user))
|
|
})
|
|
|
|
g.POST("/auth/logout", func(c echo.Context) error {
|
|
ctx := c.Request().Context()
|
|
RemoveTokensAndCookies(c)
|
|
accessToken := findAccessToken(c)
|
|
userID, _ := getUserIDFromAccessToken(accessToken, secret)
|
|
userAccessTokens, err := s.Store.GetUserAccessTokens(ctx, userID)
|
|
// Auto remove the current access token from the user access tokens.
|
|
if err == nil && len(userAccessTokens) != 0 {
|
|
accessTokens := []*storepb.AccessTokensUserSetting_AccessToken{}
|
|
for _, userAccessToken := range userAccessTokens {
|
|
if accessToken != userAccessToken.AccessToken {
|
|
accessTokens = append(accessTokens, userAccessToken)
|
|
}
|
|
}
|
|
|
|
if _, err := s.Store.UpsertUserSetting(ctx, &storepb.UserSetting{
|
|
UserId: userID,
|
|
Key: storepb.UserSettingKey_USER_SETTING_ACCESS_TOKENS,
|
|
Value: &storepb.UserSetting_AccessTokens{
|
|
AccessTokens: &storepb.AccessTokensUserSetting{
|
|
AccessTokens: accessTokens,
|
|
},
|
|
},
|
|
}); err != nil {
|
|
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert user setting, err: %s", err)).SetInternal(err)
|
|
}
|
|
}
|
|
c.Response().WriteHeader(http.StatusOK)
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (s *APIV1Service) UpsertAccessTokenToStore(ctx context.Context, user *store.User, accessToken string) error {
|
|
userAccessTokens, err := s.Store.GetUserAccessTokens(ctx, user.ID)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to get user access tokens")
|
|
}
|
|
userAccessToken := storepb.AccessTokensUserSetting_AccessToken{
|
|
AccessToken: accessToken,
|
|
Description: "Account sign in",
|
|
}
|
|
userAccessTokens = append(userAccessTokens, &userAccessToken)
|
|
if _, err := s.Store.UpsertUserSetting(ctx, &storepb.UserSetting{
|
|
UserId: user.ID,
|
|
Key: storepb.UserSettingKey_USER_SETTING_ACCESS_TOKENS,
|
|
Value: &storepb.UserSetting_AccessTokens{
|
|
AccessTokens: &storepb.AccessTokensUserSetting{
|
|
AccessTokens: userAccessTokens,
|
|
},
|
|
},
|
|
}); err != nil {
|
|
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert user setting, err: %s", err)).SetInternal(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RemoveTokensAndCookies removes the jwt token from the cookies.
|
|
func RemoveTokensAndCookies(c echo.Context) {
|
|
cookieExp := time.Now().Add(-1 * time.Hour)
|
|
setTokenCookie(c, auth.AccessTokenCookieName, "", cookieExp)
|
|
}
|
|
|
|
// setTokenCookie sets the token to the cookie.
|
|
func setTokenCookie(c echo.Context, name, token string, expiration time.Time) {
|
|
cookie := new(http.Cookie)
|
|
cookie.Name = name
|
|
cookie.Value = token
|
|
cookie.Expires = expiration
|
|
cookie.Path = "/"
|
|
// Http-only helps mitigate the risk of client side script accessing the protected cookie.
|
|
cookie.HttpOnly = true
|
|
cookie.SameSite = http.SameSiteStrictMode
|
|
c.SetCookie(cookie)
|
|
}
|