19bb31ff Andy McCall 1 month ago ↶
1
2
3
4
use super :: { now_ms, Pool} ;
5
use sqlx:: FromRow;
6
7
# [ derive ( Clone, Debug, FromRow ) ]
8
pub struct Account {
9
pub id : i64 ,
10
pub username : String,
11
pub display_name : String,
12
pub email : String,
13
pub email_verified : bool ,
14
pub password_hash : String,
15
pub session_salt : String,
16
pub totp_secret : Option< String> ,
17
pub recovery_codes : String,
18
pub is_admin : bool ,
19
pub banned : bool ,
6a178055 Andy McCall 1 month ago ↶
20
21
#[sqlx(default)]
22
pub disabled : bool ,
19bb31ff Andy McCall 1 month ago ↶
23
pub created_ms : i64 ,
24
pub last_seen_ms : i64 ,
18e381ea Andy McCall 1 month ago ↶
25
pub email_notifications : bool ,
26
pub unsubscribe_key : String,
10174045 Andy McCall 1 month ago ↶
27
28
#[sqlx(default)]
29
pub bio : String,
30
#[sqlx(default)]
31
pub website : String,
32
#[sqlx(default)]
33
pub location : String,
a160b98f Andy McCall 1 month ago ↶
34
35
pub avatar : String,
89e89836 Andy McCall 1 month ago ↶
36
37
38
#[sqlx(default)]
39
pub bluesky : String,
40
#[sqlx(default)]
41
pub mastodon : String,
42
#[sqlx(default)]
43
pub github : String,
19bb31ff Andy McCall 1 month ago ↶
44
}
45
46
impl Account {
6a178055 Andy McCall 1 month ago ↶
47
48
pub fn blocked ( & self ) -> bool {
49
self .banned | | self .disabled
50
}
51
19bb31ff Andy McCall 1 month ago ↶
52
pub fn name ( & self ) -> & str {
53
if self .display_name.is_empty ( ) {
54
& self .username
55
} else {
56
& self .display_name
57
}
58
}
59
60
pub fn totp_enabled ( & self ) -> bool {
61
self .totp_secret.is_some ( )
62
}
63
}
64
65
# [ derive ( Clone, Debug, FromRow ) ]
66
pub struct Organisation {
67
pub id : i64 ,
68
pub name : String,
69
pub display_name : String,
70
pub description : String,
71
pub default_role : String,
72
pub created_ms : i64 ,
a160b98f Andy McCall 1 month ago ↶
73
74
pub avatar : String,
19bb31ff Andy McCall 1 month ago ↶
75
}
76
77
# [ derive ( Clone, Debug, FromRow ) ]
78
pub struct Team {
79
pub id : i64 ,
80
pub org_id : i64 ,
81
pub name : String,
82
}
83
84
# [ derive ( Clone, Debug, FromRow ) ]
85
pub struct Token {
86
pub id : i64 ,
87
pub account_id : i64 ,
88
pub name : String,
89
pub hash : String,
90
pub scopes : String,
91
pub repos : String,
92
pub created_ms : i64 ,
93
pub expires_ms : Option< i64 > ,
94
pub last_used_ms : Option< i64 > ,
60f11b71 Andy McCall 1 month ago ↶
95
96
pub renew_ms : Option< i64 > ,
19bb31ff Andy McCall 1 month ago ↶
97
}
98
99
impl Token {
100
pub fn scope_list ( & self ) -> Vec< & str > {
101
self .scopes.split ( ' ,' ) .filter ( | s | ! s.is_empty ( ) ) .collect ( )
102
}
103
104
pub fn has_scope ( & self , scope : & str ) -> bool {
105
self .scope_list ( ) .contains ( & scope)
106
}
107
108
109
110
pub fn covers ( & self , slug : & str ) -> bool {
111
self .repos.is_empty ( ) | | self .repos.split ( ' ,' ) .any ( | r | r.trim ( ) .eq_ignore_ascii_case ( slug) )
112
}
113
114
pub fn expired ( & self , now : i64 ) -> bool {
115
self .expires_ms.is_some_and ( | e | e < now )
116
}
117
}
118
119
# [ derive ( Clone, Debug, FromRow ) ]
120
pub struct LinkedIdentity {
121
pub key : String,
122
pub account_id : i64 ,
123
pub name : String,
124
pub email : String,
125
pub object : Vec< u8 > ,
126
pub linked_ms : i64 ,
127
}
128
129
# [ derive ( Clone, Debug, FromRow ) ]
130
pub struct AuditRow {
131
pub id : i64 ,
132
pub at_ms : i64 ,
133
pub actor : String,
134
pub action : String,
135
pub target : String,
136
pub detail : String,
137
}
138
139
140
141
pub async fn account_by_name ( pool : & Pool, username : & str ) -> sqlx:: Result< Option< Account> > {
142
sqlx:: query_as( " SELECT * FROM accounts WHERE username = ?" ) .bind ( username) .fetch_optional ( pool) .await
143
}
144
145
pub async fn account_by_id ( pool : & Pool, id : i64 ) -> sqlx:: Result< Option< Account> > {
146
sqlx:: query_as( " SELECT * FROM accounts WHERE id = ?" ) .bind ( id) .fetch_optional ( pool) .await
147
}
148
149
pub async fn accounts ( pool : & Pool) -> sqlx:: Result< Vec< Account> > {
150
sqlx:: query_as( " SELECT * FROM accounts ORDER BY username" ) .fetch_all ( pool) .await
151
}
152
153
pub async fn account_count ( pool : & Pool) -> sqlx:: Result< i64 > {
154
sqlx:: query_scalar( " SELECT COUNT(*) FROM accounts" ) .fetch_one ( pool) .await
155
}
156
157
pub async fn create_account (
158
pool : & Pool,
159
username : & str ,
160
email : & str ,
161
password_hash : & str ,
162
session_salt : & str ,
163
is_admin : bool ,
164
) -> sqlx:: Result< Account> {
165
sqlx:: query_as(
166
" INSERT INTO accounts (username, email, password_hash, session_salt, is_admin, created_ms) VALUES (?, ?, ?, ?, ?, ?) RETURNING *" ,
167
)
168
.bind ( username)
169
.bind ( email)
170
.bind ( password_hash)
171
.bind ( session_salt)
172
.bind ( is_admin)
173
.bind ( now_ms ( ) )
174
.fetch_one ( pool)
175
.await
176
}
177
178
pub async fn set_profile ( pool : & Pool, id : i64 , display_name : & str , email : & str ) -> sqlx:: Result< ( ) > {
34beb1a7 Andy McCall 1 month ago ↶
179
180
sqlx:: query(
181
" UPDATE accounts SET display_name = ?, email_verified = CASE WHEN email = ? THEN email_verified ELSE 0 END, email = ? WHERE id = ?" ,
182
)
183
.bind ( display_name)
184
.bind ( email)
185
.bind ( email)
186
.bind ( id)
187
.execute ( pool)
188
.await? ;
19bb31ff Andy McCall 1 month ago ↶
189
Ok ( ( ) )
190
}
191
10174045 Andy McCall 1 month ago ↶
192
89e89836 Andy McCall 1 month ago ↶
193
194
pub async fn set_links ( pool : & Pool, id : i64 , bluesky : & str , mastodon : & str , github : & str ) -> sqlx:: Result< ( ) > {
195
sqlx:: query( " UPDATE accounts SET bluesky = ?, mastodon = ?, github = ? WHERE id = ?" )
196
.bind ( bluesky)
197
.bind ( mastodon)
198
.bind ( github)
199
.bind ( id)
200
.execute ( pool)
201
.await? ;
202
Ok ( ( ) )
203
}
204
10174045 Andy McCall 1 month ago ↶
205
pub async fn set_bio ( pool : & Pool, id : i64 , bio : & str , website : & str , location : & str ) -> sqlx:: Result< ( ) > {
206
sqlx:: query( " UPDATE accounts SET bio = ?, website = ?, location = ? WHERE id = ?" )
207
.bind ( bio)
208
.bind ( website)
209
.bind ( location)
210
.bind ( id)
211
.execute ( pool)
212
.await? ;
213
Ok ( ( ) )
214
}
215
19bb31ff Andy McCall 1 month ago ↶
216
pub async fn set_password ( pool : & Pool, id : i64 , password_hash : & str , session_salt : & str ) -> sqlx:: Result< ( ) > {
217
sqlx:: query( " UPDATE accounts SET password_hash = ?, session_salt = ? WHERE id = ?" )
218
.bind ( password_hash)
219
.bind ( session_salt)
220
.bind ( id)
221
.execute ( pool)
222
.await? ;
223
Ok ( ( ) )
224
}
225
226
pub async fn set_session_salt ( pool : & Pool, id : i64 , session_salt : & str ) -> sqlx:: Result< ( ) > {
227
sqlx:: query( " UPDATE accounts SET session_salt = ? WHERE id = ?" ) .bind ( session_salt) .bind ( id) .execute ( pool) .await? ;
228
Ok ( ( ) )
229
}
230
231
pub async fn set_totp ( pool : & Pool, id : i64 , secret : Option< & str > , recovery_codes : & str ) -> sqlx:: Result< ( ) > {
232
sqlx:: query( " UPDATE accounts SET totp_secret = ?, recovery_codes = ? WHERE id = ?" )
233
.bind ( secret)
234
.bind ( recovery_codes)
235
.bind ( id)
236
.execute ( pool)
237
.await? ;
238
Ok ( ( ) )
239
}
240
241
pub async fn set_admin ( pool : & Pool, id : i64 , is_admin : bool ) -> sqlx:: Result< ( ) > {
242
sqlx:: query( " UPDATE accounts SET is_admin = ? WHERE id = ?" ) .bind ( is_admin) .bind ( id) .execute ( pool) .await? ;
243
Ok ( ( ) )
244
}
245
246
pub async fn set_banned ( pool : & Pool, id : i64 , banned : bool ) -> sqlx:: Result< ( ) > {
247
sqlx:: query( " UPDATE accounts SET banned = ? WHERE id = ?" ) .bind ( banned) .bind ( id) .execute ( pool) .await? ;
6a178055 Andy McCall 1 month ago ↶
248
Ok ( ( ) )
249
}
250
251
pub async fn set_disabled ( pool : & Pool, id : i64 , disabled : bool ) -> sqlx:: Result< ( ) > {
252
sqlx:: query( " UPDATE accounts SET disabled = ? WHERE id = ?" ) .bind ( disabled) .bind ( id) .execute ( pool) .await? ;
19bb31ff Andy McCall 1 month ago ↶
253
Ok ( ( ) )
6a178055 Andy McCall 1 month ago ↶
254
}
255
256
257
258
259
260
pub async fn delete_account ( pool : & Pool, id : i64 , username : & str ) -> sqlx:: Result< ( ) > {
261
let mut tx = super :: begin_write( pool) .await? ;
262
sqlx:: query( " INSERT OR IGNORE INTO retired_usernames (username, retired_ms) VALUES (?, ?)" )
263
.bind ( username)
264
.bind ( now_ms ( ) )
265
.execute ( & mut * tx)
266
.await? ;
267
sqlx:: query( " DELETE FROM accounts WHERE id = ?" ) .bind ( id) .execute ( & mut * tx) .await? ;
268
tx.commit ( ) .await
269
}
270
271
272
273
pub async fn orgs_owned_only_by ( pool : & Pool, account_id : i64 ) -> sqlx:: Result< Vec< String> > {
274
sqlx:: query_scalar(
275
" SELECT o.name FROM organisations o JOIN org_members m ON m.org_id = o.id \
276
WHERE m.account_id = ? AND m.role = 'owner' \
277
AND (SELECT COUNT(*) FROM org_members x WHERE x.org_id = o.id AND x.role = 'owner') = 1 \
278
ORDER BY o.name" ,
279
)
280
.bind ( account_id)
281
.fetch_all ( pool)
282
.await
283
}
284
285
286
pub async fn username_retired ( pool : & Pool, name : & str ) -> sqlx:: Result< bool > {
287
Ok ( sqlx:: query_scalar:: < _ , i64 > ( " SELECT COUNT(*) FROM retired_usernames WHERE username = ?" ) .bind ( name) .fetch_one ( pool) .await? > 0 )
19bb31ff Andy McCall 1 month ago ↶
288
}
289
290
pub async fn touch_seen ( pool : & Pool, id : i64 ) -> sqlx:: Result< ( ) > {
291
sqlx:: query( " UPDATE accounts SET last_seen_ms = ? WHERE id = ?" ) .bind ( now_ms ( ) ) .bind ( id) .execute ( pool) .await? ;
292
Ok ( ( ) )
293
}
294
295
296
297
pub async fn org_by_name ( pool : & Pool, name : & str ) -> sqlx:: Result< Option< Organisation> > {
298
sqlx:: query_as( " SELECT * FROM organisations WHERE name = ?" ) .bind ( name) .fetch_optional ( pool) .await
299
}
300
301
pub async fn orgs ( pool : & Pool) -> sqlx:: Result< Vec< Organisation> > {
302
sqlx:: query_as( " SELECT * FROM organisations ORDER BY name" ) .fetch_all ( pool) .await
303
}
304
a160b98f Andy McCall 1 month ago ↶
305
306
# [ derive ( FromRow ) ]
307
struct Membership {
308
#[sqlx(flatten)]
309
org : Organisation,
310
role : String,
311
}
312
19bb31ff Andy McCall 1 month ago ↶
313
pub async fn orgs_of ( pool : & Pool, account_id : i64 ) -> sqlx:: Result< Vec< ( Organisation, String) > > {
a160b98f Andy McCall 1 month ago ↶
314
let rows: Vec< Membership> = sqlx:: query_as(
315
" SELECT o.*, m.role FROM organisations o JOIN org_members m ON m.org_id = o.id WHERE m.account_id = ? ORDER BY o.name" ,
19bb31ff Andy McCall 1 month ago ↶
316
)
317
.bind ( account_id)
318
.fetch_all ( pool)
319
.await? ;
a160b98f Andy McCall 1 month ago ↶
320
Ok ( rows.into_iter ( ) .map ( | m | ( m.org, m.role) ) .collect ( ) )
19bb31ff Andy McCall 1 month ago ↶
321
}
322
323
pub async fn create_org ( pool : & Pool, name : & str , display_name : & str , owner : i64 ) -> sqlx:: Result< Organisation> {
941abdd6 Andy McCall 1 month ago ↶
324
let mut tx = super :: begin_write( pool) .await? ;
19bb31ff Andy McCall 1 month ago ↶
325
let org: Organisation = sqlx:: query_as( " INSERT INTO organisations (name, display_name, created_ms) VALUES (?, ?, ?) RETURNING *" )
326
.bind ( name)
327
.bind ( display_name)
328
.bind ( now_ms ( ) )
329
.fetch_one ( & mut * tx)
330
.await? ;
331
sqlx:: query( " INSERT INTO org_members (org_id, account_id, role) VALUES (?, ?, 'owner')" )
332
.bind ( org.id)
333
.bind ( owner)
334
.execute ( & mut * tx)
335
.await? ;
336
tx.commit ( ) .await? ;
337
Ok ( org)
338
}
339
340
pub async fn set_org ( pool : & Pool, id : i64 , display_name : & str , description : & str , default_role : & str ) -> sqlx:: Result< ( ) > {
341
sqlx:: query( " UPDATE organisations SET display_name = ?, description = ?, default_role = ? WHERE id = ?" )
342
.bind ( display_name)
343
.bind ( description)
344
.bind ( default_role)
345
.bind ( id)
346
.execute ( pool)
347
.await? ;
348
Ok ( ( ) )
349
}
350
351
352
pub async fn org_members ( pool : & Pool, org_id : i64 ) -> sqlx:: Result< Vec< ( Account, String) > > {
353
let rows: Vec< ( i64 , String) > =
354
sqlx:: query_as( " SELECT account_id, role FROM org_members WHERE org_id = ?" ) .bind ( org_id) .fetch_all ( pool) .await? ;
355
let mut out = Vec :: new( ) ;
356
for ( id, role) in rows {
357
if let Some ( a) = account_by_id ( pool, id) .await? {
358
out.push ( ( a, role) ) ;
359
}
360
}
361
out.sort_by ( | a , b | a.0. username.cmp ( & b.0. username) ) ;
362
Ok ( out)
363
}
364
365
pub async fn org_role ( pool : & Pool, org_id : i64 , account_id : i64 ) -> sqlx:: Result< Option< String> > {
366
sqlx:: query_scalar( " SELECT role FROM org_members WHERE org_id = ? AND account_id = ?" )
367
.bind ( org_id)
368
.bind ( account_id)
369
.fetch_optional ( pool)
370
.await
371
}
372
373
pub async fn set_org_member ( pool : & Pool, org_id : i64 , account_id : i64 , role : & str ) -> sqlx:: Result< ( ) > {
374
sqlx:: query( " INSERT INTO org_members (org_id, account_id, role) VALUES (?, ?, ?) ON CONFLICT (org_id, account_id) DO UPDATE SET role = excluded.role" )
375
.bind ( org_id)
376
.bind ( account_id)
377
.bind ( role)
378
.execute ( pool)
379
.await? ;
380
Ok ( ( ) )
381
}
382
383
pub async fn remove_org_member ( pool : & Pool, org_id : i64 , account_id : i64 ) -> sqlx:: Result< ( ) > {
384
sqlx:: query( " DELETE FROM org_members WHERE org_id = ? AND account_id = ?" ) .bind ( org_id) .bind ( account_id) .execute ( pool) .await? ;
385
Ok ( ( ) )
386
}
387
388
pub async fn teams ( pool : & Pool, org_id : i64 ) -> sqlx:: Result< Vec< Team> > {
389
sqlx:: query_as( " SELECT * FROM teams WHERE org_id = ? ORDER BY name" ) .bind ( org_id) .fetch_all ( pool) .await
390
}
391
392
pub async fn team_by_name ( pool : & Pool, org_id : i64 , name : & str ) -> sqlx:: Result< Option< Team> > {
393
sqlx:: query_as( " SELECT * FROM teams WHERE org_id = ? AND name = ?" ) .bind ( org_id) .bind ( name) .fetch_optional ( pool) .await
394
}
395
396
pub async fn create_team ( pool : & Pool, org_id : i64 , name : & str ) -> sqlx:: Result< Team> {
397
sqlx:: query_as( " INSERT INTO teams (org_id, name) VALUES (?, ?) RETURNING *" ) .bind ( org_id) .bind ( name) .fetch_one ( pool) .await
398
}
399
400
pub async fn delete_team ( pool : & Pool, id : i64 ) -> sqlx:: Result< ( ) > {
401
sqlx:: query( " DELETE FROM teams WHERE id = ?" ) .bind ( id) .execute ( pool) .await? ;
402
Ok ( ( ) )
403
}
404
405
pub async fn team_members ( pool : & Pool, team_id : i64 ) -> sqlx:: Result< Vec< Account> > {
406
sqlx:: query_as( " SELECT a.* FROM accounts a JOIN team_members t ON t.account_id = a.id WHERE t.team_id = ? ORDER BY a.username" )
407
.bind ( team_id)
408
.fetch_all ( pool)
409
.await
410
}
411
412
pub async fn add_team_member ( pool : & Pool, team_id : i64 , account_id : i64 ) -> sqlx:: Result< ( ) > {
413
sqlx:: query( " INSERT OR IGNORE INTO team_members (team_id, account_id) VALUES (?, ?)" )
414
.bind ( team_id)
415
.bind ( account_id)
416
.execute ( pool)
417
.await? ;
418
Ok ( ( ) )
419
}
420
421
pub async fn remove_team_member ( pool : & Pool, team_id : i64 , account_id : i64 ) -> sqlx:: Result< ( ) > {
422
sqlx:: query( " DELETE FROM team_members WHERE team_id = ? AND account_id = ?" ) .bind ( team_id) .bind ( account_id) .execute ( pool) .await? ;
423
Ok ( ( ) )
424
}
425
426
427
cb71e18e Andy McCall 1 month ago ↶
428
429
430
431
432
pub async fn assignable ( pool : & Pool, repo_id : i64 , owner : & str ) -> sqlx:: Result< Vec< String> > {
433
sqlx:: query_scalar(
434
" SELECT username FROM accounts WHERE banned = 0 AND disabled = 0 AND id IN (\
435
SELECT id FROM accounts WHERE username = ?1 \
436
UNION SELECT account_id FROM collaborators WHERE repo_id = ?2 AND account_id IS NOT NULL \
437
UNION SELECT tm.account_id FROM team_members tm JOIN collaborators c ON c.team_id = tm.team_id WHERE c.repo_id = ?2 \
438
UNION SELECT m.account_id FROM org_members m JOIN organisations o ON o.id = m.org_id WHERE o.name = ?1) \
439
ORDER BY username COLLATE NOCASE" ,
440
)
441
.bind ( owner)
442
.bind ( repo_id)
443
.fetch_all ( pool)
444
.await
445
}
446
19bb31ff Andy McCall 1 month ago ↶
447
# [ derive ( Clone, Debug ) ]
448
pub struct Collaborator {
449
pub account : Option< Account> ,
450
pub team : Option< Team> ,
451
pub role : String,
452
}
453
454
pub async fn collaborators ( pool : & Pool, repo_id : i64 ) -> sqlx:: Result< Vec< Collaborator> > {
455
let rows: Vec< ( Option< i64 > , Option< i64 > , String) > =
456
sqlx:: query_as( " SELECT account_id, team_id, role FROM collaborators WHERE repo_id = ?" ) .bind ( repo_id) .fetch_all ( pool) .await? ;
457
let mut out = Vec :: new( ) ;
458
for ( account_id, team_id, role) in rows {
459
let account = match account_id {
460
Some ( id) => account_by_id ( pool, id) .await? ,
461
None => None ,
462
} ;
463
let team = match team_id {
464
Some ( id) => sqlx:: query_as( " SELECT * FROM teams WHERE id = ?" ) .bind ( id) .fetch_optional ( pool) .await? ,
465
None => None ,
466
} ;
467
out.push ( Collaborator { account, team, role } ) ;
468
}
469
Ok ( out)
470
}
471
472
pub async fn set_collaborator ( pool : & Pool, repo_id : i64 , account_id : Option< i64 > , team_id : Option< i64 > , role : & str ) -> sqlx:: Result< ( ) > {
473
if let Some ( a) = account_id {
474
sqlx:: query( " INSERT INTO collaborators (repo_id, account_id, role) VALUES (?, ?, ?) ON CONFLICT (repo_id, account_id) WHERE account_id IS NOT NULL DO UPDATE SET role = excluded.role" )
475
.bind ( repo_id)
476
.bind ( a)
477
.bind ( role)
478
.execute ( pool)
479
.await? ;
480
} else if let Some ( t) = team_id {
481
sqlx:: query( " INSERT INTO collaborators (repo_id, team_id, role) VALUES (?, ?, ?) ON CONFLICT (repo_id, team_id) WHERE team_id IS NOT NULL DO UPDATE SET role = excluded.role" )
482
.bind ( repo_id)
483
.bind ( t)
484
.bind ( role)
485
.execute ( pool)
486
.await? ;
487
}
488
Ok ( ( ) )
489
}
490
491
pub async fn remove_collaborator ( pool : & Pool, repo_id : i64 , account_id : Option< i64 > , team_id : Option< i64 > ) -> sqlx:: Result< ( ) > {
492
sqlx:: query( " DELETE FROM collaborators WHERE repo_id = ? AND ((account_id IS NOT NULL AND account_id = ?) OR (team_id IS NOT NULL AND team_id = ?))" )
493
.bind ( repo_id)
494
.bind ( account_id)
495
.bind ( team_id)
496
.execute ( pool)
497
.await? ;
498
Ok ( ( ) )
499
}
500
501
502
pub async fn collaborator_role ( pool : & Pool, repo_id : i64 , account_id : i64 ) -> sqlx:: Result< Option< String> > {
503
let rows: Vec< String> = sqlx:: query_scalar(
504
" SELECT role FROM collaborators WHERE repo_id = ? AND (account_id = ? OR team_id IN (SELECT team_id FROM team_members WHERE account_id = ?))" ,
505
)
506
.bind ( repo_id)
507
.bind ( account_id)
508
.bind ( account_id)
509
.fetch_all ( pool)
510
.await? ;
511
Ok ( rows.into_iter ( ) .max_by_key ( | r | crate :: auth:: Role:: parse( r) .map ( | r | r as u8 ) .unwrap_or ( 0 ) ) )
512
}
513
514
515
516
pub async fn tokens_of ( pool : & Pool, account_id : i64 ) -> sqlx:: Result< Vec< Token> > {
517
sqlx:: query_as( " SELECT * FROM tokens WHERE account_id = ? ORDER BY created_ms DESC" ) .bind ( account_id) .fetch_all ( pool) .await
518
}
519
520
pub async fn token_by_hash ( pool : & Pool, hash : & str ) -> sqlx:: Result< Option< Token> > {
521
sqlx:: query_as( " SELECT * FROM tokens WHERE hash = ?" ) .bind ( hash) .fetch_optional ( pool) .await
522
}
523
524
pub async fn create_token (
525
pool : & Pool,
526
account_id : i64 ,
527
name : & str ,
528
hash : & str ,
529
scopes : & str ,
530
repos : & str ,
531
expires_ms : Option< i64 > ,
532
) -> sqlx:: Result< Token> {
533
sqlx:: query_as(
534
" INSERT INTO tokens (account_id, name, hash, scopes, repos, created_ms, expires_ms) VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING *" ,
535
)
536
.bind ( account_id)
537
.bind ( name)
538
.bind ( hash)
539
.bind ( scopes)
540
.bind ( repos)
541
.bind ( now_ms ( ) )
542
.bind ( expires_ms)
543
.fetch_one ( pool)
544
.await
545
}
546
547
pub async fn delete_token ( pool : & Pool, account_id : i64 , id : i64 ) -> sqlx:: Result< ( ) > {
548
sqlx:: query( " DELETE FROM tokens WHERE id = ? AND account_id = ?" ) .bind ( id) .bind ( account_id) .execute ( pool) .await? ;
549
Ok ( ( ) )
550
}
551
60f11b71 Andy McCall 1 month ago ↶
552
19bb31ff Andy McCall 1 month ago ↶
553
pub async fn touch_token ( pool : & Pool, id : i64 ) -> sqlx:: Result< ( ) > {
60f11b71 Andy McCall 1 month ago ↶
554
sqlx:: query(
555
" UPDATE tokens SET last_used_ms = ?1, \
556
expires_ms = CASE WHEN renew_ms IS NULL THEN expires_ms ELSE MAX(COALESCE(expires_ms, 0), ?1 + renew_ms) END \
557
WHERE id = ?2" ,
558
)
559
.bind ( now_ms ( ) )
560
.bind ( id)
561
.execute ( pool)
562
.await? ;
563
Ok ( ( ) )
564
}
565
566
567
pub async fn set_token_renewal ( pool : & Pool, id : i64 , renew_ms : i64 ) -> sqlx:: Result< ( ) > {
568
sqlx:: query( " UPDATE tokens SET renew_ms = ? WHERE id = ?" ) .bind ( renew_ms) .bind ( id) .execute ( pool) .await? ;
19bb31ff Andy McCall 1 month ago ↶
569
Ok ( ( ) )
570
}
571
df71c789 Andy McCall 1 month ago ↶
572
573
574
575
576
# [ derive ( Clone, Debug, FromRow ) ]
577
pub struct DeviceCode {
578
pub id : i64 ,
579
pub device_hash : String,
580
pub user_code : String,
581
pub client : String,
582
pub scopes : String,
583
pub created_ms : i64 ,
584
pub expires_ms : i64 ,
585
pub last_poll_ms : i64 ,
586
pub account_id : Option< i64 > ,
587
pub approved_scopes : String,
588
pub denied : bool ,
60f11b71 Andy McCall 1 month ago ↶
589
590
pub requester : String,
df71c789 Andy McCall 1 month ago ↶
591
}
592
593
impl DeviceCode {
594
pub fn scope_list ( & self ) -> Vec< & str > {
595
self .scopes.split ( ' ,' ) .filter ( | s | ! s.is_empty ( ) ) .collect ( )
596
}
597
}
598
599
pub async fn create_device_code (
600
pool : & Pool,
601
device_hash : & str ,
602
user_code : & str ,
603
client : & str ,
604
scopes : & str ,
605
expires_ms : i64 ,
60f11b71 Andy McCall 1 month ago ↶
606
requester : & str ,
df71c789 Andy McCall 1 month ago ↶
607
) -> sqlx:: Result< DeviceCode> {
608
609
sqlx:: query( " DELETE FROM device_codes WHERE expires_ms < ?" ) .bind ( now_ms ( ) ) .execute ( pool) .await? ;
610
sqlx:: query_as(
60f11b71 Andy McCall 1 month ago ↶
611
" INSERT INTO device_codes (device_hash, user_code, client, scopes, created_ms, expires_ms, requester) VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING *" ,
df71c789 Andy McCall 1 month ago ↶
612
)
613
.bind ( device_hash)
614
.bind ( user_code)
615
.bind ( client)
616
.bind ( scopes)
617
.bind ( now_ms ( ) )
618
.bind ( expires_ms)
60f11b71 Andy McCall 1 month ago ↶
619
.bind ( requester)
df71c789 Andy McCall 1 month ago ↶
620
.fetch_one ( pool)
621
.await
622
}
623
624
pub async fn device_by_user_code ( pool : & Pool, user_code : & str ) -> sqlx:: Result< Option< DeviceCode> > {
625
sqlx:: query_as( " SELECT * FROM device_codes WHERE user_code = ?" ) .bind ( user_code) .fetch_optional ( pool) .await
626
}
627
628
pub async fn device_by_hash ( pool : & Pool, device_hash : & str ) -> sqlx:: Result< Option< DeviceCode> > {
629
sqlx:: query_as( " SELECT * FROM device_codes WHERE device_hash = ?" ) .bind ( device_hash) .fetch_optional ( pool) .await
630
}
631
632
633
634
pub async fn approve_device ( pool : & Pool, id : i64 , account_id : i64 , scopes : & str ) -> sqlx:: Result< bool > {
635
let r = sqlx:: query(
636
" UPDATE device_codes SET account_id = ?, approved_scopes = ? WHERE id = ? AND account_id IS NULL AND denied = 0 AND expires_ms >= ?" ,
637
)
638
.bind ( account_id)
639
.bind ( scopes)
640
.bind ( id)
641
.bind ( now_ms ( ) )
642
.execute ( pool)
643
.await? ;
644
Ok ( r.rows_affected ( ) = = 1 )
645
}
646
647
pub async fn deny_device ( pool : & Pool, id : i64 ) -> sqlx:: Result< ( ) > {
648
sqlx:: query( " UPDATE device_codes SET denied = 1 WHERE id = ? AND account_id IS NULL" ) .bind ( id) .execute ( pool) .await? ;
649
Ok ( ( ) )
650
}
651
652
pub async fn device_polled ( pool : & Pool, id : i64 ) -> sqlx:: Result< ( ) > {
653
sqlx:: query( " UPDATE device_codes SET last_poll_ms = ? WHERE id = ?" ) .bind ( now_ms ( ) ) .bind ( id) .execute ( pool) .await? ;
654
Ok ( ( ) )
655
}
656
657
658
659
pub async fn delete_device ( pool : & Pool, id : i64 ) -> sqlx:: Result< bool > {
660
let r = sqlx:: query( " DELETE FROM device_codes WHERE id = ?" ) .bind ( id) .execute ( pool) .await? ;
661
Ok ( r.rows_affected ( ) = = 1 )
662
}
663
19bb31ff Andy McCall 1 month ago ↶
664
665
666
pub async fn identities_of ( pool : & Pool, account_id : i64 ) -> sqlx:: Result< Vec< LinkedIdentity> > {
667
sqlx:: query_as( " SELECT * FROM identities WHERE account_id = ? ORDER BY linked_ms DESC" ) .bind ( account_id) .fetch_all ( pool) .await
668
}
669
670
pub async fn identity_by_key ( pool : & Pool, key : & str ) -> sqlx:: Result< Option< LinkedIdentity> > {
671
sqlx:: query_as( " SELECT * FROM identities WHERE key = ?" ) .bind ( key) .fetch_optional ( pool) .await
672
}
673
674
675
pub async fn identity_accounts ( pool : & Pool) -> sqlx:: Result< Vec< ( String, String) > > {
676
sqlx:: query_as( " SELECT i.key, a.username FROM identities i JOIN accounts a ON a.id = i.account_id" ) .fetch_all ( pool) .await
677
}
678
679
pub async fn link_identity ( pool : & Pool, account_id : i64 , key : & str , name : & str , email : & str , object : & [u8 ]) -> sqlx:: Result< ( ) > {
680
sqlx:: query( " INSERT INTO identities (key, account_id, name, email, object, linked_ms) VALUES (?, ?, ?, ?, ?, ?)" )
681
.bind ( key)
682
.bind ( account_id)
683
.bind ( name)
684
.bind ( email)
685
.bind ( object)
686
.bind ( now_ms ( ) )
687
.execute ( pool)
688
.await? ;
689
Ok ( ( ) )
690
}
691
692
pub async fn unlink_identity ( pool : & Pool, account_id : i64 , key : & str ) -> sqlx:: Result< ( ) > {
693
sqlx:: query( " DELETE FROM identities WHERE key = ? AND account_id = ?" ) .bind ( key) .bind ( account_id) .execute ( pool) .await? ;
694
Ok ( ( ) )
695
}
696
697
pub async fn create_challenge ( pool : & Pool, account_id : i64 , bytes_hex : & str ) -> sqlx:: Result< i64 > {
698
sqlx:: query( " DELETE FROM challenges WHERE created_ms < ?" ) .bind ( now_ms ( ) - 10 * 60 * 1000 ) .execute ( pool) .await? ;
699
let id: i64 = sqlx:: query_scalar( " INSERT INTO challenges (account_id, bytes, created_ms) VALUES (?, ?, ?) RETURNING id" )
700
.bind ( account_id)
701
.bind ( bytes_hex)
702
.bind ( now_ms ( ) )
703
.fetch_one ( pool)
704
.await? ;
705
Ok ( id)
706
}
707
708
709
pub async fn take_challenge ( pool : & Pool, account_id : i64 , id : i64 ) -> sqlx:: Result< Option< String> > {
710
let bytes: Option< String> = sqlx:: query_scalar( " SELECT bytes FROM challenges WHERE id = ? AND account_id = ? AND created_ms >= ?" )
711
.bind ( id)
712
.bind ( account_id)
713
.bind ( now_ms ( ) - 10 * 60 * 1000 )
714
.fetch_optional ( pool)
715
.await? ;
716
sqlx:: query( " DELETE FROM challenges WHERE id = ?" ) .bind ( id) .execute ( pool) .await? ;
717
Ok ( bytes)
718
}
719
720
721
722
pub async fn create_invitation ( pool : & Pool, code : & str , created_by : Option< i64 > ) -> sqlx:: Result< ( ) > {
723
sqlx:: query( " INSERT INTO invitations (code, created_by, created_ms) VALUES (?, ?, ?)" )
724
.bind ( code)
725
.bind ( created_by)
726
.bind ( now_ms ( ) )
727
.execute ( pool)
728
.await? ;
729
Ok ( ( ) )
730
}
731
732
pub async fn invitation_unused ( pool : & Pool, code : & str ) -> sqlx:: Result< bool > {
733
let n: i64 =
734
sqlx:: query_scalar( " SELECT COUNT(*) FROM invitations WHERE code = ? AND used_by IS NULL" ) .bind ( code) .fetch_one ( pool) .await? ;
735
Ok ( n > 0 )
736
}
737
738
pub async fn use_invitation ( pool : & Pool, code : & str , account_id : i64 ) -> sqlx:: Result< ( ) > {
739
sqlx:: query( " UPDATE invitations SET used_by = ? WHERE code = ?" ) .bind ( account_id) .bind ( code) .execute ( pool) .await? ;
740
Ok ( ( ) )
741
}
742
743
pub async fn invitations ( pool : & Pool) -> sqlx:: Result< Vec< ( String, Option< i64 > , i64 ) > > {
744
sqlx:: query_as( " SELECT code, used_by, created_ms FROM invitations ORDER BY created_ms DESC" ) .fetch_all ( pool) .await
745
}
746
747
748
749
pub async fn setting ( pool : & Pool, key : & str ) -> sqlx:: Result< Option< String> > {
750
sqlx:: query_scalar( " SELECT value FROM settings WHERE key = ?" ) .bind ( key) .fetch_optional ( pool) .await
751
}
752
753
pub async fn set_setting ( pool : & Pool, key : & str , value : & str ) -> sqlx:: Result< ( ) > {
754
sqlx:: query( " INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT (key) DO UPDATE SET value = excluded.value" )
755
.bind ( key)
756
.bind ( value)
757
.execute ( pool)
758
.await? ;
759
Ok ( ( ) )
760
}
761
762
763
764
pub async fn audit ( pool : & Pool, actor : Option< & Account> , action : & str , target : & str , detail : & str ) -> sqlx:: Result< ( ) > {
765
sqlx:: query( " INSERT INTO audit (at_ms, actor_id, actor, action, target, detail) VALUES (?, ?, ?, ?, ?, ?)" )
766
.bind ( now_ms ( ) )
767
.bind ( actor.map ( | a | a.id ) )
768
.bind ( actor.map ( | a | a.username.as_str ( ) ) .unwrap_or ( " " ) )
769
.bind ( action)
770
.bind ( target)
771
.bind ( detail)
772
.execute ( pool)
773
.await? ;
774
Ok ( ( ) )
775
}
776
e8e7158b Andy McCall 1 month ago ↶
777
778
pub async fn audit_page ( pool : & Pool, before : Option< i64 > , limit : i64 ) -> sqlx:: Result< Vec< AuditRow> > {
779
sqlx:: query_as( " SELECT id, at_ms, actor, action, target, detail FROM audit WHERE (? IS NULL OR id < ?) ORDER BY id DESC LIMIT ?" )
780
.bind ( before)
781
.bind ( before)
782
.bind ( limit)
783
.fetch_all ( pool)
784
.await
785
}
786
19bb31ff Andy McCall 1 month ago ↶
787
pub async fn audit_rows ( pool : & Pool, limit : i64 ) -> sqlx:: Result< Vec< AuditRow> > {
788
sqlx:: query_as( " SELECT id, at_ms, actor, action, target, detail FROM audit ORDER BY at_ms DESC, id DESC LIMIT ?" )
789
.bind ( limit)
790
.fetch_all ( pool)
791
.await
792
}
793
794
795
796
pub async fn visible_repo_ids ( pool : & Pool, account_id : i64 ) -> sqlx:: Result< Vec< i64 > > {
797
sqlx:: query_scalar(
798
" SELECT r.id FROM repositories r WHERE r.owner IN (SELECT username FROM accounts WHERE id = ?) \
799
OR r.owner IN (SELECT o.name FROM organisations o JOIN org_members m ON m.org_id = o.id WHERE m.account_id = ?) \
800
OR r.id IN (SELECT repo_id FROM collaborators WHERE account_id = ? OR team_id IN (SELECT team_id FROM team_members WHERE account_id = ?))" ,
801
)
802
.bind ( account_id)
803
.bind ( account_id)
804
.bind ( account_id)
805
.bind ( account_id)
806
.fetch_all ( pool)
807
.await
34beb1a7 Andy McCall 1 month ago ↶
808
}
809
810
811
812
813
# [ derive ( Clone, Debug, FromRow ) ]
814
pub struct AccountLink {
815
pub id : i64 ,
816
pub account_id : i64 ,
817
pub kind : String,
818
pub hash : String,
819
pub email : String,
820
pub created_by : Option< i64 > ,
821
pub created_ms : i64 ,
822
pub expires_ms : i64 ,
823
pub used_ms : Option< i64 > ,
824
}
825
826
827
pub async fn create_link (
828
pool : & Pool,
829
account_id : i64 ,
830
kind : & str ,
831
email : & str ,
832
created_by : Option< i64 > ,
833
ttl_ms : i64 ,
834
) -> sqlx:: Result< String> {
835
let secret = crate :: auth:: random_hex( 32 ) ;
836
let now = now_ms ( ) ;
837
sqlx:: query(
838
" INSERT INTO account_links (account_id, kind, hash, email, created_by, created_ms, expires_ms) VALUES (?, ?, ?, ?, ?, ?, ?)" ,
839
)
840
.bind ( account_id)
841
.bind ( kind)
842
.bind ( crate :: auth:: token_hash( & secret) )
843
.bind ( email)
844
.bind ( created_by)
845
.bind ( now)
846
.bind ( now + ttl_ms)
847
.execute ( pool)
848
.await? ;
849
Ok ( secret)
850
}
851
852
853
pub async fn live_link ( pool : & Pool, kind : & str , secret : & str ) -> sqlx:: Result< Option< AccountLink> > {
854
sqlx:: query_as( " SELECT * FROM account_links WHERE kind = ? AND hash = ? AND used_ms IS NULL AND expires_ms > ?" )
855
.bind ( kind)
856
.bind ( crate :: auth:: token_hash( secret) )
857
.bind ( now_ms ( ) )
858
.fetch_optional ( pool)
859
.await
860
}
861
862
863
pub async fn use_link ( pool : & Pool, id : i64 ) -> sqlx:: Result< bool > {
864
let r =
865
sqlx:: query( " UPDATE account_links SET used_ms = ? WHERE id = ? AND used_ms IS NULL" ) .bind ( now_ms ( ) ) .bind ( id) .execute ( pool) .await? ;
866
Ok ( r.rows_affected ( ) = = 1 )
867
}
868
869
870
pub async fn void_links ( pool : & Pool, account_id : i64 , kind : & str ) -> sqlx:: Result< ( ) > {
871
sqlx:: query( " UPDATE account_links SET used_ms = ? WHERE account_id = ? AND kind = ? AND used_ms IS NULL" )
872
.bind ( now_ms ( ) )
873
.bind ( account_id)
874
.bind ( kind)
875
.execute ( pool)
876
.await? ;
877
Ok ( ( ) )
878
}
879
880
881
pub async fn last_link_ms ( pool : & Pool, account_id : i64 , kind : & str , email : Option< & str > ) -> sqlx:: Result< Option< i64 > > {
882
match email {
883
Some ( e) => {
884
sqlx:: query_scalar( " SELECT MAX(created_ms) FROM account_links WHERE account_id = ? AND kind = ? AND email = ?" )
885
.bind ( account_id)
886
.bind ( kind)
887
.bind ( e)
888
.fetch_one ( pool)
889
.await
890
}
891
None => {
892
sqlx:: query_scalar( " SELECT MAX(created_ms) FROM account_links WHERE account_id = ? AND kind = ?" )
893
.bind ( account_id)
894
.bind ( kind)
895
.fetch_one ( pool)
896
.await
897
}
898
}
899
}
900
901
902
pub async fn links_since ( pool : & Pool, account_id : i64 , kind : & str , since_ms : i64 ) -> sqlx:: Result< i64 > {
903
sqlx:: query_scalar( " SELECT COUNT(*) FROM account_links WHERE account_id = ? AND kind = ? AND created_ms >= ?" )
904
.bind ( account_id)
905
.bind ( kind)
906
.bind ( since_ms)
907
.fetch_one ( pool)
908
.await
909
}
910
911
pub async fn set_email_verified ( pool : & Pool, id : i64 , verified : bool ) -> sqlx:: Result< ( ) > {
912
sqlx:: query( " UPDATE accounts SET email_verified = ? WHERE id = ?" ) .bind ( verified) .bind ( id) .execute ( pool) .await? ;
913
Ok ( ( ) )
914
}
915
916
917
pub async fn account_by_verified_email ( pool : & Pool, email : & str ) -> sqlx:: Result< Option< Account> > {
918
let found: Vec< Account> = sqlx:: query_as( " SELECT * FROM accounts WHERE email_verified = 1 AND lower(email) = lower(?) LIMIT 2" )
919
.bind ( email)
920
.fetch_all ( pool)
921
.await? ;
922
Ok ( if found.len ( ) = = 1 { found.into_iter ( ) .next ( ) } else { None } )
a160b98f Andy McCall 1 month ago ↶
923
}
924
925
926
pub async fn set_avatar ( pool : & Pool, kind : & str , id : i64 , value : & str ) -> sqlx:: Result< ( ) > {
927
let table = if kind = = " org" { " organisations" } else { " accounts" } ;
928
sqlx:: query( & format! ( " UPDATE {table} SET avatar = ? WHERE id = ?" ) ) .bind ( value) .bind ( id) .execute ( pool) .await? ;
929
Ok ( ( ) )
19bb31ff Andy McCall 1 month ago ↶
930
}