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