all repos

onasty @ 445822dad429b08586716d6fea39fa538f056c51

a one-time notes service

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

Smirnov Oleksandr Smirnov Oleksandr
ss2316544@gmail.com
feat: mailer service (#55)..., 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/store/psql/sessionrepo"
16
	"github.com/olexsmir/onasty/internal/store/psql/userepo"
17
	"github.com/olexsmir/onasty/internal/store/psql/vertokrepo"
18
	"github.com/olexsmir/onasty/internal/store/rdb/usercache"
19
)
20
21
type UserServicer interface {
22
	SignUp(ctx context.Context, inp dtos.CreateUserDTO) (uuid.UUID, error)
23
	SignIn(ctx context.Context, inp dtos.SignInDTO) (dtos.TokensDTO, error)
24
	RefreshTokens(ctx context.Context, refreshToken string) (dtos.TokensDTO, error)
25
	Logout(ctx context.Context, userID uuid.UUID) error
26
27
	ChangePassword(ctx context.Context, userID uuid.UUID, inp dtos.ResetUserPasswordDTO) error
28
29
	Verify(ctx context.Context, verificationKey string) error
30
	ResendVerificationEmail(ctx context.Context, credentials dtos.SignInDTO) error
31
32
	ParseJWTToken(token string) (jwtutil.Payload, error)
33
34
	CheckIfUserExists(ctx context.Context, userID uuid.UUID) (bool, error)
35
	CheckIfUserIsActivated(ctx context.Context, userID uuid.UUID) (bool, error)
36
}
37
38
var _ UserServicer = (*UserSrv)(nil)
39
40
type UserSrv struct {
41
	userstore    userepo.UserStorer
42
	sessionstore sessionrepo.SessionStorer
43
	vertokrepo   vertokrepo.VerificationTokenStorer
44
	hasher       hasher.Hasher
45
	jwtTokenizer jwtutil.JWTTokenizer
46
	mailermq     mailermq.Mailer
47
	cache        usercache.UserCacheer
48
49
	refreshTokenTTL      time.Duration
50
	verificationTokenTTL time.Duration
51
	appURL               string
52
}
53
54
func New(
55
	userstore userepo.UserStorer,
56
	sessionstore sessionrepo.SessionStorer,
57
	vertokrepo vertokrepo.VerificationTokenStorer,
58
	hasher hasher.Hasher,
59
	jwtTokenizer jwtutil.JWTTokenizer,
60
	mailermq mailermq.Mailer,
61
	cache usercache.UserCacheer,
62
	refreshTokenTTL, verificationTokenTTL time.Duration,
63
	appURL string,
64
) *UserSrv {
65
	return &UserSrv{
66
		userstore:            userstore,
67
		sessionstore:         sessionstore,
68
		vertokrepo:           vertokrepo,
69
		hasher:               hasher,
70
		jwtTokenizer:         jwtTokenizer,
71
		mailermq:             mailermq,
72
		cache:                cache,
73
		refreshTokenTTL:      refreshTokenTTL,
74
		verificationTokenTTL: verificationTokenTTL,
75
		appURL:               appURL,
76
	}
77
}
78
79
func (u *UserSrv) SignUp(ctx context.Context, inp dtos.CreateUserDTO) (uuid.UUID, error) {
80
	hashedPassword, err := u.hasher.Hash(inp.Password)
81
	if err != nil {
82
		return uuid.UUID{}, err
83
	}
84
85
	uid, err := u.userstore.Create(ctx, dtos.CreateUserDTO{
86
		Username:    inp.Username,
87
		Email:       inp.Email,
88
		Password:    hashedPassword,
89
		CreatedAt:   inp.CreatedAt,
90
		LastLoginAt: inp.LastLoginAt,
91
	})
92
	if err != nil {
93
		return uuid.Nil, err
94
	}
95
96
	vtok := uuid.Must(uuid.NewV4()).String()
97
	if err := u.vertokrepo.Create(ctx, vtok, uid, time.Now(), time.Now().Add(u.verificationTokenTTL)); err != nil {
98
		return uuid.Nil, err
99
	}
100
101
	if err := u.mailermq.SendVerificationEmail(ctx, mailermq.SendVerificationEmailRequest{
102
		Receiver: inp.Email,
103
		Token:    vtok,
104
	}); err != nil {
105
		return uuid.Nil, err
106
	}
107
108
	return uid, nil
109
}
110
111
func (u *UserSrv) SignIn(ctx context.Context, inp dtos.SignInDTO) (dtos.TokensDTO, error) {
112
	hashedPassword, err := u.hasher.Hash(inp.Password)
113
	if err != nil {
114
		return dtos.TokensDTO{}, err
115
	}
116
117
	user, err := u.userstore.GetUserByCredentials(ctx, inp.Email, hashedPassword)
118
	if err != nil {
119
		if errors.Is(err, models.ErrUserNotFound) {
120
			return dtos.TokensDTO{}, models.ErrUserWrongCredentials
121
		}
122
		return dtos.TokensDTO{}, err
123
	}
124
125
	if !user.Activated {
126
		return dtos.TokensDTO{}, models.ErrUserIsNotActivated
127
	}
128
129
	tokens, err := u.getTokens(user.ID)
130
	if err != nil {
131
		return dtos.TokensDTO{}, err
132
	}
133
134
	if err := u.sessionstore.Set(ctx, user.ID, tokens.Refresh, time.Now().Add(u.refreshTokenTTL)); err != nil {
135
		return dtos.TokensDTO{}, err
136
	}
137
138
	return dtos.TokensDTO{
139
		Access:  tokens.Access,
140
		Refresh: tokens.Refresh,
141
	}, nil
142
}
143
144
func (u *UserSrv) Logout(ctx context.Context, userID uuid.UUID) error {
145
	return u.sessionstore.Delete(ctx, userID)
146
}
147
148
func (u *UserSrv) RefreshTokens(ctx context.Context, rtoken string) (dtos.TokensDTO, error) {
149
	userID, err := u.sessionstore.GetUserIDByRefreshToken(ctx, rtoken)
150
	if err != nil {
151
		return dtos.TokensDTO{}, err
152
	}
153
154
	tokens, err := u.getTokens(userID)
155
	if err != nil {
156
		return dtos.TokensDTO{}, err
157
	}
158
159
	if err := u.sessionstore.Update(ctx, userID, rtoken, tokens.Refresh); err != nil {
160
		return dtos.TokensDTO{}, err
161
	}
162
163
	return dtos.TokensDTO{
164
		Access:  tokens.Access,
165
		Refresh: tokens.Refresh,
166
	}, nil
167
}
168
169
func (u *UserSrv) ChangePassword(
170
	ctx context.Context,
171
	userID uuid.UUID,
172
	inp dtos.ResetUserPasswordDTO,
173
) error {
174
	oldPass, err := u.hasher.Hash(inp.CurrentPassword)
175
	if err != nil {
176
		return err
177
	}
178
179
	newPass, err := u.hasher.Hash(inp.NewPassword)
180
	if err != nil {
181
		return err
182
	}
183
184
	if err := u.userstore.ChangePassword(ctx, userID, oldPass, newPass); err != nil {
185
		return err
186
	}
187
188
	return nil
189
}
190
191
func (u *UserSrv) Verify(ctx context.Context, verificationKey string) error {
192
	uid, err := u.vertokrepo.GetUserIDByTokenAndMarkAsUsed(ctx, verificationKey, time.Now())
193
	if err != nil {
194
		return err
195
	}
196
197
	return u.userstore.MarkUserAsActivated(ctx, uid)
198
}
199
200
func (u *UserSrv) ResendVerificationEmail(ctx context.Context, inp dtos.SignInDTO) error {
201
	hashedPassword, err := u.hasher.Hash(inp.Password)
202
	if err != nil {
203
		return err
204
	}
205
206
	user, err := u.userstore.GetUserByCredentials(ctx, inp.Email, hashedPassword)
207
	if err != nil {
208
		if errors.Is(err, models.ErrUserNotFound) {
209
			return models.ErrUserWrongCredentials
210
		}
211
		return err
212
	}
213
214
	if user.Activated {
215
		return models.ErrUserIsAlreeadyVerified
216
	}
217
218
	token, err := u.vertokrepo.GetTokenOrUpdateTokenByUserID(
219
		ctx,
220
		user.ID,
221
		uuid.Must(uuid.NewV4()).String(),
222
		time.Now().Add(u.verificationTokenTTL))
223
	if err != nil {
224
		return err
225
	}
226
227
	if err := u.mailermq.SendVerificationEmail(ctx, mailermq.SendVerificationEmailRequest{
228
		Receiver: inp.Email,
229
		Token:    token,
230
	}); err != nil {
231
		return err
232
	}
233
234
	return nil
235
}
236
237
func (u *UserSrv) ParseJWTToken(token string) (jwtutil.Payload, error) {
238
	return u.jwtTokenizer.Parse(token)
239
}
240
241
func (u UserSrv) CheckIfUserExists(ctx context.Context, id uuid.UUID) (bool, error) {
242
	if r, err := u.cache.GetIsExists(ctx, id.String()); err == nil {
243
		return r, nil
244
	} else { //nolint:revive
245
		slog.ErrorContext(ctx, "usercache", "err", err)
246
	}
247
248
	isExists, err := u.userstore.CheckIfUserExists(ctx, id)
249
	if err != nil {
250
		return false, err
251
	}
252
253
	if err := u.cache.SetIsExists(ctx, id.String(), isExists); err != nil {
254
		slog.Error("usercache", "err", err)
255
	}
256
257
	return isExists, nil
258
}
259
260
func (u UserSrv) CheckIfUserIsActivated(ctx context.Context, userID uuid.UUID) (bool, error) {
261
	if r, err := u.cache.GetIsActivated(ctx, userID.String()); err == nil {
262
		return r, nil
263
	} else { //nolint:revive
264
		slog.ErrorContext(ctx, "usercache", "err", err)
265
	}
266
267
	isActivated, err := u.userstore.CheckIfUserExists(ctx, userID)
268
	if err != nil {
269
		return false, err
270
	}
271
272
	if err := u.cache.SetIsActivated(ctx, userID.String(), isActivated); err != nil {
273
		slog.Error("usercache", "err", err)
274
	}
275
276
	return isActivated, nil
277
}
278
279
func (u UserSrv) getTokens(userID uuid.UUID) (dtos.TokensDTO, error) {
280
	accessToken, err := u.jwtTokenizer.AccessToken(jwtutil.Payload{UserID: userID.String()})
281
	if err != nil {
282
		return dtos.TokensDTO{}, err
283
	}
284
285
	refreshToken, err := u.jwtTokenizer.RefreshToken()
286
	if err != nil {
287
		return dtos.TokensDTO{}, err
288
	}
289
290
	return dtos.TokensDTO{
291
		Access:  accessToken,
292
		Refresh: refreshToken,
293
	}, err
294
}