From 4ad0259193903eb1cff90c90935479982c6985ce Mon Sep 17 00:00:00 2001 From: Hong Jiarong Date: Tue, 14 Jul 2026 23:55:16 +0800 Subject: [PATCH] fix(admin): let project MANAGE holders list teams for grants GET /teams was org-admin only while team-access mutations require project MANAGE, so members with MANAGE saw an empty grant picker. Open the read-only team list to any org member and load it in the project page whenever the actor can manage the project. --- .../[slug]/projects/[projectId]/+page.svelte | 25 ++++++++----------- hub/src/admin/routes/teamsRoutes.ts | 8 +++++- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/hub/admin-web/src/routes/admin/org/[slug]/projects/[projectId]/+page.svelte b/hub/admin-web/src/routes/admin/org/[slug]/projects/[projectId]/+page.svelte index 59b27eb..fa2d181 100644 --- a/hub/admin-web/src/routes/admin/org/[slug]/projects/[projectId]/+page.svelte +++ b/hub/admin-web/src/routes/admin/org/[slug]/projects/[projectId]/+page.svelte @@ -45,22 +45,19 @@ const [p, a] = await Promise.all([api.project(slug, projectId), api.teamAccess(slug, projectId)]); proj = p; access = a.access; - // Org-admin-only oversight sub-resources: sessions/teams/explorer are - // not available to member-level project MANAGE holders. + // Team list is needed for grant UI whenever the actor has project MANAGE + // (org admin or member). Sessions/explorer stay org-admin oversight only. + const needTeams = p.actorIsOrgAdmin === true || p.actorCanManageProject === true; + const [s, t, e] = await Promise.all([ + p.actorIsOrgAdmin ? api.sessions(slug, projectId) : Promise.resolve({ sessions: [] as SessionSummary[] }), + needTeams ? api.teams(slug) : Promise.resolve({ teams: [] as TeamRow[] }), + p.actorIsOrgAdmin ? api.explorer(slug) : Promise.resolve(null as ExplorerData | null), + ]); + sessions = s.sessions; + teams = t.teams; + explorer = e; if (p.actorIsOrgAdmin) { - const [s, t, e] = await Promise.all([ - api.sessions(slug, projectId), - api.teams(slug), - api.explorer(slug), - ]); - sessions = s.sessions; - teams = t.teams; - explorer = e; moveFolder = p.folderId ?? ''; - } else { - sessions = []; - teams = []; - explorer = null; } } catch (err) { error = err instanceof Error ? err.message : String(err); diff --git a/hub/src/admin/routes/teamsRoutes.ts b/hub/src/admin/routes/teamsRoutes.ts index 4d34533..ac69d74 100644 --- a/hub/src/admin/routes/teamsRoutes.ts +++ b/hub/src/admin/routes/teamsRoutes.ts @@ -18,10 +18,16 @@ export async function registerTeamsRoutes( ): Promise { const guardDeps: GuardDeps = { prisma: config.prisma, sessionSecret: config.sessionSecret }; + // Read-only listing is open to any active org member so project MANAGE + // holders can pick a team when granting TEAM→PROJECT access (ADR-0004). + // Mutations below stay org-admin only. app.get("/api/org/:orgSlug/teams", async (request, reply) => { try { const { orgSlug } = request.params as { orgSlug: string }; - const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug }); + const auth = await requireOrgRole(request, reply, guardDeps, { + orgSlug, + roles: ["OWNER", "ADMIN", "MEMBER"], + }); if (auth === null) return; return { teams: await listOrgTeams(config.prisma, auth.organization.id) }; } catch (err) {