nestrs_core/guard.rs
1//! Route guards ([`CanActivate`]) — run before the handler (NestJS `UseGuards` analogue).
2
3use async_trait::async_trait;
4use axum::http::request::Parts;
5use axum::response::{IntoResponse, Response};
6use serde_json::json;
7
8/// Failure returned from [`CanActivate::can_activate`]; becomes a JSON error body (401 / 403 / 429).
9#[derive(Debug, Clone)]
10pub enum GuardError {
11 Unauthorized(String),
12 Forbidden(String),
13 /// Rate-limit rejection (`ThrottlerGuard`). The response carries
14 /// `Retry-After` and `X-RateLimit-Remaining: 0` headers.
15 TooManyRequests {
16 message: String,
17 retry_after_secs: u64,
18 },
19}
20
21impl GuardError {
22 pub fn unauthorized(message: impl Into<String>) -> Self {
23 Self::Unauthorized(message.into())
24 }
25
26 pub fn forbidden(message: impl Into<String>) -> Self {
27 Self::Forbidden(message.into())
28 }
29
30 pub fn too_many_requests(message: impl Into<String>, retry_after_secs: u64) -> Self {
31 Self::TooManyRequests {
32 message: message.into(),
33 retry_after_secs,
34 }
35 }
36}
37
38impl IntoResponse for GuardError {
39 fn into_response(self) -> Response {
40 match self {
41 GuardError::TooManyRequests {
42 message,
43 retry_after_secs,
44 } => {
45 let body = axum::Json(json!({
46 "statusCode": 429,
47 "message": message,
48 "error": "Too Many Requests",
49 }));
50 let mut resp = (axum::http::StatusCode::TOO_MANY_REQUESTS, body).into_response();
51 if let Ok(v) = retry_after_secs.to_string().parse() {
52 resp.headers_mut().insert("retry-after", v);
53 }
54 resp.headers_mut()
55 .insert("x-ratelimit-remaining", "0".parse().expect("static header"));
56 resp
57 }
58 other => {
59 let (status, message, error_label) = match other {
60 GuardError::Unauthorized(m) => {
61 (axum::http::StatusCode::UNAUTHORIZED, m, "Unauthorized")
62 }
63 GuardError::Forbidden(m) => (axum::http::StatusCode::FORBIDDEN, m, "Forbidden"),
64 GuardError::TooManyRequests { .. } => unreachable!(),
65 };
66 let body = axum::Json(json!({
67 "statusCode": status.as_u16(),
68 "message": message,
69 "error": error_label,
70 }));
71 (status, body).into_response()
72 }
73 }
74 }
75}
76
77/// Authorize the request before the handler runs. Declare per-route guard types in the `impl_routes!`
78/// macro: `GET "/x" with (A, B) => MyController::handler,` — use `with ()` when there are no route guards.
79/// For a guard on **all** routes of a controller, use `controller_guards (G)` on `impl_routes!` (see the
80/// `nestrs` crate); that runs **outside** route-level guards.
81///
82/// Stateless guards are usually unit structs with [`Default`].
83///
84/// # Dependency injection
85///
86/// Guards are resolved **once at route-registration time**, so they can hold dependencies
87/// (JWT keys, repositories, caches). Override [`Self::resolve`] to pull them from the
88/// [`crate::ProviderRegistry`]; keep the `Default` supertrait satisfied with a placeholder unit struct:
89///
90/// ```ignore
91/// #[derive(Default)]
92/// struct AuthGuard { users: Arc<UserRepository> } // real fields live here
93///
94/// // Placeholder used only to satisfy the Default bound:
95/// impl Default for AuthGuard { fn default() -> Self { Self { users: Arc::new(UserRepository::empty()) } } }
96///
97/// #[async_trait]
98/// impl CanActivate for AuthGuard {
99/// fn resolve(registry: &ProviderRegistry) -> Self {
100/// Self { users: registry.get::<UserRepository>() }
101/// }
102/// async fn can_activate(&self, parts: &Parts) -> Result<(), GuardError> { /* ... */ }
103/// }
104/// ```
105#[async_trait]
106pub trait CanActivate: Default + Send + Sync + 'static {
107 /// Build the guard instance used for **every** request on routes declaring this guard.
108 ///
109 /// The default implementation returns [`Default::default()`] (a stateless guard).
110 /// Override this to construct a stateful guard from the application's
111 /// [`crate::ProviderRegistry`] (NestJS dependency-injected guards).
112 fn resolve(_registry: &crate::ProviderRegistry) -> Self
113 where
114 Self: Sized,
115 {
116 Self::default()
117 }
118
119 async fn can_activate(&self, parts: &Parts) -> Result<(), GuardError>;
120}