all repos

onasty @ e771742

a one-time notes service

onasty/internal/service/usersrv/usersrv.go (view raw)

Olexandr Smirnov Olexandr Smirnov
ss2316544@gmail.com
docs: add missing code comments (#185)..., 9 months ago
1
package usersrv
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/noterepo"
17
	"github.com/olexsmir/onasty/internal/store/psql/passwordtokrepo"
18
	"github.com/olexsmir/onasty/internal/store/psql/sessionrepo"
19
	"github.com/olexsmir/onasty/internal/store/psql/userepo"
20
	"github.com/olexsmir/onasty/internal/store/psql/vertokrepo"
21
	"github.com/olexsmir/onasty/internal/store/rdb/usercache"
22
)
23
24
type UserServicer interface {
25
	// SignUp creates a new user and sends verification email.
26
	SignUp(ctx context.Context, inp dtos.SignUp) (uuid.UUID, error)
27
28
	// SignIn authenticates a user and returns access and refresh tokens.
29
	SignIn(ctx context.Context, inp dtos.SignIn) (dtos.Tokens, error)
30
31
	// RefreshTokens refreshes the access and refresh tokens using the provided refresh token.
32
	RefreshTokens(ctx context.Context, refreshToken string) (dtos.Tokens, error)
33
34
	// Logout logs out a user by deleting the session associated with the provided refresh token.
35
	Logout(ctx context.Context, userID uuid.UUID, refreshToken string) error
36
37
	// LogoutAll logs out a user by deleting all sessions associated with the user ID.
38
	LogoutAll(ctx context.Context, userID uuid.UUID) error
39
40
	// GetUserInfo retrieves user information by user ID.
41
	GetUserInfo(ctx context.Context, userID uuid.UUID) (dtos.UserInfo, error)
42
43
	// ChangePassword changes the user's password.
44
	ChangePassword(ctx context.Context, userID uuid.UUID, inp dtos.ChangeUserPassword) error
45
46
	// RequestPasswordReset initiates a password reset process by sending a reset email.
47
	RequestPasswordReset(ctx context.Context, inp dtos.RequestResetPassword) error
48
49
	// ResetPassword resets the user's password using the provided reset token.
50
	ResetPassword(ctx context.Context, inp dtos.ResetPassword) error
51
52
	// GetOAuthURL retrieves the OAuth URL for the specified provider.
53
	GetOAuthURL(providerName string) (dtos.OAuthRedirect, error)
54
55
	// HandleOAuthLogin handles the OAuth login process by exchanging the code for tokens.
56
	HandleOAuthLogin(ctx context.Context, providerName, code string) (dtos.Tokens, error)
57
58
	// Verify verifies the user's email using the provided verification key.
59
	Verify(ctx context.Context, verificationKey string) error
60
61
	// ResendVerificationEmail resends the verification email to the user.
62
	ResendVerificationEmail(ctx context.Context, inp dtos.ResendVerificationEmail) error
63
64
	// ParseJWTToken parses the JWT token and returns the payload.
65
	ParseJWTToken(token string) (jwtutil.Payload, error)
66
67
	// CheckIfUserExists checks if a user exists by user ID.
68
	CheckIfUserExists(ctx context.Context, userID uuid.UUID) (bool, error)
69
70
	// CheckIfUserIsActivated checks if a user is activated by user ID.
71
	CheckIfUserIsActivated(ctx context.Context, userID uuid.UUID) (bool, error)
72
}
73
74
var _ UserServicer = (*UserSrv)(nil)
75
76
type UserSrv struct {
77
	userstore    userepo.UserStorer
78
	sessionstore sessionrepo.SessionStorer
79
	vertokrepo   vertokrepo.VerificationTokenStorer
80
	pwdtokrepo   passwordtokrepo.PasswordResetTokenStorer
81
	notestore    noterepo.NoteStorer
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
	resetPasswordTokenTTL time.Duration
94
}
95
96
func New(
97
	userstore userepo.UserStorer,
98
	sessionstore sessionrepo.SessionStorer,
99
	vertokrepo vertokrepo.VerificationTokenStorer,
100
	pwdtokrepo passwordtokrepo.PasswordResetTokenStorer,
101
	notestore noterepo.NoteStorer,
102
	hasher hasher.Hasher,
103
	jwtTokenizer jwtutil.JWTTokenizer,
104
	mailermq mailermq.Mailer,
105
	cache usercache.UserCacheer,
106
	googleOauth, githubOauth oauth.Provider,
107
	refreshTokenTTL, verificationTokenTTL, resetPasswordTokenTTL time.Duration,
108
) *UserSrv {
109
	return &UserSrv{
110
		userstore:             userstore,
111
		sessionstore:          sessionstore,
112
		vertokrepo:            vertokrepo,
113
		pwdtokrepo:            pwdtokrepo,
114
		notestore:             notestore,
115
		cache:                 cache,
116
		hasher:                hasher,
117
		jwtTokenizer:          jwtTokenizer,
118
		mailermq:              mailermq,
119
		googleOauth:           googleOauth,
120
		githubOauth:           githubOauth,
121
		refreshTokenTTL:       refreshTokenTTL,
122
		verificationTokenTTL:  verificationTokenTTL,
123
		resetPasswordTokenTTL: resetPasswordTokenTTL,
124
	}
125
}
126
127
func (u *UserSrv) SignUp(ctx context.Context, inp dtos.SignUp) (uuid.UUID, error) {
128
	user := models.User{
129
		ID:          uuid.Nil, // nil, because it does not get used here
130
		Email:       inp.Email,
131
		Activated:   false,
132
		Password:    inp.Password,
133
		CreatedAt:   inp.CreatedAt,
134
		LastLoginAt: inp.LastLoginAt,
135
	}
136
	if err := user.Validate(); err != nil {
137
		return uuid.Nil, err
138
	}
139
140
	hashedPassword, err := u.hasher.Hash(inp.Password)
141
	if err != nil {
142
		return uuid.UUID{}, err
143
	}
144
145
	user.Password = hashedPassword
146
147
	userID, err := u.userstore.Create(ctx, user)
148
	if err != nil {
149
		return uuid.Nil, err
150
	}
151
152
	verificationToken := uuid.Must(uuid.NewV4()).String()
153
	if err := u.vertokrepo.Create(ctx, models.VerificationToken{
154
		UserID:    userID,
155
		Token:     verificationToken,
156
		CreatedAt: time.Now(),
157
		ExpiresAt: time.Now().Add(u.verificationTokenTTL),
158
	}); err != nil {
159
		return uuid.Nil, err
160
	}
161
162
	if err := u.mailermq.SendVerificationEmail(ctx, mailermq.SendVerificationEmailRequest{
163
		Receiver: inp.Email,
164
		Token:    verificationToken,
165
	}); err != nil {
166
		return uuid.Nil, err
167
	}
168
169
	return userID, nil
170
}
171
172
func (u *UserSrv) SignIn(ctx context.Context, inp dtos.SignIn) (dtos.Tokens, error) {
173
	user, err := u.userstore.GetByEmail(ctx, inp.Email)
174
	if err != nil {
175
		return dtos.Tokens{}, err
176
	}
177
178
	if err = u.hasher.Compare(user.Password, inp.Password); err != nil {
179
		if errors.Is(err, hasher.ErrMismatchedHashes) {
180
			return dtos.Tokens{}, models.ErrUserWrongCredentials
181
		}
182
		return dtos.Tokens{}, err
183
	}
184
185
	if !user.IsActivated() {
186
		return dtos.Tokens{}, models.ErrUserIsNotActivated
187
	}
188
189
	tokens, err := u.issueTokens(ctx, user.ID)
190
	return tokens, err
191
}
192
193
func (u *UserSrv) Logout(ctx context.Context, userID uuid.UUID, refreshToken string) error {
194
	return u.sessionstore.Delete(ctx, userID, refreshToken)
195
}
196
197
func (u *UserSrv) LogoutAll(ctx context.Context, userID uuid.UUID) error {
198
	return u.sessionstore.DeleteAllByUserID(ctx, userID)
199
}
200
201
func (u *UserSrv) GetUserInfo(ctx context.Context, userID uuid.UUID) (dtos.UserInfo, error) {
202
	user, err := u.userstore.GetByID(ctx, userID)
203
	if err != nil {
204
		return dtos.UserInfo{}, err
205
	}
206
207
	count, err := u.notestore.GetCountOfNotesByAuthorID(ctx, userID)
208
	if err != nil {
209
		return dtos.UserInfo{}, err
210
	}
211
212
	return dtos.UserInfo{
213
		Email:        user.Email,
214
		CreatedAt:    user.CreatedAt,
215
		LastLoginAt:  user.LastLoginAt,
216
		NotesCreated: int(count),
217
	}, nil
218
}
219
220
func (u *UserSrv) RefreshTokens(ctx context.Context, rtoken string) (dtos.Tokens, error) {
221
	userID, err := u.sessionstore.GetUserIDByRefreshToken(ctx, rtoken)
222
	if err != nil {
223
		return dtos.Tokens{}, err
224
	}
225
226
	tokens, err := u.createTokens(userID)
227
	if err != nil {
228
		return dtos.Tokens{}, err
229
	}
230
231
	if err := u.sessionstore.Update(ctx, userID, rtoken, tokens.Refresh); err != nil {
232
		return dtos.Tokens{}, err
233
	}
234
235
	return dtos.Tokens{
236
		Access:  tokens.Access,
237
		Refresh: tokens.Refresh,
238
	}, nil
239
}
240
241
func (u *UserSrv) ChangePassword(
242
	ctx context.Context,
243
	userID uuid.UUID,
244
	inp dtos.ChangeUserPassword,
245
) error {
246
	//nolint:exhaustruct
247
	if err := (models.User{Password: inp.NewPassword}).ValidatePassword(); err != nil {
248
		return err
249
	}
250
251
	user, err := u.userstore.GetByID(ctx, userID)
252
	if err != nil {
253
		return err
254
	}
255
256
	if err = u.hasher.Compare(user.Password, inp.CurrentPassword); err != nil {
257
		return errors.Join(err, models.ErrUserInvalidPassword)
258
	}
259
260
	newPass, err := u.hasher.Hash(inp.NewPassword)
261
	if err != nil {
262
		return err
263
	}
264
265
	if err := u.userstore.ChangePassword(ctx, userID, newPass); err != nil {
266
		return err
267
	}
268
269
	return nil
270
}
271
272
func (u *UserSrv) RequestPasswordReset(ctx context.Context, inp dtos.RequestResetPassword) error {
273
	user, err := u.userstore.GetByEmail(ctx, inp.Email)
274
	if err != nil {
275
		return err
276
	}
277
278
	token := uuid.Must(uuid.NewV4()).String()
279
	if err := u.pwdtokrepo.Create(ctx, models.ResetPasswordToken{
280
		UserID:    user.ID,
281
		Token:     token,
282
		CreatedAt: time.Now(),
283
		ExpiresAt: time.Now().Add(u.resetPasswordTokenTTL),
284
	}); err != nil {
285
		return err
286
	}
287
288
	if err := u.mailermq.SendPasswordResetEmail(ctx, mailermq.SendPasswordResetEmailRequest{
289
		Receiver: inp.Email,
290
		Token:    token,
291
	}); err != nil {
292
		return err
293
	}
294
295
	return nil
296
}
297
298
func (u *UserSrv) ResetPassword(ctx context.Context, inp dtos.ResetPassword) error {
299
	//nolint:exhaustruct
300
	if err := (models.User{Password: inp.NewPassword}).ValidatePassword(); err != nil {
301
		return err
302
	}
303
304
	uid, err := u.pwdtokrepo.GetUserIDByTokenAndMarkAsUsed(ctx, inp.Token, time.Now())
305
	if err != nil {
306
		return err
307
	}
308
309
	hashedPassword, err := u.hasher.Hash(inp.NewPassword)
310
	if err != nil {
311
		return err
312
	}
313
314
	return u.userstore.SetPassword(ctx, uid, hashedPassword)
315
}
316
317
func (u *UserSrv) Verify(ctx context.Context, verificationKey string) error {
318
	uid, err := u.vertokrepo.GetUserIDByTokenAndMarkAsUsed(ctx, verificationKey, time.Now())
319
	if err != nil {
320
		return err
321
	}
322
323
	return u.userstore.MarkUserAsActivated(ctx, uid)
324
}
325
326
func (u *UserSrv) ResendVerificationEmail(
327
	ctx context.Context,
328
	inp dtos.ResendVerificationEmail,
329
) error {
330
	user, err := u.userstore.GetByEmail(ctx, inp.Email)
331
	if err != nil {
332
		return err
333
	}
334
335
	if user.Activated {
336
		return models.ErrUserIsAlreadyVerified
337
	}
338
339
	token, err := u.vertokrepo.GetTokenOrUpdateTokenByUserID(
340
		ctx,
341
		user.ID,
342
		uuid.Must(uuid.NewV4()).String(),
343
		time.Now().Add(u.verificationTokenTTL))
344
	if err != nil {
345
		return err
346
	}
347
348
	if err := u.mailermq.SendVerificationEmail(ctx, mailermq.SendVerificationEmailRequest{
349
		Receiver: inp.Email,
350
		Token:    token,
351
	}); err != nil {
352
		return err
353
	}
354
355
	return nil
356
}
357
358
func (u *UserSrv) ParseJWTToken(token string) (jwtutil.Payload, error) {
359
	return u.jwtTokenizer.Parse(token)
360
}
361
362
func (u UserSrv) CheckIfUserExists(ctx context.Context, id uuid.UUID) (bool, error) {
363
	r, err := u.cache.GetIsExists(ctx, id.String())
364
	if err == nil {
365
		return r, nil
366
	}
367
368
	slog.ErrorContext(ctx, "usercache", "err", err)
369
370
	isExists, err := u.userstore.CheckIfUserExists(ctx, id)
371
	if err != nil {
372
		return false, err
373
	}
374
375
	if err := u.cache.SetIsExists(ctx, id.String(), isExists); err != nil {
376
		slog.ErrorContext(ctx, "usercache", "err", err)
377
	}
378
379
	return isExists, nil
380
}
381
382
func (u *UserSrv) CheckIfUserIsActivated(ctx context.Context, userID uuid.UUID) (bool, error) {
383
	r, err := u.cache.GetIsActivated(ctx, userID.String())
384
	if err == nil {
385
		return r, nil
386
	}
387
388
	slog.ErrorContext(ctx, "usercache", "err", err)
389
390
	isActivated, err := u.userstore.CheckIfUserIsActivated(ctx, userID)
391
	if err != nil {
392
		return false, err
393
	}
394
395
	if err := u.cache.SetIsActivated(ctx, userID.String(), isActivated); err != nil {
396
		slog.ErrorContext(ctx, "usercache", "err", err)
397
	}
398
399
	return isActivated, nil
400
}
401
402
func (u UserSrv) createTokens(userID uuid.UUID) (dtos.Tokens, error) {
403
	accessToken, err := u.jwtTokenizer.AccessToken(jwtutil.Payload{UserID: userID.String()})
404
	if err != nil {
405
		return dtos.Tokens{}, err
406
	}
407
408
	refreshToken, err := u.jwtTokenizer.RefreshToken()
409
	if err != nil {
410
		return dtos.Tokens{}, err
411
	}
412
413
	return dtos.Tokens{
414
		Access:  accessToken,
415
		Refresh: refreshToken,
416
	}, err
417
}
418
419
func (u UserSrv) issueTokens(ctx context.Context, userID uuid.UUID) (dtos.Tokens, error) {
420
	toks, err := u.createTokens(userID)
421
	if err != nil {
422
		return dtos.Tokens{}, err
423
	}
424
425
	if err := u.sessionstore.Set(ctx, userID, toks.Refresh, time.Now().Add(u.refreshTokenTTL)); err != nil {
426
		return dtos.Tokens{}, err
427
	}
428
429
	return toks, nil
430
}