all repos

onasty @ 3b5e67f

a one-time notes service
12 files changed, 546 insertions(+), 423 deletions(-)
refactor(api): split `usersrv` responsibilities (#195)

* refactor: extract all auth logic into auth service

* refactor(authsrv): make cache logic more straight-forward

* fix: grammar

* refactor(api): extract user handlers to separate file

* refactor(apiv1): update formatting of couldBeAuthorizedMiddleware

* Back out "refactor(apiv1): update formatting of couldBeAuthorizedMiddleware"

This backs out commit 87d7b3c661a3f6f50cf4dbc787d971b5735cdf07.

* fix(authsrv): log error if cache error'ed
Author: Olexandr Smirnov olexsmir@gmail.com
Committed by: GitHub noreply@github.com
Committed at: 2025-08-27 14:17:35 +0300
Parent: f469c12
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.gointernal/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