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}