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