all repos

onasty @ 353101d

a one-time notes service

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
fix: don't return "wrong credentials" (#201), 9 months ago
1
package usersrv
2
3
import (
4
	"context"
5
	"time"
6
7
	"github.com/gofrs/uuid/v5"
8
	"github.com/olexsmir/onasty/internal/dtos"
9
	"github.com/olexsmir/onasty/internal/events/mailermq"
10
	"github.com/olexsmir/onasty/internal/hasher"
11
	"github.com/olexsmir/onasty/internal/models"
12
	"github.com/olexsmir/onasty/internal/store/psql/changeemailrepo"
13
	"github.com/olexsmir/onasty/internal/store/psql/noterepo"
14
	"github.com/olexsmir/onasty/internal/store/psql/passwordtokrepo"
15
	"github.com/olexsmir/onasty/internal/store/psql/userepo"
16
	"github.com/olexsmir/onasty/internal/store/psql/vertokrepo"
17
)
18
19
type UserServicer interface {
20
	// GetUserInfo retrieves user information by user ID.
21
	GetUserInfo(ctx context.Context, userID uuid.UUID) (dtos.UserInfo, error)
22
23
	// ChangePassword changes the user's password.
24
	ChangePassword(ctx context.Context, userID uuid.UUID, inp dtos.ChangeUserPassword) error
25
26
	// RequestPasswordReset initiates a password reset process by sending a reset email.
27
	RequestPasswordReset(ctx context.Context, inp dtos.RequestResetPassword) error
28
29
	// ResetPassword resets the user's password using the provided reset token.
30
	ResetPassword(ctx context.Context, inp dtos.ResetPassword) error
31
32
	RequestEmailChange(ctx context.Context, userID uuid.UUID, inp dtos.ChangeEmail) error
33
34
	ChangeEmail(ctx context.Context, token string) error
35
36
	// Verify verifies the user's email using the provided verification key.
37
	Verify(ctx context.Context, verificationKey string) error
38
39
	// ResendVerificationEmail resends the verification email to the user.
40
	ResendVerificationEmail(ctx context.Context, inp dtos.ResendVerificationEmail) error
41
}
42
43
var _ UserServicer = (*UserSrv)(nil)
44
45
type UserSrv struct {
46
	userstore       userepo.UserStorer
47
	vertokrepo      vertokrepo.VerificationTokenStorer
48
	pwdtokrepo      passwordtokrepo.PasswordResetTokenStorer
49
	changeemailrepo changeemailrepo.ChangeEmailStorer
50
	notestore       noterepo.NoteStorer
51
52
	hasher   hasher.Hasher
53
	mailermq mailermq.Mailer
54
55
	verificationTokenTTL  time.Duration
56
	resetPasswordTokenTTL time.Duration
57
	changeEmailTokenTTL   time.Duration
58
}
59
60
func New(
61
	userstore userepo.UserStorer,
62
	vertokrepo vertokrepo.VerificationTokenStorer,
63
	pwdtokrepo passwordtokrepo.PasswordResetTokenStorer,
64
	changeemailrepo changeemailrepo.ChangeEmailStorer,
65
	notestore noterepo.NoteStorer,
66
	hasher hasher.Hasher,
67
	mailermq mailermq.Mailer,
68
	verificationTokenTTL, resetPasswordTokenTTL, changeEmailTokenTTL time.Duration,
69
) *UserSrv {
70
	return &UserSrv{
71
		userstore:             userstore,
72
		vertokrepo:            vertokrepo,
73
		pwdtokrepo:            pwdtokrepo,
74
		changeemailrepo:       changeemailrepo,
75
		notestore:             notestore,
76
		hasher:                hasher,
77
		mailermq:              mailermq,
78
		verificationTokenTTL:  verificationTokenTTL,
79
		resetPasswordTokenTTL: resetPasswordTokenTTL,
80
		changeEmailTokenTTL:   changeEmailTokenTTL,
81
	}
82
}
83
84
func (u *UserSrv) GetUserInfo(ctx context.Context, userID uuid.UUID) (dtos.UserInfo, error) {
85
	user, err := u.userstore.GetByID(ctx, userID)
86
	if err != nil {
87
		return dtos.UserInfo{}, err
88
	}
89
90
	count, err := u.notestore.GetCountOfNotesByAuthorID(ctx, userID)
91
	if err != nil {
92
		return dtos.UserInfo{}, err
93
	}
94
95
	return dtos.UserInfo{
96
		Email:        user.Email,
97
		CreatedAt:    user.CreatedAt,
98
		LastLoginAt:  user.LastLoginAt,
99
		NotesCreated: int(count),
100
	}, nil
101
}
102
103
func (u *UserSrv) ChangePassword(
104
	ctx context.Context,
105
	userID uuid.UUID,
106
	inp dtos.ChangeUserPassword,
107
) error {
108
	//nolint:exhaustruct
109
	if err := (models.User{Password: inp.NewPassword}).ValidatePassword(); err != nil {
110
		return err
111
	}
112
113
	user, err := u.userstore.GetByID(ctx, userID)
114
	if err != nil {
115
		return err
116
	}
117
118
	if err = u.hasher.Compare(user.Password, inp.CurrentPassword); err != nil {
119
		return models.ErrUserNotFound
120
	}
121
122
	newPass, err := u.hasher.Hash(inp.NewPassword)
123
	if err != nil {
124
		return err
125
	}
126
127
	if err := u.userstore.ChangePassword(ctx, userID, newPass); err != nil {
128
		return err
129
	}
130
131
	return nil
132
}
133
134
func (u *UserSrv) RequestPasswordReset(ctx context.Context, inp dtos.RequestResetPassword) error {
135
	user, err := u.userstore.GetByEmail(ctx, inp.Email)
136
	if err != nil {
137
		return err
138
	}
139
140
	token := uuid.Must(uuid.NewV4()).String()
141
	if err := u.pwdtokrepo.Create(ctx, models.ResetPasswordToken{
142
		UserID:    user.ID,
143
		Token:     token,
144
		CreatedAt: time.Now(),
145
		ExpiresAt: time.Now().Add(u.resetPasswordTokenTTL),
146
	}); err != nil {
147
		return err
148
	}
149
150
	if err := u.mailermq.SendPasswordResetEmail(ctx, mailermq.SendPasswordResetEmailRequest{
151
		Receiver: inp.Email,
152
		Token:    token,
153
	}); err != nil {
154
		return err
155
	}
156
157
	return nil
158
}
159
160
func (u *UserSrv) ResetPassword(ctx context.Context, inp dtos.ResetPassword) error {
161
	//nolint:exhaustruct
162
	if err := (models.User{Password: inp.NewPassword}).ValidatePassword(); err != nil {
163
		return err
164
	}
165
166
	uid, err := u.pwdtokrepo.GetUserIDByTokenAndMarkAsUsed(ctx, inp.Token, time.Now())
167
	if err != nil {
168
		return err
169
	}
170
171
	hashedPassword, err := u.hasher.Hash(inp.NewPassword)
172
	if err != nil {
173
		return err
174
	}
175
176
	return u.userstore.SetPassword(ctx, uid, hashedPassword)
177
}
178
179
func (u *UserSrv) RequestEmailChange(
180
	ctx context.Context,
181
	userID uuid.UUID,
182
	inp dtos.ChangeEmail,
183
) error {
184
	user, err := u.userstore.GetByID(ctx, userID)
185
	if err != nil {
186
		return err
187
	}
188
189
	if user.Email == inp.NewEmail {
190
		return models.ErrUserEmailIsAlreadyInUse
191
	}
192
193
	token := uuid.Must(uuid.NewV4()).String()
194
	changeEmailInput := models.ChangeEmailToken{
195
		UserID:    userID,
196
		Token:     token,
197
		NewEmail:  inp.NewEmail,
198
		CreatedAt: time.Now(),
199
		ExpiresAt: time.Now().Add(u.changeEmailTokenTTL),
200
	}
201
	if err := changeEmailInput.Validate(); err != nil {
202
		return err
203
	}
204
205
	if err := u.changeemailrepo.Create(ctx, changeEmailInput); err != nil {
206
		return err
207
	}
208
209
	if err := u.mailermq.SendChangeEmailConfirmation(ctx, mailermq.SendChangeEmailConfirmationRequest{
210
		Receiver: user.Email,
211
		Token:    token,
212
		NewEmail: inp.NewEmail,
213
	}); err != nil {
214
		return err
215
	}
216
217
	return nil
218
}
219
220
func (u *UserSrv) ChangeEmail(ctx context.Context, givenToken string) error {
221
	token, err := u.changeemailrepo.GetByToken(ctx, givenToken)
222
	if err != nil {
223
		return err
224
	}
225
226
	user, err := u.userstore.GetByID(ctx, token.UserID)
227
	if err != nil {
228
		return err
229
	}
230
231
	if user.Email == token.NewEmail {
232
		return models.ErrUserEmailIsAlreadyInUse
233
	}
234
235
	if err := u.userstore.SetEmail(ctx, token.UserID, token.NewEmail); err != nil {
236
		return err
237
	}
238
239
	if err := u.changeemailrepo.MarkAsUsed(ctx, token.Token, time.Now()); err != nil {
240
		return err
241
	}
242
243
	return nil
244
}
245
246
func (u *UserSrv) Verify(ctx context.Context, verificationKey string) error {
247
	uid, err := u.vertokrepo.GetUserIDByTokenAndMarkAsUsed(ctx, verificationKey, time.Now())
248
	if err != nil {
249
		return err
250
	}
251
252
	return u.userstore.MarkUserAsActivated(ctx, uid)
253
}
254
255
func (u *UserSrv) ResendVerificationEmail(
256
	ctx context.Context,
257
	inp dtos.ResendVerificationEmail,
258
) error {
259
	user, err := u.userstore.GetByEmail(ctx, inp.Email)
260
	if err != nil {
261
		return err
262
	}
263
264
	if user.Activated {
265
		return models.ErrUserIsAlreadyVerified
266
	}
267
268
	token, err := u.vertokrepo.GetTokenOrUpdateTokenByUserID(
269
		ctx,
270
		user.ID,
271
		uuid.Must(uuid.NewV4()).String(),
272
		time.Now().Add(u.verificationTokenTTL))
273
	if err != nil {
274
		return err
275
	}
276
277
	if err := u.mailermq.SendVerificationEmail(ctx, mailermq.SendVerificationEmailRequest{
278
		Receiver: inp.Email,
279
		Token:    token,
280
	}); err != nil {
281
		return err
282
	}
283
284
	return nil
285
}