forked from EduCraft/curriculum-project-hub
feat: secure organization Feishu credentials
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
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 } : {}),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user