use db::conn::sea_orm::*; use db::space::space_bans as ban_entity; use db::space::spaces as space_entity; use time::OffsetDateTime; use uuid::Uuid; use crate::error::{SpaceError, SpaceResult}; use crate::features::SpaceService; use super::types::{CreateBanCmd, SpaceBanPage, SpaceBanView, UpdateBanCmd}; async fn owner_space(svc: &SpaceService, caller: Uuid, space_id: Uuid) -> SpaceResult { let db = svc.db.reader(); let space = space_entity::Entity::find_by_id(space_id) .one(&db) .await? .ok_or_else(|| SpaceError::NotFound(format!("space {space_id}")))?; if space.owner_id != caller { return Err(SpaceError::Forbidden("only the owner can manage bans".to_string())); } Ok(space) } fn parse_ts(s: Option<&str>) -> SpaceResult> { match s { None | Some("") => Ok(None), Some(v) => OffsetDateTime::parse(v, &time::format_description::well_known::Rfc3339) .map(Some) .map_err(|_| SpaceError::BadRequest("invalid timestamp".to_string())), } } pub async fn ban_user(svc: &SpaceService, caller: Uuid, space_id: Uuid, cmd: CreateBanCmd) -> SpaceResult { owner_space(svc, caller, space_id).await?; let expires_at = parse_ts(cmd.expires_at.as_deref())?; let db = svc.db.writer(); let now = OffsetDateTime::now_utc(); let existing = ban_entity::Entity::find_by_id((space_id, cmd.user_id)).one(&db).await?; if existing.is_some() { return Err(SpaceError::Conflict(format!("user {} is already banned in space {space_id}", cmd.user_id))); } let active: ban_entity::ActiveModel = ban_entity::ActiveModel::builder() .set_space_id(space_id) .set_user_id(cmd.user_id) .set_reason(cmd.reason) .set_banned_by(caller) .set_created_at(now) .set_updated_at(now) .set_expires_at(expires_at) .set_allow_agent_join(cmd.allow_agent_join.unwrap_or(false)) .set_allow_speak(cmd.allow_speak.unwrap_or(false)) .set_allow_push_repo(cmd.allow_push_repo.unwrap_or(false)) .set_allow_comment_any(cmd.allow_comment_any.unwrap_or(false)) .into(); let saved = active.insert(&db).await?; Ok(saved.into()) } pub async fn get_ban(svc: &SpaceService, caller: Uuid, space_id: Uuid, user_id: Uuid) -> SpaceResult { owner_space(svc, caller, space_id).await?; let db = svc.db.reader(); let ban = ban_entity::Entity::find_by_id((space_id, user_id)) .one(&db) .await? .ok_or_else(|| SpaceError::NotFound(format!("ban {user_id} in space {space_id}")))?; Ok(ban.into()) } pub async fn update_ban(svc: &SpaceService, caller: Uuid, space_id: Uuid, user_id: Uuid, cmd: UpdateBanCmd) -> SpaceResult { owner_space(svc, caller, space_id).await?; let db = svc.db.writer(); let txn = db.begin().await?; let ban = ban_entity::Entity::find_by_id((space_id, user_id)) .one(&txn) .await? .ok_or_else(|| SpaceError::NotFound(format!("ban {user_id} in space {space_id}")))?; let mut active: ban_entity::ActiveModel = ban.into(); if let Some(reason) = cmd.reason { active.reason = Set(reason); } if let Some(expires_at) = cmd.expires_at { active.expires_at = Set(parse_ts(expires_at.as_deref())?); } if let Some(v) = cmd.allow_agent_join { active.allow_agent_join = Set(v); } if let Some(v) = cmd.allow_speak { active.allow_speak = Set(v); } if let Some(v) = cmd.allow_push_repo { active.allow_push_repo = Set(v); } if let Some(v) = cmd.allow_comment_any { active.allow_comment_any = Set(v); } active.updated_at = Set(OffsetDateTime::now_utc()); let saved = active.update(&txn).await?; txn.commit().await?; Ok(saved.into()) } pub async fn unban_user(svc: &SpaceService, caller: Uuid, space_id: Uuid, user_id: Uuid) -> SpaceResult<()> { owner_space(svc, caller, space_id).await?; let db = svc.db.writer(); let txn = db.begin().await?; let deleted = ban_entity::Entity::delete_by_id((space_id, user_id)).exec(&txn).await?; if deleted.rows_affected == 0 { return Err(SpaceError::NotFound(format!("ban {user_id} in space {space_id}"))); } txn.commit().await?; Ok(()) } pub async fn list_bans(svc: &SpaceService, caller: Uuid, space_id: Uuid, limit: u64, offset: u64) -> SpaceResult { owner_space(svc, caller, space_id).await?; let db = svc.db.reader(); let limit = std::cmp::max(std::cmp::min(limit, svc.config.max_pagination), 1); let items = ban_entity::Entity::find() .filter(ban_entity::Column::SpaceId.eq(space_id)) .order_by_desc(ban_entity::Column::CreatedAt) .limit(Some(limit)) .offset(Some(offset)) .all(&db) .await?; let total = ban_entity::Entity::find() .filter(ban_entity::Column::SpaceId.eq(space_id)) .count(&db) .await?; Ok(SpaceBanPage { items: items.into_iter().map(SpaceBanView::from).collect(), total: total as i64, }) }