forked from bai/curriculum-project-hub
feat(hub): add org members, teams, and project team-access APIs
Manage memberships with last-OWNER protection, team lifecycle (archive revokes TEAM grants), and TEAM→PROJECT access under org admin auth.
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
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 };
|
||||
|
||||
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 });
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user