use actix_identity::{
error::{GetIdentityError, LoginError},
Identity, IdentityExt,
};
use actix_web::{
body::BoxBody,
error::ResponseError,
http::{header::ContentType, StatusCode},
HttpMessage, HttpRequest, HttpResponse, HttpResponseBuilder,
};
use serde::{de::DeserializeOwned, ser::SerializeStruct, Serialize, Serializer};
use serde_json::Error as SerdeError;
use thiserror::Error;
use super::Authenticate;
#[derive(Debug, Error)]
pub enum SessionError {
#[error("Unauthenticated")]
Unauthenticated,
#[error("Authentication failure: {0}")]
AuthenticationFailure(#[from] LoginError),
#[error("Corrupted data: {0}")]
ParseError(#[from] SerdeError),
}
impl From<GetIdentityError> for SessionError {
fn from(_error: GetIdentityError) -> Self {
Self::Unauthenticated
}
}
impl Serialize for SessionError {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_struct("Error", 2)?;
state.serialize_field("error", &self.to_string())?;
state.serialize_field("code", &self.status_code().as_u16())?;
state.end()
}
}
impl ResponseError for SessionError {
fn status_code(&self) -> StatusCode {
match self {
Self::Unauthenticated => StatusCode::UNAUTHORIZED,
Self::AuthenticationFailure(_) | Self::ParseError(_) => {
StatusCode::INTERNAL_SERVER_ERROR
}
}
}
fn error_response(&self) -> HttpResponse<BoxBody> {
let status_code = self.status_code();
HttpResponseBuilder::new(status_code)
.content_type(ContentType::json())
.json(self)
}
}
pub struct Session<T: Serialize + DeserializeOwned> {
data: T,
identity: Option<Identity>,
}
impl<T: Serialize + DeserializeOwned> Session<T> {
pub fn start(request: &HttpRequest, data: T) -> Result<Self, SessionError> {
let identity = Identity::login(&request.extensions(), serde_json::to_string(&data)?)?;
Ok(Self {
data,
identity: Some(identity),
})
}
pub fn force_stop(&mut self) {
if let Some(identity) = self.identity.take() {
identity.logout();
}
}
pub const fn is_stopped(&self) -> bool {
self.identity.is_none()
}
}
impl<T: Serialize + DeserializeOwned> Authenticate for Session<T> {
type Error = SessionError;
type Output = T;
fn authenticate(request: &HttpRequest) -> Result<Self, Self::Error> {
let identity = request.get_identity().map_err(SessionError::from)?;
let data = serde_json::from_str(identity.id()?.as_str())?;
Ok(Self {
data,
identity: Some(identity),
})
}
fn data(&self) -> &Self::Output {
&self.data
}
}
#[cfg(test)]
mod tests {
use actix_http::Request;
use actix_identity::IdentityMiddleware;
use actix_session::SessionMiddleware;
use actix_web::{
body::MessageBody,
cookie::Key,
dev::{Service, ServiceResponse},
get,
http::{Method, StatusCode},
post, test, App, HttpResponse, Responder,
};
use super::*;
use crate::{authentication::Authenticated, tests::session_store::MockSessionStore};
#[post("/start_session")]
async fn start_session(request: HttpRequest) -> impl Responder {
let status_code = Session::start(&request, "user_id".to_owned())
.map_or(StatusCode::INTERNAL_SERVER_ERROR, |_| StatusCode::OK);
HttpResponse::new(status_code)
}
#[get("/session_data")]
async fn session_data(session: Authenticated<Session<String>>) -> impl Responder {
HttpResponse::Ok().json(session.data())
}
#[post("/stop_session")]
async fn stop_session(mut session: Authenticated<Session<String>>) -> impl Responder {
session.force_stop();
HttpResponse::Ok()
}
async fn create_app(
) -> impl Service<Request, Response = ServiceResponse<impl MessageBody>, Error = actix_web::Error>
{
test::init_service(
App::new()
.wrap(IdentityMiddleware::default())
.wrap(SessionMiddleware::new(
MockSessionStore::default(),
Key::generate(),
))
.service(start_session)
.service(session_data)
.service(stop_session),
)
.await
}
#[actix_web::test]
async fn authentication_by_session() {
let app = create_app().await;
let request = test::TestRequest::post().uri("/start_session").to_request();
let result = test::call_service(&app, request).await;
assert!(result.status().is_success());
let cookies = result.response().cookies().collect::<Vec<_>>();
assert_eq!(cookies.len(), 1);
#[allow(clippy::indexing_slicing)]
let cookie = &cookies[0];
let tests = [
(Method::GET, "/session_data", StatusCode::OK),
(Method::POST, "/stop_session", StatusCode::OK),
(Method::GET, "/session_data", StatusCode::UNAUTHORIZED),
(Method::POST, "/stop_session", StatusCode::UNAUTHORIZED),
];
for (method, uri, status_code) in tests {
let request = test::TestRequest::default()
.method(method)
.uri(uri)
.cookie(cookie.clone())
.to_request();
let result = test::call_service(&app, request).await;
assert_eq!(result.status(), status_code, "Failed for uri: {uri}");
}
}
}