Skip to content
Clone
8fefadc
152 lines 4.9 KB Rust
use models::users::user_sessions::{self, SessionKind};
use sea_orm::sea_query::Expr;
use sea_orm::{
ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect, Set,
};
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use utoipa::{IntoParams, ToSchema};
use uuid::Uuid;
use crate::errors::UserError;
use crate::services::service::UserService;
use crate::share::page::{DEFAULT_PAGE_LIMIT, MAX_PAGE_LIMIT};
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct SessionDto {
pub id: Uuid,
pub kind: SessionKind,
pub device_id: Option<Uuid>,
pub user_agent_hash: Option<String>,
pub client_ip_hash: Option<String>,
pub last_seen_at: OffsetDateTime,
pub created_at: OffsetDateTime,
pub expires_at: OffsetDateTime,
pub is_revoked: bool,
pub revoked_at: Option<OffsetDateTime>,
pub is_current: bool,
}
impl SessionDto {
fn build(m: user_sessions::Model, current_token: Option<&str>) -> Self {
let is_current = current_token.is_some_and(|t| t == m.token_hash);
Self {
id: m.id,
kind: m.kind,
device_id: m.device_id,
user_agent_hash: m.user_agent_hash,
client_ip_hash: m.client_ip_hash,
last_seen_at: m.last_seen_at,
created_at: m.created_at,
expires_at: m.expires_at,
is_revoked: m.is_revoked,
revoked_at: m.revoked_at,
is_current,
}
}
}
#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
pub struct ListSessionsQuery {
pub kind: Option<SessionKind>,
pub include_revoked: Option<bool>,
pub limit: Option<u64>,
pub offset: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct CountResp {
pub count: i64,
}
impl UserService {
pub async fn list_sessions(
&self,
uid: Uuid,
q: ListSessionsQuery,
current_token: Option<&str>,
) -> Result<Vec<SessionDto>, UserError> {
let mut sel = user_sessions::Entity::find()
.filter(user_sessions::Column::UserId.eq(uid))
.order_by_desc(user_sessions::Column::CreatedAt);
if let Some(kind) = q.kind {
sel = sel.filter(user_sessions::Column::Kind.eq(kind));
}
if q.include_revoked != Some(true) {
sel = sel.filter(user_sessions::Column::IsRevoked.eq(false));
}
let rows = sel
.limit(
q.limit
.unwrap_or(DEFAULT_PAGE_LIMIT)
.clamp(1, MAX_PAGE_LIMIT),
)
.offset(
q.offset
.unwrap_or(0)
.clamp(0, export::pagination::MAX_OFFSET),
)
.all(self.db().read())
.await?;
Ok(rows
.into_iter()
.map(|r| SessionDto::build(r, current_token))
.collect())
}
pub async fn get_session(
&self,
uid: Uuid,
session_id: Uuid,
current_token: Option<&str>,
) -> Result<SessionDto, UserError> {
let row = user_sessions::Entity::find_by_id(session_id)
.one(self.db().read())
.await?
.ok_or(UserError::NotFound)?;
if row.user_id != uid {
return Err(UserError::NotFound);
}
Ok(SessionDto::build(row, current_token))
}
#[tracing::instrument(
name = "user.revoke_session",
skip_all,
fields(uid = %uid, session_id = %session_id)
)]
pub async fn revoke_session(&self, uid: Uuid, session_id: Uuid) -> Result<(), UserError> {
let row = user_sessions::Entity::find_by_id(session_id)
.one(self.db().read())
.await?
.ok_or(UserError::NotFound)?;
if row.user_id != uid {
return Err(UserError::Forbidden);
}
let now = OffsetDateTime::now_utc();
let mut am: user_sessions::ActiveModel = row.into();
am.is_revoked = Set(true);
am.revoked_at = Set(Some(now));
am.updated_at = Set(now);
am.update(self.db().write()).await?;
Ok(())
}
#[tracing::instrument(name = "user.revoke_all_sessions", skip_all, fields(uid = %uid))]
pub async fn revoke_all_sessions(&self, uid: Uuid) -> Result<CountResp, UserError> {
let now = OffsetDateTime::now_utc();
let result = user_sessions::Entity::update_many()
.filter(user_sessions::Column::UserId.eq(uid))
.filter(user_sessions::Column::IsRevoked.eq(false))
.col_expr(user_sessions::Column::IsRevoked, Expr::value(true))
.col_expr(user_sessions::Column::RevokedAt, Expr::value(Some(now)))
.col_expr(user_sessions::Column::UpdatedAt, Expr::value(now))
.exec(self.db().write())
.await?;
Ok(CountResp {
count: result.rows_affected as i64,
})
}
}