import type { PrismaClient } from "@prisma/client"; import type { FastifyInstance } from "fastify"; import { addTeamMember, archiveTeam, createTeam, listOrgTeams, listTeamMembers, revokeTeamMember, updateTeam, } from "../../org/teams.js"; import { requireOrgRole, type GuardDeps } from "../auth/guards.js"; import { handleRouteError } from "../errors.js"; export async function registerTeamsRoutes( app: FastifyInstance, config: { readonly prisma: PrismaClient; readonly sessionSecret: string }, ): 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, roles: ["OWNER", "ADMIN", "MEMBER"], }); if (auth === null) return; return { teams: await listOrgTeams(config.prisma, auth.organization.id) }; } catch (err) { return handleRouteError(reply, err); } }); app.post("/api/org/:orgSlug/teams", async (request, reply) => { try { const { orgSlug } = request.params as { orgSlug: string }; const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug }); if (auth === null) return; const body = (request.body ?? {}) as { slug?: unknown; name?: unknown; description?: unknown }; if (typeof body.slug !== "string" || typeof body.name !== "string") { return reply.status(400).send({ error: { code: "bad_request", message: "slug and name are required" }, }); } const team = await createTeam(config.prisma, { organizationId: auth.organization.id, slug: body.slug, name: body.name, ...(typeof body.description === "string" ? { description: body.description } : {}), }); return reply.status(201).send(team); } catch (err) { return handleRouteError(reply, err); } }); app.patch("/api/org/:orgSlug/teams/:teamId", async (request, reply) => { try { const { orgSlug, teamId } = request.params as { orgSlug: string; teamId: string }; const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug }); if (auth === null) return; const body = request.body as { name?: unknown; description?: unknown }; return await updateTeam(config.prisma, { organizationId: auth.organization.id, teamId, ...(typeof body.name === "string" ? { name: body.name } : {}), ...(body.description === null || typeof body.description === "string" ? { description: body.description as string | null } : {}), }); } catch (err) { return handleRouteError(reply, err); } }); app.post("/api/org/:orgSlug/teams/:teamId/archive", async (request, reply) => { try { const { orgSlug, teamId } = request.params as { orgSlug: string; teamId: string }; const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug }); if (auth === null) return; return await archiveTeam(config.prisma, { organizationId: auth.organization.id, teamId, }); } catch (err) { return handleRouteError(reply, err); } }); app.get("/api/org/:orgSlug/teams/:teamId/members", async (request, reply) => { try { const { orgSlug, teamId } = request.params as { orgSlug: string; teamId: string }; const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug }); if (auth === null) return; return { members: await listTeamMembers(config.prisma, { organizationId: auth.organization.id, teamId, }), }; } catch (err) { return handleRouteError(reply, err); } }); app.post("/api/org/:orgSlug/teams/:teamId/members", async (request, reply) => { try { const { orgSlug, teamId } = request.params as { orgSlug: string; teamId: string }; const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug }); if (auth === null) return; const body = request.body as { userId?: unknown; feishuOpenId?: unknown }; const member = await addTeamMember(config.prisma, { organizationId: auth.organization.id, teamId, ...(typeof body.userId === "string" ? { userId: body.userId } : {}), ...(typeof body.feishuOpenId === "string" ? { feishuOpenId: body.feishuOpenId } : {}), }); return reply.status(201).send(member); } catch (err) { return handleRouteError(reply, err); } }); app.post("/api/org/:orgSlug/teams/:teamId/members/:userId/revoke", async (request, reply) => { try { const { orgSlug, teamId, userId } = request.params as { orgSlug: string; teamId: string; userId: string; }; const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug }); if (auth === null) return; return await revokeTeamMember(config.prisma, { organizationId: auth.organization.id, teamId, userId, }); } catch (err) { return handleRouteError(reply, err); } }); }