use crate::error::AppError; use crate::models::common::{DeliveryChannel, NotificationType, Role}; use crate::models::notifications::NotificationTemplate; use crate::models::users::User; use crate::service::NotificationService; use crate::session::Session; use serde::{Deserialize, Serialize}; use uuid::Uuid; use super::util::clamp_limit_offset; #[derive(Debug, Deserialize, Serialize)] pub struct CreateTemplateParams { pub key: String, pub notification_type: NotificationType, pub channel: DeliveryChannel, pub locale: String, pub subject_template: Option, pub title_template: String, pub body_template: String, pub action_text_template: Option, pub enabled: bool, } #[derive(Debug, Deserialize, Serialize)] pub struct UpdateTemplateParams { pub subject_template: Option, pub title_template: Option, pub body_template: Option, pub action_text_template: Option, pub enabled: Option, } impl NotificationTemplate { pub async fn find_by_id(pool: &sqlx::PgPool, id: Uuid) -> Result, AppError> { sqlx::query_as::<_, NotificationTemplate>( "SELECT id, key, notification_type, channel, locale, subject_template, title_template, \ body_template, action_text_template, enabled, created_by, created_at, updated_at \ FROM notification_template WHERE id = $1", ) .bind(id) .fetch_optional(pool) .await .map_err(AppError::Database) } pub async fn list_all( pool: &sqlx::PgPool, limit: i64, offset: i64, ) -> Result, AppError> { sqlx::query_as::<_, NotificationTemplate>( "SELECT id, key, notification_type, channel, locale, subject_template, title_template, \ body_template, action_text_template, enabled, created_by, created_at, updated_at \ FROM notification_template ORDER BY key, channel, locale LIMIT $1 OFFSET $2", ) .bind(limit) .bind(offset) .fetch_all(pool) .await .map_err(AppError::Database) } } impl NotificationService { /// Check if user is system admin async fn ensure_system_admin(&self, session: &Session) -> Result { let user_id = session.user().ok_or(AppError::Unauthorized)?; let user: User = sqlx::query_as( "SELECT id, username, display_name, avatar_url, bio, status, role, visibility, \ is_active, is_bot, last_login_at, created_at, updated_at, deleted_at \ FROM \"user\" WHERE id = $1 AND deleted_at IS NULL", ) .bind(user_id) .fetch_optional(self.ctx.db.reader()) .await .map_err(AppError::Database)? .ok_or(AppError::NotFound("User not found".into()))?; if user.role != Role::System && user.role != Role::Admin { return Err(AppError::Forbidden("System admin access required".into())); } Ok(user_id) } pub async fn list_templates( &self, session: &Session, limit: i64, offset: i64, ) -> Result, AppError> { self.ensure_system_admin(session).await?; let (limit, offset) = clamp_limit_offset(limit, offset); NotificationTemplate::list_all(self.ctx.db.reader(), limit, offset).await } pub async fn get_template( &self, session: &Session, template_id: Uuid, ) -> Result { self.ensure_system_admin(session).await?; NotificationTemplate::find_by_id(self.ctx.db.reader(), template_id) .await? .ok_or(AppError::NotFound("template not found".into())) } pub async fn create_template( &self, session: &Session, params: CreateTemplateParams, ) -> Result { let user_id = self.ensure_system_admin(session).await?; let id = Uuid::now_v7(); let now = chrono::Utc::now(); sqlx::query( "INSERT INTO notification_template \ (id, key, notification_type, channel, locale, subject_template, title_template, \ body_template, action_text_template, enabled, created_by, created_at, updated_at) \ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $12)", ) .bind(id) .bind(¶ms.key) .bind(params.notification_type.as_str()) .bind(params.channel.as_str()) .bind(¶ms.locale) .bind(params.subject_template) .bind(¶ms.title_template) .bind(¶ms.body_template) .bind(params.action_text_template) .bind(params.enabled) .bind(user_id) .bind(now) .execute(self.ctx.db.writer()) .await .map_err(AppError::Database)?; NotificationTemplate::find_by_id(self.ctx.db.reader(), id) .await? .ok_or(AppError::InternalServerError( "failed to fetch created template".into(), )) } pub async fn update_template( &self, session: &Session, template_id: Uuid, params: UpdateTemplateParams, ) -> Result { self.ensure_system_admin(session).await?; let now = chrono::Utc::now(); let existing = NotificationTemplate::find_by_id(self.ctx.db.reader(), template_id) .await? .ok_or(AppError::NotFound("template not found".into()))?; let subject_template = params.subject_template.or(existing.subject_template); let title_template = params.title_template.unwrap_or(existing.title_template); let body_template = params.body_template.unwrap_or(existing.body_template); let action_text_template = params .action_text_template .or(existing.action_text_template); let enabled = params.enabled.unwrap_or(existing.enabled); sqlx::query( "UPDATE notification_template \ SET subject_template = $1, title_template = $2, body_template = $3, \ action_text_template = $4, enabled = $5, updated_at = $6 \ WHERE id = $7", ) .bind(subject_template) .bind(&title_template) .bind(&body_template) .bind(action_text_template) .bind(enabled) .bind(now) .bind(template_id) .execute(self.ctx.db.writer()) .await .map_err(AppError::Database)?; NotificationTemplate::find_by_id(self.ctx.db.reader(), template_id) .await? .ok_or(AppError::InternalServerError( "failed to fetch updated template".into(), )) } pub async fn delete_template( &self, session: &Session, template_id: Uuid, ) -> Result<(), AppError> { self.ensure_system_admin(session).await?; let result = sqlx::query("DELETE FROM notification_template WHERE id = $1") .bind(template_id) .execute(self.ctx.db.writer()) .await .map_err(AppError::Database)?; if result.rows_affected() == 0 { return Err(AppError::NotFound("template not found".into())); } Ok(()) } }