5ef202e
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<Uuid>,
pub expires_at: OffsetDateTime,
pub grace_expires_at: Option<OffsetDateTime>,
pub signing_key_id: Option<Uuid>,
pub app_id: Option<String>,
}
pub async fn create_session(
db: &DatabaseConnection,
new: NewSession,
) -> Result<db::user::user_sessions::Model, JwtError> {
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<Option<db::user::user_sessions::Model>, 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<C: ConnectionTrait>(
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(())
}