Skip to main content

toro_auth_core/
session.rs

1use 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}