12 files changed,
546 insertions(+),
423 deletions(-)
Author:
Olexandr Smirnov
olexsmir@gmail.com
Committed by:
GitHub
noreply@github.com
Committed at:
2025-08-27 14:17:35 +0300
Parent:
f469c12
jump to
M
cmd/api/main.go
··· 18 18 "github.com/olexsmir/onasty/internal/logger" 19 19 "github.com/olexsmir/onasty/internal/metrics" 20 20 "github.com/olexsmir/onasty/internal/oauth" 21 + "github.com/olexsmir/onasty/internal/service/authsrv" 21 22 "github.com/olexsmir/onasty/internal/service/notesrv" 22 23 "github.com/olexsmir/onasty/internal/service/usersrv" 23 24 "github.com/olexsmir/onasty/internal/store/psql/changeemailrepo" ··· 105 106 usercache := usercache.New(redisDB, cfg.CacheUsersTTL) 106 107 usersrv := usersrv.New( 107 108 userepo, 108 - sessionrepo, 109 109 vertokrepo, 110 110 pwdtokrepo, 111 111 changeemailrepo, 112 112 noterepo, 113 113 userPasswordHasher, 114 + mailermq, 115 + cfg.VerificationTokenTTL, 116 + cfg.ResetPasswordTokenTTL, 117 + cfg.ChangeEmailTokenTTL, 118 + ) 119 + 120 + authsrv := authsrv.New( 121 + userepo, 122 + sessionrepo, 123 + vertokrepo, 124 + usercache, 125 + userPasswordHasher, 114 126 jwtTokenizer, 115 127 mailermq, 116 - usercache, 117 128 googleOauth, 118 129 githubOauth, 119 130 cfg.JwtRefreshTokenTTL, 120 131 cfg.VerificationTokenTTL, 121 - cfg.ResetPasswordTokenTTL, 122 - cfg.ChangeEmailTokenTTL, 123 132 ) 124 133 125 134 rateLimiterConfig := ratelimit.Config{ ··· 135 144 } 136 145 137 146 handler := httptransport.NewTransport( 147 + authsrv, 138 148 usersrv, 139 149 notesrv, 140 150 cfg.AppEnv,
M
e2e/e2e_test.go
··· 15 15 "github.com/olexsmir/onasty/internal/hasher" 16 16 "github.com/olexsmir/onasty/internal/jwtutil" 17 17 "github.com/olexsmir/onasty/internal/logger" 18 + "github.com/olexsmir/onasty/internal/service/authsrv" 18 19 "github.com/olexsmir/onasty/internal/service/notesrv" 19 20 "github.com/olexsmir/onasty/internal/service/usersrv" 20 21 "github.com/olexsmir/onasty/internal/store/psql/changeemailrepo" ··· 106 107 changeemailrepo := changeemailrepo.New(e.postgresDB) 107 108 108 109 stubOAuthProvider := newOauthProviderStub() 110 + mailerMockService := newMailerMockService() 109 111 110 112 notecache := notecache.New(e.redisDB, cfg.CacheUsersTTL) 111 113 noterepo := noterepo.New(e.postgresDB) ··· 115 117 usercache := usercache.New(e.redisDB, cfg.CacheUsersTTL) 116 118 usersrv := usersrv.New( 117 119 userepo, 118 - sessionrepo, 119 120 vertokrepo, 120 121 pwdtokrepo, 121 122 changeemailrepo, 122 123 noterepo, 123 124 e.hasher, 124 - e.jwtTokenizer, 125 - newMailerMockService(), 125 + mailerMockService, 126 + cfg.VerificationTokenTTL, 127 + cfg.ResetPasswordTokenTTL, 128 + cfg.ChangeEmailTokenTTL, 129 + ) 130 + 131 + authsrv := authsrv.New( 132 + userepo, 133 + sessionrepo, 134 + vertokrepo, 126 135 usercache, 136 + e.hasher, 137 + e.jwtTokenizer, 138 + mailerMockService, 127 139 stubOAuthProvider, 128 140 stubOAuthProvider, 129 141 cfg.JwtRefreshTokenTTL, 130 142 cfg.VerificationTokenTTL, 131 - cfg.ResetPasswordTokenTTL, 132 - cfg.ChangeEmailTokenTTL, 133 143 ) 134 144 135 145 // for testing purposes, it's ok to have high values ig ··· 140 150 } 141 151 142 152 handler := httptransport.NewTransport( 153 + authsrv, 143 154 usersrv, 144 155 notesrv, 145 156 cfg.AppEnv,
A
internal/service/authsrv/authsrv.go
··· 1 +package authsrv 2 + 3 +import ( 4 + "context" 5 + "errors" 6 + "log/slog" 7 + "time" 8 + 9 + "github.com/gofrs/uuid/v5" 10 + "github.com/olexsmir/onasty/internal/dtos" 11 + "github.com/olexsmir/onasty/internal/events/mailermq" 12 + "github.com/olexsmir/onasty/internal/hasher" 13 + "github.com/olexsmir/onasty/internal/jwtutil" 14 + "github.com/olexsmir/onasty/internal/models" 15 + "github.com/olexsmir/onasty/internal/oauth" 16 + "github.com/olexsmir/onasty/internal/store/psql/sessionrepo" 17 + "github.com/olexsmir/onasty/internal/store/psql/userepo" 18 + "github.com/olexsmir/onasty/internal/store/psql/vertokrepo" 19 + "github.com/olexsmir/onasty/internal/store/rdb/usercache" 20 +) 21 + 22 +type AuthServicer interface { 23 + // SignUp creates a new user and sends a verification email. 24 + // 25 + // Uses [models.User.Validate] to validate credentials (see more possible returned errors). 26 + // 27 + // If provided email already in use returns [models.ErrUserEmailIsAlreadyInUse]. 28 + // 29 + SignUp(ctx context.Context, credentials dtos.SignUp) error 30 + 31 + // SignIn authenticates a user and returns access and refresh tokens. 32 + // 33 + // If user not found returns [models.ErrUserNotFound], and if credentials don't match [models.ErrUserWrongCredentials] 34 + // 35 + // If inactivated user tries to login, returns [models.ErrUserIsNotActivated] 36 + // 37 + SignIn(ctx context.Context, credentials dtos.SignIn) (dtos.Tokens, error) 38 + 39 + // RefreshTokens refreshes the access and refresh tokens using the provided refresh token. 40 + // 41 + // If couldn't find a user liked with token, returns [models.ErrUserNotFound] 42 + // 43 + RefreshTokens(ctx context.Context, refreshToken string) (dtos.Tokens, error) 44 + 45 + // Logout logs out a user by deleting the session associated with the provided refresh token. 46 + Logout(ctx context.Context, userID uuid.UUID, refreshToken string) error 47 + 48 + // LogoutAll logs out a user by deleting all sessions associated with the user ID. 49 + LogoutAll(ctx context.Context, userID uuid.UUID) error 50 + 51 + // GetOAuthURL retrieves the OAuth URL for the specified provider. 52 + // 53 + // If [providerName] is incorrect returns [ErrProviderNotSupported] 54 + // 55 + GetOAuthURL(providerName string) (dtos.OAuthRedirect, error) 56 + 57 + // HandleOAuthLogin handles the OAuth login process by exchanging the code for tokens. 58 + // 59 + HandleOAuthLogin(ctx context.Context, providerName, code string) (dtos.Tokens, error) 60 + 61 + // ParseJWTToken parses the JWT token and returns the payload. 62 + // 63 + // If token is expired, returns [jwtutil.ErrTokenExpired], 64 + // 65 + // If token is invalid returns: [jwturil.ErrTokenSignatureInvalid], [jwt.ErrUnexpectedSigningMethod] 66 + // 67 + ParseJWTToken(token string) (jwtutil.Payload, error) 68 + 69 + // CheckIfUserExists checks if a user exists by user ID. 70 + CheckIfUserExists(ctx context.Context, userID uuid.UUID) (bool, error) 71 + 72 + // CheckIfUserIsActivated checks if a user is activated by user ID. 73 + CheckIfUserIsActivated(ctx context.Context, userID uuid.UUID) (bool, error) 74 +} 75 + 76 +var _ AuthServicer = (*AuthSrv)(nil) 77 + 78 +type AuthSrv struct { 79 + userstore userepo.UserStorer 80 + sessionstore sessionrepo.SessionStorer 81 + vertokrepo vertokrepo.VerificationTokenStorer 82 + cache usercache.UserCacheer 83 + 84 + hasher hasher.Hasher 85 + jwtTokenizer jwtutil.JWTTokenizer 86 + mailermq mailermq.Mailer 87 + 88 + googleOauth oauth.Provider 89 + githubOauth oauth.Provider 90 + 91 + refreshTokenTTL time.Duration 92 + verificationTokenTTL time.Duration 93 +} 94 + 95 +func New( 96 + userstore userepo.UserStorer, 97 + sessionstore sessionrepo.SessionStorer, 98 + vertokrepo vertokrepo.VerificationTokenStorer, 99 + cache usercache.UserCacheer, 100 + hasher hasher.Hasher, 101 + jwtTokenizer jwtutil.JWTTokenizer, 102 + mailermq mailermq.Mailer, 103 + googleOauth, githubOauth oauth.Provider, 104 + refreshTokenTTL, verificationTokenTTL time.Duration, 105 +) *AuthSrv { 106 + return &AuthSrv{ 107 + userstore: userstore, 108 + sessionstore: sessionstore, 109 + vertokrepo: vertokrepo, 110 + cache: cache, 111 + hasher: hasher, 112 + jwtTokenizer: jwtTokenizer, 113 + mailermq: mailermq, 114 + googleOauth: googleOauth, 115 + githubOauth: githubOauth, 116 + refreshTokenTTL: refreshTokenTTL, 117 + verificationTokenTTL: verificationTokenTTL, 118 + } 119 +} 120 + 121 +func (a *AuthSrv) SignUp(ctx context.Context, inp dtos.SignUp) error { 122 + user := models.User{ 123 + ID: uuid.Nil, // nil, since we do not know it yet 124 + Email: inp.Email, 125 + Activated: false, 126 + Password: inp.Password, 127 + CreatedAt: inp.CreatedAt, 128 + LastLoginAt: inp.LastLoginAt, 129 + } 130 + if err := user.Validate(); err != nil { 131 + return err 132 + } 133 + 134 + hashedPassword, err := a.hasher.Hash(inp.Password) 135 + if err != nil { 136 + return err 137 + } 138 + 139 + user.Password = hashedPassword 140 + 141 + userID, err := a.userstore.Create(ctx, user) 142 + if err != nil { 143 + return err 144 + } 145 + 146 + verificationToken := uuid.Must(uuid.NewV4()).String() 147 + if err := a.vertokrepo.Create(ctx, models.VerificationToken{ 148 + UserID: userID, 149 + Token: verificationToken, 150 + CreatedAt: time.Now(), 151 + ExpiresAt: time.Now().Add(a.verificationTokenTTL), 152 + }); err != nil { 153 + return err 154 + } 155 + 156 + if err := a.mailermq.SendVerificationEmail(ctx, mailermq.SendVerificationEmailRequest{ 157 + Receiver: inp.Email, 158 + Token: verificationToken, 159 + }); err != nil { 160 + return err 161 + } 162 + 163 + return nil 164 +} 165 + 166 +func (a *AuthSrv) SignIn(ctx context.Context, inp dtos.SignIn) (dtos.Tokens, error) { 167 + user, err := a.userstore.GetByEmail(ctx, inp.Email) 168 + if err != nil { 169 + return dtos.Tokens{}, err 170 + } 171 + 172 + if err = a.hasher.Compare(user.Password, inp.Password); err != nil { 173 + if errors.Is(err, hasher.ErrMismatchedHashes) { 174 + return dtos.Tokens{}, models.ErrUserWrongCredentials 175 + } 176 + return dtos.Tokens{}, err 177 + } 178 + 179 + if !user.IsActivated() { 180 + return dtos.Tokens{}, models.ErrUserIsNotActivated 181 + } 182 + 183 + return a.issueTokens(ctx, user.ID) 184 +} 185 + 186 +func (a *AuthSrv) RefreshTokens(ctx context.Context, rtoken string) (dtos.Tokens, error) { 187 + userID, err := a.sessionstore.GetUserIDByRefreshToken(ctx, rtoken) 188 + if err != nil { 189 + return dtos.Tokens{}, err 190 + } 191 + 192 + tokens, err := a.createTokens(userID) 193 + if err != nil { 194 + return dtos.Tokens{}, err 195 + } 196 + 197 + if err := a.sessionstore.Update(ctx, userID, rtoken, tokens.Refresh); err != nil { 198 + return dtos.Tokens{}, err 199 + } 200 + 201 + return dtos.Tokens{ 202 + Access: tokens.Access, 203 + Refresh: tokens.Refresh, 204 + }, nil 205 +} 206 + 207 +func (a *AuthSrv) Logout(ctx context.Context, userID uuid.UUID, refreshToken string) error { 208 + return a.sessionstore.Delete(ctx, userID, refreshToken) 209 +} 210 + 211 +func (a *AuthSrv) LogoutAll(ctx context.Context, userID uuid.UUID) error { 212 + return a.sessionstore.DeleteAllByUserID(ctx, userID) 213 +} 214 + 215 +func (a *AuthSrv) CheckIfUserExists(ctx context.Context, uid uuid.UUID) (bool, error) { 216 + isExists, err := a.cache.GetIsExists(ctx, uid.String()) 217 + if err == nil { 218 + return isExists, nil 219 + } 220 + slog.ErrorContext(ctx, "failed to fetch 'is user exists' cache", "err", err) 221 + 222 + isExists, err = a.userstore.CheckIfUserExists(ctx, uid) 223 + if err != nil { 224 + return false, err 225 + } 226 + 227 + if err := a.cache.SetIsExists(ctx, uid.String(), isExists); err != nil { 228 + slog.ErrorContext(ctx, "failed to update 'is user exists' cache", "err", err) 229 + } 230 + 231 + return isExists, nil 232 +} 233 + 234 +func (a *AuthSrv) CheckIfUserIsActivated(ctx context.Context, uid uuid.UUID) (bool, error) { 235 + isActivated, err := a.cache.GetIsActivated(ctx, uid.String()) 236 + if err == nil { 237 + return isActivated, nil 238 + } 239 + slog.ErrorContext(ctx, "failed to fetch 'is user activated' cache", "err", err) 240 + 241 + isActivated, err = a.userstore.CheckIfUserIsActivated(ctx, uid) 242 + if err != nil { 243 + return false, err 244 + } 245 + 246 + if err := a.cache.SetIsActivated(ctx, uid.String(), isActivated); err != nil { 247 + slog.ErrorContext(ctx, "failed to update 'is user activated' cache", "err", err) 248 + } 249 + 250 + return isActivated, nil 251 +}
A
internal/service/authsrv/jwt.go
··· 1 +package authsrv 2 + 3 +import ( 4 + "context" 5 + "time" 6 + 7 + "github.com/gofrs/uuid/v5" 8 + "github.com/olexsmir/onasty/internal/dtos" 9 + "github.com/olexsmir/onasty/internal/jwtutil" 10 +) 11 + 12 +func (a *AuthSrv) ParseJWTToken(token string) (jwtutil.Payload, error) { 13 + return a.jwtTokenizer.Parse(token) 14 +} 15 + 16 +func (a AuthSrv) issueTokens(ctx context.Context, userID uuid.UUID) (dtos.Tokens, error) { 17 + toks, err := a.createTokens(userID) 18 + if err != nil { 19 + return dtos.Tokens{}, err 20 + } 21 + 22 + if err := a.sessionstore.Set(ctx, userID, toks.Refresh, time.Now().Add(a.refreshTokenTTL)); err != nil { 23 + return dtos.Tokens{}, err 24 + } 25 + 26 + return toks, nil 27 +} 28 + 29 +func (a AuthSrv) createTokens(userID uuid.UUID) (dtos.Tokens, error) { 30 + accessToken, err := a.jwtTokenizer.AccessToken(jwtutil.Payload{UserID: userID.String()}) 31 + if err != nil { 32 + return dtos.Tokens{}, err 33 + } 34 + 35 + refreshToken, err := a.jwtTokenizer.RefreshToken() 36 + if err != nil { 37 + return dtos.Tokens{}, err 38 + } 39 + 40 + return dtos.Tokens{ 41 + Access: accessToken, 42 + Refresh: refreshToken, 43 + }, err 44 +}
M
internal/service/usersrv/oauth.go
→ internal/service/authsrv/oauth.go
··· 1 -package usersrv 1 +package authsrv 2 2 3 3 import ( 4 4 "context" ··· 19 19 githubProvider = "github" 20 20 ) 21 21 22 -func (u *UserSrv) GetOAuthURL(providerName string) (dtos.OAuthRedirect, error) { 22 +func (a *AuthSrv) GetOAuthURL(providerName string) (dtos.OAuthRedirect, error) { 23 23 state := uuid.Must(uuid.NewV4()).String() 24 24 25 25 switch providerName { 26 26 case googleProvider: 27 27 return dtos.OAuthRedirect{ 28 - URL: u.googleOauth.GetAuthURL(state), 28 + URL: a.googleOauth.GetAuthURL(state), 29 29 State: state, 30 30 }, nil 31 31 case githubProvider: 32 32 return dtos.OAuthRedirect{ 33 - URL: u.githubOauth.GetAuthURL(state), 33 + URL: a.githubOauth.GetAuthURL(state), 34 34 State: state, 35 35 }, nil 36 36 default: ··· 38 38 } 39 39 } 40 40 41 -func (u *UserSrv) HandleOAuthLogin( 41 +func (a *AuthSrv) HandleOAuthLogin( 42 42 ctx context.Context, 43 43 providerName, code string, 44 44 ) (dtos.Tokens, error) { 45 - userInfo, err := u.getUserInfoBasedOnProvider(ctx, providerName, code) 45 + userInfo, err := a.getUserInfoBasedOnProvider(ctx, providerName, code) 46 46 if err != nil { 47 47 return dtos.Tokens{}, err 48 48 } 49 49 50 - userID, err := u.getUserByOAuthIDOrCreateOne(ctx, userInfo) 50 + userID, err := a.getUserByOAuthIDOrCreateOne(ctx, userInfo) 51 51 if err != nil { 52 52 return dtos.Tokens{}, err 53 53 } 54 54 55 - if err = u.userstore.LinkOAuthIdentity(ctx, userID, userInfo.Provider, userInfo.ProviderID); err != nil { 55 + if err = a.userstore.LinkOAuthIdentity(ctx, userID, userInfo.Provider, userInfo.ProviderID); err != nil { 56 56 slog.ErrorContext(ctx, "failed to link user identity", "user_id", userID, "err", err) 57 57 return dtos.Tokens{}, err 58 58 } 59 59 60 - tokens, err := u.issueTokens(ctx, userID) 61 - 62 - return tokens, err 60 + return a.issueTokens(ctx, userID) 63 61 } 64 62 65 -func (u *UserSrv) getUserInfoBasedOnProvider( 63 +func (a *AuthSrv) getUserInfoBasedOnProvider( 66 64 ctx context.Context, 67 65 providerName, code string, 68 66 ) (oauth.UserInfo, error) { ··· 71 69 72 70 switch providerName { 73 71 case googleProvider: 74 - userInfo, err = u.googleOauth.ExchangeCode(ctx, code) 72 + userInfo, err = a.googleOauth.ExchangeCode(ctx, code) 75 73 case githubProvider: 76 - userInfo, err = u.githubOauth.ExchangeCode(ctx, code) 74 + userInfo, err = a.githubOauth.ExchangeCode(ctx, code) 77 75 default: 78 76 return oauth.UserInfo{}, ErrProviderNotSupported 79 77 } ··· 81 79 return userInfo, err 82 80 } 83 81 84 -func (u *UserSrv) getUserByOAuthIDOrCreateOne( 82 +func (a *AuthSrv) getUserByOAuthIDOrCreateOne( 85 83 ctx context.Context, 86 84 info oauth.UserInfo, 87 85 ) (uuid.UUID, error) { 88 - user, err := u.userstore.GetByOAuthID(ctx, info.Provider, info.ProviderID) 86 + user, err := a.userstore.GetByOAuthID(ctx, info.Provider, info.ProviderID) 89 87 if err != nil { 90 88 if errors.Is(err, models.ErrUserNotFound) { 91 - uid, cerr := u.userstore.Create(ctx, models.User{ 89 + uid, cerr := a.userstore.Create(ctx, models.User{ 92 90 ID: uuid.Nil, 93 91 Email: info.Email, 94 92 Activated: true,
M
internal/service/usersrv/usersrv.go
··· 2 2 3 3 import ( 4 4 "context" 5 - "errors" 6 - "log/slog" 7 5 "time" 8 6 9 7 "github.com/gofrs/uuid/v5" 10 8 "github.com/olexsmir/onasty/internal/dtos" 11 9 "github.com/olexsmir/onasty/internal/events/mailermq" 12 10 "github.com/olexsmir/onasty/internal/hasher" 13 - "github.com/olexsmir/onasty/internal/jwtutil" 14 11 "github.com/olexsmir/onasty/internal/models" 15 - "github.com/olexsmir/onasty/internal/oauth" 16 12 "github.com/olexsmir/onasty/internal/store/psql/changeemailrepo" 17 13 "github.com/olexsmir/onasty/internal/store/psql/noterepo" 18 14 "github.com/olexsmir/onasty/internal/store/psql/passwordtokrepo" 19 - "github.com/olexsmir/onasty/internal/store/psql/sessionrepo" 20 15 "github.com/olexsmir/onasty/internal/store/psql/userepo" 21 16 "github.com/olexsmir/onasty/internal/store/psql/vertokrepo" 22 - "github.com/olexsmir/onasty/internal/store/rdb/usercache" 23 17 ) 24 18 25 19 type UserServicer interface { 26 - // SignUp creates a new user and sends verification email. 27 - SignUp(ctx context.Context, inp dtos.SignUp) (uuid.UUID, error) 28 - 29 - // SignIn authenticates a user and returns access and refresh tokens. 30 - SignIn(ctx context.Context, inp dtos.SignIn) (dtos.Tokens, error) 31 - 32 - // RefreshTokens refreshes the access and refresh tokens using the provided refresh token. 33 - RefreshTokens(ctx context.Context, refreshToken string) (dtos.Tokens, error) 34 - 35 - // Logout logs out a user by deleting the session associated with the provided refresh token. 36 - Logout(ctx context.Context, userID uuid.UUID, refreshToken string) error 37 - 38 - // LogoutAll logs out a user by deleting all sessions associated with the user ID. 39 - LogoutAll(ctx context.Context, userID uuid.UUID) error 40 - 41 20 // GetUserInfo retrieves user information by user ID. 42 21 GetUserInfo(ctx context.Context, userID uuid.UUID) (dtos.UserInfo, error) 43 22 ··· 54 33 55 34 ChangeEmail(ctx context.Context, token string) error 56 35 57 - // GetOAuthURL retrieves the OAuth URL for the specified provider. 58 - GetOAuthURL(providerName string) (dtos.OAuthRedirect, error) 59 - 60 - // HandleOAuthLogin handles the OAuth login process by exchanging the code for tokens. 61 - HandleOAuthLogin(ctx context.Context, providerName, code string) (dtos.Tokens, error) 62 - 63 36 // Verify verifies the user's email using the provided verification key. 64 37 Verify(ctx context.Context, verificationKey string) error 65 38 66 39 // ResendVerificationEmail resends the verification email to the user. 67 40 ResendVerificationEmail(ctx context.Context, inp dtos.ResendVerificationEmail) error 68 - 69 - // ParseJWTToken parses the JWT token and returns the payload. 70 - ParseJWTToken(token string) (jwtutil.Payload, error) 71 - 72 - // CheckIfUserExists checks if a user exists by user ID. 73 - CheckIfUserExists(ctx context.Context, userID uuid.UUID) (bool, error) 74 - 75 - // CheckIfUserIsActivated checks if a user is activated by user ID. 76 - CheckIfUserIsActivated(ctx context.Context, userID uuid.UUID) (bool, error) 77 41 } 78 42 79 43 var _ UserServicer = (*UserSrv)(nil) 80 44 81 45 type UserSrv struct { 82 46 userstore userepo.UserStorer 83 - sessionstore sessionrepo.SessionStorer 84 47 vertokrepo vertokrepo.VerificationTokenStorer 85 48 pwdtokrepo passwordtokrepo.PasswordResetTokenStorer 86 49 changeemailrepo changeemailrepo.ChangeEmailStorer 87 50 notestore noterepo.NoteStorer 88 - cache usercache.UserCacheer 89 51 90 - hasher hasher.Hasher 91 - jwtTokenizer jwtutil.JWTTokenizer 92 - mailermq mailermq.Mailer 93 - 94 - googleOauth oauth.Provider 95 - githubOauth oauth.Provider 52 + hasher hasher.Hasher 53 + mailermq mailermq.Mailer 96 54 97 - refreshTokenTTL time.Duration 98 55 verificationTokenTTL time.Duration 99 56 resetPasswordTokenTTL time.Duration 100 57 changeEmailTokenTTL time.Duration ··· 102 59 103 60 func New( 104 61 userstore userepo.UserStorer, 105 - sessionstore sessionrepo.SessionStorer, 106 62 vertokrepo vertokrepo.VerificationTokenStorer, 107 63 pwdtokrepo passwordtokrepo.PasswordResetTokenStorer, 108 64 changeemailrepo changeemailrepo.ChangeEmailStorer, 109 65 notestore noterepo.NoteStorer, 110 66 hasher hasher.Hasher, 111 - jwtTokenizer jwtutil.JWTTokenizer, 112 67 mailermq mailermq.Mailer, 113 - cache usercache.UserCacheer, 114 - googleOauth, githubOauth oauth.Provider, 115 - refreshTokenTTL, verificationTokenTTL, resetPasswordTokenTTL, changeEmailTokenTTL time.Duration, 68 + verificationTokenTTL, resetPasswordTokenTTL, changeEmailTokenTTL time.Duration, 116 69 ) *UserSrv { 117 70 return &UserSrv{ 118 71 userstore: userstore, 119 - sessionstore: sessionstore, 120 72 vertokrepo: vertokrepo, 121 73 pwdtokrepo: pwdtokrepo, 122 74 changeemailrepo: changeemailrepo, 123 75 notestore: notestore, 124 - cache: cache, 125 76 hasher: hasher, 126 - jwtTokenizer: jwtTokenizer, 127 77 mailermq: mailermq, 128 - googleOauth: googleOauth, 129 - githubOauth: githubOauth, 130 - refreshTokenTTL: refreshTokenTTL, 131 78 verificationTokenTTL: verificationTokenTTL, 132 79 resetPasswordTokenTTL: resetPasswordTokenTTL, 133 80 changeEmailTokenTTL: changeEmailTokenTTL, 134 81 } 135 82 } 136 83 137 -func (u *UserSrv) SignUp(ctx context.Context, inp dtos.SignUp) (uuid.UUID, error) { 138 - user := models.User{ 139 - ID: uuid.Nil, // nil, because it does not get used here 140 - Email: inp.Email, 141 - Activated: false, 142 - Password: inp.Password, 143 - CreatedAt: inp.CreatedAt, 144 - LastLoginAt: inp.LastLoginAt, 145 - } 146 - if err := user.Validate(); err != nil { 147 - return uuid.Nil, err 148 - } 149 - 150 - hashedPassword, err := u.hasher.Hash(inp.Password) 151 - if err != nil { 152 - return uuid.UUID{}, err 153 - } 154 - 155 - user.Password = hashedPassword 156 - 157 - userID, err := u.userstore.Create(ctx, user) 158 - if err != nil { 159 - return uuid.Nil, err 160 - } 161 - 162 - verificationToken := uuid.Must(uuid.NewV4()).String() 163 - if err := u.vertokrepo.Create(ctx, models.VerificationToken{ 164 - UserID: userID, 165 - Token: verificationToken, 166 - CreatedAt: time.Now(), 167 - ExpiresAt: time.Now().Add(u.verificationTokenTTL), 168 - }); err != nil { 169 - return uuid.Nil, err 170 - } 171 - 172 - if err := u.mailermq.SendVerificationEmail(ctx, mailermq.SendVerificationEmailRequest{ 173 - Receiver: inp.Email, 174 - Token: verificationToken, 175 - }); err != nil { 176 - return uuid.Nil, err 177 - } 178 - 179 - return userID, nil 180 -} 181 - 182 -func (u *UserSrv) SignIn(ctx context.Context, inp dtos.SignIn) (dtos.Tokens, error) { 183 - user, err := u.userstore.GetByEmail(ctx, inp.Email) 184 - if err != nil { 185 - return dtos.Tokens{}, err 186 - } 187 - 188 - if err = u.hasher.Compare(user.Password, inp.Password); err != nil { 189 - if errors.Is(err, hasher.ErrMismatchedHashes) { 190 - return dtos.Tokens{}, models.ErrUserWrongCredentials 191 - } 192 - return dtos.Tokens{}, err 193 - } 194 - 195 - if !user.IsActivated() { 196 - return dtos.Tokens{}, models.ErrUserIsNotActivated 197 - } 198 - 199 - tokens, err := u.issueTokens(ctx, user.ID) 200 - return tokens, err 201 -} 202 - 203 -func (u *UserSrv) Logout(ctx context.Context, userID uuid.UUID, refreshToken string) error { 204 - return u.sessionstore.Delete(ctx, userID, refreshToken) 205 -} 206 - 207 -func (u *UserSrv) LogoutAll(ctx context.Context, userID uuid.UUID) error { 208 - return u.sessionstore.DeleteAllByUserID(ctx, userID) 209 -} 210 - 211 84 func (u *UserSrv) GetUserInfo(ctx context.Context, userID uuid.UUID) (dtos.UserInfo, error) { 212 85 user, err := u.userstore.GetByID(ctx, userID) 213 86 if err != nil { ··· 224 97 CreatedAt: user.CreatedAt, 225 98 LastLoginAt: user.LastLoginAt, 226 99 NotesCreated: int(count), 227 - }, nil 228 -} 229 - 230 -func (u *UserSrv) RefreshTokens(ctx context.Context, rtoken string) (dtos.Tokens, error) { 231 - userID, err := u.sessionstore.GetUserIDByRefreshToken(ctx, rtoken) 232 - if err != nil { 233 - return dtos.Tokens{}, err 234 - } 235 - 236 - tokens, err := u.createTokens(userID) 237 - if err != nil { 238 - return dtos.Tokens{}, err 239 - } 240 - 241 - if err := u.sessionstore.Update(ctx, userID, rtoken, tokens.Refresh); err != nil { 242 - return dtos.Tokens{}, err 243 - } 244 - 245 - return dtos.Tokens{ 246 - Access: tokens.Access, 247 - Refresh: tokens.Refresh, 248 100 }, nil 249 101 } 250 102 ··· 431 283 432 284 return nil 433 285 } 434 - 435 -func (u *UserSrv) ParseJWTToken(token string) (jwtutil.Payload, error) { 436 - return u.jwtTokenizer.Parse(token) 437 -} 438 - 439 -func (u UserSrv) CheckIfUserExists(ctx context.Context, id uuid.UUID) (bool, error) { 440 - r, err := u.cache.GetIsExists(ctx, id.String()) 441 - if err == nil { 442 - return r, nil 443 - } 444 - 445 - slog.ErrorContext(ctx, "usercache", "err", err) 446 - 447 - isExists, err := u.userstore.CheckIfUserExists(ctx, id) 448 - if err != nil { 449 - return false, err 450 - } 451 - 452 - if err := u.cache.SetIsExists(ctx, id.String(), isExists); err != nil { 453 - slog.ErrorContext(ctx, "usercache", "err", err) 454 - } 455 - 456 - return isExists, nil 457 -} 458 - 459 -func (u *UserSrv) CheckIfUserIsActivated(ctx context.Context, userID uuid.UUID) (bool, error) { 460 - r, err := u.cache.GetIsActivated(ctx, userID.String()) 461 - if err == nil { 462 - return r, nil 463 - } 464 - 465 - slog.ErrorContext(ctx, "usercache", "err", err) 466 - 467 - isActivated, err := u.userstore.CheckIfUserIsActivated(ctx, userID) 468 - if err != nil { 469 - return false, err 470 - } 471 - 472 - if err := u.cache.SetIsActivated(ctx, userID.String(), isActivated); err != nil { 473 - slog.ErrorContext(ctx, "usercache", "err", err) 474 - } 475 - 476 - return isActivated, nil 477 -} 478 - 479 -func (u UserSrv) createTokens(userID uuid.UUID) (dtos.Tokens, error) { 480 - accessToken, err := u.jwtTokenizer.AccessToken(jwtutil.Payload{UserID: userID.String()}) 481 - if err != nil { 482 - return dtos.Tokens{}, err 483 - } 484 - 485 - refreshToken, err := u.jwtTokenizer.RefreshToken() 486 - if err != nil { 487 - return dtos.Tokens{}, err 488 - } 489 - 490 - return dtos.Tokens{ 491 - Access: accessToken, 492 - Refresh: refreshToken, 493 - }, err 494 -} 495 - 496 -func (u UserSrv) issueTokens(ctx context.Context, userID uuid.UUID) (dtos.Tokens, error) { 497 - toks, err := u.createTokens(userID) 498 - if err != nil { 499 - return dtos.Tokens{}, err 500 - } 501 - 502 - if err := u.sessionstore.Set(ctx, userID, toks.Refresh, time.Now().Add(u.refreshTokenTTL)); err != nil { 503 - return dtos.Tokens{}, err 504 - } 505 - 506 - return toks, nil 507 -}
M
internal/transport/http/apiv1/apiv1.go
··· 3 3 import ( 4 4 "github.com/gin-gonic/gin" 5 5 "github.com/olexsmir/onasty/internal/config" 6 + "github.com/olexsmir/onasty/internal/service/authsrv" 6 7 "github.com/olexsmir/onasty/internal/service/notesrv" 7 8 "github.com/olexsmir/onasty/internal/service/usersrv" 8 9 "github.com/olexsmir/onasty/internal/transport/http/ratelimit" 9 10 ) 10 11 11 12 type APIV1 struct { 13 + authsrv authsrv.AuthServicer 12 14 usersrv usersrv.UserServicer 13 15 notesrv notesrv.NoteServicer 14 16 slowRatelimitCfg ratelimit.Config ··· 17 19 } 18 20 19 21 func NewAPIV1( 22 + as authsrv.AuthServicer, 20 23 us usersrv.UserServicer, 21 24 ns notesrv.NoteServicer, 22 25 slowRatelimitCfg ratelimit.Config, ··· 24 27 domain string, 25 28 ) *APIV1 { 26 29 return &APIV1{ 30 + authsrv: as, 27 31 usersrv: us, 28 32 notesrv: ns, 29 33 slowRatelimitCfg: slowRatelimitCfg,
M
internal/transport/http/apiv1/auth.go
··· 20 20 return 21 21 } 22 22 23 - if _, err := a.usersrv.SignUp(c.Request.Context(), dtos.SignUp{ 23 + if err := a.authsrv.SignUp(c.Request.Context(), dtos.SignUp{ 24 24 Email: req.Email, 25 25 Password: req.Password, 26 26 CreatedAt: time.Now(), ··· 50 50 return 51 51 } 52 52 53 - toks, err := a.usersrv.SignIn(c.Request.Context(), dtos.SignIn{ 53 + toks, err := a.authsrv.SignIn(c.Request.Context(), dtos.SignIn{ 54 54 Email: req.Email, 55 55 Password: req.Password, 56 56 }) ··· 76 76 return 77 77 } 78 78 79 - toks, err := a.usersrv.RefreshTokens(c.Request.Context(), req.RefreshToken) 79 + toks, err := a.authsrv.RefreshTokens(c.Request.Context(), req.RefreshToken) 80 80 if err != nil { 81 81 errorResponse(c, err) 82 82 return ··· 88 88 }) 89 89 } 90 90 91 -func (a *APIV1) verifyHandler(c *gin.Context) { 92 - if err := a.usersrv.Verify(c.Request.Context(), c.Param("token")); err != nil { 93 - errorResponse(c, err) 94 - return 95 - } 96 - 97 - c.String(http.StatusOK, "email verified") 98 -} 99 - 100 -type resendVerificationEmailRequest struct { 101 - Email string `json:"email"` 102 -} 103 - 104 -func (a *APIV1) resendVerificationEmailHandler(c *gin.Context) { 105 - var req resendVerificationEmailRequest 106 - if err := c.ShouldBindJSON(&req); err != nil { 107 - newError(c, http.StatusBadRequest, "invalid request") 108 - return 109 - } 110 - 111 - if err := a.usersrv.ResendVerificationEmail( 112 - c.Request.Context(), 113 - dtos.ResendVerificationEmail{ 114 - Email: req.Email, 115 - }); err != nil { 116 - errorResponse(c, err) 117 - return 118 - } 119 - 120 - c.Status(http.StatusOK) 121 -} 122 - 123 -type requestResetPasswordRequest struct { 124 - Email string `json:"email"` 125 -} 126 - 127 -func (a *APIV1) requestResetPasswordHandler(c *gin.Context) { 128 - var req requestResetPasswordRequest 129 - if err := c.ShouldBindJSON(&req); err != nil { 130 - newError(c, http.StatusBadRequest, "invalid request") 131 - return 132 - } 133 - 134 - if err := a.usersrv.RequestPasswordReset(c.Request.Context(), dtos.RequestResetPassword{ 135 - Email: req.Email, 136 - }); err != nil { 137 - errorResponse(c, err) 138 - return 139 - } 140 - 141 - c.Status(http.StatusOK) 142 -} 143 - 144 -type resetPasswordRequest struct { 145 - Password string `json:"password"` 146 -} 147 - 148 -func (a *APIV1) resetPasswordHandler(c *gin.Context) { 149 - var req resetPasswordRequest 150 - if err := c.ShouldBindJSON(&req); err != nil { 151 - newError(c, http.StatusBadRequest, "invalid request") 152 - return 153 - } 154 - 155 - if err := a.usersrv.ResetPassword( 156 - c.Request.Context(), 157 - dtos.ResetPassword{ 158 - Token: c.Param("token"), 159 - NewPassword: req.Password, 160 - }, 161 - ); err != nil { 162 - errorResponse(c, err) 163 - return 164 - } 165 - 166 - c.Status(http.StatusOK) 167 -} 168 - 169 91 type logoutRequest struct { 170 92 RefreshToken string `json:"refresh_token"` 171 93 } ··· 177 99 return 178 100 } 179 101 180 - if err := a.usersrv.Logout(c.Request.Context(), a.getUserID(c), req.RefreshToken); err != nil { 102 + if err := a.authsrv.Logout( 103 + c.Request.Context(), 104 + a.getUserID(c), 105 + req.RefreshToken, 106 + ); err != nil { 181 107 errorResponse(c, err) 182 108 return 183 109 } ··· 186 112 } 187 113 188 114 func (a *APIV1) logOutAllHandler(c *gin.Context) { 189 - if err := a.usersrv.LogoutAll(c.Request.Context(), a.getUserID(c)); err != nil { 115 + if err := a.authsrv.LogoutAll(c.Request.Context(), a.getUserID(c)); err != nil { 190 116 errorResponse(c, err) 191 117 return 192 118 } ··· 194 120 c.Status(http.StatusNoContent) 195 121 } 196 122 197 -type changePasswordRequest struct { 198 - CurrentPassword string `json:"current_password"` 199 - NewPassword string `json:"new_password"` 200 -} 201 - 202 -func (a *APIV1) changePasswordHandler(c *gin.Context) { 203 - var req changePasswordRequest 204 - if err := c.ShouldBindJSON(&req); err != nil { 205 - newError(c, http.StatusBadRequest, "invalid request") 206 - return 207 - } 208 - 209 - if err := a.usersrv.ChangePassword( 210 - c.Request.Context(), 211 - a.getUserID(c), 212 - dtos.ChangeUserPassword{ 213 - CurrentPassword: req.CurrentPassword, 214 - NewPassword: req.NewPassword, 215 - }); err != nil { 216 - errorResponse(c, err) 217 - return 218 - } 219 - 220 - c.Status(http.StatusOK) 221 -} 222 - 223 -type changeEmailRequest struct { 224 - NewEmail string `json:"new_email"` 225 -} 226 - 227 -func (a *APIV1) requestEmailChangeHandler(c *gin.Context) { 228 - var req changeEmailRequest 229 - if err := c.ShouldBindJSON(&req); err != nil { 230 - newError(c, http.StatusBadRequest, "invalid request") 231 - return 232 - } 233 - 234 - if err := a.usersrv.RequestEmailChange( 235 - c.Request.Context(), 236 - a.getUserID(c), 237 - dtos.ChangeEmail{ 238 - NewEmail: req.NewEmail, 239 - }); err != nil { 240 - errorResponse(c, err) 241 - return 242 - } 243 - 244 - c.Status(http.StatusOK) 245 -} 246 - 247 -func (a *APIV1) changeEmailHandler(c *gin.Context) { 248 - if err := a.usersrv.ChangeEmail(c.Request.Context(), c.Param("token")); err != nil { 249 - errorResponse(c, err) 250 - return 251 - } 252 - 253 - c.String(http.StatusOK, "email changed") 254 -} 255 - 256 123 const oatuhStateCookie = "oauth_state" 257 124 258 125 func (a *APIV1) oauthLoginHandler(c *gin.Context) { 259 - redirectInfo, err := a.usersrv.GetOAuthURL(c.Param("provider")) 126 + redirectInfo, err := a.authsrv.GetOAuthURL(c.Param("provider")) 260 127 if err != nil { 261 128 errorResponse(c, err) 262 129 return ··· 283 150 return 284 151 } 285 152 286 - tokens, err := a.usersrv.HandleOAuthLogin( 153 + tokens, err := a.authsrv.HandleOAuthLogin( 287 154 c.Request.Context(), 288 155 c.Param("provider"), 289 156 c.Query("code"), ··· 298 165 RefreshToken: tokens.Refresh, 299 166 }) 300 167 } 301 - 302 -type getMeResponse struct { 303 - Email string `json:"email"` 304 - CreatedAt time.Time `json:"created_at"` 305 - LastLoginAt time.Time `json:"last_login_at"` 306 - NotesCreated int `json:"notes_created"` 307 -} 308 - 309 -func (a *APIV1) getMeHandler(c *gin.Context) { 310 - uinfo, err := a.usersrv.GetUserInfo(c.Request.Context(), a.getUserID(c)) 311 - if err != nil { 312 - errorResponse(c, err) 313 - return 314 - } 315 - 316 - c.JSON(http.StatusOK, getMeResponse{ 317 - Email: uinfo.Email, 318 - CreatedAt: uinfo.CreatedAt, 319 - LastLoginAt: uinfo.LastLoginAt, 320 - NotesCreated: uinfo.NotesCreated, 321 - }) 322 -}
M
internal/transport/http/apiv1/middleware.go
··· 90 90 91 91 // getUserId returns userId from the context 92 92 // getting user id is only possible if user is authorized 93 +// 93 94 // if userID is not set, [uuid.Nil] will be returned. 94 95 func (a *APIV1) getUserID(c *gin.Context) uuid.UUID { 95 96 userID, exists := c.Get(userIDCtxKey) ··· 106 107 } 107 108 108 109 func (a *APIV1) validateAuthorizedUser(ctx context.Context, accessToken string) (uuid.UUID, error) { 109 - tokenPayload, err := a.usersrv.ParseJWTToken(accessToken) 110 + tokenPayload, err := a.authsrv.ParseJWTToken(accessToken) 110 111 if err != nil { 111 112 return uuid.Nil, err 112 113 } 113 114 114 115 userID := uuid.Must(uuid.FromString(tokenPayload.UserID)) 115 116 116 - ok, err := a.usersrv.CheckIfUserExists(ctx, userID) 117 + ok, err := a.authsrv.CheckIfUserExists(ctx, userID) 117 118 if err != nil { 118 119 return uuid.Nil, err 119 120 } ··· 122 123 return uuid.Nil, ErrUnauthorized 123 124 } 124 125 125 - ok, err = a.usersrv.CheckIfUserIsActivated(ctx, userID) 126 + ok, err = a.authsrv.CheckIfUserIsActivated(ctx, userID) 126 127 if err != nil { 127 128 return uuid.Nil, err 128 129 }
M
internal/transport/http/apiv1/response.go
··· 8 8 "github.com/gin-gonic/gin" 9 9 "github.com/olexsmir/onasty/internal/jwtutil" 10 10 "github.com/olexsmir/onasty/internal/models" 11 + "github.com/olexsmir/onasty/internal/service/authsrv" 11 12 "github.com/olexsmir/onasty/internal/service/notesrv" 12 - "github.com/olexsmir/onasty/internal/service/usersrv" 13 13 ) 14 14 15 15 var ErrUnauthorized = errors.New("unauthorized") ··· 19 19 } 20 20 21 21 func errorResponse(c *gin.Context, err error) { 22 - if errors.Is(err, usersrv.ErrProviderNotSupported) || 22 + if errors.Is(err, authsrv.ErrProviderNotSupported) || 23 23 errors.Is(err, models.ErrResetPasswordTokenAlreadyUsed) || 24 24 errors.Is(err, models.ErrResetPasswordTokenExpired) || 25 25 errors.Is(err, models.ErrUserEmailIsAlreadyInUse) ||
A
internal/transport/http/apiv1/user.go
··· 1 +package apiv1 2 + 3 +import ( 4 + "net/http" 5 + "time" 6 + 7 + "github.com/gin-gonic/gin" 8 + "github.com/olexsmir/onasty/internal/dtos" 9 +) 10 + 11 +type getMeResponse struct { 12 + Email string `json:"email"` 13 + CreatedAt time.Time `json:"created_at"` 14 + LastLoginAt time.Time `json:"last_login_at"` 15 + NotesCreated int `json:"notes_created"` 16 +} 17 + 18 +func (a *APIV1) getMeHandler(c *gin.Context) { 19 + uinfo, err := a.usersrv.GetUserInfo(c.Request.Context(), a.getUserID(c)) 20 + if err != nil { 21 + errorResponse(c, err) 22 + return 23 + } 24 + 25 + c.JSON(http.StatusOK, getMeResponse{ 26 + Email: uinfo.Email, 27 + CreatedAt: uinfo.CreatedAt, 28 + LastLoginAt: uinfo.LastLoginAt, 29 + NotesCreated: uinfo.NotesCreated, 30 + }) 31 +} 32 + 33 +type changePasswordRequest struct { 34 + CurrentPassword string `json:"current_password"` 35 + NewPassword string `json:"new_password"` 36 +} 37 + 38 +func (a *APIV1) changePasswordHandler(c *gin.Context) { 39 + var req changePasswordRequest 40 + if err := c.ShouldBindJSON(&req); err != nil { 41 + newError(c, http.StatusBadRequest, "invalid request") 42 + return 43 + } 44 + 45 + if err := a.usersrv.ChangePassword( 46 + c.Request.Context(), 47 + a.getUserID(c), 48 + dtos.ChangeUserPassword{ 49 + CurrentPassword: req.CurrentPassword, 50 + NewPassword: req.NewPassword, 51 + }); err != nil { 52 + errorResponse(c, err) 53 + return 54 + } 55 + 56 + c.Status(http.StatusOK) 57 +} 58 + 59 +type requestResetPasswordRequest struct { 60 + Email string `json:"email"` 61 +} 62 + 63 +func (a *APIV1) requestResetPasswordHandler(c *gin.Context) { 64 + var req requestResetPasswordRequest 65 + if err := c.ShouldBindJSON(&req); err != nil { 66 + newError(c, http.StatusBadRequest, "invalid request") 67 + return 68 + } 69 + 70 + if err := a.usersrv.RequestPasswordReset( 71 + c.Request.Context(), 72 + dtos.RequestResetPassword{ 73 + Email: req.Email, 74 + }, 75 + ); err != nil { 76 + errorResponse(c, err) 77 + return 78 + } 79 + 80 + c.Status(http.StatusOK) 81 +} 82 + 83 +type resetPasswordRequest struct { 84 + Password string `json:"password"` 85 +} 86 + 87 +func (a *APIV1) resetPasswordHandler(c *gin.Context) { 88 + var req resetPasswordRequest 89 + if err := c.ShouldBindJSON(&req); err != nil { 90 + newError(c, http.StatusBadRequest, "invalid request") 91 + return 92 + } 93 + 94 + if err := a.usersrv.ResetPassword( 95 + c.Request.Context(), 96 + dtos.ResetPassword{ 97 + Token: c.Param("token"), 98 + NewPassword: req.Password, 99 + }, 100 + ); err != nil { 101 + errorResponse(c, err) 102 + return 103 + } 104 + 105 + c.Status(http.StatusOK) 106 +} 107 + 108 +type changeEmailRequest struct { 109 + NewEmail string `json:"new_email"` 110 +} 111 + 112 +func (a *APIV1) requestEmailChangeHandler(c *gin.Context) { 113 + var req changeEmailRequest 114 + if err := c.ShouldBindJSON(&req); err != nil { 115 + newError(c, http.StatusBadRequest, "invalid request") 116 + return 117 + } 118 + 119 + if err := a.usersrv.RequestEmailChange( 120 + c.Request.Context(), 121 + a.getUserID(c), 122 + dtos.ChangeEmail{ 123 + NewEmail: req.NewEmail, 124 + }); err != nil { 125 + errorResponse(c, err) 126 + return 127 + } 128 + 129 + c.Status(http.StatusOK) 130 +} 131 + 132 +func (a *APIV1) changeEmailHandler(c *gin.Context) { 133 + if err := a.usersrv.ChangeEmail( 134 + c.Request.Context(), 135 + c.Param("token"), 136 + ); err != nil { 137 + errorResponse(c, err) 138 + return 139 + } 140 + 141 + c.String(http.StatusOK, "email changed") 142 +} 143 + 144 +func (a *APIV1) verifyHandler(c *gin.Context) { 145 + if err := a.usersrv.Verify( 146 + c.Request.Context(), 147 + c.Param("token"), 148 + ); err != nil { 149 + errorResponse(c, err) 150 + return 151 + } 152 + 153 + c.String(http.StatusOK, "email verified") 154 +} 155 + 156 +type resendVerificationEmailRequest struct { 157 + Email string `json:"email"` 158 +} 159 + 160 +func (a *APIV1) resendVerificationEmailHandler(c *gin.Context) { 161 + var req resendVerificationEmailRequest 162 + if err := c.ShouldBindJSON(&req); err != nil { 163 + newError(c, http.StatusBadRequest, "invalid request") 164 + return 165 + } 166 + 167 + if err := a.usersrv.ResendVerificationEmail( 168 + c.Request.Context(), 169 + dtos.ResendVerificationEmail{ 170 + Email: req.Email, 171 + }); err != nil { 172 + errorResponse(c, err) 173 + return 174 + } 175 + 176 + c.Status(http.StatusOK) 177 +}
M
internal/transport/http/http.go
··· 6 6 7 7 "github.com/gin-gonic/gin" 8 8 "github.com/olexsmir/onasty/internal/config" 9 + "github.com/olexsmir/onasty/internal/service/authsrv" 9 10 "github.com/olexsmir/onasty/internal/service/notesrv" 10 11 "github.com/olexsmir/onasty/internal/service/usersrv" 11 12 "github.com/olexsmir/onasty/internal/transport/http/apiv1" ··· 14 15 ) 15 16 16 17 type Transport struct { 18 + authsrv authsrv.AuthServicer 17 19 usersrv usersrv.UserServicer 18 20 notesrv notesrv.NoteServicer 19 21 ··· 27 29 } 28 30 29 31 func NewTransport( 32 + as authsrv.AuthServicer, 30 33 us usersrv.UserServicer, 31 34 ns notesrv.NoteServicer, 32 35 env config.Environment, ··· 37 40 slowRatelimitCfg ratelimit.Config, 38 41 ) *Transport { 39 42 return &Transport{ 43 + authsrv: as, 40 44 usersrv: us, 41 45 notesrv: ns, 42 46 env: env, ··· 62 66 { 63 67 api.GET("/ping", t.pingHandler) 64 68 apiv1. 65 - NewAPIV1(t.usersrv, t.notesrv, t.slowRatelimitCfg, t.env, t.domain). 69 + NewAPIV1(t.authsrv, t.usersrv, t.notesrv, t.slowRatelimitCfg, t.env, t.domain). 66 70 Routes(api.Group("/v1")) 67 71 } 68 72