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