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