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