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!(
55 include_str!("../../assets/static/admin/tokens/colors.css"),
57 "\n",
58 include_str!("../../assets/static/admin/tokens/spacing.css"),
59 "\n",
60 include_str!("../../assets/static/admin/tokens/radius.css"),
61 "\n",
62 include_str!("../../assets/static/admin/tokens/shadows.css"),
63 "\n",
64 include_str!("../../assets/static/admin/tokens/typography.css"),
65 "\n",
66 include_str!("../../assets/static/admin/base/reset.css"),
68 "\n",
69 include_str!("../../assets/static/admin/base/base.css"),
70 "\n",
71 include_str!("../../assets/static/admin/base/typography.css"),
72 "\n",
73 include_str!("../../assets/static/admin/base/typography-i18n.css"),
74 "\n",
75 include_str!("../../assets/static/admin/base/utilities.css"),
76 "\n",
77 include_str!("../../assets/static/admin/layout/shell.css"),
79 "\n",
80 include_str!("../../assets/static/admin/layout/topbar.css"),
81 "\n",
82 include_str!("../../assets/static/admin/layout/sidebar.css"),
83 "\n",
84 include_str!("../../assets/static/admin/layout/footer.css"),
85 "\n",
86 include_str!("../../assets/static/admin/components/cards.css"),
88 "\n",
89 include_str!("../../assets/static/admin/components/buttons.css"),
90 "\n",
91 include_str!("../../assets/static/admin/components/forms.css"),
92 "\n",
93 include_str!("../../assets/static/admin/components/tables.css"),
94 "\n",
95 include_str!("../../assets/static/admin/components/filters.css"),
96 "\n",
97 include_str!("../../assets/static/admin/components/dropdowns.css"),
98 "\n",
99 include_str!("../../assets/static/admin/components/search_palette.css"),
100 "\n",
101 include_str!("../../assets/static/admin/components/pagination.css"),
102 "\n",
103 include_str!("../../assets/static/admin/components/pills.css"),
104 "\n",
105 include_str!("../../assets/static/admin/components/flashes.css"),
106 "\n",
107 include_str!("../../assets/static/admin/components/timeline.css"),
108 "\n",
109 include_str!("../../assets/static/admin/components/tabs.css"),
110 "\n",
111 include_str!("../../assets/static/admin/pages/auth.css"),
113 "\n",
114 include_str!("../../assets/static/admin/pages/dashboard.css"),
115 "\n",
116 include_str!("../../assets/static/admin/pages/db_browser.css"),
117 "\n",
118 include_str!("../../assets/static/admin/pages/permissions.css"),
119 "\n",
120 include_str!("../../assets/static/admin/pages/sessions.css"),
121 "\n",
122 include_str!("../../assets/static/admin/pages/errors.css"),
123 "\n",
124 include_str!("../../assets/static/admin/pages/list.css"),
125 "\n",
126 include_str!("../../assets/static/admin/layout/responsive.css"),
128 "\n",
129 include_str!("../../assets/static/admin/print/print.css"),
131);
132
133fn admin_css_payload(override_css: Option<&str>) -> bytes::Bytes {
147 match override_css {
148 Some(extra) => bytes::Bytes::from(format!(
149 "{ADMIN_CSS}\n/* ---- RUSTIO_TOKENS_CSS override ---- */\n{extra}"
150 )),
151 None => bytes::Bytes::from_static(ADMIN_CSS.as_bytes()),
152 }
153}
154
155fn load_token_override() -> Option<String> {
162 let path = std::env::var("RUSTIO_TOKENS_CSS").ok()?;
163 match std::fs::read_to_string(&path) {
164 Ok(css) => Some(css),
165 Err(e) => {
166 log::warn!("RUSTIO_TOKENS_CSS={path:?} unreadable: {e}; serving baked CSS");
167 None
168 }
169 }
170}
171
172const ADMIN_JS: &str = include_str!("../../assets/static/admin.js");
175
176const FONT_GEIST: &[u8] = include_bytes!("../../assets/static/fonts/Geist-Variable.woff2");
190const FONT_GEIST_MONO: &[u8] = include_bytes!("../../assets/static/fonts/GeistMono-Variable.woff2");
191const FONT_INTER: &[u8] = include_bytes!("../../assets/static/fonts/InterVariable.woff2");
192const FONT_TAJAWAL_REG: &[u8] = include_bytes!("../../assets/static/fonts/Tajawal-Regular.woff2");
193const FONT_TAJAWAL_MED: &[u8] = include_bytes!("../../assets/static/fonts/Tajawal-Medium.woff2");
194const FONT_TAJAWAL_BOLD: &[u8] = include_bytes!("../../assets/static/fonts/Tajawal-Bold.woff2");
195const FONT_NOTO_NASKH_AR: &[u8] =
196 include_bytes!("../../assets/static/fonts/NotoNaskhArabic-Variable.woff2");
197const FONT_NOTO_THAI: &[u8] =
198 include_bytes!("../../assets/static/fonts/NotoSansThai-Variable.woff2");
199const FONT_NOTO_DEVA: &[u8] =
200 include_bytes!("../../assets/static/fonts/NotoSansDevanagari-Variable.woff2");
201const FONT_NOTO_JP: &[u8] = include_bytes!("../../assets/static/fonts/NotoSansJP-Regular.woff2");
202const FONT_NOTO_KR: &[u8] = include_bytes!("../../assets/static/fonts/NotoSansKR-Regular.woff2");
203const FONT_NOTO_SC: &[u8] = include_bytes!("../../assets/static/fonts/NotoSansSC-Regular.woff2");
204
205use super::handlers::{self, AdminCtx};
206use super::render;
207use super::types::Admin;
208
209enum Guard {
213 Allow(Identity),
214 Redirect(Response),
215}
216
217const MUST_CHANGE_WHITELIST: &[&str] = &[
227 "/admin/must-change-password",
228 "/admin/logout",
229 "/admin/account/sessions",
230];
231
232fn is_must_change_whitelisted_path(path: &str) -> bool {
236 MUST_CHANGE_WHITELIST.contains(&path)
237}
238
239const READ_ONLY_EXACT_ALLOW: &[&str] = &[
250 "/admin/login",
251 "/admin/logout",
252 "/admin/reauth",
253 "/admin/forgot-password",
254 "/admin/mfa/verify",
255 "/admin/must-change-password",
256 "/admin/password_change",
257];
258
259const READ_ONLY_PREFIX_ALLOW: &[&str] = &[
260 "/admin/reset-password/",
263 "/admin/account/sessions/",
266 "/admin/account/mfa/",
269];
270
271fn is_saved_filter_path(path: &str) -> bool {
277 if let Some(rest) = path.strip_prefix("/admin/") {
278 if let Some((_, after)) = rest.split_once('/') {
282 return after == "saved_filters" || after.starts_with("saved_filters/");
283 }
284 }
285 false
286}
287
288pub(crate) fn is_mutating_method(method: &hyper::Method) -> bool {
294 matches!(
295 *method,
296 hyper::Method::POST | hyper::Method::PUT | hyper::Method::PATCH | hyper::Method::DELETE
297 )
298}
299
300pub(crate) fn is_read_only_writable_path(path: &str) -> bool {
308 if READ_ONLY_EXACT_ALLOW.contains(&path) {
309 return true;
310 }
311 if READ_ONLY_PREFIX_ALLOW
312 .iter()
313 .any(|prefix| path.starts_with(prefix))
314 {
315 return true;
316 }
317 is_saved_filter_path(path)
318}
319
320pub(crate) fn extract_admin_name(path: &str) -> Option<&str> {
327 let rest = path.strip_prefix("/admin/")?;
328 let slug = rest.split('/').next()?;
329 if slug.is_empty() || slug.starts_with('_') {
330 return None;
331 }
332 Some(slug)
333}
334
335const MFA_ENROLL_WHITELIST: &[&str] = &[
348 "/admin/account/mfa/enroll",
349 "/admin/logout",
350 "/admin/account/sessions",
351];
352
353fn is_mfa_enroll_whitelisted_path(path: &str) -> bool {
354 MFA_ENROLL_WHITELIST.contains(&path)
355}
356
357const MFA_VERIFY_WHITELIST: &[&str] = &[
367 "/admin/mfa/verify",
368 "/admin/logout",
369 "/admin/account/sessions",
370];
371
372fn is_mfa_verify_whitelisted_path(path: &str) -> bool {
373 MFA_VERIFY_WHITELIST.contains(&path)
374}
375
376fn mfa_required_for_role(policy: crate::auth::MfaPolicy, role: Role) -> bool {
386 use crate::auth::MfaPolicy;
387 match policy {
388 MfaPolicy::Disabled | MfaPolicy::Optional => false,
389 MfaPolicy::Required => true,
390 MfaPolicy::RequiredForRoles(roles) => roles.contains(&role),
391 }
392}
393
394async fn login_guard(ctx: &AdminCtx, req: &Request) -> Result<Guard> {
395 let cookie = match req.header("cookie") {
396 Some(c) => c,
397 None => return Ok(Guard::Redirect(Response::redirect("/admin/login"))),
398 };
399 let token = match auth::session_token_from_cookie(cookie) {
400 Some(t) => t,
401 None => return Ok(Guard::Redirect(Response::redirect("/admin/login"))),
402 };
403 let ident = match auth::identity_from_session(&ctx.db, &token).await? {
404 Some(i) => i,
405 None => return Ok(Guard::Redirect(Response::redirect("/admin/login"))),
406 };
407 if !ident.is_active {
408 return Ok(Guard::Redirect(Response::redirect("/admin/login")));
409 }
410
411 if ident.must_change_password && !is_must_change_whitelisted_path(req.path()) {
417 return Ok(Guard::Redirect(Response::redirect(
418 "/admin/must-change-password",
419 )));
420 }
421
422 let policy = ctx.admin.active_mfa_policy();
431 if mfa_required_for_role(policy, ident.role)
432 && !ident.mfa_enabled
433 && !is_mfa_enroll_whitelisted_path(req.path())
434 {
435 return Ok(Guard::Redirect(Response::redirect(
436 "/admin/account/mfa/enroll",
437 )));
438 }
439
440 use crate::auth::SessionTrust;
448 if ident.mfa_enabled
449 && ident.trust_level != SessionTrust::MfaVerified
450 && !is_mfa_verify_whitelisted_path(req.path())
451 {
452 return Ok(Guard::Redirect(Response::redirect("/admin/mfa/verify")));
453 }
454
455 Ok(Guard::Allow(ident))
456}
457
458async fn role_guard(ctx: &AdminCtx, req: &Request, min: Role) -> Result<Guard> {
459 match login_guard(ctx, req).await? {
460 Guard::Redirect(r) => Ok(Guard::Redirect(r)),
461 Guard::Allow(ident) => {
462 if ident.role.includes(min) {
463 Ok(Guard::Allow(ident))
464 } else {
465 let body = render::render_forbidden_body(
466 &ctx.admin,
467 &ctx.templates,
468 &ident,
469 handlers::csrf_token(req),
470 None,
471 Some(min.label()),
472 )?;
473 Ok(Guard::Redirect(
474 Response::html(body).with_status(hyper::StatusCode::FORBIDDEN),
475 ))
476 }
477 }
478 }
479}
480
481async fn perm_guard(ctx: &AdminCtx, req: &Request, perm: &str) -> Result<Guard> {
482 match role_guard(ctx, req, Role::Staff).await? {
483 Guard::Redirect(r) => Ok(Guard::Redirect(r)),
484 Guard::Allow(ident) => {
485 if ident.role.bypasses_group_checks() {
486 return Ok(Guard::Allow(ident));
487 }
488 if auth::check_permission(&ctx.db, &ident, perm).await? {
489 Ok(Guard::Allow(ident))
490 } else {
491 let body = render::render_forbidden_body(
492 &ctx.admin,
493 &ctx.templates,
494 &ident,
495 handlers::csrf_token(req),
496 Some(perm.to_string()),
497 None,
498 )?;
499 Ok(Guard::Redirect(
500 Response::html(body).with_status(hyper::StatusCode::FORBIDDEN),
501 ))
502 }
503 }
504 }
505}
506
507#[cfg(test)]
510fn perm_guard_verdict(ident: &Identity, perm_held: bool) -> bool {
511 if !ident.is_active {
512 return false;
513 }
514 if ident.role.bypasses_group_checks() {
515 return true;
516 }
517 perm_held
518}
519
520fn parse_id(raw: Option<&str>) -> Result<i64> {
521 raw.and_then(|s| s.parse().ok())
522 .ok_or_else(|| Error::BadRequest("invalid id".into()))
523}
524
525fn model_name_from_req(req: &Request) -> Result<String> {
526 req.param("admin_name")
527 .map(|s| s.to_string())
528 .ok_or_else(|| Error::BadRequest("missing model".into()))
529}
530
531fn perm_for(ctx: &AdminCtx, admin_name: &str, action: &str) -> Result<String> {
532 let entry = ctx
533 .admin
534 .find(admin_name)
535 .ok_or_else(|| Error::NotFound(format!("no admin model: {admin_name}")))?;
536 let singular = entry.singular_name.to_ascii_lowercase();
537 Ok(format!("{admin_name}.{action}_{singular}"))
538}
539
540async fn resolve_identity_for_error_page(db: &Db, cookie_header: &str) -> Option<Identity> {
570 let token = auth::session_token_from_cookie(cookie_header)?;
571 let identity = auth::identity_from_session(db, token.as_str())
572 .await
573 .ok()
574 .flatten()?;
575 if !identity.is_active {
576 return None;
577 }
578 Some(identity)
579}
580
581fn strict_mailer_guard_check(admin: &Admin) -> std::result::Result<(), String> {
582 if admin.active_recovery_policy().strict_mailer_required() && !admin.has_custom_mailer() {
583 Err(
584 "rustio-admin: RecoveryPolicy::strict_mailer_required() = true but no mailer \
585 was registered via Admin::mailer(...).\n\n\
586 The framework's default LogMailer writes recovery emails to log::info! instead \
587 of sending them, which is unsuitable for production. Recovery routes are NOT \
588 registered with this configuration.\n\n\
589 To resolve, choose one:\n\
590 (a) register a real mailer before calling register_admin_routes:\n\
591 Admin::mailer(Arc::new(MyProjectMailer::new(...)))\n\
592 (b) opt the policy out of strict mode (the framework default — dev / CI / \
593 testing baseline):\n\
594 RecoveryPolicy::strict_mailer_required(false)\n\n\
595 See DESIGN_RECOVERY.md §12.1 for the contract."
596 .to_string(),
597 )
598 } else {
599 Ok(())
600 }
601}
602
603pub fn register_admin_routes(
605 router: Router,
606 admin: Admin,
607 db: Db,
608 templates: Arc<Templates>,
609) -> Router {
610 if let Err(msg) = strict_mailer_guard_check(&admin) {
618 panic!("{msg}");
619 }
620
621 let ctx = Arc::new(AdminCtx::new(
622 Arc::new(admin),
623 db.clone(),
624 templates.clone(),
625 ));
626
627 let auth_ctx = Arc::new(super::builtin::AuthAdminCtx {
630 admin: ctx.admin.clone(),
631 db,
632 templates,
633 });
634
635 let err_admin = ctx.admin.clone();
650 let err_templates = ctx.templates.clone();
651 let err_db = ctx.db.clone();
652 let router = router.middleware(move |req, next| {
653 let admin = err_admin.clone();
654 let templates = err_templates.clone();
655 let db = err_db.clone();
656 Box::pin(async move {
657 let is_admin_path = req.path().starts_with("/admin");
658 let cookie_header = if is_admin_path {
664 req.header("cookie").map(|s| s.to_string())
665 } else {
666 None
667 };
668 let result = next.run(req).await;
669 match result {
670 Ok(resp) => Ok(resp),
671 Err(err) if is_admin_path => {
672 let identity = match cookie_header.as_deref() {
673 Some(cookie) => resolve_identity_for_error_page(&db, cookie).await,
674 None => None,
675 };
676 Ok(render::render_admin_error_response(
677 &admin,
678 &templates,
679 identity.as_ref(),
680 err.status(),
681 err.client_message().to_string(),
682 ))
683 }
684 Err(err) => Err(err),
685 }
686 })
687 });
688
689 let ro_flag = ctx.admin.is_read_only();
701 let ro_models = std::sync::Arc::new(ctx.admin.read_only_models.clone());
702 let router = router.middleware(move |req, next| {
703 let ro_models = ro_models.clone();
704 Box::pin(async move {
705 if req.path().starts_with("/admin")
706 && is_mutating_method(req.method())
707 && !is_read_only_writable_path(req.path())
708 {
709 if ro_flag {
712 return Err(Error::Forbidden(
713 "This admin is currently in read-only mode. \
714 Project-data mutations are disabled until the operator \
715 turns read-only off."
716 .into(),
717 ));
718 }
719 if !ro_models.is_empty() {
724 if let Some(slug) = extract_admin_name(req.path()) {
725 if ro_models.contains(slug) {
726 return Err(Error::Forbidden(format!(
727 "Model `{slug}` is frozen (read-only). \
728 Mutations on this model are disabled."
729 )));
730 }
731 }
732 }
733 }
734 next.run(req).await
735 })
736 });
737
738 let admin_css = admin_css_payload(load_token_override().as_deref());
750 let router = router.get("/static/admin.css", move |_req| {
751 let body = admin_css.clone();
752 async move {
753 Ok(Response::new(hyper::StatusCode::OK, body)
754 .with_header("content-type", "text/css; charset=utf-8")
755 .with_header("cache-control", "no-cache, must-revalidate"))
756 }
757 });
758 let router = router.get("/static/admin.js", |_req| async move {
759 Ok(Response::new(
760 hyper::StatusCode::OK,
761 bytes::Bytes::from_static(ADMIN_JS.as_bytes()),
762 )
763 .with_header("content-type", "application/javascript; charset=utf-8")
764 .with_header("cache-control", "no-cache, must-revalidate"))
765 });
766
767 fn font_response(bytes: &'static [u8]) -> Response {
771 Response::new(hyper::StatusCode::OK, bytes::Bytes::from_static(bytes))
772 .with_header("content-type", "font/woff2")
773 .with_header("cache-control", "public, max-age=31536000, immutable")
774 }
775 let router = router.get("/static/fonts/Geist-Variable.woff2", |_req| async move {
776 Ok(font_response(FONT_GEIST))
777 });
778 let router = router.get(
779 "/static/fonts/GeistMono-Variable.woff2",
780 |_req| async move { Ok(font_response(FONT_GEIST_MONO)) },
781 );
782 let router = router.get("/static/fonts/Tajawal-Regular.woff2", |_req| async move {
783 Ok(font_response(FONT_TAJAWAL_REG))
784 });
785 let router = router.get("/static/fonts/Tajawal-Medium.woff2", |_req| async move {
786 Ok(font_response(FONT_TAJAWAL_MED))
787 });
788 let router = router.get("/static/fonts/Tajawal-Bold.woff2", |_req| async move {
789 Ok(font_response(FONT_TAJAWAL_BOLD))
790 });
791 let router = router.get(
792 "/static/fonts/NotoNaskhArabic-Variable.woff2",
793 |_req| async move { Ok(font_response(FONT_NOTO_NASKH_AR)) },
794 );
795 let router = router.get("/static/fonts/InterVariable.woff2", |_req| async move {
796 Ok(font_response(FONT_INTER))
797 });
798 let router = router.get(
799 "/static/fonts/NotoSansThai-Variable.woff2",
800 |_req| async move { Ok(font_response(FONT_NOTO_THAI)) },
801 );
802 let router = router.get(
803 "/static/fonts/NotoSansDevanagari-Variable.woff2",
804 |_req| async move { Ok(font_response(FONT_NOTO_DEVA)) },
805 );
806 let router = router.get(
807 "/static/fonts/NotoSansJP-Regular.woff2",
808 |_req| async move { Ok(font_response(FONT_NOTO_JP)) },
809 );
810 let router = router.get(
811 "/static/fonts/NotoSansKR-Regular.woff2",
812 |_req| async move { Ok(font_response(FONT_NOTO_KR)) },
813 );
814 let router = router.get(
815 "/static/fonts/NotoSansSC-Regular.woff2",
816 |_req| async move { Ok(font_response(FONT_NOTO_SC)) },
817 );
818
819 let c = ctx.clone();
824 let router = router.get("/admin/healthz", move |_req| {
825 let c = c.clone();
826 async move { super::healthz::healthz(&c.db).await }
827 });
828
829 let c = ctx.clone();
831 let router = router.get("/admin/login", move |req| {
832 let c = c.clone();
833 async move { handlers::show_login(&c, req).await }
834 });
835
836 let c = ctx.clone();
837 let router = router.post("/admin/login", move |req| {
838 let c = c.clone();
839 async move { handlers::do_login(&c, req).await }
840 });
841
842 let c = ctx.clone();
843 let router = router.post("/admin/logout", move |req| {
844 let c = c.clone();
845 async move { handlers::do_logout(&c, req).await }
846 });
847
848 let recovery_state = Arc::new(super::recovery_handlers::RecoveryState::from_admin(
865 &ctx.admin,
866 ));
867
868 let c = ctx.clone();
869 let router = router.get("/admin/forgot-password", move |req| {
870 let c = c.clone();
871 async move { super::recovery_handlers::show_forgot_password(&c, &req).await }
872 });
873
874 let c = ctx.clone();
875 let rs = recovery_state.clone();
876 let router = router.post("/admin/forgot-password", move |req| {
877 let c = c.clone();
878 let rs = rs.clone();
879 async move { super::recovery_handlers::do_forgot_password(&c, &rs, req).await }
880 });
881
882 let c = ctx.clone();
883 let router = router.get("/admin/forgot-password/sent", move |req| {
884 let c = c.clone();
885 async move { super::recovery_handlers::show_forgot_password_sent(&c, &req).await }
886 });
887
888 let c = ctx.clone();
889 let router = router.get("/admin/reset-password/:token", move |req| {
890 let c = c.clone();
891 async move {
892 let token = req
893 .param("token")
894 .ok_or_else(|| Error::BadRequest("missing token".into()))?
895 .to_string();
896 super::recovery_handlers::show_reset_password(&c, &req, &token).await
897 }
898 });
899
900 let c = ctx.clone();
901 let rs = recovery_state.clone();
902 let router = router.post("/admin/reset-password/:token", move |req| {
903 let c = c.clone();
904 let rs = rs.clone();
905 async move {
906 let token = req
907 .param("token")
908 .ok_or_else(|| Error::BadRequest("missing token".into()))?
909 .to_string();
910 super::recovery_handlers::do_reset_password(&c, &rs, req, &token).await
911 }
912 });
913
914 let c = ctx.clone();
916 let router = router.get("/admin", move |req| {
917 let c = c.clone();
918 async move {
919 match role_guard(&c, &req, Role::Staff).await? {
920 Guard::Redirect(r) => Ok(r),
921 Guard::Allow(ident) => handlers::dashboard(&c, ident, &req).await,
922 }
923 }
924 });
925
926 let c = ctx.clone();
932 let router = router.get("/admin/db", move |req| {
933 let c = c.clone();
934 async move {
935 match role_guard(&c, &req, Role::Developer).await? {
936 Guard::Redirect(r) => Ok(r),
937 Guard::Allow(ident) => super::db_browser::show_db_browser(&c, ident, &req).await,
938 }
939 }
940 });
941
942 let c = ctx.clone();
947 let router = router.get("/admin/notifications", move |req| {
948 let c = c.clone();
949 async move {
950 match role_guard(&c, &req, Role::Staff).await? {
951 Guard::Redirect(r) => Ok(r),
952 Guard::Allow(ident) => handlers::show_notifications(&c, ident, &req).await,
953 }
954 }
955 });
956 let c = ctx.clone();
957 let router = router.post("/admin/notifications/mark_all_read", move |req| {
958 let c = c.clone();
959 async move {
960 match role_guard(&c, &req, Role::Staff).await? {
961 Guard::Redirect(r) => Ok(r),
962 Guard::Allow(ident) => {
963 handlers::do_mark_all_notifications_read(&c, ident, req).await
964 }
965 }
966 }
967 });
968
969 let c = ctx.clone();
974 let router = router.get("/admin/feature_flags", 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_feature_flags(&c, ident, &req).await,
980 }
981 }
982 });
983 let c = ctx.clone();
984 let router = router.post("/admin/feature_flags", move |req| {
985 let c = c.clone();
986 async move {
987 match role_guard(&c, &req, Role::Administrator).await? {
988 Guard::Redirect(r) => Ok(r),
989 Guard::Allow(ident) => handlers::do_create_feature_flag(&c, ident, req).await,
990 }
991 }
992 });
993 let c = ctx.clone();
994 let router = router.post("/admin/feature_flags/:key/toggle", move |req| {
995 let c = c.clone();
996 async move {
997 let key = req
998 .param("key")
999 .ok_or_else(|| Error::BadRequest("missing flag key".into()))?
1000 .to_string();
1001 match role_guard(&c, &req, Role::Administrator).await? {
1002 Guard::Redirect(r) => Ok(r),
1003 Guard::Allow(ident) => handlers::do_toggle_feature_flag(&c, ident, &key, req).await,
1004 }
1005 }
1006 });
1007
1008 let c = ctx.clone();
1014 let router = router.get("/admin/health", move |req| {
1015 let c = c.clone();
1016 async move {
1017 match role_guard(&c, &req, Role::Administrator).await? {
1018 Guard::Redirect(r) => Ok(r),
1019 Guard::Allow(ident) => handlers::show_health(&c, ident, &req).await,
1020 }
1021 }
1022 });
1023
1024 let c = ctx.clone();
1026 let router = router.get("/admin/history", move |req| {
1027 let c = c.clone();
1028 async move {
1029 match role_guard(&c, &req, Role::Administrator).await? {
1030 Guard::Redirect(r) => Ok(r),
1031 Guard::Allow(ident) => handlers::show_log_entries(&c, ident, &req).await,
1032 }
1033 }
1034 });
1035
1036 let c = ctx.clone();
1039 let router = router.get("/admin/account/sessions", move |req| {
1040 let c = c.clone();
1041 async move {
1042 match role_guard(&c, &req, Role::User).await? {
1043 Guard::Redirect(r) => Ok(r),
1044 Guard::Allow(ident) => handlers::show_account_sessions(&c, ident, &req).await,
1045 }
1046 }
1047 });
1048
1049 let c = ctx.clone();
1057 let router = router.post("/admin/account/sessions/revoke-others", move |req| {
1058 let c = c.clone();
1059 async move {
1060 match role_guard(&c, &req, Role::User).await? {
1061 Guard::Redirect(r) => Ok(r),
1062 Guard::Allow(ident) => handlers::do_revoke_other_sessions(&c, ident, req).await,
1063 }
1064 }
1065 });
1066
1067 let c = ctx.clone();
1068 let router = router.post("/admin/account/sessions/revoke-all", move |req| {
1069 let c = c.clone();
1070 async move {
1071 match role_guard(&c, &req, Role::User).await? {
1072 Guard::Redirect(r) => Ok(r),
1073 Guard::Allow(ident) => handlers::do_revoke_all_sessions(&c, ident, req).await,
1074 }
1075 }
1076 });
1077
1078 let c = ctx.clone();
1079 let router = router.post("/admin/account/sessions/:id/revoke", move |req| {
1080 let c = c.clone();
1081 async move {
1082 match role_guard(&c, &req, Role::User).await? {
1083 Guard::Redirect(r) => Ok(r),
1084 Guard::Allow(ident) => {
1085 let id = parse_id(req.param("id"))?;
1086 handlers::do_revoke_session(&c, ident, req, id).await
1087 }
1088 }
1089 }
1090 });
1091
1092 let c = ctx.clone();
1096 let router = router.get("/admin/password_change", move |req| {
1097 let c = c.clone();
1098 async move {
1099 match role_guard(&c, &req, Role::User).await? {
1100 Guard::Redirect(r) => Ok(r),
1101 Guard::Allow(ident) => handlers::show_password_change(&c, ident, &req).await,
1102 }
1103 }
1104 });
1105 let c = ctx.clone();
1106 let router = router.post("/admin/password_change", move |req| {
1107 let c = c.clone();
1108 async move {
1109 match role_guard(&c, &req, Role::User).await? {
1110 Guard::Redirect(r) => Ok(r),
1111 Guard::Allow(ident) => handlers::do_password_change(&c, ident, req).await,
1112 }
1113 }
1114 });
1115
1116 let c = ctx.clone();
1125 let router = router.get("/admin/reauth", move |req| {
1126 let c = c.clone();
1127 async move {
1128 match role_guard(&c, &req, Role::User).await? {
1129 Guard::Redirect(r) => Ok(r),
1130 Guard::Allow(ident) => {
1131 super::admin_recovery_handlers::show_reauth(&c, ident, &req).await
1132 }
1133 }
1134 }
1135 });
1136
1137 let c = ctx.clone();
1138 let router = router.post("/admin/reauth", move |req| {
1139 let c = c.clone();
1140 async move {
1141 match role_guard(&c, &req, Role::User).await? {
1142 Guard::Redirect(r) => Ok(r),
1143 Guard::Allow(ident) => {
1144 super::admin_recovery_handlers::do_reauth(&c, ident, req).await
1145 }
1146 }
1147 }
1148 });
1149
1150 let c = ctx.clone();
1160 let router = router.get("/admin/must-change-password", move |req| {
1161 let c = c.clone();
1162 async move {
1163 match role_guard(&c, &req, Role::User).await? {
1164 Guard::Redirect(r) => Ok(r),
1165 Guard::Allow(ident) => {
1166 super::admin_recovery_handlers::show_must_change_password(&c, ident, &req).await
1167 }
1168 }
1169 }
1170 });
1171
1172 let c = ctx.clone();
1173 let router = router.post("/admin/must-change-password", move |req| {
1174 let c = c.clone();
1175 async move {
1176 match role_guard(&c, &req, Role::User).await? {
1177 Guard::Redirect(r) => Ok(r),
1178 Guard::Allow(ident) => {
1179 super::admin_recovery_handlers::do_must_change_password(&c, ident, req).await
1180 }
1181 }
1182 }
1183 });
1184
1185 let c = ctx.clone();
1203 let router = router.get("/admin/mfa/verify", move |req| {
1204 let c = c.clone();
1205 async move {
1206 match role_guard(&c, &req, Role::User).await? {
1207 Guard::Redirect(r) => Ok(r),
1208 Guard::Allow(ident) => super::mfa_handlers::show_verify(&c, ident, &req).await,
1209 }
1210 }
1211 });
1212
1213 let c = ctx.clone();
1214 let router = router.post("/admin/mfa/verify", move |req| {
1215 let c = c.clone();
1216 async move {
1217 match role_guard(&c, &req, Role::User).await? {
1218 Guard::Redirect(r) => Ok(r),
1219 Guard::Allow(ident) => super::mfa_handlers::do_verify(&c, ident, req).await,
1220 }
1221 }
1222 });
1223
1224 let c = ctx.clone();
1226 let router = router.get("/admin/account/mfa/enroll", move |req| {
1227 let c = c.clone();
1228 async move {
1229 match role_guard(&c, &req, Role::User).await? {
1230 Guard::Redirect(r) => Ok(r),
1231 Guard::Allow(ident) => super::mfa_handlers::show_enroll(&c, ident, &req).await,
1232 }
1233 }
1234 });
1235
1236 let c = ctx.clone();
1237 let router = router.post("/admin/account/mfa/enroll", move |req| {
1238 let c = c.clone();
1239 async move {
1240 match role_guard(&c, &req, Role::User).await? {
1241 Guard::Redirect(r) => Ok(r),
1242 Guard::Allow(ident) => super::mfa_handlers::do_enroll(&c, ident, req).await,
1243 }
1244 }
1245 });
1246
1247 let c = ctx.clone();
1249 let router = router.get("/admin/account/mfa/regenerate-codes", move |req| {
1250 let c = c.clone();
1251 async move {
1252 match role_guard(&c, &req, Role::User).await? {
1253 Guard::Redirect(r) => Ok(r),
1254 Guard::Allow(ident) => super::mfa_handlers::show_regenerate(&c, ident, &req).await,
1255 }
1256 }
1257 });
1258
1259 let c = ctx.clone();
1260 let router = router.post("/admin/account/mfa/regenerate-codes", move |req| {
1261 let c = c.clone();
1262 async move {
1263 match role_guard(&c, &req, Role::User).await? {
1264 Guard::Redirect(r) => Ok(r),
1265 Guard::Allow(ident) => super::mfa_handlers::do_regenerate(&c, ident, req).await,
1266 }
1267 }
1268 });
1269
1270 let c = ctx.clone();
1272 let router = router.get("/admin/account/mfa/disable", move |req| {
1273 let c = c.clone();
1274 async move {
1275 match role_guard(&c, &req, Role::User).await? {
1276 Guard::Redirect(r) => Ok(r),
1277 Guard::Allow(ident) => super::mfa_handlers::show_disable(&c, ident, &req).await,
1278 }
1279 }
1280 });
1281
1282 let c = ctx.clone();
1283 let router = router.post("/admin/account/mfa/disable", move |req| {
1284 let c = c.clone();
1285 async move {
1286 match role_guard(&c, &req, Role::User).await? {
1287 Guard::Redirect(r) => Ok(r),
1288 Guard::Allow(ident) => super::mfa_handlers::do_disable(&c, ident, req).await,
1289 }
1290 }
1291 });
1292
1293 let c = ctx.clone();
1295 let ac = auth_ctx.clone();
1296 let router = router.get("/admin/users", move |req| {
1297 let c = c.clone();
1298 let ac = ac.clone();
1299 async move {
1300 match role_guard(&c, &req, Role::Administrator).await? {
1301 Guard::Redirect(r) => Ok(r),
1302 Guard::Allow(ident) => {
1303 super::builtin::list_users(&ac, ident, handlers::csrf_token(&req)).await
1304 }
1305 }
1306 }
1307 });
1308
1309 let c = ctx.clone();
1310 let ac = auth_ctx.clone();
1311 let router = router.get("/admin/users/new", move |req| {
1312 let c = c.clone();
1313 let ac = ac.clone();
1314 async move {
1315 match role_guard(&c, &req, Role::Administrator).await? {
1316 Guard::Redirect(r) => Ok(r),
1317 Guard::Allow(ident) => {
1318 super::builtin::show_new_user(&ac, ident, handlers::csrf_token(&req)).await
1319 }
1320 }
1321 }
1322 });
1323
1324 let c = ctx.clone();
1325 let ac = auth_ctx.clone();
1326 let router = router.post("/admin/users/new", move |req| {
1327 let c = c.clone();
1328 let ac = ac.clone();
1329 async move {
1330 match role_guard(&c, &req, Role::Administrator).await? {
1331 Guard::Redirect(r) => Ok(r),
1332 Guard::Allow(ident) => super::builtin::do_new_user(&ac, ident, req).await,
1333 }
1334 }
1335 });
1336
1337 let c = ctx.clone();
1338 let ac = auth_ctx.clone();
1339 let router = router.get("/admin/users/:id/edit", move |req| {
1340 let c = c.clone();
1341 let ac = ac.clone();
1342 async move {
1343 match role_guard(&c, &req, Role::Administrator).await? {
1344 Guard::Redirect(r) => Ok(r),
1345 Guard::Allow(ident) => {
1346 let id = parse_id(req.param("id"))?;
1347 super::builtin::show_user_edit(&ac, ident, id, handlers::csrf_token(&req)).await
1348 }
1349 }
1350 }
1351 });
1352
1353 let c = ctx.clone();
1354 let ac = auth_ctx.clone();
1355 let router = router.post("/admin/users/:id/edit", move |req| {
1356 let c = c.clone();
1357 let ac = ac.clone();
1358 async move {
1359 match role_guard(&c, &req, Role::Administrator).await? {
1360 Guard::Redirect(r) => Ok(r),
1361 Guard::Allow(ident) => {
1362 let id = parse_id(req.param("id"))?;
1363 super::builtin::do_user_edit(&ac, ident, id, req).await
1364 }
1365 }
1366 }
1367 });
1368
1369 let c = ctx.clone();
1370 let ac = auth_ctx.clone();
1371 let router = router.get("/admin/users/:id/delete", move |req| {
1372 let c = c.clone();
1373 let ac = ac.clone();
1374 async move {
1375 match role_guard(&c, &req, Role::Administrator).await? {
1376 Guard::Redirect(r) => Ok(r),
1377 Guard::Allow(ident) => {
1378 let id = parse_id(req.param("id"))?;
1379 super::builtin::show_user_delete(&ac, ident, id, handlers::csrf_token(&req))
1380 .await
1381 }
1382 }
1383 }
1384 });
1385
1386 let c = ctx.clone();
1387 let ac = auth_ctx.clone();
1388 let router = router.post("/admin/users/:id/delete", move |req| {
1389 let c = c.clone();
1390 let ac = ac.clone();
1391 async move {
1392 match role_guard(&c, &req, Role::Administrator).await? {
1393 Guard::Redirect(r) => Ok(r),
1394 Guard::Allow(ident) => {
1395 let id = parse_id(req.param("id"))?;
1396 super::builtin::do_user_delete(&ac, ident, id, req).await
1397 }
1398 }
1399 }
1400 });
1401
1402 let c = ctx.clone();
1418 let router = router.get("/admin/users/:id/reset-password", move |req| {
1419 let c = c.clone();
1420 async move {
1421 match role_guard(&c, &req, Role::Administrator).await? {
1422 Guard::Redirect(r) => Ok(r),
1423 Guard::Allow(ident) => {
1424 let id = parse_id(req.param("id"))?;
1425 super::admin_recovery_handlers::show_admin_reset_password(&c, ident, id, &req)
1426 .await
1427 }
1428 }
1429 }
1430 });
1431
1432 let c = ctx.clone();
1434 let router = router.post("/admin/users/:id/reset-password", move |req| {
1435 let c = c.clone();
1436 async move {
1437 match role_guard(&c, &req, Role::Administrator).await? {
1438 Guard::Redirect(r) => Ok(r),
1439 Guard::Allow(ident) => {
1440 let id = parse_id(req.param("id"))?;
1441 super::admin_recovery_handlers::do_admin_reset_password(&c, ident, id, req)
1442 .await
1443 }
1444 }
1445 }
1446 });
1447
1448 let c = ctx.clone();
1450 let router = router.get("/admin/users/:id/lock", move |req| {
1451 let c = c.clone();
1452 async move {
1453 match role_guard(&c, &req, Role::Administrator).await? {
1454 Guard::Redirect(r) => Ok(r),
1455 Guard::Allow(ident) => {
1456 let id = parse_id(req.param("id"))?;
1457 super::admin_recovery_handlers::show_lock_user(&c, ident, id, &req).await
1458 }
1459 }
1460 }
1461 });
1462
1463 let c = ctx.clone();
1465 let router = router.post("/admin/users/:id/lock", move |req| {
1466 let c = c.clone();
1467 async move {
1468 match role_guard(&c, &req, Role::Administrator).await? {
1469 Guard::Redirect(r) => Ok(r),
1470 Guard::Allow(ident) => {
1471 let id = parse_id(req.param("id"))?;
1472 super::admin_recovery_handlers::do_lock_user(&c, ident, id, req).await
1473 }
1474 }
1475 }
1476 });
1477
1478 let c = ctx.clone();
1480 let router = router.get("/admin/users/:id/unlock", move |req| {
1481 let c = c.clone();
1482 async move {
1483 match role_guard(&c, &req, Role::Administrator).await? {
1484 Guard::Redirect(r) => Ok(r),
1485 Guard::Allow(ident) => {
1486 let id = parse_id(req.param("id"))?;
1487 super::admin_recovery_handlers::show_unlock_user(&c, ident, id, &req).await
1488 }
1489 }
1490 }
1491 });
1492
1493 let c = ctx.clone();
1495 let router = router.post("/admin/users/:id/unlock", move |req| {
1496 let c = c.clone();
1497 async move {
1498 match role_guard(&c, &req, Role::Administrator).await? {
1499 Guard::Redirect(r) => Ok(r),
1500 Guard::Allow(ident) => {
1501 let id = parse_id(req.param("id"))?;
1502 super::admin_recovery_handlers::do_unlock_user(&c, ident, id, req).await
1503 }
1504 }
1505 }
1506 });
1507
1508 let c = ctx.clone();
1511 let router = router.get("/admin/users/:id/revoke-sessions", move |req| {
1512 let c = c.clone();
1513 async move {
1514 match role_guard(&c, &req, Role::Administrator).await? {
1515 Guard::Redirect(r) => Ok(r),
1516 Guard::Allow(ident) => {
1517 let id = parse_id(req.param("id"))?;
1518 super::admin_recovery_handlers::show_admin_revoke_sessions(&c, ident, id, &req)
1519 .await
1520 }
1521 }
1522 }
1523 });
1524
1525 let c = ctx.clone();
1527 let router = router.post("/admin/users/:id/revoke-sessions", move |req| {
1528 let c = c.clone();
1529 async move {
1530 match role_guard(&c, &req, Role::Administrator).await? {
1531 Guard::Redirect(r) => Ok(r),
1532 Guard::Allow(ident) => {
1533 let id = parse_id(req.param("id"))?;
1534 super::admin_recovery_handlers::do_admin_revoke_sessions(&c, ident, id, req)
1535 .await
1536 }
1537 }
1538 }
1539 });
1540
1541 let c = ctx.clone();
1548 let router = router.post("/admin/users/:id/sessions/:session_id/revoke", move |req| {
1549 let c = c.clone();
1550 async move {
1551 match role_guard(&c, &req, Role::Administrator).await? {
1552 Guard::Redirect(r) => Ok(r),
1553 Guard::Allow(ident) => {
1554 let user_id = parse_id(req.param("id"))?;
1555 let session_id = parse_id(req.param("session_id"))?;
1556 super::admin_recovery_handlers::do_admin_revoke_one_session(
1557 &c, ident, user_id, session_id, req,
1558 )
1559 .await
1560 }
1561 }
1562 }
1563 });
1564
1565 let c = ctx.clone();
1572 let ac = auth_ctx.clone();
1573 let router = router.get("/admin/users/:id", move |req| {
1574 let c = c.clone();
1575 let ac = ac.clone();
1576 async move {
1577 match role_guard(&c, &req, Role::Administrator).await? {
1578 Guard::Redirect(r) => Ok(r),
1579 Guard::Allow(ident) => {
1580 let id = parse_id(req.param("id"))?;
1581 let q = req.query();
1582 let tab = q.get("tab").map(|s| s.to_string());
1583 let page: i64 = q.get("page").and_then(|s| s.parse().ok()).unwrap_or(1);
1584 let viewing_session_id = match req
1585 .header("cookie")
1586 .and_then(crate::auth::session_token_from_cookie)
1587 {
1588 Some(token) => crate::auth::current_session_id(&ac.db, &token)
1589 .await
1590 .ok()
1591 .flatten(),
1592 None => None,
1593 };
1594 super::builtin::show_user_view(
1595 &ac,
1596 ident,
1597 id,
1598 handlers::csrf_token(&req),
1599 tab,
1600 page,
1601 viewing_session_id,
1602 )
1603 .await
1604 }
1605 }
1606 }
1607 });
1608
1609 let c = ctx.clone();
1611 let ac = auth_ctx.clone();
1612 let router = router.get("/admin/groups", move |req| {
1613 let c = c.clone();
1614 let ac = ac.clone();
1615 async move {
1616 match role_guard(&c, &req, Role::Administrator).await? {
1617 Guard::Redirect(r) => Ok(r),
1618 Guard::Allow(ident) => {
1619 super::builtin::list_groups(&ac, ident, handlers::csrf_token(&req)).await
1620 }
1621 }
1622 }
1623 });
1624
1625 let c = ctx.clone();
1626 let ac = auth_ctx.clone();
1627 let router = router.get("/admin/groups/new", move |req| {
1628 let c = c.clone();
1629 let ac = ac.clone();
1630 async move {
1631 match role_guard(&c, &req, Role::Administrator).await? {
1632 Guard::Redirect(r) => Ok(r),
1633 Guard::Allow(ident) => {
1634 super::builtin::show_new_group(&ac, ident, handlers::csrf_token(&req)).await
1635 }
1636 }
1637 }
1638 });
1639
1640 let c = ctx.clone();
1641 let ac = auth_ctx.clone();
1642 let router = router.post("/admin/groups/new", move |req| {
1643 let c = c.clone();
1644 let ac = ac.clone();
1645 async move {
1646 match role_guard(&c, &req, Role::Administrator).await? {
1647 Guard::Redirect(r) => Ok(r),
1648 Guard::Allow(ident) => super::builtin::do_new_group(&ac, ident, req).await,
1649 }
1650 }
1651 });
1652
1653 let c = ctx.clone();
1654 let ac = auth_ctx.clone();
1655 let router = router.get("/admin/groups/:id/edit", move |req| {
1656 let c = c.clone();
1657 let ac = ac.clone();
1658 async move {
1659 match role_guard(&c, &req, Role::Administrator).await? {
1660 Guard::Redirect(r) => Ok(r),
1661 Guard::Allow(ident) => {
1662 let id = parse_id(req.param("id"))?;
1663 super::builtin::show_group_edit(&ac, ident, id, handlers::csrf_token(&req))
1664 .await
1665 }
1666 }
1667 }
1668 });
1669
1670 let c = ctx.clone();
1671 let ac = auth_ctx.clone();
1672 let router = router.post("/admin/groups/:id/edit", move |req| {
1673 let c = c.clone();
1674 let ac = ac.clone();
1675 async move {
1676 match role_guard(&c, &req, Role::Administrator).await? {
1677 Guard::Redirect(r) => Ok(r),
1678 Guard::Allow(ident) => {
1679 let id = parse_id(req.param("id"))?;
1680 super::builtin::do_group_edit(&ac, ident, id, req).await
1681 }
1682 }
1683 }
1684 });
1685
1686 let c = ctx.clone();
1687 let ac = auth_ctx.clone();
1688 let router = router.get("/admin/groups/:id/delete", move |req| {
1689 let c = c.clone();
1690 let ac = ac.clone();
1691 async move {
1692 match role_guard(&c, &req, Role::Administrator).await? {
1693 Guard::Redirect(r) => Ok(r),
1694 Guard::Allow(ident) => {
1695 let id = parse_id(req.param("id"))?;
1696 super::builtin::show_group_delete(&ac, ident, id, handlers::csrf_token(&req))
1697 .await
1698 }
1699 }
1700 }
1701 });
1702
1703 let c = ctx.clone();
1704 let ac = auth_ctx.clone();
1705 let router = router.post("/admin/groups/:id/delete", move |req| {
1706 let c = c.clone();
1707 let ac = ac.clone();
1708 async move {
1709 match role_guard(&c, &req, Role::Administrator).await? {
1710 Guard::Redirect(r) => Ok(r),
1711 Guard::Allow(ident) => {
1712 let id = parse_id(req.param("id"))?;
1713 super::builtin::do_group_delete(&ac, ident, id, req).await
1714 }
1715 }
1716 }
1717 });
1718
1719 let c = ctx.clone();
1728 let router = router.get("/admin/uploads/:filename", move |req| {
1729 let c = c.clone();
1730 async move {
1731 match role_guard(&c, &req, Role::Staff).await? {
1732 Guard::Redirect(r) => Ok(r),
1733 Guard::Allow(ident) => {
1734 let filename = req
1735 .param("filename")
1736 .map(str::to_string)
1737 .unwrap_or_default();
1738 handlers::serve_upload(&c, ident, &filename, req).await
1739 }
1740 }
1741 }
1742 });
1743
1744 let c = ctx.clone();
1750 let router = router.get("/admin/_lookup/:admin_name", move |req| {
1751 let c = c.clone();
1752 async move {
1753 let name = model_name_from_req(&req)?;
1754 let perm = perm_for(&c, &name, "view")?;
1755 match perm_guard(&c, &req, &perm).await? {
1756 Guard::Redirect(r) => Ok(r),
1757 Guard::Allow(ident) => handlers::lookup_model(&c, ident, &name, req).await,
1758 }
1759 }
1760 });
1761
1762 let c = ctx.clone();
1770 let router = router.get("/admin/_search", move |req| {
1771 let c = c.clone();
1772 async move {
1773 match role_guard(&c, &req, Role::Staff).await? {
1774 Guard::Redirect(r) => Ok(r),
1775 Guard::Allow(ident) => handlers::search_models(&c, ident, req).await,
1776 }
1777 }
1778 });
1779
1780 let c = ctx.clone();
1788 let router = router.get("/admin/docs", move |req| {
1789 let c = c.clone();
1790 async move {
1791 match role_guard(&c, &req, Role::Staff).await? {
1792 Guard::Redirect(r) => Ok(r),
1793 Guard::Allow(ident) => handlers::show_docs_index(&c, ident, &req).await,
1794 }
1795 }
1796 });
1797 let c = ctx.clone();
1798 let router = router.get("/admin/docs/:slug", move |req| {
1799 let c = c.clone();
1800 async move {
1801 let slug = req
1802 .param("slug")
1803 .ok_or_else(|| Error::BadRequest("missing doc slug".into()))?
1804 .to_string();
1805 match role_guard(&c, &req, Role::Staff).await? {
1806 Guard::Redirect(r) => Ok(r),
1807 Guard::Allow(ident) => handlers::show_doc_page(&c, ident, &slug, &req).await,
1808 }
1809 }
1810 });
1811
1812 let c = ctx.clone();
1819 let router = router.get("/admin/apis/openapi.json", move |req| {
1820 let c = c.clone();
1821 async move {
1822 match role_guard(&c, &req, Role::Staff).await? {
1823 Guard::Redirect(r) => Ok(r),
1824 Guard::Allow(_) => {
1825 let spec = super::openapi::build_spec(&c.admin);
1826 super::json_api::json_response(spec)
1827 }
1828 }
1829 }
1830 });
1831
1832 let c = ctx.clone();
1838 let router = router.get("/admin/apis/sdk.ts", move |req| {
1839 let c = c.clone();
1840 async move {
1841 match role_guard(&c, &req, Role::Staff).await? {
1842 Guard::Redirect(r) => Ok(r),
1843 Guard::Allow(_) => {
1844 let body = super::sdk_gen::build_typescript(&c.admin);
1845 Ok(crate::http::Response::ok(body)
1846 .with_header("content-type", "text/typescript; charset=utf-8")
1847 .with_header(
1848 "content-disposition",
1849 "attachment; filename=\"rustio-sdk.ts\"",
1850 ))
1851 }
1852 }
1853 }
1854 });
1855
1856 let c = ctx.clone();
1862 let router = router.get("/admin/apis", move |req| {
1863 let c = c.clone();
1864 async move {
1865 match role_guard(&c, &req, Role::Staff).await? {
1866 Guard::Redirect(r) => Ok(r),
1867 Guard::Allow(ident) => handlers::show_apis_index(&c, ident, &req).await,
1868 }
1869 }
1870 });
1871
1872 let c = ctx.clone();
1880 let router = router.get("/admin/apis/playground", move |req| {
1881 let c = c.clone();
1882 async move {
1883 match role_guard(&c, &req, Role::Staff).await? {
1884 Guard::Redirect(r) => Ok(r),
1885 Guard::Allow(ident) => handlers::show_apis_playground(&c, ident, &req).await,
1886 }
1887 }
1888 });
1889
1890 let c = ctx.clone();
1892 let router = router.get("/admin/:admin_name", move |req| {
1893 let c = c.clone();
1894 async move {
1895 let name = model_name_from_req(&req)?;
1896 let perm = perm_for(&c, &name, "view")?;
1897 match perm_guard(&c, &req, &perm).await? {
1898 Guard::Redirect(r) => Ok(r),
1899 Guard::Allow(ident) => handlers::list_model(&c, ident, &name, &req).await,
1900 }
1901 }
1902 });
1903
1904 let c = ctx.clone();
1909 let router = router.get("/admin/:admin_name/export.csv", move |req| {
1910 let c = c.clone();
1911 async move {
1912 let name = model_name_from_req(&req)?;
1913 let perm = perm_for(&c, &name, "view")?;
1914 match perm_guard(&c, &req, &perm).await? {
1915 Guard::Redirect(r) => Ok(r),
1916 Guard::Allow(ident) => handlers::export_model_csv(&c, ident, &name, req).await,
1917 }
1918 }
1919 });
1920
1921 let c = ctx.clone();
1927 let router = router.post("/admin/:admin_name/import.csv", move |req| {
1928 let c = c.clone();
1929 async move {
1930 let name = model_name_from_req(&req)?;
1931 let perm = perm_for(&c, &name, "change")?;
1932 match perm_guard(&c, &req, &perm).await? {
1933 Guard::Redirect(r) => Ok(r),
1934 Guard::Allow(ident) => handlers::import_model_csv(&c, ident, &name, req).await,
1935 }
1936 }
1937 });
1938
1939 let c = ctx.clone();
1945 let router = router.post("/admin/:admin_name/saved_filters", move |req| {
1946 let c = c.clone();
1947 async move {
1948 let name = model_name_from_req(&req)?;
1949 let perm = perm_for(&c, &name, "view")?;
1950 match perm_guard(&c, &req, &perm).await? {
1951 Guard::Redirect(r) => Ok(r),
1952 Guard::Allow(ident) => handlers::do_save_filter(&c, ident, &name, req).await,
1953 }
1954 }
1955 });
1956
1957 let c = ctx.clone();
1961 let router = router.post("/admin/:admin_name/saved_filters/:id/delete", move |req| {
1962 let c = c.clone();
1963 async move {
1964 let name = model_name_from_req(&req)?;
1965 let id = parse_id(req.param("id"))?;
1966 let perm = perm_for(&c, &name, "view")?;
1967 match perm_guard(&c, &req, &perm).await? {
1968 Guard::Redirect(r) => Ok(r),
1969 Guard::Allow(ident) => handlers::do_delete_filter(&c, ident, &name, id, req).await,
1970 }
1971 }
1972 });
1973
1974 let c = ctx.clone();
1976 let router = router.get("/admin/:admin_name/new", move |req| {
1977 let c = c.clone();
1978 async move {
1979 let name = model_name_from_req(&req)?;
1980 let perm = perm_for(&c, &name, "add")?;
1981 match perm_guard(&c, &req, &perm).await? {
1982 Guard::Redirect(r) => Ok(r),
1983 Guard::Allow(ident) => handlers::show_new_form(&c, ident, &name, &req).await,
1984 }
1985 }
1986 });
1987 let c = ctx.clone();
1988 let router = router.post("/admin/:admin_name/new", move |req| {
1989 let c = c.clone();
1990 async move {
1991 let name = model_name_from_req(&req)?;
1992 let perm = perm_for(&c, &name, "add")?;
1993 match perm_guard(&c, &req, &perm).await? {
1994 Guard::Redirect(r) => Ok(r),
1995 Guard::Allow(ident) => handlers::do_create(&c, ident, &name, req).await,
1996 }
1997 }
1998 });
1999
2000 let c = ctx.clone();
2007 let router = router.get("/admin/:admin_name/:id", move |req| {
2008 let c = c.clone();
2009 async move {
2010 let name = model_name_from_req(&req)?;
2011 let perm = perm_for(&c, &name, "view")?;
2012 match perm_guard(&c, &req, &perm).await? {
2013 Guard::Redirect(r) => Ok(r),
2014 Guard::Allow(ident) => {
2015 let id = parse_id(req.param("id"))?;
2016 handlers::show_object_json(&c, ident, &name, id, &req).await
2017 }
2018 }
2019 }
2020 });
2021
2022 let c = ctx.clone();
2024 let router = router.get("/admin/:admin_name/:id/edit", move |req| {
2025 let c = c.clone();
2026 async move {
2027 let name = model_name_from_req(&req)?;
2028 let perm = perm_for(&c, &name, "change")?;
2029 match perm_guard(&c, &req, &perm).await? {
2030 Guard::Redirect(r) => Ok(r),
2031 Guard::Allow(ident) => {
2032 let id = parse_id(req.param("id"))?;
2033 handlers::show_edit_form(&c, ident, &name, id, &req).await
2034 }
2035 }
2036 }
2037 });
2038 let c = ctx.clone();
2039 let router = router.post("/admin/:admin_name/:id/edit", move |req| {
2040 let c = c.clone();
2041 async move {
2042 let name = model_name_from_req(&req)?;
2043 let perm = perm_for(&c, &name, "change")?;
2044 match perm_guard(&c, &req, &perm).await? {
2045 Guard::Redirect(r) => Ok(r),
2046 Guard::Allow(ident) => {
2047 let id = parse_id(req.param("id"))?;
2048 handlers::do_update(&c, ident, &name, id, req).await
2049 }
2050 }
2051 }
2052 });
2053
2054 let c = ctx.clone();
2057 let router = router.get("/admin/:admin_name/:id/history", 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, "view")?;
2062 match perm_guard(&c, &req, &perm).await? {
2063 Guard::Redirect(r) => Ok(r),
2064 Guard::Allow(ident) => {
2065 let id = parse_id(req.param("id"))?;
2066 handlers::show_object_history(&c, ident, &name, id, &req).await
2067 }
2068 }
2069 }
2070 });
2071
2072 let c = ctx.clone();
2074 let router = router.get("/admin/:admin_name/:id/delete", move |req| {
2075 let c = c.clone();
2076 async move {
2077 let name = model_name_from_req(&req)?;
2078 let perm = perm_for(&c, &name, "delete")?;
2079 match perm_guard(&c, &req, &perm).await? {
2080 Guard::Redirect(r) => Ok(r),
2081 Guard::Allow(ident) => {
2082 let id = parse_id(req.param("id"))?;
2083 handlers::show_delete_confirm(&c, ident, &name, id, &req).await
2084 }
2085 }
2086 }
2087 });
2088 let c = ctx.clone();
2089 let router = router.post("/admin/:admin_name/:id/delete", move |req| {
2090 let c = c.clone();
2091 async move {
2092 let name = model_name_from_req(&req)?;
2093 let perm = perm_for(&c, &name, "delete")?;
2094 match perm_guard(&c, &req, &perm).await? {
2095 Guard::Redirect(r) => Ok(r),
2096 Guard::Allow(ident) => {
2097 let id = parse_id(req.param("id"))?;
2098 handlers::do_delete(&c, ident, &name, req, id).await
2099 }
2100 }
2101 }
2102 });
2103
2104 let c = ctx.clone();
2109 let router = router.post("/admin/:admin_name/bulk_delete", move |req| {
2110 let c = c.clone();
2111 async move {
2112 let name = model_name_from_req(&req)?;
2113 let perm = perm_for(&c, &name, "delete")?;
2114 match perm_guard(&c, &req, &perm).await? {
2115 Guard::Redirect(r) => Ok(r),
2116 Guard::Allow(ident) => handlers::handle_bulk_delete(&c, ident, &name, &req).await,
2117 }
2118 }
2119 });
2120
2121 let c = ctx.clone();
2126 router.post("/admin/:admin_name/bulk/:action", move |req| {
2127 let c = c.clone();
2128 async move {
2129 let name = model_name_from_req(&req)?;
2130 let action = req
2131 .param("action")
2132 .ok_or_else(|| Error::BadRequest("missing bulk action name".into()))?
2133 .to_string();
2134 let perm = perm_for(&c, &name, "change")?;
2135 match perm_guard(&c, &req, &perm).await? {
2136 Guard::Redirect(r) => Ok(r),
2137 Guard::Allow(ident) => {
2138 handlers::handle_bulk_action(&c, ident, &name, &action, &req).await
2139 }
2140 }
2141 }
2142 })
2143}
2144
2145#[cfg(test)]
2146mod tests {
2147 use super::*;
2148
2149 fn make_identity(role: Role, is_active: bool) -> Identity {
2150 Identity {
2151 user_id: 42,
2152 email: "test@example.com".into(),
2153 role,
2154 is_active,
2155 is_demo: false,
2156 demo_label: None,
2157 must_change_password: false,
2158 mfa_enabled: false,
2159 trust_level: crate::auth::SessionTrust::Authenticated,
2160 }
2161 }
2162
2163 #[test]
2168 fn admin_css_payload_none_is_the_baked_bundle_verbatim() {
2169 let css = admin_css_payload(None);
2170 assert_eq!(css.as_ref(), ADMIN_CSS.as_bytes());
2171 assert!(ADMIN_CSS.contains("--rio-accent"));
2173 }
2174
2175 #[test]
2176 fn admin_css_payload_appends_override_after_baked_bundle() {
2177 let override_css = ":root{--rio-accent:#abcdef}";
2178 let css = admin_css_payload(Some(override_css));
2179 let text = std::str::from_utf8(css.as_ref()).expect("utf-8");
2180 assert!(text.ends_with(override_css));
2182 let baked = text.find("--rio-brand-light").expect("baked token present");
2186 let overridden = text
2187 .rfind("--rio-accent:#abcdef")
2188 .expect("override present");
2189 assert!(overridden > baked, "override must follow the baked bundle");
2190 }
2191
2192 #[test]
2197 fn role_guard_decision_admin_meets_staff_floor() {
2198 let id = make_identity(Role::Administrator, true);
2199 assert!(id.role.includes(Role::Staff));
2200 }
2201
2202 #[test]
2203 fn role_guard_decision_user_does_not_meet_staff() {
2204 let id = make_identity(Role::User, true);
2205 assert!(!id.role.includes(Role::Staff));
2206 }
2207
2208 #[test]
2209 fn role_guard_decision_administrator_does_not_meet_developer() {
2210 let id = make_identity(Role::Administrator, true);
2211 assert!(!id.role.includes(Role::Developer));
2212 }
2213
2214 #[test]
2215 fn role_guard_decision_developer_meets_everything() {
2216 let id = make_identity(Role::Developer, true);
2217 for &min in &[
2218 Role::User,
2219 Role::Staff,
2220 Role::Supervisor,
2221 Role::Administrator,
2222 Role::Developer,
2223 ] {
2224 assert!(id.role.includes(min), "Developer should meet {min:?}");
2225 }
2226 }
2227
2228 #[test]
2231 fn perm_guard_admin_short_circuits_without_perm() {
2232 let id = make_identity(Role::Administrator, true);
2233 assert!(perm_guard_verdict(&id, false));
2234 }
2235
2236 #[test]
2237 fn perm_guard_developer_short_circuits_without_perm() {
2238 let id = make_identity(Role::Developer, true);
2239 assert!(perm_guard_verdict(&id, false));
2240 }
2241
2242 #[test]
2243 fn perm_guard_staff_with_perm_passes() {
2244 let id = make_identity(Role::Staff, true);
2245 assert!(perm_guard_verdict(&id, true));
2246 }
2247
2248 #[test]
2249 fn perm_guard_staff_without_perm_denies() {
2250 let id = make_identity(Role::Staff, true);
2251 assert!(!perm_guard_verdict(&id, false));
2252 }
2253
2254 #[test]
2255 fn perm_guard_inactive_admin_denies_even_with_bypass() {
2256 let id = make_identity(Role::Administrator, false);
2258 assert!(!perm_guard_verdict(&id, true));
2259 }
2260
2261 #[test]
2262 fn perm_guard_supervisor_without_perm_denies() {
2263 let id = make_identity(Role::Supervisor, true);
2265 assert!(!perm_guard_verdict(&id, false));
2266 }
2267
2268 #[test]
2273 fn strict_mailer_guard_passes_for_default_admin() {
2274 let admin = super::super::types::Admin::new();
2275 assert!(strict_mailer_guard_check(&admin).is_ok());
2276 }
2277
2278 #[test]
2281 fn strict_mailer_guard_fails_when_required_but_default_mailer() {
2282 use crate::auth::DefaultRecoveryPolicy;
2283 let admin = super::super::types::Admin::new().recovery_policy(std::sync::Arc::new(
2284 DefaultRecoveryPolicy::new().with_strict_mailer_required(true),
2285 ));
2286 let err = strict_mailer_guard_check(&admin).expect_err("guard should fail");
2287 assert!(
2288 err.contains("strict_mailer_required"),
2289 "error message must name the policy method: {err}"
2290 );
2291 assert!(
2292 err.contains("Admin::mailer"),
2293 "error message must direct the operator to the fix: {err}"
2294 );
2295 }
2296
2297 #[test]
2302 fn strict_mailer_guard_passes_when_mailer_was_explicitly_overridden() {
2303 use crate::auth::DefaultRecoveryPolicy;
2304 use crate::email::LogMailer;
2305 let admin = super::super::types::Admin::new()
2306 .recovery_policy(std::sync::Arc::new(
2307 DefaultRecoveryPolicy::new().with_strict_mailer_required(true),
2308 ))
2309 .mailer(std::sync::Arc::new(LogMailer));
2310 assert!(strict_mailer_guard_check(&admin).is_ok());
2311 }
2312
2313 #[test]
2316 fn strict_mailer_guard_passes_when_strict_mode_disabled() {
2317 let admin = super::super::types::Admin::new();
2318 assert!(strict_mailer_guard_check(&admin).is_ok());
2319 }
2320
2321 #[test]
2324 fn whitelist_accepts_the_three_locked_paths() {
2325 assert!(super::is_must_change_whitelisted_path(
2327 "/admin/must-change-password"
2328 ));
2329 assert!(super::is_must_change_whitelisted_path("/admin/logout"));
2330 assert!(super::is_must_change_whitelisted_path(
2331 "/admin/account/sessions"
2332 ));
2333 }
2334
2335 #[test]
2336 fn whitelist_rejects_subpaths_of_account_sessions() {
2337 assert!(!super::is_must_change_whitelisted_path(
2342 "/admin/account/sessions/revoke"
2343 ));
2344 assert!(!super::is_must_change_whitelisted_path(
2345 "/admin/account/sessions/revoke-others"
2346 ));
2347 assert!(!super::is_must_change_whitelisted_path(
2348 "/admin/account/sessions/"
2349 ));
2350 }
2351
2352 #[test]
2353 fn whitelist_rejects_other_admin_paths() {
2354 for path in [
2355 "/admin",
2356 "/admin/",
2357 "/admin/users",
2358 "/admin/users/42",
2359 "/admin/login",
2360 "/admin/password_change",
2361 "/admin/forgot-password",
2362 "/admin/reauth",
2363 "/admin/must-change-password/", ] {
2365 assert!(
2366 !super::is_must_change_whitelisted_path(path),
2367 "expected reject for {path:?}"
2368 );
2369 }
2370 }
2371
2372 #[test]
2373 fn whitelist_rejects_paths_outside_admin_surface() {
2374 for path in ["/", "/login", "/static/admin.css", "/api"] {
2375 assert!(
2376 !super::is_must_change_whitelisted_path(path),
2377 "expected reject for {path:?}"
2378 );
2379 }
2380 }
2381
2382 #[test]
2385 fn read_only_allows_auth_flow_exact_paths() {
2386 for path in [
2387 "/admin/login",
2388 "/admin/logout",
2389 "/admin/reauth",
2390 "/admin/forgot-password",
2391 "/admin/mfa/verify",
2392 "/admin/must-change-password",
2393 "/admin/password_change",
2394 ] {
2395 assert!(
2396 super::is_read_only_writable_path(path),
2397 "auth path {path:?} must be writable in read-only mode"
2398 );
2399 }
2400 }
2401
2402 #[test]
2403 fn read_only_allows_prefix_paths() {
2404 for path in [
2408 "/admin/reset-password/abc123",
2409 "/admin/reset-password/abc123/whatever",
2410 "/admin/account/sessions/42/revoke",
2411 "/admin/account/sessions/revoke-all",
2412 "/admin/account/mfa/enroll",
2413 "/admin/account/mfa/disable",
2414 ] {
2415 assert!(
2416 super::is_read_only_writable_path(path),
2417 "prefix-allowlisted path {path:?} must be writable"
2418 );
2419 }
2420 }
2421
2422 #[test]
2423 fn read_only_blocks_project_data_mutations() {
2424 for path in [
2427 "/admin/posts/new",
2428 "/admin/posts/42/edit",
2429 "/admin/posts/42/delete",
2430 "/admin/posts/bulk_delete",
2431 "/admin/posts/bulk/archive",
2432 "/admin/users/new",
2433 "/admin/users/42/edit",
2434 "/admin/users/42/reset-password",
2435 "/admin/users/42/lock",
2436 "/admin/users/42/sessions/99/revoke",
2437 "/admin/groups/new",
2438 "/admin/groups/42/delete",
2439 ] {
2440 assert!(
2441 !super::is_read_only_writable_path(path),
2442 "data-mutation path {path:?} must be blocked in read-only mode"
2443 );
2444 }
2445 }
2446
2447 #[test]
2448 fn read_only_blocks_random_paths_outside_admin_surface() {
2449 for path in ["/", "/login", "/static/admin.css", "/api/v1/posts"] {
2454 assert!(
2455 !super::is_read_only_writable_path(path),
2456 "non-admin path {path:?} must not be writable"
2457 );
2458 }
2459 }
2460
2461 #[test]
2462 fn extract_admin_name_parses_slug_segment() {
2463 assert_eq!(super::extract_admin_name("/admin/posts"), Some("posts"));
2464 assert_eq!(super::extract_admin_name("/admin/posts/"), Some("posts"));
2465 assert_eq!(
2466 super::extract_admin_name("/admin/posts/42/edit"),
2467 Some("posts")
2468 );
2469 assert_eq!(
2470 super::extract_admin_name("/admin/users/42/sessions/99/revoke"),
2471 Some("users")
2472 );
2473 }
2474
2475 #[test]
2476 fn extract_admin_name_rejects_root_reserved_and_non_admin() {
2477 assert_eq!(super::extract_admin_name("/admin/"), None);
2479 assert_eq!(super::extract_admin_name("/admin"), None);
2480 assert_eq!(super::extract_admin_name("/admin/_search"), None);
2483 assert_eq!(super::extract_admin_name("/admin/_lookup/posts"), None);
2484 assert_eq!(super::extract_admin_name("/login"), None);
2486 assert_eq!(super::extract_admin_name("/static/admin.css"), None);
2487 }
2488
2489 #[test]
2490 fn read_only_model_builder_and_accessor_round_trip() {
2491 let admin = super::super::types::Admin::new()
2492 .read_only_model("archive_posts")
2493 .read_only_model("legacy_invoices");
2494 assert!(admin.is_model_read_only("archive_posts"));
2495 assert!(admin.is_model_read_only("legacy_invoices"));
2496 assert!(!admin.is_model_read_only("posts"));
2497 assert!(!admin.is_read_only());
2499 }
2500
2501 #[test]
2502 fn is_mutating_method_recognises_write_verbs() {
2503 use hyper::Method;
2504 for m in [Method::POST, Method::PUT, Method::PATCH, Method::DELETE] {
2505 assert!(super::is_mutating_method(&m), "{m} must be mutating");
2506 }
2507 for m in [Method::GET, Method::HEAD, Method::OPTIONS] {
2508 assert!(!super::is_mutating_method(&m), "{m} must not be mutating");
2509 }
2510 }
2511}