actix_web_admin/auth/
middleware.rs1use actix_session::SessionExt;
2use actix_web::body::BoxBody;
3use actix_web::{
4 dev::{Service, ServiceRequest, ServiceResponse, Transform},
5 Error, HttpResponse,
6};
7use std::future::{ready, Ready};
8use std::pin::Pin;
9use std::rc::Rc;
10use std::task::{Context, Poll};
11
12pub struct RequireAuth {
17 admin_prefix: String,
18}
19
20impl RequireAuth {
21 pub fn new(admin_prefix: &str) -> Self {
22 RequireAuth {
23 admin_prefix: admin_prefix.trim_end_matches('/').to_string(),
24 }
25 }
26}
27
28impl<S> Transform<S, ServiceRequest> for RequireAuth
29where
30 S: Service<ServiceRequest, Response = ServiceResponse<BoxBody>, Error = Error> + 'static,
31 S::Future: 'static,
32{
33 type Response = ServiceResponse<BoxBody>;
34 type Error = Error;
35 type Transform = RequireAuthMiddleware<S>;
36 type InitError = ();
37 type Future = Ready<Result<Self::Transform, Self::InitError>>;
38
39 fn new_transform(&self, service: S) -> Self::Future {
40 ready(Ok(RequireAuthMiddleware {
41 service: Rc::new(service),
42 login_url: format!("{}/login", self.admin_prefix),
43 admin_prefix: self.admin_prefix.clone(),
44 }))
45 }
46}
47
48pub struct RequireAuthMiddleware<S> {
49 service: Rc<S>,
50 login_url: String,
51 admin_prefix: String,
52}
53
54impl<S> Service<ServiceRequest> for RequireAuthMiddleware<S>
55where
56 S: Service<ServiceRequest, Response = ServiceResponse<BoxBody>, Error = Error> + 'static,
57 S::Future: 'static,
58{
59 type Response = ServiceResponse<BoxBody>;
60 type Error = Error;
61 type Future = Pin<Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>>>>;
62
63 fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
64 self.service.poll_ready(cx)
65 }
66
67 fn call(&self, req: ServiceRequest) -> Self::Future {
68 let svc = self.service.clone();
69 let path = req.path().to_string();
70 let login_url = self.login_url.clone();
71 let admin_prefix = self.admin_prefix.clone();
72 let is_public = path == format!("{admin_prefix}/login")
73 || path == format!("{admin_prefix}/login/")
74 || path == format!("{admin_prefix}/logout")
75 || path == format!("{admin_prefix}/logout/");
76
77 if is_public {
78 return Box::pin(async move { svc.call(req).await });
79 }
80
81 let session = req.get_session();
82 let is_auth = session
83 .get::<String>("admin_user")
84 .ok()
85 .flatten()
86 .is_some();
87
88 if is_auth {
89 Box::pin(async move { svc.call(req).await })
90 } else {
91 let response = HttpResponse::Found()
92 .insert_header(("Location", login_url))
93 .finish();
94 Box::pin(async move { Ok(req.into_response(response)) })
95 }
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use actix_web::{test, web, App};
103 use actix_session::{storage::CookieSessionStore, SessionMiddleware};
104 use actix_web::cookie::Key;
105
106 async fn test_app() -> impl actix_web::dev::Service<
107 actix_http::Request,
108 Response = actix_web::dev::ServiceResponse,
109 Error = actix_web::Error,
110 > {
111 test::init_service(
112 App::new()
113 .wrap(SessionMiddleware::new(
114 CookieSessionStore::default(),
115 Key::generate(),
116 ))
117 .service(
118 web::scope("/admin")
119 .wrap(RequireAuth::new("/admin"))
120 .route("/login", web::get().to(|| async { HttpResponse::Ok().body("login page") }))
121 .route("/dashboard", web::get().to(|| async { HttpResponse::Ok().body("dashboard") })),
122 ),
123 )
124 .await
125 }
126
127 #[actix_web::test]
128 async fn test_public_login_page() {
129 let app = test_app().await;
130 let req = test::TestRequest::get().uri("/admin/login").to_request();
131 let resp = test::call_service(&app, req).await;
132 assert!(resp.status().is_success(), "login page should be public");
133 }
134
135 #[actix_web::test]
136 async fn test_protected_route_redirects() {
137 let app = test_app().await;
138 let req = test::TestRequest::get()
139 .uri("/admin/dashboard")
140 .to_request();
141 let resp = test::call_service(&app, req).await;
142 assert_eq!(
143 resp.status(),
144 302,
145 "unauthenticated requests should redirect"
146 );
147 let location = resp.response().headers().get("Location");
148 assert!(location.is_some(), "should have Location header");
149 assert_eq!(
150 location.unwrap().to_str().unwrap(),
151 "/admin/login",
152 "should redirect to login"
153 );
154 }
155
156}