all repos

onasty @ 5495cb456caa00f4564d1c1eed06a766811debaf

a one-time notes service

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

Smirnov Oleksandr Smirnov Oleksandr
ss2316544@gmail.com
feat: add oauth2 login for google and github (#109)..., 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("username", "email", "password", "activated", "created_at", "last_login_at").
55
		Values(inp.Username, 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_username_key") {
67
		return uuid.UUID{}, models.ErrUsernameIsAlreadyInUse
68
	}
69
70
	if psqlutil.IsDuplicateErr(err, "users_email_key") {
71
		return uuid.UUID{}, models.ErrUserEmailIsAlreadyInUse
72
	}
73
74
	return id, err
75
}
76
77
func (r *UserRepo) GetByEmail(
78
	ctx context.Context,
79
	email string,
80
) (models.User, error) {
81
	query, args, err := pgq.
82
		Select("id", "username", "email", "password", "activated", "created_at", "last_login_at").
83
		From("users").
84
		Where(pgq.Eq{"email": email}).
85
		SQL()
86
	if err != nil {
87
		return models.User{}, err
88
	}
89
90
	var user models.User
91
	err = r.db.QueryRow(ctx, query, args...).
92
		Scan(&user.ID, &user.Username, &user.Email, &user.Password, &user.Activated, &user.CreatedAt, &user.LastLoginAt)
93
	if errors.Is(err, pgx.ErrNoRows) {
94
		return models.User{}, models.ErrUserNotFound
95
	}
96
97
	return user, err
98
}
99
100
func (r *UserRepo) GetUserIDByEmail(ctx context.Context, email string) (uuid.UUID, error) {
101
	query, args, err := pgq.
102
		Select("id").
103
		From("users").
104
		Where(pgq.Eq{"email": email}).
105
		SQL()
106
	if err != nil {
107
		return uuid.Nil, err
108
	}
109
110
	var id uuid.UUID
111
	err = r.db.QueryRow(ctx, query, args...).Scan(&id)
112
	if errors.Is(err, pgx.ErrNoRows) {
113
		return uuid.Nil, models.ErrUserNotFound
114
	}
115
116
	return id, err
117
}
118
119
func (r *UserRepo) GetByOAuthID(
120
	ctx context.Context,
121
	provider, providerID string,
122
) (models.User, error) {
123
	query := `--sql
124
	select u.id, u.username, u.email, u.password, u.activated, u.created_at, u.last_login_at
125
	from users u
126
	join oauth_identities oi on u.id = oi.user_id
127
	where oi.provider = $1
128
		and oi.provider_id = $2
129
	limit 1`
130
131
	var user models.User
132
	err := r.db.QueryRow(ctx, query, provider, providerID).
133
		Scan(&user.ID, &user.Username, &user.Email, &user.Password, &user.Activated, &user.CreatedAt, &user.LastLoginAt)
134
	if errors.Is(err, pgx.ErrNoRows) {
135
		return models.User{}, models.ErrUserNotFound
136
	}
137
138
	return user, err
139
}
140
141
func (r *UserRepo) LinkOAuthIdentity(
142
	ctx context.Context,
143
	userID uuid.UUID,
144
	provider, providerID string,
145
) error {
146
	query := `--sql
147
	insert into oauth_identities (user_id, provider, provider_id)
148
	values ($1, $2, $3)`
149
150
	_, err := r.db.Exec(ctx, query, userID, provider, providerID)
151
	return err
152
}
153
154
func (r *UserRepo) MarkUserAsActivated(ctx context.Context, id uuid.UUID) error {
155
	query, args, err := pgq.
156
		Update("users").
157
		Set("activated ", true).
158
		Where(pgq.Eq{"id": id.String()}).
159
		SQL()
160
	if err != nil {
161
		return err
162
	}
163
164
	_, err = r.db.Exec(ctx, query, args...)
165
	return err
166
}
167
168
func (r *UserRepo) ChangePassword(
169
	ctx context.Context,
170
	userID uuid.UUID,
171
	oldPass, newPass string,
172
) error {
173
	query, args, err := pgq.
174
		Update("users").
175
		Set("password", newPass).
176
		Where(pgq.Eq{
177
			"id":       userID.String(),
178
			"password": oldPass,
179
		}).
180
		SQL()
181
	if err != nil {
182
		return err
183
	}
184
	_, err = r.db.Exec(ctx, query, args...)
185
	return err
186
}
187
188
func (r *UserRepo) SetPassword(ctx context.Context, userID uuid.UUID, password string) error {
189
	query, args, err := pgq.
190
		Update("users").
191
		Set("password", password).
192
		Where(pgq.Eq{"id": userID.String()}).
193
		SQL()
194
	if err != nil {
195
		return err
196
	}
197
198
	_, err = r.db.Exec(ctx, query, args...)
199
	return err
200
}
201
202
func (r *UserRepo) CheckIfUserExists(ctx context.Context, id uuid.UUID) (bool, error) {
203
	var exists bool
204
	err := r.db.QueryRow(
205
		ctx,
206
		`SELECT EXISTS(SELECT 1 FROM users WHERE id = $1)`,
207
		id.String(),
208
	).Scan(&exists)
209
	if errors.Is(err, pgx.ErrNoRows) {
210
		return false, models.ErrUserNotFound
211
	}
212
213
	return exists, err
214
}
215
216
func (r *UserRepo) CheckIfUserIsActivated(ctx context.Context, id uuid.UUID) (bool, error) {
217
	var activated bool
218
	err := r.db.QueryRow(ctx, `SELECT activated FROM users WHERE id = $1`, id.String()).
219
		Scan(&activated)
220
	if errors.Is(err, pgx.ErrNoRows) {
221
		return false, models.ErrUserNotFound
222
	}
223
	return activated, err
224
}