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
13
# [ derive ( Template ) ]
14
# [ template ( path = " admin.html" ) ]
15
pub struct AdminPage {
16
shell : Shell,
17
accounts : Vec< accounts:: Account> ,
18
orgs : Vec< accounts:: Organisation> ,
19
mode : String,
20
invitations : Vec< ( String, Option< i64 > , i64 ) > ,
21
audit : Vec< accounts:: AuditRow> ,
22
repos : usize ,
23
notice : Option< String> ,
3556d08e Andy McCall 1 month ago ↶
24
25
26
27
mail_smtp : String,
28
mail_from : String,
29
mail_enabled : bool ,
cd46e56b Andy McCall 21 days ago ↶
30
31
mail : crate:: mail:: Choice,
3556d08e Andy McCall 1 month ago ↶
32
my_email : String,
e8e7158b Andy McCall 1 month ago ↶
33
34
audit_older : Option< i64 > ,
e71846aa Andy McCall 1 month ago ↶
35
36
assistants : super:: assistants:: Panel,
2a83f530 Andy McCall 1 month ago ↶
37
38
variables : super:: variables:: Panel,
4ac01a73 Andy McCall 1 month ago ↶
39
40
secrets : super:: secrets:: Panel,
83940d53 Andy McCall 1 month ago ↶
41
42
providers : Vec< super:: signin:: ProviderView> ,
19bb31ff Andy McCall 1 month ago ↶
43
}
44
45
fn require_admin ( viewer : & Viewer, shell : & Shell) -> Result< accounts:: Account, Error> {
46
match viewer.account ( ) {
47
Some ( a) if a.is_admin => Ok ( a.clone ( ) ) ,
48
_ => Err ( Error:: not_found( shell.clone ( ) ) ) ,
49
}
50
}
51
52
async fn admin_page ( app : & Shared, shell : Shell, notice : Option< String> ) -> Result< Page< AdminPage> , Error> {
e8e7158b Andy McCall 1 month ago ↶
53
admin_page_at ( app, shell, notice, None ) .await
54
}
55
56
57
const AUDIT_PAGE : i64 = 50 ;
58
59
async fn admin_page_at ( app : & Shared, shell : Shell, notice : Option< String> , audit_before : Option< i64 > ) -> Result< Page< AdminPage> , Error> {
19bb31ff Andy McCall 1 month ago ↶
60
let e = | e | Error:: from_db( shell.clone ( ) , e) ;
61
let accounts_list = accounts:: accounts( & app.db) .await.map_err ( e) ? ;
62
let orgs = accounts:: orgs( & app.db) .await.map_err ( e) ? ;
63
let mode = super :: session:: registration_mode( app) .await;
64
let invitations = accounts:: invitations( & app.db) .await.map_err ( e) ? ;
e8e7158b Andy McCall 1 month ago ↶
65
let mut audit = accounts:: audit_page( & app.db, audit_before, AUDIT_PAGE + 1 ) .await.map_err ( e) ? ;
66
let audit_older = if audit.len ( ) as i64 > AUDIT_PAGE {
67
audit.truncate ( AUDIT_PAGE as usize ) ;
68
audit.last ( ) .map ( | r | r.id )
69
} else {
70
None
71
} ;
19bb31ff Andy McCall 1 month ago ↶
72
let repos = crate :: db:: repos:: list( & app.db) .await.map_err ( e) ? .len ( ) ;
3556d08e Andy McCall 1 month ago ↶
73
let mail = crate :: mail:: settings( app) ;
74
let my_email = match shell.user.as_deref ( ) {
75
Some ( u) => accounts:: account_by_name( & app.db, u) .await.map_err ( e) ? .map ( | a | a.email ) .unwrap_or_default ( ) ,
76
None => String :: new( ) ,
77
} ;
e71846aa Andy McCall 1 month ago ↶
78
let assistants = super :: assistants:: panel( app, & site_assistants ( ) ) .await.map_err ( e) ? ;
2a83f530 Andy McCall 1 month ago ↶
79
let variables = super :: variables:: panel( app, & site_assistants ( ) ) .await.map_err ( e) ? ;
4ac01a73 Andy McCall 1 month ago ↶
80
let secrets = super :: secrets:: panel( app, & site_assistants ( ) ) .await.map_err ( e) ? ;
83940d53 Andy McCall 1 month ago ↶
81
let providers = super :: signin:: provider_views( app) .await.map_err ( e) ? ;
3556d08e Andy McCall 1 month ago ↶
82
Ok ( Page( AdminPage {
83940d53 Andy McCall 1 month ago ↶
83
providers,
e71846aa Andy McCall 1 month ago ↶
84
assistants,
2a83f530 Andy McCall 1 month ago ↶
85
variables,
4ac01a73 Andy McCall 1 month ago ↶
86
secrets,
3556d08e Andy McCall 1 month ago ↶
87
shell,
88
accounts: accounts_list,
89
orgs,
90
mode,
91
invitations,
92
audit,
93
repos,
94
notice,
cd46e56b Andy McCall 21 days ago ↶
95
mail: crate :: mail:: describe( & mail.smtp) ,
3556d08e Andy McCall 1 month ago ↶
96
mail_smtp: crate :: mail:: masked( & mail.smtp) ,
97
mail_from: mail.from.clone ( ) ,
98
mail_enabled: ! mail.smtp.is_empty ( ) & & ! mail.from.is_empty ( ) ,
99
my_email,
e8e7158b Andy McCall 1 month ago ↶
100
audit_older,
3556d08e Andy McCall 1 month ago ↶
101
} ) )
102
}
103
cd46e56b Andy McCall 21 days ago ↶
104
impl AdminPage {
105
fn mail_services ( & self ) -> & 'static [ ( & 'static str , & 'static str ) ] {
106
crate :: mail:: SERVICES
107
}
108
}
109
3556d08e Andy McCall 1 month ago ↶
110
# [ derive ( Deserialize ) ]
111
pub struct MailForm {
112
#[serde(rename = "_csrf")]
113
csrf_token : String,
114
#[serde(default)]
115
smtp : String,
116
#[serde(default)]
117
from : String,
cd46e56b Andy McCall 21 days ago ↶
118
119
120
#[serde(default)]
121
service : String,
122
#[serde(default)]
123
region : String,
124
#[serde(default)]
125
user : String,
126
#[serde(default)]
127
secret : String,
128
#[serde(default)]
129
host : String,
130
#[serde(default)]
131
port : String,
132
#[serde(default)]
133
security : String,
3556d08e Andy McCall 1 month ago ↶
134
}
135
136
137
138
139
pub async fn set_mail ( State( app ) : State< Shared> , viewer : Viewer, Form( f ) : Form< MailForm> ) -> Result< Page< AdminPage> , Error> {
140
let shell = Shell:: new( & app, " /admin" , & viewer) ;
141
let me = require_admin ( & viewer, & shell) ? ;
142
csrf ( & viewer, & f.csrf_token, & shell) ? ;
143
let current = crate :: mail:: settings( & app) ;
cd46e56b Andy McCall 21 days ago ↶
144
145
let smtp = match f.service.as_str ( ) {
146
" off" => String :: new( ) ,
147
" " | " url" => {
148
let smtp = f.smtp.trim ( ) ;
149
if ! current.smtp.is_empty ( ) & & smtp = = crate :: mail:: masked( & current.smtp) {
150
current.smtp.clone ( )
151
} else {
152
smtp.to_string ( )
153
}
154
}
155
_ => {
156
let asked = crate :: mail:: Asked {
157
service: f.service.clone ( ) ,
158
region: f.region.clone ( ) ,
159
user: f.user.clone ( ) ,
160
secret: f.secret.trim ( ) .to_string ( ) ,
161
host: f.host.clone ( ) ,
162
port: f.port.clone ( ) ,
163
security: f.security.clone ( ) ,
164
} ;
165
match crate :: mail:: compose( & asked, & current.smtp) {
166
Ok ( url) => url,
167
Err ( why) => return admin_page ( & app, shell, Some ( why) ) .await,
168
}
169
}
170
} ;
3556d08e Andy McCall 1 month ago ↶
171
let from = f.from.trim ( ) .to_string ( ) ;
172
if ! smtp.is_empty ( ) & & ! ( smtp.starts_with ( " smtp://" ) | | smtp.starts_with ( " smtps://" ) | | smtp.starts_with ( " file://" ) ) {
173
return admin_page (
174
& app,
175
shell,
176
Some ( " The SMTP URL starts with smtp:// or smtps:// (file:///dir writes messages to a directory, for testing)." .into ( ) ) ,
177
)
178
.await;
179
}
180
if ! smtp.is_empty ( ) & & from.is_empty ( ) {
cd46e56b Andy McCall 21 days ago ↶
181
return admin_page ( & app, shell, Some ( " A From address is needed: OxygenHub <[email protected] >." .into ( ) ) ) .await;
3556d08e Andy McCall 1 month ago ↶
182
}
183
crate :: mail:: save( & app, & smtp, & from) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
184
accounts:: audit( & app.db, Some ( & me) , " admin.mail" , if smtp.is_empty ( ) { " off" } else { " on" } , & from) .await.ok ( ) ;
185
admin_page ( & app, shell, Some ( if smtp.is_empty ( ) { " Mail is off." .into ( ) } else { " Mail settings saved." .into ( ) } ) ) .await
186
}
187
188
189
pub async fn test_mail (
190
State( app ) : State< Shared> ,
191
viewer : Viewer,
192
Form( f ) : Form< super:: session:: CsrfForm> ,
193
) -> Result< Page< AdminPage> , Error> {
194
let shell = Shell:: new( & app, " /admin" , & viewer) ;
195
let me = require_admin ( & viewer, & shell) ? ;
196
csrf ( & viewer, & f.csrf, & shell) ? ;
197
if ! crate :: mail:: enabled( & app) {
198
return admin_page ( & app, shell, Some ( " Mail is off; save an SMTP URL and a From address first." .into ( ) ) ) .await;
199
}
200
if me.email.is_empty ( ) {
201
return admin_page ( & app, shell, Some ( " Your account has no email address to send the test to." .into ( ) ) ) .await;
202
}
cd46e56b Andy McCall 21 days ago ↶
203
let base = app.config.base_url ( ) .to_string ( ) ;
204
let notice = match crate :: mail:: send_letter(
3556d08e Andy McCall 1 month ago ↶
205
& app,
206
me.name ( ) ,
207
& me.email,
b91e2d81 Andy McCall 21 days ago ↶
208
" 🧪 Test message" ,
cd46e56b Andy McCall 21 days ago ↶
209
" 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." ,
210
Some ( crate :: mail:: Action { label: " Open the hub" , url: & base } ) ,
211
None ,
3556d08e Andy McCall 1 month ago ↶
212
)
213
.await
214
{
215
Ok ( ( ) ) => format! ( " Sent a test message to {} ." , me.email) ,
216
Err ( e) => format! ( " Sending failed: {e} " ) ,
217
} ;
218
admin_page ( & app, shell, Some ( notice) ) .await
19bb31ff Andy McCall 1 month ago ↶
219
}
220
e8e7158b Andy McCall 1 month ago ↶
221
# [ derive ( Deserialize ) ]
222
pub struct IndexQuery {
223
audit_before : Option< i64 > ,
224
}
225
226
pub async fn index (
227
State( app ) : State< Shared> ,
228
viewer : Viewer,
229
axum:: extract:: Query( q ) : axum:: extract:: Query< IndexQuery> ,
230
) -> Result< Page< AdminPage> , Error> {
19bb31ff Andy McCall 1 month ago ↶
231
let shell = Shell:: new( & app, " /admin" , & viewer) ;
232
require_admin ( & viewer, & shell) ? ;
e8e7158b Andy McCall 1 month ago ↶
233
admin_page_at ( & app, shell, None , q.audit_before) .await
19bb31ff Andy McCall 1 month ago ↶
234
}
235
236
# [ derive ( Deserialize ) ]
237
pub struct ModeForm {
238
#[serde(rename = "_csrf")]
239
csrf_token : String,
240
mode : String,
241
}
242
243
pub async fn set_mode ( State( app ) : State< Shared> , viewer : Viewer, Form( f ) : Form< ModeForm> ) -> Result< Page< AdminPage> , Error> {
244
let shell = Shell:: new( & app, " /admin" , & viewer) ;
245
let me = require_admin ( & viewer, & shell) ? ;
246
csrf ( & viewer, & f.csrf_token, & shell) ? ;
247
if ! matches! ( f.mode.as_str ( ) , " open" | " invite" | " closed" ) {
248
return Err ( Error:: bad( shell, " mode is open, invite or closed" ) ) ;
249
}
250
accounts:: set_setting( & app.db, " registration_mode" , & f.mode) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
251
accounts:: audit( & app.db, Some ( & me) , " admin.registration" , & f.mode, " " ) .await.ok ( ) ;
252
admin_page ( & app, shell, Some ( format! ( " Registration is {} ." , f.mode) ) ) .await
253
}
254
255
pub async fn invite ( State( app ) : State< Shared> , viewer : Viewer, Form( f ) : Form< super:: session:: CsrfForm> ) -> Result< Page< AdminPage> , Error> {
256
let shell = Shell:: new( & app, " /admin" , & viewer) ;
257
let me = require_admin ( & viewer, & shell) ? ;
258
csrf ( & viewer, & f.csrf, & shell) ? ;
259
let code = auth:: random_hex( 12 ) ;
260
accounts:: create_invitation( & app.db, & code, Some ( me.id) ) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
261
accounts:: audit( & app.db, Some ( & me) , " admin.invite" , & code, " " ) .await.ok ( ) ;
262
admin_page ( & app, shell, Some ( format! ( " Invitation created: {} /register?invite={code} " , app.config.base_url ( ) ) ) ) .await
263
}
264
265
# [ derive ( Deserialize ) ]
266
pub struct AccountForm {
267
#[serde(rename = "_csrf")]
268
csrf_token : String,
269
username : String,
270
action : String,
6a178055 Andy McCall 1 month ago ↶
271
272
#[serde(default)]
273
confirm : String,
19bb31ff Andy McCall 1 month ago ↶
274
}
275
276
pub async fn account ( State( app ) : State< Shared> , viewer : Viewer, Form( f ) : Form< AccountForm> ) -> Result< Page< AdminPage> , Error> {
277
let shell = Shell:: new( & app, " /admin" , & viewer) ;
278
let me = require_admin ( & viewer, & shell) ? ;
279
csrf ( & viewer, & f.csrf_token, & shell) ? ;
280
let Some ( target) = accounts:: account_by_name( & app.db, f.username.trim ( ) ) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? else {
281
return admin_page ( & app, shell, Some ( " No such account." .into ( ) ) ) .await;
282
} ;
6a178055 Andy McCall 1 month ago ↶
283
if target.id = = me.id & & matches! ( f.action.as_str ( ) , " ban" | " demote" | " disable" | " delete" ) {
19bb31ff Andy McCall 1 month ago ↶
284
return admin_page ( & app, shell, Some ( " Not on yourself." .into ( ) ) ) .await;
285
}
286
let e = | e | Error:: from_db( shell.clone ( ) , e) ;
6a178055 Andy McCall 1 month ago ↶
287
288
if target.is_admin & & matches! ( f.action.as_str ( ) , " disable" | " delete" ) {
289
let others =
290
accounts:: accounts( & app.db) .await.map_err ( e) ? .into_iter ( ) .filter ( | a | a.is_admin & & ! a.blocked ( ) & & a.id ! = target.id ) .count ( ) ;
291
if others = = 0 {
292
return admin_page ( & app, shell, Some ( format! ( " {} is the last admin who can sign in." , target.username) ) ) .await;
293
}
294
}
295
if f.action = = " delete" {
296
if f.confirm.trim ( ) ! = target.username {
297
return admin_page ( & app, shell, Some ( format! ( " Not deleted: type {} to confirm." , target.username) ) ) .await;
298
}
299
let repos = crate :: db:: repos:: list_by_owner( & app.db, & target.username) .await.map_err ( e) ? ;
300
let orgs = accounts:: orgs_owned_only_by( & app.db, target.id) .await.map_err ( e) ? ;
301
if ! repos.is_empty ( ) | | ! orgs.is_empty ( ) {
302
let mut held: Vec< String> = repos.iter ( ) .map ( | r | r.slug ( ) ) .collect ( ) ;
303
held.extend ( orgs.iter ( ) .map ( | o | format! ( " organisation {o} (sole owner)" ) ) ) ;
304
return admin_page (
305
& app,
306
shell,
307
Some ( format! ( " Not deleted: {} still owns {} . Transfer or delete them first." , target.username, held.join ( " , " ) ) ) ,
308
)
309
.await;
310
}
311
accounts:: delete_account( & app.db, target.id, & target.username) .await.map_err ( e) ? ;
312
accounts:: audit( & app.db, Some ( & me) , " admin.account.delete" , & target.username, " " ) .await.ok ( ) ;
313
return admin_page ( & app, shell, Some ( format! ( " {} deleted; the name stays retired." , target.username) ) ) .await;
314
}
34beb1a7 Andy McCall 1 month ago ↶
315
if f.action = = " reset_link" {
6a178055 Andy McCall 1 month ago ↶
316
if target.blocked ( ) {
317
return admin_page ( & app, shell, Some ( " A banned or disabled account cannot be reset." .into ( ) ) ) .await;
34beb1a7 Andy McCall 1 month ago ↶
318
}
319
let url = super :: recovery:: admin_reset_link( & app, & me, & target) .await.map_err ( e) ? ;
320
return admin_page (
321
& app,
322
shell,
323
Some ( format! ( " Reset link for {} (works once, for 24 hours; shown only now): {url} " , target.username) ) ,
324
)
325
.await;
326
}
19bb31ff Andy McCall 1 month ago ↶
327
match f.action.as_str ( ) {
328
" ban" => accounts:: set_banned( & app.db, target.id, true ) .await.map_err ( e) ? ,
329
" unban" => accounts:: set_banned( & app.db, target.id, false ) .await.map_err ( e) ? ,
6a178055 Andy McCall 1 month ago ↶
330
" disable" => accounts:: set_disabled( & app.db, target.id, true ) .await.map_err ( e) ? ,
331
" enable" => accounts:: set_disabled( & app.db, target.id, false ) .await.map_err ( e) ? ,
19bb31ff Andy McCall 1 month ago ↶
332
" promote" => accounts:: set_admin( & app.db, target.id, true ) .await.map_err ( e) ? ,
333
" demote" => accounts:: set_admin( & app.db, target.id, false ) .await.map_err ( e) ? ,
334
_ => return Err ( Error:: bad( shell, " unknown action" ) ) ,
335
}
336
accounts:: audit( & app.db, Some ( & me) , & format! ( " admin.account.{} " , f.action) , & target.username, " " ) .await.ok ( ) ;
337
admin_page ( & app, shell, Some ( format! ( " {} : {} ." , target.username, f.action) ) ) .await
338
}
8fb35df7 Andy McCall 1 month ago ↶
339
340
341
342
343
pub struct StorageRow {
344
pub slug : String,
345
pub bytes : u64 ,
346
347
pub loose : usize ,
348
pub packs : usize ,
349
}
350
351
# [ derive ( Template ) ]
352
# [ template ( path = " admin_storage.html" ) ]
353
pub struct StoragePage {
354
shell : Shell,
355
rows : Vec< StorageRow> ,
356
total : u64 ,
357
notice : Option< String> ,
358
error : Option< String> ,
359
}
360
361
362
fn footprint ( dir : & std:: path:: Path) -> ( u64 , usize , usize ) {
363
let ( mut bytes, mut loose, mut packs) = ( 0 u64 , 0 usize , 0 usize ) ;
364
let mut stack = vec! [ dir.to_path_buf ( ) ] ;
365
while let Some ( d) = stack.pop ( ) {
366
let Ok ( rd) = std:: fs:: read_dir( & d) else { continue } ;
367
for e in rd.flatten ( ) {
368
let Ok ( ft) = e.file_type ( ) else { continue } ;
369
if ft.is_dir ( ) {
370
stack.push ( e.path ( ) ) ;
371
} else if ft.is_file ( ) {
372
bytes + = e.metadata ( ) .map ( | m | m.len ( ) ) .unwrap_or ( 0 ) ;
373
let p = e.path ( ) ;
374
if p.components ( ) .any ( | c | c.as_os_str ( ) = = " loose" ) {
375
loose + = 1 ;
376
} else if p.extension ( ) .is_some_and ( | x | x = = " pack" ) {
377
packs + = 1 ;
378
}
379
}
380
}
381
}
382
( bytes, loose, packs)
383
}
384
385
async fn storage_page ( app : & Shared, shell : Shell, notice : Option< String> , error : Option< String> ) -> Result< Page< StoragePage> , Error> {
386
let repos = crate :: db:: repos:: list( & app.db) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
387
let dirs: Vec< ( String, std:: path:: PathBuf) > =
388
repos.iter ( ) .filter_map ( | r | crate :: repos:: RepoName:: new( & r.owner, & r.name) .map ( | n | ( n.to_string ( ) , app.repos.dir ( & n) ) ) ) .collect ( ) ;
389
let mut rows = tokio:: task:: spawn_blocking( move | | {
390
dirs.into_iter ( )
391
.map ( | ( slug , dir ) | {
392
let ( bytes, loose, packs) = footprint ( & dir) ;
393
StorageRow { slug, bytes, loose, packs }
394
} )
395
.collect:: < Vec< _ > > ( )
396
} )
397
.await
398
.unwrap_or_default ( ) ;
399
rows.sort_by_key ( | r | std:: cmp:: Reverse( r.bytes) ) ;
400
let total = rows.iter ( ) .map ( | r | r.bytes ) .sum ( ) ;
401
Ok ( Page( StoragePage { shell, rows, total, notice, error } ) )
402
}
403
404
pub async fn storage ( State( app ) : State< Shared> , viewer : Viewer) -> Result< Page< StoragePage> , Error> {
405
let shell = Shell:: new( & app, " /admin/storage" , & viewer) ;
406
require_admin ( & viewer, & shell) ? ;
407
storage_page ( & app, shell, None , None ) .await
408
}
409
410
# [ derive ( Deserialize ) ]
411
pub struct MaintainForm {
412
#[serde(rename = "_csrf")]
413
csrf_token : String,
414
415
repo : String,
416
}
417
418
419
420
pub async fn maintain ( State( app ) : State< Shared> , viewer : Viewer, Form( f ) : Form< MaintainForm> ) -> Result< Page< StoragePage> , Error> {
421
let shell = Shell:: new( & app, " /admin/storage" , & viewer) ;
422
let me = require_admin ( & viewer, & shell) ? ;
423
csrf ( & viewer, & f.csrf_token, & shell) ? ;
424
let names: Vec< crate:: repos:: RepoName> = if f.repo = = " *" {
425
crate :: db:: repos:: list( & app.db)
426
.await
427
.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ?
428
.iter ( )
429
.filter_map ( | r | crate :: repos:: RepoName:: new( & r.owner, & r.name) )
430
.collect ( )
431
} else {
432
match crate :: repos:: RepoName:: parse( & f.repo) {
433
Some ( n) if app.repos.exists ( & n) => vec! [ n] ,
434
_ => return storage_page ( & app, shell, None , Some ( format! ( " No repository {} ." , f.repo) ) ) .await,
435
}
436
} ;
437
let ( mut packed, mut pruned, mut freed, mut failed) = ( 0 usize , 0 usize , 0 u64 , Vec :: new( ) ) ;
438
for name in & names {
439
match app.repos.write ( name, | r , _| r.gc ( false ) .map_err ( crate :: repos:: RepoError:: Oxy) ) .await {
440
Ok ( rep) => {
441
packed + = rep.packed.objects;
442
pruned + = rep.pruned.objects;
443
freed + = rep.pruned.bytes + rep.packed.raw_bytes.saturating_sub ( rep.packed.packed_bytes) ;
444
}
445
Err ( e) => failed.push ( format! ( " {name} : {e} " ) ) ,
446
}
447
}
448
let summary = format! (
449
" Maintained {} repositor{} : {packed} loose objects packed, {pruned} unreachable objects pruned, about {} saved." ,
450
names.len ( ) ,
451
if names.len ( ) = = 1 { " y" } else { " ies" } ,
452
crate :: render:: bytes( freed)
453
) ;
454
accounts:: audit( & app.db, Some ( & me) , " admin.gc" , & f.repo, & summary) .await.ok ( ) ;
455
let error = ( ! failed.is_empty ( ) ) .then ( | | failed.join ( " ; " ) ) ;
456
storage_page ( & app, shell, Some ( summary) , error) .await
457
}
e71846aa Andy McCall 1 month ago ↶
458
2a83f530 Andy McCall 1 month ago ↶
459
fn site_assistants ( ) -> crate:: levels:: At {
460
crate :: levels:: At:: site( )
e71846aa Andy McCall 1 month ago ↶
461
}
462
463
464
pub async fn assistants (
465
State( app ) : State< Shared> ,
466
viewer : Viewer,
467
Form( f ) : Form< super:: assistants:: AssistantForm> ,
468
) -> Result< Page< AdminPage> , Error> {
469
let shell = Shell:: new( & app, " /admin" , & viewer) ;
470
require_admin ( & viewer, & shell) ? ;
471
csrf ( & viewer, & f.csrf_token, & shell) ? ;
472
let outcome = super :: assistants:: apply( & app, & viewer, & site_assistants ( ) , f) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
473
admin_page ( & app, shell, Some ( outcome.unwrap_or_else ( | e | e ) ) ) .await
474
}
2a83f530 Andy McCall 1 month ago ↶
475
476
477
pub async fn variables (
478
State( app ) : State< Shared> ,
479
viewer : Viewer,
480
Form( f ) : Form< super:: variables:: VariableForm> ,
481
) -> Result< Page< AdminPage> , Error> {
482
let shell = Shell:: new( & app, " /admin" , & viewer) ;
483
require_admin ( & viewer, & shell) ? ;
484
csrf ( & viewer, & f.csrf_token, & shell) ? ;
485
let outcome = super :: variables:: apply( & app, & viewer, & site_assistants ( ) , f) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
486
admin_page ( & app, shell, Some ( outcome.unwrap_or_else ( | e | e ) ) ) .await
487
}
4ac01a73 Andy McCall 1 month ago ↶
488
489
490
pub async fn secrets (
491
State( app ) : State< Shared> ,
492
viewer : Viewer,
493
Form( f ) : Form< super:: secrets:: SecretForm> ,
494
) -> Result< Page< AdminPage> , Error> {
495
let shell = Shell:: new( & app, " /admin" , & viewer) ;
496
require_admin ( & viewer, & shell) ? ;
497
csrf ( & viewer, & f.csrf_token, & shell) ? ;
498
let outcome = super :: secrets:: apply( & app, & viewer, & site_assistants ( ) , f) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
499
admin_page ( & app, shell, Some ( outcome.unwrap_or_else ( | e | e ) ) ) .await
500
}
83940d53 Andy McCall 1 month ago ↶
501
502
503
pub async fn signin (
504
State( app ) : State< Shared> ,
505
viewer : Viewer,
506
Form( f ) : Form< super:: signin:: ProviderForm> ,
507
) -> Result< Page< AdminPage> , Error> {
508
let shell = Shell:: new( & app, " /admin" , & viewer) ;
509
require_admin ( & viewer, & shell) ? ;
510
csrf ( & viewer, & f.csrf_token, & shell) ? ;
511
let outcome = super :: signin:: apply( & app, & viewer, f) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ;
512
admin_page ( & app, shell, Some ( outcome.unwrap_or_else ( | e | e ) ) ) .await
513
}
155cab14 Andy McCall 1 month ago ↶
514
515
516
pub async fn assistant_models (
517
State( app ) : State< Shared> ,
518
viewer : Viewer,
519
Form( f ) : Form< super:: assistants:: ModelsForm> ,
520
) -> Result< axum:: Json< serde_json:: Value> , Error> {
521
let shell = Shell:: new( & app, " /admin" , & viewer) ;
522
require_admin ( & viewer, & shell) ? ;
523
csrf ( & viewer, & f.csrf_token, & shell) ? ;
524
Ok ( axum:: Json( super :: assistants:: models( & app, & site_assistants ( ) , f) .await.map_err ( | e | Error:: from_db( shell.clone ( ) , e) ) ? ) )
525
}