forked from EduCraft/curriculum-project-hub
137 lines
5.4 KiB
TypeScript
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 } : {}),
|
|
};
|
|
}
|