forked from EduCraft/curriculum-project-hub
229 lines
7.9 KiB
TypeScript
229 lines
7.9 KiB
TypeScript
/**
|
|
* Org explorer HTTP routes under `/api/org/:orgSlug`.
|
|
*/
|
|
import type { PrismaClient } from "@prisma/client";
|
|
import type { FastifyInstance } from "fastify";
|
|
import {
|
|
archiveFolder,
|
|
archiveOrgProjectChatBinding,
|
|
archiveProject,
|
|
createOrgFolder,
|
|
createOrgProject,
|
|
getOrgProjectDetail,
|
|
listOrgExplorer,
|
|
moveOrgProjectToFolder,
|
|
renameFolder,
|
|
renameProject,
|
|
} from "../../org/explorer.js";
|
|
import { requireOrgRole, type GuardDeps } from "../auth/guards.js";
|
|
import { handleRouteError } from "../errors.js";
|
|
|
|
export interface ExplorerRouteConfig {
|
|
readonly prisma: PrismaClient;
|
|
readonly sessionSecret: string;
|
|
readonly projectWorkspaceRoot: string;
|
|
}
|
|
|
|
export async function registerExplorerRoutes(
|
|
app: FastifyInstance,
|
|
config: ExplorerRouteConfig,
|
|
): Promise<void> {
|
|
const guardDeps: GuardDeps = { prisma: config.prisma, sessionSecret: config.sessionSecret };
|
|
|
|
app.get("/api/org/:orgSlug/explorer", async (request, reply) => {
|
|
try {
|
|
const { orgSlug } = request.params as { orgSlug: string };
|
|
const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug });
|
|
if (auth === null) return;
|
|
return await listOrgExplorer(config.prisma, auth.organization.id);
|
|
} catch (err) {
|
|
return handleRouteError(reply, err);
|
|
}
|
|
});
|
|
|
|
app.post("/api/org/:orgSlug/folders", 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 {
|
|
name?: unknown;
|
|
parentId?: unknown;
|
|
sortKey?: unknown;
|
|
};
|
|
if (typeof body.name !== "string") {
|
|
return reply.status(400).send({
|
|
error: { code: "bad_request", message: "name is required" },
|
|
});
|
|
}
|
|
const folder = await createOrgFolder(config.prisma, {
|
|
organizationId: auth.organization.id,
|
|
name: body.name,
|
|
...(typeof body.parentId === "string" ? { parentId: body.parentId } : {}),
|
|
...(typeof body.sortKey === "string" ? { sortKey: body.sortKey } : {}),
|
|
});
|
|
return reply.status(201).send({
|
|
id: folder.id,
|
|
name: folder.name,
|
|
parentId: folder.parentId,
|
|
sortKey: folder.sortKey,
|
|
});
|
|
} catch (err) {
|
|
return handleRouteError(reply, err);
|
|
}
|
|
});
|
|
|
|
app.patch("/api/org/:orgSlug/folders/:folderId", async (request, reply) => {
|
|
try {
|
|
const { orgSlug, folderId } = request.params as { orgSlug: string; folderId: string };
|
|
const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug });
|
|
if (auth === null) return;
|
|
const body = request.body as {
|
|
name?: unknown;
|
|
sortKey?: unknown;
|
|
parentId?: unknown;
|
|
};
|
|
const folder = await renameFolder(config.prisma, {
|
|
organizationId: auth.organization.id,
|
|
folderId,
|
|
...(typeof body.name === "string" ? { name: body.name } : {}),
|
|
...(typeof body.sortKey === "string" ? { sortKey: body.sortKey } : {}),
|
|
...(body.parentId === null || typeof body.parentId === "string"
|
|
? { parentId: body.parentId as string | null }
|
|
: {}),
|
|
});
|
|
return {
|
|
id: folder.id,
|
|
name: folder.name,
|
|
parentId: folder.parentId,
|
|
sortKey: folder.sortKey,
|
|
};
|
|
} catch (err) {
|
|
return handleRouteError(reply, err);
|
|
}
|
|
});
|
|
|
|
app.post("/api/org/:orgSlug/folders/:folderId/archive", async (request, reply) => {
|
|
try {
|
|
const { orgSlug, folderId } = request.params as { orgSlug: string; folderId: string };
|
|
const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug });
|
|
if (auth === null) return;
|
|
return await archiveFolder(config.prisma, {
|
|
organizationId: auth.organization.id,
|
|
folderId,
|
|
});
|
|
} catch (err) {
|
|
return handleRouteError(reply, err);
|
|
}
|
|
});
|
|
|
|
app.post("/api/org/:orgSlug/projects", 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 { name?: unknown; folderId?: unknown };
|
|
if (typeof body.name !== "string") {
|
|
return reply.status(400).send({
|
|
error: { code: "bad_request", message: "name is required" },
|
|
});
|
|
}
|
|
const result = await createOrgProject(config.prisma, {
|
|
organizationId: auth.organization.id,
|
|
actorFeishuOpenId: auth.feishuOpenId,
|
|
name: body.name,
|
|
workspaceRoot: config.projectWorkspaceRoot,
|
|
...(typeof body.folderId === "string" ? { folderId: body.folderId } : {}),
|
|
});
|
|
return reply.status(201).send(result);
|
|
} catch (err) {
|
|
return handleRouteError(reply, err);
|
|
}
|
|
});
|
|
|
|
app.get("/api/org/:orgSlug/projects/:projectId", 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;
|
|
return await getOrgProjectDetail(config.prisma, {
|
|
organizationId: auth.organization.id,
|
|
projectId,
|
|
});
|
|
} catch (err) {
|
|
return handleRouteError(reply, err);
|
|
}
|
|
});
|
|
|
|
app.patch("/api/org/:orgSlug/projects/:projectId", 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;
|
|
const body = request.body as { name?: unknown };
|
|
if (typeof body.name !== "string") {
|
|
return reply.status(400).send({
|
|
error: { code: "bad_request", message: "name is required" },
|
|
});
|
|
}
|
|
return await renameProject(config.prisma, {
|
|
organizationId: auth.organization.id,
|
|
projectId,
|
|
name: body.name,
|
|
});
|
|
} catch (err) {
|
|
return handleRouteError(reply, err);
|
|
}
|
|
});
|
|
|
|
app.patch("/api/org/:orgSlug/projects/:projectId/folder", 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;
|
|
const body = request.body as { folderId?: unknown };
|
|
if (body.folderId !== null && typeof body.folderId !== "string") {
|
|
return reply.status(400).send({
|
|
error: { code: "bad_request", message: "folderId must be string or null" },
|
|
});
|
|
}
|
|
return await moveOrgProjectToFolder(config.prisma, {
|
|
organizationId: auth.organization.id,
|
|
projectId,
|
|
folderId: body.folderId as string | null,
|
|
});
|
|
} catch (err) {
|
|
return handleRouteError(reply, err);
|
|
}
|
|
});
|
|
|
|
app.post("/api/org/:orgSlug/projects/:projectId/archive", 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;
|
|
return await archiveProject(config.prisma, {
|
|
organizationId: auth.organization.id,
|
|
projectId,
|
|
});
|
|
} catch (err) {
|
|
return handleRouteError(reply, err);
|
|
}
|
|
});
|
|
|
|
app.post("/api/org/:orgSlug/projects/:projectId/binding/archive", 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;
|
|
return await archiveOrgProjectChatBinding(config.prisma, {
|
|
organizationId: auth.organization.id,
|
|
projectId,
|
|
actorFeishuOpenId: auth.feishuOpenId,
|
|
});
|
|
} catch (err) {
|
|
return handleRouteError(reply, err);
|
|
}
|
|
});
|
|
}
|