use sea_orm::{ActiveModelTrait, ConnectionTrait, DatabaseConnection, EntityTrait, QueryFilter, Set}; use time::OffsetDateTime; use uuid::Uuid; use crate::claims::sha256_hex; use crate::error::JwtError; pub struct NewSession { pub user_id: Uuid, pub token_hash: String, pub family_id: Uuid, pub parent_refresh_id: Option, pub expires_at: OffsetDateTime, pub grace_expires_at: Option, pub signing_key_id: Option, pub app_id: Option, } pub async fn create_session( db: &DatabaseConnection, new: NewSession, ) -> Result { use db::user::user_sessions::ActiveModel; let model: ActiveModel = ActiveModel::builder() .set_id(Uuid::new_v4()) .set_user_id(new.user_id) .set_token_hash(new.token_hash) .set_user_agent_hash(None) .set_client_ip_hash(None) .set_kind(db::user::user_sessions::SessionKind::BearerAccess) .set_family_id(new.family_id) .set_parent_refresh_id(new.parent_refresh_id) .set_is_revoked(false) .set_revoked_at(None) .set_created_at(OffsetDateTime::now_utc()) .set_expires_at(new.expires_at) .set_replaced_at(None) .set_grace_expires_at(new.grace_expires_at) .set_updated_at(OffsetDateTime::now_utc()) .set_replaced_by(None) .set_signing_key_id(new.signing_key_id) .set_app_id(new.app_id) .into(); model.insert(db).await.map_err(JwtError::from) } pub async fn find_session_by_hash( db: &DatabaseConnection, token_hash: &str, ) -> Result, JwtError> { use db::user::user_sessions::Entity as E; Ok(E::find() .filter(E::COLUMN.token_hash.eq(token_hash)) .one(db) .await?) } pub async fn revoke_session(db: &DatabaseConnection, id: Uuid) -> Result<(), JwtError> { use db::user::user_sessions::{ActiveModel, Entity as E}; let model = E::find_by_id(id).one(db).await?; if let Some(m) = model { let mut am: ActiveModel = m.into(); am.is_revoked = Set(true); am.revoked_at = Set(Some(OffsetDateTime::now_utc())); am.updated_at = Set(OffsetDateTime::now_utc()); am.update(db).await?; } Ok(()) } pub async fn mark_replaced( db: &DatabaseConnection, old_id: Uuid, new_id: Uuid, grace_expires_at: OffsetDateTime, ) -> Result<(), JwtError> { use db::user::user_sessions::{ActiveModel, Entity as E}; let model = E::find_by_id(old_id).one(db).await?; if let Some(m) = model { let mut am: ActiveModel = m.into(); am.replaced_at = Set(Some(OffsetDateTime::now_utc())); am.replaced_by = Set(Some(new_id)); am.grace_expires_at = Set(Some(grace_expires_at)); am.updated_at = Set(OffsetDateTime::now_utc()); am.update(db).await?; } Ok(()) } pub fn hash_token(token: &str) -> String { sha256_hex(token.as_bytes()) } /// Erase the persistence lookup for a logout (delete the session row). pub async fn delete_session(db: &DatabaseConnection, id: Uuid) -> Result<(), JwtError> { use db::user::user_sessions::Entity as E; E::delete_by_id(id).exec(db).await?; Ok(()) } pub async fn delete_sessions_by_family( db: &DatabaseConnection, family_id: Uuid, ) -> Result<(), JwtError> { use db::user::user_sessions::Entity as E; E::delete_many() .filter(E::COLUMN.family_id.eq(family_id)) .exec(db) .await?; Ok(()) } /// Revoke every active session for a user (used after password reset, etc.). /// Accepts any sea_orm connection so the caller can pass either a pooled /// `DatabaseConnection` or an open `DatabaseTransaction` to keep the /// revocation atomic with the password update. pub async fn delete_sessions_by_user( db: &C, user_id: Uuid, ) -> Result<(), JwtError> { use db::user::user_sessions::Entity; Entity::delete_many() .filter(Entity::COLUMN.user_id.eq(user_id)) .exec(db) .await?; Ok(()) }