use models::users::{user_devices, user_sessions}; use sea_orm::sea_query::Expr; use sea_orm::{ ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, IntoActiveModel, QueryFilter, QueryOrder, QuerySelect, Set, TransactionTrait, }; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; use uuid::Uuid; use super::AuthService; use crate::errors::AuthError; use crate::share::types::ListQuery; #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct DeviceDto { pub id: Uuid, pub device_name: Option, pub device_type: Option, pub os: Option, pub is_trusted: bool, pub first_seen_at: OffsetDateTime, pub last_seen_at: OffsetDateTime, } #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct UpdateDeviceCmd { pub device_name: Option, pub device_type: Option, pub is_trusted: Option, } impl AuthService { pub async fn list_devices( &self, uid: Uuid, query: ListQuery, ) -> Result, AuthError> { let (limit, offset) = query.page(); let rows = user_devices::Entity::find() .filter(user_devices::Column::UserId.eq(uid)) .order_by_desc(user_devices::Column::LastSeenAt) .limit(limit) .offset(offset) .all(self.db().read()) .await?; Ok(rows .into_iter() .map(|row| DeviceDto { id: row.id, device_name: row.device_name, device_type: row.device_type, os: row.os, is_trusted: row.is_trusted, first_seen_at: row.first_seen_at, last_seen_at: row.last_seen_at, }) .collect()) } pub async fn update_device( &self, uid: Uuid, device_id: Uuid, cmd: UpdateDeviceCmd, ) -> Result<(), AuthError> { let db = self.db(); let Some(device) = user_devices::Entity::find() .filter(user_devices::Column::Id.eq(device_id)) .filter(user_devices::Column::UserId.eq(uid)) .one(db.write()) .await? else { return Err(AuthError::NotFound); }; let device_name = cmd.device_name.or(device.device_name.clone()); let device_type = cmd.device_type.or(device.device_type.clone()); let mut active = device.into_active_model(); active.device_name = Set(device_name); active.device_type = Set(device_type); if let Some(trusted) = cmd.is_trusted { active.is_trusted = Set(trusted); } active.updated_at = Set(OffsetDateTime::now_utc()); active.update(db.write()).await?; Ok(()) } pub async fn delete_device(&self, uid: Uuid, device_id: Uuid) -> Result<(), AuthError> { let db = self.db(); let device_exists = user_devices::Entity::find() .filter(user_devices::Column::Id.eq(device_id)) .filter(user_devices::Column::UserId.eq(uid)) .one(db.write()) .await?; if device_exists.is_none() { return Err(AuthError::NotFound); } let now = OffsetDateTime::now_utc(); TransactionTrait::transaction(db.primary(), |txn| { Box::pin(async move { user_devices::Entity::delete_by_id(device_id) .exec(txn) .await?; user_sessions::Entity::update_many() .filter(user_sessions::Column::UserId.eq(uid)) .filter(user_sessions::Column::DeviceId.eq(Some(device_id))) .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(txn) .await?; Ok::<(), DbErr>(()) }) }) .await?; Ok(()) } }