1package storage
2
3import (
4 "context"
5 "database/sql"
6 "errors"
7 "fmt"
8)
9
10type rowScanner interface {
11 Scan(dest ...any) error
12}
13
14type execer interface {
15 ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
16 QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
17}
18
19const userColumns = `id, username, display_name, password_hash, disabled, uid_number, created_at, last_login_at, credentials_epoch`
20
21func (s *Store) CreateUser(ctx context.Context, input CreateUserInput) (User, error) {
22 const operation = "create user"
23 if err := s.checkOpen(operation); err != nil {
24 return User{}, err
25 }
26 user, err := s.prepareUser(operation, input)
27 if err != nil {
28 return User{}, err
29 }
30 tx, err := s.sql.BeginTx(ctx, nil)
31 if err != nil {
32 return User{}, classify("begin "+operation, err)
33 }
34 defer tx.Rollback()
35 user, err = insertUser(ctx, tx, operation, user)
36 if err != nil {
37 return User{}, err
38 }
39 if err := tx.Commit(); err != nil {
40 return User{}, classify("commit "+operation, err)
41 }
42 return user, nil
43}
44
45func (s *Store) prepareUser(operation string, input CreateUserInput) (User, error) {
46 username, err := normalizeUsername(input.Username)
47 if err != nil {
48 return User{}, invalid(operation, err)
49 }
50 if err := validateDisplayName(input.DisplayName); err != nil {
51 return User{}, invalid(operation, err)
52 }
53 if len(input.PasswordHash) == 0 {
54 return User{}, invalid(operation, fmt.Errorf("password hash is empty"))
55 }
56 id, err := s.newID()
57 if err != nil {
58 return User{}, fmt.Errorf("%s: %w", operation, err)
59 }
60 return User{
61 ID: id,
62 Username: username,
63 DisplayName: input.DisplayName,
64 PasswordHash: append([]byte(nil), input.PasswordHash...),
65 Disabled: input.Disabled,
66 CreatedAt: canonicalTime(s.now()),
67 }, nil
68}
69
70func insertUser(ctx context.Context, tx *sql.Tx, operation string, user User) (User, error) {
71 var uid int
72 if err := tx.QueryRowContext(ctx, `SELECT next_uid_number FROM settings WHERE id = 1`).Scan(&uid); err != nil {
73 return User{}, classify("read next uid", err)
74 }
75 if uid < firstUID || uid > lastUID {
76 return User{}, fmt.Errorf("%s: %w", operation, ErrUIDExhausted)
77 }
78 if _, err := tx.ExecContext(ctx, `UPDATE settings SET next_uid_number = ? WHERE id = 1`, uid+1); err != nil {
79 return User{}, classify("advance uid", err)
80 }
81 if _, err := tx.ExecContext(ctx, `
82INSERT INTO users (
83 id, username, display_name, password_hash,
84 disabled, uid_number, created_at, last_login_at
85) VALUES (?, ?, ?, ?, ?, ?, ?, NULL)`,
86 user.ID, user.Username, user.DisplayName,
87 user.PasswordHash, boolToInt(user.Disabled), uid,
88 timeToInt(user.CreatedAt)); err != nil {
89 return User{}, classify(operation, err)
90 }
91 if err := clearBootstrap(ctx, tx, operation, user.CreatedAt); err != nil {
92 return User{}, err
93 }
94 user.UIDNumber = uid
95 return user, nil
96}
97
98func (s *Store) UserByID(ctx context.Context, id string) (User, error) {
99 return s.userBy(ctx, "get user by id", `SELECT `+userColumns+` FROM users WHERE id = ?`, id)
100}
101
102func (s *Store) UserByUsername(ctx context.Context, username string) (User, error) {
103 const operation = "get user by username"
104 normalized, err := normalizeUsername(username)
105 if err != nil {
106 return User{}, invalid(operation, err)
107 }
108 return s.userBy(ctx, operation, `SELECT `+userColumns+` FROM users WHERE username = ?`, normalized)
109}
110
111func (s *Store) userBy(ctx context.Context, operation, query string, arg any) (User, error) {
112 if err := s.checkOpen(operation); err != nil {
113 return User{}, err
114 }
115 user, err := scanUser(s.sql.QueryRowContext(ctx, query, arg))
116 if errors.Is(err, sql.ErrNoRows) {
117 return User{}, fmt.Errorf("%s: %w", operation, ErrNotFound)
118 }
119 if err != nil {
120 return User{}, classify(operation, err)
121 }
122 return user, nil
123}
124
125func (s *Store) ListUsers(ctx context.Context) ([]User, error) {
126 const operation = "list users"
127 if err := s.checkOpen(operation); err != nil {
128 return nil, err
129 }
130 rows, err := s.sql.QueryContext(ctx, `SELECT `+userColumns+` FROM users ORDER BY username, id`)
131 if err != nil {
132 return nil, classify(operation, err)
133 }
134 defer rows.Close()
135 var users []User
136 for rows.Next() {
137 user, err := scanUser(rows)
138 if err != nil {
139 return nil, classify(operation, err)
140 }
141 users = append(users, user)
142 }
143 if err := rows.Err(); err != nil {
144 return nil, classify(operation, err)
145 }
146 return users, nil
147}
148
149func (s *Store) UpdateUser(ctx context.Context, id string, input UpdateUserInput) (User, error) {
150 const operation = "update user"
151 if err := s.checkOpen(operation); err != nil {
152 return User{}, err
153 }
154 if err := validateDisplayName(input.DisplayName); err != nil {
155 return User{}, invalid(operation, err)
156 }
157 if len(input.PasswordHash) == 0 {
158 return User{}, invalid(operation, fmt.Errorf("password hash is empty"))
159 }
160 var lastLogin any
161 if input.LastLoginAt != nil {
162 lastLogin = timeToInt(*input.LastLoginAt)
163 }
164 result, err := s.sql.ExecContext(ctx, `
165UPDATE users
166SET display_name = ?, password_hash = ?,
167 disabled = ?, last_login_at = ?
168WHERE id = ?`, input.DisplayName,
169 append([]byte(nil), input.PasswordHash...), boolToInt(input.Disabled), lastLogin, id)
170 if err != nil {
171 return User{}, classify(operation, err)
172 }
173 if err := requireChanged(operation, result); err != nil {
174 return User{}, err
175 }
176 return s.UserByID(ctx, id)
177}
178
179func (s *Store) DeleteUser(ctx context.Context, id string) error {
180 return s.deleteByID(ctx, "delete user", "users", id)
181}
182
183func scanUser(row rowScanner) (User, error) {
184 var user User
185 var disabled int
186 var createdAt int64
187 var lastLogin sql.NullInt64
188 if err := row.Scan(&user.ID, &user.Username,
189 &user.DisplayName, &user.PasswordHash, &disabled, &user.UIDNumber,
190 &createdAt, &lastLogin, &user.CredentialsEpoch); err != nil {
191 return User{}, err
192 }
193 user.Disabled = disabled != 0
194 user.CreatedAt = intToTime(createdAt)
195 user.PasswordHash = append([]byte(nil), user.PasswordHash...)
196 if lastLogin.Valid {
197 value := intToTime(lastLogin.Int64)
198 user.LastLoginAt = &value
199 }
200 return user, nil
201}
202
203func (s *Store) CreateGroup(ctx context.Context, input CreateGroupInput) (Group, error) {
204 const operation = "create group"
205 if err := s.checkOpen(operation); err != nil {
206 return Group{}, err
207 }
208 group, err := s.prepareGroup(operation, input)
209 if err != nil {
210 return Group{}, err
211 }
212 tx, err := s.sql.BeginTx(ctx, nil)
213 if err != nil {
214 return Group{}, classify("begin "+operation, err)
215 }
216 defer tx.Rollback()
217 group, err = insertGroup(ctx, tx, operation, group)
218 if err != nil {
219 return Group{}, err
220 }
221 if err := tx.Commit(); err != nil {
222 return Group{}, classify("commit "+operation, err)
223 }
224 return group, nil
225}
226
227func (s *Store) prepareGroup(operation string, input CreateGroupInput) (Group, error) {
228 name, err := normalizeGroupName(input.Name)
229 if err != nil {
230 return Group{}, invalid(operation, err)
231 }
232 if err := validateGroupDescription(input.Description); err != nil {
233 return Group{}, invalid(operation, err)
234 }
235 id, err := s.newID()
236 if err != nil {
237 return Group{}, fmt.Errorf("%s: %w", operation, err)
238 }
239 return Group{ID: id, Name: name, Description: input.Description, CreatedAt: canonicalTime(s.now())}, nil
240}
241
242func insertGroup(ctx context.Context, tx *sql.Tx, operation string, group Group) (Group, error) {
243 var gid int64
244 if err := tx.QueryRowContext(ctx, `SELECT next_gid_number FROM settings WHERE id = 1`).Scan(&gid); err != nil {
245 return Group{}, classify("read next gid", err)
246 }
247 if gid < firstGID || gid > lastGID {
248 return Group{}, fmt.Errorf("%s: %w", operation, ErrGIDExhausted)
249 }
250 if _, err := tx.ExecContext(ctx, `UPDATE settings SET next_gid_number = ? WHERE id = 1`, gid+1); err != nil {
251 return Group{}, classify("advance gid", err)
252 }
253 if _, err := tx.ExecContext(ctx, `INSERT INTO groups (id, name, description, gid_number, created_at) VALUES (?, ?, ?, ?, ?)`, group.ID, group.Name, group.Description, gid, timeToInt(group.CreatedAt)); err != nil {
254 return Group{}, classify(operation, err)
255 }
256 group.GIDNumber = int(gid)
257 return group, nil
258}
259
260func (s *Store) GroupByID(ctx context.Context, id string) (Group, error) {
261 return s.groupBy(ctx, "get group by id", `SELECT id, name, description, gid_number, created_at FROM groups WHERE id = ?`, id)
262}
263
264func (s *Store) GroupByName(ctx context.Context, name string) (Group, error) {
265 const operation = "get group by name"
266 normalized, err := normalizeGroupName(name)
267 if err != nil {
268 return Group{}, invalid(operation, err)
269 }
270 return s.groupBy(ctx, operation, `SELECT id, name, description, gid_number, created_at FROM groups WHERE name = ?`, normalized)
271}
272
273func (s *Store) groupBy(ctx context.Context, operation, query string, arg any) (Group, error) {
274 if err := s.checkOpen(operation); err != nil {
275 return Group{}, err
276 }
277 group, err := scanGroup(s.sql.QueryRowContext(ctx, query, arg))
278 if errors.Is(err, sql.ErrNoRows) {
279 return Group{}, fmt.Errorf("%s: %w", operation, ErrNotFound)
280 }
281 if err != nil {
282 return Group{}, classify(operation, err)
283 }
284 return group, nil
285}
286
287func (s *Store) ListGroups(ctx context.Context) ([]Group, error) {
288 const operation = "list groups"
289 if err := s.checkOpen(operation); err != nil {
290 return nil, err
291 }
292 rows, err := s.sql.QueryContext(ctx, `SELECT id, name, description, gid_number, created_at FROM groups ORDER BY name, id`)
293 if err != nil {
294 return nil, classify(operation, err)
295 }
296 defer rows.Close()
297 var groups []Group
298 for rows.Next() {
299 group, err := scanGroup(rows)
300 if err != nil {
301 return nil, classify(operation, err)
302 }
303 groups = append(groups, group)
304 }
305 if err := rows.Err(); err != nil {
306 return nil, classify(operation, err)
307 }
308 return groups, nil
309}
310
311func (s *Store) UpdateGroupDescription(ctx context.Context, id, description string) (Group, error) {
312 const operation = "update group"
313 if err := s.checkOpen(operation); err != nil {
314 return Group{}, err
315 }
316 if err := validateGroupDescription(description); err != nil {
317 return Group{}, invalid(operation, err)
318 }
319 result, err := s.sql.ExecContext(ctx, `UPDATE groups SET description = ? WHERE id = ?`, description, id)
320 if err != nil {
321 return Group{}, classify(operation, err)
322 }
323 if err := requireChanged(operation, result); err != nil {
324 return Group{}, err
325 }
326 return s.GroupByID(ctx, id)
327}
328
329func (s *Store) DeleteGroup(ctx context.Context, id string) error {
330 return s.deleteByID(ctx, "delete group", "groups", id)
331}
332
333func scanGroup(row rowScanner) (Group, error) {
334 var group Group
335 var createdAt int64
336 if err := row.Scan(&group.ID, &group.Name, &group.Description, &group.GIDNumber, &createdAt); err != nil {
337 return Group{}, err
338 }
339 group.CreatedAt = intToTime(createdAt)
340 return group, nil
341}
342
343func (s *Store) deleteByID(ctx context.Context, operation, table, id string) error {
344 if err := s.checkOpen(operation); err != nil {
345 return err
346 }
347 query := `DELETE FROM ` + table + ` WHERE id = ?`
348 result, err := s.sql.ExecContext(ctx, query, id)
349 if err != nil {
350 return classify(operation, err)
351 }
352 return requireChanged(operation, result)
353}
354
355func requireChanged(operation string, result sql.Result) error {
356 count, err := result.RowsAffected()
357 if err != nil {
358 return classify(operation+" rows affected", err)
359 }
360 if count == 0 {
361 return fmt.Errorf("%s: %w", operation, ErrNotFound)
362 }
363 return nil
364}