all repos

onasty @ c672974dbcb27a4f64fae83dca5e7b031e15d22a

a one-time notes service

onasty/internal/store/psql/userepo/userepo.go (view raw)

Olexandr Smirnov Olexandr Smirnov
ss2316544@gmail.com
feat(api): change email (#191)..., 9 months ago
1
package userepo
2
3
import (
4
	"context"
5
	"errors"
6
7
	"github.com/gofrs/uuid/v5"
8
	"github.com/henvic/pgq"
9
	"github.com/jackc/pgx/v5"
10
	"github.com/olexsmir/onasty/internal/models"
11
	"github.com/olexsmir/onasty/internal/store/psqlutil"
12
)
13
14
type UserStorer interface {
15
	// Create creates a new user.
16
	Create(ctx context.Context, inp models.User) (uuid.UUID, error)
17
18
	// GetByEmail returns user by email and password
19
	// the password should be hashed.
20
	GetByEmail(ctx context.Context, email string) (models.User, error)
21
22
	// GetByID returns user by id.
23
	// If user not found, returns [models.ErrUserNotFound].
24
	GetByID(ctx context.Context, userID uuid.UUID) (models.User, error)
25
26
	// GetUserIDByEmail returns user id that is associated with their email.
27
	// If user not found, returns [models.ErrUserNotFound].
28
	GetUserIDByEmail(ctx context.Context, email string) (uuid.UUID, error)
29
30
	// MakrUserAsActivated marks user as activated by their id
31
	MarkUserAsActivated(ctx context.Context, id uuid.UUID) error
32
33
	// ChangePassword changes user password from oldPassword to newPassword
34
	// and oldPassword and newPassword should be hashed
35
	ChangePassword(ctx context.Context, userID uuid.UUID, newPassword string) error
36
37
	// SetPassword sets new password for user by their id
38
	// password should be hashed
39
	SetPassword(ctx context.Context, userID uuid.UUID, newPassword string) error
40
41
	// SetEmail sets new email for user by their id
42
	SetEmail(ctx context.Context, userID uuid.UUID, email string) error
43
44
	GetByOAuthID(ctx context.Context, provider, providerID string) (models.User, error)
45
	LinkOAuthIdentity(ctx context.Context, userID uuid.UUID, provider, providerID string) error
46
47
	CheckIfUserExists(ctx context.Context, userID uuid.UUID) (bool, error)
48
	CheckIfUserIsActivated(ctx context.Context, userID uuid.UUID) (bool, error)
49
}
50
51
var _ UserStorer = (*UserRepo)(nil)
52
53
type UserRepo struct {
54
	db *psqlutil.DB
55
}
56
57
func New(db *psqlutil.DB) *UserRepo {
58
	return &UserRepo{
59
		db: db,
60
	}
61
}
62
63
func (r *UserRepo) Create(ctx context.Context, inp models.User) (uuid.UUID, error) {
64
	query, args, err := pgq.
65
		Insert("users").
66
		Columns("email", "password", "activated", "created_at", "last_login_at").
67
		Values(inp.Email, inp.Password, inp.Activated, inp.CreatedAt, inp.LastLoginAt).
68
		Returning("id").
69
		SQL()
70
	if err != nil {
71
		return uuid.UUID{}, err
72
	}
73
74
	var id uuid.UUID
75
	err = r.db.QueryRow(ctx, query, args...).Scan(&id)
76
77
	// FIXME: somehow this does return errors but i can't errors.Is them in api layer
78
	if psqlutil.IsDuplicateErr(err, "users_email_key") {
79
		return uuid.UUID{}, models.ErrUserEmailIsAlreadyInUse
80
	}
81
82
	return id, err
83
}
84
85
func (r *UserRepo) GetByEmail(
86
	ctx context.Context,
87
	email string,
88
) (models.User, error) {
89
	query, args, err := pgq.
90
		Select("id", "email", "password", "activated", "created_at", "last_login_at").
91
		From("users").
92
		Where(pgq.Eq{"email": email}).
93
		SQL()
94
	if err != nil {
95
		return models.User{}, err
96
	}
97
98
	var user models.User
99
	err = r.db.QueryRow(ctx, query, args...).
100
		Scan(&user.ID, &user.Email, &user.Password, &user.Activated, &user.CreatedAt, &user.LastLoginAt)
101
	if errors.Is(err, pgx.ErrNoRows) {
102
		return models.User{}, models.ErrUserNotFound
103
	}
104
105
	return user, err
106
}
107
108
func (r *UserRepo) GetUserIDByEmail(ctx context.Context, email string) (uuid.UUID, error) {
109
	query, args, err := pgq.
110
		Select("id").
111
		From("users").
112
		Where(pgq.Eq{"email": email}).
113
		SQL()
114
	if err != nil {
115
		return uuid.Nil, err
116
	}
117
118
	var id uuid.UUID
119
	err = r.db.QueryRow(ctx, query, args...).Scan(&id)
120
	if errors.Is(err, pgx.ErrNoRows) {
121
		return uuid.Nil, models.ErrUserNotFound
122
	}
123
124
	return id, err
125
}
126
127
func (r *UserRepo) GetByID(ctx context.Context, userID uuid.UUID) (models.User, error) {
128
	query := `--sql
129
select id, email, password, activated, created_at, last_login_at
130
from users
131
where id = $1`
132
133
	var user models.User
134
	err := r.db.QueryRow(ctx, query, userID).
135
		Scan(&user.ID, &user.Email, &user.Password, &user.Activated, &user.CreatedAt, &user.LastLoginAt)
136
	if errors.Is(err, pgx.ErrNoRows) {
137
		return models.User{}, models.ErrUserNotFound
138
	}
139
140
	return user, err
141
}
142
143
func (r *UserRepo) GetByOAuthID(
144
	ctx context.Context,
145
	provider, providerID string,
146
) (models.User, error) {
147
	query := `--sql
148
	select u.id, u.email, u.password, u.activated, u.created_at, u.last_login_at
149
	from users u
150
	join oauth_identities oi on u.id = oi.user_id
151
	where oi.provider = $1
152
		and oi.provider_id = $2
153
	limit 1`
154
155
	var user models.User
156
	err := r.db.QueryRow(ctx, query, provider, providerID).
157
		Scan(&user.ID, &user.Email, &user.Password, &user.Activated, &user.CreatedAt, &user.LastLoginAt)
158
	if errors.Is(err, pgx.ErrNoRows) {
159
		return models.User{}, models.ErrUserNotFound
160
	}
161
162
	return user, err
163
}
164
165
func (r *UserRepo) LinkOAuthIdentity(
166
	ctx context.Context,
167
	userID uuid.UUID,
168
	provider, providerID string,
169
) error {
170
	query := `--sql
171
insert into oauth_identities (user_id, provider, provider_id)
172
values ($1, $2, $3)
173
on conflict (provider, provider_id) do update
174
set user_id = $1,
175
	provider = $2,
176
	provider_id = $3`
177
178
	_, err := r.db.Exec(ctx, query, userID, provider, providerID)
179
	return err
180
}
181
182
func (r *UserRepo) MarkUserAsActivated(ctx context.Context, id uuid.UUID) error {
183
	query, args, err := pgq.
184
		Update("users").
185
		Set("activated ", true).
186
		Where(pgq.Eq{"id": id.String()}).
187
		SQL()
188
	if err != nil {
189
		return err
190
	}
191
192
	_, err = r.db.Exec(ctx, query, args...)
193
	return err
194
}
195
196
func (r *UserRepo) ChangePassword(
197
	ctx context.Context,
198
	userID uuid.UUID,
199
	newPasswd string,
200
) error {
201
	query, args, err := pgq.
202
		Update("users").
203
		Set("password", newPasswd).
204
		Where(pgq.Eq{"id": userID.String()}).
205
		SQL()
206
	if err != nil {
207
		return err
208
	}
209
	_, err = r.db.Exec(ctx, query, args...)
210
	return err
211
}
212
213
func (r *UserRepo) SetPassword(ctx context.Context, userID uuid.UUID, password string) error {
214
	query, args, err := pgq.
215
		Update("users").
216
		Set("password", password).
217
		Where(pgq.Eq{"id": userID.String()}).
218
		SQL()
219
	if err != nil {
220
		return err
221
	}
222
223
	ct, err := r.db.Exec(ctx, query, args...)
224
	if err != nil {
225
		return err
226
	}
227
228
	if ct.RowsAffected() == 0 {
229
		return models.ErrUserNotFound
230
	}
231
232
	return nil
233
}
234
235
func (r *UserRepo) SetEmail(ctx context.Context, userID uuid.UUID, email string) error {
236
	query, args, err := pgq.
237
		Update("users").
238
		Set("email", email).
239
		Where(pgq.Eq{"id": userID.String()}).
240
		SQL()
241
	if err != nil {
242
		return err
243
	}
244
245
	ct, err := r.db.Exec(ctx, query, args...)
246
	if err != nil {
247
		return err
248
	}
249
250
	if ct.RowsAffected() == 0 {
251
		return models.ErrUserNotFound
252
	}
253
254
	return nil
255
}
256
257
func (r *UserRepo) CheckIfUserExists(ctx context.Context, id uuid.UUID) (bool, error) {
258
	var exists bool
259
	err := r.db.QueryRow(
260
		ctx,
261
		`SELECT EXISTS(SELECT 1 FROM users WHERE id = $1)`,
262
		id.String(),
263
	).Scan(&exists)
264
	if errors.Is(err, pgx.ErrNoRows) {
265
		return false, models.ErrUserNotFound
266
	}
267
268
	return exists, err
269
}
270
271
func (r *UserRepo) CheckIfUserIsActivated(ctx context.Context, id uuid.UUID) (bool, error) {
272
	var activated bool
273
	err := r.db.QueryRow(ctx, `SELECT activated FROM users WHERE id = $1`, id.String()).
274
		Scan(&activated)
275
	if errors.Is(err, pgx.ErrNoRows) {
276
		return false, models.ErrUserNotFound
277
	}
278
	return activated, err
279
}