Files
new-api-key-usage/apps/server/src/app.ts
T
skycurtain d47cb5543c feat(engine): 实现同步引擎与 engine_status 快照
apps/engine 独立进程(syncRound + 防重叠调度);上游 log/token;QPU 采样;
server 读 engine_status;时刻出参 toUnixSec;中文日志;zod-openapi 扩展顺序修复。
2026-10-07 12:54:13 +08:00

127 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { OpenAPIHono } from "@hono/zod-openapi";
import { apiReference } from "@scalar/hono-api-reference";
import { ERR, appLogger } from "@new-api-key-usage/core";
import { createRepos, type Db } from "@new-api-key-usage/db";
import { jsonError, type AppVars } from "./middleware.ts";
import { adminSessionRoutes } from "./routes/admin-session.ts";
import { adminEngineRoutes } from "./routes/admin-engine.ts";
import { adminKeyRoutes } from "./routes/admin-keys.ts";
import { adminMemberRoutes } from "./routes/admin-members.ts";
import { adminUsageRoutes } from "./routes/admin-usage.ts";
import { adminPaymentRoutes } from "./routes/admin-payments.ts";
import { adminAuditRoutes } from "./routes/admin-audit.ts";
import { memberViewRoutes } from "./routes/member-view.ts";
export interface CreateAppOptions {
db: Db;
skKeyEncryptionSecret: string;
newapiBase: string;
cookieSecure?: boolean;
adminInitUsername?: string;
adminInitPassword?: string;
/** 可注入的上游验证(测试);默认走 real HTTP */
fetchTokenUsage?: AppVars["fetchTokenUsage"];
}
export function createApp(opts: CreateAppOptions) {
const app = new OpenAPIHono<{ Variables: AppVars }>({
defaultHook: (result, c) => {
if (!result.success) {
const fields: Record<string, string> = {};
for (const issue of result.error.issues) {
const key = issue.path.join(".") || "body";
fields[key] = issue.message;
}
return jsonError(c, 422, ERR.validation_failed(fields));
}
},
});
const repos = createRepos(opts.db);
app.use("*", async (c, next) => {
const start = Date.now();
try {
await next();
} finally {
appLogger("http").info(
`${c.req.method} ${c.req.path} -> ${c.res.status} | ${Date.now() - start}ms`,
);
}
});
app.use("*", async (c, next) => {
c.set("repos", repos);
c.set("skKeyEncryptionSecret", opts.skKeyEncryptionSecret);
c.set("newapiBase", opts.newapiBase);
c.set("cookieSecure", opts.cookieSecure ?? true);
if (opts.fetchTokenUsage) c.set("fetchTokenUsage", opts.fetchTokenUsage);
if (opts.adminInitUsername)
c.set("adminInitUsername", opts.adminInitUsername);
if (opts.adminInitPassword)
c.set("adminInitPassword", opts.adminInitPassword);
await next();
});
app.notFound((c) => {
if (c.req.path.startsWith("/api")) {
return jsonError(c, 404, ERR.not_found());
}
return c.text("Not Found", 404);
});
app.onError((err, c) => {
appLogger("server").error(`未捕获错误 ${c.req.method} ${c.req.path}:${err}`);
return jsonError(c, 500, ERR.internal());
});
app.openAPIRegistry.registerComponent("securitySchemes", "adminSession", {
type: "apiKey",
in: "cookie",
name: "admin_session",
description: "管理面会话 cookie(HttpOnly · Path=/api/admin)",
});
app.openAPIRegistry.registerComponent("securitySchemes", "memberToken", {
type: "http",
scheme: "bearer",
description: "成员视图 Bearer 令牌",
});
app.doc("/api/openapi.json", {
openapi: "3.1.0",
info: {
title: "new-api-key-usage",
version: "0.0.0",
description:
"NewAPI key 用量统计——管理面 `/api/admin/*`(会话 cookie)与成员视图 `/api/member/*`(Bearer)。成功响应无信封,错误见 ErrorEnvelope。",
},
tags: [
{ name: "引导与会话", description: "三态判定 / 登录 / 改密" },
{ name: "引擎", description: "同步引擎状态" },
{ name: "Key 管理", description: "登记 / 启停 / 归属" },
{ name: "成员管理", description: "开户 / 令牌 / 启停" },
{ name: "成员视图", description: "令牌自助查询" },
],
});
app.get(
"/api/docs",
apiReference({
theme: "kepler",
spec: { url: "/api/openapi.json" },
}),
);
const admin = new OpenAPIHono<{ Variables: AppVars }>();
admin.route("/", adminSessionRoutes());
admin.route("/", adminEngineRoutes());
admin.route("/", adminKeyRoutes());
admin.route("/", adminMemberRoutes());
admin.route("/", adminUsageRoutes());
admin.route("/", adminPaymentRoutes());
admin.route("/", adminAuditRoutes());
app.route("/api/admin", admin);
app.route("/api/member", memberViewRoutes());
return app;
}