-
Notifications
You must be signed in to change notification settings - Fork 0
/
user.go
184 lines (153 loc) · 4.53 KB
/
user.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
package brokers
import (
"context"
"crypto/sha256"
"database/sql"
"errors"
"time"
"github.com/rwx-yxu/greenlight/internal/models"
)
// Define a custom ErrDuplicateEmail error.
var (
ErrDuplicateEmail = errors.New("duplicate email")
)
type user struct {
db *sql.DB
}
type UserReader interface {
GetByEmail(email string) (*models.User, error)
GetByToken(scope, tokenPlaintext string) (*models.User, error)
}
type UserWriter interface {
Insert(user *models.User) error
Update(user *models.User) error
}
type UserReadWriter interface {
UserReader
UserWriter
}
func NewUser(db *sql.DB) UserReadWriter {
return &user{db: db}
}
func (u user) Insert(user *models.User) error {
query := `
INSERT INTO users (name, email, password_hash, activated)
VALUES ($1, $2, $3, $4)
RETURNING id, created_at, version`
args := []any{user.Name, user.Email, user.Password.Hash, user.Activated}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
// If the table already contains a record with this email address, then when we try
// to perform the insert there will be a violation of the UNIQUE "users_email_key"
// constraint that we set up in the previous chapter. We check for this error
// specifically, and return custom ErrDuplicateEmail error instead.
err := u.db.QueryRowContext(ctx, query, args...).Scan(&user.ID, &user.CreatedAt, &user.Version)
if err != nil {
switch {
case err.Error() == `pq: duplicate key value violates unique constraint "users_email_key"`:
return ErrDuplicateEmail
default:
return err
}
}
return nil
}
func (u user) GetByEmail(email string) (*models.User, error) {
query := `
SELECT id, created_at, name, email, password_hash, activated, version
FROM users
WHERE email = $1`
var user models.User
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
err := u.db.QueryRowContext(ctx, query, email).Scan(
&user.ID,
&user.CreatedAt,
&user.Name,
&user.Email,
&user.Password.Hash,
&user.Activated,
&user.Version,
)
if err != nil {
switch {
case errors.Is(err, sql.ErrNoRows):
return nil, ErrRecordNotFound
default:
return nil, err
}
}
return &user, nil
}
func (u user) Update(user *models.User) error {
query := `
UPDATE users
SET name = $1, email = $2, password_hash = $3, activated = $4, version = version + 1
WHERE id = $5 AND version = $6
RETURNING version`
args := []any{
user.Name,
user.Email,
user.Password.Hash,
user.Activated,
user.ID,
user.Version,
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
err := u.db.QueryRowContext(ctx, query, args...).Scan(&user.Version)
if err != nil {
switch {
case err.Error() == `pq: duplicate key value violates unique constraint "users_email_key"`:
return ErrDuplicateEmail
case errors.Is(err, sql.ErrNoRows):
return ErrEditConflict
default:
return err
}
}
return nil
}
func (u user) GetByToken(scope, tokenPlaintext string) (*models.User, error) {
// Calculate the SHA-256 hash of the plaintext token provided by the client.
// Remember that this returns a byte *array* with length 32, not a slice.
tokenHash := sha256.Sum256([]byte(tokenPlaintext))
// Set up the SQL query.
query := `
SELECT users.id, users.created_at, users.name, users.email, users.password_hash, users.activated, users.version
FROM users
INNER JOIN tokens
ON users.id = tokens.user_id
WHERE tokens.hash = $1
AND tokens.scope = $2
AND tokens.expiry > $3`
// Create a slice containing the query arguments. Notice how we use the [:] operator
// to get a slice containing the token hash, rather than passing in the array (which
// is not supported by the pq driver), and that we pass the current time as the
// value to check against the token expiry.
args := []any{tokenHash[:], scope, time.Now()}
var user models.User
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
// Execute the query, scanning the return values into a User struct. If no matching
// record is found we return an ErrRecordNotFound error.
err := u.db.QueryRowContext(ctx, query, args...).Scan(
&user.ID,
&user.CreatedAt,
&user.Name,
&user.Email,
&user.Password.Hash,
&user.Activated,
&user.Version,
)
if err != nil {
switch {
case errors.Is(err, sql.ErrNoRows):
return nil, ErrRecordNotFound
default:
return nil, err
}
}
// Return the matching user.
return &user, nil
}