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/tokens/motion.css"),
67 "\n",
68 include_str!("../../assets/static/admin/tokens/compat.css"),
69 "\n",
70 include_str!("../../assets/static/admin/base/fonts.css"),
72 "\n",
73 include_str!("../../assets/static/admin/base/base.css"),
74 "\n",
75 include_str!("../../assets/static/admin/base/typography-i18n.css"),
76 "\n",
77 include_str!("../../assets/static/admin/components/buttons.css"),
79 "\n",
80 include_str!("../../assets/static/admin/components/forms.css"),
81 "\n",
82 include_str!("../../assets/static/admin/components/data.css"),
83 "\n",
84 include_str!("../../assets/static/admin/components/feedback.css"),
85 "\n",
86 include_str!("../../assets/static/admin/components/navigation.css"),
87 "\n",
88 include_str!("../../assets/static/admin/components/code.css"),
89 "\n",
90 include_str!("../../assets/static/admin/components/adaptive-views.css"),
91 "\n",
92 include_str!("../../assets/static/admin/layout/console.css"),
94 "\n",
95 include_str!("../../assets/static/admin/pages/dashboard.css"),
97 "\n",
98 include_str!("../../assets/static/admin/pages/list.css"),
99 "\n",
100 include_str!("../../assets/static/admin/pages/form.css"),
101 "\n",
102 include_str!("../../assets/static/admin/pages/auth.css"),
103 "\n",
104 include_str!("../../assets/static/admin/pages/states.css"),
105 "\n",
106 include_str!("../../assets/static/admin/pages/permissions.css"),
107 "\n",
108 include_str!("../../assets/static/admin/pages/detail.css"),
109 "\n",
110 include_str!("../../assets/static/admin/pages/account.css"),
111 "\n",
112 include_str!("../../assets/static/admin/pages/tools.css"),
113 "\n",
114 include_str!("../../assets/static/admin/pages/view-designer.css"),
115 "\n",
116 include_str!("../../assets/static/admin/print/print.css"),
118);
119
120fn admin_css_payload(override_css: Option<&str>) -> bytes::Bytes {
134 match override_css {
135 Some(extra) => bytes::Bytes::from(format!(
136 "{ADMIN_CSS}\n/* ---- RUSTIO_TOKENS_CSS override ---- */\n{extra}"
137 )),
138 None => bytes::Bytes::from_static(ADMIN_CSS.as_bytes()),
139 }
140}
141
142fn override_is_dark_leak_hazard(css: &str) -> bool {
152 let defines_root_colors = css.contains(":root")
153 && (css.contains("--rio-bg")
154 || css.contains("--rio-surface")
155 || css.contains("--rio-accent"));
156 let handles_dark =
157 css.contains("[data-theme=\"dark\"]") || css.contains("prefers-color-scheme");
158 defines_root_colors && !handles_dark
159}
160
161fn load_token_override() -> Option<String> {
169 let path = std::env::var("RUSTIO_TOKENS_CSS").ok()?;
170 match std::fs::read_to_string(&path) {
171 Ok(css) => {
172 if override_is_dark_leak_hazard(&css) {
173 log::warn!(
174 "RUSTIO_TOKENS_CSS={path:?} is a light-only override: it sets :root \
175 color tokens but has no [data-theme=\"dark\"] or prefers-color-scheme \
176 block, so it will leak light surfaces into dark mode. Regenerate with a \
177 dark-aware generator or add a dark block."
178 );
179 }
180 Some(css)
181 }
182 Err(e) => {
183 log::warn!("RUSTIO_TOKENS_CSS={path:?} unreadable: {e}; serving baked CSS");
184 None
185 }
186 }
187}
188
189const ADMIN_JS: &str = include_str!("../../assets/static/admin.js");
192
193const FONT_INTER: &[u8] = include_bytes!("../../assets/static/fonts/InterVariable.woff2");
206const FONT_TAJAWAL_REG: &[u8] = include_bytes!("../../assets/static/fonts/Tajawal-Regular.woff2");
207const FONT_TAJAWAL_MED: &[u8] = include_bytes!("../../assets/static/fonts/Tajawal-Medium.woff2");
208const FONT_TAJAWAL_BOLD: &[u8] = include_bytes!("../../assets/static/fonts/Tajawal-Bold.woff2");
209const FONT_NOTO_NASKH_AR: &[u8] =
210 include_bytes!("../../assets/static/fonts/NotoNaskhArabic-Variable.woff2");
211const FONT_NOTO_THAI: &[u8] =
212 include_bytes!("../../assets/static/fonts/NotoSansThai-Variable.woff2");
213const FONT_NOTO_DEVA: &[u8] =
214 include_bytes!("../../assets/static/fonts/NotoSansDevanagari-Variable.woff2");
215const FONT_NOTO_JP: &[u8] = include_bytes!("../../assets/static/fonts/NotoSansJP-Regular.woff2");
216const FONT_NOTO_KR: &[u8] = include_bytes!("../../assets/static/fonts/NotoSansKR-Regular.woff2");
217const FONT_NOTO_SC: &[u8] = include_bytes!("../../assets/static/fonts/NotoSansSC-Regular.woff2");
218const FONT_JETBRAINS_LATIN: &[u8] =
224 include_bytes!("../../assets/static/fonts/JetBrainsMono-Variable-latin.woff2");
225const FONT_JETBRAINS_LATIN_EXT: &[u8] =
226 include_bytes!("../../assets/static/fonts/JetBrainsMono-Variable-latinext.woff2");
227
228fn font_response(bytes: &'static [u8]) -> Response {
230 Response::new(hyper::StatusCode::OK, bytes::Bytes::from_static(bytes))
231 .with_header("content-type", "font/woff2")
232 .with_header("cache-control", "public, max-age=31536000, immutable")
233}
234
235fn register_font_routes(router: Router) -> Router {
239 const FONTS: &[(&str, &[u8])] = &[
240 ("/static/fonts/InterVariable.woff2", FONT_INTER),
241 (
242 "/static/fonts/JetBrainsMono-Variable-latin.woff2",
243 FONT_JETBRAINS_LATIN,
244 ),
245 (
246 "/static/fonts/JetBrainsMono-Variable-latinext.woff2",
247 FONT_JETBRAINS_LATIN_EXT,
248 ),
249 ("/static/fonts/Tajawal-Regular.woff2", FONT_TAJAWAL_REG),
250 ("/static/fonts/Tajawal-Medium.woff2", FONT_TAJAWAL_MED),
251 ("/static/fonts/Tajawal-Bold.woff2", FONT_TAJAWAL_BOLD),
252 (
253 "/static/fonts/NotoNaskhArabic-Variable.woff2",
254 FONT_NOTO_NASKH_AR,
255 ),
256 ("/static/fonts/NotoSansThai-Variable.woff2", FONT_NOTO_THAI),
257 (
258 "/static/fonts/NotoSansDevanagari-Variable.woff2",
259 FONT_NOTO_DEVA,
260 ),
261 ("/static/fonts/NotoSansJP-Regular.woff2", FONT_NOTO_JP),
262 ("/static/fonts/NotoSansKR-Regular.woff2", FONT_NOTO_KR),
263 ("/static/fonts/NotoSansSC-Regular.woff2", FONT_NOTO_SC),
264 ];
265 let mut router = router;
266 for &(path, bytes) in FONTS {
267 router = router.get(path, move |_req| async move { Ok(font_response(bytes)) });
268 }
269 router
270}
271
272use super::handlers::{self, AdminCtx};
273use super::render;
274use super::types::Admin;
275
276enum Guard {
280 Allow(Identity),
281 Redirect(Response),
282}
283
284const MUST_CHANGE_WHITELIST: &[&str] = &[
294 "/admin/must-change-password",
295 "/admin/logout",
296 "/admin/account/sessions",
297];
298
299fn is_must_change_whitelisted_path(path: &str) -> bool {
303 MUST_CHANGE_WHITELIST.contains(&path)
304}
305
306const READ_ONLY_EXACT_ALLOW: &[&str] = &[
317 "/admin/login",
318 "/admin/logout",
319 "/admin/reauth",
320 "/admin/forgot-password",
321 "/admin/mfa/verify",
322 "/admin/must-change-password",
323 "/admin/password_change",
324];
325
326const READ_ONLY_PREFIX_ALLOW: &[&str] = &[
327 "/admin/reset-password/",
330 "/admin/account/sessions/",
333 "/admin/account/mfa/",
336];
337
338fn is_saved_filter_path(path: &str) -> bool {
344 if let Some(rest) = path.strip_prefix("/admin/") {
345 if let Some((_, after)) = rest.split_once('/') {
349 return after == "saved_filters" || after.starts_with("saved_filters/");
350 }
351 }
352 false
353}
354
355pub(crate) fn is_mutating_method(method: &hyper::Method) -> bool {
361 matches!(
362 *method,
363 hyper::Method::POST | hyper::Method::PUT | hyper::Method::PATCH | hyper::Method::DELETE
364 )
365}
366
367pub(crate) fn is_read_only_writable_path(path: &str) -> bool {
375 if READ_ONLY_EXACT_ALLOW.contains(&path) {
376 return true;
377 }
378 if READ_ONLY_PREFIX_ALLOW
379 .iter()
380 .any(|prefix| path.starts_with(prefix))
381 {
382 return true;
383 }
384 is_saved_filter_path(path)
385}
386
387pub(crate) fn extract_admin_name(path: &str) -> Option<&str> {
394 let rest = path.strip_prefix("/admin/")?;
395 let slug = rest.split('/').next()?;
396 if slug.is_empty() || slug.starts_with('_') {
397 return None;
398 }
399 Some(slug)
400}
401
402const MFA_ENROLL_WHITELIST: &[&str] = &[
415 "/admin/account/mfa/enroll",
416 "/admin/logout",
417 "/admin/account/sessions",
418];
419
420fn is_mfa_enroll_whitelisted_path(path: &str) -> bool {
421 MFA_ENROLL_WHITELIST.contains(&path)
422}
423
424const MFA_VERIFY_WHITELIST: &[&str] = &[
434 "/admin/mfa/verify",
435 "/admin/logout",
436 "/admin/account/sessions",
437];
438
439fn is_mfa_verify_whitelisted_path(path: &str) -> bool {
440 MFA_VERIFY_WHITELIST.contains(&path)
441}
442
443fn mfa_required_for_role(policy: crate::auth::MfaPolicy, role: Role) -> bool {
453 use crate::auth::MfaPolicy;
454 match policy {
455 MfaPolicy::Disabled | MfaPolicy::Optional => false,
456 MfaPolicy::Required => true,
457 MfaPolicy::RequiredForRoles(roles) => roles.contains(&role),
458 }
459}
460
461async fn login_guard(ctx: &AdminCtx, req: &Request) -> Result<Guard> {
462 let cookie = match req.header("cookie") {
463 Some(c) => c,
464 None => return Ok(Guard::Redirect(Response::redirect("/admin/login"))),
465 };
466 let token = match auth::session_token_from_cookie(cookie) {
467 Some(t) => t,
468 None => return Ok(Guard::Redirect(Response::redirect("/admin/login"))),
469 };
470 let ident = match auth::identity_from_session(&ctx.db, &token).await? {
471 Some(i) => i,
472 None => return Ok(Guard::Redirect(Response::redirect("/admin/login"))),
473 };
474 if !ident.is_active {
475 return Ok(Guard::Redirect(Response::redirect("/admin/login")));
476 }
477
478 if ident.must_change_password && !is_must_change_whitelisted_path(req.path()) {
484 return Ok(Guard::Redirect(Response::redirect(
485 "/admin/must-change-password",
486 )));
487 }
488
489 let policy = ctx.admin.active_mfa_policy();
498 if mfa_required_for_role(policy, ident.role)
499 && !ident.mfa_enabled
500 && !is_mfa_enroll_whitelisted_path(req.path())
501 {
502 return Ok(Guard::Redirect(Response::redirect(
503 "/admin/account/mfa/enroll",
504 )));
505 }
506
507 use crate::auth::SessionTrust;
515 if ident.mfa_enabled
516 && ident.trust_level != SessionTrust::MfaVerified
517 && !is_mfa_verify_whitelisted_path(req.path())
518 {
519 return Ok(Guard::Redirect(Response::redirect("/admin/mfa/verify")));
520 }
521
522 Ok(Guard::Allow(ident))
523}
524
525async fn role_guard(ctx: &AdminCtx, req: &Request, min: Role) -> Result<Guard> {
526 match login_guard(ctx, req).await? {
527 Guard::Redirect(r) => Ok(Guard::Redirect(r)),
528 Guard::Allow(ident) => {
529 if ident.role.includes(min) {
530 Ok(Guard::Allow(ident))
531 } else {
532 let body = render::render_forbidden_body(
533 &ctx.admin,
534 &ctx.templates,
535 &ident,
536 handlers::csrf_token(req),
537 None,
538 Some(min.label()),
539 )?;
540 Ok(Guard::Redirect(
541 Response::html(body).with_status(hyper::StatusCode::FORBIDDEN),
542 ))
543 }
544 }
545 }
546}
547
548async fn perm_guard(ctx: &AdminCtx, req: &Request, perm: &str) -> Result<Guard> {
549 match role_guard(ctx, req, Role::Staff).await? {
550 Guard::Redirect(r) => Ok(Guard::Redirect(r)),
551 Guard::Allow(ident) => {
552 if ident.role.bypasses_group_checks() {
553 return Ok(Guard::Allow(ident));
554 }
555 if auth::check_permission(&ctx.db, &ident, perm).await? {
556 Ok(Guard::Allow(ident))
557 } else {
558 let body = render::render_forbidden_body(
559 &ctx.admin,
560 &ctx.templates,
561 &ident,
562 handlers::csrf_token(req),
563 Some(perm.to_string()),
564 None,
565 )?;
566 Ok(Guard::Redirect(
567 Response::html(body).with_status(hyper::StatusCode::FORBIDDEN),
568 ))
569 }
570 }
571 }
572}
573
574#[cfg(test)]
577fn perm_guard_verdict(ident: &Identity, perm_held: bool) -> bool {
578 if !ident.is_active {
579 return false;
580 }
581 if ident.role.bypasses_group_checks() {
582 return true;
583 }
584 perm_held
585}
586
587fn parse_id(raw: Option<&str>) -> Result<i64> {
588 raw.and_then(|s| s.parse().ok())
589 .ok_or_else(|| Error::BadRequest("invalid id".into()))
590}
591
592fn model_name_from_req(req: &Request) -> Result<String> {
593 req.param("admin_name")
594 .map(|s| s.to_string())
595 .ok_or_else(|| Error::BadRequest("missing model".into()))
596}
597
598fn perm_for(ctx: &AdminCtx, admin_name: &str, action: &str) -> Result<String> {
599 let entry = ctx
600 .admin
601 .find(admin_name)
602 .ok_or_else(|| Error::NotFound(format!("no admin model: {admin_name}")))?;
603 let singular = entry.singular_name.to_ascii_lowercase();
604 Ok(format!("{admin_name}.{action}_{singular}"))
605}
606
607async fn resolve_identity_for_error_page(db: &Db, cookie_header: &str) -> Option<Identity> {
637 let token = auth::session_token_from_cookie(cookie_header)?;
638 let identity = auth::identity_from_session(db, token.as_str())
639 .await
640 .ok()
641 .flatten()?;
642 if !identity.is_active {
643 return None;
644 }
645 Some(identity)
646}
647
648fn strict_mailer_guard_check(admin: &Admin) -> std::result::Result<(), String> {
649 if admin.active_recovery_policy().strict_mailer_required() && !admin.has_custom_mailer() {
650 Err(
651 "rustio-admin: RecoveryPolicy::strict_mailer_required() = true but no mailer \
652 was registered via Admin::mailer(...).\n\n\
653 The framework's default LogMailer writes recovery emails to log::info! instead \
654 of sending them, which is unsuitable for production. Recovery routes are NOT \
655 registered with this configuration.\n\n\
656 To resolve, choose one:\n\
657 (a) register a real mailer before calling register_admin_routes:\n\
658 Admin::mailer(Arc::new(MyProjectMailer::new(...)))\n\
659 (b) opt the policy out of strict mode (the framework default — dev / CI / \
660 testing baseline):\n\
661 RecoveryPolicy::strict_mailer_required(false)\n\n\
662 See DESIGN_RECOVERY.md §12.1 for the contract."
663 .to_string(),
664 )
665 } else {
666 Ok(())
667 }
668}
669
670pub fn register_admin_routes(
672 router: Router,
673 admin: Admin,
674 db: Db,
675 templates: Arc<Templates>,
676) -> Router {
677 if let Err(msg) = strict_mailer_guard_check(&admin) {
685 panic!("{msg}");
686 }
687
688 let ctx = Arc::new(AdminCtx::new(
689 Arc::new(admin),
690 db.clone(),
691 templates.clone(),
692 ));
693
694 let auth_ctx = Arc::new(super::builtin::AuthAdminCtx {
697 admin: ctx.admin.clone(),
698 db,
699 templates,
700 });
701
702 let err_admin = ctx.admin.clone();
717 let err_templates = ctx.templates.clone();
718 let err_db = ctx.db.clone();
719 let router = router.middleware(move |req, next| {
720 let admin = err_admin.clone();
721 let templates = err_templates.clone();
722 let db = err_db.clone();
723 Box::pin(async move {
724 let is_admin_path = req.path().starts_with("/admin");
725 let cookie_header = if is_admin_path {
731 req.header("cookie").map(|s| s.to_string())
732 } else {
733 None
734 };
735 let result = next.run(req).await;
736 match result {
737 Ok(resp) => Ok(resp),
738 Err(err) if is_admin_path => {
739 let identity = match cookie_header.as_deref() {
740 Some(cookie) => resolve_identity_for_error_page(&db, cookie).await,
741 None => None,
742 };
743 Ok(render::render_admin_error_response(
744 &admin,
745 &templates,
746 identity.as_ref(),
747 err.status(),
748 err.client_message().to_string(),
749 ))
750 }
751 Err(err) => Err(err),
752 }
753 })
754 });
755
756 let ro_flag = ctx.admin.is_read_only();
768 let ro_models = std::sync::Arc::new(ctx.admin.read_only_models.clone());
769 let router = router.middleware(move |req, next| {
770 let ro_models = ro_models.clone();
771 Box::pin(async move {
772 if req.path().starts_with("/admin")
773 && is_mutating_method(req.method())
774 && !is_read_only_writable_path(req.path())
775 {
776 if ro_flag {
779 return Err(Error::Forbidden(
780 "This admin is currently in read-only mode. \
781 Project-data mutations are disabled until the operator \
782 turns read-only off."
783 .into(),
784 ));
785 }
786 if !ro_models.is_empty() {
791 if let Some(slug) = extract_admin_name(req.path()) {
792 if ro_models.contains(slug) {
793 return Err(Error::Forbidden(format!(
794 "Model `{slug}` is frozen (read-only). \
795 Mutations on this model are disabled."
796 )));
797 }
798 }
799 }
800 }
801 next.run(req).await
802 })
803 });
804
805 let admin_css = admin_css_payload(load_token_override().as_deref());
817 let router = router.get("/static/admin.css", move |_req| {
818 let body = admin_css.clone();
819 async move {
820 Ok(Response::new(hyper::StatusCode::OK, body)
821 .with_header("content-type", "text/css; charset=utf-8")
822 .with_header("cache-control", "no-cache, must-revalidate"))
823 }
824 });
825 let router = router.get("/static/admin.js", |_req| async move {
826 Ok(Response::new(
827 hyper::StatusCode::OK,
828 bytes::Bytes::from_static(ADMIN_JS.as_bytes()),
829 )
830 .with_header("content-type", "application/javascript; charset=utf-8")
831 .with_header("cache-control", "no-cache, must-revalidate"))
832 });
833
834 let router = register_font_routes(router);
837
838 let c = ctx.clone();
843 let router = router.get("/admin/healthz", move |_req| {
844 let c = c.clone();
845 async move { super::healthz::healthz(&c.db).await }
846 });
847
848 let c = ctx.clone();
850 let router = router.get("/admin/login", move |req| {
851 let c = c.clone();
852 async move { handlers::show_login(&c, req).await }
853 });
854
855 let c = ctx.clone();
856 let router = router.post("/admin/login", move |req| {
857 let c = c.clone();
858 async move { handlers::do_login(&c, req).await }
859 });
860
861 let c = ctx.clone();
862 let router = router.post("/admin/logout", move |req| {
863 let c = c.clone();
864 async move { handlers::do_logout(&c, req).await }
865 });
866
867 let recovery_state = Arc::new(super::recovery_handlers::RecoveryState::from_admin(
884 &ctx.admin,
885 ));
886
887 let c = ctx.clone();
888 let router = router.get("/admin/forgot-password", move |req| {
889 let c = c.clone();
890 async move { super::recovery_handlers::show_forgot_password(&c, &req).await }
891 });
892
893 let c = ctx.clone();
894 let rs = recovery_state.clone();
895 let router = router.post("/admin/forgot-password", move |req| {
896 let c = c.clone();
897 let rs = rs.clone();
898 async move { super::recovery_handlers::do_forgot_password(&c, &rs, req).await }
899 });
900
901 let c = ctx.clone();
902 let router = router.get("/admin/forgot-password/sent", move |req| {
903 let c = c.clone();
904 async move { super::recovery_handlers::show_forgot_password_sent(&c, &req).await }
905 });
906
907 let c = ctx.clone();
908 let router = router.get("/admin/reset-password/:token", move |req| {
909 let c = c.clone();
910 async move {
911 let token = req
912 .param("token")
913 .ok_or_else(|| Error::BadRequest("missing token".into()))?
914 .to_string();
915 super::recovery_handlers::show_reset_password(&c, &req, &token).await
916 }
917 });
918
919 let c = ctx.clone();
920 let rs = recovery_state.clone();
921 let router = router.post("/admin/reset-password/:token", move |req| {
922 let c = c.clone();
923 let rs = rs.clone();
924 async move {
925 let token = req
926 .param("token")
927 .ok_or_else(|| Error::BadRequest("missing token".into()))?
928 .to_string();
929 super::recovery_handlers::do_reset_password(&c, &rs, req, &token).await
930 }
931 });
932
933 let c = ctx.clone();
935 let router = router.get("/admin", move |req| {
936 let c = c.clone();
937 async move {
938 match role_guard(&c, &req, Role::Staff).await? {
939 Guard::Redirect(r) => Ok(r),
940 Guard::Allow(ident) => handlers::dashboard(&c, ident, &req).await,
941 }
942 }
943 });
944
945 let c = ctx.clone();
951 let router = router.get("/admin/db", move |req| {
952 let c = c.clone();
953 async move {
954 match role_guard(&c, &req, Role::Developer).await? {
955 Guard::Redirect(r) => Ok(r),
956 Guard::Allow(ident) => super::db_browser::show_db_browser(&c, ident, &req).await,
957 }
958 }
959 });
960
961 let c = ctx.clone();
968 let router = router.get("/admin/dev/view-designer", move |req| {
969 let c = c.clone();
970 async move {
971 match role_guard(&c, &req, Role::Developer).await? {
972 Guard::Redirect(r) => Ok(r),
973 Guard::Allow(ident) => handlers::show_view_designer(&c, ident, &req).await,
974 }
975 }
976 });
977 let c = ctx.clone();
978 let router = router.get("/admin/dev/view-designer/:admin_name", move |req| {
979 let c = c.clone();
980 async move {
981 match role_guard(&c, &req, Role::Developer).await? {
982 Guard::Redirect(r) => Ok(r),
983 Guard::Allow(ident) => handlers::show_view_designer_model(&c, ident, &req).await,
984 }
985 }
986 });
987 let c = ctx.clone();
988 let router = router.post("/admin/dev/view-designer/:admin_name/save", move |req| {
989 let c = c.clone();
990 async move {
991 match role_guard(&c, &req, Role::Developer).await? {
992 Guard::Redirect(r) => Ok(r),
993 Guard::Allow(ident) => handlers::do_save_view_spec(&c, ident, req).await,
994 }
995 }
996 });
997
998 let c = ctx.clone();
1003 let router = router.get("/admin/dev/branding", move |req| {
1004 let c = c.clone();
1005 async move {
1006 match role_guard(&c, &req, Role::Developer).await? {
1007 Guard::Redirect(r) => Ok(r),
1008 Guard::Allow(ident) => handlers::show_branding(&c, ident, &req).await,
1009 }
1010 }
1011 });
1012
1013 let c = ctx.clone();
1017 let router = router.get("/admin/dev/schema", move |req| {
1018 let c = c.clone();
1019 async move {
1020 match role_guard(&c, &req, Role::Developer).await? {
1021 Guard::Redirect(r) => Ok(r),
1022 Guard::Allow(ident) => handlers::show_schema(&c, ident, &req).await,
1023 }
1024 }
1025 });
1026
1027 let c = ctx.clone();
1032 let router = router.get("/admin/notifications", move |req| {
1033 let c = c.clone();
1034 async move {
1035 match role_guard(&c, &req, Role::Staff).await? {
1036 Guard::Redirect(r) => Ok(r),
1037 Guard::Allow(ident) => handlers::show_notifications(&c, ident, &req).await,
1038 }
1039 }
1040 });
1041 let c = ctx.clone();
1042 let router = router.post("/admin/notifications/mark_all_read", move |req| {
1043 let c = c.clone();
1044 async move {
1045 match role_guard(&c, &req, Role::Staff).await? {
1046 Guard::Redirect(r) => Ok(r),
1047 Guard::Allow(ident) => {
1048 handlers::do_mark_all_notifications_read(&c, ident, req).await
1049 }
1050 }
1051 }
1052 });
1053
1054 let c = ctx.clone();
1059 let router = router.get("/admin/feature_flags", move |req| {
1060 let c = c.clone();
1061 async move {
1062 match role_guard(&c, &req, Role::Administrator).await? {
1063 Guard::Redirect(r) => Ok(r),
1064 Guard::Allow(ident) => handlers::show_feature_flags(&c, ident, &req).await,
1065 }
1066 }
1067 });
1068 let c = ctx.clone();
1069 let router = router.post("/admin/feature_flags", move |req| {
1070 let c = c.clone();
1071 async move {
1072 match role_guard(&c, &req, Role::Administrator).await? {
1073 Guard::Redirect(r) => Ok(r),
1074 Guard::Allow(ident) => handlers::do_create_feature_flag(&c, ident, req).await,
1075 }
1076 }
1077 });
1078 let c = ctx.clone();
1079 let router = router.post("/admin/feature_flags/:key/toggle", move |req| {
1080 let c = c.clone();
1081 async move {
1082 let key = req
1083 .param("key")
1084 .ok_or_else(|| Error::BadRequest("missing flag key".into()))?
1085 .to_string();
1086 match role_guard(&c, &req, Role::Administrator).await? {
1087 Guard::Redirect(r) => Ok(r),
1088 Guard::Allow(ident) => handlers::do_toggle_feature_flag(&c, ident, &key, req).await,
1089 }
1090 }
1091 });
1092
1093 let c = ctx.clone();
1099 let router = router.get("/admin/health", move |req| {
1100 let c = c.clone();
1101 async move {
1102 match role_guard(&c, &req, Role::Administrator).await? {
1103 Guard::Redirect(r) => Ok(r),
1104 Guard::Allow(ident) => handlers::show_health(&c, ident, &req).await,
1105 }
1106 }
1107 });
1108
1109 let c = ctx.clone();
1111 let router = router.get("/admin/history", move |req| {
1112 let c = c.clone();
1113 async move {
1114 match role_guard(&c, &req, Role::Administrator).await? {
1115 Guard::Redirect(r) => Ok(r),
1116 Guard::Allow(ident) => handlers::show_log_entries(&c, ident, &req).await,
1117 }
1118 }
1119 });
1120
1121 let c = ctx.clone();
1124 let router = router.get("/admin/account/sessions", move |req| {
1125 let c = c.clone();
1126 async move {
1127 match role_guard(&c, &req, Role::User).await? {
1128 Guard::Redirect(r) => Ok(r),
1129 Guard::Allow(ident) => handlers::show_account_sessions(&c, ident, &req).await,
1130 }
1131 }
1132 });
1133
1134 let c = ctx.clone();
1142 let router = router.post("/admin/account/sessions/revoke-others", move |req| {
1143 let c = c.clone();
1144 async move {
1145 match role_guard(&c, &req, Role::User).await? {
1146 Guard::Redirect(r) => Ok(r),
1147 Guard::Allow(ident) => handlers::do_revoke_other_sessions(&c, ident, req).await,
1148 }
1149 }
1150 });
1151
1152 let c = ctx.clone();
1153 let router = router.post("/admin/account/sessions/revoke-all", 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) => handlers::do_revoke_all_sessions(&c, ident, req).await,
1159 }
1160 }
1161 });
1162
1163 let c = ctx.clone();
1164 let router = router.post("/admin/account/sessions/:id/revoke", 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) => {
1170 let id = parse_id(req.param("id"))?;
1171 handlers::do_revoke_session(&c, ident, req, id).await
1172 }
1173 }
1174 }
1175 });
1176
1177 let c = ctx.clone();
1181 let router = router.get("/admin/password_change", move |req| {
1182 let c = c.clone();
1183 async move {
1184 match role_guard(&c, &req, Role::User).await? {
1185 Guard::Redirect(r) => Ok(r),
1186 Guard::Allow(ident) => handlers::show_password_change(&c, ident, &req).await,
1187 }
1188 }
1189 });
1190 let c = ctx.clone();
1191 let router = router.post("/admin/password_change", move |req| {
1192 let c = c.clone();
1193 async move {
1194 match role_guard(&c, &req, Role::User).await? {
1195 Guard::Redirect(r) => Ok(r),
1196 Guard::Allow(ident) => handlers::do_password_change(&c, ident, req).await,
1197 }
1198 }
1199 });
1200
1201 let c = ctx.clone();
1210 let router = router.get("/admin/reauth", 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) => {
1216 super::admin_recovery_handlers::show_reauth(&c, ident, &req).await
1217 }
1218 }
1219 }
1220 });
1221
1222 let c = ctx.clone();
1223 let router = router.post("/admin/reauth", move |req| {
1224 let c = c.clone();
1225 async move {
1226 match role_guard(&c, &req, Role::User).await? {
1227 Guard::Redirect(r) => Ok(r),
1228 Guard::Allow(ident) => {
1229 super::admin_recovery_handlers::do_reauth(&c, ident, req).await
1230 }
1231 }
1232 }
1233 });
1234
1235 let c = ctx.clone();
1245 let router = router.get("/admin/must-change-password", move |req| {
1246 let c = c.clone();
1247 async move {
1248 match role_guard(&c, &req, Role::User).await? {
1249 Guard::Redirect(r) => Ok(r),
1250 Guard::Allow(ident) => {
1251 super::admin_recovery_handlers::show_must_change_password(&c, ident, &req).await
1252 }
1253 }
1254 }
1255 });
1256
1257 let c = ctx.clone();
1258 let router = router.post("/admin/must-change-password", move |req| {
1259 let c = c.clone();
1260 async move {
1261 match role_guard(&c, &req, Role::User).await? {
1262 Guard::Redirect(r) => Ok(r),
1263 Guard::Allow(ident) => {
1264 super::admin_recovery_handlers::do_must_change_password(&c, ident, req).await
1265 }
1266 }
1267 }
1268 });
1269
1270 let c = ctx.clone();
1288 let router = router.get("/admin/mfa/verify", move |req| {
1289 let c = c.clone();
1290 async move {
1291 match role_guard(&c, &req, Role::User).await? {
1292 Guard::Redirect(r) => Ok(r),
1293 Guard::Allow(ident) => super::mfa_handlers::show_verify(&c, ident, &req).await,
1294 }
1295 }
1296 });
1297
1298 let c = ctx.clone();
1299 let router = router.post("/admin/mfa/verify", move |req| {
1300 let c = c.clone();
1301 async move {
1302 match role_guard(&c, &req, Role::User).await? {
1303 Guard::Redirect(r) => Ok(r),
1304 Guard::Allow(ident) => super::mfa_handlers::do_verify(&c, ident, req).await,
1305 }
1306 }
1307 });
1308
1309 let c = ctx.clone();
1311 let router = router.get("/admin/account/mfa/enroll", move |req| {
1312 let c = c.clone();
1313 async move {
1314 match role_guard(&c, &req, Role::User).await? {
1315 Guard::Redirect(r) => Ok(r),
1316 Guard::Allow(ident) => super::mfa_handlers::show_enroll(&c, ident, &req).await,
1317 }
1318 }
1319 });
1320
1321 let c = ctx.clone();
1322 let router = router.post("/admin/account/mfa/enroll", move |req| {
1323 let c = c.clone();
1324 async move {
1325 match role_guard(&c, &req, Role::User).await? {
1326 Guard::Redirect(r) => Ok(r),
1327 Guard::Allow(ident) => super::mfa_handlers::do_enroll(&c, ident, req).await,
1328 }
1329 }
1330 });
1331
1332 let c = ctx.clone();
1334 let router = router.get("/admin/account/mfa/regenerate-codes", move |req| {
1335 let c = c.clone();
1336 async move {
1337 match role_guard(&c, &req, Role::User).await? {
1338 Guard::Redirect(r) => Ok(r),
1339 Guard::Allow(ident) => super::mfa_handlers::show_regenerate(&c, ident, &req).await,
1340 }
1341 }
1342 });
1343
1344 let c = ctx.clone();
1345 let router = router.post("/admin/account/mfa/regenerate-codes", move |req| {
1346 let c = c.clone();
1347 async move {
1348 match role_guard(&c, &req, Role::User).await? {
1349 Guard::Redirect(r) => Ok(r),
1350 Guard::Allow(ident) => super::mfa_handlers::do_regenerate(&c, ident, req).await,
1351 }
1352 }
1353 });
1354
1355 let c = ctx.clone();
1357 let router = router.get("/admin/account/mfa/disable", move |req| {
1358 let c = c.clone();
1359 async move {
1360 match role_guard(&c, &req, Role::User).await? {
1361 Guard::Redirect(r) => Ok(r),
1362 Guard::Allow(ident) => super::mfa_handlers::show_disable(&c, ident, &req).await,
1363 }
1364 }
1365 });
1366
1367 let c = ctx.clone();
1368 let router = router.post("/admin/account/mfa/disable", move |req| {
1369 let c = c.clone();
1370 async move {
1371 match role_guard(&c, &req, Role::User).await? {
1372 Guard::Redirect(r) => Ok(r),
1373 Guard::Allow(ident) => super::mfa_handlers::do_disable(&c, ident, req).await,
1374 }
1375 }
1376 });
1377
1378 let c = ctx.clone();
1380 let ac = auth_ctx.clone();
1381 let router = router.get("/admin/users", move |req| {
1382 let c = c.clone();
1383 let ac = ac.clone();
1384 async move {
1385 match role_guard(&c, &req, Role::Administrator).await? {
1386 Guard::Redirect(r) => Ok(r),
1387 Guard::Allow(ident) => {
1388 super::builtin::list_users(&ac, ident, handlers::csrf_token(&req)).await
1389 }
1390 }
1391 }
1392 });
1393
1394 let c = ctx.clone();
1395 let ac = auth_ctx.clone();
1396 let router = router.get("/admin/users/new", move |req| {
1397 let c = c.clone();
1398 let ac = ac.clone();
1399 async move {
1400 match role_guard(&c, &req, Role::Administrator).await? {
1401 Guard::Redirect(r) => Ok(r),
1402 Guard::Allow(ident) => {
1403 super::builtin::show_new_user(&ac, ident, handlers::csrf_token(&req)).await
1404 }
1405 }
1406 }
1407 });
1408
1409 let c = ctx.clone();
1410 let ac = auth_ctx.clone();
1411 let router = router.post("/admin/users/new", move |req| {
1412 let c = c.clone();
1413 let ac = ac.clone();
1414 async move {
1415 match role_guard(&c, &req, Role::Administrator).await? {
1416 Guard::Redirect(r) => Ok(r),
1417 Guard::Allow(ident) => super::builtin::do_new_user(&ac, ident, req).await,
1418 }
1419 }
1420 });
1421
1422 let c = ctx.clone();
1423 let ac = auth_ctx.clone();
1424 let router = router.get("/admin/users/:id/edit", move |req| {
1425 let c = c.clone();
1426 let ac = ac.clone();
1427 async move {
1428 match role_guard(&c, &req, Role::Administrator).await? {
1429 Guard::Redirect(r) => Ok(r),
1430 Guard::Allow(ident) => {
1431 let id = parse_id(req.param("id"))?;
1432 super::builtin::show_user_edit(&ac, ident, id, handlers::csrf_token(&req)).await
1433 }
1434 }
1435 }
1436 });
1437
1438 let c = ctx.clone();
1439 let ac = auth_ctx.clone();
1440 let router = router.post("/admin/users/:id/edit", move |req| {
1441 let c = c.clone();
1442 let ac = ac.clone();
1443 async move {
1444 match role_guard(&c, &req, Role::Administrator).await? {
1445 Guard::Redirect(r) => Ok(r),
1446 Guard::Allow(ident) => {
1447 let id = parse_id(req.param("id"))?;
1448 super::builtin::do_user_edit(&ac, ident, id, req).await
1449 }
1450 }
1451 }
1452 });
1453
1454 let c = ctx.clone();
1455 let ac = auth_ctx.clone();
1456 let router = router.get("/admin/users/:id/delete", move |req| {
1457 let c = c.clone();
1458 let ac = ac.clone();
1459 async move {
1460 match role_guard(&c, &req, Role::Administrator).await? {
1461 Guard::Redirect(r) => Ok(r),
1462 Guard::Allow(ident) => {
1463 let id = parse_id(req.param("id"))?;
1464 super::builtin::show_user_delete(&ac, ident, id, handlers::csrf_token(&req))
1465 .await
1466 }
1467 }
1468 }
1469 });
1470
1471 let c = ctx.clone();
1472 let ac = auth_ctx.clone();
1473 let router = router.post("/admin/users/:id/delete", move |req| {
1474 let c = c.clone();
1475 let ac = ac.clone();
1476 async move {
1477 match role_guard(&c, &req, Role::Administrator).await? {
1478 Guard::Redirect(r) => Ok(r),
1479 Guard::Allow(ident) => {
1480 let id = parse_id(req.param("id"))?;
1481 super::builtin::do_user_delete(&ac, ident, id, req).await
1482 }
1483 }
1484 }
1485 });
1486
1487 let c = ctx.clone();
1503 let router = router.get("/admin/users/:id/reset-password", move |req| {
1504 let c = c.clone();
1505 async move {
1506 match role_guard(&c, &req, Role::Administrator).await? {
1507 Guard::Redirect(r) => Ok(r),
1508 Guard::Allow(ident) => {
1509 let id = parse_id(req.param("id"))?;
1510 super::admin_recovery_handlers::show_admin_reset_password(&c, ident, id, &req)
1511 .await
1512 }
1513 }
1514 }
1515 });
1516
1517 let c = ctx.clone();
1519 let router = router.post("/admin/users/:id/reset-password", move |req| {
1520 let c = c.clone();
1521 async move {
1522 match role_guard(&c, &req, Role::Administrator).await? {
1523 Guard::Redirect(r) => Ok(r),
1524 Guard::Allow(ident) => {
1525 let id = parse_id(req.param("id"))?;
1526 super::admin_recovery_handlers::do_admin_reset_password(&c, ident, id, req)
1527 .await
1528 }
1529 }
1530 }
1531 });
1532
1533 let c = ctx.clone();
1535 let router = router.get("/admin/users/:id/lock", move |req| {
1536 let c = c.clone();
1537 async move {
1538 match role_guard(&c, &req, Role::Administrator).await? {
1539 Guard::Redirect(r) => Ok(r),
1540 Guard::Allow(ident) => {
1541 let id = parse_id(req.param("id"))?;
1542 super::admin_recovery_handlers::show_lock_user(&c, ident, id, &req).await
1543 }
1544 }
1545 }
1546 });
1547
1548 let c = ctx.clone();
1550 let router = router.post("/admin/users/:id/lock", move |req| {
1551 let c = c.clone();
1552 async move {
1553 match role_guard(&c, &req, Role::Administrator).await? {
1554 Guard::Redirect(r) => Ok(r),
1555 Guard::Allow(ident) => {
1556 let id = parse_id(req.param("id"))?;
1557 super::admin_recovery_handlers::do_lock_user(&c, ident, id, req).await
1558 }
1559 }
1560 }
1561 });
1562
1563 let c = ctx.clone();
1565 let router = router.get("/admin/users/:id/unlock", move |req| {
1566 let c = c.clone();
1567 async move {
1568 match role_guard(&c, &req, Role::Administrator).await? {
1569 Guard::Redirect(r) => Ok(r),
1570 Guard::Allow(ident) => {
1571 let id = parse_id(req.param("id"))?;
1572 super::admin_recovery_handlers::show_unlock_user(&c, ident, id, &req).await
1573 }
1574 }
1575 }
1576 });
1577
1578 let c = ctx.clone();
1580 let router = router.post("/admin/users/:id/unlock", move |req| {
1581 let c = c.clone();
1582 async move {
1583 match role_guard(&c, &req, Role::Administrator).await? {
1584 Guard::Redirect(r) => Ok(r),
1585 Guard::Allow(ident) => {
1586 let id = parse_id(req.param("id"))?;
1587 super::admin_recovery_handlers::do_unlock_user(&c, ident, id, req).await
1588 }
1589 }
1590 }
1591 });
1592
1593 let c = ctx.clone();
1596 let router = router.get("/admin/users/:id/revoke-sessions", move |req| {
1597 let c = c.clone();
1598 async move {
1599 match role_guard(&c, &req, Role::Administrator).await? {
1600 Guard::Redirect(r) => Ok(r),
1601 Guard::Allow(ident) => {
1602 let id = parse_id(req.param("id"))?;
1603 super::admin_recovery_handlers::show_admin_revoke_sessions(&c, ident, id, &req)
1604 .await
1605 }
1606 }
1607 }
1608 });
1609
1610 let c = ctx.clone();
1612 let router = router.post("/admin/users/:id/revoke-sessions", move |req| {
1613 let c = c.clone();
1614 async move {
1615 match role_guard(&c, &req, Role::Administrator).await? {
1616 Guard::Redirect(r) => Ok(r),
1617 Guard::Allow(ident) => {
1618 let id = parse_id(req.param("id"))?;
1619 super::admin_recovery_handlers::do_admin_revoke_sessions(&c, ident, id, req)
1620 .await
1621 }
1622 }
1623 }
1624 });
1625
1626 let c = ctx.clone();
1633 let router = router.post("/admin/users/:id/sessions/:session_id/revoke", move |req| {
1634 let c = c.clone();
1635 async move {
1636 match role_guard(&c, &req, Role::Administrator).await? {
1637 Guard::Redirect(r) => Ok(r),
1638 Guard::Allow(ident) => {
1639 let user_id = parse_id(req.param("id"))?;
1640 let session_id = parse_id(req.param("session_id"))?;
1641 super::admin_recovery_handlers::do_admin_revoke_one_session(
1642 &c, ident, user_id, session_id, req,
1643 )
1644 .await
1645 }
1646 }
1647 }
1648 });
1649
1650 let c = ctx.clone();
1657 let ac = auth_ctx.clone();
1658 let router = router.get("/admin/users/:id", move |req| {
1659 let c = c.clone();
1660 let ac = ac.clone();
1661 async move {
1662 match role_guard(&c, &req, Role::Administrator).await? {
1663 Guard::Redirect(r) => Ok(r),
1664 Guard::Allow(ident) => {
1665 let id = parse_id(req.param("id"))?;
1666 let q = req.query();
1667 let tab = q.get("tab").map(|s| s.to_string());
1668 let page: i64 = q.get("page").and_then(|s| s.parse().ok()).unwrap_or(1);
1669 let viewing_session_id = match req
1670 .header("cookie")
1671 .and_then(crate::auth::session_token_from_cookie)
1672 {
1673 Some(token) => crate::auth::current_session_id(&ac.db, &token)
1674 .await
1675 .ok()
1676 .flatten(),
1677 None => None,
1678 };
1679 super::builtin::show_user_view(
1680 &ac,
1681 ident,
1682 id,
1683 handlers::csrf_token(&req),
1684 tab,
1685 page,
1686 viewing_session_id,
1687 )
1688 .await
1689 }
1690 }
1691 }
1692 });
1693
1694 let c = ctx.clone();
1696 let ac = auth_ctx.clone();
1697 let router = router.get("/admin/groups", move |req| {
1698 let c = c.clone();
1699 let ac = ac.clone();
1700 async move {
1701 match role_guard(&c, &req, Role::Administrator).await? {
1702 Guard::Redirect(r) => Ok(r),
1703 Guard::Allow(ident) => {
1704 super::builtin::list_groups(&ac, ident, handlers::csrf_token(&req)).await
1705 }
1706 }
1707 }
1708 });
1709
1710 let c = ctx.clone();
1711 let ac = auth_ctx.clone();
1712 let router = router.get("/admin/groups/new", move |req| {
1713 let c = c.clone();
1714 let ac = ac.clone();
1715 async move {
1716 match role_guard(&c, &req, Role::Administrator).await? {
1717 Guard::Redirect(r) => Ok(r),
1718 Guard::Allow(ident) => {
1719 super::builtin::show_new_group(&ac, ident, handlers::csrf_token(&req)).await
1720 }
1721 }
1722 }
1723 });
1724
1725 let c = ctx.clone();
1726 let ac = auth_ctx.clone();
1727 let router = router.post("/admin/groups/new", move |req| {
1728 let c = c.clone();
1729 let ac = ac.clone();
1730 async move {
1731 match role_guard(&c, &req, Role::Administrator).await? {
1732 Guard::Redirect(r) => Ok(r),
1733 Guard::Allow(ident) => super::builtin::do_new_group(&ac, ident, req).await,
1734 }
1735 }
1736 });
1737
1738 let c = ctx.clone();
1739 let ac = auth_ctx.clone();
1740 let router = router.get("/admin/groups/:id/edit", move |req| {
1741 let c = c.clone();
1742 let ac = ac.clone();
1743 async move {
1744 match role_guard(&c, &req, Role::Administrator).await? {
1745 Guard::Redirect(r) => Ok(r),
1746 Guard::Allow(ident) => {
1747 let id = parse_id(req.param("id"))?;
1748 super::builtin::show_group_edit(&ac, ident, id, handlers::csrf_token(&req))
1749 .await
1750 }
1751 }
1752 }
1753 });
1754
1755 let c = ctx.clone();
1756 let ac = auth_ctx.clone();
1757 let router = router.post("/admin/groups/:id/edit", move |req| {
1758 let c = c.clone();
1759 let ac = ac.clone();
1760 async move {
1761 match role_guard(&c, &req, Role::Administrator).await? {
1762 Guard::Redirect(r) => Ok(r),
1763 Guard::Allow(ident) => {
1764 let id = parse_id(req.param("id"))?;
1765 super::builtin::do_group_edit(&ac, ident, id, req).await
1766 }
1767 }
1768 }
1769 });
1770
1771 let c = ctx.clone();
1772 let ac = auth_ctx.clone();
1773 let router = router.get("/admin/groups/:id/delete", move |req| {
1774 let c = c.clone();
1775 let ac = ac.clone();
1776 async move {
1777 match role_guard(&c, &req, Role::Administrator).await? {
1778 Guard::Redirect(r) => Ok(r),
1779 Guard::Allow(ident) => {
1780 let id = parse_id(req.param("id"))?;
1781 super::builtin::show_group_delete(&ac, ident, id, handlers::csrf_token(&req))
1782 .await
1783 }
1784 }
1785 }
1786 });
1787
1788 let c = ctx.clone();
1789 let ac = auth_ctx.clone();
1790 let router = router.post("/admin/groups/:id/delete", move |req| {
1791 let c = c.clone();
1792 let ac = ac.clone();
1793 async move {
1794 match role_guard(&c, &req, Role::Administrator).await? {
1795 Guard::Redirect(r) => Ok(r),
1796 Guard::Allow(ident) => {
1797 let id = parse_id(req.param("id"))?;
1798 super::builtin::do_group_delete(&ac, ident, id, req).await
1799 }
1800 }
1801 }
1802 });
1803
1804 let c = ctx.clone();
1813 let router = router.get("/admin/uploads/:filename", move |req| {
1814 let c = c.clone();
1815 async move {
1816 match role_guard(&c, &req, Role::Staff).await? {
1817 Guard::Redirect(r) => Ok(r),
1818 Guard::Allow(ident) => {
1819 let filename = req
1820 .param("filename")
1821 .map(str::to_string)
1822 .unwrap_or_default();
1823 handlers::serve_upload(&c, ident, &filename, req).await
1824 }
1825 }
1826 }
1827 });
1828
1829 let c = ctx.clone();
1835 let router = router.get("/admin/_lookup/:admin_name", move |req| {
1836 let c = c.clone();
1837 async move {
1838 let name = model_name_from_req(&req)?;
1839 let perm = perm_for(&c, &name, "view")?;
1840 match perm_guard(&c, &req, &perm).await? {
1841 Guard::Redirect(r) => Ok(r),
1842 Guard::Allow(ident) => handlers::lookup_model(&c, ident, &name, req).await,
1843 }
1844 }
1845 });
1846
1847 let c = ctx.clone();
1855 let router = router.get("/admin/_search", move |req| {
1856 let c = c.clone();
1857 async move {
1858 match role_guard(&c, &req, Role::Staff).await? {
1859 Guard::Redirect(r) => Ok(r),
1860 Guard::Allow(ident) => handlers::search_models(&c, ident, req).await,
1861 }
1862 }
1863 });
1864
1865 let c = ctx.clone();
1873 let router = router.get("/admin/docs", move |req| {
1874 let c = c.clone();
1875 async move {
1876 match role_guard(&c, &req, Role::Staff).await? {
1877 Guard::Redirect(r) => Ok(r),
1878 Guard::Allow(ident) => handlers::show_docs_index(&c, ident, &req).await,
1879 }
1880 }
1881 });
1882 let c = ctx.clone();
1883 let router = router.get("/admin/docs/:slug", move |req| {
1884 let c = c.clone();
1885 async move {
1886 let slug = req
1887 .param("slug")
1888 .ok_or_else(|| Error::BadRequest("missing doc slug".into()))?
1889 .to_string();
1890 match role_guard(&c, &req, Role::Staff).await? {
1891 Guard::Redirect(r) => Ok(r),
1892 Guard::Allow(ident) => handlers::show_doc_page(&c, ident, &slug, &req).await,
1893 }
1894 }
1895 });
1896
1897 let c = ctx.clone();
1904 let router = router.get("/admin/apis/openapi.json", move |req| {
1905 let c = c.clone();
1906 async move {
1907 match role_guard(&c, &req, Role::Staff).await? {
1908 Guard::Redirect(r) => Ok(r),
1909 Guard::Allow(_) => {
1910 let spec = super::openapi::build_spec(&c.admin);
1911 super::json_api::json_response(spec)
1912 }
1913 }
1914 }
1915 });
1916
1917 let c = ctx.clone();
1923 let router = router.get("/admin/apis/sdk.ts", move |req| {
1924 let c = c.clone();
1925 async move {
1926 match role_guard(&c, &req, Role::Staff).await? {
1927 Guard::Redirect(r) => Ok(r),
1928 Guard::Allow(_) => {
1929 let body = super::sdk_gen::build_typescript(&c.admin);
1930 Ok(crate::http::Response::ok(body)
1931 .with_header("content-type", "text/typescript; charset=utf-8")
1932 .with_header(
1933 "content-disposition",
1934 "attachment; filename=\"rustio-sdk.ts\"",
1935 ))
1936 }
1937 }
1938 }
1939 });
1940
1941 let c = ctx.clone();
1947 let router = router.get("/admin/apis", move |req| {
1948 let c = c.clone();
1949 async move {
1950 match role_guard(&c, &req, Role::Staff).await? {
1951 Guard::Redirect(r) => Ok(r),
1952 Guard::Allow(ident) => handlers::show_apis_index(&c, ident, &req).await,
1953 }
1954 }
1955 });
1956
1957 let c = ctx.clone();
1965 let router = router.get("/admin/apis/playground", move |req| {
1966 let c = c.clone();
1967 async move {
1968 match role_guard(&c, &req, Role::Staff).await? {
1969 Guard::Redirect(r) => Ok(r),
1970 Guard::Allow(ident) => handlers::show_apis_playground(&c, ident, &req).await,
1971 }
1972 }
1973 });
1974
1975 let c = ctx.clone();
1977 let router = router.get("/admin/:admin_name", move |req| {
1978 let c = c.clone();
1979 async move {
1980 let name = model_name_from_req(&req)?;
1981 let perm = perm_for(&c, &name, "view")?;
1982 match perm_guard(&c, &req, &perm).await? {
1983 Guard::Redirect(r) => Ok(r),
1984 Guard::Allow(ident) => handlers::list_model(&c, ident, &name, &req).await,
1985 }
1986 }
1987 });
1988
1989 let c = ctx.clone();
1994 let router = router.get("/admin/:admin_name/export.csv", move |req| {
1995 let c = c.clone();
1996 async move {
1997 let name = model_name_from_req(&req)?;
1998 let perm = perm_for(&c, &name, "view")?;
1999 match perm_guard(&c, &req, &perm).await? {
2000 Guard::Redirect(r) => Ok(r),
2001 Guard::Allow(ident) => handlers::export_model_csv(&c, ident, &name, req).await,
2002 }
2003 }
2004 });
2005
2006 let c = ctx.clone();
2012 let router = router.post("/admin/:admin_name/import.csv", move |req| {
2013 let c = c.clone();
2014 async move {
2015 let name = model_name_from_req(&req)?;
2016 let perm = perm_for(&c, &name, "change")?;
2017 match perm_guard(&c, &req, &perm).await? {
2018 Guard::Redirect(r) => Ok(r),
2019 Guard::Allow(ident) => handlers::import_model_csv(&c, ident, &name, req).await,
2020 }
2021 }
2022 });
2023
2024 let c = ctx.clone();
2030 let router = router.post("/admin/:admin_name/saved_filters", move |req| {
2031 let c = c.clone();
2032 async move {
2033 let name = model_name_from_req(&req)?;
2034 let perm = perm_for(&c, &name, "view")?;
2035 match perm_guard(&c, &req, &perm).await? {
2036 Guard::Redirect(r) => Ok(r),
2037 Guard::Allow(ident) => handlers::do_save_filter(&c, ident, &name, req).await,
2038 }
2039 }
2040 });
2041
2042 let c = ctx.clone();
2046 let router = router.post("/admin/:admin_name/saved_filters/:id/delete", move |req| {
2047 let c = c.clone();
2048 async move {
2049 let name = model_name_from_req(&req)?;
2050 let id = parse_id(req.param("id"))?;
2051 let perm = perm_for(&c, &name, "view")?;
2052 match perm_guard(&c, &req, &perm).await? {
2053 Guard::Redirect(r) => Ok(r),
2054 Guard::Allow(ident) => handlers::do_delete_filter(&c, ident, &name, id, req).await,
2055 }
2056 }
2057 });
2058
2059 let c = ctx.clone();
2061 let router = router.get("/admin/:admin_name/new", move |req| {
2062 let c = c.clone();
2063 async move {
2064 let name = model_name_from_req(&req)?;
2065 let perm = perm_for(&c, &name, "add")?;
2066 match perm_guard(&c, &req, &perm).await? {
2067 Guard::Redirect(r) => Ok(r),
2068 Guard::Allow(ident) => handlers::show_new_form(&c, ident, &name, &req).await,
2069 }
2070 }
2071 });
2072 let c = ctx.clone();
2073 let router = router.post("/admin/:admin_name/new", move |req| {
2074 let c = c.clone();
2075 async move {
2076 let name = model_name_from_req(&req)?;
2077 let perm = perm_for(&c, &name, "add")?;
2078 match perm_guard(&c, &req, &perm).await? {
2079 Guard::Redirect(r) => Ok(r),
2080 Guard::Allow(ident) => handlers::do_create(&c, ident, &name, req).await,
2081 }
2082 }
2083 });
2084
2085 let c = ctx.clone();
2092 let router = router.get("/admin/:admin_name/:id", move |req| {
2093 let c = c.clone();
2094 async move {
2095 let name = model_name_from_req(&req)?;
2096 let perm = perm_for(&c, &name, "view")?;
2097 match perm_guard(&c, &req, &perm).await? {
2098 Guard::Redirect(r) => Ok(r),
2099 Guard::Allow(ident) => {
2100 let id = parse_id(req.param("id"))?;
2101 handlers::show_object_json(&c, ident, &name, id, &req).await
2102 }
2103 }
2104 }
2105 });
2106
2107 let c = ctx.clone();
2109 let router = router.get("/admin/:admin_name/:id/edit", 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, "change")?;
2114 match perm_guard(&c, &req, &perm).await? {
2115 Guard::Redirect(r) => Ok(r),
2116 Guard::Allow(ident) => {
2117 let id = parse_id(req.param("id"))?;
2118 handlers::show_edit_form(&c, ident, &name, id, &req).await
2119 }
2120 }
2121 }
2122 });
2123 let c = ctx.clone();
2124 let router = router.post("/admin/:admin_name/:id/edit", move |req| {
2125 let c = c.clone();
2126 async move {
2127 let name = model_name_from_req(&req)?;
2128 let perm = perm_for(&c, &name, "change")?;
2129 match perm_guard(&c, &req, &perm).await? {
2130 Guard::Redirect(r) => Ok(r),
2131 Guard::Allow(ident) => {
2132 let id = parse_id(req.param("id"))?;
2133 handlers::do_update(&c, ident, &name, id, req).await
2134 }
2135 }
2136 }
2137 });
2138
2139 let c = ctx.clone();
2142 let router = router.get("/admin/:admin_name/:id/history", move |req| {
2143 let c = c.clone();
2144 async move {
2145 let name = model_name_from_req(&req)?;
2146 let perm = perm_for(&c, &name, "view")?;
2147 match perm_guard(&c, &req, &perm).await? {
2148 Guard::Redirect(r) => Ok(r),
2149 Guard::Allow(ident) => {
2150 let id = parse_id(req.param("id"))?;
2151 handlers::show_object_history(&c, ident, &name, id, &req).await
2152 }
2153 }
2154 }
2155 });
2156
2157 let c = ctx.clone();
2159 let router = router.get("/admin/:admin_name/:id/delete", move |req| {
2160 let c = c.clone();
2161 async move {
2162 let name = model_name_from_req(&req)?;
2163 let perm = perm_for(&c, &name, "delete")?;
2164 match perm_guard(&c, &req, &perm).await? {
2165 Guard::Redirect(r) => Ok(r),
2166 Guard::Allow(ident) => {
2167 let id = parse_id(req.param("id"))?;
2168 handlers::show_delete_confirm(&c, ident, &name, id, &req).await
2169 }
2170 }
2171 }
2172 });
2173 let c = ctx.clone();
2174 let router = router.post("/admin/:admin_name/:id/delete", move |req| {
2175 let c = c.clone();
2176 async move {
2177 let name = model_name_from_req(&req)?;
2178 let perm = perm_for(&c, &name, "delete")?;
2179 match perm_guard(&c, &req, &perm).await? {
2180 Guard::Redirect(r) => Ok(r),
2181 Guard::Allow(ident) => {
2182 let id = parse_id(req.param("id"))?;
2183 handlers::do_delete(&c, ident, &name, req, id).await
2184 }
2185 }
2186 }
2187 });
2188
2189 let c = ctx.clone();
2194 let router = router.post("/admin/:admin_name/bulk_delete", move |req| {
2195 let c = c.clone();
2196 async move {
2197 let name = model_name_from_req(&req)?;
2198 let perm = perm_for(&c, &name, "delete")?;
2199 match perm_guard(&c, &req, &perm).await? {
2200 Guard::Redirect(r) => Ok(r),
2201 Guard::Allow(ident) => handlers::handle_bulk_delete(&c, ident, &name, &req).await,
2202 }
2203 }
2204 });
2205
2206 let c = ctx.clone();
2211 router.post("/admin/:admin_name/bulk/:action", move |req| {
2212 let c = c.clone();
2213 async move {
2214 let name = model_name_from_req(&req)?;
2215 let action = req
2216 .param("action")
2217 .ok_or_else(|| Error::BadRequest("missing bulk action name".into()))?
2218 .to_string();
2219 let perm = perm_for(&c, &name, "change")?;
2220 match perm_guard(&c, &req, &perm).await? {
2221 Guard::Redirect(r) => Ok(r),
2222 Guard::Allow(ident) => {
2223 handlers::handle_bulk_action(&c, ident, &name, &action, &req).await
2224 }
2225 }
2226 }
2227 })
2228}
2229
2230#[cfg(test)]
2231mod tests {
2232 use super::*;
2233
2234 fn make_identity(role: Role, is_active: bool) -> Identity {
2235 Identity {
2236 user_id: 42,
2237 email: "test@example.com".into(),
2238 role,
2239 is_active,
2240 is_demo: false,
2241 demo_label: None,
2242 must_change_password: false,
2243 mfa_enabled: false,
2244 trust_level: crate::auth::SessionTrust::Authenticated,
2245 }
2246 }
2247
2248 #[test]
2253 fn admin_css_payload_none_is_the_baked_bundle_verbatim() {
2254 let css = admin_css_payload(None);
2255 assert_eq!(css.as_ref(), ADMIN_CSS.as_bytes());
2256 assert!(ADMIN_CSS.contains("--rio-rust"));
2258 }
2259
2260 #[test]
2261 fn admin_css_payload_appends_override_after_baked_bundle() {
2262 let override_css = ":root{--rio-rust:#abcdef}";
2263 let css = admin_css_payload(Some(override_css));
2264 let text = std::str::from_utf8(css.as_ref()).expect("utf-8");
2265 assert!(text.ends_with(override_css));
2267 let baked = text.find("--rio-rust").expect("baked token present");
2272 let overridden = text.rfind("--rio-rust:#abcdef").expect("override present");
2273 assert!(overridden > baked, "override must follow the baked bundle");
2274 }
2275
2276 const SHOP_LIGHT_ONLY: &str = "/* GENERATED by rustio-design */\n\
2283 :root {\n --rio-brand-light: #0e6b5b;\n --rio-accent: #0e6b5b;\n \
2284 --rio-bg: #f7f5f2;\n --rio-surface: #ffffff;\n}\n";
2285
2286 const DARK_AWARE: &str = "/* Generated by rio-theme */\n\
2289 :root {\n --rio-accent: #0e6b5b;\n --rio-bg: #f7f5f2;\n}\n\
2290 :root[data-theme=\"dark\"] {\n --rio-bg: #0f172a;\n}\n\
2291 @media (prefers-color-scheme: dark) {\n :root {\n --rio-bg: #0f172a;\n }\n}\n";
2292
2293 #[test]
2294 fn dark_leak_detector_warns_on_shop_light_only_override() {
2295 assert!(
2296 override_is_dark_leak_hazard(SHOP_LIGHT_ONLY),
2297 "the shop's current light-only override must be flagged"
2298 );
2299 }
2300
2301 #[test]
2302 fn dark_leak_detector_clears_rio_theme_dual_block_output() {
2303 assert!(
2304 !override_is_dark_leak_hazard(DARK_AWARE),
2305 "rio-theme's dual-block output must NOT be flagged"
2306 );
2307 }
2308
2309 #[test]
2310 fn dark_leak_detector_clears_explicit_dark_block_alone() {
2311 let css = ":root{--rio-bg:#fff}\n:root[data-theme=\"dark\"]{--rio-bg:#0f172a}";
2312 assert!(!override_is_dark_leak_hazard(css));
2313 }
2314
2315 #[test]
2316 fn dark_leak_detector_clears_media_block_alone() {
2317 let css =
2318 ":root{--rio-bg:#fff}\n@media (prefers-color-scheme: dark){:root{--rio-bg:#0f172a}}";
2319 assert!(!override_is_dark_leak_hazard(css));
2320 }
2321
2322 #[test]
2323 fn dark_leak_detector_ignores_override_with_no_color_tokens() {
2324 let css = ":root{--rio-radius-md:10px}";
2326 assert!(!override_is_dark_leak_hazard(css));
2327 }
2328
2329 #[tokio::test]
2332 async fn font_routes_serve_shipped_and_404_retired() {
2333 let router = register_font_routes(Router::new());
2334 let req = |p: &str| {
2335 Request::new(
2336 hyper::Method::GET,
2337 p.to_string(),
2338 String::new(),
2339 Default::default(),
2340 bytes::Bytes::new(),
2341 )
2342 };
2343 for p in [
2344 "/static/fonts/InterVariable.woff2",
2345 "/static/fonts/JetBrainsMono-Variable-latin.woff2",
2346 "/static/fonts/NotoNaskhArabic-Variable.woff2",
2347 "/static/fonts/Tajawal-Regular.woff2",
2348 ] {
2349 assert_eq!(
2350 router.dispatch(req(p)).await.status.as_u16(),
2351 200,
2352 "{p} is a shipped face and must be served"
2353 );
2354 }
2355 for p in [
2356 "/static/fonts/Geist-Variable.woff2",
2357 "/static/fonts/GeistMono-Variable.woff2",
2358 "/static/fonts/Spectral-400-latin.woff2",
2359 "/static/fonts/HankenGrotesk-Variable-latin.woff2",
2360 ] {
2361 assert_eq!(
2362 router.dispatch(req(p)).await.status.as_u16(),
2363 404,
2364 "{p} was retired and must 404"
2365 );
2366 }
2367 }
2368
2369 #[test]
2374 fn role_guard_decision_admin_meets_staff_floor() {
2375 let id = make_identity(Role::Administrator, true);
2376 assert!(id.role.includes(Role::Staff));
2377 }
2378
2379 #[test]
2380 fn role_guard_decision_user_does_not_meet_staff() {
2381 let id = make_identity(Role::User, true);
2382 assert!(!id.role.includes(Role::Staff));
2383 }
2384
2385 #[test]
2386 fn role_guard_decision_administrator_does_not_meet_developer() {
2387 let id = make_identity(Role::Administrator, true);
2388 assert!(!id.role.includes(Role::Developer));
2389 }
2390
2391 #[test]
2392 fn role_guard_decision_developer_meets_everything() {
2393 let id = make_identity(Role::Developer, true);
2394 for &min in &[
2395 Role::User,
2396 Role::Staff,
2397 Role::Supervisor,
2398 Role::Administrator,
2399 Role::Developer,
2400 ] {
2401 assert!(id.role.includes(min), "Developer should meet {min:?}");
2402 }
2403 }
2404
2405 #[test]
2408 fn perm_guard_admin_short_circuits_without_perm() {
2409 let id = make_identity(Role::Administrator, true);
2410 assert!(perm_guard_verdict(&id, false));
2411 }
2412
2413 #[test]
2414 fn perm_guard_developer_short_circuits_without_perm() {
2415 let id = make_identity(Role::Developer, true);
2416 assert!(perm_guard_verdict(&id, false));
2417 }
2418
2419 #[test]
2420 fn perm_guard_staff_with_perm_passes() {
2421 let id = make_identity(Role::Staff, true);
2422 assert!(perm_guard_verdict(&id, true));
2423 }
2424
2425 #[test]
2426 fn perm_guard_staff_without_perm_denies() {
2427 let id = make_identity(Role::Staff, true);
2428 assert!(!perm_guard_verdict(&id, false));
2429 }
2430
2431 #[test]
2432 fn perm_guard_inactive_admin_denies_even_with_bypass() {
2433 let id = make_identity(Role::Administrator, false);
2435 assert!(!perm_guard_verdict(&id, true));
2436 }
2437
2438 #[test]
2439 fn perm_guard_supervisor_without_perm_denies() {
2440 let id = make_identity(Role::Supervisor, true);
2442 assert!(!perm_guard_verdict(&id, false));
2443 }
2444
2445 #[test]
2450 fn strict_mailer_guard_passes_for_default_admin() {
2451 let admin = super::super::types::Admin::new();
2452 assert!(strict_mailer_guard_check(&admin).is_ok());
2453 }
2454
2455 #[test]
2458 fn strict_mailer_guard_fails_when_required_but_default_mailer() {
2459 use crate::auth::DefaultRecoveryPolicy;
2460 let admin = super::super::types::Admin::new().recovery_policy(std::sync::Arc::new(
2461 DefaultRecoveryPolicy::new().with_strict_mailer_required(true),
2462 ));
2463 let err = strict_mailer_guard_check(&admin).expect_err("guard should fail");
2464 assert!(
2465 err.contains("strict_mailer_required"),
2466 "error message must name the policy method: {err}"
2467 );
2468 assert!(
2469 err.contains("Admin::mailer"),
2470 "error message must direct the operator to the fix: {err}"
2471 );
2472 }
2473
2474 #[test]
2479 fn strict_mailer_guard_passes_when_mailer_was_explicitly_overridden() {
2480 use crate::auth::DefaultRecoveryPolicy;
2481 use crate::email::LogMailer;
2482 let admin = super::super::types::Admin::new()
2483 .recovery_policy(std::sync::Arc::new(
2484 DefaultRecoveryPolicy::new().with_strict_mailer_required(true),
2485 ))
2486 .mailer(std::sync::Arc::new(LogMailer));
2487 assert!(strict_mailer_guard_check(&admin).is_ok());
2488 }
2489
2490 #[test]
2493 fn strict_mailer_guard_passes_when_strict_mode_disabled() {
2494 let admin = super::super::types::Admin::new();
2495 assert!(strict_mailer_guard_check(&admin).is_ok());
2496 }
2497
2498 #[test]
2501 fn whitelist_accepts_the_three_locked_paths() {
2502 assert!(super::is_must_change_whitelisted_path(
2504 "/admin/must-change-password"
2505 ));
2506 assert!(super::is_must_change_whitelisted_path("/admin/logout"));
2507 assert!(super::is_must_change_whitelisted_path(
2508 "/admin/account/sessions"
2509 ));
2510 }
2511
2512 #[test]
2513 fn whitelist_rejects_subpaths_of_account_sessions() {
2514 assert!(!super::is_must_change_whitelisted_path(
2519 "/admin/account/sessions/revoke"
2520 ));
2521 assert!(!super::is_must_change_whitelisted_path(
2522 "/admin/account/sessions/revoke-others"
2523 ));
2524 assert!(!super::is_must_change_whitelisted_path(
2525 "/admin/account/sessions/"
2526 ));
2527 }
2528
2529 #[test]
2530 fn whitelist_rejects_other_admin_paths() {
2531 for path in [
2532 "/admin",
2533 "/admin/",
2534 "/admin/users",
2535 "/admin/users/42",
2536 "/admin/login",
2537 "/admin/password_change",
2538 "/admin/forgot-password",
2539 "/admin/reauth",
2540 "/admin/must-change-password/", ] {
2542 assert!(
2543 !super::is_must_change_whitelisted_path(path),
2544 "expected reject for {path:?}"
2545 );
2546 }
2547 }
2548
2549 #[test]
2550 fn whitelist_rejects_paths_outside_admin_surface() {
2551 for path in ["/", "/login", "/static/admin.css", "/api"] {
2552 assert!(
2553 !super::is_must_change_whitelisted_path(path),
2554 "expected reject for {path:?}"
2555 );
2556 }
2557 }
2558
2559 #[test]
2562 fn read_only_allows_auth_flow_exact_paths() {
2563 for path in [
2564 "/admin/login",
2565 "/admin/logout",
2566 "/admin/reauth",
2567 "/admin/forgot-password",
2568 "/admin/mfa/verify",
2569 "/admin/must-change-password",
2570 "/admin/password_change",
2571 ] {
2572 assert!(
2573 super::is_read_only_writable_path(path),
2574 "auth path {path:?} must be writable in read-only mode"
2575 );
2576 }
2577 }
2578
2579 #[test]
2580 fn read_only_allows_prefix_paths() {
2581 for path in [
2585 "/admin/reset-password/abc123",
2586 "/admin/reset-password/abc123/whatever",
2587 "/admin/account/sessions/42/revoke",
2588 "/admin/account/sessions/revoke-all",
2589 "/admin/account/mfa/enroll",
2590 "/admin/account/mfa/disable",
2591 ] {
2592 assert!(
2593 super::is_read_only_writable_path(path),
2594 "prefix-allowlisted path {path:?} must be writable"
2595 );
2596 }
2597 }
2598
2599 #[test]
2600 fn read_only_blocks_project_data_mutations() {
2601 for path in [
2604 "/admin/posts/new",
2605 "/admin/posts/42/edit",
2606 "/admin/posts/42/delete",
2607 "/admin/posts/bulk_delete",
2608 "/admin/posts/bulk/archive",
2609 "/admin/users/new",
2610 "/admin/users/42/edit",
2611 "/admin/users/42/reset-password",
2612 "/admin/users/42/lock",
2613 "/admin/users/42/sessions/99/revoke",
2614 "/admin/groups/new",
2615 "/admin/groups/42/delete",
2616 ] {
2617 assert!(
2618 !super::is_read_only_writable_path(path),
2619 "data-mutation path {path:?} must be blocked in read-only mode"
2620 );
2621 }
2622 }
2623
2624 #[test]
2625 fn read_only_blocks_random_paths_outside_admin_surface() {
2626 for path in ["/", "/login", "/static/admin.css", "/api/v1/posts"] {
2631 assert!(
2632 !super::is_read_only_writable_path(path),
2633 "non-admin path {path:?} must not be writable"
2634 );
2635 }
2636 }
2637
2638 #[test]
2639 fn extract_admin_name_parses_slug_segment() {
2640 assert_eq!(super::extract_admin_name("/admin/posts"), Some("posts"));
2641 assert_eq!(super::extract_admin_name("/admin/posts/"), Some("posts"));
2642 assert_eq!(
2643 super::extract_admin_name("/admin/posts/42/edit"),
2644 Some("posts")
2645 );
2646 assert_eq!(
2647 super::extract_admin_name("/admin/users/42/sessions/99/revoke"),
2648 Some("users")
2649 );
2650 }
2651
2652 #[test]
2653 fn extract_admin_name_rejects_root_reserved_and_non_admin() {
2654 assert_eq!(super::extract_admin_name("/admin/"), None);
2656 assert_eq!(super::extract_admin_name("/admin"), None);
2657 assert_eq!(super::extract_admin_name("/admin/_search"), None);
2660 assert_eq!(super::extract_admin_name("/admin/_lookup/posts"), None);
2661 assert_eq!(super::extract_admin_name("/login"), None);
2663 assert_eq!(super::extract_admin_name("/static/admin.css"), None);
2664 }
2665
2666 #[test]
2667 fn read_only_model_builder_and_accessor_round_trip() {
2668 let admin = super::super::types::Admin::new()
2669 .read_only_model("archive_posts")
2670 .read_only_model("legacy_invoices");
2671 assert!(admin.is_model_read_only("archive_posts"));
2672 assert!(admin.is_model_read_only("legacy_invoices"));
2673 assert!(!admin.is_model_read_only("posts"));
2674 assert!(!admin.is_read_only());
2676 }
2677
2678 #[test]
2679 fn is_mutating_method_recognises_write_verbs() {
2680 use hyper::Method;
2681 for m in [Method::POST, Method::PUT, Method::PATCH, Method::DELETE] {
2682 assert!(super::is_mutating_method(&m), "{m} must be mutating");
2683 }
2684 for m in [Method::GET, Method::HEAD, Method::OPTIONS] {
2685 assert!(!super::is_mutating_method(&m), "{m} must not be mutating");
2686 }
2687 }
2688}
2689
2690#[cfg(test)]
2697mod css_lockstep_tests {
2698 fn manifest_imports() -> Vec<&'static str> {
2700 let manifest = include_str!("../../assets/static/admin/admin.css");
2701 manifest
2702 .match_indices("@import url(\"")
2703 .map(|(i, m)| {
2704 let rest = &manifest[i + m.len()..];
2705 &rest[..rest.find("\")").expect("unterminated @import url(...)")]
2706 })
2707 .collect()
2708 }
2709
2710 fn baked_fragments() -> Vec<&'static str> {
2716 let routes_src = include_str!("routes.rs");
2717 let needle = concat!("include_str!(\"", "../../assets/static/admin/");
2718 routes_src
2719 .match_indices(needle)
2720 .map(|(i, m)| {
2721 let rest = &routes_src[i + m.len()..];
2722 &rest[..rest.find("\")").expect("unterminated include_str!(...)")]
2723 })
2724 .filter(|f| *f != "admin.css")
2725 .collect()
2726 }
2727
2728 #[test]
2729 fn import_manifest_matches_concat_bundle() {
2730 assert_eq!(
2731 manifest_imports(),
2732 baked_fragments(),
2733 "admin.css @import manifest and ADMIN_CSS concat! bundle have drifted; \
2734 update BOTH lists in lock-step (see the note on ADMIN_CSS in routes.rs)"
2735 );
2736 }
2737}