fix(projects): include project_members when listing user projects

Users who accepted a project invitation could not see that project
on their /user/{username} page because get_user_projects only queried
projects where created_by == user_uid, ignoring project_members entries.
Now unions created_projects and member_projects with privacy filtering.
This commit is contained in:
ZhenYi 2026-04-22 22:38:52 +08:00
parent 16b681c55b
commit aef5280ae8

View File

@ -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 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 offset = (page - 1) * per_page;
let mut condition = Condition::all().add(project::Column::CreatedBy.eq(target_user.uid)); // Projects where user is the creator
let created_projects: Vec<Uuid> = project::Entity::find()
if !is_owner && !has_admin_privilege { .filter(project::Column::CreatedBy.eq(target_user.uid))
condition = condition.add(project::Column::IsPublic.eq(true)); .select_only()
} .column(project::Column::Id)
.into_tuple::<Uuid>()
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)
.all(&self.db) .all(&self.db)
.await?; .await?;
// Projects where user is a member (via invitation)
let member_projects: Vec<Uuid> = project_members::Entity::find()
.filter(project_members::Column::User.eq(target_user.uid))
.select_only()
.column(project_members::Column::Project)
.into_tuple::<Uuid>()
.all(&self.db)
.await?;
// Union + dedup (preserving first occurrence order)
let mut project_ids: Vec<Uuid> = created_projects;
let new_ids: Vec<Uuid> = 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<Uuid> = 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::Model> = 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<Uuid> = let user_project_memberships: std::collections::HashSet<Uuid> =
if let Some(uid) = current_user_uid { if let Some(uid) = current_user_uid {
project_members::Entity::find() project_members::Entity::find()
@ -95,7 +125,11 @@ impl AppService {
}; };
let mut project_infos: Vec<UserProjectInfo> = Vec::new(); let mut project_infos: Vec<UserProjectInfo> = 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() let member_count = project_members::Entity::find()
.filter(project_members::Column::Project.eq(project.id)) .filter(project_members::Column::Project.eq(project.id))
.count(&self.db) .count(&self.db)