all repos

onasty @ 8e14c68

a one-time notes service

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

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