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