use models::users::user_push_subscriptions::{self, PushSubscriptionPlatform}; use sea_orm::{ ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, QuerySelect, Set, }; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; use utoipa::ToSchema; use uuid::Uuid; use crate::errors::UserError; use crate::services::service::UserService; use crate::share::page::PageQuery; #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct PushSubscriptionDto { pub id: Uuid, pub endpoint: String, pub platform: PushSubscriptionPlatform, pub user_agent: Option, pub created_at: OffsetDateTime, pub updated_at: Option, } impl From for PushSubscriptionDto { fn from(m: user_push_subscriptions::Model) -> Self { Self { id: m.id, endpoint: m.endpoint, platform: m.platform, user_agent: m.user_agent, created_at: m.created_at, updated_at: m.updated_at, } } } #[derive(Debug, Clone, Deserialize, ToSchema)] pub struct UpsertPushCmd { pub endpoint: String, pub p256dh_key: String, pub auth_key: String, pub platform: PushSubscriptionPlatform, pub user_agent: Option, } impl UserService { pub async fn list_push_subscriptions( &self, uid: i64, q: PageQuery, ) -> Result, UserError> { let rows = user_push_subscriptions::Entity::find() .filter(user_push_subscriptions::Column::UserId.eq(uid)) .order_by_desc(user_push_subscriptions::Column::CreatedAt) .limit(q.limit()) .offset(q.offset()) .all(self.db().read()) .await?; Ok(rows.into_iter().map(PushSubscriptionDto::from).collect()) } #[tracing::instrument(name = "user.upsert_push_subscription", skip_all, fields(uid = %uid))] pub async fn upsert_push_subscription( &self, uid: i64, cmd: UpsertPushCmd, ) -> Result { if cmd.endpoint.is_empty() || cmd.endpoint.len() > 2048 { return Err(UserError::BadRequest("endpoint invalid".into())); } let existing = user_push_subscriptions::Entity::find() .filter(user_push_subscriptions::Column::UserId.eq(uid)) .filter(user_push_subscriptions::Column::Endpoint.eq(&cmd.endpoint)) .one(self.db().read()) .await?; let now = OffsetDateTime::now_utc(); let model = match existing { Some(row) => { let mut am: user_push_subscriptions::ActiveModel = row.into(); am.p256dh_key = Set(cmd.p256dh_key); am.auth_key = Set(cmd.auth_key); am.platform = Set(cmd.platform); am.user_agent = Set(cmd.user_agent); am.updated_at = Set(Some(now)); am.update(self.db().write()).await? } None => { let active = user_push_subscriptions::ActiveModel { id: Set(Uuid::now_v7()), user_id: Set(uid), endpoint: Set(cmd.endpoint), p256dh_key: Set(cmd.p256dh_key), auth_key: Set(cmd.auth_key), platform: Set(cmd.platform), user_agent: Set(cmd.user_agent), created_at: Set(now), updated_at: Set(Some(now)), }; active.insert(self.db().write()).await? } }; Ok(PushSubscriptionDto::from(model)) } #[tracing::instrument( name = "user.delete_push_subscription", skip_all, fields(uid = %uid, push_id = %push_id) )] pub async fn delete_push_subscription(&self, uid: i64, push_id: Uuid) -> Result<(), UserError> { let row = user_push_subscriptions::Entity::find_by_id(push_id) .one(self.db().read()) .await? .ok_or(UserError::NotFound)?; if row.user_id != uid { return Err(UserError::Forbidden); } user_push_subscriptions::Entity::delete_by_id(push_id) .exec(self.db().write()) .await?; Ok(()) } }