all repos

onasty @ e771742

a one-time notes service

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

Olexandr Smirnov Olexandr Smirnov
ss2316544@gmail.com
docs: add missing code comments (#185)..., 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
	GetByOAuthID(ctx context.Context, provider, providerID string) (models.User, error)
42
	LinkOAuthIdentity(ctx context.Context, userID uuid.UUID, provider, providerID string) error
43
44
	CheckIfUserExists(ctx context.Context, userID uuid.UUID) (bool, error)
45
	CheckIfUserIsActivated(ctx context.Context, userID uuid.UUID) (bool, error)
46
}
47
48
var _ UserStorer = (*UserRepo)(nil)
49
50
type UserRepo struct {
51
	db *psqlutil.DB
52
}
53
54
func New(db *psqlutil.DB) *UserRepo {
55
	return &UserRepo{
56
		db: db,
57
	}
58
}
59
60
func (r *UserRepo) Create(ctx context.Context, inp models.User) (uuid.UUID, error) {
61
	query, args, err := pgq.
62
		Insert("users").
63
		Columns("email", "password", "activated", "created_at", "last_login_at").
64
		Values(inp.Email, inp.Password, inp.Activated, inp.CreatedAt, inp.LastLoginAt).
65
		Returning("id").
66
		SQL()
67
	if err != nil {
68
		return uuid.UUID{}, err
69
	}
70
71
	var id uuid.UUID
72
	err = r.db.QueryRow(ctx, query, args...).Scan(&id)
73
74
	// FIXME: somehow this does return errors but i can't errors.Is them in api layer
75
	if psqlutil.IsDuplicateErr(err, "users_email_key") {
76
		return uuid.UUID{}, models.ErrUserEmailIsAlreadyInUse
77
	}
78
79
	return id, err
80
}
81
82
func (r *UserRepo) GetByEmail(
83
	ctx context.Context,
84
	email string,
85
) (models.User, error) {
86
	query, args, err := pgq.
87
		Select("id", "email", "password", "activated", "created_at", "last_login_at").
88
		From("users").
89
		Where(pgq.Eq{"email": email}).
90
		SQL()
91
	if err != nil {
92
		return models.User{}, err
93
	}
94
95
	var user models.User
96
	err = r.db.QueryRow(ctx, query, args...).
97
		Scan(&user.ID, &user.Email, &user.Password, &user.Activated, &user.CreatedAt, &user.LastLoginAt)
98
	if errors.Is(err, pgx.ErrNoRows) {
99
		return models.User{}, models.ErrUserNotFound
100
	}
101
102
	return user, err
103
}
104
105
func (r *UserRepo) GetUserIDByEmail(ctx context.Context, email string) (uuid.UUID, error) {
106
	query, args, err := pgq.
107
		Select("id").
108
		From("users").
109
		Where(pgq.Eq{"email": email}).
110
		SQL()
111
	if err != nil {
112
		return uuid.Nil, err
113
	}
114
115
	var id uuid.UUID
116
	err = r.db.QueryRow(ctx, query, args...).Scan(&id)
117
	if errors.Is(err, pgx.ErrNoRows) {
118
		return uuid.Nil, models.ErrUserNotFound
119
	}
120
121
	return id, err
122
}
123
124
func (r *UserRepo) GetByID(ctx context.Context, userID uuid.UUID) (models.User, error) {
125
	query := `--sql
126
select id, email, password, activated, created_at, last_login_at
127
from users
128
where id = $1`
129
130
	var user models.User
131
	err := r.db.QueryRow(ctx, query, userID).
132
		Scan(&user.ID, &user.Email, &user.Password, &user.Activated, &user.CreatedAt, &user.LastLoginAt)
133
	if errors.Is(err, pgx.ErrNoRows) {
134
		return models.User{}, models.ErrUserNotFound
135
	}
136
137
	return user, err
138
}
139
140
func (r *UserRepo) GetByOAuthID(
141
	ctx context.Context,
142
	provider, providerID string,
143
) (models.User, error) {
144
	query := `--sql
145
	select u.id, u.email, u.password, u.activated, u.created_at, u.last_login_at
146
	from users u
147
	join oauth_identities oi on u.id = oi.user_id
148
	where oi.provider = $1
149
		and oi.provider_id = $2
150
	limit 1`
151
152
	var user models.User
153
	err := r.db.QueryRow(ctx, query, provider, providerID).
154
		Scan(&user.ID, &user.Email, &user.Password, &user.Activated, &user.CreatedAt, &user.LastLoginAt)
155
	if errors.Is(err, pgx.ErrNoRows) {
156
		return models.User{}, models.ErrUserNotFound
157
	}
158
159
	return user, err
160
}
161
162
func (r *UserRepo) LinkOAuthIdentity(
163
	ctx context.Context,
164
	userID uuid.UUID,
165
	provider, providerID string,
166
) error {
167
	query := `--sql
168
insert into oauth_identities (user_id, provider, provider_id)
169
values ($1, $2, $3)
170
on conflict (provider, provider_id) do update
171
set user_id = $1,
172
	provider = $2,
173
	provider_id = $3`
174
175
	_, err := r.db.Exec(ctx, query, userID, provider, providerID)
176
	return err
177
}
178
179
func (r *UserRepo) MarkUserAsActivated(ctx context.Context, id uuid.UUID) error {
180
	query, args, err := pgq.
181
		Update("users").
182
		Set("activated ", true).
183
		Where(pgq.Eq{"id": id.String()}).
184
		SQL()
185
	if err != nil {
186
		return err
187
	}
188
189
	_, err = r.db.Exec(ctx, query, args...)
190
	return err
191
}
192
193
func (r *UserRepo) ChangePassword(
194
	ctx context.Context,
195
	userID uuid.UUID,
196
	newPasswd string,
197
) error {
198
	query, args, err := pgq.
199
		Update("users").
200
		Set("password", newPasswd).
201
		Where(pgq.Eq{"id": userID.String()}).
202
		SQL()
203
	if err != nil {
204
		return err
205
	}
206
	_, err = r.db.Exec(ctx, query, args...)
207
	return err
208
}
209
210
func (r *UserRepo) SetPassword(ctx context.Context, userID uuid.UUID, password string) error {
211
	query, args, err := pgq.
212
		Update("users").
213
		Set("password", password).
214
		Where(pgq.Eq{"id": userID.String()}).
215
		SQL()
216
	if err != nil {
217
		return err
218
	}
219
220
	_, err = r.db.Exec(ctx, query, args...)
221
	return err
222
}
223
224
func (r *UserRepo) CheckIfUserExists(ctx context.Context, id uuid.UUID) (bool, error) {
225
	var exists bool
226
	err := r.db.QueryRow(
227
		ctx,
228
		`SELECT EXISTS(SELECT 1 FROM users WHERE id = $1)`,
229
		id.String(),
230
	).Scan(&exists)
231
	if errors.Is(err, pgx.ErrNoRows) {
232
		return false, models.ErrUserNotFound
233
	}
234
235
	return exists, err
236
}
237
238
func (r *UserRepo) CheckIfUserIsActivated(ctx context.Context, id uuid.UUID) (bool, error) {
239
	var activated bool
240
	err := r.db.QueryRow(ctx, `SELECT activated FROM users WHERE id = $1`, id.String()).
241
		Scan(&activated)
242
	if errors.Is(err, pgx.ErrNoRows) {
243
		return false, models.ErrUserNotFound
244
	}
245
	return activated, err
246
}