1use std::collections::HashMap;
2use std::fmt::{Display, Write};
3
4use std::convert::Infallible;
5
6use anyhow::{anyhow, bail};
7use axum::Router;
8use axum::extract::Request;
9use axum::handler::Handler;
10use axum::middleware::from_fn;
11use axum::response::IntoResponse;
12use axum::routing::{self, MethodRouter, Route};
13use tower::{Layer, Service};
14
15use crate::AppState;
16
17#[derive(Default)]
32pub struct Routes {
33 router: Router<AppState>,
34 names: Vec<(String, String)>,
35 last_path: Option<String>,
36 listing: Vec<RouteInfo>,
37 fallback: Option<MethodRouter<AppState>>,
38 domains: Vec<(String, Routes)>,
39}
40
41pub(crate) struct RouteParts {
43 pub router: Router<AppState>,
44 pub names: Vec<(String, String)>,
45 pub listing: Vec<RouteInfo>,
46 pub fallback: Option<MethodRouter<AppState>>,
47 pub domains: Vec<(String, Routes)>,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
52#[non_exhaustive]
53pub struct RouteInfo {
54 pub method: String,
56 pub path: String,
58 pub name: Option<String>,
60 pub module: String,
62 pub middleware: Vec<String>,
64 pub domain: Option<String>,
66}
67
68macro_rules! method {
69 ($($method:ident),*) => {$(
70 pub fn $method<H, T>(self, path: &str, handler: H) -> Self
72 where
73 H: Handler<T, AppState>,
74 T: 'static,
75 {
76 self.add(path, routing::$method(handler), &stringify!($method).to_uppercase())
77 }
78 )*};
79}
80
81impl Routes {
82 pub fn new() -> Self {
84 Self::default()
85 }
86
87 method!(get, post, put, patch, delete);
88
89 pub fn resource(mut self, path: &str, name: &str, resource: Resource) -> Self {
115 assert!(
116 path.starts_with('/') && !path.ends_with('/'),
117 "Routes::resource(\"{path}\", …): the path must start with `/` and not end with one"
118 );
119 let member = format!("{path}/{{id}}");
120 let actions = [
121 ("index", "GET", path.to_owned(), resource.index),
122 ("create", "GET", format!("{path}/new"), resource.create),
123 ("store", "POST", path.to_owned(), resource.store),
124 ("show", "GET", member.clone(), resource.show),
125 ("edit", "GET", format!("{member}/edit"), resource.edit),
126 ("update", "PUT", member.clone(), resource.update),
127 ("destroy", "DELETE", member, resource.destroy),
128 ];
129 for (action, method, at, router) in actions {
130 if let Some(router) = router {
131 let method = if action == "update" {
132 "PUT|PATCH"
133 } else {
134 method
135 };
136 self = self
137 .add(&at, router, method)
138 .name(&format!("{name}.{action}"));
139 }
140 }
141 self
142 }
143
144 pub fn route(self, path: &str, method_router: MethodRouter<AppState>) -> Self {
146 self.add(path, method_router, "*")
147 }
148
149 pub fn view(self, path: &str, template: &str) -> Self {
159 let template = template.to_owned();
160 self.add(
161 path,
162 axum::routing::get(move || {
163 let template = template.clone();
164 async move { crate::view(&template, minijinja::context! {}) }
165 }),
166 "GET",
167 )
168 }
169
170 pub fn redirect(self, path: &str, to: &str) -> Self {
173 let to = to.to_owned();
174 self.add(
175 path,
176 axum::routing::any(move || {
177 let to = to.clone();
178 async move { redirect_with(axum::http::StatusCode::FOUND, &to) }
179 }),
180 "*",
181 )
182 }
183
184 pub fn permanent_redirect(self, path: &str, to: &str) -> Self {
186 let to = to.to_owned();
187 self.add(
188 path,
189 axum::routing::any(move || {
190 let to = to.clone();
191 async move { redirect_with(axum::http::StatusCode::MOVED_PERMANENTLY, &to) }
192 }),
193 "*",
194 )
195 }
196
197 fn add(mut self, path: &str, method_router: MethodRouter<AppState>, method: &str) -> Self {
198 self.router = self.router.route(path, method_router);
199 self.last_path = Some(path.to_owned());
200 self.listing.push(RouteInfo {
201 method: method.to_owned(),
202 path: path.to_owned(),
203 name: None,
204 module: String::new(),
205 middleware: Vec::new(),
206 domain: None,
207 });
208 self
209 }
210
211 fn mark(mut self, middleware: &str) -> Self {
213 for route in &mut self.listing {
214 route.middleware.push(middleware.to_owned());
215 }
216 self
217 }
218
219 pub fn name(mut self, name: &str) -> Self {
225 let path = self
226 .last_path
227 .clone()
228 .expect("Routes::name() must follow a route");
229 self.names.push((name.to_owned(), path));
230 let path = self.last_path.clone();
234 for route in self.listing.iter_mut().rev() {
235 if Some(&route.path) != path.as_ref() {
236 break;
237 }
238 if route.name.is_none() {
239 route.name = Some(name.to_owned());
240 }
241 }
242 self
243 }
244
245 pub fn require_auth(self) -> Self {
248 self.route_layer(from_fn(crate::auth::require_auth))
249 .mark("auth")
250 }
251
252 pub fn require_verified(self) -> Self {
255 self.route_layer(from_fn(crate::auth::require_verified))
256 .mark("verified")
257 }
258
259 pub fn require_gate(self, name: &str) -> Self {
263 self.requirement(
264 crate::auth::Requirement::Gate(name.to_owned()),
265 "gate",
266 name,
267 )
268 }
269
270 pub fn require_role(self, role: &str) -> Self {
273 self.requirement(
274 crate::auth::Requirement::Role(role.to_owned()),
275 "role",
276 role,
277 )
278 }
279
280 pub fn require_permission(self, permission: &str) -> Self {
283 self.requirement(
284 crate::auth::Requirement::Permission(permission.to_owned()),
285 "permission",
286 permission,
287 )
288 }
289
290 pub fn require_ability(self, ability: &str) -> Self {
293 self.requirement(
294 crate::auth::Requirement::Ability(ability.to_owned()),
295 "ability",
296 ability,
297 )
298 }
299
300 pub fn require_device(self) -> Self {
304 self.device_guard(None, "device")
305 }
306
307 pub fn require_device_ability(self, ability: &str) -> Self {
310 self.device_guard(Some(ability), &format!("device:{ability}"))
311 }
312
313 fn device_guard(self, ability: Option<&str>, mark: &str) -> Self {
314 let ability = ability.map(|a| std::sync::Arc::new(a.to_owned()));
315 self.route_layer(from_fn(
316 move |req: Request, next: axum::middleware::Next| {
317 crate::auth::device_guard(ability.clone(), req, next)
318 },
319 ))
320 .mark(mark)
321 }
322
323 fn requirement(self, requirement: crate::auth::Requirement, kind: &str, name: &str) -> Self {
324 let requirement = std::sync::Arc::new(requirement);
325 self.route_layer(from_fn(
326 move |req: Request, next: axum::middleware::Next| {
327 crate::auth::require(requirement.clone(), req, next)
328 },
329 ))
330 .mark(&format!("{kind}:{name}"))
331 }
332
333 pub fn require_password_confirmed(self) -> Self {
337 self.route_layer(from_fn(crate::auth::require_password_confirmed))
338 .route_layer(from_fn(crate::auth::require_auth))
339 .mark("password.confirm")
340 }
341
342 pub fn guest_only(self) -> Self {
345 self.route_layer(from_fn(crate::auth::guest_only))
346 .mark("guest")
347 }
348
349 pub fn throttle(self, max: u32, per: std::time::Duration) -> Self {
352 let covered: Vec<String> = self
353 .listing
354 .iter()
355 .map(|route| format!("{} {}", route.method, route.path))
356 .collect();
357 let id =
358 crate::webhook::sha256_hex(format!("{}|{max}|{}", covered.join(","), per.as_secs()));
359 let limiter = std::sync::Arc::new(crate::rate_limit::Limiter::new(
360 id[..16].to_owned(),
361 max,
362 per,
363 ));
364 self.route_layer(from_fn(
365 move |req: Request, next: axum::middleware::Next| {
366 let limiter = limiter.clone();
367 async move { crate::rate_limit::check(&limiter, req, next).await }
368 },
369 ))
370 .mark(&format!("throttle:{max}/{}s", per.as_secs()))
371 }
372
373 pub fn throttle_by(self, name: &str) -> Self {
376 let owned = name.to_owned();
377 self.route_layer(from_fn(
378 move |req: Request, next: axum::middleware::Next| {
379 let name = owned.clone();
380 async move { crate::rate_limit::check_named(&name, req, next).await }
381 },
382 ))
383 .mark(&format!("throttle:{name}"))
384 }
385
386 pub fn webhook<W: crate::webhook::Webhook>(self, path: &str) -> Self {
391 let mut routes = self
392 .post(path, crate::webhook::receive::<W>)
393 .name(&format!("webhooks.{}", W::PROVIDER));
394 if let Some(route) = routes.listing.last_mut() {
395 route.middleware.push("no-csrf".into());
396 route.middleware.push(format!("webhook:{}", W::PROVIDER));
397 }
398 routes
399 }
400
401 pub fn etag(self) -> Self {
408 self.route_layer(from_fn(
409 |req: Request, next: axum::middleware::Next| async move {
410 let mut res = next.run(req).await;
411 res.extensions_mut().insert(crate::security::WantsEtag);
412 res
413 },
414 ))
415 .mark("etag")
416 }
417
418 pub fn without_csrf(self) -> Self {
422 self.mark("no-csrf")
423 }
424
425 pub fn cors(self, origins: &[&str]) -> Self {
431 use axum::http::{HeaderName, HeaderValue, Method, header};
432 use tower_http::cors::{AllowOrigin, CorsLayer};
433
434 let origin = if origins.contains(&"*") {
435 AllowOrigin::any()
436 } else {
437 AllowOrigin::list(
438 origins
439 .iter()
440 .filter_map(|o| HeaderValue::from_str(o.trim_end_matches('/')).ok()),
441 )
442 };
443 let layer = CorsLayer::new()
444 .allow_origin(origin)
445 .allow_methods([
446 Method::GET,
447 Method::POST,
448 Method::PUT,
449 Method::PATCH,
450 Method::DELETE,
451 Method::OPTIONS,
452 ])
453 .allow_headers([
454 header::CONTENT_TYPE,
455 header::AUTHORIZATION,
456 header::ACCEPT,
457 HeaderName::from_static(crate::CSRF_HEADER),
458 ])
459 .max_age(std::time::Duration::from_secs(3600));
460 self.cors_layer(layer)
461 }
462
463 pub fn cors_layer(mut self, layer: tower_http::cors::CorsLayer) -> Self {
466 self.router = self.router.layer(layer);
469 self.mark("cors")
470 }
471
472 pub fn route_layer<L>(mut self, layer: L) -> Self
474 where
475 L: Layer<Route> + Clone + Send + Sync + 'static,
476 L::Service: Service<Request> + Clone + Send + Sync + 'static,
477 <L::Service as Service<Request>>::Response: IntoResponse + 'static,
478 <L::Service as Service<Request>>::Error: Into<Infallible> + 'static,
479 <L::Service as Service<Request>>::Future: Send + 'static,
480 {
481 self.router = self.router.route_layer(layer);
482 self
483 }
484
485 pub fn merge(mut self, other: impl Into<Routes>) -> Self {
487 let other = other.into();
488 self.router = self.router.merge(other.router);
489 self.names.extend(other.names);
490 self.listing.extend(other.listing);
491 self.domains.extend(other.domains);
492 assert!(
493 self.fallback.is_none() || other.fallback.is_none(),
494 "Routes::merge: both sides have a fallback"
495 );
496 self.fallback = self.fallback.or(other.fallback);
497 self.last_path = None;
498 self
499 }
500
501 pub fn fallback<H, T>(mut self, handler: H) -> Self
515 where
516 H: Handler<T, AppState>,
517 T: 'static,
518 {
519 assert!(
520 self.fallback.is_none(),
521 "Routes::fallback: these routes have a fallback already"
522 );
523 self.fallback = Some(routing::any(handler));
524 self
525 }
526
527 pub fn domain(mut self, pattern: &str, routes: impl Into<Routes>) -> Self {
553 self.domains.push((pattern.to_owned(), routes.into()));
554 self.last_path = None;
555 self
556 }
557
558 pub fn group(mut self, path: &str, name: &str, routes: impl Into<Routes>) -> Self {
583 assert!(
584 path.starts_with('/') && !path.ends_with('/'),
585 "Routes::group(\"{path}\", …): the prefix must start with `/` and not end with one"
586 );
587 let routes = routes.into();
588 assert!(
589 routes.domains.is_empty() && routes.fallback.is_none(),
590 "Routes::group(\"{path}\", …): put `domain` and `fallback` outside path groups"
591 );
592 let join = |inner: &str| {
593 if inner == "/" {
594 path.to_owned()
595 } else {
596 format!("{path}{inner}")
597 }
598 };
599 self.router = self.router.nest(path, routes.router);
600 self.names.extend(
601 routes
602 .names
603 .into_iter()
604 .map(|(route_name, route_path)| (format!("{name}{route_name}"), join(&route_path))),
605 );
606 self.listing
607 .extend(routes.listing.into_iter().map(|info| RouteInfo {
608 path: join(&info.path),
609 name: info.name.map(|route_name| format!("{name}{route_name}")),
610 ..info
611 }));
612 self.last_path = None;
613 self
614 }
615
616 pub(crate) fn into_parts(self) -> RouteParts {
617 RouteParts {
618 router: self.router,
619 names: self.names,
620 listing: self.listing,
621 fallback: self.fallback,
622 domains: self.domains,
623 }
624 }
625}
626
627impl From<Router<AppState>> for Routes {
628 fn from(router: Router<AppState>) -> Self {
629 Self {
630 router,
631 ..Self::default()
632 }
633 }
634}
635
636#[derive(Debug, Default)]
638pub struct RouteTable {
639 paths: HashMap<String, String>,
640 domains: HashMap<String, String>,
642 methods: HashMap<String, String>,
644}
645
646impl RouteTable {
647 pub(crate) fn insert(&mut self, name: String, path: String) -> anyhow::Result<()> {
648 if let Some(existing) = self.paths.get(&name) {
649 bail!("route name `{name}` is used for both `{existing}` and `{path}`");
650 }
651 self.paths.insert(name, path);
652 Ok(())
653 }
654
655 pub fn path(&self, name: &str) -> Option<&str> {
657 self.paths.get(name).map(String::as_str)
658 }
659
660 #[cfg_attr(not(test), allow(dead_code))]
662 pub(crate) fn names(&self) -> impl Iterator<Item = &str> {
663 self.paths.keys().map(String::as_str)
664 }
665
666 pub(crate) fn set_method(&mut self, name: &str, method: &str) {
668 self.methods.insert(name.to_owned(), method.to_owned());
669 }
670
671 pub(crate) fn set_domain(&mut self, name: &str, domain: &str) {
673 self.domains.insert(name.to_owned(), domain.to_owned());
674 }
675
676 pub(crate) fn name_of(
683 &self,
684 path: &str,
685 domain: Option<&str>,
686 method: Option<&str>,
687 ) -> Option<&str> {
688 let on_path = || {
689 self.paths
690 .iter()
691 .filter(move |(name, p)| {
692 p.as_str() == path && self.domains.get(*name).map(String::as_str) == domain
693 })
694 .map(|(name, _)| name.as_str())
695 };
696 let wanted = method.map(|m| if m == "HEAD" { "GET" } else { m });
697 let by_method = wanted.and_then(|wanted| {
698 on_path()
699 .filter(|name| {
700 self.methods.get(*name).is_some_and(|listed| {
701 listed == "*" || listed.split('|').any(|m| m == wanted)
702 })
703 })
704 .min()
705 });
706 by_method.or_else(|| on_path().min())
707 }
708
709 pub fn url(&self, name: &str, params: &[&dyn Display]) -> anyhow::Result<String> {
711 let pattern = self
712 .path(name)
713 .ok_or_else(|| anyhow!("route `{name}` is not defined"))?;
714 fill(pattern, params).map_err(|err| anyhow!("route `{name}`: {err}"))
715 }
716}
717
718fn fill(pattern: &str, params: &[&dyn Display]) -> anyhow::Result<String> {
719 let mut out = String::with_capacity(pattern.len());
720 let mut params = params.iter();
721 let mut rest = pattern;
722
723 while let Some(start) = rest.find('{') {
724 out.push_str(&rest[..start]);
725 let end = rest[start..]
726 .find('}')
727 .ok_or_else(|| anyhow!("unclosed `{{` in `{pattern}`"))?
728 + start;
729 let placeholder = &rest[start + 1..end];
730 let value = params
731 .next()
732 .ok_or_else(|| anyhow!("missing value for `{{{placeholder}}}`"))?
733 .to_string();
734 encode(&mut out, &value, placeholder.starts_with('*'));
735 rest = &rest[end + 1..];
736 }
737 out.push_str(rest);
738
739 if params.next().is_some() {
740 bail!("too many parameters for `{pattern}`");
741 }
742 Ok(out)
743}
744
745pub(crate) fn encode(out: &mut String, value: &str, keep_slashes: bool) {
746 for byte in value.bytes() {
747 match byte {
748 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
749 out.push(byte as char)
750 }
751 b'/' if keep_slashes => out.push('/'),
752 _ => {
753 let _ = write!(out, "%{byte:02X}");
754 }
755 }
756 }
757}
758
759#[cfg(test)]
760mod tests {
761 use super::*;
762
763 fn table() -> RouteTable {
764 let mut t = RouteTable::default();
765 t.insert("home".into(), "/".into()).unwrap();
766 t.insert("products.show".into(), "/products/{id}".into())
767 .unwrap();
768 t.insert("docs".into(), "/docs/{*path}".into()).unwrap();
769 t
770 }
771
772 #[test]
773 fn builds_urls() {
774 let t = table();
775 assert_eq!(t.url("home", &[]).unwrap(), "/");
776 assert_eq!(t.url("products.show", &[&42]).unwrap(), "/products/42");
777 assert_eq!(
778 t.url("products.show", &[&"a b/c"]).unwrap(),
779 "/products/a%20b%2Fc"
780 );
781 assert_eq!(
782 t.url("docs", &[&"guide/intro"]).unwrap(),
783 "/docs/guide/intro"
784 );
785 }
786
787 #[test]
788 fn rejects_bad_calls() {
789 let t = table();
790 assert!(t.url("missing", &[]).is_err());
791 assert!(t.url("products.show", &[]).is_err());
792 assert!(t.url("home", &[&1]).is_err());
793 }
794
795 #[test]
796 fn rejects_duplicate_names() {
797 let mut t = table();
798 assert!(t.insert("home".into(), "/home".into()).is_err());
799 }
800}
801
802#[derive(Default)]
805#[must_use = "a resource does nothing until given to Routes::resource"]
806pub struct Resource {
807 index: Option<MethodRouter<AppState>>,
808 create: Option<MethodRouter<AppState>>,
809 store: Option<MethodRouter<AppState>>,
810 show: Option<MethodRouter<AppState>>,
811 edit: Option<MethodRouter<AppState>>,
812 update: Option<MethodRouter<AppState>>,
813 destroy: Option<MethodRouter<AppState>>,
814}
815
816macro_rules! resource_action {
817 ($($action:ident => $router:expr),* $(,)?) => {$(
818 pub fn $action<H, T>(mut self, handler: H) -> Self
820 where
821 H: Handler<T, AppState>,
822 T: 'static,
823 {
824 let make: fn(H) -> MethodRouter<AppState> = $router;
825 self.$action = Some(make(handler));
826 self
827 }
828 )*};
829}
830
831impl Resource {
832 pub fn new() -> Self {
834 Self::default()
835 }
836
837 resource_action!(
838 index => routing::get,
839 create => routing::get,
840 store => routing::post,
841 show => routing::get,
842 edit => routing::get,
843 update => |h| routing::put(h.clone()).patch(h),
844 destroy => routing::delete,
845 );
846}
847
848pub(crate) fn route_name_matches(name: &str, pattern: &str) -> bool {
851 let mut parts = pattern.split('*');
852 let first = parts.next().unwrap_or_default();
853 let Some(mut rest) = name.strip_prefix(first) else {
854 return false;
855 };
856 let parts: Vec<&str> = parts.collect();
857 let Some((last, middle)) = parts.split_last() else {
858 return rest.is_empty(); };
860 for part in middle {
861 match rest.find(part) {
862 Some(at) => rest = &rest[at + part.len()..],
863 None => return false,
864 }
865 }
866 rest.ends_with(last)
867}
868
869#[derive(Debug, Clone)]
883pub struct CurrentRoute {
884 name: Option<String>,
885 path: Option<String>,
886}
887
888impl CurrentRoute {
889 pub fn name(&self) -> Option<&str> {
891 self.name.as_deref()
892 }
893
894 pub fn path(&self) -> Option<&str> {
896 self.path.as_deref()
897 }
898
899 pub fn is(&self, pattern: &str) -> bool {
902 self.name
903 .as_deref()
904 .is_some_and(|name| route_name_matches(name, pattern))
905 }
906
907 pub(crate) fn of(
908 method: &axum::http::Method,
909 extensions: &axum::http::Extensions,
910 state: &AppState,
911 ) -> Self {
912 let path = extensions
913 .get::<axum::extract::MatchedPath>()
914 .map(|p| p.as_str().to_owned());
915 let domain = extensions
916 .get::<crate::domain::MatchedDomain>()
917 .map(|d| &*d.0);
918 let name = path
919 .as_deref()
920 .and_then(|p| state.routes.name_of(p, domain, Some(method.as_str())))
921 .map(str::to_owned);
922 Self { name, path }
923 }
924}
925
926impl<S: Send + Sync> axum::extract::FromRequestParts<S> for CurrentRoute {
927 type Rejection = Infallible;
928
929 async fn from_request_parts(
930 parts: &mut axum::http::request::Parts,
931 _: &S,
932 ) -> Result<Self, Infallible> {
933 Ok(match parts.extensions.get::<AppState>() {
934 Some(state) => Self::of(&parts.method, &parts.extensions, state),
935 None => Self {
936 name: None,
937 path: None,
938 },
939 })
940 }
941}
942
943#[cfg(test)]
944mod route_name_tests {
945 use super::route_name_matches;
946
947 #[test]
948 fn patterns_match_like_laravel() {
949 assert!(route_name_matches("admin.users.index", "admin.*"));
950 assert!(route_name_matches("admin.users.index", "*.index"));
951 assert!(route_name_matches("admin.users.index", "admin.*.index"));
952 assert!(route_name_matches("products.show", "products.show"));
953 assert!(!route_name_matches("products.show", "products"));
954 assert!(!route_name_matches("shop.products.show", "products.*"));
955 assert!(route_name_matches("anything", "*"));
956 assert!(!route_name_matches("admin", "admin.*"));
957 assert!(!route_name_matches("admin.users.index", "admin.*.posts.*"));
959 assert!(route_name_matches(
960 "admin.users.posts.edit",
961 "admin.*.posts.*"
962 ));
963 }
964
965 #[tokio::test]
968 async fn current_route_without_the_app_is_none() {
969 use axum::extract::FromRequestParts;
970 let (mut parts, ()) = axum::http::Request::builder()
971 .uri("/x")
972 .body(())
973 .unwrap()
974 .into_parts();
975 let current = super::CurrentRoute::from_request_parts(&mut parts, &())
976 .await
977 .unwrap();
978 assert_eq!(current.name(), None);
979 }
980}
981
982fn redirect_with(status: axum::http::StatusCode, to: &str) -> axum::response::Response {
984 match axum::http::HeaderValue::from_str(to) {
985 Ok(location) => (status, [(axum::http::header::LOCATION, location)]).into_response(),
986 Err(_) => axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response(),
987 }
988}