all repos

onasty @ b650519

a one-time notes service

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

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