Skip to main content

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}