131 行
3.6 KiB
JavaScript
131 行
3.6 KiB
JavaScript
const crypto = require("node:crypto");
|
|
const fs = require("node:fs/promises");
|
|
|
|
require("dotenv/config");
|
|
|
|
const fastify = require("fastify");
|
|
const cors = require("@fastify/cors");
|
|
const cookie = require("@fastify/cookie");
|
|
const multipart = require("@fastify/multipart");
|
|
|
|
const { getConfig } = require("./config");
|
|
const authPlugin = require("./plugins/auth");
|
|
const prismaPlugin = require("./plugins/prisma");
|
|
const authRoutes = require("./routes/auth");
|
|
const meRoutes = require("./routes/me");
|
|
const reimbursementRoutes = require("./routes/reimbursements");
|
|
const invoicePoolRoutes = require("./routes/invoicePool");
|
|
const workflowRoutes = require("./routes/workflow");
|
|
const delegationRoutes = require("./routes/delegations");
|
|
const auditRoutes = require("./routes/audit");
|
|
const { startDelegationRevokeJob } = require("./jobs/delegationRevokeJob");
|
|
const { startOcrJob } = require("./jobs/ocrJob");
|
|
const { AppError } = require("./utils/AppError");
|
|
|
|
async function buildApp() {
|
|
const config = getConfig();
|
|
|
|
const app = fastify({
|
|
logger: true,
|
|
trustProxy: true,
|
|
genReqId: () => crypto.randomUUID(),
|
|
});
|
|
|
|
app.decorate("config", config);
|
|
await fs.mkdir(config.storageDir, { recursive: true });
|
|
|
|
await app.register(cookie);
|
|
await app.register(multipart, {
|
|
attachFieldsToBody: false,
|
|
limits: {
|
|
fileSize: config.maxUploadSizeBytes,
|
|
files: 1,
|
|
},
|
|
});
|
|
|
|
const allowedOrigins = config.appOrigin
|
|
? config.appOrigin
|
|
.split(",")
|
|
.map((s) => s.trim())
|
|
.filter(Boolean)
|
|
: null;
|
|
|
|
await app.register(cors, {
|
|
credentials: true,
|
|
origin: (origin, cb) => {
|
|
if (!origin) return cb(null, true);
|
|
if (!allowedOrigins) return cb(null, true);
|
|
if (allowedOrigins.includes(origin)) return cb(null, true);
|
|
return cb(new Error("CORS origin not allowed"), false);
|
|
},
|
|
});
|
|
|
|
app.addHook("onRequest", async (request, reply) => {
|
|
reply.header("x-request-id", request.id);
|
|
});
|
|
|
|
app.setNotFoundHandler(async () => {
|
|
throw new AppError({
|
|
code: "SYS_404",
|
|
message: "Not Found",
|
|
statusCode: 404,
|
|
});
|
|
});
|
|
|
|
app.setErrorHandler((error, request, reply) => {
|
|
if (error.code === "FST_REQ_FILE_TOO_LARGE") {
|
|
return reply.status(400).send({
|
|
requestId: request.id,
|
|
error: {
|
|
code: "VALID_001",
|
|
message: "参数校验失败",
|
|
details: { file: "file_too_large" },
|
|
},
|
|
});
|
|
}
|
|
|
|
if (error instanceof AppError) {
|
|
return reply.status(error.statusCode).send({
|
|
requestId: request.id,
|
|
error: {
|
|
code: error.code,
|
|
message: error.message,
|
|
details: error.details ?? {},
|
|
},
|
|
});
|
|
}
|
|
|
|
app.log.error({ err: error, requestId: request.id }, "Unhandled error");
|
|
return reply.status(500).send({
|
|
requestId: request.id,
|
|
error: {
|
|
code: "SYS_001",
|
|
message: "服务器内部错误",
|
|
details: {},
|
|
},
|
|
});
|
|
});
|
|
|
|
await app.register(prismaPlugin);
|
|
await app.register(authPlugin);
|
|
|
|
app.get("/health", async (request, reply) => {
|
|
return reply.send({ requestId: request.id, data: { ok: true } });
|
|
});
|
|
|
|
await app.register(authRoutes, { prefix: "/api/auth" });
|
|
await app.register(meRoutes, { prefix: "/api" });
|
|
await app.register(reimbursementRoutes, { prefix: "/api" });
|
|
await app.register(invoicePoolRoutes, { prefix: "/api" });
|
|
await app.register(workflowRoutes, { prefix: "/api" });
|
|
await app.register(delegationRoutes, { prefix: "/api" });
|
|
await app.register(auditRoutes, { prefix: "/api" });
|
|
|
|
startDelegationRevokeJob(app);
|
|
startOcrJob(app);
|
|
|
|
return app;
|
|
}
|
|
|
|
module.exports = { buildApp };
|