Files
curriculum-project-hub/hub/src/admin/routes/feishuApplicationConnectionRoutes.ts
T

137 lines
5.4 KiB
TypeScript

import type { PrismaClient } from "@prisma/client";
import type { FastifyInstance } from "fastify";
import { FeishuApplicationConnectionService } from "../../connections/feishuApplicationConnections.js";
import { FeishuReadinessError, type FeishuReadinessProbe } from "../../connections/feishuReadiness.js";
import type { LocalSecretEnvelope } from "../../security/secretEnvelope.js";
import { requireOrgRole, type GuardDeps } from "../auth/guards.js";
import { handleRouteError } from "../errors.js";
export interface FeishuApplicationConnectionRouteConfig {
readonly prisma: PrismaClient;
readonly sessionSecret: string;
readonly secretEnvelope: LocalSecretEnvelope;
readonly readinessProbe?: FeishuReadinessProbe;
}
export async function registerFeishuApplicationConnectionRoutes(
app: FastifyInstance,
config: FeishuApplicationConnectionRouteConfig,
): Promise<void> {
const guardDeps: GuardDeps = { prisma: config.prisma, sessionSecret: config.sessionSecret };
const connections = new FeishuApplicationConnectionService(
config.prisma,
config.secretEnvelope,
config.readinessProbe,
);
app.get("/api/org/:orgSlug/feishu-application-connection", async (request, reply) => {
try {
const { orgSlug } = request.params as { orgSlug: string };
const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug });
if (auth === null) return;
return { connection: await connections.readMetadata(auth.organization.id) };
} catch (error) {
request.log.error({
requestId: request.id,
operation: "feishu_application_connection.read",
errorCode: "feishu_application_connection_read_failed",
}, "Feishu Application Connection read failed");
return handleRouteError(reply, error);
}
});
app.put("/api/org/:orgSlug/feishu-application-connection", 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 = parseBody(request.body);
const result = await connections.rotateCustomerApplication({
organizationId: auth.organization.id,
actorUserId: auth.user.id,
...body,
});
request.log.info({
organizationId: auth.organization.id,
connectionId: result.id,
status: result.status,
secretVersion: result.activeVersion,
keyId: result.keyId,
}, result.created ? "Feishu Application Connection created" : "Feishu Application Connection rotated");
const { created, ...metadata } = result;
return reply.status(created ? 201 : 200).send(metadata);
} catch (error) {
const facts = error instanceof FeishuReadinessError
? {
errorCode: error.code,
failureCategory: error.category,
...(error.upstreamStatus !== undefined ? { upstreamStatus: error.upstreamStatus } : {}),
}
: { errorCode: "feishu_application_connection_write_failed" };
request.log.error({
requestId: request.id,
operation: "feishu_application_connection.rotate",
...facts,
}, "Feishu Application Connection mutation failed");
return handleRouteError(reply, error);
}
});
app.delete("/api/org/:orgSlug/feishu-application-connection", async (request, reply) => {
try {
const { orgSlug } = request.params as { orgSlug: string };
const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug });
if (auth === null) return;
const result = await connections.disable({
organizationId: auth.organization.id,
actorUserId: auth.user.id,
});
request.log.info({
organizationId: auth.organization.id,
connectionId: result.id,
status: result.status,
secretVersion: result.activeVersion,
keyId: result.keyId,
}, "Feishu Application Connection disabled");
return reply.send(result);
} catch (error) {
request.log.error({
requestId: request.id,
operation: "feishu_application_connection.disable",
errorCode: "feishu_application_connection_disable_failed",
}, "Feishu Application Connection disable failed");
return handleRouteError(reply, error);
}
});
}
function parseBody(value: unknown): {
readonly appId: string;
readonly appSecret: string;
readonly botOpenId: string;
readonly verificationToken?: string;
readonly encryptKey?: string;
} {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
throw new Error("invalid Feishu Application credential body");
}
const body = value as Record<string, unknown>;
for (const name of ["appId", "appSecret", "botOpenId"] as const) {
if (typeof body[name] !== "string") throw new Error(`${name} is required`);
}
for (const name of ["verificationToken", "encryptKey"] as const) {
if (body[name] !== undefined && typeof body[name] !== "string") {
throw new Error(`${name} must be a string`);
}
}
return {
appId: body["appId"] as string,
appSecret: body["appSecret"] as string,
botOpenId: body["botOpenId"] as string,
...(body["verificationToken"] !== undefined
? { verificationToken: body["verificationToken"] as string }
: {}),
...(body["encryptKey"] !== undefined ? { encryptKey: body["encryptKey"] as string } : {}),
};
}