diff --git a/libs/service/user/projects.rs b/libs/service/user/projects.rs index 14f17d9..4bd339d 100644 --- a/libs/service/user/projects.rs +++ b/libs/service/user/projects.rs @@ -60,25 +60,55 @@ impl AppService { let per_page = std::cmp::Ord::min(std::cmp::Ord::max(query.per_page.unwrap_or(20), 1), 100); let offset = (page - 1) * per_page; - let mut condition = Condition::all().add(project::Column::CreatedBy.eq(target_user.uid)); - - if !is_owner && !has_admin_privilege { - condition = condition.add(project::Column::IsPublic.eq(true)); - } - - let total_count = project::Entity::find() - .filter(condition.clone()) - .count(&self.db) - .await?; - - let project_list = project::Entity::find() - .filter(condition) - .order_by_desc(project::Column::CreatedAt) - .limit(per_page) - .offset(offset) + // Projects where user is the creator + let created_projects: Vec = project::Entity::find() + .filter(project::Column::CreatedBy.eq(target_user.uid)) + .select_only() + .column(project::Column::Id) + .into_tuple::() .all(&self.db) .await?; + // Projects where user is a member (via invitation) + let member_projects: Vec = project_members::Entity::find() + .filter(project_members::Column::User.eq(target_user.uid)) + .select_only() + .column(project_members::Column::Project) + .into_tuple::() + .all(&self.db) + .await?; + + // Union + dedup (preserving first occurrence order) + let mut project_ids: Vec = created_projects; + let new_ids: Vec = member_projects.into_iter().filter(|id| !project_ids.contains(id)).collect(); + project_ids.extend(new_ids); + + let total_count = project_ids.len() as u64; + + // Paginate + let page_ids: Vec = project_ids.into_iter().skip(offset as usize).take(per_page as usize).collect(); + + if page_ids.is_empty() { + return Ok(UserProjectsResponse { + username: target_user.username, + projects: vec![], + total_count, + }); + } + + let project_list: Vec = project::Entity::find() + .filter(project::Column::Id.is_in(page_ids.clone())) + .all(&self.db) + .await?; + + // Preserve the order from project_ids (created projects first, then member projects) + let mut sorted_projects = project_list; + sorted_projects.sort_by(|a, b| { + let a_idx = page_ids.iter().position(|&x| x == a.id).unwrap_or(usize::MAX); + let b_idx = page_ids.iter().position(|&x| x == b.id).unwrap_or(usize::MAX); + a_idx.cmp(&b_idx) + }); + let user_project_memberships: std::collections::HashSet = if let Some(uid) = current_user_uid { project_members::Entity::find() @@ -95,7 +125,11 @@ impl AppService { }; let mut project_infos: Vec = Vec::new(); - for project in project_list { + for project in sorted_projects { + // Privacy: non-owners/non-admins only see public projects (member or created) + if !is_owner && !has_admin_privilege && !project.is_public { + continue; + } let member_count = project_members::Entity::find() .filter(project_members::Column::Project.eq(project.id)) .count(&self.db)