mdgaziur001

Untitled

Aug 8th, 2025
562
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Go 10.97 KB | None | 0 0
  1. package user
  2.  
  3. import (
  4.     "context"
  5.     "database/sql"
  6.     encore "encore.dev"
  7.     "encore.dev/beta/auth"
  8.     "encore.dev/beta/errs"
  9.     "encore.dev/rlog"
  10.     "encore.dev/storage/cache"
  11.     "errors"
  12.     "github.com/go-playground/validator/v10"
  13.     "github.com/golang-jwt/jwt/v5"
  14.     "github.com/google/uuid"
  15.     "github.com/matthewhartstonge/argon2"
  16.     "strings"
  17.     "time"
  18. )
  19.  
  20. var authCacheCluster = cache.NewCluster("auth-cache-cluster", cache.ClusterConfig{
  21.     EvictionPolicy: cache.VolatileTTL,
  22. })
  23.  
  24. type AuthTokenInfo struct {
  25.     TokenId   string
  26.     UserAgent string
  27.     IpAddress string
  28.     Kind      AuthTokenKind
  29. }
  30.  
  31. type AuthTokenKind int
  32.  
  33. const (
  34.     AccessToken AuthTokenKind = iota
  35.     RefreshToken
  36. )
  37.  
  38. type AuthToken struct {
  39.     UserId  string
  40.     TokenId string
  41.     Kind    AuthTokenKind
  42.     jwt.RegisteredClaims
  43. }
  44.  
  45. type AuthInfo struct {
  46.     User        User
  47.     AuthTokenId string
  48. }
  49.  
  50. var AccessTokenValidity = cache.NewStructKeyspace[string, AuthTokenInfo](authCacheCluster, cache.KeyspaceConfig{
  51.     KeyPattern:    "auth/accessToken/:key",
  52.     DefaultExpiry: cache.ExpireIn(10 * time.Minute),
  53. })
  54.  
  55. var RefreshTokenValidity = cache.NewStructKeyspace[string, AuthTokenInfo](authCacheCluster, cache.KeyspaceConfig{
  56.     KeyPattern:    "auth/refreshToken/:key",
  57.     DefaultExpiry: cache.ExpireIn(30 * 24 * time.Hour),
  58. })
  59.  
  60. var validate = validator.New()
  61. var secrets struct {
  62.     JWTSecret string
  63. }
  64.  
  65. type LoginParams struct {
  66.     Email    string `json:"email" validate:"required,email"`
  67.     Password string `json:"password" validate:"required,min=8,max=100"`
  68. }
  69.  
  70. type LoginResult struct {
  71.     AccessToken  string `json:"access_token"`
  72.     RefreshToken string `json:"refresh_token"`
  73. }
  74.  
  75. //encore:authhandler
  76. func ValidateLogin(ctx context.Context, token string) (auth.UID, *AuthInfo, error) {
  77.     if strings.HasPrefix(token, "Bearer") {
  78.         token = strings.Split(token, " ")[1]
  79.     }
  80.     parsedToken, _, err := getTokenWithoutValidation(token)
  81.     if err != nil {
  82.         rlog.Warn("Failed to decode token", "err", err)
  83.         return "", nil, &errs.Error{
  84.             Code: errs.Unauthenticated,
  85.         }
  86.     }
  87.  
  88.     tokenKind := AuthTokenKind(parsedToken.Claims.(jwt.MapClaims)["Kind"].(float64))
  89.     if tokenKind != AccessToken {
  90.         rlog.Warn("Got something other than access token", "kind", tokenKind)
  91.         return "", nil, &errs.Error{
  92.             Code: errs.Unauthenticated,
  93.         }
  94.     }
  95.  
  96.     userId := parsedToken.Claims.(jwt.MapClaims)["UserId"].(string)
  97.     var userPassword string
  98.     err = userdb.QueryRow(ctx, `select password from users where id = $1`, userId).Scan(&userPassword)
  99.     if err != nil {
  100.         rlog.Warn("Failed to fetch user", "err", err)
  101.         return "", nil, &errs.Error{
  102.             Code: errs.Unauthenticated,
  103.         }
  104.     }
  105.  
  106.     validatedToken, err := verifyToken(token, secrets.JWTSecret+userPassword)
  107.     if err != nil {
  108.         rlog.Warn("Failed to validate login", "err", err)
  109.         return "", nil, &errs.Error{
  110.             Code: errs.Unauthenticated,
  111.         }
  112.     }
  113.  
  114.     _, err = AccessTokenValidity.Get(ctx, validatedToken.Claims.(jwt.MapClaims)["TokenId"].(string))
  115.     if err != nil {
  116.         rlog.Warn("Failed to get access token from cache", "err", err)
  117.         return "", nil, &errs.Error{
  118.             Code: errs.Unauthenticated,
  119.         }
  120.     }
  121.  
  122.     user := User{}
  123.     err = userdb.QueryRow(ctx, `select * from users where id = $1`, userId).Scan(
  124.         &user.Id,
  125.         &user.FirstName,
  126.         &user.LastName,
  127.         &user.Email,
  128.         &user.Password,
  129.         &user.Joined,
  130.     )
  131.     if err != nil {
  132.         rlog.Error("Failed to fetch user after validation", "err", err)
  133.         return "", nil, &errs.Error{
  134.             Code: errs.Internal,
  135.         }
  136.     }
  137.  
  138.     return auth.UID(userId), &AuthInfo{
  139.         User:        user,
  140.         AuthTokenId: validatedToken.Claims.(jwt.MapClaims)["TokenId"].(string),
  141.     }, nil
  142. }
  143.  
  144. //encore:api method=POST public
  145. func Login(ctx context.Context, params *LoginParams) (LoginResult, error) {
  146.     if err := validate.Struct(params); err != nil {
  147.         return LoginResult{}, &errs.Error{
  148.             Code:    errs.InvalidArgument,
  149.             Message: err.Error(),
  150.         }
  151.     }
  152.  
  153.     user := User{}
  154.     err := userdb.QueryRow(ctx, `select id, password from users where email = $1`, params.Email).Scan(
  155.         &user.Id,
  156.         &user.Password,
  157.     )
  158.     if err != nil {
  159.         if errors.Is(err, sql.ErrNoRows) {
  160.             return LoginResult{}, &errs.Error{
  161.                 Code:    errs.InvalidArgument,
  162.                 Message: "Invalid email or password",
  163.             }
  164.         } else {
  165.             rlog.Warn("Failed to query for user", "email", params.Email, "err", err)
  166.             return LoginResult{}, &errs.Error{
  167.                 Code: errs.Internal,
  168.             }
  169.         }
  170.     }
  171.  
  172.     tokenId := uuid.New().String()
  173.  
  174.     signedAccessToken, err := signToken(AuthToken{
  175.         UserId:  user.Id,
  176.         TokenId: tokenId,
  177.         Kind:    AccessToken,
  178.         RegisteredClaims: jwt.RegisteredClaims{
  179.             ExpiresAt: jwt.NewNumericDate(time.Now().Add(10 * time.Minute)),
  180.         },
  181.     }, secrets.JWTSecret+user.Password)
  182.     if err != nil {
  183.         rlog.Error("Failed to sign access token", "err", err)
  184.         return LoginResult{}, &errs.Error{
  185.             Code: errs.Internal,
  186.         }
  187.     }
  188.  
  189.     signedRefreshToken, err := signToken(AuthToken{
  190.         UserId:  user.Id,
  191.         TokenId: tokenId,
  192.         Kind:    RefreshToken,
  193.         RegisteredClaims: jwt.RegisteredClaims{
  194.             ExpiresAt: jwt.NewNumericDate(time.Now().Add(30 * 24 * time.Hour)),
  195.         },
  196.     }, secrets.JWTSecret+user.Password)
  197.     if err != nil {
  198.         rlog.Error("Failed to sign refresh token", "err", err)
  199.         return LoginResult{}, &errs.Error{
  200.             Code: errs.Internal,
  201.         }
  202.     }
  203.  
  204.     accessTokenInfo := AuthTokenInfo{
  205.         TokenId:   tokenId,
  206.         UserAgent: encore.CurrentRequest().Headers.Get("User-Agent"),
  207.         IpAddress: encore.CurrentRequest().Headers.Get("X-Forwarded-For"),
  208.         Kind:      AccessToken,
  209.     }
  210.     err = AccessTokenValidity.Set(ctx, tokenId, accessTokenInfo)
  211.     if err != nil {
  212.         rlog.Error("Failed to save access token id in cache", "err", err)
  213.         return LoginResult{}, &errs.Error{
  214.             Code: errs.Internal,
  215.         }
  216.     }
  217.  
  218.     refreshTokenInfo := AuthTokenInfo{
  219.         TokenId:   tokenId,
  220.         UserAgent: encore.CurrentRequest().Headers.Get("User-Agent"),
  221.         IpAddress: encore.CurrentRequest().Headers.Get("X-Forwarded-For"),
  222.         Kind:      RefreshToken,
  223.     }
  224.     err = RefreshTokenValidity.Set(ctx, tokenId, refreshTokenInfo)
  225.     if err != nil {
  226.         rlog.Error("Failed to save refresh token id in cache", "err", err)
  227.         return LoginResult{}, &errs.Error{
  228.             Code: errs.Internal,
  229.         }
  230.     }
  231.  
  232.     res := LoginResult{
  233.         AccessToken:  signedAccessToken,
  234.         RefreshToken: signedRefreshToken,
  235.     }
  236.     return res, nil
  237. }
  238.  
  239. type RegistrationParams struct {
  240.     FirstName string `json:"first_name" validate:"required,min=1,max=64"`
  241.     LastName  string `json:"last_name" validate:"required,min=1,max=64"`
  242.     Email     string `json:"email" validate:"required,email,max=50"`
  243.     Password  string `json:"password" validate:"required,min=8,max=100"`
  244. }
  245.  
  246. type RegistrationResult struct {
  247.     Message string `json:"message"`
  248. }
  249.  
  250. //encore:api method=POST public
  251. func Register(ctx context.Context, params *RegistrationParams) (RegistrationResult, error) {
  252.     if err := validate.Struct(params); err != nil {
  253.         return RegistrationResult{}, &errs.Error{
  254.             Code:    errs.InvalidArgument,
  255.             Message: err.Error(),
  256.         }
  257.     }
  258.  
  259.     rows, err := userdb.Query(ctx, `select * from users where email=($1)`, params.Email)
  260.     if err != nil {
  261.         rlog.Error("Failed to query for user", "email", params.Email, "err", err)
  262.         return RegistrationResult{}, &errs.Error{
  263.             Code: errs.Internal,
  264.         }
  265.     }
  266.  
  267.     if rows.Next() {
  268.         return RegistrationResult{}, &errs.Error{
  269.             Code:    errs.InvalidArgument,
  270.             Message: "User already exists",
  271.         }
  272.     }
  273.  
  274.     argon := argon2.DefaultConfig()
  275.  
  276.     hashedPassword, err := argon.HashEncoded([]byte(params.Password))
  277.     if err != nil {
  278.         rlog.Error("Failed to hash password", "err", err)
  279.         return RegistrationResult{}, &errs.Error{
  280.             Code: errs.Internal,
  281.         }
  282.     }
  283.  
  284.     _, err = userdb.Exec(ctx, `insert into users (first_name, last_name, email, password) values ($1, $2, $3, $4)`,
  285.         params.FirstName, params.LastName, params.Email, hashedPassword)
  286.     if err != nil {
  287.         rlog.Error("Failed to insert user", "email", params.Email, "err", err)
  288.         return RegistrationResult{}, &errs.Error{
  289.             Code: errs.Internal,
  290.         }
  291.     }
  292.  
  293.     return RegistrationResult{
  294.         Message: "Registration successful!",
  295.     }, nil
  296. }
  297.  
  298. //encore:api method=POST auth
  299. func Logout(ctx context.Context) error {
  300.     authTokenId := auth.Data().(*AuthInfo).AuthTokenId
  301.     _, err := AccessTokenValidity.Delete(ctx, authTokenId)
  302.     if err != nil {
  303.         rlog.Error("Failed to delete access token info", "err", err)
  304.         return &errs.Error{
  305.             Code: errs.Internal,
  306.         }
  307.     }
  308.     _, err = RefreshTokenValidity.Delete(ctx, authTokenId)
  309.     if err != nil {
  310.         rlog.Error("Failed to delete refresh token info", "err", err)
  311.         return &errs.Error{
  312.             Code: errs.Internal,
  313.         }
  314.     }
  315.  
  316.     return nil
  317. }
  318.  
  319. type RefreshAccessTokenParams struct {
  320.     RefreshToken string `json:"refresh_token"`
  321. }
  322.  
  323. type RefreshAccessTokenResult struct {
  324.     AccessToken string `json:"access_token"`
  325. }
  326.  
  327. //encore:api method=POST public
  328. func RefreshAccessToken(ctx context.Context, params *RefreshAccessTokenParams) (RefreshAccessTokenResult, error) {
  329.     token := params.RefreshToken
  330.     parsedToken, _, err := getTokenWithoutValidation(token)
  331.     if err != nil {
  332.         rlog.Warn("Failed to decode token", "err", err)
  333.         return RefreshAccessTokenResult{}, &errs.Error{
  334.             Code: errs.Unauthenticated,
  335.         }
  336.     }
  337.  
  338.     tokenKind := AuthTokenKind(parsedToken.Claims.(jwt.MapClaims)["Kind"].(float64))
  339.     if tokenKind != RefreshToken {
  340.         rlog.Warn("Got something other than refresh token", "kind", tokenKind)
  341.         return RefreshAccessTokenResult{}, &errs.Error{
  342.             Code: errs.Unauthenticated,
  343.         }
  344.     }
  345.  
  346.     userId := parsedToken.Claims.(jwt.MapClaims)["UserId"].(string)
  347.     var userPassword string
  348.     err = userdb.QueryRow(ctx, `select password from users where id = $1`, userId).Scan(&userPassword)
  349.     if err != nil {
  350.         rlog.Warn("Failed to fetch user", "err", err)
  351.         return RefreshAccessTokenResult{}, &errs.Error{
  352.             Code: errs.Unauthenticated,
  353.         }
  354.     }
  355.  
  356.     validatedToken, err := verifyToken(token, secrets.JWTSecret+userPassword)
  357.     if err != nil {
  358.         rlog.Warn("Failed to validate login", "err", err)
  359.         return RefreshAccessTokenResult{}, &errs.Error{
  360.             Code: errs.Unauthenticated,
  361.         }
  362.     }
  363.  
  364.     refreshTokenInfo, err := AccessTokenValidity.Get(ctx, validatedToken.Claims.(jwt.MapClaims)["TokenId"].(string))
  365.     if err != nil {
  366.         rlog.Warn("Failed to get refresh token from cache", "err", err)
  367.         return RefreshAccessTokenResult{}, &errs.Error{
  368.             Code: errs.Unauthenticated,
  369.         }
  370.     }
  371.  
  372.     accessToken, err := signToken(AuthToken{
  373.         UserId:  userId,
  374.         TokenId: refreshTokenInfo.TokenId,
  375.         Kind:    AccessToken,
  376.         RegisteredClaims: jwt.RegisteredClaims{
  377.             ExpiresAt: jwt.NewNumericDate(time.Now().Add(10 * time.Minute)),
  378.         },
  379.     }, secrets.JWTSecret+userPassword)
  380.  
  381.     if err != nil {
  382.         rlog.Error("Failed to sign refresh token", "err", err)
  383.         return RefreshAccessTokenResult{}, &errs.Error{
  384.             Code: errs.Internal,
  385.         }
  386.     }
  387.  
  388.     return RefreshAccessTokenResult{
  389.         accessToken,
  390.     }, nil
  391. }
  392.  
  393. type CurrentUserResponse struct {
  394.     CurrentUser User `json:"current_user"`
  395. }
  396.  
  397. //encore:api method=GET auth
  398. func GetCurrentUser(ctx context.Context) (CurrentUserResponse, error) {
  399.     user := auth.Data().(*AuthInfo).User
  400.     user.Password = "<redacted>"
  401.     return CurrentUserResponse{
  402.         CurrentUser: user,
  403.     }, nil
  404. }
  405.  
Advertisement
Add Comment
Please, Sign In to add comment