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