02d8172
use chrono::Utc;
use sea_orm::{
ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder,
QuerySelect, Set,
};
use uuid::Uuid;
use crate::errors::{SpaceError, SpaceResult};
use crate::services::service::SpaceService;
use crate::services::types::{
AddMemberBody, JoinRequestSummary, JoinRuleSummary, SYSTEM_ROLE_MEMBER,
};
use crate::services::{member, service};
use models::space::{
SpaceJoinRequestState, SpaceMemberRole, space_join_requests, space_join_rules, space_roles,
};
use models::users::users;
fn rule_summary(row: &space_join_rules::Model) -> JoinRuleSummary {
JoinRuleSummary {
id: row.id,
organization_id: row.organization_id,
role_id: row.role_id,
trigger: row.trigger.clone(),
matcher: row.matcher.clone(),
enabled: row.enabled,
created_at: row.created_at,
updated_at: row.updated_at,
}
}
impl SpaceService {
async fn join_request_summary(
&self,
row: &space_join_requests::Model,
) -> SpaceResult<JoinRequestSummary> {
let user = users::Entity::find_by_id(row.user_id)
.one(self.db_conn().read())
.await?;
Ok(JoinRequestSummary {
id: row.id,
organization_id: row.organization_id,
user_id: row.user_id,
username: user
.as_ref()
.map(|value| value.username.clone())
.unwrap_or_default(),
displayname: user.and_then(|value| value.displayname),
status: row.status,
note: row.note.clone(),
decided_by: row.decided_by,
decided_at: row.decided_at,
created_at: row.created_at,
updated_at: row.updated_at,
})
}
// 加入规则的可选字段本身就多,参数表与 HTTP 请求体一一对应,拆结构体只会增加噪音。
#[allow(clippy::too_many_arguments)]
#[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, request_id = %request_id.unwrap_or_default()))]
pub async fn admin_create_join_rule(
&self,
actor_user_id: i64,
organization_id: Uuid,
role_id: Uuid,
trigger: String,
matcher: String,
enabled: bool,
request_id: Option<&str>,
) -> SpaceResult<JoinRuleSummary> {
self.require_permission(actor_user_id, organization_id, "role.update", request_id)
.await?;
let trigger = trigger.trim().to_string();
let matcher = matcher.trim().to_string();
if trigger.is_empty() || matcher.is_empty() {
return Err(SpaceError::MalformedRequest);
}
let role = space_roles::Entity::find_by_id(role_id)
.filter(space_roles::Column::OrganizationId.eq(organization_id))
.one(self.db_conn().read())
.await?
.ok_or(SpaceError::SpaceRoleNotFound)?;
let row = space_join_rules::ActiveModel {
organization_id: Set(organization_id),
role_id: Set(role.id),
trigger: Set(trigger),
matcher: Set(matcher),
enabled: Set(enabled),
..Default::default()
}
.insert(self.db_conn().write())
.await?;
service::bump_version(self.db_conn().write(), organization_id).await?;
Ok(rule_summary(&row))
}
// 同上:可选字段多,保持与请求体一致的参数顺序。
#[allow(clippy::too_many_arguments)]
#[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, rule_id = %rule_id, request_id = %request_id.unwrap_or_default()))]
pub async fn admin_update_join_rule(
&self,
actor_user_id: i64,
organization_id: Uuid,
rule_id: Uuid,
role_id: Option<Uuid>,
trigger: Option<String>,
matcher: Option<String>,
enabled: Option<bool>,
request_id: Option<&str>,
) -> SpaceResult<JoinRuleSummary> {
self.require_permission(actor_user_id, organization_id, "role.update", request_id)
.await?;
let row = space_join_rules::Entity::find_by_id(rule_id)
.filter(space_join_rules::Column::OrganizationId.eq(organization_id))
.one(self.db_conn().read())
.await?
.ok_or(SpaceError::SpaceNotFound)?;
let mut active: space_join_rules::ActiveModel = row.into();
if let Some(role_id) = role_id {
let role = space_roles::Entity::find_by_id(role_id)
.filter(space_roles::Column::OrganizationId.eq(organization_id))
.one(self.db_conn().read())
.await?
.ok_or(SpaceError::SpaceRoleNotFound)?;
active.role_id = Set(role.id);
}
if let Some(trigger) = trigger {
let trigger = trigger.trim().to_string();
if trigger.is_empty() {
return Err(SpaceError::MalformedRequest);
}
active.trigger = Set(trigger);
}
if let Some(matcher) = matcher {
let matcher = matcher.trim().to_string();
if matcher.is_empty() {
return Err(SpaceError::MalformedRequest);
}
active.matcher = Set(matcher);
}
if let Some(enabled) = enabled {
active.enabled = Set(enabled);
}
let updated = active.update(self.db_conn().write()).await?;
service::bump_version(self.db_conn().write(), organization_id).await?;
Ok(rule_summary(&updated))
}
#[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, rule_id = %rule_id, request_id = %request_id.unwrap_or_default()))]
pub async fn admin_delete_join_rule(
&self,
actor_user_id: i64,
organization_id: Uuid,
rule_id: Uuid,
request_id: Option<&str>,
) -> SpaceResult<bool> {
self.require_permission(actor_user_id, organization_id, "role.update", request_id)
.await?;
let deleted = space_join_rules::Entity::delete_many()
.filter(space_join_rules::Column::Id.eq(rule_id))
.filter(space_join_rules::Column::OrganizationId.eq(organization_id))
.exec(self.db_conn().write())
.await?;
if deleted.rows_affected > 0 {
service::bump_version(self.db_conn().write(), organization_id).await?;
}
Ok(deleted.rows_affected > 0)
}
#[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, request_id = %request_id.unwrap_or_default()))]
pub async fn admin_list_join_rules(
&self,
actor_user_id: i64,
organization_id: Uuid,
request_id: Option<&str>,
) -> SpaceResult<Vec<JoinRuleSummary>> {
self.require_permission(actor_user_id, organization_id, "role.view", request_id)
.await?;
let rows = space_join_rules::Entity::find()
.filter(space_join_rules::Column::OrganizationId.eq(organization_id))
.order_by_asc(space_join_rules::Column::CreatedAt)
.all(self.db_conn().read())
.await?;
Ok(rows.iter().map(rule_summary).collect())
}
#[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, request_id = %request_id.unwrap_or_default()))]
pub async fn admin_create_join_request(
&self,
actor_user_id: i64,
organization_id: Uuid,
note: Option<String>,
request_id: Option<&str>,
) -> SpaceResult<JoinRequestSummary> {
let organization = self.load_organization(organization_id).await?;
if organization.status != models::space::SpaceStatus::Active {
return Err(SpaceError::SpaceInactive);
}
if let Some(existing) =
member::find_any(self.db_conn().read(), organization_id, actor_user_id).await?
{
if existing.banned_at.is_some() {
return Err(SpaceError::SpaceMemberBanned);
}
if existing.left_at.is_none() {
return Err(SpaceError::SpaceMemberExists);
}
}
if let Some(pending) = space_join_requests::Entity::find()
.filter(space_join_requests::Column::OrganizationId.eq(organization_id))
.filter(space_join_requests::Column::UserId.eq(actor_user_id))
.filter(space_join_requests::Column::Status.eq(SpaceJoinRequestState::Pending))
.one(self.db_conn().read())
.await?
{
let mut active: space_join_requests::ActiveModel = pending.into();
active.note = Set(note);
let updated = active.update(self.db_conn().write()).await?;
return self.join_request_summary(&updated).await;
}
let row = space_join_requests::ActiveModel {
organization_id: Set(organization_id),
user_id: Set(actor_user_id),
status: Set(SpaceJoinRequestState::Pending),
note: Set(note),
..Default::default()
}
.insert(self.db_conn().write())
.await?;
self.join_request_summary(&row).await
}
#[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, request_id = %request_id.unwrap_or_default()))]
pub async fn admin_list_join_requests(
&self,
actor_user_id: i64,
organization_id: Uuid,
status: Option<SpaceJoinRequestState>,
limit: u64,
offset: u64,
request_id: Option<&str>,
) -> SpaceResult<(Vec<JoinRequestSummary>, u64)> {
self.require_permission(actor_user_id, organization_id, "member.view", request_id)
.await?;
let mut select = space_join_requests::Entity::find()
.filter(space_join_requests::Column::OrganizationId.eq(organization_id));
if let Some(status) = status {
select = select.filter(space_join_requests::Column::Status.eq(status));
}
let total = select.clone().count(self.db_conn().read()).await?;
let rows = select
.order_by_desc(space_join_requests::Column::CreatedAt)
.limit(limit)
.offset(offset)
.all(self.db_conn().read())
.await?;
let mut out = Vec::with_capacity(rows.len());
for row in rows {
out.push(self.join_request_summary(&row).await?);
}
Ok((out, total))
}
#[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, join_request_id = %join_request_id, request_id = %request_id.unwrap_or_default()))]
pub async fn admin_withdraw_join_request(
&self,
actor_user_id: i64,
organization_id: Uuid,
join_request_id: Uuid,
request_id: Option<&str>,
) -> SpaceResult<JoinRequestSummary> {
let row = space_join_requests::Entity::find_by_id(join_request_id)
.filter(space_join_requests::Column::OrganizationId.eq(organization_id))
.one(self.db_conn().read())
.await?
.ok_or(SpaceError::SpaceMemberNotFound)?;
if row.user_id != actor_user_id {
return Err(SpaceError::Forbidden);
}
if row.status != SpaceJoinRequestState::Pending {
return Err(SpaceError::Forbidden);
}
let mut active: space_join_requests::ActiveModel = row.into();
active.status = Set(SpaceJoinRequestState::Withdrawn);
let updated = active.update(self.db_conn().write()).await?;
self.join_request_summary(&updated).await
}
#[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, join_request_id = %join_request_id, request_id = %request_id.unwrap_or_default()))]
async fn finish_join_request(
&self,
actor_user_id: i64,
organization_id: Uuid,
join_request_id: Uuid,
status: SpaceJoinRequestState,
note: Option<String>,
request_id: Option<&str>,
) -> SpaceResult<JoinRequestSummary> {
self.require_permission(actor_user_id, organization_id, "member.add", request_id)
.await?;
let row = space_join_requests::Entity::find_by_id(join_request_id)
.filter(space_join_requests::Column::OrganizationId.eq(organization_id))
.one(self.db_conn().read())
.await?
.ok_or(SpaceError::SpaceMemberNotFound)?;
if row.status != SpaceJoinRequestState::Pending {
return Err(SpaceError::Forbidden);
}
if status == SpaceJoinRequestState::Approved {
let default_role = space_roles::Entity::find()
.filter(space_roles::Column::OrganizationId.eq(organization_id))
.filter(space_roles::Column::Key.eq(SYSTEM_ROLE_MEMBER))
.one(self.db_conn().read())
.await?;
self.admin_add_member(
actor_user_id,
organization_id,
AddMemberBody {
user_id: row.user_id,
role: Some(SpaceMemberRole::Member),
role_keys: default_role
.filter(|role| role.default_role)
.map(|role| vec![role.key])
.unwrap_or_default(),
},
request_id,
)
.await?;
}
let mut active: space_join_requests::ActiveModel = row.into();
active.status = Set(status);
active.decided_by = Set(Some(actor_user_id));
active.decided_at = Set(Some(Utc::now()));
if let Some(note) = note {
active.note = Set(Some(note));
}
let updated = active.update(self.db_conn().write()).await?;
self.join_request_summary(&updated).await
}
#[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, join_request_id = %join_request_id, request_id = %request_id.unwrap_or_default()))]
pub async fn admin_approve_join_request(
&self,
actor_user_id: i64,
organization_id: Uuid,
join_request_id: Uuid,
request_id: Option<&str>,
) -> SpaceResult<JoinRequestSummary> {
self.finish_join_request(
actor_user_id,
organization_id,
join_request_id,
SpaceJoinRequestState::Approved,
None,
request_id,
)
.await
}
#[tracing::instrument(skip_all, fields(actor_user_id = actor_user_id, organization_id = %organization_id, join_request_id = %join_request_id, request_id = %request_id.unwrap_or_default()))]
pub async fn admin_reject_join_request(
&self,
actor_user_id: i64,
organization_id: Uuid,
join_request_id: Uuid,
note: Option<String>,
request_id: Option<&str>,
) -> SpaceResult<JoinRequestSummary> {
self.finish_join_request(
actor_user_id,
organization_id,
join_request_id,
SpaceJoinRequestState::Rejected,
note,
request_id,
)
.await
}
}