all repos

onasty @ 757b945

a one-time notes service

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

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