use sea_orm::{ ActiveModelTrait, ConnectionTrait, DatabaseConnection, EntityTrait, QueryFilter, Set, }; use time::OffsetDateTime; use uuid::Uuid; use crate::claims::sha256_hex; use crate::error::JwtError; use crate::metrics; 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 result: Result = async { let model: ActiveModel = ActiveModel::builder() .set_id(Uuid::now_v7()) .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) } .await; metrics::record_session_op("create_session", &result); result } pub async fn find_session_by_hash( db: &DatabaseConnection, token_hash: &str, ) -> Result, JwtError> { use db::user::user_sessions::Entity as E; let result: Result, JwtError> = async { Ok(E::find() .filter(E::COLUMN.token_hash.eq(token_hash)) .one(db) .await?) } .await; metrics::record_session_op("find_session_by_hash", &result); result } pub async fn revoke_session(db: &DatabaseConnection, id: Uuid) -> Result<(), JwtError> { use db::user::user_sessions::{ActiveModel, Entity as E}; let result: Result<(), JwtError> = async { 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(()) } .await; metrics::record_session_op("revoke_session", &result); result } 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 result: Result<(), JwtError> = async { 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(()) } .await; metrics::record_session_op("mark_replaced", &result); result } pub fn hash_token(token: &str) -> String { sha256_hex(token.as_bytes()) } pub async fn delete_session(db: &DatabaseConnection, id: Uuid) -> Result<(), JwtError> { use db::user::user_sessions::Entity as E; let result: Result<(), JwtError> = async { E::delete_by_id(id).exec(db).await?; Ok(()) } .await; metrics::record_session_op("delete_session", &result); result } pub async fn delete_sessions_by_family( db: &DatabaseConnection, family_id: Uuid, ) -> Result<(), JwtError> { use db::user::user_sessions::Entity as E; let result: Result<(), JwtError> = async { E::delete_many() .filter(E::COLUMN.family_id.eq(family_id)) .exec(db) .await?; Ok(()) } .await; metrics::record_session_op("delete_sessions_by_family", &result); result } pub async fn delete_sessions_by_user( db: &C, user_id: Uuid, ) -> Result<(), JwtError> { use db::user::user_sessions::Entity; let result: Result<(), JwtError> = async { Entity::delete_many() .filter(Entity::COLUMN.user_id.eq(user_id)) .exec(db) .await?; Ok(()) } .await; metrics::record_session_op("delete_sessions_by_user", &result); result }