toro_auth_core/
session.rs1use actix_web::{
2 FromRequest, HttpResponse, Responder,
3 cookie::{
4 Cookie,
5 time::{Duration, OffsetDateTime},
6 },
7 http::StatusCode,
8 web::{Data, Json, ServiceConfig, get, post},
9};
10use async_trait::async_trait;
11use serde::{Deserialize, Serialize};
12use std::{marker::PhantomData, pin::Pin};
13
14use crate::{IntoPublic, ObjectId};
15
16#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
17pub struct Session<T> {
18 pub id: String,
19 pub user_id: String,
20 #[serde(skip_serializing_if = "Option::is_none")]
21 _mapped: Option<PhantomData<T>>,
22}
23
24impl<T> Session<T> {
25 pub fn new(id: String, user_id: String) -> Self {
26 Self {
27 id,
28 user_id,
29 _mapped: None,
30 }
31 }
32}
33
34#[derive(Debug)]
35pub enum SessionError {
36 InvalidOrMissingSession,
37 InternalServerError,
38 ServiceUnavailable,
39 InvalidLogin,
40}
41
42impl std::fmt::Display for SessionError {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 write!(f, "{:#?}", self)
45 }
46}
47
48impl From<SessionError> for HttpResponse {
49 fn from(value: SessionError) -> Self {
50 match value {
51 SessionError::InternalServerError => HttpResponse::InternalServerError().finish(),
52 SessionError::InvalidOrMissingSession | SessionError::InvalidLogin => {
53 HttpResponse::Unauthorized().finish()
54 }
55 SessionError::ServiceUnavailable => HttpResponse::ServiceUnavailable().finish(),
56 }
57 }
58}
59
60impl actix_web::error::ResponseError for SessionError {
61 fn status_code(&self) -> StatusCode {
62 match self {
63 SessionError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR,
64 SessionError::InvalidOrMissingSession | SessionError::InvalidLogin => {
65 StatusCode::UNAUTHORIZED
66 }
67 SessionError::ServiceUnavailable => StatusCode::SERVICE_UNAVAILABLE,
68 }
69 }
70
71 fn error_response(&self) -> HttpResponse<actix_web::body::BoxBody> {
72 HttpResponse::build(self.status_code()).finish()
73 }
74}
75
76#[derive(Serialize, Deserialize)]
77pub struct LoginRequest {
78 username: String,
79 password: String,
80}
81
82pub struct SessionRes<T> {
83 pub inner: T,
84}
85
86#[derive(Clone)]
87pub struct SessionProvider<T>
88where
89 T: IntoPublic
90 + ObjectId
91 + Serialize
92 + for<'de> Deserialize<'de>
93 + Clone
94 + Send
95 + Sync
96 + 'static,
97{
98 login_path: String,
99 validate_path: String,
100 backend: Data<Box<dyn SessionBackend<T>>>,
101}
102
103impl<
104 T: IntoPublic + ObjectId + Serialize + for<'de> Deserialize<'de> + Clone + Send + Sync + 'static,
105> SessionProvider<T>
106{
107 pub fn default_with_backend(backend: Data<Box<dyn SessionBackend<T>>>) -> Self {
108 Self {
109 login_path: String::from("session/login"),
110 validate_path: String::from("session/validate"),
111 backend,
112 }
113 }
114
115 pub fn configure(&self, cfg: &mut ServiceConfig) {
116 let data = Data::new(self.clone());
117 cfg.app_data(data.clone())
118 .route(&self.login_path, post().to(login::<T>))
119 .route(&self.validate_path, get().to(validate::<T>));
120 }
121
122 pub async fn validate(&self, session_id: String) -> Result<T, SessionError> {
123 self.backend.validate(session_id).await
124 }
125
126 pub async fn login(
127 &self,
128 username: String,
129 password: String,
130 ) -> Result<Session<T>, SessionError> {
131 self.backend.login(username, password).await
132 }
133}
134
135async fn validate<
136 T: IntoPublic + ObjectId + Serialize + for<'de> Deserialize<'de> + Clone + Send + Sync + 'static,
137>(
138 session: SessionRes<T>,
139) -> impl Responder {
140 HttpResponse::Ok().json(session.inner.clone().into_public())
141}
142
143async fn login<
144 T: IntoPublic + ObjectId + Serialize + for<'de> Deserialize<'de> + Clone + Send + Sync + 'static,
145>(
146 session_provider: Data<SessionProvider<T>>,
147 request: Json<LoginRequest>,
148) -> Result<impl Responder, SessionError> {
149 let request = request.0;
150 let session = session_provider
151 .login(request.username, request.password)
152 .await?;
153
154 let session_cookie = Cookie::build("sessionId", session.id)
155 .path("/")
156 .expires(OffsetDateTime::now_utc().checked_add(Duration::minutes(10)))
157 .finish();
158 Ok(HttpResponse::Ok().cookie(session_cookie).finish())
159}
160
161impl<
162 T: IntoPublic + ObjectId + Serialize + for<'de> Deserialize<'de> + Clone + Send + Sync + 'static,
163> FromRequest for SessionRes<T>
164{
165 type Error = SessionError;
166 type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;
167
168 fn from_request(req: &actix_web::HttpRequest, _: &mut actix_web::dev::Payload) -> Self::Future {
169 let req = req.clone();
170 Box::pin(async move {
171 let Some(session_id) = req.cookie("sessionId") else {
172 return Err(SessionError::InvalidOrMissingSession);
173 };
174
175 let Some(session_provider) = req.app_data::<Data<SessionProvider<T>>>() else {
176 return Err(SessionError::InternalServerError);
177 };
178
179 let res = session_provider.validate(session_id.value().into()).await?;
180
181 Ok(SessionRes { inner: res })
182 })
183 }
184}
185
186#[async_trait]
187pub trait SessionBackend<T: ObjectId + Serialize + for<'de> Deserialize<'de>>: Send + Sync {
188 async fn validate(&self, session_id: String) -> Result<T, SessionError>;
189 async fn login(&self, username: String, password: String) -> Result<Session<T>, SessionError>;
190}