all repos

onasty @ 5495cb456caa00f4564d1c1eed06a766811debaf

a one-time notes service

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

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