Skip to content
Clone
a06673b
200 lines 6.5 KB Rust
use db::conn::sea_orm::*;
use db::space::spaces as space_entity;
use time::OffsetDateTime;
use uuid::Uuid;
use crate::error::{SpaceError, SpaceResult};
use crate::features::authz;
use crate::features::SpaceService;
use super::types::{
CreateSpaceCmd, SpaceAllowJoinLevel, SpacePage, SpaceView, UpdateSpaceCmd,
};
pub async fn create_space(
svc: &SpaceService,
caller: Uuid,
cmd: CreateSpaceCmd,
) -> SpaceResult<SpaceView> {
let name = cmd.name.trim();
if name.is_empty() || name.len() > svc.config.max_space_name_len {
return Err(SpaceError::BadRequest(format!(
"invalid space name (length 1..={})",
svc.config.max_space_name_len
)));
}
if let Some(desc) = cmd.description.as_deref()
&& desc.len() > svc.config.max_space_desc_len {
return Err(SpaceError::BadRequest("description too long".into()));
}
let writer = svc.db.writer();
let now = OffsetDateTime::now_utc();
let id = Uuid::new_v4();
let active: space_entity::ActiveModel = space_entity::ActiveModel::builder()
.set_id(id)
.set_lower_name(name.to_lowercase())
.set_name(name.to_string())
.set_description(cmd.description)
.set_owner_id(caller)
.set_avatar_url(cmd.avatar_url)
.set_public(cmd.public)
.set_is_active(true)
.set_allow_join(cmd.allow_join)
.set_allow_invite(cmd.allow_invite)
.set_invite_sort_url_hash(None)
.set_member_count(0)
.set_banner_url(None)
.set_banner_width(None)
.set_banner_height(None)
.set_invite_banner_url(None)
.set_invite_banner_width(None)
.set_invite_banner_height(None)
.set_embed_splash_url(None)
.set_embed_splash_width(None)
.set_embed_splash_height(None)
.set_welcome_message(cmd.welcome_message)
.set_allow_join_level(space_entity::AllowJoinLevel::from(
cmd.allow_join_level.unwrap_or(SpaceAllowJoinLevel::None),
))
.set_member_must_enable_2fa(cmd.member_must_enable_2fa)
.set_created_at(now)
.set_updated_at(now)
.into();
let saved = active.insert(&writer).await?;
Ok(saved.into())
}
pub async fn get_space(
svc: &SpaceService,
caller: Option<Uuid>,
id: Uuid,
) -> SpaceResult<SpaceView> {
let db = svc.db.reader();
let space = space_entity::Entity::find_by_id(id)
.one(&db)
.await?
.ok_or_else(|| SpaceError::NotFound(format!("space {id}")))?;
if !space.is_active {
return Err(SpaceError::NotFound(format!("space {id}")));
}
if space.public {
return Ok(space.into());
}
let uid = caller.ok_or_else(|| SpaceError::NotFound(format!("space {id}")))?;
match authz::effective_permissions(&db, id, uid).await {
Ok(_) => Ok(space.into()),
Err(SpaceError::Forbidden(_)) => Err(SpaceError::NotFound(format!("space {id}"))),
Err(e) => Err(e),
}
}
pub async fn update_space(
svc: &SpaceService,
caller: Uuid,
id: Uuid,
cmd: UpdateSpaceCmd,
) -> SpaceResult<SpaceView> {
let writer = svc.db.writer();
let txn = writer.begin().await?;
let space = space_entity::Entity::find_by_id(id)
.one(&txn)
.await?
.ok_or_else(|| SpaceError::NotFound(format!("space {id}")))?;
if space.owner_id != caller {
return Err(SpaceError::Forbidden("only the owner can update the space".into()));
}
let mut active: space_entity::ActiveModel = space.into();
if let Some(name) = cmd.name {
let name = name.trim();
if name.is_empty() || name.len() > svc.config.max_space_name_len {
return Err(SpaceError::BadRequest("invalid space name".into()));
}
active.name = Set(name.to_string());
active.lower_name = Set(name.to_lowercase());
}
if let Some(desc) = cmd.description {
if desc.as_deref().is_some_and(|d| d.len() > svc.config.max_space_desc_len) {
return Err(SpaceError::BadRequest("description too long".into()));
}
active.description = Set(desc);
}
if let Some(avatar) = cmd.avatar_url {
active.avatar_url = Set(avatar);
}
if let Some(public) = cmd.public {
active.public = Set(public);
}
if let Some(allow_join) = cmd.allow_join {
active.allow_join = Set(allow_join);
}
if let Some(allow_invite) = cmd.allow_invite {
active.allow_invite = Set(allow_invite);
}
if let Some(welcome) = cmd.welcome_message {
if welcome.as_deref().is_some_and(|w| w.len() > svc.config.max_space_desc_len) {
return Err(SpaceError::BadRequest("welcome message too long".into()));
}
active.welcome_message = Set(welcome);
}
if let Some(level) = cmd.allow_join_level {
active.allow_join_level = Set(level.into());
}
if let Some(twofa) = cmd.member_must_enable_2fa {
active.member_must_enable_2fa = Set(twofa);
}
active.updated_at = Set(OffsetDateTime::now_utc());
let saved = active.update(&txn).await?;
txn.commit().await?;
Ok(saved.into())
}
pub async fn delete_space(svc: &SpaceService, caller: Uuid, id: Uuid) -> SpaceResult<()> {
let writer = svc.db.writer();
let txn = writer.begin().await?;
let space = space_entity::Entity::find_by_id(id)
.one(&txn)
.await?
.ok_or_else(|| SpaceError::NotFound(format!("space {id}")))?;
if space.owner_id != caller {
return Err(SpaceError::Forbidden("only the owner can delete the space".into()));
}
let mut active: space_entity::ActiveModel = space.into();
active.is_active = Set(false);
active.updated_at = Set(OffsetDateTime::now_utc());
active.update(&txn).await?;
txn.commit().await?;
Ok(())
}
pub async fn list_spaces(
svc: &SpaceService,
caller: Uuid,
limit: u64,
offset: u64,
) -> SpaceResult<SpacePage> {
let limit = std::cmp::max(std::cmp::min(limit, svc.config.max_pagination), 1) as u64;
let db = svc.db.reader();
let (items, total) = futures::join!(
space_entity::Entity::find()
.filter(space_entity::Column::OwnerId.eq(caller))
.limit(Some(limit))
.offset(Some(offset))
.all(&db),
space_entity::Entity::find()
.filter(space_entity::Column::OwnerId.eq(caller))
.count(&db),
);
let items = items?;
let total = total? as i64;
Ok(SpacePage {
items: items.into_iter().map(Into::into).collect(),
total,
})
}