use std::{sync::Arc, time::Duration};
use jiff::Timestamp;
use topcoat::{
context::Cx,
session::{self, TokenHash},
};
use uuid::Uuid;
use super::{DynAuthenticator, PanelUser, SignedIn, infrastructure_failure};
use crate::panel::state::{PanelState, current};
pub const SESSION_LIFETIME: Duration = Duration::from_hours(24 * 7);
#[derive(Debug, Clone, toasty::Model)]
pub struct AuthSession {
#[key]
pub token_hash: String,
#[index]
pub user_id: String,
pub panel: String,
pub tenant: Option<Uuid>,
#[index]
pub expires_at: Timestamp,
pub created_at: Timestamp,
}
pub(super) fn token_key(hash: &TokenHash) -> String {
use std::fmt::Write as _;
let mut key = String::with_capacity(64);
for byte in hash.iter() {
write!(key, "{byte:02x}").expect("writing to a String cannot fail");
}
key
}
pub(super) async fn record(
cx: &Cx,
session: &session::Session,
user: &dyn PanelUser,
panel: &PanelState,
) -> topcoat::Result<()> {
let mut db = crate::db::db(cx);
toasty::create!(AuthSession {
token_hash: token_key(&session.token_hash),
user_id: user.user_id(),
panel: panel.prefix.clone(),
tenant: None,
expires_at: Timestamp::try_from(session.expires_at).map_err(topcoat::Error::from)?,
created_at: Timestamp::now(),
})
.exec(&mut db)
.await
.map_err(infrastructure_failure)?;
Ok(())
}
pub(super) async fn select_tenant(cx: &Cx, tenant: Uuid) -> topcoat::Result<()> {
let Some(hash) = session::token_hash(cx).await? else {
return Err(topcoat::router::error::forbidden().into());
};
let mut db = crate::db::db(cx);
AuthSession::filter(AuthSession::fields().token_hash().eq(token_key(&hash)))
.update()
.tenant(Some(tenant))
.exec(&mut db)
.await
.map_err(infrastructure_failure)?;
Ok(())
}
pub(super) async fn delete_session(cx: &Cx, hash: &TokenHash) -> topcoat::Result<()> {
delete_session_row(cx, &token_key(hash)).await
}
async fn delete_session_row(cx: &Cx, key: &str) -> topcoat::Result<()> {
let mut db = crate::db::db(cx);
AuthSession::filter(AuthSession::fields().token_hash().eq(key.to_string()))
.delete()
.exec(&mut db)
.await
.map_err(infrastructure_failure)?;
Ok(())
}
pub async fn revoke_sessions_for_user(cx: &Cx, user_id: &str) -> topcoat::Result<()> {
let mut db = crate::db::db(cx);
let user = AuthSession::fields().user_id().eq(user_id.to_string());
let sessions = match current(cx) {
Some(panel) => {
AuthSession::filter(user.and(AuthSession::fields().panel().eq(panel.prefix.clone())))
}
None => AuthSession::filter(user),
};
sessions
.delete()
.exec(&mut db)
.await
.map_err(infrastructure_failure)?;
Ok(())
}
pub(super) const SESSION_SWEEP_BATCH: usize = 500;
pub(super) async fn sweep_expired_sessions(cx: &Cx) -> topcoat::Result<()> {
let now = Timestamp::now();
let mut db = crate::db::db(cx);
let expired: Vec<String> = AuthSession::filter(AuthSession::fields().expires_at().le(now))
.limit(SESSION_SWEEP_BATCH)
.exec(&mut db)
.await
.map_err(infrastructure_failure)?
.into_iter()
.map(|row| row.token_hash)
.collect();
if expired.is_empty() {
return Ok(());
}
AuthSession::filter(
AuthSession::fields()
.token_hash()
.in_list(expired)
.and(AuthSession::fields().expires_at().le(now)),
)
.delete()
.exec(&mut db)
.await
.map_err(infrastructure_failure)?;
Ok(())
}
pub(super) async fn session_row(cx: &Cx) -> topcoat::Result<Option<AuthSession>> {
let Some(hash) = session::token_hash(cx).await? else {
return Ok(None);
};
let key = token_key(&hash);
let mut db = crate::db::db(cx);
let row = AuthSession::filter(AuthSession::fields().token_hash().eq(key))
.first()
.exec(&mut db)
.await
.map_err(infrastructure_failure)?;
let Some(row) = row else {
return Ok(None);
};
if row.expires_at <= Timestamp::now() {
delete_session_row(cx, &row.token_hash).await?;
return Ok(None);
}
Ok(Some(row))
}
pub(super) async fn session_user(
cx: &Cx,
row: AuthSession,
panel: &Arc<PanelState>,
authenticator: &dyn DynAuthenticator,
) -> topcoat::Result<Option<SignedIn>> {
match authenticator
.find_by_id(cx, &row.user_id)
.await
.map_err(infrastructure_failure)?
{
Some(user) => Ok(Some(SignedIn {
user,
panel: Arc::clone(panel),
tenant: row.tenant,
})),
None => {
delete_session_row(cx, &row.token_hash).await?;
Ok(None)
}
}
}
pub(super) async fn resolve(
cx: &Cx,
panel: &Arc<PanelState>,
authenticator: &dyn DynAuthenticator,
) -> topcoat::Result<Option<SignedIn>> {
match session_row(cx).await? {
Some(row) if row.panel == panel.prefix => session_user(cx, row, panel, authenticator).await,
_ => Ok(None),
}
}