forked from EduCraft/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,85 @@
|
||||
import type { PermissionRole, PrismaClient } from "@prisma/client";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import {
|
||||
grantTeamProjectAccess,
|
||||
listProjectTeamAccess,
|
||||
revokeTeamProjectAccess,
|
||||
} from "../../permissions/projectTeamAccess.js";
|
||||
import { requireOrgProject, requireOrgRole, type GuardDeps } from "../auth/guards.js";
|
||||
import { handleRouteError } from "../errors.js";
|
||||
|
||||
const ROLES: readonly PermissionRole[] = ["READ", "EDIT", "MANAGE"];
|
||||
|
||||
export async function registerAccessRoutes(
|
||||
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/projects/:projectId/team-access", async (request, reply) => {
|
||||
try {
|
||||
const { orgSlug, projectId } = request.params as { orgSlug: string; projectId: string };
|
||||
const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug });
|
||||
if (auth === null) return;
|
||||
await requireOrgProject(guardDeps, auth.organization.id, projectId);
|
||||
return { access: await listProjectTeamAccess(config.prisma, projectId) };
|
||||
} catch (err) {
|
||||
return handleRouteError(reply, err);
|
||||
}
|
||||
});
|
||||
|
||||
app.put("/api/org/:orgSlug/projects/:projectId/team-access", async (request, reply) => {
|
||||
try {
|
||||
const { orgSlug, projectId } = request.params as { orgSlug: string; projectId: string };
|
||||
const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug });
|
||||
if (auth === null) return;
|
||||
await requireOrgProject(guardDeps, auth.organization.id, projectId);
|
||||
const body = request.body as {
|
||||
teamId?: unknown;
|
||||
teamSlug?: unknown;
|
||||
role?: unknown;
|
||||
};
|
||||
const role = parseRole(body.role);
|
||||
if (role === null) {
|
||||
return reply.status(400).send({
|
||||
error: { code: "bad_request", message: "role must be READ|EDIT|MANAGE" },
|
||||
});
|
||||
}
|
||||
const entry = await grantTeamProjectAccess(config.prisma, {
|
||||
projectId,
|
||||
role,
|
||||
createdByUserId: auth.user.id,
|
||||
...(typeof body.teamId === "string" ? { teamId: body.teamId } : {}),
|
||||
...(typeof body.teamSlug === "string" ? { teamSlug: body.teamSlug } : {}),
|
||||
});
|
||||
return entry;
|
||||
} catch (err) {
|
||||
return handleRouteError(reply, err);
|
||||
}
|
||||
});
|
||||
|
||||
app.delete(
|
||||
"/api/org/:orgSlug/projects/:projectId/team-access/:teamId",
|
||||
async (request, reply) => {
|
||||
try {
|
||||
const { orgSlug, projectId, teamId } = request.params as {
|
||||
orgSlug: string;
|
||||
projectId: string;
|
||||
teamId: string;
|
||||
};
|
||||
const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug });
|
||||
if (auth === null) return;
|
||||
await requireOrgProject(guardDeps, auth.organization.id, projectId);
|
||||
const count = await revokeTeamProjectAccess(config.prisma, { projectId, teamId });
|
||||
return { revoked: count };
|
||||
} catch (err) {
|
||||
return handleRouteError(reply, err);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function parseRole(value: unknown): PermissionRole | null {
|
||||
if (typeof value !== "string") return null;
|
||||
return (ROLES as readonly string[]).includes(value) ? (value as PermissionRole) : null;
|
||||
}
|
||||
Reference in New Issue
Block a user