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