all repos

onasty @ c5b3657354ba81457857c6d8358d63859cbca406

a one-time notes service

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

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