use std::borrow::Cow;
use std::sync::Arc;
use axum::extract::FromRequestParts;
use axum::http::request::Parts;
use axum::{async_trait, RequestPartsExt};
use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use cookie::time::OffsetDateTime;
use cookie::{Cookie, CookieJar, Expiration, Key, SameSite};
use super::extractors::Cookies;
use crate::model::auth::Authentication;
use crate::utils::apierror::ApiError;
#[derive(Debug, Clone)]
pub struct Token {
pub id: String,
pub secret: String,
}
impl Token {
fn try_parse(input: &str) -> Option<Token> {
let parts: Vec<&str> = input.trim().split_ascii_whitespace().collect();
if parts.len() == 2 && parts[0] == "Basic" {
let Ok(decoded) = BASE64_STANDARD.decode(parts[1]) else {
return None;
};
let Ok(decoded) = String::from_utf8(decoded) else {
return None;
};
let parts: Vec<&str> = decoded.split(':').collect();
if parts.len() == 2 {
Some(Token {
id: parts[0].to_string(),
secret: parts[1].to_string(),
})
} else {
None
}
} else {
None
}
}
}
pub trait AxumStateForCookies {
fn get_domain(&self) -> Cow<'static, str> {
Cow::Borrowed("localhost")
}
fn get_id_cookie_name(&self) -> Cow<'static, str> {
Cow::Borrowed("cenotelie-user")
}
fn get_cookie_key(&self) -> &Key;
}
pub struct AuthData {
cookie_domain: Cow<'static, str>,
cookie_id_name: Cow<'static, str>,
cookie_key: Key,
pub cookie_jar: CookieJar,
pub token: Option<Token>,
}
impl Default for AuthData {
fn default() -> Self {
Self {
cookie_domain: Cow::Borrowed("localhost"),
cookie_id_name: Cow::Borrowed("cratery"),
cookie_key: Key::from(&[0; 64]),
cookie_jar: CookieJar::default(),
token: None,
}
}
}
impl From<Token> for AuthData {
fn from(token: Token) -> Self {
Self {
cookie_domain: Cow::Borrowed("localhost"),
cookie_id_name: Cow::Borrowed("cratery"),
cookie_key: Key::from(&[0; 64]),
cookie_jar: CookieJar::default(),
token: Some(token),
}
}
}
#[async_trait]
impl<S> FromRequestParts<Arc<S>> for AuthData
where
S: AxumStateForCookies + Send + Sync,
{
type Rejection = ();
async fn from_request_parts(parts: &mut Parts, state: &Arc<S>) -> Result<Self, Self::Rejection> {
let cookie_key = state.get_cookie_key().clone();
let cookie_jar = parts.extract::<Cookies>().await?.0;
let token = if let Some(header) = parts.headers.get("authorization") {
header.to_str().ok().and_then(Token::try_parse)
} else {
None
};
Ok(AuthData {
cookie_domain: state.get_domain(),
cookie_id_name: state.get_id_cookie_name(),
cookie_key,
cookie_jar,
token,
})
}
}
impl AuthData {
fn build_cookie<'data>(
domain: &str,
name: Cow<'data, str>,
value: Cow<'data, str>,
is_delete_flag: bool,
) -> Cookie<'static> {
let is_local = domain == "localhost";
let mut builder = Cookie::build((name.into_owned(), value.into_owned()))
.domain(domain.to_string())
.path("/")
.same_site(SameSite::Strict)
.secure(!is_local)
.http_only(true);
if is_delete_flag {
builder = builder.expires(Expiration::DateTime(OffsetDateTime::UNIX_EPOCH));
}
builder.build()
}
fn make_private_cookie(&mut self, name: &str, cookie: Cookie<'static>) -> Cookie<'static> {
self.cookie_jar.private_mut(&self.cookie_key).add(cookie);
self.cookie_jar.get(name).unwrap().clone()
}
pub fn create_cookie(&mut self, name: &str, value: &str, is_private: bool) -> Cookie<'static> {
let cookie = AuthData::build_cookie(&self.cookie_domain, Cow::Borrowed(name), Cow::Borrowed(value), false);
if is_private {
self.make_private_cookie(name, cookie)
} else {
cookie
}
}
pub fn create_expired_cookie(&mut self, name: &str, is_private: bool) -> Cookie<'static> {
let cookie = AuthData::build_cookie(&self.cookie_domain, Cow::Borrowed(name), Cow::Borrowed(""), true);
if is_private {
self.make_private_cookie(name, cookie)
} else {
cookie
}
}
pub fn create_id_cookie(&mut self, value: &Authentication) -> Cookie<'static> {
self.create_cookie(&self.cookie_id_name.clone(), &serde_json::to_string(value).unwrap(), true)
}
pub fn create_expired_id_cookie(&mut self) -> Cookie<'static> {
self.create_expired_cookie(&self.cookie_id_name.clone(), true)
}
pub fn try_authenticate_cookie(&self) -> Result<Option<Authentication>, ApiError> {
Ok(self
.cookie_jar
.private(&self.cookie_key)
.get(&self.cookie_id_name)
.map(|cookie| serde_json::from_str(cookie.value()))
.transpose()?)
}
}