use crate::auth::permissions::{Groupe, Permission};
use crate::auth::user_trait::RuniqueUser;
use crate::middleware::security::csrf::rotate_csrf_token;
use crate::middleware::session::session_db::RuniqueSessionStore;
use crate::utils::config::TraceResult;
use crate::utils::constante::session_key::session::{SESSION_ACTIVE_KEY, SESSION_USER_ID_KEY};
use crate::utils::pk::Pk;
use serde::{Deserialize, Serialize};
use tower_sessions::Session;
const DEFAULT_AUTH_SESSION_TTL_SECS: i64 = 86_400;
static AUTH_SESSION_TTL_SECS: std::sync::OnceLock<i64> = std::sync::OnceLock::new();
pub fn set_auth_session_ttl_secs(secs: i64) {
match AUTH_SESSION_TTL_SECS.get() {
None => {
if AUTH_SESSION_TTL_SECS.set(secs).is_err() {
tracing::warn!("auth session TTL set raced at build — keeping the first value");
}
}
Some(&existing) if existing != secs => {
tracing::warn!(
existing,
attempted = secs,
"auth session TTL already set to a different value (multi-app in one process?) — keeping the first"
);
}
Some(_) => {}
}
}
fn resolve_ttl_secs(configured: Option<i64>) -> i64 {
configured.unwrap_or(DEFAULT_AUTH_SESSION_TTL_SECS)
}
fn auth_session_ttl_secs() -> i64 {
resolve_ttl_secs(AUTH_SESSION_TTL_SECS.get().copied())
}
#[cfg(test)]
mod ttl_tests {
use super::{DEFAULT_AUTH_SESSION_TTL_SECS, resolve_ttl_secs};
#[test]
fn ttl_uses_builder_value_else_default() {
assert_eq!(resolve_ttl_secs(Some(172_800)), 172_800);
assert_eq!(resolve_ttl_secs(None), DEFAULT_AUTH_SESSION_TTL_SECS);
assert_eq!(DEFAULT_AUTH_SESSION_TTL_SECS, 86_400);
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CurrentUser {
pub id: Pk,
pub username: String,
pub is_staff: bool,
pub is_superuser: bool,
pub groupes: Vec<Groupe>,
}
impl CurrentUser {
#[must_use]
pub fn permission_for(&self, resource_key: &str) -> Option<Permission> {
self.permissions_effectives()
.into_iter()
.find(|p| p.resource_key == resource_key)
}
pub fn permissions_effectives(&self) -> Vec<Permission> {
let mut agg: std::collections::HashMap<String, Permission> =
std::collections::HashMap::new();
for groupe in &self.groupes {
for perm in &groupe.permissions {
agg.entry(perm.resource_key.clone())
.or_insert_with(|| Permission::zeroed(perm.resource_key.clone()))
.merge_from(perm);
}
}
agg.into_values().collect()
}
#[must_use]
pub fn can_access_resource(&self, resource_key: &str) -> bool {
self.is_superuser
|| self
.permission_for(resource_key)
.is_some_and(|p| p.can_read)
}
#[must_use]
pub fn can_access_admin(&self) -> bool {
self.is_staff || self.is_superuser
}
}
pub async fn is_authenticated(session: &Session) -> bool {
session
.get::<Pk>(SESSION_USER_ID_KEY)
.await
.ok()
.flatten()
.is_some()
}
pub(crate) async fn get_user_id(session: &Session) -> Option<Pk> {
session.get::<Pk>(SESSION_USER_ID_KEY).await.ok().flatten()
}
pub async fn login(
session: &Session,
user: &impl RuniqueUser,
db_store: Option<&RuniqueSessionStore>,
exclusive: bool,
) -> Result<(), LoginError> {
if !user.can_sign_in() {
return Err(LoginError::CannotSignIn);
}
let user_id = user.user_id();
let username = user.username();
let is_superuser = user.is_superuser();
let existing_id: Option<_> = session.get::<Pk>(SESSION_USER_ID_KEY).await.ok().flatten();
let is_privilege_elevation = existing_id != Some(user_id);
if let Some(existing) = existing_id
&& existing != user_id
{
logout(session, db_store).await.trace(
crate::utils::runique_log::get_log()
.session
.as_ref()
.and_then(|s| s.store),
"pre-login logout of previous session",
);
}
if is_privilege_elevation {
session.cycle_id().await.trace_or(
crate::utils::runique_log::get_log()
.session
.as_ref()
.and_then(|s| s.store),
tracing::Level::WARN,
"cycle session id (session fixation protection)",
);
rotate_csrf_token(session).await?;
}
if let Some(level) = crate::utils::runique_log::get_log()
.auth
.as_ref()
.and_then(|a| a.login)
{
crate::runique_log!(
level,
user_id = %user_id,
username = %username,
is_superuser,
exclusive,
db_persist = db_store.is_some(),
"login"
);
}
session.insert(SESSION_USER_ID_KEY, user_id).await?;
let ttl_secs = auth_session_ttl_secs();
session.set_expiry(Some(tower_sessions::Expiry::OnInactivity(
tower_sessions::cookie::time::Duration::seconds(ttl_secs),
)));
if let Some(store) = db_store {
session.save().await?;
let Some(cookie_id) = session.id().map(|id| id.to_string()) else {
crate::runique_log!(
tracing::Level::WARN,
user_id = %user_id,
"session id unavailable after save — skipping DB persistence"
);
return Ok(());
};
let session_id = uuid::Uuid::new_v4().to_string();
let expires_at = chrono::Utc::now()
.naive_utc()
.checked_add_signed(chrono::Duration::seconds(ttl_secs))
.unwrap_or_else(|| chrono::Utc::now().naive_utc());
store
.create(&cookie_id, user_id, &session_id, expires_at)
.await
.trace_or(
crate::utils::runique_log::get_log()
.session
.as_ref()
.and_then(|s| s.store),
tracing::Level::WARN,
"persist session to DB",
);
if exclusive {
store
.invalidate_other_sessions(user_id, &cookie_id)
.await
.trace_or(
crate::utils::runique_log::get_log()
.session
.as_ref()
.and_then(|s| s.exclusive_login),
tracing::Level::WARN,
"invalidate other sessions (exclusive login)",
);
}
}
Ok(())
}
#[derive(Debug)]
pub enum LoginError {
CannotSignIn,
Session(tower_sessions::session::Error),
}
impl std::fmt::Display for LoginError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::CannotSignIn => f.write_str(&crate::utils::trad::t("auth.login_cannot_sign_in")),
Self::Session(e) => {
f.write_str(&crate::utils::trad::tf("auth.login_session_failed", &[e]))
}
}
}
}
impl std::error::Error for LoginError {}
impl From<tower_sessions::session::Error> for LoginError {
fn from(e: tower_sessions::session::Error) -> Self {
Self::Session(e)
}
}
pub async fn logout(
session: &Session,
db_store: Option<&RuniqueSessionStore>,
) -> Result<(), tower_sessions::session::Error> {
if session
.get::<Pk>(SESSION_USER_ID_KEY)
.await
.ok()
.flatten()
.is_none()
{
return Ok(());
}
if let Some(store) = db_store
&& let Some(cookie_id) = session.id().map(|id| id.to_string())
{
store.delete(&cookie_id).await.trace(
crate::utils::runique_log::get_log()
.session
.as_ref()
.and_then(|s| s.store),
"delete session from DB on logout",
);
}
session.flush().await?;
session.cycle_id().await
}
pub async fn protect_session(
session: &Session,
duration_secs: i64,
) -> Result<(), tower_sessions::session::Error> {
let protect_until = chrono::Utc::now().timestamp().saturating_add(duration_secs);
session.insert(SESSION_ACTIVE_KEY, protect_until).await
}
pub async fn unprotect_session(session: &Session) -> Result<(), tower_sessions::session::Error> {
session.remove::<i64>(SESSION_ACTIVE_KEY).await?;
Ok(())
}