all repos

onasty @ 060f2e6

a one-time notes service

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

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