Files
curriculum-project-hub/hub/src/admin/routes/orgRoutes.ts
T
hongjr03 251ad43ca2 feat(hub): capability connection admin API + UI (ADR-0027)
Adds the admin surface for managing org-scoped capability credentials,
so operators can configure the Aliyun docmind AccessKey from the web UI
instead of needing a CLI or database access.

Backend:
- CapabilityConnectionService: rotate/list/read/disable, mirroring
  FeishuApplicationConnectionService but keyed by (orgId, capabilityId)
- capabilityReadiness: probeDocmindCredential validates the AccessKey by
  calling QueryDocParserStatus with a dummy id (400 = auth ok, 401/403 = bad)
- Admin routes: GET/PUT/DELETE /api/org/:slug/capability-connections/:capId
  + GET list, wired into orgRoutes

Frontend:
- api.ts: CapabilityConnection type + capabilityConnections/rotate/disable
  client methods
- /admin/org/:slug/capabilities page: lists known capabilities
  (pdf_to_md_bundle, audio_video_to_text), shows status/version/keyId,
  inline rotate/disable forms with AccessKey ID/Secret/Endpoint fields
- Nav item '能力' added to sidebar

Version bump 0.0.31 → 0.0.32.
2026-07-18 17:55:54 +08:00

153 lines
5.8 KiB
TypeScript

/**
* Mount point for `/api/org/:orgSlug/*` org-admin APIs.
*/
import type { PrismaClient } from "@prisma/client";
import type { FastifyInstance } from "fastify";
import { requireOrgRole, type GuardDeps } from "../auth/guards.js";
import { handleRouteError } from "../errors.js";
import {
ensureOrganizationProjectSettings,
setMembersCanCreateProjects,
} from "../../projectOnboarding.js";
import { registerAccessRoutes } from "./accessRoutes.js";
import { registerExplorerRoutes } from "./explorerRoutes.js";
import { registerMembersRoutes } from "./membersRoutes.js";
import { registerSessionsAndUsageRoutes } from "./sessionsRoutes.js";
import { registerTeamsRoutes } from "./teamsRoutes.js";
import { registerProviderConnectionRoutes } from "./providerConnectionRoutes.js";
import { registerCapacityRoutes } from "./capacityRoutes.js";
import { readSkillStoreRoot } from "../../agent/skillStore.js";
import { registerAgentConfigRoutes } from "./agentConfigRoutes.js";
import type { LocalSecretEnvelope } from "../../security/secretEnvelope.js";
import type { ProviderReadinessProbe } from "../../connections/providerReadiness.js";
import type { FeishuReadinessProbe } from "../../connections/feishuReadiness.js";
import { registerFeishuApplicationConnectionRoutes } from "./feishuApplicationConnectionRoutes.js";
import { registerCapabilityConnectionRoutes } from "./capabilityConnectionRoutes.js";
import type { CapabilityReadinessProbe } from "../../capability/capabilityReadiness.js";
export interface OrgRouteConfig {
readonly prisma: PrismaClient;
readonly sessionSecret: string;
readonly projectWorkspaceRoot: string;
readonly secretEnvelope: LocalSecretEnvelope;
readonly providerReadinessProbe?: ProviderReadinessProbe;
readonly feishuConnectionReadinessProbe?: FeishuReadinessProbe;
readonly capabilityReadinessProbe?: CapabilityReadinessProbe;
}
export async function registerOrgRoutes(app: FastifyInstance, config: OrgRouteConfig): Promise<void> {
const guardDeps: GuardDeps = { prisma: config.prisma, sessionSecret: config.sessionSecret };
app.get("/api/org/:orgSlug", async (request, reply) => {
try {
const { orgSlug } = request.params as { orgSlug: string };
const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug });
if (auth === null) return;
return {
organization: {
id: auth.organization.id,
slug: auth.organization.slug,
name: auth.organization.name,
status: auth.organization.status,
},
actorRole: auth.membershipRole,
};
} catch (err) {
return handleRouteError(reply, err);
}
});
app.get("/api/org/:orgSlug/settings", async (request, reply) => {
try {
const { orgSlug } = request.params as { orgSlug: string };
const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug });
if (auth === null) return;
const settings = await ensureOrganizationProjectSettings(config.prisma, auth.organization.id);
return {
membersCanCreateProjects: settings.membersCanCreateProjects,
};
} catch (err) {
return handleRouteError(reply, err);
}
});
app.patch("/api/org/:orgSlug/settings", 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 { membersCanCreateProjects?: unknown };
if (typeof body.membersCanCreateProjects !== "boolean") {
return reply.status(400).send({
error: { code: "bad_request", message: "membersCanCreateProjects must be a boolean" },
});
}
const settings = await setMembersCanCreateProjects(config.prisma, {
organizationId: auth.organization.id,
enabled: body.membersCanCreateProjects,
});
return {
membersCanCreateProjects: settings.membersCanCreateProjects,
};
} catch (err) {
return handleRouteError(reply, err);
}
});
await registerExplorerRoutes(app, {
prisma: config.prisma,
sessionSecret: config.sessionSecret,
projectWorkspaceRoot: config.projectWorkspaceRoot,
});
await registerMembersRoutes(app, {
prisma: config.prisma,
sessionSecret: config.sessionSecret,
});
await registerTeamsRoutes(app, {
prisma: config.prisma,
sessionSecret: config.sessionSecret,
});
await registerAccessRoutes(app, {
prisma: config.prisma,
sessionSecret: config.sessionSecret,
});
await registerCapacityRoutes(app, {
prisma: config.prisma,
sessionSecret: config.sessionSecret,
});
await registerAgentConfigRoutes(app, {
prisma: config.prisma,
sessionSecret: config.sessionSecret,
skillStoreRoot: readSkillStoreRoot(),
secretEnvelope: config.secretEnvelope,
});
await registerSessionsAndUsageRoutes(app, {
prisma: config.prisma,
sessionSecret: config.sessionSecret,
});
await registerProviderConnectionRoutes(app, {
prisma: config.prisma,
sessionSecret: config.sessionSecret,
secretEnvelope: config.secretEnvelope,
...(config.providerReadinessProbe !== undefined
? { providerReadinessProbe: config.providerReadinessProbe }
: {}),
});
await registerFeishuApplicationConnectionRoutes(app, {
prisma: config.prisma,
sessionSecret: config.sessionSecret,
secretEnvelope: config.secretEnvelope,
...(config.feishuConnectionReadinessProbe !== undefined
? { readinessProbe: config.feishuConnectionReadinessProbe }
: {}),
});
await registerCapabilityConnectionRoutes(app, {
prisma: config.prisma,
sessionSecret: config.sessionSecret,
secretEnvelope: config.secretEnvelope,
...(config.capabilityReadinessProbe !== undefined
? { readinessProbe: config.capabilityReadinessProbe }
: {}),
});
}