all repos

onasty @ 9d9c06c

a one-time notes service

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

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