all repos

onasty @ 87e5f907f15cd68fab95e58ea44392d4cbcde839

a one-time notes service

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

Smirnov Oleksandr Smirnov Oleksandr
ss2316544@gmail.com
refactor: deal with TODOs and typos (#30)..., 1 year ago
1
package usersrv
2
3
import (
4
	"context"
5
	"errors"
6
	"time"
7
8
	"github.com/gofrs/uuid/v5"
9
	"github.com/olexsmir/onasty/internal/dtos"
10
	"github.com/olexsmir/onasty/internal/hasher"
11
	"github.com/olexsmir/onasty/internal/jwtutil"
12
	"github.com/olexsmir/onasty/internal/mailer"
13
	"github.com/olexsmir/onasty/internal/models"
14
	"github.com/olexsmir/onasty/internal/store/psql/sessionrepo"
15
	"github.com/olexsmir/onasty/internal/store/psql/userepo"
16
	"github.com/olexsmir/onasty/internal/store/psql/vertokrepo"
17
	"github.com/olexsmir/onasty/internal/transport/http/reqid"
18
)
19
20
type UserServicer interface {
21
	SignUp(ctx context.Context, inp dtos.CreateUserDTO) (uuid.UUID, error)
22
	SignIn(ctx context.Context, inp dtos.SignInDTO) (dtos.TokensDTO, error)
23
	RefreshTokens(ctx context.Context, refreshToken string) (dtos.TokensDTO, error)
24
	Logout(ctx context.Context, userID uuid.UUID) error
25
26
	ChangePassword(ctx context.Context, userID uuid.UUID, inp dtos.ResetUserPasswordDTO) error
27
28
	Verify(ctx context.Context, verificationKey string) error
29
	ResendVerificationEmail(ctx context.Context, credentials dtos.SignInDTO) error
30
31
	ParseJWTToken(token string) (jwtutil.Payload, error)
32
33
	CheckIfUserExists(ctx context.Context, userID uuid.UUID) (bool, error)
34
	CheckIfUserIsActivated(ctx context.Context, userID uuid.UUID) (bool, error)
35
}
36
37
var _ UserServicer = (*UserSrv)(nil)
38
39
type UserSrv struct {
40
	userstore    userepo.UserStorer
41
	sessionstore sessionrepo.SessionStorer
42
	vertokrepo   vertokrepo.VerificationTokenStorer
43
	hasher       hasher.Hasher
44
	jwtTokenizer jwtutil.JWTTokenizer
45
	mailer       mailer.Mailer
46
47
	refreshTokenTTL      time.Duration
48
	verificationTokenTTL time.Duration
49
	appURL               string
50
}
51
52
func New(
53
	userstore userepo.UserStorer,
54
	sessionstore sessionrepo.SessionStorer,
55
	vertokrepo vertokrepo.VerificationTokenStorer,
56
	hasher hasher.Hasher,
57
	jwtTokenizer jwtutil.JWTTokenizer,
58
	mailer mailer.Mailer,
59
	refreshTokenTTL, verificationTokenTTL time.Duration,
60
	appURL string,
61
) *UserSrv {
62
	return &UserSrv{
63
		userstore:            userstore,
64
		sessionstore:         sessionstore,
65
		vertokrepo:           vertokrepo,
66
		hasher:               hasher,
67
		jwtTokenizer:         jwtTokenizer,
68
		mailer:               mailer,
69
		refreshTokenTTL:      refreshTokenTTL,
70
		verificationTokenTTL: verificationTokenTTL,
71
		appURL:               appURL,
72
	}
73
}
74
75
func (u *UserSrv) SignUp(ctx context.Context, inp dtos.CreateUserDTO) (uuid.UUID, error) {
76
	hashedPassword, err := u.hasher.Hash(inp.Password)
77
	if err != nil {
78
		return uuid.UUID{}, err
79
	}
80
81
	uid, err := u.userstore.Create(ctx, dtos.CreateUserDTO{
82
		Username:    inp.Username,
83
		Email:       inp.Email,
84
		Password:    hashedPassword,
85
		CreatedAt:   inp.CreatedAt,
86
		LastLoginAt: inp.LastLoginAt,
87
	})
88
	if err != nil {
89
		return uuid.Nil, err
90
	}
91
92
	vtok := uuid.Must(uuid.NewV4()).String()
93
	if err := u.vertokrepo.Create(ctx, vtok, uid, time.Now(), time.Now().Add(u.verificationTokenTTL)); err != nil {
94
		return uuid.Nil, err
95
	}
96
97
	sendingCtx, cancel := getContextForEmailSending(ctx)
98
	go u.sendVerificationEmail(sendingCtx, cancel, inp.Email, vtok, u.appURL)
99
100
	return uid, nil
101
}
102
103
func (u *UserSrv) SignIn(ctx context.Context, inp dtos.SignInDTO) (dtos.TokensDTO, error) {
104
	hashedPassword, err := u.hasher.Hash(inp.Password)
105
	if err != nil {
106
		return dtos.TokensDTO{}, err
107
	}
108
109
	user, err := u.userstore.GetUserByCredentials(ctx, inp.Email, hashedPassword)
110
	if err != nil {
111
		if errors.Is(err, models.ErrUserNotFound) {
112
			return dtos.TokensDTO{}, models.ErrUserWrongCredentials
113
		}
114
		return dtos.TokensDTO{}, err
115
	}
116
117
	if !user.Activated {
118
		return dtos.TokensDTO{}, models.ErrUserIsNotActivated
119
	}
120
121
	tokens, err := u.getTokens(user.ID)
122
	if err != nil {
123
		return dtos.TokensDTO{}, err
124
	}
125
126
	if err := u.sessionstore.Set(ctx, user.ID, tokens.Refresh, time.Now().Add(u.refreshTokenTTL)); err != nil {
127
		return dtos.TokensDTO{}, err
128
	}
129
130
	return dtos.TokensDTO{
131
		Access:  tokens.Access,
132
		Refresh: tokens.Refresh,
133
	}, nil
134
}
135
136
func (u *UserSrv) Logout(ctx context.Context, userID uuid.UUID) error {
137
	return u.sessionstore.Delete(ctx, userID)
138
}
139
140
func (u *UserSrv) RefreshTokens(ctx context.Context, rtoken string) (dtos.TokensDTO, error) {
141
	userID, err := u.sessionstore.GetUserIDByRefreshToken(ctx, rtoken)
142
	if err != nil {
143
		return dtos.TokensDTO{}, err
144
	}
145
146
	tokens, err := u.getTokens(userID)
147
	if err != nil {
148
		return dtos.TokensDTO{}, err
149
	}
150
151
	if err := u.sessionstore.Update(ctx, userID, rtoken, tokens.Refresh); err != nil {
152
		return dtos.TokensDTO{}, err
153
	}
154
155
	return dtos.TokensDTO{
156
		Access:  tokens.Access,
157
		Refresh: tokens.Refresh,
158
	}, nil
159
}
160
161
func (u *UserSrv) ChangePassword(
162
	ctx context.Context,
163
	userID uuid.UUID,
164
	inp dtos.ResetUserPasswordDTO,
165
) error {
166
	oldPass, err := u.hasher.Hash(inp.CurrentPassword)
167
	if err != nil {
168
		return err
169
	}
170
171
	newPass, err := u.hasher.Hash(inp.NewPassword)
172
	if err != nil {
173
		return err
174
	}
175
176
	if err := u.userstore.ChangePassword(ctx, userID, oldPass, newPass); err != nil {
177
		return err
178
	}
179
180
	return nil
181
}
182
183
func (u *UserSrv) Verify(ctx context.Context, verificationKey string) error {
184
	uid, err := u.vertokrepo.GetUserIDByTokenAndMarkAsUsed(ctx, verificationKey, time.Now())
185
	if err != nil {
186
		return err
187
	}
188
189
	return u.userstore.MarkUserAsActivated(ctx, uid)
190
}
191
192
func (u *UserSrv) ResendVerificationEmail(ctx context.Context, inp dtos.SignInDTO) error {
193
	hashedPassword, err := u.hasher.Hash(inp.Password)
194
	if err != nil {
195
		return err
196
	}
197
198
	user, err := u.userstore.GetUserByCredentials(ctx, inp.Email, hashedPassword)
199
	if err != nil {
200
		if errors.Is(err, models.ErrUserNotFound) {
201
			return models.ErrUserWrongCredentials
202
		}
203
		return err
204
	}
205
206
	if user.Activated {
207
		return models.ErrUserIsAlreeadyVerified
208
	}
209
210
	token, err := u.vertokrepo.GetTokenOrUpdateTokenByUserID(
211
		ctx,
212
		user.ID,
213
		uuid.Must(uuid.NewV4()).String(),
214
		time.Now().Add(u.verificationTokenTTL))
215
	if err != nil {
216
		return err
217
	}
218
219
	sendingCtx, cancel := getContextForEmailSending(ctx)
220
	go u.sendVerificationEmail(sendingCtx, cancel, inp.Email, token, u.appURL)
221
222
	return nil
223
}
224
225
func (u *UserSrv) ParseJWTToken(token string) (jwtutil.Payload, error) {
226
	return u.jwtTokenizer.Parse(token)
227
}
228
229
func (u UserSrv) CheckIfUserExists(ctx context.Context, id uuid.UUID) (bool, error) {
230
	return u.userstore.CheckIfUserExists(ctx, id)
231
}
232
233
func (u UserSrv) CheckIfUserIsActivated(ctx context.Context, userID uuid.UUID) (bool, error) {
234
	return u.userstore.CheckIfUserIsActivated(ctx, userID)
235
}
236
237
func (u UserSrv) getTokens(userID uuid.UUID) (dtos.TokensDTO, error) {
238
	accessToken, err := u.jwtTokenizer.AccessToken(jwtutil.Payload{UserID: userID.String()})
239
	if err != nil {
240
		return dtos.TokensDTO{}, err
241
	}
242
243
	refreshToken, err := u.jwtTokenizer.RefreshToken()
244
	if err != nil {
245
		return dtos.TokensDTO{}, err
246
	}
247
248
	return dtos.TokensDTO{
249
		Access:  accessToken,
250
		Refresh: refreshToken,
251
	}, err
252
}
253
254
func getContextForEmailSending(ctx context.Context) (context.Context, context.CancelFunc) {
255
	rid := reqid.GetContext(ctx)
256
	resCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
257
	resCtx = reqid.SetContext(resCtx, rid)
258
259
	return resCtx, cancel
260
}