1use std::sync::Arc;
25
26use crate::auth::{self, Identity, Role};
27use crate::error::{Error, Result};
28use crate::http::{Request, Response};
29use crate::orm::Db;
30use crate::router::Router;
31use crate::templates::Templates;
32
33const ADMIN_CSS: &str = include_str!("../../assets/static/admin.css");
38
39const ADMIN_JS: &str = include_str!("../../assets/static/admin.js");
42
43const FONT_GEIST: &[u8] = include_bytes!("../../assets/static/fonts/Geist-Variable.woff2");
53const FONT_GEIST_MONO: &[u8] = include_bytes!("../../assets/static/fonts/GeistMono-Variable.woff2");
54const FONT_TAJAWAL_REG: &[u8] = include_bytes!("../../assets/static/fonts/Tajawal-Regular.woff2");
55const FONT_TAJAWAL_MED: &[u8] = include_bytes!("../../assets/static/fonts/Tajawal-Medium.woff2");
56const FONT_TAJAWAL_BOLD: &[u8] = include_bytes!("../../assets/static/fonts/Tajawal-Bold.woff2");
57const FONT_NOTO_NASKH_AR: &[u8] =
58 include_bytes!("../../assets/static/fonts/NotoNaskhArabic-Variable.woff2");
59
60use super::handlers::{self, AdminCtx};
61use super::render;
62use super::types::Admin;
63
64enum Guard {
68 Allow(Identity),
69 Redirect(Response),
70}
71
72async fn login_guard(ctx: &AdminCtx, req: &Request) -> Result<Guard> {
73 let cookie = match req.header("cookie") {
74 Some(c) => c,
75 None => return Ok(Guard::Redirect(Response::redirect("/admin/login"))),
76 };
77 let token = match auth::session_token_from_cookie(cookie) {
78 Some(t) => t,
79 None => return Ok(Guard::Redirect(Response::redirect("/admin/login"))),
80 };
81 let ident = match auth::identity_from_session(&ctx.db, &token).await? {
82 Some(i) => i,
83 None => return Ok(Guard::Redirect(Response::redirect("/admin/login"))),
84 };
85 if !ident.is_active {
86 return Ok(Guard::Redirect(Response::redirect("/admin/login")));
87 }
88 Ok(Guard::Allow(ident))
89}
90
91async fn role_guard(ctx: &AdminCtx, req: &Request, min: Role) -> Result<Guard> {
92 match login_guard(ctx, req).await? {
93 Guard::Redirect(r) => Ok(Guard::Redirect(r)),
94 Guard::Allow(ident) => {
95 if ident.role.includes(min) {
96 Ok(Guard::Allow(ident))
97 } else {
98 let body = render::render_forbidden_body(
99 &ctx.admin,
100 &ctx.templates,
101 &ident,
102 handlers::csrf_token(req),
103 None,
104 Some(min.label()),
105 )?;
106 Ok(Guard::Redirect(
107 Response::html(body).with_status(hyper::StatusCode::FORBIDDEN),
108 ))
109 }
110 }
111 }
112}
113
114async fn perm_guard(ctx: &AdminCtx, req: &Request, perm: &str) -> Result<Guard> {
115 match role_guard(ctx, req, Role::Staff).await? {
116 Guard::Redirect(r) => Ok(Guard::Redirect(r)),
117 Guard::Allow(ident) => {
118 if ident.role.bypasses_group_checks() {
119 return Ok(Guard::Allow(ident));
120 }
121 if auth::check_permission(&ctx.db, &ident, perm).await? {
122 Ok(Guard::Allow(ident))
123 } else {
124 let body = render::render_forbidden_body(
125 &ctx.admin,
126 &ctx.templates,
127 &ident,
128 handlers::csrf_token(req),
129 Some(perm.to_string()),
130 None,
131 )?;
132 Ok(Guard::Redirect(
133 Response::html(body).with_status(hyper::StatusCode::FORBIDDEN),
134 ))
135 }
136 }
137 }
138}
139
140#[cfg(test)]
143fn perm_guard_verdict(ident: &Identity, perm_held: bool) -> bool {
144 if !ident.is_active {
145 return false;
146 }
147 if ident.role.bypasses_group_checks() {
148 return true;
149 }
150 perm_held
151}
152
153fn parse_id(raw: Option<&str>) -> Result<i64> {
154 raw.and_then(|s| s.parse().ok())
155 .ok_or_else(|| Error::BadRequest("invalid id".into()))
156}
157
158fn model_name_from_req(req: &Request) -> Result<String> {
159 req.param("admin_name")
160 .map(|s| s.to_string())
161 .ok_or_else(|| Error::BadRequest("missing model".into()))
162}
163
164fn perm_for(ctx: &AdminCtx, admin_name: &str, action: &str) -> Result<String> {
165 let entry = ctx
166 .admin
167 .find(admin_name)
168 .ok_or_else(|| Error::NotFound(format!("no admin model: {admin_name}")))?;
169 let singular = entry.singular_name.to_ascii_lowercase();
170 Ok(format!("{admin_name}.{action}_{singular}"))
171}
172
173fn strict_mailer_guard_check(admin: &Admin) -> std::result::Result<(), String> {
193 if admin.active_recovery_policy().strict_mailer_required() && !admin.has_custom_mailer() {
194 Err(
195 "rustio-admin: RecoveryPolicy::strict_mailer_required() = true but no mailer \
196 was registered via Admin::mailer(...).\n\n\
197 The framework's default LogMailer writes recovery emails to log::info! instead \
198 of sending them, which is unsuitable for production. Recovery routes are NOT \
199 registered with this configuration.\n\n\
200 To resolve, choose one:\n\
201 (a) register a real mailer before calling register_admin_routes:\n\
202 Admin::mailer(Arc::new(MyProjectMailer::new(...)))\n\
203 (b) opt the policy out of strict mode (the framework default — dev / CI / \
204 testing baseline):\n\
205 RecoveryPolicy::strict_mailer_required(false)\n\n\
206 See DESIGN_RECOVERY.md §12.1 for the contract."
207 .to_string(),
208 )
209 } else {
210 Ok(())
211 }
212}
213
214pub fn register_admin_routes(
215 router: Router,
216 admin: Admin,
217 db: Db,
218 templates: Arc<Templates>,
219) -> Router {
220 if let Err(msg) = strict_mailer_guard_check(&admin) {
228 panic!("{msg}");
229 }
230
231 let ctx = Arc::new(AdminCtx::new(
232 Arc::new(admin),
233 db.clone(),
234 templates.clone(),
235 ));
236
237 let auth_ctx = Arc::new(super::builtin::AuthAdminCtx {
240 admin: ctx.admin.clone(),
241 db,
242 templates,
243 });
244
245 let err_admin = ctx.admin.clone();
252 let err_templates = ctx.templates.clone();
253 let router = router.middleware(move |req, next| {
254 let admin = err_admin.clone();
255 let templates = err_templates.clone();
256 Box::pin(async move {
257 let is_admin_path = req.path().starts_with("/admin");
258 let result = next.run(req).await;
259 match result {
260 Ok(resp) => Ok(resp),
261 Err(err) if is_admin_path => Ok(render::render_admin_error_response(
262 &admin,
263 &templates,
264 None,
265 err.status(),
266 err.client_message().to_string(),
267 )),
268 Err(err) => Err(err),
269 }
270 })
271 });
272
273 let router = router.get("/static/admin.css", |_req| async move {
279 Ok(Response::new(
280 hyper::StatusCode::OK,
281 bytes::Bytes::from_static(ADMIN_CSS.as_bytes()),
282 )
283 .with_header("content-type", "text/css; charset=utf-8")
284 .with_header("cache-control", "no-cache, must-revalidate"))
285 });
286 let router = router.get("/static/admin.js", |_req| async move {
287 Ok(Response::new(
288 hyper::StatusCode::OK,
289 bytes::Bytes::from_static(ADMIN_JS.as_bytes()),
290 )
291 .with_header("content-type", "application/javascript; charset=utf-8")
292 .with_header("cache-control", "no-cache, must-revalidate"))
293 });
294
295 fn font_response(bytes: &'static [u8]) -> Response {
299 Response::new(hyper::StatusCode::OK, bytes::Bytes::from_static(bytes))
300 .with_header("content-type", "font/woff2")
301 .with_header("cache-control", "public, max-age=31536000, immutable")
302 }
303 let router = router.get("/static/fonts/Geist-Variable.woff2", |_req| async move {
304 Ok(font_response(FONT_GEIST))
305 });
306 let router = router.get(
307 "/static/fonts/GeistMono-Variable.woff2",
308 |_req| async move { Ok(font_response(FONT_GEIST_MONO)) },
309 );
310 let router = router.get("/static/fonts/Tajawal-Regular.woff2", |_req| async move {
311 Ok(font_response(FONT_TAJAWAL_REG))
312 });
313 let router = router.get("/static/fonts/Tajawal-Medium.woff2", |_req| async move {
314 Ok(font_response(FONT_TAJAWAL_MED))
315 });
316 let router = router.get("/static/fonts/Tajawal-Bold.woff2", |_req| async move {
317 Ok(font_response(FONT_TAJAWAL_BOLD))
318 });
319 let router = router.get(
320 "/static/fonts/NotoNaskhArabic-Variable.woff2",
321 |_req| async move { Ok(font_response(FONT_NOTO_NASKH_AR)) },
322 );
323
324 let c = ctx.clone();
326 let router = router.get("/admin/login", move |req| {
327 let c = c.clone();
328 async move { handlers::show_login(&c, req).await }
329 });
330
331 let c = ctx.clone();
332 let router = router.post("/admin/login", move |req| {
333 let c = c.clone();
334 async move { handlers::do_login(&c, req).await }
335 });
336
337 let c = ctx.clone();
338 let router = router.post("/admin/logout", move |req| {
339 let c = c.clone();
340 async move { handlers::do_logout(&c, req).await }
341 });
342
343 let recovery_state = Arc::new(super::recovery_handlers::RecoveryState::from_admin(
360 &ctx.admin,
361 ));
362
363 let c = ctx.clone();
364 let router = router.get("/admin/forgot-password", move |req| {
365 let c = c.clone();
366 async move { super::recovery_handlers::show_forgot_password(&c, &req).await }
367 });
368
369 let c = ctx.clone();
370 let rs = recovery_state.clone();
371 let router = router.post("/admin/forgot-password", move |req| {
372 let c = c.clone();
373 let rs = rs.clone();
374 async move { super::recovery_handlers::do_forgot_password(&c, &rs, req).await }
375 });
376
377 let c = ctx.clone();
378 let router = router.get("/admin/forgot-password/sent", move |req| {
379 let c = c.clone();
380 async move { super::recovery_handlers::show_forgot_password_sent(&c, &req).await }
381 });
382
383 let c = ctx.clone();
384 let router = router.get("/admin/reset-password/:token", move |req| {
385 let c = c.clone();
386 async move {
387 let token = req
388 .param("token")
389 .ok_or_else(|| Error::BadRequest("missing token".into()))?
390 .to_string();
391 super::recovery_handlers::show_reset_password(&c, &req, &token).await
392 }
393 });
394
395 let c = ctx.clone();
396 let rs = recovery_state.clone();
397 let router = router.post("/admin/reset-password/:token", move |req| {
398 let c = c.clone();
399 let rs = rs.clone();
400 async move {
401 let token = req
402 .param("token")
403 .ok_or_else(|| Error::BadRequest("missing token".into()))?
404 .to_string();
405 super::recovery_handlers::do_reset_password(&c, &rs, req, &token).await
406 }
407 });
408
409 let c = ctx.clone();
411 let router = router.get("/admin", move |req| {
412 let c = c.clone();
413 async move {
414 match role_guard(&c, &req, Role::Staff).await? {
415 Guard::Redirect(r) => Ok(r),
416 Guard::Allow(ident) => handlers::dashboard(&c, ident, &req).await,
417 }
418 }
419 });
420
421 let c = ctx.clone();
423 let router = router.get("/admin/history", move |req| {
424 let c = c.clone();
425 async move {
426 match role_guard(&c, &req, Role::Administrator).await? {
427 Guard::Redirect(r) => Ok(r),
428 Guard::Allow(ident) => handlers::show_log_entries(&c, ident, &req).await,
429 }
430 }
431 });
432
433 let c = ctx.clone();
436 let router = router.get("/admin/account/sessions", move |req| {
437 let c = c.clone();
438 async move {
439 match role_guard(&c, &req, Role::User).await? {
440 Guard::Redirect(r) => Ok(r),
441 Guard::Allow(ident) => handlers::show_account_sessions(&c, ident, &req).await,
442 }
443 }
444 });
445
446 let c = ctx.clone();
454 let router = router.post("/admin/account/sessions/revoke-others", move |req| {
455 let c = c.clone();
456 async move {
457 match role_guard(&c, &req, Role::User).await? {
458 Guard::Redirect(r) => Ok(r),
459 Guard::Allow(ident) => handlers::do_revoke_other_sessions(&c, ident, req).await,
460 }
461 }
462 });
463
464 let c = ctx.clone();
465 let router = router.post("/admin/account/sessions/revoke-all", move |req| {
466 let c = c.clone();
467 async move {
468 match role_guard(&c, &req, Role::User).await? {
469 Guard::Redirect(r) => Ok(r),
470 Guard::Allow(ident) => handlers::do_revoke_all_sessions(&c, ident, req).await,
471 }
472 }
473 });
474
475 let c = ctx.clone();
476 let router = router.post("/admin/account/sessions/:id/revoke", move |req| {
477 let c = c.clone();
478 async move {
479 match role_guard(&c, &req, Role::User).await? {
480 Guard::Redirect(r) => Ok(r),
481 Guard::Allow(ident) => {
482 let id = parse_id(req.param("id"))?;
483 handlers::do_revoke_session(&c, ident, req, id).await
484 }
485 }
486 }
487 });
488
489 let c = ctx.clone();
493 let router = router.get("/admin/password_change", move |req| {
494 let c = c.clone();
495 async move {
496 match role_guard(&c, &req, Role::User).await? {
497 Guard::Redirect(r) => Ok(r),
498 Guard::Allow(ident) => handlers::show_password_change(&c, ident, &req).await,
499 }
500 }
501 });
502 let c = ctx.clone();
503 let router = router.post("/admin/password_change", move |req| {
504 let c = c.clone();
505 async move {
506 match role_guard(&c, &req, Role::User).await? {
507 Guard::Redirect(r) => Ok(r),
508 Guard::Allow(ident) => handlers::do_password_change(&c, ident, req).await,
509 }
510 }
511 });
512
513 let c = ctx.clone();
515 let ac = auth_ctx.clone();
516 let router = router.get("/admin/users", move |req| {
517 let c = c.clone();
518 let ac = ac.clone();
519 async move {
520 match role_guard(&c, &req, Role::Administrator).await? {
521 Guard::Redirect(r) => Ok(r),
522 Guard::Allow(ident) => {
523 super::builtin::list_users(&ac, ident, handlers::csrf_token(&req)).await
524 }
525 }
526 }
527 });
528
529 let c = ctx.clone();
530 let ac = auth_ctx.clone();
531 let router = router.get("/admin/users/new", move |req| {
532 let c = c.clone();
533 let ac = ac.clone();
534 async move {
535 match role_guard(&c, &req, Role::Administrator).await? {
536 Guard::Redirect(r) => Ok(r),
537 Guard::Allow(ident) => {
538 super::builtin::show_new_user(&ac, ident, handlers::csrf_token(&req)).await
539 }
540 }
541 }
542 });
543
544 let c = ctx.clone();
545 let ac = auth_ctx.clone();
546 let router = router.post("/admin/users/new", move |req| {
547 let c = c.clone();
548 let ac = ac.clone();
549 async move {
550 match role_guard(&c, &req, Role::Administrator).await? {
551 Guard::Redirect(r) => Ok(r),
552 Guard::Allow(ident) => super::builtin::do_new_user(&ac, ident, req).await,
553 }
554 }
555 });
556
557 let c = ctx.clone();
558 let ac = auth_ctx.clone();
559 let router = router.get("/admin/users/:id/edit", move |req| {
560 let c = c.clone();
561 let ac = ac.clone();
562 async move {
563 match role_guard(&c, &req, Role::Administrator).await? {
564 Guard::Redirect(r) => Ok(r),
565 Guard::Allow(ident) => {
566 let id = parse_id(req.param("id"))?;
567 super::builtin::show_user_edit(&ac, ident, id, handlers::csrf_token(&req)).await
568 }
569 }
570 }
571 });
572
573 let c = ctx.clone();
574 let ac = auth_ctx.clone();
575 let router = router.post("/admin/users/:id/edit", move |req| {
576 let c = c.clone();
577 let ac = ac.clone();
578 async move {
579 match role_guard(&c, &req, Role::Administrator).await? {
580 Guard::Redirect(r) => Ok(r),
581 Guard::Allow(ident) => {
582 let id = parse_id(req.param("id"))?;
583 super::builtin::do_user_edit(&ac, ident, id, req).await
584 }
585 }
586 }
587 });
588
589 let c = ctx.clone();
590 let ac = auth_ctx.clone();
591 let router = router.get("/admin/users/:id/delete", move |req| {
592 let c = c.clone();
593 let ac = ac.clone();
594 async move {
595 match role_guard(&c, &req, Role::Administrator).await? {
596 Guard::Redirect(r) => Ok(r),
597 Guard::Allow(ident) => {
598 let id = parse_id(req.param("id"))?;
599 super::builtin::show_user_delete(&ac, ident, id, handlers::csrf_token(&req))
600 .await
601 }
602 }
603 }
604 });
605
606 let c = ctx.clone();
607 let ac = auth_ctx.clone();
608 let router = router.post("/admin/users/:id/delete", move |req| {
609 let c = c.clone();
610 let ac = ac.clone();
611 async move {
612 match role_guard(&c, &req, Role::Administrator).await? {
613 Guard::Redirect(r) => Ok(r),
614 Guard::Allow(ident) => {
615 let id = parse_id(req.param("id"))?;
616 super::builtin::do_user_delete(&ac, ident, id, req).await
617 }
618 }
619 }
620 });
621
622 let c = ctx.clone();
629 let ac = auth_ctx.clone();
630 let router = router.get("/admin/users/:id", move |req| {
631 let c = c.clone();
632 let ac = ac.clone();
633 async move {
634 match role_guard(&c, &req, Role::Administrator).await? {
635 Guard::Redirect(r) => Ok(r),
636 Guard::Allow(ident) => {
637 let id = parse_id(req.param("id"))?;
638 let q = req.query();
639 let tab = q.get("tab").map(|s| s.to_string());
640 let page: i64 = q.get("page").and_then(|s| s.parse().ok()).unwrap_or(1);
641 super::builtin::show_user_view(
642 &ac,
643 ident,
644 id,
645 handlers::csrf_token(&req),
646 tab,
647 page,
648 )
649 .await
650 }
651 }
652 }
653 });
654
655 let c = ctx.clone();
657 let ac = auth_ctx.clone();
658 let router = router.get("/admin/groups", move |req| {
659 let c = c.clone();
660 let ac = ac.clone();
661 async move {
662 match role_guard(&c, &req, Role::Administrator).await? {
663 Guard::Redirect(r) => Ok(r),
664 Guard::Allow(ident) => {
665 super::builtin::list_groups(&ac, ident, handlers::csrf_token(&req)).await
666 }
667 }
668 }
669 });
670
671 let c = ctx.clone();
672 let ac = auth_ctx.clone();
673 let router = router.get("/admin/groups/new", move |req| {
674 let c = c.clone();
675 let ac = ac.clone();
676 async move {
677 match role_guard(&c, &req, Role::Administrator).await? {
678 Guard::Redirect(r) => Ok(r),
679 Guard::Allow(ident) => {
680 super::builtin::show_new_group(&ac, ident, handlers::csrf_token(&req)).await
681 }
682 }
683 }
684 });
685
686 let c = ctx.clone();
687 let ac = auth_ctx.clone();
688 let router = router.post("/admin/groups/new", move |req| {
689 let c = c.clone();
690 let ac = ac.clone();
691 async move {
692 match role_guard(&c, &req, Role::Administrator).await? {
693 Guard::Redirect(r) => Ok(r),
694 Guard::Allow(ident) => super::builtin::do_new_group(&ac, ident, req).await,
695 }
696 }
697 });
698
699 let c = ctx.clone();
700 let ac = auth_ctx.clone();
701 let router = router.get("/admin/groups/:id/edit", move |req| {
702 let c = c.clone();
703 let ac = ac.clone();
704 async move {
705 match role_guard(&c, &req, Role::Administrator).await? {
706 Guard::Redirect(r) => Ok(r),
707 Guard::Allow(ident) => {
708 let id = parse_id(req.param("id"))?;
709 super::builtin::show_group_edit(&ac, ident, id, handlers::csrf_token(&req))
710 .await
711 }
712 }
713 }
714 });
715
716 let c = ctx.clone();
717 let ac = auth_ctx.clone();
718 let router = router.post("/admin/groups/:id/edit", move |req| {
719 let c = c.clone();
720 let ac = ac.clone();
721 async move {
722 match role_guard(&c, &req, Role::Administrator).await? {
723 Guard::Redirect(r) => Ok(r),
724 Guard::Allow(ident) => {
725 let id = parse_id(req.param("id"))?;
726 super::builtin::do_group_edit(&ac, ident, id, req).await
727 }
728 }
729 }
730 });
731
732 let c = ctx.clone();
733 let ac = auth_ctx.clone();
734 let router = router.get("/admin/groups/:id/delete", move |req| {
735 let c = c.clone();
736 let ac = ac.clone();
737 async move {
738 match role_guard(&c, &req, Role::Administrator).await? {
739 Guard::Redirect(r) => Ok(r),
740 Guard::Allow(ident) => {
741 let id = parse_id(req.param("id"))?;
742 super::builtin::show_group_delete(&ac, ident, id, handlers::csrf_token(&req))
743 .await
744 }
745 }
746 }
747 });
748
749 let c = ctx.clone();
750 let ac = auth_ctx.clone();
751 let router = router.post("/admin/groups/:id/delete", move |req| {
752 let c = c.clone();
753 let ac = ac.clone();
754 async move {
755 match role_guard(&c, &req, Role::Administrator).await? {
756 Guard::Redirect(r) => Ok(r),
757 Guard::Allow(ident) => {
758 let id = parse_id(req.param("id"))?;
759 super::builtin::do_group_delete(&ac, ident, id, req).await
760 }
761 }
762 }
763 });
764
765 let c = ctx.clone();
767 let router = router.get("/admin/:admin_name", move |req| {
768 let c = c.clone();
769 async move {
770 let name = model_name_from_req(&req)?;
771 let perm = perm_for(&c, &name, "view")?;
772 match perm_guard(&c, &req, &perm).await? {
773 Guard::Redirect(r) => Ok(r),
774 Guard::Allow(ident) => handlers::list_model(&c, ident, &name, &req).await,
775 }
776 }
777 });
778
779 let c = ctx.clone();
781 let router = router.get("/admin/:admin_name/new", move |req| {
782 let c = c.clone();
783 async move {
784 let name = model_name_from_req(&req)?;
785 let perm = perm_for(&c, &name, "add")?;
786 match perm_guard(&c, &req, &perm).await? {
787 Guard::Redirect(r) => Ok(r),
788 Guard::Allow(ident) => handlers::show_new_form(&c, ident, &name, &req).await,
789 }
790 }
791 });
792 let c = ctx.clone();
793 let router = router.post("/admin/:admin_name/new", move |req| {
794 let c = c.clone();
795 async move {
796 let name = model_name_from_req(&req)?;
797 let perm = perm_for(&c, &name, "add")?;
798 match perm_guard(&c, &req, &perm).await? {
799 Guard::Redirect(r) => Ok(r),
800 Guard::Allow(ident) => handlers::do_create(&c, ident, &name, req).await,
801 }
802 }
803 });
804
805 let c = ctx.clone();
807 let router = router.get("/admin/:admin_name/:id/edit", move |req| {
808 let c = c.clone();
809 async move {
810 let name = model_name_from_req(&req)?;
811 let perm = perm_for(&c, &name, "change")?;
812 match perm_guard(&c, &req, &perm).await? {
813 Guard::Redirect(r) => Ok(r),
814 Guard::Allow(ident) => {
815 let id = parse_id(req.param("id"))?;
816 handlers::show_edit_form(&c, ident, &name, id, &req).await
817 }
818 }
819 }
820 });
821 let c = ctx.clone();
822 let router = router.post("/admin/:admin_name/:id/edit", move |req| {
823 let c = c.clone();
824 async move {
825 let name = model_name_from_req(&req)?;
826 let perm = perm_for(&c, &name, "change")?;
827 match perm_guard(&c, &req, &perm).await? {
828 Guard::Redirect(r) => Ok(r),
829 Guard::Allow(ident) => {
830 let id = parse_id(req.param("id"))?;
831 handlers::do_update(&c, ident, &name, id, req).await
832 }
833 }
834 }
835 });
836
837 let c = ctx.clone();
840 let router = router.get("/admin/:admin_name/:id/history", move |req| {
841 let c = c.clone();
842 async move {
843 let name = model_name_from_req(&req)?;
844 let perm = perm_for(&c, &name, "view")?;
845 match perm_guard(&c, &req, &perm).await? {
846 Guard::Redirect(r) => Ok(r),
847 Guard::Allow(ident) => {
848 let id = parse_id(req.param("id"))?;
849 handlers::show_object_history(&c, ident, &name, id, &req).await
850 }
851 }
852 }
853 });
854
855 let c = ctx.clone();
857 let router = router.get("/admin/:admin_name/:id/delete", move |req| {
858 let c = c.clone();
859 async move {
860 let name = model_name_from_req(&req)?;
861 let perm = perm_for(&c, &name, "delete")?;
862 match perm_guard(&c, &req, &perm).await? {
863 Guard::Redirect(r) => Ok(r),
864 Guard::Allow(ident) => {
865 let id = parse_id(req.param("id"))?;
866 handlers::show_delete_confirm(&c, ident, &name, id, &req).await
867 }
868 }
869 }
870 });
871 let c = ctx.clone();
872 let router = router.post("/admin/:admin_name/:id/delete", move |req| {
873 let c = c.clone();
874 async move {
875 let name = model_name_from_req(&req)?;
876 let perm = perm_for(&c, &name, "delete")?;
877 match perm_guard(&c, &req, &perm).await? {
878 Guard::Redirect(r) => Ok(r),
879 Guard::Allow(ident) => {
880 let id = parse_id(req.param("id"))?;
881 handlers::do_delete(&c, ident, &name, id).await
882 }
883 }
884 }
885 });
886
887 let c = ctx.clone();
892 let router = router.post("/admin/:admin_name/bulk_delete", move |req| {
893 let c = c.clone();
894 async move {
895 let name = model_name_from_req(&req)?;
896 let perm = perm_for(&c, &name, "delete")?;
897 match perm_guard(&c, &req, &perm).await? {
898 Guard::Redirect(r) => Ok(r),
899 Guard::Allow(ident) => handlers::handle_bulk_delete(&c, ident, &name, &req).await,
900 }
901 }
902 });
903
904 let c = ctx.clone();
909 router.post("/admin/:admin_name/bulk/:action", move |req| {
910 let c = c.clone();
911 async move {
912 let name = model_name_from_req(&req)?;
913 let action = req
914 .param("action")
915 .ok_or_else(|| Error::BadRequest("missing bulk action name".into()))?
916 .to_string();
917 let perm = perm_for(&c, &name, "change")?;
918 match perm_guard(&c, &req, &perm).await? {
919 Guard::Redirect(r) => Ok(r),
920 Guard::Allow(ident) => {
921 handlers::handle_bulk_action(&c, ident, &name, &action, &req).await
922 }
923 }
924 }
925 })
926}
927
928#[cfg(test)]
929mod tests {
930 use super::*;
931
932 fn make_identity(role: Role, is_active: bool) -> Identity {
933 Identity {
934 user_id: 42,
935 email: "test@example.com".into(),
936 role,
937 is_active,
938 is_demo: false,
939 demo_label: None,
940 }
941 }
942
943 #[test]
948 fn role_guard_decision_admin_meets_staff_floor() {
949 let id = make_identity(Role::Administrator, true);
950 assert!(id.role.includes(Role::Staff));
951 }
952
953 #[test]
954 fn role_guard_decision_user_does_not_meet_staff() {
955 let id = make_identity(Role::User, true);
956 assert!(!id.role.includes(Role::Staff));
957 }
958
959 #[test]
960 fn role_guard_decision_administrator_does_not_meet_developer() {
961 let id = make_identity(Role::Administrator, true);
962 assert!(!id.role.includes(Role::Developer));
963 }
964
965 #[test]
966 fn role_guard_decision_developer_meets_everything() {
967 let id = make_identity(Role::Developer, true);
968 for &min in &[
969 Role::User,
970 Role::Staff,
971 Role::Supervisor,
972 Role::Administrator,
973 Role::Developer,
974 ] {
975 assert!(id.role.includes(min), "Developer should meet {min:?}");
976 }
977 }
978
979 #[test]
982 fn perm_guard_admin_short_circuits_without_perm() {
983 let id = make_identity(Role::Administrator, true);
984 assert!(perm_guard_verdict(&id, false));
985 }
986
987 #[test]
988 fn perm_guard_developer_short_circuits_without_perm() {
989 let id = make_identity(Role::Developer, true);
990 assert!(perm_guard_verdict(&id, false));
991 }
992
993 #[test]
994 fn perm_guard_staff_with_perm_passes() {
995 let id = make_identity(Role::Staff, true);
996 assert!(perm_guard_verdict(&id, true));
997 }
998
999 #[test]
1000 fn perm_guard_staff_without_perm_denies() {
1001 let id = make_identity(Role::Staff, true);
1002 assert!(!perm_guard_verdict(&id, false));
1003 }
1004
1005 #[test]
1006 fn perm_guard_inactive_admin_denies_even_with_bypass() {
1007 let id = make_identity(Role::Administrator, false);
1009 assert!(!perm_guard_verdict(&id, true));
1010 }
1011
1012 #[test]
1013 fn perm_guard_supervisor_without_perm_denies() {
1014 let id = make_identity(Role::Supervisor, true);
1016 assert!(!perm_guard_verdict(&id, false));
1017 }
1018
1019 #[test]
1024 fn strict_mailer_guard_passes_for_default_admin() {
1025 let admin = super::super::types::Admin::new();
1026 assert!(strict_mailer_guard_check(&admin).is_ok());
1027 }
1028
1029 #[test]
1032 fn strict_mailer_guard_fails_when_required_but_default_mailer() {
1033 use crate::auth::DefaultRecoveryPolicy;
1034 let admin = super::super::types::Admin::new().recovery_policy(std::sync::Arc::new(
1035 DefaultRecoveryPolicy::new().with_strict_mailer_required(true),
1036 ));
1037 let err = strict_mailer_guard_check(&admin).expect_err("guard should fail");
1038 assert!(
1039 err.contains("strict_mailer_required"),
1040 "error message must name the policy method: {err}"
1041 );
1042 assert!(
1043 err.contains("Admin::mailer"),
1044 "error message must direct the operator to the fix: {err}"
1045 );
1046 }
1047
1048 #[test]
1053 fn strict_mailer_guard_passes_when_mailer_was_explicitly_overridden() {
1054 use crate::auth::DefaultRecoveryPolicy;
1055 use crate::email::LogMailer;
1056 let admin = super::super::types::Admin::new()
1057 .recovery_policy(std::sync::Arc::new(
1058 DefaultRecoveryPolicy::new().with_strict_mailer_required(true),
1059 ))
1060 .mailer(std::sync::Arc::new(LogMailer));
1061 assert!(strict_mailer_guard_check(&admin).is_ok());
1062 }
1063
1064 #[test]
1067 fn strict_mailer_guard_passes_when_strict_mode_disabled() {
1068 let admin = super::super::types::Admin::new();
1069 assert!(strict_mailer_guard_check(&admin).is_ok());
1070 }
1071}