all repos

onasty @ 51d3b53b0c8469cfd0ca12bb2a505d7fe77d228e

a one-time notes service

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

Smirnov Oleksandr Smirnov Oleksandr
ss2316544@gmail.com
fix: oauth state (#128)..., 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) 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) (dtos.OAuthRedirect, 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
	user := models.User{
97
		ID:          uuid.Nil, // nil, because it does not get used here
98
		Email:       inp.Email,
99
		Activated:   false,
100
		Password:    inp.Password,
101
		CreatedAt:   inp.CreatedAt,
102
		LastLoginAt: inp.LastLoginAt,
103
	}
104
	if err := user.Validate(); err != nil {
105
		return uuid.Nil, err
106
	}
107
108
	hashedPassword, err := u.hasher.Hash(inp.Password)
109
	if err != nil {
110
		return uuid.UUID{}, err
111
	}
112
113
	user.Password = hashedPassword
114
115
	userID, err := u.userstore.Create(ctx, user)
116
	if err != nil {
117
		return uuid.Nil, err
118
	}
119
120
	verificationToken := uuid.Must(uuid.NewV4()).String()
121
	if err := u.vertokrepo.Create(ctx, models.VerificationToken{
122
		UserID:    userID,
123
		Token:     verificationToken,
124
		CreatedAt: time.Now(),
125
		ExpiresAt: 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
	//nolint:exhaustruct
192
	if err := (models.User{Password: inp.NewPassword}).ValidatePassword(); err != nil {
193
		return err
194
	}
195
196
	user, err := u.userstore.GetByID(ctx, userID)
197
	if err != nil {
198
		return err
199
	}
200
201
	if err = u.hasher.Compare(user.Password, inp.CurrentPassword); err != nil {
202
		return errors.Join(err, models.ErrUserInvalidPassword)
203
	}
204
205
	newPass, err := u.hasher.Hash(inp.NewPassword)
206
	if err != nil {
207
		return err
208
	}
209
210
	if err := u.userstore.ChangePassword(ctx, userID, newPass); err != nil {
211
		return err
212
	}
213
214
	return nil
215
}
216
217
func (u *UserSrv) RequestPasswordReset(ctx context.Context, inp dtos.RequestResetPassword) error {
218
	user, err := u.userstore.GetByEmail(ctx, inp.Email)
219
	if err != nil {
220
		return err
221
	}
222
223
	token := uuid.Must(uuid.NewV4()).String()
224
	if err := u.pwdtokrepo.Create(ctx, models.ResetPasswordToken{
225
		UserID:    user.ID,
226
		Token:     token,
227
		CreatedAt: time.Now(),
228
		ExpiresAt: time.Now().Add(u.resetPasswordTokenTTL),
229
	}); err != nil {
230
		return err
231
	}
232
233
	if err := u.mailermq.SendPasswordResetEmail(ctx, mailermq.SendPasswordResetEmailRequest{
234
		Receiver: inp.Email,
235
		Token:    token,
236
	}); err != nil {
237
		return err
238
	}
239
240
	return nil
241
}
242
243
func (u *UserSrv) ResetPassword(ctx context.Context, inp dtos.ResetPassword) error {
244
	//nolint:exhaustruct
245
	if err := (models.User{Password: inp.NewPassword}).ValidatePassword(); err != nil {
246
		return err
247
	}
248
249
	uid, err := u.pwdtokrepo.GetUserIDByTokenAndMarkAsUsed(ctx, inp.Token, time.Now())
250
	if err != nil {
251
		return err
252
	}
253
254
	hashedPassword, err := u.hasher.Hash(inp.NewPassword)
255
	if err != nil {
256
		return err
257
	}
258
259
	return u.userstore.SetPassword(ctx, uid, hashedPassword)
260
}
261
262
func (u *UserSrv) Verify(ctx context.Context, verificationKey string) error {
263
	uid, err := u.vertokrepo.GetUserIDByTokenAndMarkAsUsed(ctx, verificationKey, time.Now())
264
	if err != nil {
265
		return err
266
	}
267
268
	return u.userstore.MarkUserAsActivated(ctx, uid)
269
}
270
271
func (u *UserSrv) ResendVerificationEmail(ctx context.Context, inp dtos.SignIn) error {
272
	user, err := u.userstore.GetByEmail(ctx, inp.Email)
273
	if err != nil {
274
		return err
275
	}
276
277
	if err = u.hasher.Compare(user.Password, inp.Password); err != nil {
278
		return models.ErrUserWrongCredentials
279
	}
280
281
	if user.Activated {
282
		return models.ErrUserIsAlreadyVerified
283
	}
284
285
	token, err := u.vertokrepo.GetTokenOrUpdateTokenByUserID(
286
		ctx,
287
		user.ID,
288
		uuid.Must(uuid.NewV4()).String(),
289
		time.Now().Add(u.verificationTokenTTL))
290
	if err != nil {
291
		return err
292
	}
293
294
	if err := u.mailermq.SendVerificationEmail(ctx, mailermq.SendVerificationEmailRequest{
295
		Receiver: inp.Email,
296
		Token:    token,
297
	}); err != nil {
298
		return err
299
	}
300
301
	return nil
302
}
303
304
func (u *UserSrv) ParseJWTToken(token string) (jwtutil.Payload, error) {
305
	return u.jwtTokenizer.Parse(token)
306
}
307
308
func (u UserSrv) CheckIfUserExists(ctx context.Context, id uuid.UUID) (bool, error) {
309
	r, err := u.cache.GetIsExists(ctx, id.String())
310
	if err == nil {
311
		return r, nil
312
	}
313
314
	slog.ErrorContext(ctx, "usercache", "err", err)
315
316
	isExists, err := u.userstore.CheckIfUserExists(ctx, id)
317
	if err != nil {
318
		return false, err
319
	}
320
321
	if err := u.cache.SetIsExists(ctx, id.String(), isExists); err != nil {
322
		slog.ErrorContext(ctx, "usercache", "err", err)
323
	}
324
325
	return isExists, nil
326
}
327
328
func (u *UserSrv) CheckIfUserIsActivated(ctx context.Context, userID uuid.UUID) (bool, error) {
329
	r, err := u.cache.GetIsActivated(ctx, userID.String())
330
	if err == nil {
331
		return r, nil
332
	}
333
334
	slog.ErrorContext(ctx, "usercache", "err", err)
335
336
	isActivated, err := u.userstore.CheckIfUserIsActivated(ctx, userID)
337
	if err != nil {
338
		return false, err
339
	}
340
341
	if err := u.cache.SetIsActivated(ctx, userID.String(), isActivated); err != nil {
342
		slog.ErrorContext(ctx, "usercache", "err", err)
343
	}
344
345
	return isActivated, nil
346
}
347
348
func (u UserSrv) createTokens(userID uuid.UUID) (dtos.Tokens, error) {
349
	accessToken, err := u.jwtTokenizer.AccessToken(jwtutil.Payload{UserID: userID.String()})
350
	if err != nil {
351
		return dtos.Tokens{}, err
352
	}
353
354
	refreshToken, err := u.jwtTokenizer.RefreshToken()
355
	if err != nil {
356
		return dtos.Tokens{}, err
357
	}
358
359
	return dtos.Tokens{
360
		Access:  accessToken,
361
		Refresh: refreshToken,
362
	}, err
363
}
364
365
func (u UserSrv) issueTokens(ctx context.Context, userID uuid.UUID) (dtos.Tokens, error) {
366
	toks, err := u.createTokens(userID)
367
	if err != nil {
368
		return dtos.Tokens{}, err
369
	}
370
371
	if err := u.sessionstore.Set(ctx, userID, toks.Refresh, time.Now().Add(u.refreshTokenTTL)); err != nil {
372
		return dtos.Tokens{}, err
373
	}
374
375
	return toks, nil
376
}