319 lines
10 KiB
Rust
319 lines
10 KiB
Rust
use crate::AppService;
|
|
use crate::error::AppError;
|
|
use chrono::Utc;
|
|
use models::projects::{MemberRole, project_audit_log, project_members};
|
|
use models::users::user;
|
|
use sea_orm::*;
|
|
use serde::{Deserialize, Serialize};
|
|
use session::Session;
|
|
use utoipa::ToSchema;
|
|
use uuid::Uuid;
|
|
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema)]
|
|
pub struct MemberInfo {
|
|
pub user_id: Uuid,
|
|
pub username: String,
|
|
pub display_name: Option<String>,
|
|
pub avatar_url: Option<String>,
|
|
pub scope: MemberRole,
|
|
pub joined_at: chrono::DateTime<Utc>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize, ToSchema)]
|
|
pub struct MemberListResponse {
|
|
pub members: Vec<MemberInfo>,
|
|
pub total: u64,
|
|
pub page: u64,
|
|
pub per_page: u64,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize, ToSchema)]
|
|
pub struct UpdateMemberRoleRequest {
|
|
pub user_id: Uuid,
|
|
pub scope: MemberRole,
|
|
}
|
|
|
|
impl AppService {
|
|
pub async fn project_get_members(
|
|
&self,
|
|
project_name: String,
|
|
page: Option<u64>,
|
|
per_page: Option<u64>,
|
|
ctx: &Session,
|
|
) -> Result<MemberListResponse, AppError> {
|
|
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
|
let project = self
|
|
.utils_find_project_by_name(project_name.clone())
|
|
.await?;
|
|
|
|
let _requester_member = project_members::Entity::find()
|
|
.filter(project_members::Column::Project.eq(project.id))
|
|
.filter(project_members::Column::User.eq(user_uid))
|
|
.one(&self.db)
|
|
.await?;
|
|
|
|
let page = page.unwrap_or(1);
|
|
let per_page = per_page.unwrap_or(20);
|
|
|
|
let members = project_members::Entity::find()
|
|
.filter(project_members::Column::Project.eq(project.id))
|
|
.order_by_asc(project_members::Column::JoinedAt)
|
|
.paginate(&self.db, per_page)
|
|
.fetch_page(page - 1)
|
|
.await?;
|
|
|
|
let total = project_members::Entity::find()
|
|
.filter(project_members::Column::Project.eq(project.id))
|
|
.count(&self.db)
|
|
.await?;
|
|
|
|
let user_ids: Vec<Uuid> = members.iter().map(|m| m.user).collect();
|
|
|
|
let users_data = if user_ids.is_empty() {
|
|
vec![]
|
|
} else {
|
|
user::Entity::find()
|
|
.filter(user::Column::Uid.is_in(user_ids))
|
|
.all(&self.db)
|
|
.await?
|
|
};
|
|
|
|
let member_infos: Vec<MemberInfo> = members
|
|
.into_iter()
|
|
.filter_map(|member| {
|
|
let role = member.scope_role().ok()?;
|
|
users_data
|
|
.iter()
|
|
.find(|u| u.uid == member.user)
|
|
.map(|user| MemberInfo {
|
|
user_id: user.uid,
|
|
username: user.username.clone(),
|
|
display_name: user.display_name.clone(),
|
|
avatar_url: user.avatar_url.clone(),
|
|
scope: role.clone(),
|
|
joined_at: member.joined_at,
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
Ok(MemberListResponse {
|
|
members: member_infos,
|
|
total,
|
|
page,
|
|
per_page,
|
|
})
|
|
}
|
|
|
|
pub async fn project_update_member_role(
|
|
&self,
|
|
project_name: String,
|
|
request: UpdateMemberRoleRequest,
|
|
ctx: &Session,
|
|
) -> Result<(), AppError> {
|
|
let actor_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
|
let project = self
|
|
.utils_find_project_by_name(project_name.clone())
|
|
.await?;
|
|
|
|
let actor_member = project_members::Entity::find()
|
|
.filter(project_members::Column::Project.eq(project.id))
|
|
.filter(project_members::Column::User.eq(actor_uid))
|
|
.one(&self.db)
|
|
.await?
|
|
.ok_or(AppError::PermissionDenied)?;
|
|
|
|
let actor_role = actor_member
|
|
.scope_role()
|
|
.map_err(|_| AppError::RoleParseError)?;
|
|
|
|
if actor_role != MemberRole::Owner && actor_role != MemberRole::Admin {
|
|
return Err(AppError::NoPower);
|
|
}
|
|
|
|
let target_member = project_members::Entity::find()
|
|
.filter(project_members::Column::Project.eq(project.id))
|
|
.filter(project_members::Column::User.eq(request.user_id))
|
|
.one(&self.db)
|
|
.await?
|
|
.ok_or(AppError::NotFound("Member not found".to_string()))?;
|
|
|
|
let target_role = target_member
|
|
.scope_role()
|
|
.map_err(|_| AppError::RoleParseError)?;
|
|
|
|
if target_role == MemberRole::Owner {
|
|
return Err(AppError::NoPower);
|
|
}
|
|
|
|
if request.scope == MemberRole::Admin && actor_role != MemberRole::Owner {
|
|
return Err(AppError::NoPower);
|
|
}
|
|
|
|
if request.scope == MemberRole::Owner {
|
|
return Err(AppError::NoPower);
|
|
}
|
|
|
|
let mut active_member: project_members::ActiveModel = target_member.into();
|
|
active_member.scope = Set(request.scope.to_string());
|
|
active_member.update(&self.db).await?;
|
|
|
|
let actor_username = user::Entity::find_by_id(actor_uid)
|
|
.one(&self.db)
|
|
.await
|
|
.ok()
|
|
.flatten()
|
|
.map(|u| u.username)
|
|
.unwrap_or_default();
|
|
let target_username = user::Entity::find_by_id(request.user_id)
|
|
.one(&self.db)
|
|
.await
|
|
.ok()
|
|
.flatten()
|
|
.map(|u| u.username)
|
|
.unwrap_or_default();
|
|
let _ = self
|
|
.project_log_activity(
|
|
project.id,
|
|
None,
|
|
actor_uid,
|
|
super::activity::ActivityLogParams {
|
|
event_type: "member_role_change".to_string(),
|
|
title: format!(
|
|
"{} changed {}'s role to {}",
|
|
actor_username, target_username, request.scope
|
|
),
|
|
repo_id: None,
|
|
content: None,
|
|
event_id: None,
|
|
event_sub_id: None,
|
|
metadata: Some(serde_json::json!({
|
|
"target_user_id": request.user_id.to_string(),
|
|
"new_role": request.scope.to_string(),
|
|
})),
|
|
is_private: false,
|
|
},
|
|
)
|
|
.await;
|
|
|
|
let log = project_audit_log::ActiveModel {
|
|
project: Set(project.id),
|
|
actor: Set(actor_uid),
|
|
action: Set("update_member_role".to_string()),
|
|
details: Set(Some(serde_json::json!({
|
|
"project_name": project.name,
|
|
"target_user_id": request.user_id,
|
|
"new_role": request.scope.to_string(),
|
|
}))),
|
|
created_at: Set(Utc::now()),
|
|
..Default::default()
|
|
};
|
|
log.insert(&self.db).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn project_remove_member(
|
|
&self,
|
|
project_name: String,
|
|
user_id: Uuid,
|
|
ctx: &Session,
|
|
) -> Result<(), AppError> {
|
|
let actor_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
|
let project = self
|
|
.utils_find_project_by_name(project_name.clone())
|
|
.await?;
|
|
|
|
let actor_member = project_members::Entity::find()
|
|
.filter(project_members::Column::Project.eq(project.id))
|
|
.filter(project_members::Column::User.eq(actor_uid))
|
|
.one(&self.db)
|
|
.await?
|
|
.ok_or(AppError::PermissionDenied)?;
|
|
|
|
let actor_role = actor_member
|
|
.scope_role()
|
|
.map_err(|_| AppError::RoleParseError)?;
|
|
|
|
if actor_role != MemberRole::Owner && actor_role != MemberRole::Admin {
|
|
return Err(AppError::NoPower);
|
|
}
|
|
|
|
let target_member = project_members::Entity::find()
|
|
.filter(project_members::Column::Project.eq(project.id))
|
|
.filter(project_members::Column::User.eq(user_id))
|
|
.one(&self.db)
|
|
.await?
|
|
.ok_or(AppError::NotFound("Member not found".to_string()))?;
|
|
|
|
let target_role = target_member
|
|
.scope_role()
|
|
.map_err(|_| AppError::RoleParseError)?;
|
|
|
|
if target_role == MemberRole::Owner {
|
|
return Err(AppError::NoPower);
|
|
}
|
|
|
|
if actor_role == MemberRole::Admin && target_role == MemberRole::Admin {
|
|
return Err(AppError::NoPower);
|
|
}
|
|
|
|
project_members::Entity::delete_many()
|
|
.filter(project_members::Column::Project.eq(project.id))
|
|
.filter(project_members::Column::User.eq(user_id))
|
|
.exec(&self.db)
|
|
.await?;
|
|
|
|
let actor_username = user::Entity::find_by_id(actor_uid)
|
|
.one(&self.db)
|
|
.await
|
|
.ok()
|
|
.flatten()
|
|
.map(|u| u.username)
|
|
.unwrap_or_default();
|
|
let target_username = user::Entity::find_by_id(user_id)
|
|
.one(&self.db)
|
|
.await
|
|
.ok()
|
|
.flatten()
|
|
.map(|u| u.username)
|
|
.unwrap_or_default();
|
|
let _ = self
|
|
.project_log_activity(
|
|
project.id,
|
|
None,
|
|
actor_uid,
|
|
super::activity::ActivityLogParams {
|
|
event_type: "member_remove".to_string(),
|
|
title: format!(
|
|
"{} removed {} from the project",
|
|
actor_username, target_username
|
|
),
|
|
repo_id: None,
|
|
content: None,
|
|
event_id: None,
|
|
event_sub_id: None,
|
|
metadata: Some(serde_json::json!({
|
|
"removed_user_id": user_id.to_string(),
|
|
})),
|
|
is_private: false,
|
|
},
|
|
)
|
|
.await;
|
|
|
|
let log = project_audit_log::ActiveModel {
|
|
project: Set(project.id),
|
|
actor: Set(actor_uid),
|
|
action: Set("remove_member".to_string()),
|
|
details: Set(Some(serde_json::json!({
|
|
"project_name": project.name,
|
|
"removed_user_id": user_id,
|
|
}))),
|
|
created_at: Set(Utc::now()),
|
|
..Default::default()
|
|
};
|
|
log.insert(&self.db).await?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|