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/layout/console.css"),
92 "\n",
93 include_str!("../../assets/static/admin/pages/dashboard.css"),
95 "\n",
96 include_str!("../../assets/static/admin/pages/list.css"),
97 "\n",
98 include_str!("../../assets/static/admin/pages/form.css"),
99 "\n",
100 include_str!("../../assets/static/admin/pages/auth.css"),
101 "\n",
102 include_str!("../../assets/static/admin/pages/states.css"),
103 "\n",
104 include_str!("../../assets/static/admin/pages/permissions.css"),
105 "\n",
106 include_str!("../../assets/static/admin/pages/detail.css"),
107 "\n",
108 include_str!("../../assets/static/admin/pages/account.css"),
109 "\n",
110 include_str!("../../assets/static/admin/pages/tools.css"),
111 "\n",
112 include_str!("../../assets/static/admin/print/print.css"),
114);
115
116fn admin_css_payload(override_css: Option<&str>) -> bytes::Bytes {
130 match override_css {
131 Some(extra) => bytes::Bytes::from(format!(
132 "{ADMIN_CSS}\n/* ---- RUSTIO_TOKENS_CSS override ---- */\n{extra}"
133 )),
134 None => bytes::Bytes::from_static(ADMIN_CSS.as_bytes()),
135 }
136}
137
138fn override_is_dark_leak_hazard(css: &str) -> bool {
148 let defines_root_colors = css.contains(":root")
149 && (css.contains("--rio-bg")
150 || css.contains("--rio-surface")
151 || css.contains("--rio-accent"));
152 let handles_dark =
153 css.contains("[data-theme=\"dark\"]") || css.contains("prefers-color-scheme");
154 defines_root_colors && !handles_dark
155}
156
157fn load_token_override() -> Option<String> {
165 let path = std::env::var("RUSTIO_TOKENS_CSS").ok()?;
166 match std::fs::read_to_string(&path) {
167 Ok(css) => {
168 if override_is_dark_leak_hazard(&css) {
169 log::warn!(
170 "RUSTIO_TOKENS_CSS={path:?} is a light-only override: it sets :root \
171 color tokens but has no [data-theme=\"dark\"] or prefers-color-scheme \
172 block, so it will leak light surfaces into dark mode. Regenerate with a \
173 dark-aware generator or add a dark block."
174 );
175 }
176 Some(css)
177 }
178 Err(e) => {
179 log::warn!("RUSTIO_TOKENS_CSS={path:?} unreadable: {e}; serving baked CSS");
180 None
181 }
182 }
183}
184
185const ADMIN_JS: &str = include_str!("../../assets/static/admin.js");
188
189const FONT_INTER: &[u8] = include_bytes!("../../assets/static/fonts/InterVariable.woff2");
202const FONT_TAJAWAL_REG: &[u8] = include_bytes!("../../assets/static/fonts/Tajawal-Regular.woff2");
203const FONT_TAJAWAL_MED: &[u8] = include_bytes!("../../assets/static/fonts/Tajawal-Medium.woff2");
204const FONT_TAJAWAL_BOLD: &[u8] = include_bytes!("../../assets/static/fonts/Tajawal-Bold.woff2");
205const FONT_NOTO_NASKH_AR: &[u8] =
206 include_bytes!("../../assets/static/fonts/NotoNaskhArabic-Variable.woff2");
207const FONT_NOTO_THAI: &[u8] =
208 include_bytes!("../../assets/static/fonts/NotoSansThai-Variable.woff2");
209const FONT_NOTO_DEVA: &[u8] =
210 include_bytes!("../../assets/static/fonts/NotoSansDevanagari-Variable.woff2");
211const FONT_NOTO_JP: &[u8] = include_bytes!("../../assets/static/fonts/NotoSansJP-Regular.woff2");
212const FONT_NOTO_KR: &[u8] = include_bytes!("../../assets/static/fonts/NotoSansKR-Regular.woff2");
213const FONT_NOTO_SC: &[u8] = include_bytes!("../../assets/static/fonts/NotoSansSC-Regular.woff2");
214const FONT_JETBRAINS_LATIN: &[u8] =
220 include_bytes!("../../assets/static/fonts/JetBrainsMono-Variable-latin.woff2");
221const FONT_JETBRAINS_LATIN_EXT: &[u8] =
222 include_bytes!("../../assets/static/fonts/JetBrainsMono-Variable-latinext.woff2");
223
224fn font_response(bytes: &'static [u8]) -> Response {
226 Response::new(hyper::StatusCode::OK, bytes::Bytes::from_static(bytes))
227 .with_header("content-type", "font/woff2")
228 .with_header("cache-control", "public, max-age=31536000, immutable")
229}
230
231fn register_font_routes(router: Router) -> Router {
235 const FONTS: &[(&str, &[u8])] = &[
236 ("/static/fonts/InterVariable.woff2", FONT_INTER),
237 (
238 "/static/fonts/JetBrainsMono-Variable-latin.woff2",
239 FONT_JETBRAINS_LATIN,
240 ),
241 (
242 "/static/fonts/JetBrainsMono-Variable-latinext.woff2",
243 FONT_JETBRAINS_LATIN_EXT,
244 ),
245 ("/static/fonts/Tajawal-Regular.woff2", FONT_TAJAWAL_REG),
246 ("/static/fonts/Tajawal-Medium.woff2", FONT_TAJAWAL_MED),
247 ("/static/fonts/Tajawal-Bold.woff2", FONT_TAJAWAL_BOLD),
248 (
249 "/static/fonts/NotoNaskhArabic-Variable.woff2",
250 FONT_NOTO_NASKH_AR,
251 ),
252 ("/static/fonts/NotoSansThai-Variable.woff2", FONT_NOTO_THAI),
253 (
254 "/static/fonts/NotoSansDevanagari-Variable.woff2",
255 FONT_NOTO_DEVA,
256 ),
257 ("/static/fonts/NotoSansJP-Regular.woff2", FONT_NOTO_JP),
258 ("/static/fonts/NotoSansKR-Regular.woff2", FONT_NOTO_KR),
259 ("/static/fonts/NotoSansSC-Regular.woff2", FONT_NOTO_SC),
260 ];
261 let mut router = router;
262 for &(path, bytes) in FONTS {
263 router = router.get(path, move |_req| async move { Ok(font_response(bytes)) });
264 }
265 router
266}
267
268use super::handlers::{self, AdminCtx};
269use super::render;
270use super::types::Admin;
271
272enum Guard {
276 Allow(Identity),
277 Redirect(Response),
278}
279
280const MUST_CHANGE_WHITELIST: &[&str] = &[
290 "/admin/must-change-password",
291 "/admin/logout",
292 "/admin/account/sessions",
293];
294
295fn is_must_change_whitelisted_path(path: &str) -> bool {
299 MUST_CHANGE_WHITELIST.contains(&path)
300}
301
302const READ_ONLY_EXACT_ALLOW: &[&str] = &[
313 "/admin/login",
314 "/admin/logout",
315 "/admin/reauth",
316 "/admin/forgot-password",
317 "/admin/mfa/verify",
318 "/admin/must-change-password",
319 "/admin/password_change",
320];
321
322const READ_ONLY_PREFIX_ALLOW: &[&str] = &[
323 "/admin/reset-password/",
326 "/admin/account/sessions/",
329 "/admin/account/mfa/",
332];
333
334fn is_saved_filter_path(path: &str) -> bool {
340 if let Some(rest) = path.strip_prefix("/admin/") {
341 if let Some((_, after)) = rest.split_once('/') {
345 return after == "saved_filters" || after.starts_with("saved_filters/");
346 }
347 }
348 false
349}
350
351pub(crate) fn is_mutating_method(method: &hyper::Method) -> bool {
357 matches!(
358 *method,
359 hyper::Method::POST | hyper::Method::PUT | hyper::Method::PATCH | hyper::Method::DELETE
360 )
361}
362
363pub(crate) fn is_read_only_writable_path(path: &str) -> bool {
371 if READ_ONLY_EXACT_ALLOW.contains(&path) {
372 return true;
373 }
374 if READ_ONLY_PREFIX_ALLOW
375 .iter()
376 .any(|prefix| path.starts_with(prefix))
377 {
378 return true;
379 }
380 is_saved_filter_path(path)
381}
382
383pub(crate) fn extract_admin_name(path: &str) -> Option<&str> {
390 let rest = path.strip_prefix("/admin/")?;
391 let slug = rest.split('/').next()?;
392 if slug.is_empty() || slug.starts_with('_') {
393 return None;
394 }
395 Some(slug)
396}
397
398const MFA_ENROLL_WHITELIST: &[&str] = &[
411 "/admin/account/mfa/enroll",
412 "/admin/logout",
413 "/admin/account/sessions",
414];
415
416fn is_mfa_enroll_whitelisted_path(path: &str) -> bool {
417 MFA_ENROLL_WHITELIST.contains(&path)
418}
419
420const MFA_VERIFY_WHITELIST: &[&str] = &[
430 "/admin/mfa/verify",
431 "/admin/logout",
432 "/admin/account/sessions",
433];
434
435fn is_mfa_verify_whitelisted_path(path: &str) -> bool {
436 MFA_VERIFY_WHITELIST.contains(&path)
437}
438
439fn mfa_required_for_role(policy: crate::auth::MfaPolicy, role: Role) -> bool {
449 use crate::auth::MfaPolicy;
450 match policy {
451 MfaPolicy::Disabled | MfaPolicy::Optional => false,
452 MfaPolicy::Required => true,
453 MfaPolicy::RequiredForRoles(roles) => roles.contains(&role),
454 }
455}
456
457async fn login_guard(ctx: &AdminCtx, req: &Request) -> Result<Guard> {
458 let cookie = match req.header("cookie") {
459 Some(c) => c,
460 None => return Ok(Guard::Redirect(Response::redirect("/admin/login"))),
461 };
462 let token = match auth::session_token_from_cookie(cookie) {
463 Some(t) => t,
464 None => return Ok(Guard::Redirect(Response::redirect("/admin/login"))),
465 };
466 let ident = match auth::identity_from_session(&ctx.db, &token).await? {
467 Some(i) => i,
468 None => return Ok(Guard::Redirect(Response::redirect("/admin/login"))),
469 };
470 if !ident.is_active {
471 return Ok(Guard::Redirect(Response::redirect("/admin/login")));
472 }
473
474 if ident.must_change_password && !is_must_change_whitelisted_path(req.path()) {
480 return Ok(Guard::Redirect(Response::redirect(
481 "/admin/must-change-password",
482 )));
483 }
484
485 let policy = ctx.admin.active_mfa_policy();
494 if mfa_required_for_role(policy, ident.role)
495 && !ident.mfa_enabled
496 && !is_mfa_enroll_whitelisted_path(req.path())
497 {
498 return Ok(Guard::Redirect(Response::redirect(
499 "/admin/account/mfa/enroll",
500 )));
501 }
502
503 use crate::auth::SessionTrust;
511 if ident.mfa_enabled
512 && ident.trust_level != SessionTrust::MfaVerified
513 && !is_mfa_verify_whitelisted_path(req.path())
514 {
515 return Ok(Guard::Redirect(Response::redirect("/admin/mfa/verify")));
516 }
517
518 Ok(Guard::Allow(ident))
519}
520
521async fn role_guard(ctx: &AdminCtx, req: &Request, min: Role) -> Result<Guard> {
522 match login_guard(ctx, req).await? {
523 Guard::Redirect(r) => Ok(Guard::Redirect(r)),
524 Guard::Allow(ident) => {
525 if ident.role.includes(min) {
526 Ok(Guard::Allow(ident))
527 } else {
528 let body = render::render_forbidden_body(
529 &ctx.admin,
530 &ctx.templates,
531 &ident,
532 handlers::csrf_token(req),
533 None,
534 Some(min.label()),
535 )?;
536 Ok(Guard::Redirect(
537 Response::html(body).with_status(hyper::StatusCode::FORBIDDEN),
538 ))
539 }
540 }
541 }
542}
543
544async fn perm_guard(ctx: &AdminCtx, req: &Request, perm: &str) -> Result<Guard> {
545 match role_guard(ctx, req, Role::Staff).await? {
546 Guard::Redirect(r) => Ok(Guard::Redirect(r)),
547 Guard::Allow(ident) => {
548 if ident.role.bypasses_group_checks() {
549 return Ok(Guard::Allow(ident));
550 }
551 if auth::check_permission(&ctx.db, &ident, perm).await? {
552 Ok(Guard::Allow(ident))
553 } else {
554 let body = render::render_forbidden_body(
555 &ctx.admin,
556 &ctx.templates,
557 &ident,
558 handlers::csrf_token(req),
559 Some(perm.to_string()),
560 None,
561 )?;
562 Ok(Guard::Redirect(
563 Response::html(body).with_status(hyper::StatusCode::FORBIDDEN),
564 ))
565 }
566 }
567 }
568}
569
570#[cfg(test)]
573fn perm_guard_verdict(ident: &Identity, perm_held: bool) -> bool {
574 if !ident.is_active {
575 return false;
576 }
577 if ident.role.bypasses_group_checks() {
578 return true;
579 }
580 perm_held
581}
582
583fn parse_id(raw: Option<&str>) -> Result<i64> {
584 raw.and_then(|s| s.parse().ok())
585 .ok_or_else(|| Error::BadRequest("invalid id".into()))
586}
587
588fn model_name_from_req(req: &Request) -> Result<String> {
589 req.param("admin_name")
590 .map(|s| s.to_string())
591 .ok_or_else(|| Error::BadRequest("missing model".into()))
592}
593
594fn perm_for(ctx: &AdminCtx, admin_name: &str, action: &str) -> Result<String> {
595 let entry = ctx
596 .admin
597 .find(admin_name)
598 .ok_or_else(|| Error::NotFound(format!("no admin model: {admin_name}")))?;
599 let singular = entry.singular_name.to_ascii_lowercase();
600 Ok(format!("{admin_name}.{action}_{singular}"))
601}
602
603async fn resolve_identity_for_error_page(db: &Db, cookie_header: &str) -> Option<Identity> {
633 let token = auth::session_token_from_cookie(cookie_header)?;
634 let identity = auth::identity_from_session(db, token.as_str())
635 .await
636 .ok()
637 .flatten()?;
638 if !identity.is_active {
639 return None;
640 }
641 Some(identity)
642}
643
644fn strict_mailer_guard_check(admin: &Admin) -> std::result::Result<(), String> {
645 if admin.active_recovery_policy().strict_mailer_required() && !admin.has_custom_mailer() {
646 Err(
647 "rustio-admin: RecoveryPolicy::strict_mailer_required() = true but no mailer \
648 was registered via Admin::mailer(...).\n\n\
649 The framework's default LogMailer writes recovery emails to log::info! instead \
650 of sending them, which is unsuitable for production. Recovery routes are NOT \
651 registered with this configuration.\n\n\
652 To resolve, choose one:\n\
653 (a) register a real mailer before calling register_admin_routes:\n\
654 Admin::mailer(Arc::new(MyProjectMailer::new(...)))\n\
655 (b) opt the policy out of strict mode (the framework default — dev / CI / \
656 testing baseline):\n\
657 RecoveryPolicy::strict_mailer_required(false)\n\n\
658 See DESIGN_RECOVERY.md §12.1 for the contract."
659 .to_string(),
660 )
661 } else {
662 Ok(())
663 }
664}
665
666pub fn register_admin_routes(
668 router: Router,
669 admin: Admin,
670 db: Db,
671 templates: Arc<Templates>,
672) -> Router {
673 if let Err(msg) = strict_mailer_guard_check(&admin) {
681 panic!("{msg}");
682 }
683
684 let ctx = Arc::new(AdminCtx::new(
685 Arc::new(admin),
686 db.clone(),
687 templates.clone(),
688 ));
689
690 let auth_ctx = Arc::new(super::builtin::AuthAdminCtx {
693 admin: ctx.admin.clone(),
694 db,
695 templates,
696 });
697
698 let err_admin = ctx.admin.clone();
713 let err_templates = ctx.templates.clone();
714 let err_db = ctx.db.clone();
715 let router = router.middleware(move |req, next| {
716 let admin = err_admin.clone();
717 let templates = err_templates.clone();
718 let db = err_db.clone();
719 Box::pin(async move {
720 let is_admin_path = req.path().starts_with("/admin");
721 let cookie_header = if is_admin_path {
727 req.header("cookie").map(|s| s.to_string())
728 } else {
729 None
730 };
731 let result = next.run(req).await;
732 match result {
733 Ok(resp) => Ok(resp),
734 Err(err) if is_admin_path => {
735 let identity = match cookie_header.as_deref() {
736 Some(cookie) => resolve_identity_for_error_page(&db, cookie).await,
737 None => None,
738 };
739 Ok(render::render_admin_error_response(
740 &admin,
741 &templates,
742 identity.as_ref(),
743 err.status(),
744 err.client_message().to_string(),
745 ))
746 }
747 Err(err) => Err(err),
748 }
749 })
750 });
751
752 let ro_flag = ctx.admin.is_read_only();
764 let ro_models = std::sync::Arc::new(ctx.admin.read_only_models.clone());
765 let router = router.middleware(move |req, next| {
766 let ro_models = ro_models.clone();
767 Box::pin(async move {
768 if req.path().starts_with("/admin")
769 && is_mutating_method(req.method())
770 && !is_read_only_writable_path(req.path())
771 {
772 if ro_flag {
775 return Err(Error::Forbidden(
776 "This admin is currently in read-only mode. \
777 Project-data mutations are disabled until the operator \
778 turns read-only off."
779 .into(),
780 ));
781 }
782 if !ro_models.is_empty() {
787 if let Some(slug) = extract_admin_name(req.path()) {
788 if ro_models.contains(slug) {
789 return Err(Error::Forbidden(format!(
790 "Model `{slug}` is frozen (read-only). \
791 Mutations on this model are disabled."
792 )));
793 }
794 }
795 }
796 }
797 next.run(req).await
798 })
799 });
800
801 let admin_css = admin_css_payload(load_token_override().as_deref());
813 let router = router.get("/static/admin.css", move |_req| {
814 let body = admin_css.clone();
815 async move {
816 Ok(Response::new(hyper::StatusCode::OK, body)
817 .with_header("content-type", "text/css; charset=utf-8")
818 .with_header("cache-control", "no-cache, must-revalidate"))
819 }
820 });
821 let router = router.get("/static/admin.js", |_req| async move {
822 Ok(Response::new(
823 hyper::StatusCode::OK,
824 bytes::Bytes::from_static(ADMIN_JS.as_bytes()),
825 )
826 .with_header("content-type", "application/javascript; charset=utf-8")
827 .with_header("cache-control", "no-cache, must-revalidate"))
828 });
829
830 let router = register_font_routes(router);
833
834 let c = ctx.clone();
839 let router = router.get("/admin/healthz", move |_req| {
840 let c = c.clone();
841 async move { super::healthz::healthz(&c.db).await }
842 });
843
844 let c = ctx.clone();
846 let router = router.get("/admin/login", move |req| {
847 let c = c.clone();
848 async move { handlers::show_login(&c, req).await }
849 });
850
851 let c = ctx.clone();
852 let router = router.post("/admin/login", move |req| {
853 let c = c.clone();
854 async move { handlers::do_login(&c, req).await }
855 });
856
857 let c = ctx.clone();
858 let router = router.post("/admin/logout", move |req| {
859 let c = c.clone();
860 async move { handlers::do_logout(&c, req).await }
861 });
862
863 let recovery_state = Arc::new(super::recovery_handlers::RecoveryState::from_admin(
880 &ctx.admin,
881 ));
882
883 let c = ctx.clone();
884 let router = router.get("/admin/forgot-password", move |req| {
885 let c = c.clone();
886 async move { super::recovery_handlers::show_forgot_password(&c, &req).await }
887 });
888
889 let c = ctx.clone();
890 let rs = recovery_state.clone();
891 let router = router.post("/admin/forgot-password", move |req| {
892 let c = c.clone();
893 let rs = rs.clone();
894 async move { super::recovery_handlers::do_forgot_password(&c, &rs, req).await }
895 });
896
897 let c = ctx.clone();
898 let router = router.get("/admin/forgot-password/sent", move |req| {
899 let c = c.clone();
900 async move { super::recovery_handlers::show_forgot_password_sent(&c, &req).await }
901 });
902
903 let c = ctx.clone();
904 let router = router.get("/admin/reset-password/:token", move |req| {
905 let c = c.clone();
906 async move {
907 let token = req
908 .param("token")
909 .ok_or_else(|| Error::BadRequest("missing token".into()))?
910 .to_string();
911 super::recovery_handlers::show_reset_password(&c, &req, &token).await
912 }
913 });
914
915 let c = ctx.clone();
916 let rs = recovery_state.clone();
917 let router = router.post("/admin/reset-password/:token", move |req| {
918 let c = c.clone();
919 let rs = rs.clone();
920 async move {
921 let token = req
922 .param("token")
923 .ok_or_else(|| Error::BadRequest("missing token".into()))?
924 .to_string();
925 super::recovery_handlers::do_reset_password(&c, &rs, req, &token).await
926 }
927 });
928
929 let c = ctx.clone();
931 let router = router.get("/admin", move |req| {
932 let c = c.clone();
933 async move {
934 match role_guard(&c, &req, Role::Staff).await? {
935 Guard::Redirect(r) => Ok(r),
936 Guard::Allow(ident) => handlers::dashboard(&c, ident, &req).await,
937 }
938 }
939 });
940
941 let c = ctx.clone();
947 let router = router.get("/admin/db", move |req| {
948 let c = c.clone();
949 async move {
950 match role_guard(&c, &req, Role::Developer).await? {
951 Guard::Redirect(r) => Ok(r),
952 Guard::Allow(ident) => super::db_browser::show_db_browser(&c, ident, &req).await,
953 }
954 }
955 });
956
957 let c = ctx.clone();
962 let router = router.get("/admin/notifications", move |req| {
963 let c = c.clone();
964 async move {
965 match role_guard(&c, &req, Role::Staff).await? {
966 Guard::Redirect(r) => Ok(r),
967 Guard::Allow(ident) => handlers::show_notifications(&c, ident, &req).await,
968 }
969 }
970 });
971 let c = ctx.clone();
972 let router = router.post("/admin/notifications/mark_all_read", move |req| {
973 let c = c.clone();
974 async move {
975 match role_guard(&c, &req, Role::Staff).await? {
976 Guard::Redirect(r) => Ok(r),
977 Guard::Allow(ident) => {
978 handlers::do_mark_all_notifications_read(&c, ident, req).await
979 }
980 }
981 }
982 });
983
984 let c = ctx.clone();
989 let router = router.get("/admin/feature_flags", move |req| {
990 let c = c.clone();
991 async move {
992 match role_guard(&c, &req, Role::Administrator).await? {
993 Guard::Redirect(r) => Ok(r),
994 Guard::Allow(ident) => handlers::show_feature_flags(&c, ident, &req).await,
995 }
996 }
997 });
998 let c = ctx.clone();
999 let router = router.post("/admin/feature_flags", move |req| {
1000 let c = c.clone();
1001 async move {
1002 match role_guard(&c, &req, Role::Administrator).await? {
1003 Guard::Redirect(r) => Ok(r),
1004 Guard::Allow(ident) => handlers::do_create_feature_flag(&c, ident, req).await,
1005 }
1006 }
1007 });
1008 let c = ctx.clone();
1009 let router = router.post("/admin/feature_flags/:key/toggle", move |req| {
1010 let c = c.clone();
1011 async move {
1012 let key = req
1013 .param("key")
1014 .ok_or_else(|| Error::BadRequest("missing flag key".into()))?
1015 .to_string();
1016 match role_guard(&c, &req, Role::Administrator).await? {
1017 Guard::Redirect(r) => Ok(r),
1018 Guard::Allow(ident) => handlers::do_toggle_feature_flag(&c, ident, &key, req).await,
1019 }
1020 }
1021 });
1022
1023 let c = ctx.clone();
1029 let router = router.get("/admin/health", move |req| {
1030 let c = c.clone();
1031 async move {
1032 match role_guard(&c, &req, Role::Administrator).await? {
1033 Guard::Redirect(r) => Ok(r),
1034 Guard::Allow(ident) => handlers::show_health(&c, ident, &req).await,
1035 }
1036 }
1037 });
1038
1039 let c = ctx.clone();
1041 let router = router.get("/admin/history", move |req| {
1042 let c = c.clone();
1043 async move {
1044 match role_guard(&c, &req, Role::Administrator).await? {
1045 Guard::Redirect(r) => Ok(r),
1046 Guard::Allow(ident) => handlers::show_log_entries(&c, ident, &req).await,
1047 }
1048 }
1049 });
1050
1051 let c = ctx.clone();
1054 let router = router.get("/admin/account/sessions", move |req| {
1055 let c = c.clone();
1056 async move {
1057 match role_guard(&c, &req, Role::User).await? {
1058 Guard::Redirect(r) => Ok(r),
1059 Guard::Allow(ident) => handlers::show_account_sessions(&c, ident, &req).await,
1060 }
1061 }
1062 });
1063
1064 let c = ctx.clone();
1072 let router = router.post("/admin/account/sessions/revoke-others", move |req| {
1073 let c = c.clone();
1074 async move {
1075 match role_guard(&c, &req, Role::User).await? {
1076 Guard::Redirect(r) => Ok(r),
1077 Guard::Allow(ident) => handlers::do_revoke_other_sessions(&c, ident, req).await,
1078 }
1079 }
1080 });
1081
1082 let c = ctx.clone();
1083 let router = router.post("/admin/account/sessions/revoke-all", move |req| {
1084 let c = c.clone();
1085 async move {
1086 match role_guard(&c, &req, Role::User).await? {
1087 Guard::Redirect(r) => Ok(r),
1088 Guard::Allow(ident) => handlers::do_revoke_all_sessions(&c, ident, req).await,
1089 }
1090 }
1091 });
1092
1093 let c = ctx.clone();
1094 let router = router.post("/admin/account/sessions/:id/revoke", move |req| {
1095 let c = c.clone();
1096 async move {
1097 match role_guard(&c, &req, Role::User).await? {
1098 Guard::Redirect(r) => Ok(r),
1099 Guard::Allow(ident) => {
1100 let id = parse_id(req.param("id"))?;
1101 handlers::do_revoke_session(&c, ident, req, id).await
1102 }
1103 }
1104 }
1105 });
1106
1107 let c = ctx.clone();
1111 let router = router.get("/admin/password_change", move |req| {
1112 let c = c.clone();
1113 async move {
1114 match role_guard(&c, &req, Role::User).await? {
1115 Guard::Redirect(r) => Ok(r),
1116 Guard::Allow(ident) => handlers::show_password_change(&c, ident, &req).await,
1117 }
1118 }
1119 });
1120 let c = ctx.clone();
1121 let router = router.post("/admin/password_change", move |req| {
1122 let c = c.clone();
1123 async move {
1124 match role_guard(&c, &req, Role::User).await? {
1125 Guard::Redirect(r) => Ok(r),
1126 Guard::Allow(ident) => handlers::do_password_change(&c, ident, req).await,
1127 }
1128 }
1129 });
1130
1131 let c = ctx.clone();
1140 let router = router.get("/admin/reauth", move |req| {
1141 let c = c.clone();
1142 async move {
1143 match role_guard(&c, &req, Role::User).await? {
1144 Guard::Redirect(r) => Ok(r),
1145 Guard::Allow(ident) => {
1146 super::admin_recovery_handlers::show_reauth(&c, ident, &req).await
1147 }
1148 }
1149 }
1150 });
1151
1152 let c = ctx.clone();
1153 let router = router.post("/admin/reauth", 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) => {
1159 super::admin_recovery_handlers::do_reauth(&c, ident, req).await
1160 }
1161 }
1162 }
1163 });
1164
1165 let c = ctx.clone();
1175 let router = router.get("/admin/must-change-password", move |req| {
1176 let c = c.clone();
1177 async move {
1178 match role_guard(&c, &req, Role::User).await? {
1179 Guard::Redirect(r) => Ok(r),
1180 Guard::Allow(ident) => {
1181 super::admin_recovery_handlers::show_must_change_password(&c, ident, &req).await
1182 }
1183 }
1184 }
1185 });
1186
1187 let c = ctx.clone();
1188 let router = router.post("/admin/must-change-password", move |req| {
1189 let c = c.clone();
1190 async move {
1191 match role_guard(&c, &req, Role::User).await? {
1192 Guard::Redirect(r) => Ok(r),
1193 Guard::Allow(ident) => {
1194 super::admin_recovery_handlers::do_must_change_password(&c, ident, req).await
1195 }
1196 }
1197 }
1198 });
1199
1200 let c = ctx.clone();
1218 let router = router.get("/admin/mfa/verify", move |req| {
1219 let c = c.clone();
1220 async move {
1221 match role_guard(&c, &req, Role::User).await? {
1222 Guard::Redirect(r) => Ok(r),
1223 Guard::Allow(ident) => super::mfa_handlers::show_verify(&c, ident, &req).await,
1224 }
1225 }
1226 });
1227
1228 let c = ctx.clone();
1229 let router = router.post("/admin/mfa/verify", move |req| {
1230 let c = c.clone();
1231 async move {
1232 match role_guard(&c, &req, Role::User).await? {
1233 Guard::Redirect(r) => Ok(r),
1234 Guard::Allow(ident) => super::mfa_handlers::do_verify(&c, ident, req).await,
1235 }
1236 }
1237 });
1238
1239 let c = ctx.clone();
1241 let router = router.get("/admin/account/mfa/enroll", move |req| {
1242 let c = c.clone();
1243 async move {
1244 match role_guard(&c, &req, Role::User).await? {
1245 Guard::Redirect(r) => Ok(r),
1246 Guard::Allow(ident) => super::mfa_handlers::show_enroll(&c, ident, &req).await,
1247 }
1248 }
1249 });
1250
1251 let c = ctx.clone();
1252 let router = router.post("/admin/account/mfa/enroll", move |req| {
1253 let c = c.clone();
1254 async move {
1255 match role_guard(&c, &req, Role::User).await? {
1256 Guard::Redirect(r) => Ok(r),
1257 Guard::Allow(ident) => super::mfa_handlers::do_enroll(&c, ident, req).await,
1258 }
1259 }
1260 });
1261
1262 let c = ctx.clone();
1264 let router = router.get("/admin/account/mfa/regenerate-codes", move |req| {
1265 let c = c.clone();
1266 async move {
1267 match role_guard(&c, &req, Role::User).await? {
1268 Guard::Redirect(r) => Ok(r),
1269 Guard::Allow(ident) => super::mfa_handlers::show_regenerate(&c, ident, &req).await,
1270 }
1271 }
1272 });
1273
1274 let c = ctx.clone();
1275 let router = router.post("/admin/account/mfa/regenerate-codes", move |req| {
1276 let c = c.clone();
1277 async move {
1278 match role_guard(&c, &req, Role::User).await? {
1279 Guard::Redirect(r) => Ok(r),
1280 Guard::Allow(ident) => super::mfa_handlers::do_regenerate(&c, ident, req).await,
1281 }
1282 }
1283 });
1284
1285 let c = ctx.clone();
1287 let router = router.get("/admin/account/mfa/disable", move |req| {
1288 let c = c.clone();
1289 async move {
1290 match role_guard(&c, &req, Role::User).await? {
1291 Guard::Redirect(r) => Ok(r),
1292 Guard::Allow(ident) => super::mfa_handlers::show_disable(&c, ident, &req).await,
1293 }
1294 }
1295 });
1296
1297 let c = ctx.clone();
1298 let router = router.post("/admin/account/mfa/disable", move |req| {
1299 let c = c.clone();
1300 async move {
1301 match role_guard(&c, &req, Role::User).await? {
1302 Guard::Redirect(r) => Ok(r),
1303 Guard::Allow(ident) => super::mfa_handlers::do_disable(&c, ident, req).await,
1304 }
1305 }
1306 });
1307
1308 let c = ctx.clone();
1310 let ac = auth_ctx.clone();
1311 let router = router.get("/admin/users", move |req| {
1312 let c = c.clone();
1313 let ac = ac.clone();
1314 async move {
1315 match role_guard(&c, &req, Role::Administrator).await? {
1316 Guard::Redirect(r) => Ok(r),
1317 Guard::Allow(ident) => {
1318 super::builtin::list_users(&ac, ident, handlers::csrf_token(&req)).await
1319 }
1320 }
1321 }
1322 });
1323
1324 let c = ctx.clone();
1325 let ac = auth_ctx.clone();
1326 let router = router.get("/admin/users/new", move |req| {
1327 let c = c.clone();
1328 let ac = ac.clone();
1329 async move {
1330 match role_guard(&c, &req, Role::Administrator).await? {
1331 Guard::Redirect(r) => Ok(r),
1332 Guard::Allow(ident) => {
1333 super::builtin::show_new_user(&ac, ident, handlers::csrf_token(&req)).await
1334 }
1335 }
1336 }
1337 });
1338
1339 let c = ctx.clone();
1340 let ac = auth_ctx.clone();
1341 let router = router.post("/admin/users/new", move |req| {
1342 let c = c.clone();
1343 let ac = ac.clone();
1344 async move {
1345 match role_guard(&c, &req, Role::Administrator).await? {
1346 Guard::Redirect(r) => Ok(r),
1347 Guard::Allow(ident) => super::builtin::do_new_user(&ac, ident, req).await,
1348 }
1349 }
1350 });
1351
1352 let c = ctx.clone();
1353 let ac = auth_ctx.clone();
1354 let router = router.get("/admin/users/:id/edit", move |req| {
1355 let c = c.clone();
1356 let ac = ac.clone();
1357 async move {
1358 match role_guard(&c, &req, Role::Administrator).await? {
1359 Guard::Redirect(r) => Ok(r),
1360 Guard::Allow(ident) => {
1361 let id = parse_id(req.param("id"))?;
1362 super::builtin::show_user_edit(&ac, ident, id, handlers::csrf_token(&req)).await
1363 }
1364 }
1365 }
1366 });
1367
1368 let c = ctx.clone();
1369 let ac = auth_ctx.clone();
1370 let router = router.post("/admin/users/:id/edit", move |req| {
1371 let c = c.clone();
1372 let ac = ac.clone();
1373 async move {
1374 match role_guard(&c, &req, Role::Administrator).await? {
1375 Guard::Redirect(r) => Ok(r),
1376 Guard::Allow(ident) => {
1377 let id = parse_id(req.param("id"))?;
1378 super::builtin::do_user_edit(&ac, ident, id, req).await
1379 }
1380 }
1381 }
1382 });
1383
1384 let c = ctx.clone();
1385 let ac = auth_ctx.clone();
1386 let router = router.get("/admin/users/:id/delete", move |req| {
1387 let c = c.clone();
1388 let ac = ac.clone();
1389 async move {
1390 match role_guard(&c, &req, Role::Administrator).await? {
1391 Guard::Redirect(r) => Ok(r),
1392 Guard::Allow(ident) => {
1393 let id = parse_id(req.param("id"))?;
1394 super::builtin::show_user_delete(&ac, ident, id, handlers::csrf_token(&req))
1395 .await
1396 }
1397 }
1398 }
1399 });
1400
1401 let c = ctx.clone();
1402 let ac = auth_ctx.clone();
1403 let router = router.post("/admin/users/:id/delete", move |req| {
1404 let c = c.clone();
1405 let ac = ac.clone();
1406 async move {
1407 match role_guard(&c, &req, Role::Administrator).await? {
1408 Guard::Redirect(r) => Ok(r),
1409 Guard::Allow(ident) => {
1410 let id = parse_id(req.param("id"))?;
1411 super::builtin::do_user_delete(&ac, ident, id, req).await
1412 }
1413 }
1414 }
1415 });
1416
1417 let c = ctx.clone();
1433 let router = router.get("/admin/users/:id/reset-password", move |req| {
1434 let c = c.clone();
1435 async move {
1436 match role_guard(&c, &req, Role::Administrator).await? {
1437 Guard::Redirect(r) => Ok(r),
1438 Guard::Allow(ident) => {
1439 let id = parse_id(req.param("id"))?;
1440 super::admin_recovery_handlers::show_admin_reset_password(&c, ident, id, &req)
1441 .await
1442 }
1443 }
1444 }
1445 });
1446
1447 let c = ctx.clone();
1449 let router = router.post("/admin/users/:id/reset-password", move |req| {
1450 let c = c.clone();
1451 async move {
1452 match role_guard(&c, &req, Role::Administrator).await? {
1453 Guard::Redirect(r) => Ok(r),
1454 Guard::Allow(ident) => {
1455 let id = parse_id(req.param("id"))?;
1456 super::admin_recovery_handlers::do_admin_reset_password(&c, ident, id, req)
1457 .await
1458 }
1459 }
1460 }
1461 });
1462
1463 let c = ctx.clone();
1465 let router = router.get("/admin/users/:id/lock", move |req| {
1466 let c = c.clone();
1467 async move {
1468 match role_guard(&c, &req, Role::Administrator).await? {
1469 Guard::Redirect(r) => Ok(r),
1470 Guard::Allow(ident) => {
1471 let id = parse_id(req.param("id"))?;
1472 super::admin_recovery_handlers::show_lock_user(&c, ident, id, &req).await
1473 }
1474 }
1475 }
1476 });
1477
1478 let c = ctx.clone();
1480 let router = router.post("/admin/users/:id/lock", move |req| {
1481 let c = c.clone();
1482 async move {
1483 match role_guard(&c, &req, Role::Administrator).await? {
1484 Guard::Redirect(r) => Ok(r),
1485 Guard::Allow(ident) => {
1486 let id = parse_id(req.param("id"))?;
1487 super::admin_recovery_handlers::do_lock_user(&c, ident, id, req).await
1488 }
1489 }
1490 }
1491 });
1492
1493 let c = ctx.clone();
1495 let router = router.get("/admin/users/:id/unlock", move |req| {
1496 let c = c.clone();
1497 async move {
1498 match role_guard(&c, &req, Role::Administrator).await? {
1499 Guard::Redirect(r) => Ok(r),
1500 Guard::Allow(ident) => {
1501 let id = parse_id(req.param("id"))?;
1502 super::admin_recovery_handlers::show_unlock_user(&c, ident, id, &req).await
1503 }
1504 }
1505 }
1506 });
1507
1508 let c = ctx.clone();
1510 let router = router.post("/admin/users/:id/unlock", move |req| {
1511 let c = c.clone();
1512 async move {
1513 match role_guard(&c, &req, Role::Administrator).await? {
1514 Guard::Redirect(r) => Ok(r),
1515 Guard::Allow(ident) => {
1516 let id = parse_id(req.param("id"))?;
1517 super::admin_recovery_handlers::do_unlock_user(&c, ident, id, req).await
1518 }
1519 }
1520 }
1521 });
1522
1523 let c = ctx.clone();
1526 let router = router.get("/admin/users/:id/revoke-sessions", move |req| {
1527 let c = c.clone();
1528 async move {
1529 match role_guard(&c, &req, Role::Administrator).await? {
1530 Guard::Redirect(r) => Ok(r),
1531 Guard::Allow(ident) => {
1532 let id = parse_id(req.param("id"))?;
1533 super::admin_recovery_handlers::show_admin_revoke_sessions(&c, ident, id, &req)
1534 .await
1535 }
1536 }
1537 }
1538 });
1539
1540 let c = ctx.clone();
1542 let router = router.post("/admin/users/:id/revoke-sessions", move |req| {
1543 let c = c.clone();
1544 async move {
1545 match role_guard(&c, &req, Role::Administrator).await? {
1546 Guard::Redirect(r) => Ok(r),
1547 Guard::Allow(ident) => {
1548 let id = parse_id(req.param("id"))?;
1549 super::admin_recovery_handlers::do_admin_revoke_sessions(&c, ident, id, req)
1550 .await
1551 }
1552 }
1553 }
1554 });
1555
1556 let c = ctx.clone();
1563 let router = router.post("/admin/users/:id/sessions/:session_id/revoke", move |req| {
1564 let c = c.clone();
1565 async move {
1566 match role_guard(&c, &req, Role::Administrator).await? {
1567 Guard::Redirect(r) => Ok(r),
1568 Guard::Allow(ident) => {
1569 let user_id = parse_id(req.param("id"))?;
1570 let session_id = parse_id(req.param("session_id"))?;
1571 super::admin_recovery_handlers::do_admin_revoke_one_session(
1572 &c, ident, user_id, session_id, req,
1573 )
1574 .await
1575 }
1576 }
1577 }
1578 });
1579
1580 let c = ctx.clone();
1587 let ac = auth_ctx.clone();
1588 let router = router.get("/admin/users/:id", move |req| {
1589 let c = c.clone();
1590 let ac = ac.clone();
1591 async move {
1592 match role_guard(&c, &req, Role::Administrator).await? {
1593 Guard::Redirect(r) => Ok(r),
1594 Guard::Allow(ident) => {
1595 let id = parse_id(req.param("id"))?;
1596 let q = req.query();
1597 let tab = q.get("tab").map(|s| s.to_string());
1598 let page: i64 = q.get("page").and_then(|s| s.parse().ok()).unwrap_or(1);
1599 let viewing_session_id = match req
1600 .header("cookie")
1601 .and_then(crate::auth::session_token_from_cookie)
1602 {
1603 Some(token) => crate::auth::current_session_id(&ac.db, &token)
1604 .await
1605 .ok()
1606 .flatten(),
1607 None => None,
1608 };
1609 super::builtin::show_user_view(
1610 &ac,
1611 ident,
1612 id,
1613 handlers::csrf_token(&req),
1614 tab,
1615 page,
1616 viewing_session_id,
1617 )
1618 .await
1619 }
1620 }
1621 }
1622 });
1623
1624 let c = ctx.clone();
1626 let ac = auth_ctx.clone();
1627 let router = router.get("/admin/groups", move |req| {
1628 let c = c.clone();
1629 let ac = ac.clone();
1630 async move {
1631 match role_guard(&c, &req, Role::Administrator).await? {
1632 Guard::Redirect(r) => Ok(r),
1633 Guard::Allow(ident) => {
1634 super::builtin::list_groups(&ac, ident, handlers::csrf_token(&req)).await
1635 }
1636 }
1637 }
1638 });
1639
1640 let c = ctx.clone();
1641 let ac = auth_ctx.clone();
1642 let router = router.get("/admin/groups/new", move |req| {
1643 let c = c.clone();
1644 let ac = ac.clone();
1645 async move {
1646 match role_guard(&c, &req, Role::Administrator).await? {
1647 Guard::Redirect(r) => Ok(r),
1648 Guard::Allow(ident) => {
1649 super::builtin::show_new_group(&ac, ident, handlers::csrf_token(&req)).await
1650 }
1651 }
1652 }
1653 });
1654
1655 let c = ctx.clone();
1656 let ac = auth_ctx.clone();
1657 let router = router.post("/admin/groups/new", move |req| {
1658 let c = c.clone();
1659 let ac = ac.clone();
1660 async move {
1661 match role_guard(&c, &req, Role::Administrator).await? {
1662 Guard::Redirect(r) => Ok(r),
1663 Guard::Allow(ident) => super::builtin::do_new_group(&ac, ident, req).await,
1664 }
1665 }
1666 });
1667
1668 let c = ctx.clone();
1669 let ac = auth_ctx.clone();
1670 let router = router.get("/admin/groups/:id/edit", move |req| {
1671 let c = c.clone();
1672 let ac = ac.clone();
1673 async move {
1674 match role_guard(&c, &req, Role::Administrator).await? {
1675 Guard::Redirect(r) => Ok(r),
1676 Guard::Allow(ident) => {
1677 let id = parse_id(req.param("id"))?;
1678 super::builtin::show_group_edit(&ac, ident, id, handlers::csrf_token(&req))
1679 .await
1680 }
1681 }
1682 }
1683 });
1684
1685 let c = ctx.clone();
1686 let ac = auth_ctx.clone();
1687 let router = router.post("/admin/groups/:id/edit", move |req| {
1688 let c = c.clone();
1689 let ac = ac.clone();
1690 async move {
1691 match role_guard(&c, &req, Role::Administrator).await? {
1692 Guard::Redirect(r) => Ok(r),
1693 Guard::Allow(ident) => {
1694 let id = parse_id(req.param("id"))?;
1695 super::builtin::do_group_edit(&ac, ident, id, req).await
1696 }
1697 }
1698 }
1699 });
1700
1701 let c = ctx.clone();
1702 let ac = auth_ctx.clone();
1703 let router = router.get("/admin/groups/:id/delete", move |req| {
1704 let c = c.clone();
1705 let ac = ac.clone();
1706 async move {
1707 match role_guard(&c, &req, Role::Administrator).await? {
1708 Guard::Redirect(r) => Ok(r),
1709 Guard::Allow(ident) => {
1710 let id = parse_id(req.param("id"))?;
1711 super::builtin::show_group_delete(&ac, ident, id, handlers::csrf_token(&req))
1712 .await
1713 }
1714 }
1715 }
1716 });
1717
1718 let c = ctx.clone();
1719 let ac = auth_ctx.clone();
1720 let router = router.post("/admin/groups/:id/delete", move |req| {
1721 let c = c.clone();
1722 let ac = ac.clone();
1723 async move {
1724 match role_guard(&c, &req, Role::Administrator).await? {
1725 Guard::Redirect(r) => Ok(r),
1726 Guard::Allow(ident) => {
1727 let id = parse_id(req.param("id"))?;
1728 super::builtin::do_group_delete(&ac, ident, id, req).await
1729 }
1730 }
1731 }
1732 });
1733
1734 let c = ctx.clone();
1743 let router = router.get("/admin/uploads/:filename", move |req| {
1744 let c = c.clone();
1745 async move {
1746 match role_guard(&c, &req, Role::Staff).await? {
1747 Guard::Redirect(r) => Ok(r),
1748 Guard::Allow(ident) => {
1749 let filename = req
1750 .param("filename")
1751 .map(str::to_string)
1752 .unwrap_or_default();
1753 handlers::serve_upload(&c, ident, &filename, req).await
1754 }
1755 }
1756 }
1757 });
1758
1759 let c = ctx.clone();
1765 let router = router.get("/admin/_lookup/:admin_name", move |req| {
1766 let c = c.clone();
1767 async move {
1768 let name = model_name_from_req(&req)?;
1769 let perm = perm_for(&c, &name, "view")?;
1770 match perm_guard(&c, &req, &perm).await? {
1771 Guard::Redirect(r) => Ok(r),
1772 Guard::Allow(ident) => handlers::lookup_model(&c, ident, &name, req).await,
1773 }
1774 }
1775 });
1776
1777 let c = ctx.clone();
1785 let router = router.get("/admin/_search", move |req| {
1786 let c = c.clone();
1787 async move {
1788 match role_guard(&c, &req, Role::Staff).await? {
1789 Guard::Redirect(r) => Ok(r),
1790 Guard::Allow(ident) => handlers::search_models(&c, ident, req).await,
1791 }
1792 }
1793 });
1794
1795 let c = ctx.clone();
1803 let router = router.get("/admin/docs", move |req| {
1804 let c = c.clone();
1805 async move {
1806 match role_guard(&c, &req, Role::Staff).await? {
1807 Guard::Redirect(r) => Ok(r),
1808 Guard::Allow(ident) => handlers::show_docs_index(&c, ident, &req).await,
1809 }
1810 }
1811 });
1812 let c = ctx.clone();
1813 let router = router.get("/admin/docs/:slug", move |req| {
1814 let c = c.clone();
1815 async move {
1816 let slug = req
1817 .param("slug")
1818 .ok_or_else(|| Error::BadRequest("missing doc slug".into()))?
1819 .to_string();
1820 match role_guard(&c, &req, Role::Staff).await? {
1821 Guard::Redirect(r) => Ok(r),
1822 Guard::Allow(ident) => handlers::show_doc_page(&c, ident, &slug, &req).await,
1823 }
1824 }
1825 });
1826
1827 let c = ctx.clone();
1834 let router = router.get("/admin/apis/openapi.json", move |req| {
1835 let c = c.clone();
1836 async move {
1837 match role_guard(&c, &req, Role::Staff).await? {
1838 Guard::Redirect(r) => Ok(r),
1839 Guard::Allow(_) => {
1840 let spec = super::openapi::build_spec(&c.admin);
1841 super::json_api::json_response(spec)
1842 }
1843 }
1844 }
1845 });
1846
1847 let c = ctx.clone();
1853 let router = router.get("/admin/apis/sdk.ts", move |req| {
1854 let c = c.clone();
1855 async move {
1856 match role_guard(&c, &req, Role::Staff).await? {
1857 Guard::Redirect(r) => Ok(r),
1858 Guard::Allow(_) => {
1859 let body = super::sdk_gen::build_typescript(&c.admin);
1860 Ok(crate::http::Response::ok(body)
1861 .with_header("content-type", "text/typescript; charset=utf-8")
1862 .with_header(
1863 "content-disposition",
1864 "attachment; filename=\"rustio-sdk.ts\"",
1865 ))
1866 }
1867 }
1868 }
1869 });
1870
1871 let c = ctx.clone();
1877 let router = router.get("/admin/apis", move |req| {
1878 let c = c.clone();
1879 async move {
1880 match role_guard(&c, &req, Role::Staff).await? {
1881 Guard::Redirect(r) => Ok(r),
1882 Guard::Allow(ident) => handlers::show_apis_index(&c, ident, &req).await,
1883 }
1884 }
1885 });
1886
1887 let c = ctx.clone();
1895 let router = router.get("/admin/apis/playground", move |req| {
1896 let c = c.clone();
1897 async move {
1898 match role_guard(&c, &req, Role::Staff).await? {
1899 Guard::Redirect(r) => Ok(r),
1900 Guard::Allow(ident) => handlers::show_apis_playground(&c, ident, &req).await,
1901 }
1902 }
1903 });
1904
1905 let c = ctx.clone();
1907 let router = router.get("/admin/:admin_name", move |req| {
1908 let c = c.clone();
1909 async move {
1910 let name = model_name_from_req(&req)?;
1911 let perm = perm_for(&c, &name, "view")?;
1912 match perm_guard(&c, &req, &perm).await? {
1913 Guard::Redirect(r) => Ok(r),
1914 Guard::Allow(ident) => handlers::list_model(&c, ident, &name, &req).await,
1915 }
1916 }
1917 });
1918
1919 let c = ctx.clone();
1924 let router = router.get("/admin/:admin_name/export.csv", move |req| {
1925 let c = c.clone();
1926 async move {
1927 let name = model_name_from_req(&req)?;
1928 let perm = perm_for(&c, &name, "view")?;
1929 match perm_guard(&c, &req, &perm).await? {
1930 Guard::Redirect(r) => Ok(r),
1931 Guard::Allow(ident) => handlers::export_model_csv(&c, ident, &name, req).await,
1932 }
1933 }
1934 });
1935
1936 let c = ctx.clone();
1942 let router = router.post("/admin/:admin_name/import.csv", move |req| {
1943 let c = c.clone();
1944 async move {
1945 let name = model_name_from_req(&req)?;
1946 let perm = perm_for(&c, &name, "change")?;
1947 match perm_guard(&c, &req, &perm).await? {
1948 Guard::Redirect(r) => Ok(r),
1949 Guard::Allow(ident) => handlers::import_model_csv(&c, ident, &name, req).await,
1950 }
1951 }
1952 });
1953
1954 let c = ctx.clone();
1960 let router = router.post("/admin/:admin_name/saved_filters", move |req| {
1961 let c = c.clone();
1962 async move {
1963 let name = model_name_from_req(&req)?;
1964 let perm = perm_for(&c, &name, "view")?;
1965 match perm_guard(&c, &req, &perm).await? {
1966 Guard::Redirect(r) => Ok(r),
1967 Guard::Allow(ident) => handlers::do_save_filter(&c, ident, &name, req).await,
1968 }
1969 }
1970 });
1971
1972 let c = ctx.clone();
1976 let router = router.post("/admin/:admin_name/saved_filters/:id/delete", move |req| {
1977 let c = c.clone();
1978 async move {
1979 let name = model_name_from_req(&req)?;
1980 let id = parse_id(req.param("id"))?;
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::do_delete_filter(&c, ident, &name, id, req).await,
1985 }
1986 }
1987 });
1988
1989 let c = ctx.clone();
1991 let router = router.get("/admin/:admin_name/new", move |req| {
1992 let c = c.clone();
1993 async move {
1994 let name = model_name_from_req(&req)?;
1995 let perm = perm_for(&c, &name, "add")?;
1996 match perm_guard(&c, &req, &perm).await? {
1997 Guard::Redirect(r) => Ok(r),
1998 Guard::Allow(ident) => handlers::show_new_form(&c, ident, &name, &req).await,
1999 }
2000 }
2001 });
2002 let c = ctx.clone();
2003 let router = router.post("/admin/:admin_name/new", move |req| {
2004 let c = c.clone();
2005 async move {
2006 let name = model_name_from_req(&req)?;
2007 let perm = perm_for(&c, &name, "add")?;
2008 match perm_guard(&c, &req, &perm).await? {
2009 Guard::Redirect(r) => Ok(r),
2010 Guard::Allow(ident) => handlers::do_create(&c, ident, &name, req).await,
2011 }
2012 }
2013 });
2014
2015 let c = ctx.clone();
2022 let router = router.get("/admin/:admin_name/:id", move |req| {
2023 let c = c.clone();
2024 async move {
2025 let name = model_name_from_req(&req)?;
2026 let perm = perm_for(&c, &name, "view")?;
2027 match perm_guard(&c, &req, &perm).await? {
2028 Guard::Redirect(r) => Ok(r),
2029 Guard::Allow(ident) => {
2030 let id = parse_id(req.param("id"))?;
2031 handlers::show_object_json(&c, ident, &name, id, &req).await
2032 }
2033 }
2034 }
2035 });
2036
2037 let c = ctx.clone();
2039 let router = router.get("/admin/:admin_name/:id/edit", move |req| {
2040 let c = c.clone();
2041 async move {
2042 let name = model_name_from_req(&req)?;
2043 let perm = perm_for(&c, &name, "change")?;
2044 match perm_guard(&c, &req, &perm).await? {
2045 Guard::Redirect(r) => Ok(r),
2046 Guard::Allow(ident) => {
2047 let id = parse_id(req.param("id"))?;
2048 handlers::show_edit_form(&c, ident, &name, id, &req).await
2049 }
2050 }
2051 }
2052 });
2053 let c = ctx.clone();
2054 let router = router.post("/admin/:admin_name/:id/edit", move |req| {
2055 let c = c.clone();
2056 async move {
2057 let name = model_name_from_req(&req)?;
2058 let perm = perm_for(&c, &name, "change")?;
2059 match perm_guard(&c, &req, &perm).await? {
2060 Guard::Redirect(r) => Ok(r),
2061 Guard::Allow(ident) => {
2062 let id = parse_id(req.param("id"))?;
2063 handlers::do_update(&c, ident, &name, id, req).await
2064 }
2065 }
2066 }
2067 });
2068
2069 let c = ctx.clone();
2072 let router = router.get("/admin/:admin_name/:id/history", move |req| {
2073 let c = c.clone();
2074 async move {
2075 let name = model_name_from_req(&req)?;
2076 let perm = perm_for(&c, &name, "view")?;
2077 match perm_guard(&c, &req, &perm).await? {
2078 Guard::Redirect(r) => Ok(r),
2079 Guard::Allow(ident) => {
2080 let id = parse_id(req.param("id"))?;
2081 handlers::show_object_history(&c, ident, &name, id, &req).await
2082 }
2083 }
2084 }
2085 });
2086
2087 let c = ctx.clone();
2089 let router = router.get("/admin/:admin_name/:id/delete", move |req| {
2090 let c = c.clone();
2091 async move {
2092 let name = model_name_from_req(&req)?;
2093 let perm = perm_for(&c, &name, "delete")?;
2094 match perm_guard(&c, &req, &perm).await? {
2095 Guard::Redirect(r) => Ok(r),
2096 Guard::Allow(ident) => {
2097 let id = parse_id(req.param("id"))?;
2098 handlers::show_delete_confirm(&c, ident, &name, id, &req).await
2099 }
2100 }
2101 }
2102 });
2103 let c = ctx.clone();
2104 let router = router.post("/admin/:admin_name/:id/delete", move |req| {
2105 let c = c.clone();
2106 async move {
2107 let name = model_name_from_req(&req)?;
2108 let perm = perm_for(&c, &name, "delete")?;
2109 match perm_guard(&c, &req, &perm).await? {
2110 Guard::Redirect(r) => Ok(r),
2111 Guard::Allow(ident) => {
2112 let id = parse_id(req.param("id"))?;
2113 handlers::do_delete(&c, ident, &name, req, id).await
2114 }
2115 }
2116 }
2117 });
2118
2119 let c = ctx.clone();
2124 let router = router.post("/admin/:admin_name/bulk_delete", 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, "delete")?;
2129 match perm_guard(&c, &req, &perm).await? {
2130 Guard::Redirect(r) => Ok(r),
2131 Guard::Allow(ident) => handlers::handle_bulk_delete(&c, ident, &name, &req).await,
2132 }
2133 }
2134 });
2135
2136 let c = ctx.clone();
2141 router.post("/admin/:admin_name/bulk/:action", move |req| {
2142 let c = c.clone();
2143 async move {
2144 let name = model_name_from_req(&req)?;
2145 let action = req
2146 .param("action")
2147 .ok_or_else(|| Error::BadRequest("missing bulk action name".into()))?
2148 .to_string();
2149 let perm = perm_for(&c, &name, "change")?;
2150 match perm_guard(&c, &req, &perm).await? {
2151 Guard::Redirect(r) => Ok(r),
2152 Guard::Allow(ident) => {
2153 handlers::handle_bulk_action(&c, ident, &name, &action, &req).await
2154 }
2155 }
2156 }
2157 })
2158}
2159
2160#[cfg(test)]
2161mod tests {
2162 use super::*;
2163
2164 fn make_identity(role: Role, is_active: bool) -> Identity {
2165 Identity {
2166 user_id: 42,
2167 email: "test@example.com".into(),
2168 role,
2169 is_active,
2170 is_demo: false,
2171 demo_label: None,
2172 must_change_password: false,
2173 mfa_enabled: false,
2174 trust_level: crate::auth::SessionTrust::Authenticated,
2175 }
2176 }
2177
2178 #[test]
2183 fn admin_css_payload_none_is_the_baked_bundle_verbatim() {
2184 let css = admin_css_payload(None);
2185 assert_eq!(css.as_ref(), ADMIN_CSS.as_bytes());
2186 assert!(ADMIN_CSS.contains("--rio-rust"));
2188 }
2189
2190 #[test]
2191 fn admin_css_payload_appends_override_after_baked_bundle() {
2192 let override_css = ":root{--rio-rust:#abcdef}";
2193 let css = admin_css_payload(Some(override_css));
2194 let text = std::str::from_utf8(css.as_ref()).expect("utf-8");
2195 assert!(text.ends_with(override_css));
2197 let baked = text.find("--rio-rust").expect("baked token present");
2202 let overridden = text.rfind("--rio-rust:#abcdef").expect("override present");
2203 assert!(overridden > baked, "override must follow the baked bundle");
2204 }
2205
2206 const SHOP_LIGHT_ONLY: &str = "/* GENERATED by rustio-design */\n\
2213 :root {\n --rio-brand-light: #0e6b5b;\n --rio-accent: #0e6b5b;\n \
2214 --rio-bg: #f7f5f2;\n --rio-surface: #ffffff;\n}\n";
2215
2216 const DARK_AWARE: &str = "/* Generated by rio-theme */\n\
2219 :root {\n --rio-accent: #0e6b5b;\n --rio-bg: #f7f5f2;\n}\n\
2220 :root[data-theme=\"dark\"] {\n --rio-bg: #0f172a;\n}\n\
2221 @media (prefers-color-scheme: dark) {\n :root {\n --rio-bg: #0f172a;\n }\n}\n";
2222
2223 #[test]
2224 fn dark_leak_detector_warns_on_shop_light_only_override() {
2225 assert!(
2226 override_is_dark_leak_hazard(SHOP_LIGHT_ONLY),
2227 "the shop's current light-only override must be flagged"
2228 );
2229 }
2230
2231 #[test]
2232 fn dark_leak_detector_clears_rio_theme_dual_block_output() {
2233 assert!(
2234 !override_is_dark_leak_hazard(DARK_AWARE),
2235 "rio-theme's dual-block output must NOT be flagged"
2236 );
2237 }
2238
2239 #[test]
2240 fn dark_leak_detector_clears_explicit_dark_block_alone() {
2241 let css = ":root{--rio-bg:#fff}\n:root[data-theme=\"dark\"]{--rio-bg:#0f172a}";
2242 assert!(!override_is_dark_leak_hazard(css));
2243 }
2244
2245 #[test]
2246 fn dark_leak_detector_clears_media_block_alone() {
2247 let css =
2248 ":root{--rio-bg:#fff}\n@media (prefers-color-scheme: dark){:root{--rio-bg:#0f172a}}";
2249 assert!(!override_is_dark_leak_hazard(css));
2250 }
2251
2252 #[test]
2253 fn dark_leak_detector_ignores_override_with_no_color_tokens() {
2254 let css = ":root{--rio-radius-md:10px}";
2256 assert!(!override_is_dark_leak_hazard(css));
2257 }
2258
2259 #[tokio::test]
2262 async fn font_routes_serve_shipped_and_404_retired() {
2263 let router = register_font_routes(Router::new());
2264 let req = |p: &str| {
2265 Request::new(
2266 hyper::Method::GET,
2267 p.to_string(),
2268 String::new(),
2269 Default::default(),
2270 bytes::Bytes::new(),
2271 )
2272 };
2273 for p in [
2274 "/static/fonts/InterVariable.woff2",
2275 "/static/fonts/JetBrainsMono-Variable-latin.woff2",
2276 "/static/fonts/NotoNaskhArabic-Variable.woff2",
2277 "/static/fonts/Tajawal-Regular.woff2",
2278 ] {
2279 assert_eq!(
2280 router.dispatch(req(p)).await.status.as_u16(),
2281 200,
2282 "{p} is a shipped face and must be served"
2283 );
2284 }
2285 for p in [
2286 "/static/fonts/Geist-Variable.woff2",
2287 "/static/fonts/GeistMono-Variable.woff2",
2288 "/static/fonts/Spectral-400-latin.woff2",
2289 "/static/fonts/HankenGrotesk-Variable-latin.woff2",
2290 ] {
2291 assert_eq!(
2292 router.dispatch(req(p)).await.status.as_u16(),
2293 404,
2294 "{p} was retired and must 404"
2295 );
2296 }
2297 }
2298
2299 #[test]
2304 fn role_guard_decision_admin_meets_staff_floor() {
2305 let id = make_identity(Role::Administrator, true);
2306 assert!(id.role.includes(Role::Staff));
2307 }
2308
2309 #[test]
2310 fn role_guard_decision_user_does_not_meet_staff() {
2311 let id = make_identity(Role::User, true);
2312 assert!(!id.role.includes(Role::Staff));
2313 }
2314
2315 #[test]
2316 fn role_guard_decision_administrator_does_not_meet_developer() {
2317 let id = make_identity(Role::Administrator, true);
2318 assert!(!id.role.includes(Role::Developer));
2319 }
2320
2321 #[test]
2322 fn role_guard_decision_developer_meets_everything() {
2323 let id = make_identity(Role::Developer, true);
2324 for &min in &[
2325 Role::User,
2326 Role::Staff,
2327 Role::Supervisor,
2328 Role::Administrator,
2329 Role::Developer,
2330 ] {
2331 assert!(id.role.includes(min), "Developer should meet {min:?}");
2332 }
2333 }
2334
2335 #[test]
2338 fn perm_guard_admin_short_circuits_without_perm() {
2339 let id = make_identity(Role::Administrator, true);
2340 assert!(perm_guard_verdict(&id, false));
2341 }
2342
2343 #[test]
2344 fn perm_guard_developer_short_circuits_without_perm() {
2345 let id = make_identity(Role::Developer, true);
2346 assert!(perm_guard_verdict(&id, false));
2347 }
2348
2349 #[test]
2350 fn perm_guard_staff_with_perm_passes() {
2351 let id = make_identity(Role::Staff, true);
2352 assert!(perm_guard_verdict(&id, true));
2353 }
2354
2355 #[test]
2356 fn perm_guard_staff_without_perm_denies() {
2357 let id = make_identity(Role::Staff, true);
2358 assert!(!perm_guard_verdict(&id, false));
2359 }
2360
2361 #[test]
2362 fn perm_guard_inactive_admin_denies_even_with_bypass() {
2363 let id = make_identity(Role::Administrator, false);
2365 assert!(!perm_guard_verdict(&id, true));
2366 }
2367
2368 #[test]
2369 fn perm_guard_supervisor_without_perm_denies() {
2370 let id = make_identity(Role::Supervisor, true);
2372 assert!(!perm_guard_verdict(&id, false));
2373 }
2374
2375 #[test]
2380 fn strict_mailer_guard_passes_for_default_admin() {
2381 let admin = super::super::types::Admin::new();
2382 assert!(strict_mailer_guard_check(&admin).is_ok());
2383 }
2384
2385 #[test]
2388 fn strict_mailer_guard_fails_when_required_but_default_mailer() {
2389 use crate::auth::DefaultRecoveryPolicy;
2390 let admin = super::super::types::Admin::new().recovery_policy(std::sync::Arc::new(
2391 DefaultRecoveryPolicy::new().with_strict_mailer_required(true),
2392 ));
2393 let err = strict_mailer_guard_check(&admin).expect_err("guard should fail");
2394 assert!(
2395 err.contains("strict_mailer_required"),
2396 "error message must name the policy method: {err}"
2397 );
2398 assert!(
2399 err.contains("Admin::mailer"),
2400 "error message must direct the operator to the fix: {err}"
2401 );
2402 }
2403
2404 #[test]
2409 fn strict_mailer_guard_passes_when_mailer_was_explicitly_overridden() {
2410 use crate::auth::DefaultRecoveryPolicy;
2411 use crate::email::LogMailer;
2412 let admin = super::super::types::Admin::new()
2413 .recovery_policy(std::sync::Arc::new(
2414 DefaultRecoveryPolicy::new().with_strict_mailer_required(true),
2415 ))
2416 .mailer(std::sync::Arc::new(LogMailer));
2417 assert!(strict_mailer_guard_check(&admin).is_ok());
2418 }
2419
2420 #[test]
2423 fn strict_mailer_guard_passes_when_strict_mode_disabled() {
2424 let admin = super::super::types::Admin::new();
2425 assert!(strict_mailer_guard_check(&admin).is_ok());
2426 }
2427
2428 #[test]
2431 fn whitelist_accepts_the_three_locked_paths() {
2432 assert!(super::is_must_change_whitelisted_path(
2434 "/admin/must-change-password"
2435 ));
2436 assert!(super::is_must_change_whitelisted_path("/admin/logout"));
2437 assert!(super::is_must_change_whitelisted_path(
2438 "/admin/account/sessions"
2439 ));
2440 }
2441
2442 #[test]
2443 fn whitelist_rejects_subpaths_of_account_sessions() {
2444 assert!(!super::is_must_change_whitelisted_path(
2449 "/admin/account/sessions/revoke"
2450 ));
2451 assert!(!super::is_must_change_whitelisted_path(
2452 "/admin/account/sessions/revoke-others"
2453 ));
2454 assert!(!super::is_must_change_whitelisted_path(
2455 "/admin/account/sessions/"
2456 ));
2457 }
2458
2459 #[test]
2460 fn whitelist_rejects_other_admin_paths() {
2461 for path in [
2462 "/admin",
2463 "/admin/",
2464 "/admin/users",
2465 "/admin/users/42",
2466 "/admin/login",
2467 "/admin/password_change",
2468 "/admin/forgot-password",
2469 "/admin/reauth",
2470 "/admin/must-change-password/", ] {
2472 assert!(
2473 !super::is_must_change_whitelisted_path(path),
2474 "expected reject for {path:?}"
2475 );
2476 }
2477 }
2478
2479 #[test]
2480 fn whitelist_rejects_paths_outside_admin_surface() {
2481 for path in ["/", "/login", "/static/admin.css", "/api"] {
2482 assert!(
2483 !super::is_must_change_whitelisted_path(path),
2484 "expected reject for {path:?}"
2485 );
2486 }
2487 }
2488
2489 #[test]
2492 fn read_only_allows_auth_flow_exact_paths() {
2493 for path in [
2494 "/admin/login",
2495 "/admin/logout",
2496 "/admin/reauth",
2497 "/admin/forgot-password",
2498 "/admin/mfa/verify",
2499 "/admin/must-change-password",
2500 "/admin/password_change",
2501 ] {
2502 assert!(
2503 super::is_read_only_writable_path(path),
2504 "auth path {path:?} must be writable in read-only mode"
2505 );
2506 }
2507 }
2508
2509 #[test]
2510 fn read_only_allows_prefix_paths() {
2511 for path in [
2515 "/admin/reset-password/abc123",
2516 "/admin/reset-password/abc123/whatever",
2517 "/admin/account/sessions/42/revoke",
2518 "/admin/account/sessions/revoke-all",
2519 "/admin/account/mfa/enroll",
2520 "/admin/account/mfa/disable",
2521 ] {
2522 assert!(
2523 super::is_read_only_writable_path(path),
2524 "prefix-allowlisted path {path:?} must be writable"
2525 );
2526 }
2527 }
2528
2529 #[test]
2530 fn read_only_blocks_project_data_mutations() {
2531 for path in [
2534 "/admin/posts/new",
2535 "/admin/posts/42/edit",
2536 "/admin/posts/42/delete",
2537 "/admin/posts/bulk_delete",
2538 "/admin/posts/bulk/archive",
2539 "/admin/users/new",
2540 "/admin/users/42/edit",
2541 "/admin/users/42/reset-password",
2542 "/admin/users/42/lock",
2543 "/admin/users/42/sessions/99/revoke",
2544 "/admin/groups/new",
2545 "/admin/groups/42/delete",
2546 ] {
2547 assert!(
2548 !super::is_read_only_writable_path(path),
2549 "data-mutation path {path:?} must be blocked in read-only mode"
2550 );
2551 }
2552 }
2553
2554 #[test]
2555 fn read_only_blocks_random_paths_outside_admin_surface() {
2556 for path in ["/", "/login", "/static/admin.css", "/api/v1/posts"] {
2561 assert!(
2562 !super::is_read_only_writable_path(path),
2563 "non-admin path {path:?} must not be writable"
2564 );
2565 }
2566 }
2567
2568 #[test]
2569 fn extract_admin_name_parses_slug_segment() {
2570 assert_eq!(super::extract_admin_name("/admin/posts"), Some("posts"));
2571 assert_eq!(super::extract_admin_name("/admin/posts/"), Some("posts"));
2572 assert_eq!(
2573 super::extract_admin_name("/admin/posts/42/edit"),
2574 Some("posts")
2575 );
2576 assert_eq!(
2577 super::extract_admin_name("/admin/users/42/sessions/99/revoke"),
2578 Some("users")
2579 );
2580 }
2581
2582 #[test]
2583 fn extract_admin_name_rejects_root_reserved_and_non_admin() {
2584 assert_eq!(super::extract_admin_name("/admin/"), None);
2586 assert_eq!(super::extract_admin_name("/admin"), None);
2587 assert_eq!(super::extract_admin_name("/admin/_search"), None);
2590 assert_eq!(super::extract_admin_name("/admin/_lookup/posts"), None);
2591 assert_eq!(super::extract_admin_name("/login"), None);
2593 assert_eq!(super::extract_admin_name("/static/admin.css"), None);
2594 }
2595
2596 #[test]
2597 fn read_only_model_builder_and_accessor_round_trip() {
2598 let admin = super::super::types::Admin::new()
2599 .read_only_model("archive_posts")
2600 .read_only_model("legacy_invoices");
2601 assert!(admin.is_model_read_only("archive_posts"));
2602 assert!(admin.is_model_read_only("legacy_invoices"));
2603 assert!(!admin.is_model_read_only("posts"));
2604 assert!(!admin.is_read_only());
2606 }
2607
2608 #[test]
2609 fn is_mutating_method_recognises_write_verbs() {
2610 use hyper::Method;
2611 for m in [Method::POST, Method::PUT, Method::PATCH, Method::DELETE] {
2612 assert!(super::is_mutating_method(&m), "{m} must be mutating");
2613 }
2614 for m in [Method::GET, Method::HEAD, Method::OPTIONS] {
2615 assert!(!super::is_mutating_method(&m), "{m} must not be mutating");
2616 }
2617 }
2618}
2619
2620#[cfg(test)]
2627mod css_lockstep_tests {
2628 fn manifest_imports() -> Vec<&'static str> {
2630 let manifest = include_str!("../../assets/static/admin/admin.css");
2631 manifest
2632 .match_indices("@import url(\"")
2633 .map(|(i, m)| {
2634 let rest = &manifest[i + m.len()..];
2635 &rest[..rest.find("\")").expect("unterminated @import url(...)")]
2636 })
2637 .collect()
2638 }
2639
2640 fn baked_fragments() -> Vec<&'static str> {
2646 let routes_src = include_str!("routes.rs");
2647 let needle = concat!("include_str!(\"", "../../assets/static/admin/");
2648 routes_src
2649 .match_indices(needle)
2650 .map(|(i, m)| {
2651 let rest = &routes_src[i + m.len()..];
2652 &rest[..rest.find("\")").expect("unterminated include_str!(...)")]
2653 })
2654 .filter(|f| *f != "admin.css")
2655 .collect()
2656 }
2657
2658 #[test]
2659 fn import_manifest_matches_concat_bundle() {
2660 assert_eq!(
2661 manifest_imports(),
2662 baked_fragments(),
2663 "admin.css @import manifest and ADMIN_CSS concat! bundle have drifted; \
2664 update BOTH lists in lock-step (see the note on ADMIN_CSS in routes.rs)"
2665 );
2666 }
2667}