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 = concat!(
51 include_str!("../../assets/static/admin/tokens/colors.css"),
53 "\n",
54 include_str!("../../assets/static/admin/tokens/spacing.css"),
55 "\n",
56 include_str!("../../assets/static/admin/tokens/radius.css"),
57 "\n",
58 include_str!("../../assets/static/admin/tokens/shadows.css"),
59 "\n",
60 include_str!("../../assets/static/admin/tokens/typography.css"),
61 "\n",
62 include_str!("../../assets/static/admin/base/reset.css"),
64 "\n",
65 include_str!("../../assets/static/admin/base/base.css"),
66 "\n",
67 include_str!("../../assets/static/admin/base/typography.css"),
68 "\n",
69 include_str!("../../assets/static/admin/base/typography-i18n.css"),
70 "\n",
71 include_str!("../../assets/static/admin/base/utilities.css"),
72 "\n",
73 include_str!("../../assets/static/admin/layout/shell.css"),
75 "\n",
76 include_str!("../../assets/static/admin/layout/topbar.css"),
77 "\n",
78 include_str!("../../assets/static/admin/layout/sidebar.css"),
79 "\n",
80 include_str!("../../assets/static/admin/layout/footer.css"),
81 "\n",
82 include_str!("../../assets/static/admin/components/cards.css"),
84 "\n",
85 include_str!("../../assets/static/admin/components/buttons.css"),
86 "\n",
87 include_str!("../../assets/static/admin/components/forms.css"),
88 "\n",
89 include_str!("../../assets/static/admin/components/tables.css"),
90 "\n",
91 include_str!("../../assets/static/admin/components/filters.css"),
92 "\n",
93 include_str!("../../assets/static/admin/components/dropdowns.css"),
94 "\n",
95 include_str!("../../assets/static/admin/components/search_palette.css"),
96 "\n",
97 include_str!("../../assets/static/admin/components/pagination.css"),
98 "\n",
99 include_str!("../../assets/static/admin/components/pills.css"),
100 "\n",
101 include_str!("../../assets/static/admin/components/flashes.css"),
102 "\n",
103 include_str!("../../assets/static/admin/components/timeline.css"),
104 "\n",
105 include_str!("../../assets/static/admin/components/tabs.css"),
106 "\n",
107 include_str!("../../assets/static/admin/pages/auth.css"),
109 "\n",
110 include_str!("../../assets/static/admin/pages/dashboard.css"),
111 "\n",
112 include_str!("../../assets/static/admin/pages/db_browser.css"),
113 "\n",
114 include_str!("../../assets/static/admin/pages/permissions.css"),
115 "\n",
116 include_str!("../../assets/static/admin/pages/sessions.css"),
117 "\n",
118 include_str!("../../assets/static/admin/pages/errors.css"),
119 "\n",
120 include_str!("../../assets/static/admin/layout/responsive.css"),
122 "\n",
123 include_str!("../../assets/static/admin/print/print.css"),
125);
126
127const ADMIN_JS: &str = include_str!("../../assets/static/admin.js");
130
131const FONT_GEIST: &[u8] = include_bytes!("../../assets/static/fonts/Geist-Variable.woff2");
145const FONT_GEIST_MONO: &[u8] = include_bytes!("../../assets/static/fonts/GeistMono-Variable.woff2");
146const FONT_INTER: &[u8] = include_bytes!("../../assets/static/fonts/InterVariable.woff2");
147const FONT_TAJAWAL_REG: &[u8] = include_bytes!("../../assets/static/fonts/Tajawal-Regular.woff2");
148const FONT_TAJAWAL_MED: &[u8] = include_bytes!("../../assets/static/fonts/Tajawal-Medium.woff2");
149const FONT_TAJAWAL_BOLD: &[u8] = include_bytes!("../../assets/static/fonts/Tajawal-Bold.woff2");
150const FONT_NOTO_NASKH_AR: &[u8] =
151 include_bytes!("../../assets/static/fonts/NotoNaskhArabic-Variable.woff2");
152const FONT_NOTO_THAI: &[u8] =
153 include_bytes!("../../assets/static/fonts/NotoSansThai-Variable.woff2");
154const FONT_NOTO_DEVA: &[u8] =
155 include_bytes!("../../assets/static/fonts/NotoSansDevanagari-Variable.woff2");
156const FONT_NOTO_JP: &[u8] = include_bytes!("../../assets/static/fonts/NotoSansJP-Regular.woff2");
157const FONT_NOTO_KR: &[u8] = include_bytes!("../../assets/static/fonts/NotoSansKR-Regular.woff2");
158const FONT_NOTO_SC: &[u8] = include_bytes!("../../assets/static/fonts/NotoSansSC-Regular.woff2");
159
160use super::handlers::{self, AdminCtx};
161use super::render;
162use super::types::Admin;
163
164enum Guard {
168 Allow(Identity),
169 Redirect(Response),
170}
171
172const MUST_CHANGE_WHITELIST: &[&str] = &[
182 "/admin/must-change-password",
183 "/admin/logout",
184 "/admin/account/sessions",
185];
186
187fn is_must_change_whitelisted_path(path: &str) -> bool {
191 MUST_CHANGE_WHITELIST.contains(&path)
192}
193
194const READ_ONLY_EXACT_ALLOW: &[&str] = &[
205 "/admin/login",
206 "/admin/logout",
207 "/admin/reauth",
208 "/admin/forgot-password",
209 "/admin/mfa/verify",
210 "/admin/must-change-password",
211 "/admin/password_change",
212];
213
214const READ_ONLY_PREFIX_ALLOW: &[&str] = &[
215 "/admin/reset-password/",
218 "/admin/account/sessions/",
221 "/admin/account/mfa/",
224];
225
226fn is_saved_filter_path(path: &str) -> bool {
232 if let Some(rest) = path.strip_prefix("/admin/") {
233 if let Some((_, after)) = rest.split_once('/') {
237 return after == "saved_filters" || after.starts_with("saved_filters/");
238 }
239 }
240 false
241}
242
243pub(crate) fn is_mutating_method(method: &hyper::Method) -> bool {
249 matches!(
250 *method,
251 hyper::Method::POST | hyper::Method::PUT | hyper::Method::PATCH | hyper::Method::DELETE
252 )
253}
254
255pub(crate) fn is_read_only_writable_path(path: &str) -> bool {
263 if READ_ONLY_EXACT_ALLOW.contains(&path) {
264 return true;
265 }
266 if READ_ONLY_PREFIX_ALLOW
267 .iter()
268 .any(|prefix| path.starts_with(prefix))
269 {
270 return true;
271 }
272 is_saved_filter_path(path)
273}
274
275pub(crate) fn extract_admin_name(path: &str) -> Option<&str> {
282 let rest = path.strip_prefix("/admin/")?;
283 let slug = rest.split('/').next()?;
284 if slug.is_empty() || slug.starts_with('_') {
285 return None;
286 }
287 Some(slug)
288}
289
290const MFA_ENROLL_WHITELIST: &[&str] = &[
303 "/admin/account/mfa/enroll",
304 "/admin/logout",
305 "/admin/account/sessions",
306];
307
308fn is_mfa_enroll_whitelisted_path(path: &str) -> bool {
309 MFA_ENROLL_WHITELIST.contains(&path)
310}
311
312const MFA_VERIFY_WHITELIST: &[&str] = &[
322 "/admin/mfa/verify",
323 "/admin/logout",
324 "/admin/account/sessions",
325];
326
327fn is_mfa_verify_whitelisted_path(path: &str) -> bool {
328 MFA_VERIFY_WHITELIST.contains(&path)
329}
330
331fn mfa_required_for_role(policy: crate::auth::MfaPolicy, role: Role) -> bool {
341 use crate::auth::MfaPolicy;
342 match policy {
343 MfaPolicy::Disabled | MfaPolicy::Optional => false,
344 MfaPolicy::Required => true,
345 MfaPolicy::RequiredForRoles(roles) => roles.contains(&role),
346 }
347}
348
349async fn login_guard(ctx: &AdminCtx, req: &Request) -> Result<Guard> {
350 let cookie = match req.header("cookie") {
351 Some(c) => c,
352 None => return Ok(Guard::Redirect(Response::redirect("/admin/login"))),
353 };
354 let token = match auth::session_token_from_cookie(cookie) {
355 Some(t) => t,
356 None => return Ok(Guard::Redirect(Response::redirect("/admin/login"))),
357 };
358 let ident = match auth::identity_from_session(&ctx.db, &token).await? {
359 Some(i) => i,
360 None => return Ok(Guard::Redirect(Response::redirect("/admin/login"))),
361 };
362 if !ident.is_active {
363 return Ok(Guard::Redirect(Response::redirect("/admin/login")));
364 }
365
366 if ident.must_change_password && !is_must_change_whitelisted_path(req.path()) {
372 return Ok(Guard::Redirect(Response::redirect(
373 "/admin/must-change-password",
374 )));
375 }
376
377 let policy = ctx.admin.active_mfa_policy();
386 if mfa_required_for_role(policy, ident.role)
387 && !ident.mfa_enabled
388 && !is_mfa_enroll_whitelisted_path(req.path())
389 {
390 return Ok(Guard::Redirect(Response::redirect(
391 "/admin/account/mfa/enroll",
392 )));
393 }
394
395 use crate::auth::SessionTrust;
403 if ident.mfa_enabled
404 && ident.trust_level != SessionTrust::MfaVerified
405 && !is_mfa_verify_whitelisted_path(req.path())
406 {
407 return Ok(Guard::Redirect(Response::redirect("/admin/mfa/verify")));
408 }
409
410 Ok(Guard::Allow(ident))
411}
412
413async fn role_guard(ctx: &AdminCtx, req: &Request, min: Role) -> Result<Guard> {
414 match login_guard(ctx, req).await? {
415 Guard::Redirect(r) => Ok(Guard::Redirect(r)),
416 Guard::Allow(ident) => {
417 if ident.role.includes(min) {
418 Ok(Guard::Allow(ident))
419 } else {
420 let body = render::render_forbidden_body(
421 &ctx.admin,
422 &ctx.templates,
423 &ident,
424 handlers::csrf_token(req),
425 None,
426 Some(min.label()),
427 )?;
428 Ok(Guard::Redirect(
429 Response::html(body).with_status(hyper::StatusCode::FORBIDDEN),
430 ))
431 }
432 }
433 }
434}
435
436async fn perm_guard(ctx: &AdminCtx, req: &Request, perm: &str) -> Result<Guard> {
437 match role_guard(ctx, req, Role::Staff).await? {
438 Guard::Redirect(r) => Ok(Guard::Redirect(r)),
439 Guard::Allow(ident) => {
440 if ident.role.bypasses_group_checks() {
441 return Ok(Guard::Allow(ident));
442 }
443 if auth::check_permission(&ctx.db, &ident, perm).await? {
444 Ok(Guard::Allow(ident))
445 } else {
446 let body = render::render_forbidden_body(
447 &ctx.admin,
448 &ctx.templates,
449 &ident,
450 handlers::csrf_token(req),
451 Some(perm.to_string()),
452 None,
453 )?;
454 Ok(Guard::Redirect(
455 Response::html(body).with_status(hyper::StatusCode::FORBIDDEN),
456 ))
457 }
458 }
459 }
460}
461
462#[cfg(test)]
465fn perm_guard_verdict(ident: &Identity, perm_held: bool) -> bool {
466 if !ident.is_active {
467 return false;
468 }
469 if ident.role.bypasses_group_checks() {
470 return true;
471 }
472 perm_held
473}
474
475fn parse_id(raw: Option<&str>) -> Result<i64> {
476 raw.and_then(|s| s.parse().ok())
477 .ok_or_else(|| Error::BadRequest("invalid id".into()))
478}
479
480fn model_name_from_req(req: &Request) -> Result<String> {
481 req.param("admin_name")
482 .map(|s| s.to_string())
483 .ok_or_else(|| Error::BadRequest("missing model".into()))
484}
485
486fn perm_for(ctx: &AdminCtx, admin_name: &str, action: &str) -> Result<String> {
487 let entry = ctx
488 .admin
489 .find(admin_name)
490 .ok_or_else(|| Error::NotFound(format!("no admin model: {admin_name}")))?;
491 let singular = entry.singular_name.to_ascii_lowercase();
492 Ok(format!("{admin_name}.{action}_{singular}"))
493}
494
495async fn resolve_identity_for_error_page(db: &Db, cookie_header: &str) -> Option<Identity> {
525 let token = auth::session_token_from_cookie(cookie_header)?;
526 let identity = auth::identity_from_session(db, token.as_str())
527 .await
528 .ok()
529 .flatten()?;
530 if !identity.is_active {
531 return None;
532 }
533 Some(identity)
534}
535
536fn strict_mailer_guard_check(admin: &Admin) -> std::result::Result<(), String> {
537 if admin.active_recovery_policy().strict_mailer_required() && !admin.has_custom_mailer() {
538 Err(
539 "rustio-admin: RecoveryPolicy::strict_mailer_required() = true but no mailer \
540 was registered via Admin::mailer(...).\n\n\
541 The framework's default LogMailer writes recovery emails to log::info! instead \
542 of sending them, which is unsuitable for production. Recovery routes are NOT \
543 registered with this configuration.\n\n\
544 To resolve, choose one:\n\
545 (a) register a real mailer before calling register_admin_routes:\n\
546 Admin::mailer(Arc::new(MyProjectMailer::new(...)))\n\
547 (b) opt the policy out of strict mode (the framework default — dev / CI / \
548 testing baseline):\n\
549 RecoveryPolicy::strict_mailer_required(false)\n\n\
550 See DESIGN_RECOVERY.md §12.1 for the contract."
551 .to_string(),
552 )
553 } else {
554 Ok(())
555 }
556}
557
558pub fn register_admin_routes(
560 router: Router,
561 admin: Admin,
562 db: Db,
563 templates: Arc<Templates>,
564) -> Router {
565 if let Err(msg) = strict_mailer_guard_check(&admin) {
573 panic!("{msg}");
574 }
575
576 let ctx = Arc::new(AdminCtx::new(
577 Arc::new(admin),
578 db.clone(),
579 templates.clone(),
580 ));
581
582 let auth_ctx = Arc::new(super::builtin::AuthAdminCtx {
585 admin: ctx.admin.clone(),
586 db,
587 templates,
588 });
589
590 let err_admin = ctx.admin.clone();
605 let err_templates = ctx.templates.clone();
606 let err_db = ctx.db.clone();
607 let router = router.middleware(move |req, next| {
608 let admin = err_admin.clone();
609 let templates = err_templates.clone();
610 let db = err_db.clone();
611 Box::pin(async move {
612 let is_admin_path = req.path().starts_with("/admin");
613 let cookie_header = if is_admin_path {
619 req.header("cookie").map(|s| s.to_string())
620 } else {
621 None
622 };
623 let result = next.run(req).await;
624 match result {
625 Ok(resp) => Ok(resp),
626 Err(err) if is_admin_path => {
627 let identity = match cookie_header.as_deref() {
628 Some(cookie) => resolve_identity_for_error_page(&db, cookie).await,
629 None => None,
630 };
631 Ok(render::render_admin_error_response(
632 &admin,
633 &templates,
634 identity.as_ref(),
635 err.status(),
636 err.client_message().to_string(),
637 ))
638 }
639 Err(err) => Err(err),
640 }
641 })
642 });
643
644 let ro_flag = ctx.admin.is_read_only();
656 let ro_models = std::sync::Arc::new(ctx.admin.read_only_models.clone());
657 let router = router.middleware(move |req, next| {
658 let ro_models = ro_models.clone();
659 Box::pin(async move {
660 if req.path().starts_with("/admin")
661 && is_mutating_method(req.method())
662 && !is_read_only_writable_path(req.path())
663 {
664 if ro_flag {
667 return Err(Error::Forbidden(
668 "This admin is currently in read-only mode. \
669 Project-data mutations are disabled until the operator \
670 turns read-only off."
671 .into(),
672 ));
673 }
674 if !ro_models.is_empty() {
679 if let Some(slug) = extract_admin_name(req.path()) {
680 if ro_models.contains(slug) {
681 return Err(Error::Forbidden(format!(
682 "Model `{slug}` is frozen (read-only). \
683 Mutations on this model are disabled."
684 )));
685 }
686 }
687 }
688 }
689 next.run(req).await
690 })
691 });
692
693 let router = router.get("/static/admin.css", |_req| async move {
699 Ok(Response::new(
700 hyper::StatusCode::OK,
701 bytes::Bytes::from_static(ADMIN_CSS.as_bytes()),
702 )
703 .with_header("content-type", "text/css; charset=utf-8")
704 .with_header("cache-control", "no-cache, must-revalidate"))
705 });
706 let router = router.get("/static/admin.js", |_req| async move {
707 Ok(Response::new(
708 hyper::StatusCode::OK,
709 bytes::Bytes::from_static(ADMIN_JS.as_bytes()),
710 )
711 .with_header("content-type", "application/javascript; charset=utf-8")
712 .with_header("cache-control", "no-cache, must-revalidate"))
713 });
714
715 fn font_response(bytes: &'static [u8]) -> Response {
719 Response::new(hyper::StatusCode::OK, bytes::Bytes::from_static(bytes))
720 .with_header("content-type", "font/woff2")
721 .with_header("cache-control", "public, max-age=31536000, immutable")
722 }
723 let router = router.get("/static/fonts/Geist-Variable.woff2", |_req| async move {
724 Ok(font_response(FONT_GEIST))
725 });
726 let router = router.get(
727 "/static/fonts/GeistMono-Variable.woff2",
728 |_req| async move { Ok(font_response(FONT_GEIST_MONO)) },
729 );
730 let router = router.get("/static/fonts/Tajawal-Regular.woff2", |_req| async move {
731 Ok(font_response(FONT_TAJAWAL_REG))
732 });
733 let router = router.get("/static/fonts/Tajawal-Medium.woff2", |_req| async move {
734 Ok(font_response(FONT_TAJAWAL_MED))
735 });
736 let router = router.get("/static/fonts/Tajawal-Bold.woff2", |_req| async move {
737 Ok(font_response(FONT_TAJAWAL_BOLD))
738 });
739 let router = router.get(
740 "/static/fonts/NotoNaskhArabic-Variable.woff2",
741 |_req| async move { Ok(font_response(FONT_NOTO_NASKH_AR)) },
742 );
743 let router = router.get("/static/fonts/InterVariable.woff2", |_req| async move {
744 Ok(font_response(FONT_INTER))
745 });
746 let router = router.get(
747 "/static/fonts/NotoSansThai-Variable.woff2",
748 |_req| async move { Ok(font_response(FONT_NOTO_THAI)) },
749 );
750 let router = router.get(
751 "/static/fonts/NotoSansDevanagari-Variable.woff2",
752 |_req| async move { Ok(font_response(FONT_NOTO_DEVA)) },
753 );
754 let router = router.get(
755 "/static/fonts/NotoSansJP-Regular.woff2",
756 |_req| async move { Ok(font_response(FONT_NOTO_JP)) },
757 );
758 let router = router.get(
759 "/static/fonts/NotoSansKR-Regular.woff2",
760 |_req| async move { Ok(font_response(FONT_NOTO_KR)) },
761 );
762 let router = router.get(
763 "/static/fonts/NotoSansSC-Regular.woff2",
764 |_req| async move { Ok(font_response(FONT_NOTO_SC)) },
765 );
766
767 let c = ctx.clone();
772 let router = router.get("/admin/healthz", move |_req| {
773 let c = c.clone();
774 async move { super::healthz::healthz(&c.db).await }
775 });
776
777 let c = ctx.clone();
779 let router = router.get("/admin/login", move |req| {
780 let c = c.clone();
781 async move { handlers::show_login(&c, req).await }
782 });
783
784 let c = ctx.clone();
785 let router = router.post("/admin/login", move |req| {
786 let c = c.clone();
787 async move { handlers::do_login(&c, req).await }
788 });
789
790 let c = ctx.clone();
791 let router = router.post("/admin/logout", move |req| {
792 let c = c.clone();
793 async move { handlers::do_logout(&c, req).await }
794 });
795
796 let recovery_state = Arc::new(super::recovery_handlers::RecoveryState::from_admin(
813 &ctx.admin,
814 ));
815
816 let c = ctx.clone();
817 let router = router.get("/admin/forgot-password", move |req| {
818 let c = c.clone();
819 async move { super::recovery_handlers::show_forgot_password(&c, &req).await }
820 });
821
822 let c = ctx.clone();
823 let rs = recovery_state.clone();
824 let router = router.post("/admin/forgot-password", move |req| {
825 let c = c.clone();
826 let rs = rs.clone();
827 async move { super::recovery_handlers::do_forgot_password(&c, &rs, req).await }
828 });
829
830 let c = ctx.clone();
831 let router = router.get("/admin/forgot-password/sent", move |req| {
832 let c = c.clone();
833 async move { super::recovery_handlers::show_forgot_password_sent(&c, &req).await }
834 });
835
836 let c = ctx.clone();
837 let router = router.get("/admin/reset-password/:token", move |req| {
838 let c = c.clone();
839 async move {
840 let token = req
841 .param("token")
842 .ok_or_else(|| Error::BadRequest("missing token".into()))?
843 .to_string();
844 super::recovery_handlers::show_reset_password(&c, &req, &token).await
845 }
846 });
847
848 let c = ctx.clone();
849 let rs = recovery_state.clone();
850 let router = router.post("/admin/reset-password/:token", move |req| {
851 let c = c.clone();
852 let rs = rs.clone();
853 async move {
854 let token = req
855 .param("token")
856 .ok_or_else(|| Error::BadRequest("missing token".into()))?
857 .to_string();
858 super::recovery_handlers::do_reset_password(&c, &rs, req, &token).await
859 }
860 });
861
862 let c = ctx.clone();
864 let router = router.get("/admin", move |req| {
865 let c = c.clone();
866 async move {
867 match role_guard(&c, &req, Role::Staff).await? {
868 Guard::Redirect(r) => Ok(r),
869 Guard::Allow(ident) => handlers::dashboard(&c, ident, &req).await,
870 }
871 }
872 });
873
874 let c = ctx.clone();
880 let router = router.get("/admin/db", move |req| {
881 let c = c.clone();
882 async move {
883 match role_guard(&c, &req, Role::Developer).await? {
884 Guard::Redirect(r) => Ok(r),
885 Guard::Allow(ident) => super::db_browser::show_db_browser(&c, ident, &req).await,
886 }
887 }
888 });
889
890 let c = ctx.clone();
895 let router = router.get("/admin/notifications", move |req| {
896 let c = c.clone();
897 async move {
898 match role_guard(&c, &req, Role::Staff).await? {
899 Guard::Redirect(r) => Ok(r),
900 Guard::Allow(ident) => handlers::show_notifications(&c, ident, &req).await,
901 }
902 }
903 });
904 let c = ctx.clone();
905 let router = router.post("/admin/notifications/mark_all_read", move |req| {
906 let c = c.clone();
907 async move {
908 match role_guard(&c, &req, Role::Staff).await? {
909 Guard::Redirect(r) => Ok(r),
910 Guard::Allow(ident) => {
911 handlers::do_mark_all_notifications_read(&c, ident, req).await
912 }
913 }
914 }
915 });
916
917 let c = ctx.clone();
922 let router = router.get("/admin/feature_flags", move |req| {
923 let c = c.clone();
924 async move {
925 match role_guard(&c, &req, Role::Administrator).await? {
926 Guard::Redirect(r) => Ok(r),
927 Guard::Allow(ident) => handlers::show_feature_flags(&c, ident, &req).await,
928 }
929 }
930 });
931 let c = ctx.clone();
932 let router = router.post("/admin/feature_flags", move |req| {
933 let c = c.clone();
934 async move {
935 match role_guard(&c, &req, Role::Administrator).await? {
936 Guard::Redirect(r) => Ok(r),
937 Guard::Allow(ident) => handlers::do_create_feature_flag(&c, ident, req).await,
938 }
939 }
940 });
941 let c = ctx.clone();
942 let router = router.post("/admin/feature_flags/:key/toggle", move |req| {
943 let c = c.clone();
944 async move {
945 let key = req
946 .param("key")
947 .ok_or_else(|| Error::BadRequest("missing flag key".into()))?
948 .to_string();
949 match role_guard(&c, &req, Role::Administrator).await? {
950 Guard::Redirect(r) => Ok(r),
951 Guard::Allow(ident) => handlers::do_toggle_feature_flag(&c, ident, &key, req).await,
952 }
953 }
954 });
955
956 let c = ctx.clone();
962 let router = router.get("/admin/health", move |req| {
963 let c = c.clone();
964 async move {
965 match role_guard(&c, &req, Role::Administrator).await? {
966 Guard::Redirect(r) => Ok(r),
967 Guard::Allow(ident) => handlers::show_health(&c, ident, &req).await,
968 }
969 }
970 });
971
972 let c = ctx.clone();
974 let router = router.get("/admin/history", move |req| {
975 let c = c.clone();
976 async move {
977 match role_guard(&c, &req, Role::Administrator).await? {
978 Guard::Redirect(r) => Ok(r),
979 Guard::Allow(ident) => handlers::show_log_entries(&c, ident, &req).await,
980 }
981 }
982 });
983
984 let c = ctx.clone();
987 let router = router.get("/admin/account/sessions", move |req| {
988 let c = c.clone();
989 async move {
990 match role_guard(&c, &req, Role::User).await? {
991 Guard::Redirect(r) => Ok(r),
992 Guard::Allow(ident) => handlers::show_account_sessions(&c, ident, &req).await,
993 }
994 }
995 });
996
997 let c = ctx.clone();
1005 let router = router.post("/admin/account/sessions/revoke-others", move |req| {
1006 let c = c.clone();
1007 async move {
1008 match role_guard(&c, &req, Role::User).await? {
1009 Guard::Redirect(r) => Ok(r),
1010 Guard::Allow(ident) => handlers::do_revoke_other_sessions(&c, ident, req).await,
1011 }
1012 }
1013 });
1014
1015 let c = ctx.clone();
1016 let router = router.post("/admin/account/sessions/revoke-all", move |req| {
1017 let c = c.clone();
1018 async move {
1019 match role_guard(&c, &req, Role::User).await? {
1020 Guard::Redirect(r) => Ok(r),
1021 Guard::Allow(ident) => handlers::do_revoke_all_sessions(&c, ident, req).await,
1022 }
1023 }
1024 });
1025
1026 let c = ctx.clone();
1027 let router = router.post("/admin/account/sessions/:id/revoke", move |req| {
1028 let c = c.clone();
1029 async move {
1030 match role_guard(&c, &req, Role::User).await? {
1031 Guard::Redirect(r) => Ok(r),
1032 Guard::Allow(ident) => {
1033 let id = parse_id(req.param("id"))?;
1034 handlers::do_revoke_session(&c, ident, req, id).await
1035 }
1036 }
1037 }
1038 });
1039
1040 let c = ctx.clone();
1044 let router = router.get("/admin/password_change", move |req| {
1045 let c = c.clone();
1046 async move {
1047 match role_guard(&c, &req, Role::User).await? {
1048 Guard::Redirect(r) => Ok(r),
1049 Guard::Allow(ident) => handlers::show_password_change(&c, ident, &req).await,
1050 }
1051 }
1052 });
1053 let c = ctx.clone();
1054 let router = router.post("/admin/password_change", move |req| {
1055 let c = c.clone();
1056 async move {
1057 match role_guard(&c, &req, Role::User).await? {
1058 Guard::Redirect(r) => Ok(r),
1059 Guard::Allow(ident) => handlers::do_password_change(&c, ident, req).await,
1060 }
1061 }
1062 });
1063
1064 let c = ctx.clone();
1073 let router = router.get("/admin/reauth", move |req| {
1074 let c = c.clone();
1075 async move {
1076 match role_guard(&c, &req, Role::User).await? {
1077 Guard::Redirect(r) => Ok(r),
1078 Guard::Allow(ident) => {
1079 super::admin_recovery_handlers::show_reauth(&c, ident, &req).await
1080 }
1081 }
1082 }
1083 });
1084
1085 let c = ctx.clone();
1086 let router = router.post("/admin/reauth", move |req| {
1087 let c = c.clone();
1088 async move {
1089 match role_guard(&c, &req, Role::User).await? {
1090 Guard::Redirect(r) => Ok(r),
1091 Guard::Allow(ident) => {
1092 super::admin_recovery_handlers::do_reauth(&c, ident, req).await
1093 }
1094 }
1095 }
1096 });
1097
1098 let c = ctx.clone();
1108 let router = router.get("/admin/must-change-password", move |req| {
1109 let c = c.clone();
1110 async move {
1111 match role_guard(&c, &req, Role::User).await? {
1112 Guard::Redirect(r) => Ok(r),
1113 Guard::Allow(ident) => {
1114 super::admin_recovery_handlers::show_must_change_password(&c, ident, &req).await
1115 }
1116 }
1117 }
1118 });
1119
1120 let c = ctx.clone();
1121 let router = router.post("/admin/must-change-password", move |req| {
1122 let c = c.clone();
1123 async move {
1124 match role_guard(&c, &req, Role::User).await? {
1125 Guard::Redirect(r) => Ok(r),
1126 Guard::Allow(ident) => {
1127 super::admin_recovery_handlers::do_must_change_password(&c, ident, req).await
1128 }
1129 }
1130 }
1131 });
1132
1133 let c = ctx.clone();
1151 let router = router.get("/admin/mfa/verify", move |req| {
1152 let c = c.clone();
1153 async move {
1154 match role_guard(&c, &req, Role::User).await? {
1155 Guard::Redirect(r) => Ok(r),
1156 Guard::Allow(ident) => super::mfa_handlers::show_verify(&c, ident, &req).await,
1157 }
1158 }
1159 });
1160
1161 let c = ctx.clone();
1162 let router = router.post("/admin/mfa/verify", move |req| {
1163 let c = c.clone();
1164 async move {
1165 match role_guard(&c, &req, Role::User).await? {
1166 Guard::Redirect(r) => Ok(r),
1167 Guard::Allow(ident) => super::mfa_handlers::do_verify(&c, ident, req).await,
1168 }
1169 }
1170 });
1171
1172 let c = ctx.clone();
1174 let router = router.get("/admin/account/mfa/enroll", move |req| {
1175 let c = c.clone();
1176 async move {
1177 match role_guard(&c, &req, Role::User).await? {
1178 Guard::Redirect(r) => Ok(r),
1179 Guard::Allow(ident) => super::mfa_handlers::show_enroll(&c, ident, &req).await,
1180 }
1181 }
1182 });
1183
1184 let c = ctx.clone();
1185 let router = router.post("/admin/account/mfa/enroll", move |req| {
1186 let c = c.clone();
1187 async move {
1188 match role_guard(&c, &req, Role::User).await? {
1189 Guard::Redirect(r) => Ok(r),
1190 Guard::Allow(ident) => super::mfa_handlers::do_enroll(&c, ident, req).await,
1191 }
1192 }
1193 });
1194
1195 let c = ctx.clone();
1197 let router = router.get("/admin/account/mfa/regenerate-codes", move |req| {
1198 let c = c.clone();
1199 async move {
1200 match role_guard(&c, &req, Role::User).await? {
1201 Guard::Redirect(r) => Ok(r),
1202 Guard::Allow(ident) => super::mfa_handlers::show_regenerate(&c, ident, &req).await,
1203 }
1204 }
1205 });
1206
1207 let c = ctx.clone();
1208 let router = router.post("/admin/account/mfa/regenerate-codes", move |req| {
1209 let c = c.clone();
1210 async move {
1211 match role_guard(&c, &req, Role::User).await? {
1212 Guard::Redirect(r) => Ok(r),
1213 Guard::Allow(ident) => super::mfa_handlers::do_regenerate(&c, ident, req).await,
1214 }
1215 }
1216 });
1217
1218 let c = ctx.clone();
1220 let router = router.get("/admin/account/mfa/disable", move |req| {
1221 let c = c.clone();
1222 async move {
1223 match role_guard(&c, &req, Role::User).await? {
1224 Guard::Redirect(r) => Ok(r),
1225 Guard::Allow(ident) => super::mfa_handlers::show_disable(&c, ident, &req).await,
1226 }
1227 }
1228 });
1229
1230 let c = ctx.clone();
1231 let router = router.post("/admin/account/mfa/disable", move |req| {
1232 let c = c.clone();
1233 async move {
1234 match role_guard(&c, &req, Role::User).await? {
1235 Guard::Redirect(r) => Ok(r),
1236 Guard::Allow(ident) => super::mfa_handlers::do_disable(&c, ident, req).await,
1237 }
1238 }
1239 });
1240
1241 let c = ctx.clone();
1243 let ac = auth_ctx.clone();
1244 let router = router.get("/admin/users", move |req| {
1245 let c = c.clone();
1246 let ac = ac.clone();
1247 async move {
1248 match role_guard(&c, &req, Role::Administrator).await? {
1249 Guard::Redirect(r) => Ok(r),
1250 Guard::Allow(ident) => {
1251 super::builtin::list_users(&ac, ident, handlers::csrf_token(&req)).await
1252 }
1253 }
1254 }
1255 });
1256
1257 let c = ctx.clone();
1258 let ac = auth_ctx.clone();
1259 let router = router.get("/admin/users/new", move |req| {
1260 let c = c.clone();
1261 let ac = ac.clone();
1262 async move {
1263 match role_guard(&c, &req, Role::Administrator).await? {
1264 Guard::Redirect(r) => Ok(r),
1265 Guard::Allow(ident) => {
1266 super::builtin::show_new_user(&ac, ident, handlers::csrf_token(&req)).await
1267 }
1268 }
1269 }
1270 });
1271
1272 let c = ctx.clone();
1273 let ac = auth_ctx.clone();
1274 let router = router.post("/admin/users/new", move |req| {
1275 let c = c.clone();
1276 let ac = ac.clone();
1277 async move {
1278 match role_guard(&c, &req, Role::Administrator).await? {
1279 Guard::Redirect(r) => Ok(r),
1280 Guard::Allow(ident) => super::builtin::do_new_user(&ac, ident, req).await,
1281 }
1282 }
1283 });
1284
1285 let c = ctx.clone();
1286 let ac = auth_ctx.clone();
1287 let router = router.get("/admin/users/:id/edit", move |req| {
1288 let c = c.clone();
1289 let ac = ac.clone();
1290 async move {
1291 match role_guard(&c, &req, Role::Administrator).await? {
1292 Guard::Redirect(r) => Ok(r),
1293 Guard::Allow(ident) => {
1294 let id = parse_id(req.param("id"))?;
1295 super::builtin::show_user_edit(&ac, ident, id, handlers::csrf_token(&req)).await
1296 }
1297 }
1298 }
1299 });
1300
1301 let c = ctx.clone();
1302 let ac = auth_ctx.clone();
1303 let router = router.post("/admin/users/:id/edit", move |req| {
1304 let c = c.clone();
1305 let ac = ac.clone();
1306 async move {
1307 match role_guard(&c, &req, Role::Administrator).await? {
1308 Guard::Redirect(r) => Ok(r),
1309 Guard::Allow(ident) => {
1310 let id = parse_id(req.param("id"))?;
1311 super::builtin::do_user_edit(&ac, ident, id, req).await
1312 }
1313 }
1314 }
1315 });
1316
1317 let c = ctx.clone();
1318 let ac = auth_ctx.clone();
1319 let router = router.get("/admin/users/:id/delete", move |req| {
1320 let c = c.clone();
1321 let ac = ac.clone();
1322 async move {
1323 match role_guard(&c, &req, Role::Administrator).await? {
1324 Guard::Redirect(r) => Ok(r),
1325 Guard::Allow(ident) => {
1326 let id = parse_id(req.param("id"))?;
1327 super::builtin::show_user_delete(&ac, ident, id, handlers::csrf_token(&req))
1328 .await
1329 }
1330 }
1331 }
1332 });
1333
1334 let c = ctx.clone();
1335 let ac = auth_ctx.clone();
1336 let router = router.post("/admin/users/:id/delete", move |req| {
1337 let c = c.clone();
1338 let ac = ac.clone();
1339 async move {
1340 match role_guard(&c, &req, Role::Administrator).await? {
1341 Guard::Redirect(r) => Ok(r),
1342 Guard::Allow(ident) => {
1343 let id = parse_id(req.param("id"))?;
1344 super::builtin::do_user_delete(&ac, ident, id, req).await
1345 }
1346 }
1347 }
1348 });
1349
1350 let c = ctx.clone();
1366 let router = router.get("/admin/users/:id/reset-password", move |req| {
1367 let c = c.clone();
1368 async move {
1369 match role_guard(&c, &req, Role::Administrator).await? {
1370 Guard::Redirect(r) => Ok(r),
1371 Guard::Allow(ident) => {
1372 let id = parse_id(req.param("id"))?;
1373 super::admin_recovery_handlers::show_admin_reset_password(&c, ident, id, &req)
1374 .await
1375 }
1376 }
1377 }
1378 });
1379
1380 let c = ctx.clone();
1382 let router = router.post("/admin/users/:id/reset-password", move |req| {
1383 let c = c.clone();
1384 async move {
1385 match role_guard(&c, &req, Role::Administrator).await? {
1386 Guard::Redirect(r) => Ok(r),
1387 Guard::Allow(ident) => {
1388 let id = parse_id(req.param("id"))?;
1389 super::admin_recovery_handlers::do_admin_reset_password(&c, ident, id, req)
1390 .await
1391 }
1392 }
1393 }
1394 });
1395
1396 let c = ctx.clone();
1398 let router = router.get("/admin/users/:id/lock", move |req| {
1399 let c = c.clone();
1400 async move {
1401 match role_guard(&c, &req, Role::Administrator).await? {
1402 Guard::Redirect(r) => Ok(r),
1403 Guard::Allow(ident) => {
1404 let id = parse_id(req.param("id"))?;
1405 super::admin_recovery_handlers::show_lock_user(&c, ident, id, &req).await
1406 }
1407 }
1408 }
1409 });
1410
1411 let c = ctx.clone();
1413 let router = router.post("/admin/users/:id/lock", move |req| {
1414 let c = c.clone();
1415 async move {
1416 match role_guard(&c, &req, Role::Administrator).await? {
1417 Guard::Redirect(r) => Ok(r),
1418 Guard::Allow(ident) => {
1419 let id = parse_id(req.param("id"))?;
1420 super::admin_recovery_handlers::do_lock_user(&c, ident, id, req).await
1421 }
1422 }
1423 }
1424 });
1425
1426 let c = ctx.clone();
1428 let router = router.get("/admin/users/:id/unlock", move |req| {
1429 let c = c.clone();
1430 async move {
1431 match role_guard(&c, &req, Role::Administrator).await? {
1432 Guard::Redirect(r) => Ok(r),
1433 Guard::Allow(ident) => {
1434 let id = parse_id(req.param("id"))?;
1435 super::admin_recovery_handlers::show_unlock_user(&c, ident, id, &req).await
1436 }
1437 }
1438 }
1439 });
1440
1441 let c = ctx.clone();
1443 let router = router.post("/admin/users/:id/unlock", move |req| {
1444 let c = c.clone();
1445 async move {
1446 match role_guard(&c, &req, Role::Administrator).await? {
1447 Guard::Redirect(r) => Ok(r),
1448 Guard::Allow(ident) => {
1449 let id = parse_id(req.param("id"))?;
1450 super::admin_recovery_handlers::do_unlock_user(&c, ident, id, req).await
1451 }
1452 }
1453 }
1454 });
1455
1456 let c = ctx.clone();
1459 let router = router.get("/admin/users/:id/revoke-sessions", move |req| {
1460 let c = c.clone();
1461 async move {
1462 match role_guard(&c, &req, Role::Administrator).await? {
1463 Guard::Redirect(r) => Ok(r),
1464 Guard::Allow(ident) => {
1465 let id = parse_id(req.param("id"))?;
1466 super::admin_recovery_handlers::show_admin_revoke_sessions(&c, ident, id, &req)
1467 .await
1468 }
1469 }
1470 }
1471 });
1472
1473 let c = ctx.clone();
1475 let router = router.post("/admin/users/:id/revoke-sessions", move |req| {
1476 let c = c.clone();
1477 async move {
1478 match role_guard(&c, &req, Role::Administrator).await? {
1479 Guard::Redirect(r) => Ok(r),
1480 Guard::Allow(ident) => {
1481 let id = parse_id(req.param("id"))?;
1482 super::admin_recovery_handlers::do_admin_revoke_sessions(&c, ident, id, req)
1483 .await
1484 }
1485 }
1486 }
1487 });
1488
1489 let c = ctx.clone();
1496 let router = router.post("/admin/users/:id/sessions/:session_id/revoke", move |req| {
1497 let c = c.clone();
1498 async move {
1499 match role_guard(&c, &req, Role::Administrator).await? {
1500 Guard::Redirect(r) => Ok(r),
1501 Guard::Allow(ident) => {
1502 let user_id = parse_id(req.param("id"))?;
1503 let session_id = parse_id(req.param("session_id"))?;
1504 super::admin_recovery_handlers::do_admin_revoke_one_session(
1505 &c, ident, user_id, session_id, req,
1506 )
1507 .await
1508 }
1509 }
1510 }
1511 });
1512
1513 let c = ctx.clone();
1520 let ac = auth_ctx.clone();
1521 let router = router.get("/admin/users/:id", move |req| {
1522 let c = c.clone();
1523 let ac = ac.clone();
1524 async move {
1525 match role_guard(&c, &req, Role::Administrator).await? {
1526 Guard::Redirect(r) => Ok(r),
1527 Guard::Allow(ident) => {
1528 let id = parse_id(req.param("id"))?;
1529 let q = req.query();
1530 let tab = q.get("tab").map(|s| s.to_string());
1531 let page: i64 = q.get("page").and_then(|s| s.parse().ok()).unwrap_or(1);
1532 let viewing_session_id = match req
1533 .header("cookie")
1534 .and_then(crate::auth::session_token_from_cookie)
1535 {
1536 Some(token) => crate::auth::current_session_id(&ac.db, &token)
1537 .await
1538 .ok()
1539 .flatten(),
1540 None => None,
1541 };
1542 super::builtin::show_user_view(
1543 &ac,
1544 ident,
1545 id,
1546 handlers::csrf_token(&req),
1547 tab,
1548 page,
1549 viewing_session_id,
1550 )
1551 .await
1552 }
1553 }
1554 }
1555 });
1556
1557 let c = ctx.clone();
1559 let ac = auth_ctx.clone();
1560 let router = router.get("/admin/groups", move |req| {
1561 let c = c.clone();
1562 let ac = ac.clone();
1563 async move {
1564 match role_guard(&c, &req, Role::Administrator).await? {
1565 Guard::Redirect(r) => Ok(r),
1566 Guard::Allow(ident) => {
1567 super::builtin::list_groups(&ac, ident, handlers::csrf_token(&req)).await
1568 }
1569 }
1570 }
1571 });
1572
1573 let c = ctx.clone();
1574 let ac = auth_ctx.clone();
1575 let router = router.get("/admin/groups/new", move |req| {
1576 let c = c.clone();
1577 let ac = ac.clone();
1578 async move {
1579 match role_guard(&c, &req, Role::Administrator).await? {
1580 Guard::Redirect(r) => Ok(r),
1581 Guard::Allow(ident) => {
1582 super::builtin::show_new_group(&ac, ident, handlers::csrf_token(&req)).await
1583 }
1584 }
1585 }
1586 });
1587
1588 let c = ctx.clone();
1589 let ac = auth_ctx.clone();
1590 let router = router.post("/admin/groups/new", move |req| {
1591 let c = c.clone();
1592 let ac = ac.clone();
1593 async move {
1594 match role_guard(&c, &req, Role::Administrator).await? {
1595 Guard::Redirect(r) => Ok(r),
1596 Guard::Allow(ident) => super::builtin::do_new_group(&ac, ident, req).await,
1597 }
1598 }
1599 });
1600
1601 let c = ctx.clone();
1602 let ac = auth_ctx.clone();
1603 let router = router.get("/admin/groups/:id/edit", move |req| {
1604 let c = c.clone();
1605 let ac = ac.clone();
1606 async move {
1607 match role_guard(&c, &req, Role::Administrator).await? {
1608 Guard::Redirect(r) => Ok(r),
1609 Guard::Allow(ident) => {
1610 let id = parse_id(req.param("id"))?;
1611 super::builtin::show_group_edit(&ac, ident, id, handlers::csrf_token(&req))
1612 .await
1613 }
1614 }
1615 }
1616 });
1617
1618 let c = ctx.clone();
1619 let ac = auth_ctx.clone();
1620 let router = router.post("/admin/groups/:id/edit", move |req| {
1621 let c = c.clone();
1622 let ac = ac.clone();
1623 async move {
1624 match role_guard(&c, &req, Role::Administrator).await? {
1625 Guard::Redirect(r) => Ok(r),
1626 Guard::Allow(ident) => {
1627 let id = parse_id(req.param("id"))?;
1628 super::builtin::do_group_edit(&ac, ident, id, req).await
1629 }
1630 }
1631 }
1632 });
1633
1634 let c = ctx.clone();
1635 let ac = auth_ctx.clone();
1636 let router = router.get("/admin/groups/:id/delete", move |req| {
1637 let c = c.clone();
1638 let ac = ac.clone();
1639 async move {
1640 match role_guard(&c, &req, Role::Administrator).await? {
1641 Guard::Redirect(r) => Ok(r),
1642 Guard::Allow(ident) => {
1643 let id = parse_id(req.param("id"))?;
1644 super::builtin::show_group_delete(&ac, ident, id, handlers::csrf_token(&req))
1645 .await
1646 }
1647 }
1648 }
1649 });
1650
1651 let c = ctx.clone();
1652 let ac = auth_ctx.clone();
1653 let router = router.post("/admin/groups/:id/delete", move |req| {
1654 let c = c.clone();
1655 let ac = ac.clone();
1656 async move {
1657 match role_guard(&c, &req, Role::Administrator).await? {
1658 Guard::Redirect(r) => Ok(r),
1659 Guard::Allow(ident) => {
1660 let id = parse_id(req.param("id"))?;
1661 super::builtin::do_group_delete(&ac, ident, id, req).await
1662 }
1663 }
1664 }
1665 });
1666
1667 let c = ctx.clone();
1676 let router = router.get("/admin/uploads/:filename", move |req| {
1677 let c = c.clone();
1678 async move {
1679 match role_guard(&c, &req, Role::Staff).await? {
1680 Guard::Redirect(r) => Ok(r),
1681 Guard::Allow(ident) => {
1682 let filename = req
1683 .param("filename")
1684 .map(str::to_string)
1685 .unwrap_or_default();
1686 handlers::serve_upload(&c, ident, &filename, req).await
1687 }
1688 }
1689 }
1690 });
1691
1692 let c = ctx.clone();
1698 let router = router.get("/admin/_lookup/:admin_name", move |req| {
1699 let c = c.clone();
1700 async move {
1701 let name = model_name_from_req(&req)?;
1702 let perm = perm_for(&c, &name, "view")?;
1703 match perm_guard(&c, &req, &perm).await? {
1704 Guard::Redirect(r) => Ok(r),
1705 Guard::Allow(ident) => handlers::lookup_model(&c, ident, &name, req).await,
1706 }
1707 }
1708 });
1709
1710 let c = ctx.clone();
1718 let router = router.get("/admin/_search", move |req| {
1719 let c = c.clone();
1720 async move {
1721 match role_guard(&c, &req, Role::Staff).await? {
1722 Guard::Redirect(r) => Ok(r),
1723 Guard::Allow(ident) => handlers::search_models(&c, ident, req).await,
1724 }
1725 }
1726 });
1727
1728 let c = ctx.clone();
1736 let router = router.get("/admin/docs", move |req| {
1737 let c = c.clone();
1738 async move {
1739 match role_guard(&c, &req, Role::Staff).await? {
1740 Guard::Redirect(r) => Ok(r),
1741 Guard::Allow(ident) => handlers::show_docs_index(&c, ident, &req).await,
1742 }
1743 }
1744 });
1745 let c = ctx.clone();
1746 let router = router.get("/admin/docs/:slug", move |req| {
1747 let c = c.clone();
1748 async move {
1749 let slug = req
1750 .param("slug")
1751 .ok_or_else(|| Error::BadRequest("missing doc slug".into()))?
1752 .to_string();
1753 match role_guard(&c, &req, Role::Staff).await? {
1754 Guard::Redirect(r) => Ok(r),
1755 Guard::Allow(ident) => handlers::show_doc_page(&c, ident, &slug, &req).await,
1756 }
1757 }
1758 });
1759
1760 let c = ctx.clone();
1767 let router = router.get("/admin/apis/openapi.json", move |req| {
1768 let c = c.clone();
1769 async move {
1770 match role_guard(&c, &req, Role::Staff).await? {
1771 Guard::Redirect(r) => Ok(r),
1772 Guard::Allow(_) => {
1773 let spec = super::openapi::build_spec(&c.admin);
1774 super::json_api::json_response(spec)
1775 }
1776 }
1777 }
1778 });
1779
1780 let c = ctx.clone();
1786 let router = router.get("/admin/apis/sdk.ts", move |req| {
1787 let c = c.clone();
1788 async move {
1789 match role_guard(&c, &req, Role::Staff).await? {
1790 Guard::Redirect(r) => Ok(r),
1791 Guard::Allow(_) => {
1792 let body = super::sdk_gen::build_typescript(&c.admin);
1793 Ok(crate::http::Response::ok(body)
1794 .with_header("content-type", "text/typescript; charset=utf-8")
1795 .with_header(
1796 "content-disposition",
1797 "attachment; filename=\"rustio-sdk.ts\"",
1798 ))
1799 }
1800 }
1801 }
1802 });
1803
1804 let c = ctx.clone();
1810 let router = router.get("/admin/apis", move |req| {
1811 let c = c.clone();
1812 async move {
1813 match role_guard(&c, &req, Role::Staff).await? {
1814 Guard::Redirect(r) => Ok(r),
1815 Guard::Allow(ident) => handlers::show_apis_index(&c, ident, &req).await,
1816 }
1817 }
1818 });
1819
1820 let c = ctx.clone();
1828 let router = router.get("/admin/apis/playground", move |req| {
1829 let c = c.clone();
1830 async move {
1831 match role_guard(&c, &req, Role::Staff).await? {
1832 Guard::Redirect(r) => Ok(r),
1833 Guard::Allow(ident) => handlers::show_apis_playground(&c, ident, &req).await,
1834 }
1835 }
1836 });
1837
1838 let c = ctx.clone();
1840 let router = router.get("/admin/:admin_name", move |req| {
1841 let c = c.clone();
1842 async move {
1843 let name = model_name_from_req(&req)?;
1844 let perm = perm_for(&c, &name, "view")?;
1845 match perm_guard(&c, &req, &perm).await? {
1846 Guard::Redirect(r) => Ok(r),
1847 Guard::Allow(ident) => handlers::list_model(&c, ident, &name, &req).await,
1848 }
1849 }
1850 });
1851
1852 let c = ctx.clone();
1857 let router = router.get("/admin/:admin_name/export.csv", move |req| {
1858 let c = c.clone();
1859 async move {
1860 let name = model_name_from_req(&req)?;
1861 let perm = perm_for(&c, &name, "view")?;
1862 match perm_guard(&c, &req, &perm).await? {
1863 Guard::Redirect(r) => Ok(r),
1864 Guard::Allow(ident) => handlers::export_model_csv(&c, ident, &name, req).await,
1865 }
1866 }
1867 });
1868
1869 let c = ctx.clone();
1875 let router = router.post("/admin/:admin_name/import.csv", move |req| {
1876 let c = c.clone();
1877 async move {
1878 let name = model_name_from_req(&req)?;
1879 let perm = perm_for(&c, &name, "change")?;
1880 match perm_guard(&c, &req, &perm).await? {
1881 Guard::Redirect(r) => Ok(r),
1882 Guard::Allow(ident) => handlers::import_model_csv(&c, ident, &name, req).await,
1883 }
1884 }
1885 });
1886
1887 let c = ctx.clone();
1893 let router = router.post("/admin/:admin_name/saved_filters", move |req| {
1894 let c = c.clone();
1895 async move {
1896 let name = model_name_from_req(&req)?;
1897 let perm = perm_for(&c, &name, "view")?;
1898 match perm_guard(&c, &req, &perm).await? {
1899 Guard::Redirect(r) => Ok(r),
1900 Guard::Allow(ident) => handlers::do_save_filter(&c, ident, &name, req).await,
1901 }
1902 }
1903 });
1904
1905 let c = ctx.clone();
1909 let router = router.post("/admin/:admin_name/saved_filters/:id/delete", move |req| {
1910 let c = c.clone();
1911 async move {
1912 let name = model_name_from_req(&req)?;
1913 let id = parse_id(req.param("id"))?;
1914 let perm = perm_for(&c, &name, "view")?;
1915 match perm_guard(&c, &req, &perm).await? {
1916 Guard::Redirect(r) => Ok(r),
1917 Guard::Allow(ident) => handlers::do_delete_filter(&c, ident, &name, id, req).await,
1918 }
1919 }
1920 });
1921
1922 let c = ctx.clone();
1924 let router = router.get("/admin/:admin_name/new", move |req| {
1925 let c = c.clone();
1926 async move {
1927 let name = model_name_from_req(&req)?;
1928 let perm = perm_for(&c, &name, "add")?;
1929 match perm_guard(&c, &req, &perm).await? {
1930 Guard::Redirect(r) => Ok(r),
1931 Guard::Allow(ident) => handlers::show_new_form(&c, ident, &name, &req).await,
1932 }
1933 }
1934 });
1935 let c = ctx.clone();
1936 let router = router.post("/admin/:admin_name/new", move |req| {
1937 let c = c.clone();
1938 async move {
1939 let name = model_name_from_req(&req)?;
1940 let perm = perm_for(&c, &name, "add")?;
1941 match perm_guard(&c, &req, &perm).await? {
1942 Guard::Redirect(r) => Ok(r),
1943 Guard::Allow(ident) => handlers::do_create(&c, ident, &name, req).await,
1944 }
1945 }
1946 });
1947
1948 let c = ctx.clone();
1955 let router = router.get("/admin/:admin_name/:id", move |req| {
1956 let c = c.clone();
1957 async move {
1958 let name = model_name_from_req(&req)?;
1959 let perm = perm_for(&c, &name, "view")?;
1960 match perm_guard(&c, &req, &perm).await? {
1961 Guard::Redirect(r) => Ok(r),
1962 Guard::Allow(ident) => {
1963 let id = parse_id(req.param("id"))?;
1964 handlers::show_object_json(&c, ident, &name, id, &req).await
1965 }
1966 }
1967 }
1968 });
1969
1970 let c = ctx.clone();
1972 let router = router.get("/admin/:admin_name/:id/edit", move |req| {
1973 let c = c.clone();
1974 async move {
1975 let name = model_name_from_req(&req)?;
1976 let perm = perm_for(&c, &name, "change")?;
1977 match perm_guard(&c, &req, &perm).await? {
1978 Guard::Redirect(r) => Ok(r),
1979 Guard::Allow(ident) => {
1980 let id = parse_id(req.param("id"))?;
1981 handlers::show_edit_form(&c, ident, &name, id, &req).await
1982 }
1983 }
1984 }
1985 });
1986 let c = ctx.clone();
1987 let router = router.post("/admin/:admin_name/:id/edit", move |req| {
1988 let c = c.clone();
1989 async move {
1990 let name = model_name_from_req(&req)?;
1991 let perm = perm_for(&c, &name, "change")?;
1992 match perm_guard(&c, &req, &perm).await? {
1993 Guard::Redirect(r) => Ok(r),
1994 Guard::Allow(ident) => {
1995 let id = parse_id(req.param("id"))?;
1996 handlers::do_update(&c, ident, &name, id, req).await
1997 }
1998 }
1999 }
2000 });
2001
2002 let c = ctx.clone();
2005 let router = router.get("/admin/:admin_name/:id/history", move |req| {
2006 let c = c.clone();
2007 async move {
2008 let name = model_name_from_req(&req)?;
2009 let perm = perm_for(&c, &name, "view")?;
2010 match perm_guard(&c, &req, &perm).await? {
2011 Guard::Redirect(r) => Ok(r),
2012 Guard::Allow(ident) => {
2013 let id = parse_id(req.param("id"))?;
2014 handlers::show_object_history(&c, ident, &name, id, &req).await
2015 }
2016 }
2017 }
2018 });
2019
2020 let c = ctx.clone();
2022 let router = router.get("/admin/:admin_name/:id/delete", move |req| {
2023 let c = c.clone();
2024 async move {
2025 let name = model_name_from_req(&req)?;
2026 let perm = perm_for(&c, &name, "delete")?;
2027 match perm_guard(&c, &req, &perm).await? {
2028 Guard::Redirect(r) => Ok(r),
2029 Guard::Allow(ident) => {
2030 let id = parse_id(req.param("id"))?;
2031 handlers::show_delete_confirm(&c, ident, &name, id, &req).await
2032 }
2033 }
2034 }
2035 });
2036 let c = ctx.clone();
2037 let router = router.post("/admin/:admin_name/:id/delete", move |req| {
2038 let c = c.clone();
2039 async move {
2040 let name = model_name_from_req(&req)?;
2041 let perm = perm_for(&c, &name, "delete")?;
2042 match perm_guard(&c, &req, &perm).await? {
2043 Guard::Redirect(r) => Ok(r),
2044 Guard::Allow(ident) => {
2045 let id = parse_id(req.param("id"))?;
2046 handlers::do_delete(&c, ident, &name, req, id).await
2047 }
2048 }
2049 }
2050 });
2051
2052 let c = ctx.clone();
2057 let router = router.post("/admin/:admin_name/bulk_delete", move |req| {
2058 let c = c.clone();
2059 async move {
2060 let name = model_name_from_req(&req)?;
2061 let perm = perm_for(&c, &name, "delete")?;
2062 match perm_guard(&c, &req, &perm).await? {
2063 Guard::Redirect(r) => Ok(r),
2064 Guard::Allow(ident) => handlers::handle_bulk_delete(&c, ident, &name, &req).await,
2065 }
2066 }
2067 });
2068
2069 let c = ctx.clone();
2074 router.post("/admin/:admin_name/bulk/:action", move |req| {
2075 let c = c.clone();
2076 async move {
2077 let name = model_name_from_req(&req)?;
2078 let action = req
2079 .param("action")
2080 .ok_or_else(|| Error::BadRequest("missing bulk action name".into()))?
2081 .to_string();
2082 let perm = perm_for(&c, &name, "change")?;
2083 match perm_guard(&c, &req, &perm).await? {
2084 Guard::Redirect(r) => Ok(r),
2085 Guard::Allow(ident) => {
2086 handlers::handle_bulk_action(&c, ident, &name, &action, &req).await
2087 }
2088 }
2089 }
2090 })
2091}
2092
2093#[cfg(test)]
2094mod tests {
2095 use super::*;
2096
2097 fn make_identity(role: Role, is_active: bool) -> Identity {
2098 Identity {
2099 user_id: 42,
2100 email: "test@example.com".into(),
2101 role,
2102 is_active,
2103 is_demo: false,
2104 demo_label: None,
2105 must_change_password: false,
2106 mfa_enabled: false,
2107 trust_level: crate::auth::SessionTrust::Authenticated,
2108 }
2109 }
2110
2111 #[test]
2116 fn role_guard_decision_admin_meets_staff_floor() {
2117 let id = make_identity(Role::Administrator, true);
2118 assert!(id.role.includes(Role::Staff));
2119 }
2120
2121 #[test]
2122 fn role_guard_decision_user_does_not_meet_staff() {
2123 let id = make_identity(Role::User, true);
2124 assert!(!id.role.includes(Role::Staff));
2125 }
2126
2127 #[test]
2128 fn role_guard_decision_administrator_does_not_meet_developer() {
2129 let id = make_identity(Role::Administrator, true);
2130 assert!(!id.role.includes(Role::Developer));
2131 }
2132
2133 #[test]
2134 fn role_guard_decision_developer_meets_everything() {
2135 let id = make_identity(Role::Developer, true);
2136 for &min in &[
2137 Role::User,
2138 Role::Staff,
2139 Role::Supervisor,
2140 Role::Administrator,
2141 Role::Developer,
2142 ] {
2143 assert!(id.role.includes(min), "Developer should meet {min:?}");
2144 }
2145 }
2146
2147 #[test]
2150 fn perm_guard_admin_short_circuits_without_perm() {
2151 let id = make_identity(Role::Administrator, true);
2152 assert!(perm_guard_verdict(&id, false));
2153 }
2154
2155 #[test]
2156 fn perm_guard_developer_short_circuits_without_perm() {
2157 let id = make_identity(Role::Developer, true);
2158 assert!(perm_guard_verdict(&id, false));
2159 }
2160
2161 #[test]
2162 fn perm_guard_staff_with_perm_passes() {
2163 let id = make_identity(Role::Staff, true);
2164 assert!(perm_guard_verdict(&id, true));
2165 }
2166
2167 #[test]
2168 fn perm_guard_staff_without_perm_denies() {
2169 let id = make_identity(Role::Staff, true);
2170 assert!(!perm_guard_verdict(&id, false));
2171 }
2172
2173 #[test]
2174 fn perm_guard_inactive_admin_denies_even_with_bypass() {
2175 let id = make_identity(Role::Administrator, false);
2177 assert!(!perm_guard_verdict(&id, true));
2178 }
2179
2180 #[test]
2181 fn perm_guard_supervisor_without_perm_denies() {
2182 let id = make_identity(Role::Supervisor, true);
2184 assert!(!perm_guard_verdict(&id, false));
2185 }
2186
2187 #[test]
2192 fn strict_mailer_guard_passes_for_default_admin() {
2193 let admin = super::super::types::Admin::new();
2194 assert!(strict_mailer_guard_check(&admin).is_ok());
2195 }
2196
2197 #[test]
2200 fn strict_mailer_guard_fails_when_required_but_default_mailer() {
2201 use crate::auth::DefaultRecoveryPolicy;
2202 let admin = super::super::types::Admin::new().recovery_policy(std::sync::Arc::new(
2203 DefaultRecoveryPolicy::new().with_strict_mailer_required(true),
2204 ));
2205 let err = strict_mailer_guard_check(&admin).expect_err("guard should fail");
2206 assert!(
2207 err.contains("strict_mailer_required"),
2208 "error message must name the policy method: {err}"
2209 );
2210 assert!(
2211 err.contains("Admin::mailer"),
2212 "error message must direct the operator to the fix: {err}"
2213 );
2214 }
2215
2216 #[test]
2221 fn strict_mailer_guard_passes_when_mailer_was_explicitly_overridden() {
2222 use crate::auth::DefaultRecoveryPolicy;
2223 use crate::email::LogMailer;
2224 let admin = super::super::types::Admin::new()
2225 .recovery_policy(std::sync::Arc::new(
2226 DefaultRecoveryPolicy::new().with_strict_mailer_required(true),
2227 ))
2228 .mailer(std::sync::Arc::new(LogMailer));
2229 assert!(strict_mailer_guard_check(&admin).is_ok());
2230 }
2231
2232 #[test]
2235 fn strict_mailer_guard_passes_when_strict_mode_disabled() {
2236 let admin = super::super::types::Admin::new();
2237 assert!(strict_mailer_guard_check(&admin).is_ok());
2238 }
2239
2240 #[test]
2243 fn whitelist_accepts_the_three_locked_paths() {
2244 assert!(super::is_must_change_whitelisted_path(
2246 "/admin/must-change-password"
2247 ));
2248 assert!(super::is_must_change_whitelisted_path("/admin/logout"));
2249 assert!(super::is_must_change_whitelisted_path(
2250 "/admin/account/sessions"
2251 ));
2252 }
2253
2254 #[test]
2255 fn whitelist_rejects_subpaths_of_account_sessions() {
2256 assert!(!super::is_must_change_whitelisted_path(
2261 "/admin/account/sessions/revoke"
2262 ));
2263 assert!(!super::is_must_change_whitelisted_path(
2264 "/admin/account/sessions/revoke-others"
2265 ));
2266 assert!(!super::is_must_change_whitelisted_path(
2267 "/admin/account/sessions/"
2268 ));
2269 }
2270
2271 #[test]
2272 fn whitelist_rejects_other_admin_paths() {
2273 for path in [
2274 "/admin",
2275 "/admin/",
2276 "/admin/users",
2277 "/admin/users/42",
2278 "/admin/login",
2279 "/admin/password_change",
2280 "/admin/forgot-password",
2281 "/admin/reauth",
2282 "/admin/must-change-password/", ] {
2284 assert!(
2285 !super::is_must_change_whitelisted_path(path),
2286 "expected reject for {path:?}"
2287 );
2288 }
2289 }
2290
2291 #[test]
2292 fn whitelist_rejects_paths_outside_admin_surface() {
2293 for path in ["/", "/login", "/static/admin.css", "/api"] {
2294 assert!(
2295 !super::is_must_change_whitelisted_path(path),
2296 "expected reject for {path:?}"
2297 );
2298 }
2299 }
2300
2301 #[test]
2304 fn read_only_allows_auth_flow_exact_paths() {
2305 for path in [
2306 "/admin/login",
2307 "/admin/logout",
2308 "/admin/reauth",
2309 "/admin/forgot-password",
2310 "/admin/mfa/verify",
2311 "/admin/must-change-password",
2312 "/admin/password_change",
2313 ] {
2314 assert!(
2315 super::is_read_only_writable_path(path),
2316 "auth path {path:?} must be writable in read-only mode"
2317 );
2318 }
2319 }
2320
2321 #[test]
2322 fn read_only_allows_prefix_paths() {
2323 for path in [
2327 "/admin/reset-password/abc123",
2328 "/admin/reset-password/abc123/whatever",
2329 "/admin/account/sessions/42/revoke",
2330 "/admin/account/sessions/revoke-all",
2331 "/admin/account/mfa/enroll",
2332 "/admin/account/mfa/disable",
2333 ] {
2334 assert!(
2335 super::is_read_only_writable_path(path),
2336 "prefix-allowlisted path {path:?} must be writable"
2337 );
2338 }
2339 }
2340
2341 #[test]
2342 fn read_only_blocks_project_data_mutations() {
2343 for path in [
2346 "/admin/posts/new",
2347 "/admin/posts/42/edit",
2348 "/admin/posts/42/delete",
2349 "/admin/posts/bulk_delete",
2350 "/admin/posts/bulk/archive",
2351 "/admin/users/new",
2352 "/admin/users/42/edit",
2353 "/admin/users/42/reset-password",
2354 "/admin/users/42/lock",
2355 "/admin/users/42/sessions/99/revoke",
2356 "/admin/groups/new",
2357 "/admin/groups/42/delete",
2358 ] {
2359 assert!(
2360 !super::is_read_only_writable_path(path),
2361 "data-mutation path {path:?} must be blocked in read-only mode"
2362 );
2363 }
2364 }
2365
2366 #[test]
2367 fn read_only_blocks_random_paths_outside_admin_surface() {
2368 for path in ["/", "/login", "/static/admin.css", "/api/v1/posts"] {
2373 assert!(
2374 !super::is_read_only_writable_path(path),
2375 "non-admin path {path:?} must not be writable"
2376 );
2377 }
2378 }
2379
2380 #[test]
2381 fn extract_admin_name_parses_slug_segment() {
2382 assert_eq!(super::extract_admin_name("/admin/posts"), Some("posts"));
2383 assert_eq!(super::extract_admin_name("/admin/posts/"), Some("posts"));
2384 assert_eq!(
2385 super::extract_admin_name("/admin/posts/42/edit"),
2386 Some("posts")
2387 );
2388 assert_eq!(
2389 super::extract_admin_name("/admin/users/42/sessions/99/revoke"),
2390 Some("users")
2391 );
2392 }
2393
2394 #[test]
2395 fn extract_admin_name_rejects_root_reserved_and_non_admin() {
2396 assert_eq!(super::extract_admin_name("/admin/"), None);
2398 assert_eq!(super::extract_admin_name("/admin"), None);
2399 assert_eq!(super::extract_admin_name("/admin/_search"), None);
2402 assert_eq!(super::extract_admin_name("/admin/_lookup/posts"), None);
2403 assert_eq!(super::extract_admin_name("/login"), None);
2405 assert_eq!(super::extract_admin_name("/static/admin.css"), None);
2406 }
2407
2408 #[test]
2409 fn read_only_model_builder_and_accessor_round_trip() {
2410 let admin = super::super::types::Admin::new()
2411 .read_only_model("archive_posts")
2412 .read_only_model("legacy_invoices");
2413 assert!(admin.is_model_read_only("archive_posts"));
2414 assert!(admin.is_model_read_only("legacy_invoices"));
2415 assert!(!admin.is_model_read_only("posts"));
2416 assert!(!admin.is_read_only());
2418 }
2419
2420 #[test]
2421 fn is_mutating_method_recognises_write_verbs() {
2422 use hyper::Method;
2423 for m in [Method::POST, Method::PUT, Method::PATCH, Method::DELETE] {
2424 assert!(super::is_mutating_method(&m), "{m} must be mutating");
2425 }
2426 for m in [Method::GET, Method::HEAD, Method::OPTIONS] {
2427 assert!(!super::is_mutating_method(&m), "{m} must not be mutating");
2428 }
2429 }
2430}