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