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