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, pub avatar_url: Option, pub scope: MemberRole, pub joined_at: chrono::DateTime, } #[derive(Clone, Debug, Deserialize, Serialize, ToSchema)] pub struct MemberListResponse { pub members: Vec, 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, per_page: Option, ctx: &Session, ) -> Result { 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 = 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 = 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(()) } }