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