19bb31ff Andy McCall 1 month ago ↶
1
2
3
4
use super :: settings:: csrf;
5
use super :: { filters, Error, Page, Shell} ;
6
use crate :: auth:: { self , Viewer} ;
7
use crate :: db:: accounts;
8
use crate :: Shared;
9
use askama:: Template;
10
use axum:: extract:: { Form, State} ;
11
use serde:: Deserialize;
12
737de0fa Andy McCall 15 days ago ↶
13
14
15
# [ derive ( Clone, Copy, Debug, PartialEq, Eq ) ]
16
pub enum AdminSection {
17
Overview,
18
Accounts,
19
Registration,
20
Organisations,
21
SignIn,
22
SecurityContact,
23
Mail,
24
Runners,
25
Images,
26
RunnerSettings,
27
Insights,
28
Variables,
29
Secrets,
30
Assistants,
31
Diffusion,
32
Storage,
33
Backups,
34
Audit,
35
}
36
37
38
const ADMIN_GROUPS : & [ ( & str , & [ AdminSection] ) ] = & [
39
( " Overview" , & [ AdminSection:: Overview] ) ,
40
( " People" , & [ AdminSection:: Accounts, AdminSection:: Registration, AdminSection:: Organisations] ) ,
41
( " Sign-in" , & [ AdminSection:: SignIn, AdminSection:: SecurityContact] ) ,
42
( " Mail" , & [ AdminSection:: Mail] ) ,
43
(
44
" Flow" ,
45
& [
46
AdminSection:: Runners,
47
AdminSection:: Images,
48
AdminSection:: RunnerSettings,
49
AdminSection:: Insights,
50
AdminSection:: Variables,
51
AdminSection:: Secrets,
52
] ,
53
) ,
54
( " Integrations" , & [ AdminSection:: Assistants, AdminSection:: Diffusion] ) ,
55
( " Data" , & [ AdminSection:: Storage, AdminSection:: Backups] ) ,
56
( " Audit" , & [ AdminSection:: Audit] ) ,
57
] ;
58
59
60
pub fn admin_groups ( ) -> & 'static [ ( & 'static str , & 'static [ AdminSection] ) ] {
61
ADMIN_GROUPS
62
}
63
64
impl AdminSection {
65
66
pub fn href ( self ) -> & 'static str {
67
match self {
68
AdminSection:: Overview => " /admin" ,
69
AdminSection:: Accounts => " /admin/accounts" ,
70
AdminSection:: Registration => " /admin/registration" ,
71
AdminSection:: Organisations => " /admin/organisations" ,
72
AdminSection:: SignIn => " /admin/signin" ,
73
AdminSection:: SecurityContact => " /admin/security" ,
74
AdminSection:: Mail => " /admin/mail" ,
75
AdminSection:: Runners => " /admin/runners" ,
76
AdminSection:: Images => " /admin/runners/images" ,
77
AdminSection:: RunnerSettings => " /admin/runners/settings" ,
78
AdminSection:: Insights => " /admin/insights" ,
79
AdminSection:: Variables => " /admin/variables" ,
80
AdminSection:: Secrets => " /admin/secrets" ,
81
AdminSection:: Assistants => " /admin/assistants" ,
82
AdminSection:: Diffusion => " /admin/diffusion" ,
83
AdminSection:: Storage => " /admin/storage" ,
84
AdminSection:: Backups => " /admin/backups" ,
85
AdminSection:: Audit => " /admin/audit" ,
86
}
87
}
88
89
90
91
pub fn slug ( self ) -> & 'static str {
92
match self {
93
AdminSection:: Overview => " overview" ,
94
AdminSection:: Accounts => " accounts" ,
95
AdminSection:: Registration => " registration" ,
96
AdminSection:: Organisations => " organisations" ,
97
AdminSection:: SignIn => " signin" ,
98
AdminSection:: SecurityContact => " security" ,
99
AdminSection:: Mail => " mail" ,
100
AdminSection:: Runners => " runners" ,
101
AdminSection:: Images => " images" ,
102
AdminSection:: RunnerSettings => " runner-settings" ,
103
AdminSection:: Insights => " insights" ,
104
AdminSection:: Variables => " variables" ,
105
AdminSection:: Secrets => " secrets" ,
106
AdminSection:: Assistants => " assistants" ,
107
AdminSection:: Diffusion => " diffusion" ,
108
AdminSection:: Storage => " storage" ,
109
AdminSection:: Backups => " backups" ,
110
AdminSection:: Audit => " audit" ,
111
}
112
}
113
114
pub fn title ( self ) -> & 'static str {
115
match self {
116
AdminSection:: Overview => " Overview" ,
117
AdminSection:: Accounts => " Accounts" ,
118
AdminSection:: Registration => " Registration & invitations" ,
119
AdminSection:: Organisations => " Organisations" ,
120
AdminSection:: SignIn => " Sign in with" ,
121
AdminSection:: SecurityContact => " Security contact" ,
122
AdminSection:: Mail => " Mail" ,
123
AdminSection:: Runners => " Runners" ,
124
AdminSection:: Images => " Images" ,
125
AdminSection:: RunnerSettings => " Runner settings" ,
126
AdminSection:: Insights => " Insights" ,
127
AdminSection:: Variables => " Variables" ,
128
AdminSection:: Secrets => " Secrets" ,
129
AdminSection:: Assistants => " AI assistants" ,
130
AdminSection:: Diffusion => " Diffusion" ,
131
AdminSection:: Storage => " Storage & maintenance" ,
132
AdminSection:: Backups => " Backups" ,
133
AdminSection:: Audit => " Audit log" ,
134
}
135
}
136
137
138
fn of_path ( path : & str ) -> Option< AdminSection> {
139
ADMIN_GROUPS .iter ( ) .flat_map ( | ( _, pages ) | pages.iter ( ) .copied ( ) ) .find ( | s | s.href ( ) = = path )
140
}
141
}
142
19bb31ff Andy McCall 1 month ago ↶
143
# [ derive ( Template ) ]
144
# [ template ( path = " admin.html" ) ]
145
pub struct AdminPage {
146
shell : Shell,
737de0fa Andy McCall 15 days ago ↶
147
148
section : AdminSection,
19bb31ff Andy McCall 1 month ago ↶
149
accounts : Vec< accounts:: Account> ,
150
orgs : Vec< accounts:: Organisation> ,
151
mode : String,
152
invitations : Vec< ( String, Option< i64 > , i64 ) > ,
153
audit : Vec< accounts:: AuditRow> ,
154
repos : usize ,
155
notice : Option< String> ,
3556d08e Andy McCall 1 month ago ↶
156
157
158
159
mail_smtp : String,
160
mail_from : String,
161
mail_enabled : bool ,
cd46e56b Andy McCall 22 days ago ↶
162
163
mail : crate:: mail:: Choice,
3556d08e Andy McCall 1 month ago ↶
164
my_email : String,
e8e7158b Andy McCall 1 month ago ↶
165
166
audit_older : Option< i64 > ,
e71846aa Andy McCall 1 month ago ↶
167
168
assistants : super:: assistants:: Panel,
2a83f530 Andy McCall 1 month ago ↶
169
170
variables : super:: variables:: Panel,
4ac01a73 Andy McCall 1 month ago ↶
171
172
secrets : super:: secrets:: Panel,
83940d53 Andy McCall 1 month ago ↶
173
174
providers : Vec< super:: signin:: ProviderView> ,
3dd6582d Andy McCall 20 days ago ↶
175
176
security_contact : String,
177
security_policy : String,
2cbf6f34 Andy McCall 20 days ago ↶
178
179
security_email : String,
180
security_page : String,
181
security_default : & 'static str ,
19bb31ff Andy McCall 1 month ago ↶
182
}
183
3dd6582d Andy McCall 20 days ago ↶
184
pub ( super ) fn require_admin ( viewer : & Viewer, shell : & Shell) -> Result< accounts:: Account, Error> {
19bb31ff Andy McCall 1 month ago ↶
185
match viewer.account ( ) {
186
Some ( a) if a.is_admin => Ok ( a.clone ( ) ) ,
187
_ => Err ( Error:: not_found( shell.clone ( ) ) ) ,
188
}
189
}
190
737de0fa Andy McCall 15 days ago ↶
191
pub ( super ) async fn admin_page (
192
app : & Shared,
193
shell : Shell,
194
section : AdminSection,
195
notice : Option< String> ,
196
) -> Result< Page< AdminPage> , Error> {
197
admin_page_at ( app, shell, section, notice, None ) .await
e8e7158b Andy McCall 1 month ago ↶
198
}
199
200
201
const AUDIT_PAGE : i64 = 50 ;
202
737de0fa Andy McCall 15 days ago ↶
203
async fn admin_page_at (
204
app : & Shared,
205
shell : Shell,
206
section : AdminSection,
207
notice : Option< String> ,
208
audit_before : Option< i64 > ,
209
) -> Result< Page< AdminPage> , Error> {
19bb31ff Andy McCall 1 month ago ↶
210
let e = | e | Error:: from_db( shell.clone ( ) , e) ;
211
let accounts_list = accounts:: accounts( & app.db) .await.map_err ( e) ? ;
212
let orgs = accounts:: orgs( & app.db) .await.map_err ( e) ? ;
213
let mode = super :: session:: registration_mode( app) .await;
214
let invitations = accounts:: invitations( & app.db) .await.map_err ( e) ? ;
e8e7158b Andy McCall 1 month ago ↶
215
let mut audit = accounts:: audit_page( & app.db, audit_before, AUDIT_PAGE + 1 ) .await.map_err ( e) ? ;
216
let audit_older = if audit.len ( ) as i64 > AUDIT_PAGE {
217
audit.truncate ( AUDIT_PAGE as usize ) ;
218
audit.last ( ) .map ( | r | r.id )
219
} else {
220
None
221
} ;
19bb31ff Andy McCall 1 month ago ↶
222
let repos = crate :: db:: repos:: list( & app.db) .await.map_err ( e) ? .len ( ) ;
3556d08e Andy McCall 1 month ago ↶
223
let mail = crate :: mail:: settings( app) ;
224
let my_email = match shell.user.as_deref ( ) {
225
Some ( u) => accounts:: account_by_name( & app.db, u) .await.map_err ( e) ? .map ( | a | a.email ) .unwrap_or_default ( ) ,
226
None => String :: new( ) ,
227
} ;
e71846aa Andy McCall 1 month ago ↶
228
let assistants = super :: assistants:: panel( app, & site_assistants ( ) ) .await.map_err ( e) ? ;
2a83f530 Andy McCall 1 month ago ↶
229
let variables = super :: variables:: panel( app, & site_assistants ( ) ) .await.map_err ( e) ? ;
4ac01a73 Andy McCall 1 month ago ↶
230
let secrets = super :: secrets:: panel( app, & site_assistants ( ) ) .await.map_err ( e) ? ;
83940d53 Andy McCall 1 month ago ↶
231
let providers = super :: signin:: provider_views( app) .await.map_err ( e) ? ;
3dd6582d Andy McCall 20 days ago ↶
232
let ( security_contact, security_policy) = super :: security_txt:: settings( app) .await;
2cbf6f34 Andy McCall 20 days ago ↶
233
let ( security_email, security_page) = super :: security_txt:: page_settings( app) .await;
3556d08e Andy McCall 1 month ago ↶
234
Ok ( Page( AdminPage {
737de0fa Andy McCall 15 days ago ↶
235
section,
83940d53 Andy McCall 1 month ago ↶
236
providers,
3dd6582d Andy McCall 20 days ago ↶
237
security_contact,
238
security_policy,
2cbf6f34 Andy McCall 20 days ago ↶
239
security_email,
240
security_page,
241
security_default: super :: security_txt:: DEFAULT_PAGE ,
e71846aa Andy McCall 1 month ago ↶
242
assistants,
2a83f530 Andy McCall 1 month ago ↶
243
variables,
4ac01a73 Andy McCall 1 month ago ↶
244
secrets,
3556d08e Andy McCall 1 month ago ↶
245
shell,
246
accounts: accounts_list,
247
orgs,
248
mode,
249
invitations,
250
audit,
251
repos,
252
notice,
cd46e56b Andy McCall 22 days ago ↶
253
mail: crate :: mail:: describe( & mail.smtp) ,
3556d08e Andy McCall 1 month ago ↶
254
mail_smtp: crate :: mail:: masked( & mail.smtp) ,
255
mail_from: mail.from.clone ( ) ,
256
mail_enabled: ! mail.smtp.is_empty ( ) & & ! mail.from.is_empty ( ) ,
257
my_email,
e8e7158b Andy McCall 1 month ago ↶
258
audit_older,
3556d08e Andy McCall 1 month ago ↶
259
} ) )
260
}
261
cd46e56b Andy McCall 22 days ago ↶
262
impl AdminPage {
263
fn mail_services ( & self ) -> & 'static [ ( & 'static str , & 'static str ) ] {
264
crate :: mail:: SERVICES
265
}
266
}
267
3556d08e Andy McCall 1 month ago ↶
268
# [ derive ( Deserialize ) ]
269
pub struct MailForm {
270
#[serde(rename = "_csrf")]
271
csrf_token : String,
272
#[serde(default)]
273
smtp : String,
274
#[serde(default)]
275
from : String,
cd46e56b Andy McCall 22 days ago ↶
276
277
278
#[serde(default)]
279
service : String,
280
#[serde(default)]
281
region : String,
282
#[serde(default)]
283
user : String,
284
#[serde(default)]
285
secret : String,
286
#[serde(default)]
287
host : String,
288
#[serde(default)]
289
port : String,
290
#[serde(default)]
291
security : String,
3556d08e Andy McCall 1 month ago ↶
292
}
293
294
295
296
297
pub async fn set_mail ( State( app ) : State< Shared> , viewer : Viewer, Form( f ) : Form< MailForm> ) -> Result< Page< AdminPage> , Error> {
298
let shell = Shell:: new( & app, " /admin" , & viewer) ;
299
let me = require_admin ( & viewer, & shell) ? ;
300
csrf ( & viewer, & f.csrf_token, & shell) ? ;
301
let current = crate :: mail:: settings( & app) ;
cd46e56b Andy McCall 22 days ago ↶
302
303
let smtp = match f.service.as_str ( ) {
304
" off" => String :: new( ) ,
305
" " | " url" => {
306
let smtp = f.smtp.trim ( ) ;
307
if ! current.smtp.is_empty ( ) & & smtp = = crate :: mail:: masked( & current.smtp) {
308
current.smtp.clone ( )
309
} else {
310
smtp.to_string ( )
311
}
312
}
313
_ => {
314
let asked = crate :: mail:: Asked {
315
service: f.service.clone ( ) ,
316
region: f.region.clone ( ) ,
317
user: f.user.clone ( ) ,
318
secret: f.secret.trim ( ) .to_string ( ) ,
319
host: f.host.clone ( ) ,
320
port: f.port.clone ( ) ,
321
security: f.security.clone ( ) ,
322
} ;
323
match crate :: mail:: compose( & asked, & current.smtp) {
324
Ok ( url) => url,
737de0fa Andy McCall 15 days ago ↶
325
Err ( why) => return admin_page ( & app, shell, AdminSection:: Mail, Some ( why) ) .await,
cd46e56b Andy McCall 22 days ago ↶
326
}
327
}
328
} ;
3556d08e Andy McCall 1 month ago ↶
329
let from = f.from.trim ( ) .to_string ( ) ;
330
if ! smtp.is_empty ( ) & & ! ( smtp.starts_with ( " smtp://" ) | | smtp.starts_with ( " smtps://" ) | | smtp.starts_with ( " file://" ) ) {
331
return admin_page (
332
& app,
333
shell,
737de0fa Andy McCall 15 days ago ↶
334
AdminSection:: Mail,
3556d08e Andy McCall 1 month ago ↶
335
Some ( " The SMTP URL starts with smtp:// or smtps:// (file:///dir writes messages to a directory, for testing)." .into ( ) ) ,
336
)
337
.await;
338
}
339
if ! smtp.is_empty ( ) & & from.is_empty ( ) {
737de0fa Andy McCall 15 days ago ↶
340
return admin_page ( & app, shell, AdminSection:: Mail, Some ( " A From address is needed: OxygenHub <[email protected] >." .into ( ) ) )
341
.await;
3556d08e Andy McCall 1 month ago ↶
342
}
343
crate :: mail:: save( & app, & smtp, & from) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
344
accounts:: audit( & app.db, Some ( & me) , " admin.mail" , if smtp.is_empty ( ) { " off" } else { " on" } , & from) .await.ok ( ) ;
737de0fa Andy McCall 15 days ago ↶
345
admin_page ( & app, shell, AdminSection:: Mail, Some ( if smtp.is_empty ( ) { " Mail is off." .into ( ) } else { " Mail settings saved." .into ( ) } ) )
346
.await
3556d08e Andy McCall 1 month ago ↶
347
}
348
349
350
pub async fn test_mail (
351
State( app ) : State< Shared> ,
352
viewer : Viewer,
353
Form( f ) : Form< super:: session:: CsrfForm> ,
354
) -> Result< Page< AdminPage> , Error> {
355
let shell = Shell:: new( & app, " /admin" , & viewer) ;
356
let me = require_admin ( & viewer, & shell) ? ;
357
csrf ( & viewer, & f.csrf, & shell) ? ;
358
if ! crate :: mail:: enabled( & app) {
737de0fa Andy McCall 15 days ago ↶
359
return admin_page ( & app, shell, AdminSection:: Mail, Some ( " Mail is off; save an SMTP URL and a From address first." .into ( ) ) ) .await;
3556d08e Andy McCall 1 month ago ↶
360
}
361
if me.email.is_empty ( ) {
737de0fa Andy McCall 15 days ago ↶
362
return admin_page ( & app, shell, AdminSection:: Mail, Some ( " Your account has no email address to send the test to." .into ( ) ) ) .await;
3556d08e Andy McCall 1 month ago ↶
363
}
cd46e56b Andy McCall 22 days ago ↶
364
let base = app.config.base_url ( ) .to_string ( ) ;
365
let notice = match crate :: mail:: send_letter(
3556d08e Andy McCall 1 month ago ↶
366
& app,
367
me.name ( ) ,
368
& me.email,
369
" Test message" ,
cd46e56b Andy McCall 22 days ago ↶
370
" This is a test message from your hub's mail settings. If you can read it, mail works: verification links, password resets and notifications will reach people." ,
371
Some ( crate :: mail:: Action { label: " Open the hub" , url: & base } ) ,
372
None ,
3556d08e Andy McCall 1 month ago ↶
373
)
374
.await
375
{
376
Ok ( ( ) ) => format! ( " Sent a test message to {} ." , me.email) ,
377
Err ( e) => format! ( " Sending failed: {e} " ) ,
378
} ;
737de0fa Andy McCall 15 days ago ↶
379
admin_page ( & app, shell, AdminSection:: Mail, Some ( notice) ) .await
19bb31ff Andy McCall 1 month ago ↶
380
}
381
e8e7158b Andy McCall 1 month ago ↶
382
# [ derive ( Deserialize ) ]
383
pub struct IndexQuery {
384
audit_before : Option< i64 > ,
385
}
386
387
pub async fn index (
388
State( app ) : State< Shared> ,
389
viewer : Viewer,
390
axum:: extract:: Query( q ) : axum:: extract:: Query< IndexQuery> ,
391
) -> Result< Page< AdminPage> , Error> {
19bb31ff Andy McCall 1 month ago ↶
392
let shell = Shell:: new( & app, " /admin" , & viewer) ;
737de0fa Andy McCall 15 days ago ↶
393
require_admin ( & viewer, & shell) ? ;
394
admin_page_at ( & app, shell, AdminSection:: Overview, None , q.audit_before) .await
395
}
396
397
398
399
400
pub async fn section (
401
State( app ) : State< Shared> ,
402
viewer : Viewer,
403
uri : axum:: http:: Uri,
404
axum:: extract:: Query( q ) : axum:: extract:: Query< IndexQuery> ,
405
) -> Result< Page< AdminPage> , Error> {
406
let shell = Shell:: new( & app, uri.path ( ) , & viewer) ;
19bb31ff Andy McCall 1 month ago ↶
407
require_admin ( & viewer, & shell) ? ;
737de0fa Andy McCall 15 days ago ↶
408
let section = AdminSection:: of_path( uri.path ( ) ) .ok_or_else ( | | Error:: not_found( shell.clone ( ) ) ) ? ;
409
admin_page_at ( & app, shell, section, None , q.audit_before) .await
19bb31ff Andy McCall 1 month ago ↶
410
}
411
412
# [ derive ( Deserialize ) ]
413
pub struct ModeForm {
414
#[serde(rename = "_csrf")]
415
csrf_token : String,
416
mode : String,
417
}
418
419
pub async fn set_mode ( State( app ) : State< Shared> , viewer : Viewer, Form( f ) : Form< ModeForm> ) -> Result< Page< AdminPage> , Error> {
420
let shell = Shell:: new( & app, " /admin" , & viewer) ;
421
let me = require_admin ( & viewer, & shell) ? ;
422
csrf ( & viewer, & f.csrf_token, & shell) ? ;
423
if ! matches! ( f.mode.as_str ( ) , " open" | " invite" | " closed" ) {
424
return Err ( Error:: bad( shell, " mode is open, invite or closed" ) ) ;
425
}
426
accounts:: set_setting( & app.db, " registration_mode" , & f.mode) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
427
accounts:: audit( & app.db, Some ( & me) , " admin.registration" , & f.mode, " " ) .await.ok ( ) ;
737de0fa Andy McCall 15 days ago ↶
428
admin_page ( & app, shell, AdminSection:: Registration, Some ( format! ( " Registration is {} ." , f.mode) ) ) .await
19bb31ff Andy McCall 1 month ago ↶
429
}
430
431
pub async fn invite ( State( app ) : State< Shared> , viewer : Viewer, Form( f ) : Form< super:: session:: CsrfForm> ) -> Result< Page< AdminPage> , Error> {
432
let shell = Shell:: new( & app, " /admin" , & viewer) ;
433
let me = require_admin ( & viewer, & shell) ? ;
434
csrf ( & viewer, & f.csrf, & shell) ? ;
435
let code = auth:: random_hex( 12 ) ;
436
accounts:: create_invitation( & app.db, & code, Some ( me.id) ) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
437
accounts:: audit( & app.db, Some ( & me) , " admin.invite" , & code, " " ) .await.ok ( ) ;
737de0fa Andy McCall 15 days ago ↶
438
admin_page (
439
& app,
440
shell,
441
AdminSection:: Registration,
442
Some ( format! ( " Invitation created: {} /register?invite={code} " , app.config.base_url ( ) ) ) ,
443
)
444
.await
19bb31ff Andy McCall 1 month ago ↶
445
}
446
447
# [ derive ( Deserialize ) ]
448
pub struct AccountForm {
449
#[serde(rename = "_csrf")]
450
csrf_token : String,
451
username : String,
452
action : String,
6a178055 Andy McCall 1 month ago ↶
453
454
#[serde(default)]
455
confirm : String,
19bb31ff Andy McCall 1 month ago ↶
456
}
457
458
pub async fn account ( State( app ) : State< Shared> , viewer : Viewer, Form( f ) : Form< AccountForm> ) -> Result< Page< AdminPage> , Error> {
459
let shell = Shell:: new( & app, " /admin" , & viewer) ;
460
let me = require_admin ( & viewer, & shell) ? ;
461
csrf ( & viewer, & f.csrf_token, & shell) ? ;
462
let Some ( target) = accounts:: account_by_name( & app.db, f.username.trim ( ) ) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? else {
737de0fa Andy McCall 15 days ago ↶
463
return admin_page ( & app, shell, AdminSection:: Accounts, Some ( " No such account." .into ( ) ) ) .await;
19bb31ff Andy McCall 1 month ago ↶
464
} ;
6a178055 Andy McCall 1 month ago ↶
465
if target.id = = me.id & & matches! ( f.action.as_str ( ) , " ban" | " demote" | " disable" | " delete" ) {
737de0fa Andy McCall 15 days ago ↶
466
return admin_page ( & app, shell, AdminSection:: Accounts, Some ( " Not on yourself." .into ( ) ) ) .await;
19bb31ff Andy McCall 1 month ago ↶
467
}
468
let e = | e | Error:: from_db( shell.clone ( ) , e) ;
6a178055 Andy McCall 1 month ago ↶
469
470
if target.is_admin & & matches! ( f.action.as_str ( ) , " disable" | " delete" ) {
471
let others =
472
accounts:: accounts( & app.db) .await.map_err ( e) ? .into_iter ( ) .filter ( | a | a.is_admin & & ! a.blocked ( ) & & a.id ! = target.id ) .count ( ) ;
473
if others = = 0 {
737de0fa Andy McCall 15 days ago ↶
474
return admin_page (
475
& app,
476
shell,
477
AdminSection:: Accounts,
478
Some ( format! ( " {} is the last admin who can sign in." , target.username) ) ,
479
)
480
.await;
6a178055 Andy McCall 1 month ago ↶
481
}
482
}
483
if f.action = = " delete" {
484
if f.confirm.trim ( ) ! = target.username {
737de0fa Andy McCall 15 days ago ↶
485
return admin_page ( & app, shell, AdminSection:: Accounts, Some ( format! ( " Not deleted: type {} to confirm." , target.username) ) )
486
.await;
6a178055 Andy McCall 1 month ago ↶
487
}
488
let repos = crate :: db:: repos:: list_by_owner( & app.db, & target.username) .await.map_err ( e) ? ;
489
let orgs = accounts:: orgs_owned_only_by( & app.db, target.id) .await.map_err ( e) ? ;
490
if ! repos.is_empty ( ) | | ! orgs.is_empty ( ) {
491
let mut held: Vec< String> = repos.iter ( ) .map ( | r | r.slug ( ) ) .collect ( ) ;
492
held.extend ( orgs.iter ( ) .map ( | o | format! ( " organisation {o} (sole owner)" ) ) ) ;
493
return admin_page (
494
& app,
495
shell,
737de0fa Andy McCall 15 days ago ↶
496
AdminSection:: Accounts,
6a178055 Andy McCall 1 month ago ↶
497
Some ( format! ( " Not deleted: {} still owns {} . Transfer or delete them first." , target.username, held.join ( " , " ) ) ) ,
498
)
499
.await;
500
}
501
accounts:: delete_account( & app.db, target.id, & target.username) .await.map_err ( e) ? ;
502
accounts:: audit( & app.db, Some ( & me) , " admin.account.delete" , & target.username, " " ) .await.ok ( ) ;
737de0fa Andy McCall 15 days ago ↶
503
return admin_page ( & app, shell, AdminSection:: Accounts, Some ( format! ( " {} deleted; the name stays retired." , target.username) ) )
504
.await;
6a178055 Andy McCall 1 month ago ↶
505
}
34beb1a7 Andy McCall 1 month ago ↶
506
if f.action = = " reset_link" {
6a178055 Andy McCall 1 month ago ↶
507
if target.blocked ( ) {
737de0fa Andy McCall 15 days ago ↶
508
return admin_page ( & app, shell, AdminSection:: Accounts, Some ( " A banned or disabled account cannot be reset." .into ( ) ) ) .await;
34beb1a7 Andy McCall 1 month ago ↶
509
}
510
let url = super :: recovery:: admin_reset_link( & app, & me, & target) .await.map_err ( e) ? ;
511
return admin_page (
512
& app,
513
shell,
737de0fa Andy McCall 15 days ago ↶
514
AdminSection:: Accounts,
34beb1a7 Andy McCall 1 month ago ↶
515
Some ( format! ( " Reset link for {} (works once, for 24 hours; shown only now): {url} " , target.username) ) ,
516
)
517
.await;
518
}
19bb31ff Andy McCall 1 month ago ↶
519
match f.action.as_str ( ) {
520
" ban" => accounts:: set_banned( & app.db, target.id, true ) .await.map_err ( e) ? ,
521
" unban" => accounts:: set_banned( & app.db, target.id, false ) .await.map_err ( e) ? ,
6a178055 Andy McCall 1 month ago ↶
522
" disable" => accounts:: set_disabled( & app.db, target.id, true ) .await.map_err ( e) ? ,
523
" enable" => accounts:: set_disabled( & app.db, target.id, false ) .await.map_err ( e) ? ,
19bb31ff Andy McCall 1 month ago ↶
524
" promote" => accounts:: set_admin( & app.db, target.id, true ) .await.map_err ( e) ? ,
525
" demote" => accounts:: set_admin( & app.db, target.id, false ) .await.map_err ( e) ? ,
526
_ => return Err ( Error:: bad( shell, " unknown action" ) ) ,
527
}
528
accounts:: audit( & app.db, Some ( & me) , & format! ( " admin.account.{} " , f.action) , & target.username, " " ) .await.ok ( ) ;
737de0fa Andy McCall 15 days ago ↶
529
admin_page ( & app, shell, AdminSection:: Accounts, Some ( format! ( " {} : {} ." , target.username, f.action) ) ) .await
19bb31ff Andy McCall 1 month ago ↶
530
}
8fb35df7 Andy McCall 1 month ago ↶
531
532
533
534
535
pub struct StorageRow {
536
pub slug : String,
166f6e9b Andy McCall 16 days ago
537
538
pub drop : bool ,
8fb35df7 Andy McCall 1 month ago ↶
539
pub bytes : u64 ,
540
541
pub loose : usize ,
542
pub packs : usize ,
543
}
544
545
# [ derive ( Template ) ]
546
# [ template ( path = " admin_storage.html" ) ]
547
pub struct StoragePage {
548
shell : Shell,
549
rows : Vec< StorageRow> ,
550
total : u64 ,
551
notice : Option< String> ,
552
error : Option< String> ,
553
}
554
555
556
fn footprint ( dir : & std:: path:: Path) -> ( u64 , usize , usize ) {
557
let ( mut bytes, mut loose, mut packs) = ( 0 u64 , 0 usize , 0 usize ) ;
558
let mut stack = vec! [ dir.to_path_buf ( ) ] ;
559
while let Some ( d) = stack.pop ( ) {
560
let Ok ( rd) = std:: fs:: read_dir( & d) else { continue } ;
561
for e in rd.flatten ( ) {
562
let Ok ( ft) = e.file_type ( ) else { continue } ;
563
if ft.is_dir ( ) {
564
stack.push ( e.path ( ) ) ;
565
} else if ft.is_file ( ) {
566
bytes + = e.metadata ( ) .map ( | m | m.len ( ) ) .unwrap_or ( 0 ) ;
567
let p = e.path ( ) ;
568
if p.components ( ) .any ( | c | c.as_os_str ( ) = = " loose" ) {
569
loose + = 1 ;
570
} else if p.extension ( ) .is_some_and ( | x | x = = " pack" ) {
571
packs + = 1 ;
572
}
573
}
574
}
575
}
576
( bytes, loose, packs)
577
}
578
579
async fn storage_page ( app : & Shared, shell : Shell, notice : Option< String> , error : Option< String> ) -> Result< Page< StoragePage> , Error> {
166f6e9b Andy McCall 16 days ago
580
581
let repos = crate :: db:: repos:: list_with_drops( & app.db) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
582
let dirs: Vec< ( String, bool , std:: path:: PathBuf) > = repos
583
.iter ( )
584
.filter_map ( | r | crate :: repos:: RepoName:: new( & r.owner, & r.name) .map ( | n | ( n.to_string ( ) , r.is_drop ( ) , app.repos.dir ( & n) ) ) )
585
.collect ( ) ;
8fb35df7 Andy McCall 1 month ago ↶
586
let mut rows = tokio:: task:: spawn_blocking( move | | {
587
dirs.into_iter ( )
166f6e9b Andy McCall 16 days ago
588
.map ( | ( slug , drop , dir ) | {
8fb35df7 Andy McCall 1 month ago ↶
589
let ( bytes, loose, packs) = footprint ( & dir) ;
166f6e9b Andy McCall 16 days ago
590
StorageRow { slug, drop, bytes, loose, packs }
8fb35df7 Andy McCall 1 month ago ↶
591
} )
592
.collect:: < Vec< _ > > ( )
593
} )
594
.await
595
.unwrap_or_default ( ) ;
596
rows.sort_by_key ( | r | std:: cmp:: Reverse( r.bytes) ) ;
597
let total = rows.iter ( ) .map ( | r | r.bytes ) .sum ( ) ;
598
Ok ( Page( StoragePage { shell, rows, total, notice, error } ) )
599
}
600
601
pub async fn storage ( State( app ) : State< Shared> , viewer : Viewer) -> Result< Page< StoragePage> , Error> {
602
let shell = Shell:: new( & app, " /admin/storage" , & viewer) ;
603
require_admin ( & viewer, & shell) ? ;
604
storage_page ( & app, shell, None , None ) .await
605
}
606
607
# [ derive ( Deserialize ) ]
608
pub struct MaintainForm {
609
#[serde(rename = "_csrf")]
610
csrf_token : String,
611
612
repo : String,
613
}
614
615
616
617
pub async fn maintain ( State( app ) : State< Shared> , viewer : Viewer, Form( f ) : Form< MaintainForm> ) -> Result< Page< StoragePage> , Error> {
618
let shell = Shell:: new( & app, " /admin/storage" , & viewer) ;
619
let me = require_admin ( & viewer, & shell) ? ;
620
csrf ( & viewer, & f.csrf_token, & shell) ? ;
621
let names: Vec< crate:: repos:: RepoName> = if f.repo = = " *" {
166f6e9b Andy McCall 16 days ago
622
crate :: db:: repos:: list_with_drops( & app.db)
8fb35df7 Andy McCall 1 month ago ↶
623
.await
624
.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ?
625
.iter ( )
626
.filter_map ( | r | crate :: repos:: RepoName:: new( & r.owner, & r.name) )
627
.collect ( )
628
} else {
629
match crate :: repos:: RepoName:: parse( & f.repo) {
630
Some ( n) if app.repos.exists ( & n) => vec! [ n] ,
631
_ => return storage_page ( & app, shell, None , Some ( format! ( " No repository {} ." , f.repo) ) ) .await,
632
}
633
} ;
634
let ( mut packed, mut pruned, mut freed, mut failed) = ( 0 usize , 0 usize , 0 u64 , Vec :: new( ) ) ;
635
for name in & names {
636
match app.repos.write ( name, | r , _| r.gc ( false ) .map_err ( crate :: repos:: RepoError:: Oxy) ) .await {
637
Ok ( rep) => {
638
packed + = rep.packed.objects;
639
pruned + = rep.pruned.objects;
640
freed + = rep.pruned.bytes + rep.packed.raw_bytes.saturating_sub ( rep.packed.packed_bytes) ;
641
}
642
Err ( e) => failed.push ( format! ( " {name} : {e} " ) ) ,
643
}
644
}
645
let summary = format! (
646
" Maintained {} repositor{} : {packed} loose objects packed, {pruned} unreachable objects pruned, about {} saved." ,
647
names.len ( ) ,
648
if names.len ( ) = = 1 { " y" } else { " ies" } ,
649
crate :: render:: bytes( freed)
650
) ;
651
accounts:: audit( & app.db, Some ( & me) , " admin.gc" , & f.repo, & summary) .await.ok ( ) ;
652
let error = ( ! failed.is_empty ( ) ) .then ( | | failed.join ( " ; " ) ) ;
653
storage_page ( & app, shell, Some ( summary) , error) .await
654
}
e71846aa Andy McCall 1 month ago ↶
655
2a83f530 Andy McCall 1 month ago ↶
656
fn site_assistants ( ) -> crate:: levels:: At {
657
crate :: levels:: At:: site( )
e71846aa Andy McCall 1 month ago ↶
658
}
659
660
661
pub async fn assistants (
662
State( app ) : State< Shared> ,
663
viewer : Viewer,
664
Form( f ) : Form< super:: assistants:: AssistantForm> ,
665
) -> Result< Page< AdminPage> , Error> {
666
let shell = Shell:: new( & app, " /admin" , & viewer) ;
667
require_admin ( & viewer, & shell) ? ;
668
csrf ( & viewer, & f.csrf_token, & shell) ? ;
669
let outcome = super :: assistants:: apply( & app, & viewer, & site_assistants ( ) , f) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
737de0fa Andy McCall 15 days ago ↶
670
admin_page ( & app, shell, AdminSection:: Assistants, Some ( outcome.unwrap_or_else ( | e | e ) ) ) .await
e71846aa Andy McCall 1 month ago ↶
671
}
2a83f530 Andy McCall 1 month ago ↶
672
673
674
pub async fn variables (
675
State( app ) : State< Shared> ,
676
viewer : Viewer,
677
Form( f ) : Form< super:: variables:: VariableForm> ,
678
) -> Result< Page< AdminPage> , Error> {
679
let shell = Shell:: new( & app, " /admin" , & viewer) ;
680
require_admin ( & viewer, & shell) ? ;
681
csrf ( & viewer, & f.csrf_token, & shell) ? ;
682
let outcome = super :: variables:: apply( & app, & viewer, & site_assistants ( ) , f) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
737de0fa Andy McCall 15 days ago ↶
683
admin_page ( & app, shell, AdminSection:: Variables, Some ( outcome.unwrap_or_else ( | e | e ) ) ) .await
2a83f530 Andy McCall 1 month ago ↶
684
}
4ac01a73 Andy McCall 1 month ago ↶
685
686
687
pub async fn secrets (
688
State( app ) : State< Shared> ,
689
viewer : Viewer,
690
Form( f ) : Form< super:: secrets:: SecretForm> ,
691
) -> Result< Page< AdminPage> , Error> {
692
let shell = Shell:: new( & app, " /admin" , & viewer) ;
693
require_admin ( & viewer, & shell) ? ;
694
csrf ( & viewer, & f.csrf_token, & shell) ? ;
695
let outcome = super :: secrets:: apply( & app, & viewer, & site_assistants ( ) , f) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
737de0fa Andy McCall 15 days ago ↶
696
admin_page ( & app, shell, AdminSection:: Secrets, Some ( outcome.unwrap_or_else ( | e | e ) ) ) .await
4ac01a73 Andy McCall 1 month ago ↶
697
}
83940d53 Andy McCall 1 month ago ↶
698
699
700
pub async fn signin (
701
State( app ) : State< Shared> ,
702
viewer : Viewer,
703
Form( f ) : Form< super:: signin:: ProviderForm> ,
704
) -> Result< Page< AdminPage> , Error> {
705
let shell = Shell:: new( & app, " /admin" , & viewer) ;
706
require_admin ( & viewer, & shell) ? ;
707
csrf ( & viewer, & f.csrf_token, & shell) ? ;
708
let outcome = super :: signin:: apply( & app, & viewer, f) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
737de0fa Andy McCall 15 days ago ↶
709
admin_page ( & app, shell, AdminSection:: SignIn, Some ( outcome.unwrap_or_else ( | e | e ) ) ) .await
83940d53 Andy McCall 1 month ago ↶
710
}
155cab14 Andy McCall 1 month ago ↶
711
712
713
pub async fn assistant_models (
714
State( app ) : State< Shared> ,
715
viewer : Viewer,
716
Form( f ) : Form< super:: assistants:: ModelsForm> ,
717
) -> Result< axum:: Json< serde_json:: Value> , Error> {
718
let shell = Shell:: new( & app, " /admin" , & viewer) ;
719
require_admin ( & viewer, & shell) ? ;
720
csrf ( & viewer, & f.csrf_token, & shell) ? ;
721
Ok ( axum:: Json( super :: assistants:: models( & app, & site_assistants ( ) , f) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ) )
722
}