all repos

onasty @ bf8dc57251dbc6827d93ef4bb0e5461f6084a099

a one-time notes service

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

Olexandr Smirnov Olexandr Smirnov
ss2316544@gmail.com
feat(api): change email (#191)..., 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/changeemailrepo"
17
	"github.com/olexsmir/onasty/internal/store/psql/noterepo"
18
	"github.com/olexsmir/onasty/internal/store/psql/passwordtokrepo"
19
	"github.com/olexsmir/onasty/internal/store/psql/sessionrepo"
20
	"github.com/olexsmir/onasty/internal/store/psql/userepo"
21
	"github.com/olexsmir/onasty/internal/store/psql/vertokrepo"
22
	"github.com/olexsmir/onasty/internal/store/rdb/usercache"
23
)
24
25
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
	// GetUserInfo retrieves user information by user ID.
42
	GetUserInfo(ctx context.Context, userID uuid.UUID) (dtos.UserInfo, error)
43
44
	// ChangePassword changes the user's password.
45
	ChangePassword(ctx context.Context, userID uuid.UUID, inp dtos.ChangeUserPassword) error
46
47
	// RequestPasswordReset initiates a password reset process by sending a reset email.
48
	RequestPasswordReset(ctx context.Context, inp dtos.RequestResetPassword) error
49
50
	// ResetPassword resets the user's password using the provided reset token.
51
	ResetPassword(ctx context.Context, inp dtos.ResetPassword) error
52
53
	RequestEmailChange(ctx context.Context, userID uuid.UUID, inp dtos.ChangeEmail) error
54
55
	ChangeEmail(ctx context.Context, token string) error
56
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
	// Verify verifies the user's email using the provided verification key.
64
	Verify(ctx context.Context, verificationKey string) error
65
66
	// ResendVerificationEmail resends the verification email to the user.
