forked from bai/curriculum-project-hub
4ad0259193
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.
147 lines
5.3 KiB
TypeScript
147 lines
5.3 KiB
TypeScript
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<void> {
|
|
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);
|
|
}
|
|
});
|
|
}
|