Skip to main content

ferro_rs/auth/
middleware.rs

1//! Authentication middleware
2
3use crate::http::{HttpResponse, Response};
4use crate::middleware::{Middleware, Next};
5use crate::Request;
6use async_trait::async_trait;
7
8use super::guard::Auth;
9
10/// Authentication middleware
11///
12/// Protects routes that require authentication. Unauthenticated requests
13/// are either redirected to a login page or receive a 401 response.
14///
15/// # Example
16///
17/// ```rust,ignore
18/// use ferro_rs::{AuthMiddleware, group, get};
19///
20/// // API routes - return 401 for unauthenticated
21/// group!("/api")
22///     .middleware(AuthMiddleware::new())
23///     .routes([...]);
24///
25/// // Web routes - redirect to login
26/// group!("/dashboard")
27///     .middleware(AuthMiddleware::redirect_to("/login"))
28///     .routes([...]);
29/// ```
30pub struct AuthMiddleware {
31    /// Path to redirect to if not authenticated (None = return 401)
32    redirect_to: Option<String>,
33}
34
35impl AuthMiddleware {
36    /// Create middleware that returns 401 Unauthorized if not authenticated
37    ///
38    /// Best for API routes.
39    pub fn new() -> Self {
40        Self { redirect_to: None }
41    }
42
43    /// Create middleware that redirects to a login page if not authenticated
44    ///
45    /// Best for web routes.
46    ///
47    /// # Example
48    ///
49    /// ```rust,ignore
50    /// AuthMiddleware::redirect_to("/login")
51    /// ```
52    pub fn redirect_to(path: impl Into<String>) -> Self {
53        Self {
54            redirect_to: Some(path.into()),
55        }
56    }
57}
58
59impl Default for AuthMiddleware {
60    fn default() -> Self {
61        Self::new()
62    }
63}
64
65#[async_trait]
66impl Middleware for AuthMiddleware {
67    async fn handle(&self, request: Request, next: Next) -> Response {
68        if Auth::check() {
69            // User is authenticated, proceed
70            return next(request).await;
71        }
72
73        // User is not authenticated
74        match &self.redirect_to {
75            Some(path) => {
76                // For Inertia requests, return 409 with redirect location
77                // This tells Inertia to do a full page visit to the login page
78                if request.is_inertia() {
79                    Err(HttpResponse::text("")
80                        .status(409)
81                        .header("X-Inertia-Location", path.clone()))
82                } else {
83                    // Regular redirect for non-Inertia requests
84                    Err(HttpResponse::new()
85                        .status(302)
86                        .header("Location", path.clone()))
87                }
88            }
89            None => {
90                // Return 401 Unauthorized
91                Err(HttpResponse::json(serde_json::json!({
92                    "message": "Unauthenticated."
93                }))
94                .status(401))
95            }
96        }
97    }
98}
99
100/// Guest middleware
101///
102/// Protects routes that should only be accessible to guests (non-authenticated users).
103/// Useful for login and registration pages.
104///
105/// # Example
106///
107/// ```rust,ignore
108/// use ferro_rs::{GuestMiddleware, group, get};
109///
110/// group!("/")
111///     .middleware(GuestMiddleware::redirect_to("/dashboard"))
112///     .routes([
113///         get!("/login", auth::show_login),
114///         get!("/register", auth::show_register),
115///     ]);
116/// ```
117pub struct GuestMiddleware {
118    /// Path to redirect to if authenticated
119    redirect_to: String,
120}
121
122impl GuestMiddleware {
123    /// Create middleware that redirects authenticated users
124    ///
125    /// # Arguments
126    ///
127    /// * `redirect_to` - Path to redirect authenticated users to
128    pub fn redirect_to(path: impl Into<String>) -> Self {
129        Self {
130            redirect_to: path.into(),
131        }
132    }
133
134    /// Alias for `redirect_to` with a default path
135    pub fn new() -> Self {
136        Self::redirect_to("/")
137    }
138}
139
140impl Default for GuestMiddleware {
141    fn default() -> Self {
142        Self::new()
143    }
144}
145
146#[async_trait]
147impl Middleware for GuestMiddleware {
148    async fn handle(&self, request: Request, next: Next) -> Response {
149        if Auth::guest() {
150            // User is a guest, proceed
151            return next(request).await;
152        }
153
154        // User is authenticated, redirect them away
155        if request.is_inertia() {
156            // For Inertia requests, return 409 with redirect location
157            Err(HttpResponse::text("")
158                .status(409)
159                .header("X-Inertia-Location", &self.redirect_to))
160        } else {
161            // Regular redirect for non-Inertia requests
162            Err(HttpResponse::new()
163                .status(302)
164                .header("Location", &self.redirect_to))
165        }
166    }
167}