67
	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
}
78
79
var _ UserServicer = (*UserSrv)(nil)
80
81
type UserSrv struct {
82
	userstore       userepo.UserStorer
83
	sessionstore    sessionrepo.SessionStorer
84
	vertokrepo      vertokrepo.VerificationTokenStorer
85
	pwdtokrepo      passwordtokrepo.PasswordResetTokenStorer
86
	changeemailrepo changeemailrepo.ChangeEmailStorer
87
	notestore       noterepo.NoteStorer
88
	cache           usercache.UserCacheer
89
90
	hasher       hasher.Hasher
91
	jwtTokenizer jwtutil.JWTTokenizer
92
	mailermq     mailermq.Mailer
93
94
	googleOauth oauth.Provider
95
	githubOauth oauth.Provider
96
97
	refreshTokenTTL       time.Duration
98
	verificationTokenTTL  time.Duration
99
	resetPasswordTokenTTL time.Duration
100
	changeEmailTokenTTL   time.Duration
101
}
102
103
func New(
104
	userstore userepo.UserStorer,
105
	sessionstore sessionrepo.SessionStorer,
106
	vertokrepo vertokrepo.VerificationTokenStorer,
107
	pwdtokrepo passwordtokrepo.PasswordResetTokenStorer,
108
	changeemailrepo changeemailrepo.ChangeEmailStorer,
109
	notestore noterepo.NoteStorer,
110
	hasher hasher.Hasher,
111
	jwtTokenizer jwtutil.JWTTokenizer,
112
	mailermq mailermq.Mailer,
113
	cache usercache.UserCacheer,
114
	googleOauth, githubOauth oauth.Provider,
115
	refreshTokenTTL, verificationTokenTTL, resetPasswordTokenTTL, changeEmailTokenTTL time.Duration,
116
) *UserSrv {
117
	return &UserSrv{
118
		userstore:             userstore,
119
		sessionstore:          sessionstore,
120
		vertokrepo:            vertokrepo,
121
		pwdtokrepo:            pwdtokrepo,
122
		changeemailrepo:       changeemailrepo,
123
		notestore:             notestore,
124
		cache:                 cache,
125
		hasher:                hasher,
126
		jwtTokenizer:          jwtTokenizer,
127
		mailermq:              mailermq,
128
		googleOauth:           googleOauth,
129
		githubOauth:           githubOauth,
130
		refreshTokenTTL:       refreshTokenTTL,
131
		verificationTokenTTL:  verificationTokenTTL,
132
		resetPasswordTokenTTL: resetPasswordTokenTTL,
133
		changeEmailTokenTTL:   changeEmailTokenTTL,
134
	}
135
}
136
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
func (u *UserSrv) GetUserInfo(ctx context.Context, userID uuid.UUID) (dtos.UserInfo, error) {
212
	user, err := u.userstore.GetByID(ctx, userID)
213
	if err != nil {
214
		return dtos.UserInfo{}, err
215
	}
216
217
	count, err := u.notestore.GetCountOfNotesByAuthorID(ctx, userID)
218
	if err != nil {
219
		return dtos.UserInfo{}, err
220
	}
221
222
	return dtos.UserInfo{
223
		Email:        user.Email,
224
		CreatedAt:    user.CreatedAt,
225
		LastLoginAt:  user.LastLoginAt,
226
		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
	}, nil
249
}
250
251
func (u *UserSrv) ChangePassword(
252
	ctx context.Context,
253
	userID uuid.UUID,
254
	inp dtos.ChangeUserPassword,
255
) error {
256
	//nolint:exhaustruct
257
	if err := (models.User{Password: inp.NewPassword}).ValidatePassword(); err != nil {
258
		return err
259
	}
260
261
	user, err := u.userstore.GetByID(ctx, userID)
262
	if err != nil {
263
		return err
264
	}
265
266
	if err = u.hasher.Compare(user.Password, inp.CurrentPassword); err != nil {
267
		return errors.Join(err, models.ErrUserInvalidPassword)
268
	}
269
270
	newPass, err := u.hasher.Hash(inp.NewPassword)
271
	if err != nil {
272
		return err
273
	}
274
275
	if err := u.userstore.ChangePassword(ctx, userID, newPass); err != nil {
276
		return err
277
	}
278
279
	return nil
280
}
281
282
func (u *UserSrv) RequestPasswordReset(ctx context.Context, inp dtos.RequestResetPassword) error {
283
	user, err := u.userstore.GetByEmail(ctx, inp.Email)
284
	if err != nil {
285
		return err
286
	}
287
288
	token := uuid.Must(uuid.NewV4()).String()
289
	if err := u.pwdtokrepo.Create(ctx, models.ResetPasswordToken{
290
		UserID:    user.ID,
291
		Token:     token,
292
		CreatedAt: time.Now(),
293
		ExpiresAt: time.Now().Add(u.resetPasswordTokenTTL),
294
	}); err != nil {
295
		return err
296
	}
297
298
	if err := u.mailermq.SendPasswordResetEmail(ctx, mailermq.SendPasswordResetEmailRequest{
299
		Receiver: inp.Email,
300
		Token:    token,
301
	}); err != nil {
302
		return err
303
	}
304
305
	return nil
306
}
307
308
func (u *UserSrv) ResetPassword(ctx context.Context, inp dtos.ResetPassword) error {
309
	//nolint:exhaustruct
310
	if err := (models.User{Password: inp.NewPassword}).ValidatePassword(); err != nil {
311
		return err
312
	}
313
314
	uid, err := u.pwdtokrepo.GetUserIDByTokenAndMarkAsUsed(ctx, inp.Token, time.Now())
315
	if err != nil {
316
		return err
317
	}
318
319
	hashedPassword, err := u.hasher.Hash(inp.NewPassword)
320
	if err != nil {
321
		return err
322
	}
323
324
	return u.userstore.SetPassword(ctx, uid, hashedPassword)
325
}
326
327
func (u *UserSrv) RequestEmailChange(
328
	ctx context.Context,
329
	userID uuid.UUID,
330
	inp dtos.ChangeEmail,
331
) error {
332
	user, err := u.userstore.GetByID(ctx, userID)
333
	if err != nil {
334
		return err
335
	}
336
337
	if user.Email == inp.NewEmail {
338
		return models.ErrUserEmailIsAlreadyInUse
339
	}
340
341
	token := uuid.Must(uuid.NewV4()).String()
342
	changeEmailInput := models.ChangeEmailToken{
343
		UserID:    userID,
344
		Token:     token,
345
		NewEmail:  inp.NewEmail,
346
		CreatedAt: time.Now(),
347
		ExpiresAt: time.Now().Add(u.changeEmailTokenTTL),
348
	}
349
	if err := changeEmailInput.Validate(); err != nil {
350
		return err
351
	}
352
353
	if err := u.changeemailrepo.Create(ctx, changeEmailInput); err != nil {
354
		return err
355
	}
356
357
	if err := u.mailermq.SendChangeEmailConfirmation(ctx, mailermq.SendChangeEmailConfirmationRequest{
358
		Receiver: user.Email,
359
		Token:    token,
360
		NewEmail: inp.NewEmail,
361
	}); err != nil {
362
		return err
363
	}
364
365
	return nil
366
}
367
368
func (u *UserSrv) ChangeEmail(ctx context.Context, givenToken string) error {
369
	token, err := u.changeemailrepo.GetByToken(ctx, givenToken)
370
	if err != nil {
371
		return err
372
	}
373
374
	user, err := u.userstore.GetByID(ctx, token.UserID)
375
	if err != nil {
376
		return err
377
	}
378
379
	if user.Email == token.NewEmail {
380
		return models.ErrUserEmailIsAlreadyInUse
381
	}
382
383
	if err := u.userstore.SetEmail(ctx, token.UserID, token.NewEmail); err != nil {
384
		return err
385
	}
386
387
	if err := u.changeemailrepo.MarkAsUsed(ctx, token.Token, time.Now()); err != nil {
388
		return err
389
	}
390
391
	return nil
392
}
393
394
func (u *UserSrv) Verify(ctx context.Context, verificationKey string) error {
395
	uid, err := u.vertokrepo.GetUserIDByTokenAndMarkAsUsed(ctx, verificationKey, time.Now())
396
	if err != nil {
397
		return err
398
	}
399
400
	return u.userstore.MarkUserAsActivated(ctx, uid)
401
}
402
403
func (u *UserSrv) ResendVerificationEmail(
404
	ctx context.Context,
405
	inp dtos.ResendVerificationEmail,
406
) error {
407
	user, err := u.userstore.GetByEmail(ctx, inp.Email)
408
	if err != nil {
409
		return err
410
	}
411
412
	if user.Activated {
413
		return models.ErrUserIsAlreadyVerified
414
	}
415
416
	token, err := u.vertokrepo.GetTokenOrUpdateTokenByUserID(
417
		ctx,
418
		user.ID,
419
		uuid.Must(uuid.NewV4()).String(),
420
		time.Now().Add(u.verificationTokenTTL))
421
	if err != nil {
422
		return err
423
	}
424
425
	if err := u.mailermq.SendVerificationEmail(ctx, mailermq.SendVerificationEmailRequest{
426
		Receiver: inp.Email,
427
		Token:    token,
428
	}); err != nil {
429
		return err
430
	}
431
432
	return nil
433
}
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
}