1174 lines
50 KiB
JavaScript
1174 lines
50 KiB
JavaScript
var __defProp = Object.defineProperty;
|
||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||
var __esm = (fn, res) => function __init() {
|
||
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
||
};
|
||
var __export = (target, all) => {
|
||
for (var name in all)
|
||
__defProp(target, name, { get: all[name], enumerable: true });
|
||
};
|
||
|
||
// server/_core/vite.ts
|
||
var vite_exports = {};
|
||
__export(vite_exports, {
|
||
serveStatic: () => serveStatic,
|
||
setupVite: () => setupVite
|
||
});
|
||
import express from "express";
|
||
import fs from "fs";
|
||
import { nanoid } from "nanoid";
|
||
import path from "path";
|
||
import { fileURLToPath } from "url";
|
||
import { createServer as createViteServer } from "vite";
|
||
async function setupVite(app, server) {
|
||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||
const serverOptions = {
|
||
middlewareMode: true,
|
||
hmr: { server },
|
||
allowedHosts: true
|
||
};
|
||
const vite = await createViteServer({
|
||
configFile: path.resolve(__dirname, "../../vite.config.ts"),
|
||
server: serverOptions,
|
||
appType: "custom"
|
||
});
|
||
app.use(vite.middlewares);
|
||
app.use("*", async (req, res, next) => {
|
||
const url = req.originalUrl;
|
||
try {
|
||
const clientTemplate = path.resolve(
|
||
__dirname,
|
||
"../..",
|
||
"client",
|
||
"index.html"
|
||
);
|
||
let template = await fs.promises.readFile(clientTemplate, "utf-8");
|
||
template = template.replace(
|
||
`src="/src/main.tsx"`,
|
||
`src="/src/main.tsx?v=${nanoid()}"`
|
||
);
|
||
const page = await vite.transformIndexHtml(url, template);
|
||
res.status(200).set({ "Content-Type": "text/html" }).end(page);
|
||
} catch (e) {
|
||
vite.ssrFixStacktrace(e);
|
||
next(e);
|
||
}
|
||
});
|
||
}
|
||
function serveStatic(app) {
|
||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||
const distPath = process.env.NODE_ENV === "development" ? path.resolve(__dirname, "../..", "dist", "public") : path.resolve(__dirname, "public");
|
||
if (!fs.existsSync(distPath)) {
|
||
console.error(
|
||
`Could not find the build directory: ${distPath}, make sure to build the client first`
|
||
);
|
||
}
|
||
app.use(express.static(distPath));
|
||
app.use("*", (_req, res) => {
|
||
res.sendFile(path.resolve(distPath, "index.html"));
|
||
});
|
||
}
|
||
var init_vite = __esm({
|
||
"server/_core/vite.ts"() {
|
||
"use strict";
|
||
}
|
||
});
|
||
|
||
// server/_core/static.ts
|
||
var static_exports = {};
|
||
__export(static_exports, {
|
||
serveStatic: () => serveStatic2
|
||
});
|
||
import express2 from "express";
|
||
import fs2 from "fs";
|
||
import path2 from "path";
|
||
import { fileURLToPath as fileURLToPath2 } from "url";
|
||
function serveStatic2(app) {
|
||
const __filename = fileURLToPath2(import.meta.url);
|
||
const __dirname = path2.dirname(__filename);
|
||
const distPath = path2.resolve(__dirname, "public");
|
||
if (!fs2.existsSync(distPath)) {
|
||
console.error(
|
||
`[Static] Could not find build directory: ${distPath}`
|
||
);
|
||
console.error(`[Static] Make sure to run 'pnpm build' first`);
|
||
app.use("*", (_req, res) => {
|
||
res.status(503).send("Service starting up, please wait...");
|
||
});
|
||
return;
|
||
}
|
||
console.log(`[Static] Serving files from: ${distPath}`);
|
||
app.use(express2.static(distPath));
|
||
app.use("*", (_req, res) => {
|
||
res.sendFile(path2.resolve(distPath, "index.html"));
|
||
});
|
||
}
|
||
var init_static = __esm({
|
||
"server/_core/static.ts"() {
|
||
"use strict";
|
||
}
|
||
});
|
||
|
||
// server/_core/index.ts
|
||
import "dotenv/config";
|
||
import express3 from "express";
|
||
import cookieParser from "cookie-parser";
|
||
import { createServer } from "http";
|
||
import net from "net";
|
||
import { createExpressMiddleware } from "@trpc/server/adapters/express";
|
||
|
||
// shared/const.ts
|
||
var COOKIE_NAME = "app_session_id";
|
||
var ONE_YEAR_MS = 1e3 * 60 * 60 * 24 * 365;
|
||
var AXIOS_TIMEOUT_MS = 3e4;
|
||
var UNAUTHED_ERR_MSG = "Please login (10001)";
|
||
var NOT_ADMIN_ERR_MSG = "You do not have required permission (10002)";
|
||
|
||
// server/db.ts
|
||
import { eq } from "drizzle-orm";
|
||
import { drizzle } from "drizzle-orm/mysql2";
|
||
|
||
// drizzle/schema.ts
|
||
import { int, mysqlEnum, mysqlTable, text, timestamp, varchar } from "drizzle-orm/mysql-core";
|
||
var users = mysqlTable("users", {
|
||
/**
|
||
* Surrogate primary key. Auto-incremented numeric value managed by the database.
|
||
* Use this for relations between tables.
|
||
*/
|
||
id: int("id").autoincrement().primaryKey(),
|
||
/** NAC_AI OAuth identifier (openId) returned from the OAuth callback. Unique per user. */
|
||
openId: varchar("openId", { length: 64 }).notNull().unique(),
|
||
name: text("name"),
|
||
email: varchar("email", { length: 320 }),
|
||
loginMethod: varchar("loginMethod", { length: 64 }),
|
||
role: mysqlEnum("role", ["user", "admin"]).default("user").notNull(),
|
||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
||
lastSignedIn: timestamp("lastSignedIn").defaultNow().notNull()
|
||
});
|
||
|
||
// server/_core/env.ts
|
||
var ENV = {
|
||
appId: process.env.VITE_APP_ID ?? "",
|
||
cookieSecret: process.env.JWT_SECRET ?? "",
|
||
databaseUrl: process.env.DATABASE_URL ?? "",
|
||
oAuthServerUrl: process.env.OAUTH_SERVER_URL ?? "",
|
||
ownerOpenId: process.env.OWNER_OPEN_ID ?? "",
|
||
isProduction: process.env.NODE_ENV === "production",
|
||
forgeApiUrl: process.env.BUILT_IN_FORGE_API_URL ?? "",
|
||
forgeApiKey: process.env.BUILT_IN_FORGE_API_KEY ?? ""
|
||
// 注意:NAC数据库密钥请通过 server/secrets.ts 中的函数访问
|
||
// 例如:getNacMysqlUrl(), getNacMongoUrl(), getNacJwtSecret()
|
||
};
|
||
|
||
// server/db.ts
|
||
var _db = null;
|
||
async function getDb() {
|
||
if (!_db && process.env.DATABASE_URL) {
|
||
try {
|
||
_db = drizzle(process.env.DATABASE_URL);
|
||
} catch (error) {
|
||
console.warn("[Database] Failed to connect:", error);
|
||
_db = null;
|
||
}
|
||
}
|
||
return _db;
|
||
}
|
||
async function upsertUser(user) {
|
||
if (!user.openId) {
|
||
throw new Error("User openId is required for upsert");
|
||
}
|
||
const db2 = await getDb();
|
||
if (!db2) {
|
||
console.warn("[Database] Cannot upsert user: database not available");
|
||
return;
|
||
}
|
||
try {
|
||
const values = {
|
||
openId: user.openId
|
||
};
|
||
const updateSet = {};
|
||
const textFields = ["name", "email", "loginMethod"];
|
||
const assignNullable = (field) => {
|
||
const value = user[field];
|
||
if (value === void 0) return;
|
||
const normalized = value ?? null;
|
||
values[field] = normalized;
|
||
updateSet[field] = normalized;
|
||
};
|
||
textFields.forEach(assignNullable);
|
||
if (user.lastSignedIn !== void 0) {
|
||
values.lastSignedIn = user.lastSignedIn;
|
||
updateSet.lastSignedIn = user.lastSignedIn;
|
||
}
|
||
if (user.role !== void 0) {
|
||
values.role = user.role;
|
||
updateSet.role = user.role;
|
||
} else if (user.openId === ENV.ownerOpenId) {
|
||
values.role = "admin";
|
||
updateSet.role = "admin";
|
||
}
|
||
if (!values.lastSignedIn) {
|
||
values.lastSignedIn = /* @__PURE__ */ new Date();
|
||
}
|
||
if (Object.keys(updateSet).length === 0) {
|
||
updateSet.lastSignedIn = /* @__PURE__ */ new Date();
|
||
}
|
||
await db2.insert(users).values(values).onDuplicateKeyUpdate({
|
||
set: updateSet
|
||
});
|
||
} catch (error) {
|
||
console.error("[Database] Failed to upsert user:", error);
|
||
throw error;
|
||
}
|
||
}
|
||
async function getUserByOpenId(openId) {
|
||
const db2 = await getDb();
|
||
if (!db2) {
|
||
console.warn("[Database] Cannot get user: database not available");
|
||
return void 0;
|
||
}
|
||
const result = await db2.select().from(users).where(eq(users.openId, openId)).limit(1);
|
||
return result.length > 0 ? result[0] : void 0;
|
||
}
|
||
|
||
// server/_core/cookies.ts
|
||
function isSecureRequest(req) {
|
||
if (req.protocol === "https") return true;
|
||
const forwardedProto = req.headers["x-forwarded-proto"];
|
||
if (!forwardedProto) return false;
|
||
const protoList = Array.isArray(forwardedProto) ? forwardedProto : forwardedProto.split(",");
|
||
return protoList.some((proto) => proto.trim().toLowerCase() === "https");
|
||
}
|
||
function getSessionCookieOptions(req) {
|
||
return {
|
||
httpOnly: true,
|
||
path: "/",
|
||
sameSite: "none",
|
||
secure: isSecureRequest(req)
|
||
};
|
||
}
|
||
|
||
// shared/_core/errors.ts
|
||
var HttpError = class extends Error {
|
||
constructor(statusCode, message) {
|
||
super(message);
|
||
this.statusCode = statusCode;
|
||
this.name = "HttpError";
|
||
}
|
||
};
|
||
var ForbiddenError = (msg) => new HttpError(403, msg);
|
||
|
||
// server/_core/sdk.ts
|
||
import axios from "axios";
|
||
import { parse as parseCookieHeader } from "cookie";
|
||
import { SignJWT, jwtVerify } from "jose";
|
||
var isNonEmptyString = (value) => typeof value === "string" && value.length > 0;
|
||
var EXCHANGE_TOKEN_PATH = `/webdev.v1.WebDevAuthPublicService/ExchangeToken`;
|
||
var GET_USER_INFO_PATH = `/webdev.v1.WebDevAuthPublicService/GetUserInfo`;
|
||
var GET_USER_INFO_WITH_JWT_PATH = `/webdev.v1.WebDevAuthPublicService/GetUserInfoWithJwt`;
|
||
var OAuthService = class {
|
||
constructor(client2) {
|
||
this.client = client2;
|
||
console.log("[OAuth] Initialized with baseURL:", ENV.oAuthServerUrl);
|
||
if (!ENV.oAuthServerUrl) {
|
||
console.error(
|
||
"[OAuth] ERROR: OAUTH_SERVER_URL is not configured! Set OAUTH_SERVER_URL environment variable."
|
||
);
|
||
}
|
||
}
|
||
decodeState(state) {
|
||
const redirectUri = atob(state);
|
||
return redirectUri;
|
||
}
|
||
async getTokenByCode(code, state) {
|
||
const payload = {
|
||
clientId: ENV.appId,
|
||
grantType: "authorization_code",
|
||
code,
|
||
redirectUri: this.decodeState(state)
|
||
};
|
||
const { data } = await this.client.post(
|
||
EXCHANGE_TOKEN_PATH,
|
||
payload
|
||
);
|
||
return data;
|
||
}
|
||
async getUserInfoByToken(token) {
|
||
const { data } = await this.client.post(
|
||
GET_USER_INFO_PATH,
|
||
{
|
||
accessToken: token.accessToken
|
||
}
|
||
);
|
||
return data;
|
||
}
|
||
};
|
||
var createOAuthHttpClient = () => axios.create({
|
||
baseURL: ENV.oAuthServerUrl,
|
||
timeout: AXIOS_TIMEOUT_MS
|
||
});
|
||
var SDKServer = class {
|
||
client;
|
||
oauthService;
|
||
constructor(client2 = createOAuthHttpClient()) {
|
||
this.client = client2;
|
||
this.oauthService = new OAuthService(this.client);
|
||
}
|
||
deriveLoginMethod(platforms, fallback) {
|
||
if (fallback && fallback.length > 0) return fallback;
|
||
if (!Array.isArray(platforms) || platforms.length === 0) return null;
|
||
const set = new Set(
|
||
platforms.filter((p) => typeof p === "string")
|
||
);
|
||
if (set.has("REGISTERED_PLATFORM_EMAIL")) return "email";
|
||
if (set.has("REGISTERED_PLATFORM_GOOGLE")) return "google";
|
||
if (set.has("REGISTERED_PLATFORM_APPLE")) return "apple";
|
||
if (set.has("REGISTERED_PLATFORM_MICROSOFT") || set.has("REGISTERED_PLATFORM_AZURE"))
|
||
return "microsoft";
|
||
if (set.has("REGISTERED_PLATFORM_GITHUB")) return "github";
|
||
const first = Array.from(set)[0];
|
||
return first ? first.toLowerCase() : null;
|
||
}
|
||
/**
|
||
* Exchange OAuth authorization code for access token
|
||
* @example
|
||
* const tokenResponse = await sdk.exchangeCodeForToken(code, state);
|
||
*/
|
||
async exchangeCodeForToken(code, state) {
|
||
return this.oauthService.getTokenByCode(code, state);
|
||
}
|
||
/**
|
||
* Get user information using access token
|
||
* @example
|
||
* const userInfo = await sdk.getUserInfo(tokenResponse.accessToken);
|
||
*/
|
||
async getUserInfo(accessToken) {
|
||
const data = await this.oauthService.getUserInfoByToken({
|
||
accessToken
|
||
});
|
||
const loginMethod = this.deriveLoginMethod(
|
||
data?.platforms,
|
||
data?.platform ?? data.platform ?? null
|
||
);
|
||
return {
|
||
...data,
|
||
platform: loginMethod,
|
||
loginMethod
|
||
};
|
||
}
|
||
parseCookies(cookieHeader) {
|
||
if (!cookieHeader) {
|
||
return /* @__PURE__ */ new Map();
|
||
}
|
||
const parsed = parseCookieHeader(cookieHeader);
|
||
return new Map(Object.entries(parsed));
|
||
}
|
||
getSessionSecret() {
|
||
const secret = ENV.cookieSecret;
|
||
return new TextEncoder().encode(secret);
|
||
}
|
||
/**
|
||
* Create a session token for a NAC_AI user openId
|
||
* @example
|
||
* const sessionToken = await sdk.createSessionToken(userInfo.openId);
|
||
*/
|
||
async createSessionToken(openId, options = {}) {
|
||
return this.signSession(
|
||
{
|
||
openId,
|
||
appId: ENV.appId,
|
||
name: options.name || ""
|
||
},
|
||
options
|
||
);
|
||
}
|
||
async signSession(payload, options = {}) {
|
||
const issuedAt = Date.now();
|
||
const expiresInMs = options.expiresInMs ?? ONE_YEAR_MS;
|
||
const expirationSeconds = Math.floor((issuedAt + expiresInMs) / 1e3);
|
||
const secretKey = this.getSessionSecret();
|
||
return new SignJWT({
|
||
openId: payload.openId,
|
||
appId: payload.appId,
|
||
name: payload.name
|
||
}).setProtectedHeader({ alg: "HS256", typ: "JWT" }).setExpirationTime(expirationSeconds).sign(secretKey);
|
||
}
|
||
async verifySession(cookieValue) {
|
||
if (!cookieValue) {
|
||
console.warn("[Auth] Missing session cookie");
|
||
return null;
|
||
}
|
||
try {
|
||
const secretKey = this.getSessionSecret();
|
||
const { payload } = await jwtVerify(cookieValue, secretKey, {
|
||
algorithms: ["HS256"]
|
||
});
|
||
const { openId, appId, name } = payload;
|
||
if (!isNonEmptyString(openId) || !isNonEmptyString(appId) || !isNonEmptyString(name)) {
|
||
console.warn("[Auth] Session payload missing required fields");
|
||
return null;
|
||
}
|
||
return {
|
||
openId,
|
||
appId,
|
||
name
|
||
};
|
||
} catch (error) {
|
||
console.warn("[Auth] Session verification failed", String(error));
|
||
return null;
|
||
}
|
||
}
|
||
async getUserInfoWithJwt(jwtToken) {
|
||
const payload = {
|
||
jwtToken,
|
||
projectId: ENV.appId
|
||
};
|
||
const { data } = await this.client.post(
|
||
GET_USER_INFO_WITH_JWT_PATH,
|
||
payload
|
||
);
|
||
const loginMethod = this.deriveLoginMethod(
|
||
data?.platforms,
|
||
data?.platform ?? data.platform ?? null
|
||
);
|
||
return {
|
||
...data,
|
||
platform: loginMethod,
|
||
loginMethod
|
||
};
|
||
}
|
||
async authenticateRequest(req) {
|
||
const cookies = this.parseCookies(req.headers.cookie);
|
||
const sessionCookie = cookies.get(COOKIE_NAME);
|
||
const session = await this.verifySession(sessionCookie);
|
||
if (!session) {
|
||
throw ForbiddenError("Invalid session cookie");
|
||
}
|
||
const sessionUserId = session.openId;
|
||
const signedInAt = /* @__PURE__ */ new Date();
|
||
let user = await getUserByOpenId(sessionUserId);
|
||
if (!user) {
|
||
try {
|
||
const userInfo = await this.getUserInfoWithJwt(sessionCookie ?? "");
|
||
await upsertUser({
|
||
openId: userInfo.openId,
|
||
name: userInfo.name || null,
|
||
email: userInfo.email ?? null,
|
||
loginMethod: userInfo.loginMethod ?? userInfo.platform ?? null,
|
||
lastSignedIn: signedInAt
|
||
});
|
||
user = await getUserByOpenId(userInfo.openId);
|
||
} catch (error) {
|
||
console.error("[Auth] Failed to sync user from OAuth:", error);
|
||
throw ForbiddenError("Failed to sync user info");
|
||
}
|
||
}
|
||
if (!user) {
|
||
throw ForbiddenError("User not found");
|
||
}
|
||
await upsertUser({
|
||
openId: user.openId,
|
||
lastSignedIn: signedInAt
|
||
});
|
||
return user;
|
||
}
|
||
};
|
||
var sdk = new SDKServer();
|
||
|
||
// server/_core/oauth.ts
|
||
function getQueryParam(req, key) {
|
||
const value = req.query[key];
|
||
return typeof value === "string" ? value : void 0;
|
||
}
|
||
function registerOAuthRoutes(app) {
|
||
app.get("/api/oauth/callback", async (req, res) => {
|
||
const code = getQueryParam(req, "code");
|
||
const state = getQueryParam(req, "state");
|
||
if (!code || !state) {
|
||
res.status(400).json({ error: "code and state are required" });
|
||
return;
|
||
}
|
||
try {
|
||
const tokenResponse = await sdk.exchangeCodeForToken(code, state);
|
||
const userInfo = await sdk.getUserInfo(tokenResponse.accessToken);
|
||
if (!userInfo.openId) {
|
||
res.status(400).json({ error: "openId missing from user info" });
|
||
return;
|
||
}
|
||
await upsertUser({
|
||
openId: userInfo.openId,
|
||
name: userInfo.name || null,
|
||
email: userInfo.email ?? null,
|
||
loginMethod: userInfo.loginMethod ?? userInfo.platform ?? null,
|
||
lastSignedIn: /* @__PURE__ */ new Date()
|
||
});
|
||
const sessionToken = await sdk.createSessionToken(userInfo.openId, {
|
||
name: userInfo.name || "",
|
||
expiresInMs: ONE_YEAR_MS
|
||
});
|
||
const cookieOptions = getSessionCookieOptions(req);
|
||
res.cookie(COOKIE_NAME, sessionToken, { ...cookieOptions, maxAge: ONE_YEAR_MS });
|
||
res.redirect(302, "/");
|
||
} catch (error) {
|
||
console.error("[OAuth] Callback failed", error);
|
||
res.status(500).json({ error: "OAuth callback failed" });
|
||
}
|
||
});
|
||
}
|
||
|
||
// server/routers.ts
|
||
import { z as z2 } from "zod";
|
||
import { TRPCError as TRPCError3 } from "@trpc/server";
|
||
|
||
// server/_core/trpc.ts
|
||
import { initTRPC, TRPCError } from "@trpc/server";
|
||
import superjson from "superjson";
|
||
var t = initTRPC.context().create({
|
||
transformer: superjson
|
||
});
|
||
var router = t.router;
|
||
var publicProcedure = t.procedure;
|
||
var requireUser = t.middleware(async (opts) => {
|
||
const { ctx, next } = opts;
|
||
if (!ctx.user) {
|
||
throw new TRPCError({ code: "UNAUTHORIZED", message: UNAUTHED_ERR_MSG });
|
||
}
|
||
return next({
|
||
ctx: {
|
||
...ctx,
|
||
user: ctx.user
|
||
}
|
||
});
|
||
});
|
||
var protectedProcedure = t.procedure.use(requireUser);
|
||
var adminProcedure = t.procedure.use(
|
||
t.middleware(async (opts) => {
|
||
const { ctx, next } = opts;
|
||
if (!ctx.user || ctx.user.role !== "admin") {
|
||
throw new TRPCError({ code: "FORBIDDEN", message: NOT_ADMIN_ERR_MSG });
|
||
}
|
||
return next({
|
||
ctx: {
|
||
...ctx,
|
||
user: ctx.user
|
||
}
|
||
});
|
||
})
|
||
);
|
||
|
||
// server/_core/systemRouter.ts
|
||
import { z } from "zod";
|
||
|
||
// server/_core/notification.ts
|
||
import { TRPCError as TRPCError2 } from "@trpc/server";
|
||
var TITLE_MAX_LENGTH = 1200;
|
||
var CONTENT_MAX_LENGTH = 2e4;
|
||
var trimValue = (value) => value.trim();
|
||
var isNonEmptyString2 = (value) => typeof value === "string" && value.trim().length > 0;
|
||
var buildEndpointUrl = (baseUrl) => {
|
||
const normalizedBase = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
||
return new URL(
|
||
"webdevtoken.v1.WebDevService/SendNotification",
|
||
normalizedBase
|
||
).toString();
|
||
};
|
||
var validatePayload = (input) => {
|
||
if (!isNonEmptyString2(input.title)) {
|
||
throw new TRPCError2({
|
||
code: "BAD_REQUEST",
|
||
message: "Notification title is required."
|
||
});
|
||
}
|
||
if (!isNonEmptyString2(input.content)) {
|
||
throw new TRPCError2({
|
||
code: "BAD_REQUEST",
|
||
message: "Notification content is required."
|
||
});
|
||
}
|
||
const title = trimValue(input.title);
|
||
const content = trimValue(input.content);
|
||
if (title.length > TITLE_MAX_LENGTH) {
|
||
throw new TRPCError2({
|
||
code: "BAD_REQUEST",
|
||
message: `Notification title must be at most ${TITLE_MAX_LENGTH} characters.`
|
||
});
|
||
}
|
||
if (content.length > CONTENT_MAX_LENGTH) {
|
||
throw new TRPCError2({
|
||
code: "BAD_REQUEST",
|
||
message: `Notification content must be at most ${CONTENT_MAX_LENGTH} characters.`
|
||
});
|
||
}
|
||
return { title, content };
|
||
};
|
||
async function notifyOwner(payload) {
|
||
const { title, content } = validatePayload(payload);
|
||
if (!ENV.forgeApiUrl) {
|
||
throw new TRPCError2({
|
||
code: "INTERNAL_SERVER_ERROR",
|
||
message: "Notification service URL is not configured."
|
||
});
|
||
}
|
||
if (!ENV.forgeApiKey) {
|
||
throw new TRPCError2({
|
||
code: "INTERNAL_SERVER_ERROR",
|
||
message: "Notification service API key is not configured."
|
||
});
|
||
}
|
||
const endpoint = buildEndpointUrl(ENV.forgeApiUrl);
|
||
try {
|
||
const response = await fetch(endpoint, {
|
||
method: "POST",
|
||
headers: {
|
||
accept: "application/json",
|
||
authorization: `Bearer ${ENV.forgeApiKey}`,
|
||
"content-type": "application/json",
|
||
"connect-protocol-version": "1"
|
||
},
|
||
body: JSON.stringify({ title, content })
|
||
});
|
||
if (!response.ok) {
|
||
const detail = await response.text().catch(() => "");
|
||
console.warn(
|
||
`[Notification] Failed to notify owner (${response.status} ${response.statusText})${detail ? `: ${detail}` : ""}`
|
||
);
|
||
return false;
|
||
}
|
||
return true;
|
||
} catch (error) {
|
||
console.warn("[Notification] Error calling notification service:", error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// server/_core/systemRouter.ts
|
||
var systemRouter = router({
|
||
health: publicProcedure.input(
|
||
z.object({
|
||
timestamp: z.number().min(0, "timestamp cannot be negative")
|
||
})
|
||
).query(() => ({
|
||
ok: true
|
||
})),
|
||
notifyOwner: adminProcedure.input(
|
||
z.object({
|
||
title: z.string().min(1, "title is required"),
|
||
content: z.string().min(1, "content is required")
|
||
})
|
||
).mutation(async ({ input }) => {
|
||
const delivered = await notifyOwner(input);
|
||
return {
|
||
success: delivered
|
||
};
|
||
})
|
||
});
|
||
|
||
// server/nacAuth.ts
|
||
import mysql from "mysql2/promise";
|
||
import bcrypt from "bcryptjs";
|
||
import jwt from "jsonwebtoken";
|
||
|
||
// server/secrets.ts
|
||
function readSecret(key) {
|
||
return process.env[key];
|
||
}
|
||
function getNacMysqlUrl() {
|
||
const val = readSecret("NAC_MYSQL_URL");
|
||
if (!val) throw new Error("[Secrets] NAC_MYSQL_URL \u672A\u914D\u7F6E");
|
||
return val;
|
||
}
|
||
function getNacMongoUrl() {
|
||
const val = readSecret("NAC_MONGO_URL");
|
||
if (!val) throw new Error("[Secrets] NAC_MONGO_URL \u672A\u914D\u7F6E");
|
||
return val;
|
||
}
|
||
function getNacJwtSecret() {
|
||
const val = readSecret("NAC_JWT_SECRET");
|
||
if (!val) throw new Error("[Secrets] NAC_JWT_SECRET \u672A\u914D\u7F6E");
|
||
return val;
|
||
}
|
||
|
||
// server/nacAuth.ts
|
||
var pool = null;
|
||
function getNacMysqlPool() {
|
||
if (pool) return pool;
|
||
const url = new URL(getNacMysqlUrl());
|
||
pool = mysql.createPool({
|
||
host: url.hostname,
|
||
port: parseInt(url.port || "3306"),
|
||
user: url.username,
|
||
password: url.password,
|
||
database: url.pathname.slice(1),
|
||
waitForConnections: true,
|
||
connectionLimit: 5,
|
||
connectTimeout: 1e4
|
||
});
|
||
return pool;
|
||
}
|
||
function resolveRole(kyc_level, node_status) {
|
||
if (kyc_level >= 2 && node_status === "constitutional") return "admin";
|
||
if (kyc_level >= 1) return "legal";
|
||
return "reviewer";
|
||
}
|
||
async function loginWithNacCredentials(email, password) {
|
||
const pool2 = getNacMysqlPool();
|
||
const [rows] = await pool2.execute(
|
||
"SELECT id, name, email, password, kyc_level, node_status, is_active FROM users WHERE email = ? AND is_active = 1",
|
||
[email]
|
||
);
|
||
if (rows.length === 0) return null;
|
||
const dbUser = rows[0];
|
||
const valid = await bcrypt.compare(password, dbUser.password);
|
||
if (!valid) return null;
|
||
const role = resolveRole(dbUser.kyc_level, dbUser.node_status);
|
||
const user = {
|
||
id: dbUser.id,
|
||
name: dbUser.name,
|
||
email: dbUser.email,
|
||
kyc_level: dbUser.kyc_level,
|
||
node_status: dbUser.node_status,
|
||
is_active: dbUser.is_active,
|
||
role
|
||
};
|
||
const token = jwt.sign(
|
||
{ id: user.id, email: user.email, role: user.role },
|
||
getNacJwtSecret(),
|
||
{ expiresIn: "24h" }
|
||
);
|
||
return { user, token };
|
||
}
|
||
function verifyNacToken(token) {
|
||
try {
|
||
return jwt.verify(token, getNacJwtSecret());
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
async function listNacUsers(limit = 50, offset = 0) {
|
||
const pool2 = getNacMysqlPool();
|
||
const [rows] = await pool2.execute(
|
||
"SELECT id, name, email, kyc_level, node_status, is_active, created_at, last_login_at FROM users ORDER BY id DESC LIMIT ? OFFSET ?",
|
||
[limit, offset]
|
||
);
|
||
return rows.map((r) => ({ ...r, role: resolveRole(r.kyc_level, r.node_status) }));
|
||
}
|
||
async function getNacUserCount() {
|
||
const pool2 = getNacMysqlPool();
|
||
const [rows] = await pool2.execute("SELECT COUNT(*) as cnt FROM users");
|
||
return rows[0]?.cnt || 0;
|
||
}
|
||
|
||
// server/mongodb.ts
|
||
import { MongoClient } from "mongodb";
|
||
var client = null;
|
||
var db = null;
|
||
async function getMongoDb() {
|
||
if (db) return db;
|
||
try {
|
||
client = new MongoClient(getNacMongoUrl(), {
|
||
serverSelectionTimeoutMS: 5e3,
|
||
connectTimeoutMS: 5e3
|
||
});
|
||
await client.connect();
|
||
db = client.db("nac_knowledge_engine");
|
||
console.log("[MongoDB] Connected to nac_knowledge_engine");
|
||
return db;
|
||
} catch (error) {
|
||
console.error("[MongoDB] Connection failed:", error.message);
|
||
return null;
|
||
}
|
||
}
|
||
var COLLECTIONS = {
|
||
COMPLIANCE_RULES: "compliance_rules",
|
||
CRAWLERS: "crawlers",
|
||
CRAWLER_LOGS: "crawler_logs",
|
||
APPROVAL_CASES: "approval_cases",
|
||
TAG_RULES: "tag_rules",
|
||
PROTOCOL_REGISTRY: "protocol_registry",
|
||
AUDIT_LOGS: "audit_logs",
|
||
KNOWLEDGE_STATS: "knowledge_stats"
|
||
};
|
||
|
||
// server/routers.ts
|
||
import { ObjectId } from "mongodb";
|
||
var nacAuthProcedure = publicProcedure.use(async ({ ctx, next }) => {
|
||
const token = ctx.req.cookies?.["nac_admin_token"] || ctx.req.headers["x-nac-token"];
|
||
if (!token) throw new TRPCError3({ code: "UNAUTHORIZED", message: "\u8BF7\u5148\u767B\u5F55" });
|
||
const payload = verifyNacToken(token);
|
||
if (!payload) throw new TRPCError3({ code: "UNAUTHORIZED", message: "\u767B\u5F55\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55" });
|
||
return next({ ctx: { ...ctx, nacUser: payload } });
|
||
});
|
||
var nacAdminProcedure = nacAuthProcedure.use(async ({ ctx, next }) => {
|
||
if (ctx.nacUser?.role !== "admin") {
|
||
throw new TRPCError3({ code: "FORBIDDEN", message: "\u9700\u8981\u7BA1\u7406\u5458\u6743\u9650" });
|
||
}
|
||
return next({ ctx });
|
||
});
|
||
async function writeAuditLog(action, userId, email, detail) {
|
||
try {
|
||
const db2 = await getMongoDb();
|
||
if (!db2) return;
|
||
await db2.collection(COLLECTIONS.AUDIT_LOGS).insertOne({
|
||
action,
|
||
userId,
|
||
email,
|
||
detail,
|
||
timestamp: /* @__PURE__ */ new Date(),
|
||
immutable: true
|
||
});
|
||
} catch (e) {
|
||
console.error("[AuditLog] Failed:", e.message);
|
||
}
|
||
}
|
||
async function ensureKnowledgeBaseData() {
|
||
const db2 = await getMongoDb();
|
||
if (!db2) return;
|
||
const protocolCount = await db2.collection(COLLECTIONS.PROTOCOL_REGISTRY).countDocuments();
|
||
if (protocolCount === 0) {
|
||
await db2.collection(COLLECTIONS.PROTOCOL_REGISTRY).insertMany([
|
||
{ name: "nac-charter-compiler", type: "contract_validation", version: "1.0.0", endpoint: "charter.newassetchain.io", trigger: "asset_type in ALL", status: "active", createdAt: /* @__PURE__ */ new Date() },
|
||
{ name: "nac-cnnl-validator", type: "constitutional_check", version: "1.0.0", endpoint: "cnnl.newassetchain.io", trigger: "asset_type in ALL", status: "active", createdAt: /* @__PURE__ */ new Date() },
|
||
{ name: "nac-acc20-engine", type: "compliance_approval", version: "1.0.0", endpoint: "acc20.newassetchain.io", trigger: "asset_type in ALL", status: "active", createdAt: /* @__PURE__ */ new Date() },
|
||
{ name: "nac-gnacs-classifier", type: "asset_classification", version: "1.0.0", endpoint: "gnacs.newassetchain.io", trigger: "asset_type in ALL", status: "active", createdAt: /* @__PURE__ */ new Date() },
|
||
{ name: "nac-valuation-ai", type: "valuation_model", version: "0.9.0", endpoint: "valuation.newassetchain.io", trigger: "asset_type is RealEstate", status: "pending", createdAt: /* @__PURE__ */ new Date() }
|
||
]);
|
||
}
|
||
const crawlerCount = await db2.collection(COLLECTIONS.CRAWLERS).countDocuments();
|
||
if (crawlerCount === 0) {
|
||
await db2.collection(COLLECTIONS.CRAWLERS).insertMany([
|
||
{ name: "CN-CSRC\u6CD5\u89C4\u91C7\u96C6\u5668", jurisdiction: "CN", type: "external", source: "http://www.csrc.gov.cn", category: "regulation", frequency: "daily", status: "active", lastRun: null, successRate: 0, totalCollected: 0, createdAt: /* @__PURE__ */ new Date() },
|
||
{ name: "HK-SFC\u6CD5\u89C4\u91C7\u96C6\u5668", jurisdiction: "HK", type: "external", source: "https://www.sfc.hk", category: "regulation", frequency: "daily", status: "active", lastRun: null, successRate: 0, totalCollected: 0, createdAt: /* @__PURE__ */ new Date() },
|
||
{ name: "US-SEC\u6CD5\u89C4\u91C7\u96C6\u5668", jurisdiction: "US", type: "external", source: "https://www.sec.gov", category: "regulation", frequency: "daily", status: "active", lastRun: null, successRate: 0, totalCollected: 0, createdAt: /* @__PURE__ */ new Date() },
|
||
{ name: "EU-ESMA\u6CD5\u89C4\u91C7\u96C6\u5668", jurisdiction: "EU", type: "external", source: "https://www.esma.europa.eu", category: "regulation", frequency: "weekly", status: "active", lastRun: null, successRate: 0, totalCollected: 0, createdAt: /* @__PURE__ */ new Date() },
|
||
{ name: "SG-MAS\u6CD5\u89C4\u91C7\u96C6\u5668", jurisdiction: "SG", type: "external", source: "https://www.mas.gov.sg", category: "regulation", frequency: "daily", status: "active", lastRun: null, successRate: 0, totalCollected: 0, createdAt: /* @__PURE__ */ new Date() },
|
||
{ name: "AE-DFSA\u6CD5\u89C4\u91C7\u96C6\u5668", jurisdiction: "AE", type: "external", source: "https://www.dfsa.ae", category: "regulation", frequency: "weekly", status: "active", lastRun: null, successRate: 0, totalCollected: 0, createdAt: /* @__PURE__ */ new Date() },
|
||
{ name: "CN-\u88C1\u5224\u6587\u4E66\u7F51\u91C7\u96C6\u5668", jurisdiction: "CN", type: "external", source: "https://wenshu.court.gov.cn", category: "credit", frequency: "weekly", status: "active", lastRun: null, successRate: 0, totalCollected: 0, createdAt: /* @__PURE__ */ new Date() },
|
||
{ name: "\u5185\u90E8\u4E0A\u94FE\u6587\u4EF6\u91C7\u96C6\u5668", jurisdiction: "ALL", type: "internal", source: "internal://onboarding", category: "asset_document", frequency: "realtime", status: "active", lastRun: null, successRate: 0, totalCollected: 0, createdAt: /* @__PURE__ */ new Date() }
|
||
]);
|
||
}
|
||
const ruleCount = await db2.collection(COLLECTIONS.COMPLIANCE_RULES).countDocuments();
|
||
if (ruleCount === 0) {
|
||
await db2.collection(COLLECTIONS.COMPLIANCE_RULES).insertMany([
|
||
{ jurisdiction: "CN", assetType: "RealEstate", ruleName: "\u4E0D\u52A8\u4EA7\u767B\u8BB0\u8BC1\u8981\u6C42", description: "\u4E2D\u56FD\u5883\u5185\u623F\u5730\u4EA7\u4E0A\u94FE\u5FC5\u987B\u63D0\u4F9B\u4E0D\u52A8\u4EA7\u767B\u8BB0\u8BC1", required: true, status: "active", tags: ["CN", "RealEstate", "Document", "Required"], createdAt: /* @__PURE__ */ new Date() },
|
||
{ jurisdiction: "HK", assetType: "Securities", ruleName: "SFC\u6301\u724C\u8981\u6C42", description: "\u9999\u6E2F\u8BC1\u5238\u7C7B\u8D44\u4EA7\u4E0A\u94FE\u987B\u7ECFSFC\u6301\u724C\u673A\u6784\u5BA1\u6838", required: true, status: "active", tags: ["HK", "Securities", "License", "SFC"], createdAt: /* @__PURE__ */ new Date() },
|
||
{ jurisdiction: "US", assetType: "Securities", ruleName: "Reg D\u8C41\u514D\u7533\u62A5", description: "\u7F8E\u56FD\u8BC1\u5238\u7C7B\u8D44\u4EA7\u987B\u6EE1\u8DB3Reg D/S\u8C41\u514D\u6761\u4EF6", required: true, status: "active", tags: ["US", "Securities", "RegD", "SEC"], createdAt: /* @__PURE__ */ new Date() },
|
||
{ jurisdiction: "EU", assetType: "ALL", ruleName: "MiCA\u5408\u89C4\u8981\u6C42", description: "\u6B27\u76DF\u5883\u5185\u6240\u6709\u52A0\u5BC6\u8D44\u4EA7\u987B\u7B26\u5408MiCA\u6CD5\u89C4", required: true, status: "active", tags: ["EU", "ALL", "MiCA", "ESMA"], createdAt: /* @__PURE__ */ new Date() },
|
||
{ jurisdiction: "SG", assetType: "DigitalToken", ruleName: "MAS\u6570\u5B57\u4EE3\u5E01\u670D\u52A1\u724C\u7167", description: "\u65B0\u52A0\u5761\u6570\u5B57\u4EE3\u5E01\u670D\u52A1\u987B\u6301MAS\u724C\u7167", required: true, status: "active", tags: ["SG", "DigitalToken", "MAS", "License"], createdAt: /* @__PURE__ */ new Date() },
|
||
{ jurisdiction: "AE", assetType: "RealEstate", ruleName: "DLD\u4EA7\u6743\u8BC1\u4E66\u8981\u6C42", description: "\u8FEA\u62DC\u623F\u5730\u4EA7\u4E0A\u94FE\u987B\u63D0\u4F9BDLD\u9881\u53D1\u7684\u4EA7\u6743\u8BC1\u4E66", required: true, status: "active", tags: ["AE", "RealEstate", "DLD", "Document"], createdAt: /* @__PURE__ */ new Date() }
|
||
]);
|
||
}
|
||
}
|
||
var appRouter = router({
|
||
system: systemRouter,
|
||
// ─── NAC原生认证(不使用NAC_AI OAuth)────────────────────────────
|
||
nacAuth: router({
|
||
login: publicProcedure.input(z2.object({ email: z2.string().email(), password: z2.string().min(1) })).mutation(async ({ input, ctx }) => {
|
||
try {
|
||
const result = await loginWithNacCredentials(input.email, input.password);
|
||
if (!result) throw new TRPCError3({ code: "UNAUTHORIZED", message: "\u90AE\u7BB1\u6216\u5BC6\u7801\u9519\u8BEF" });
|
||
const cookieOptions = getSessionCookieOptions(ctx.req);
|
||
ctx.res.cookie("nac_admin_token", result.token, { ...cookieOptions, maxAge: 24 * 60 * 60 * 1e3 });
|
||
await writeAuditLog("LOGIN", result.user.id, result.user.email, { ip: ctx.req.ip });
|
||
return { success: true, user: { id: result.user.id, name: result.user.name, email: result.user.email, role: result.user.role, kyc_level: result.user.kyc_level } };
|
||
} catch (e) {
|
||
if (e instanceof TRPCError3) throw e;
|
||
throw new TRPCError3({ code: "INTERNAL_SERVER_ERROR", message: "\u767B\u5F55\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528" });
|
||
}
|
||
}),
|
||
logout: publicProcedure.mutation(({ ctx }) => {
|
||
ctx.res.clearCookie("nac_admin_token");
|
||
return { success: true };
|
||
}),
|
||
me: nacAuthProcedure.query(async ({ ctx }) => {
|
||
const nacUser = ctx.nacUser;
|
||
return { id: nacUser.id, email: nacUser.email, role: nacUser.role };
|
||
})
|
||
}),
|
||
// ─── 全局态势感知仪表盘 ──────────────────────────────────────────
|
||
dashboard: router({
|
||
stats: nacAuthProcedure.query(async () => {
|
||
const db2 = await getMongoDb();
|
||
if (!db2) return { error: "\u6570\u636E\u5E93\u8FDE\u63A5\u5931\u8D25" };
|
||
await ensureKnowledgeBaseData();
|
||
const [ruleCount, crawlerCount, caseCount, protocolCount, userCount, auditCount] = await Promise.all([
|
||
db2.collection(COLLECTIONS.COMPLIANCE_RULES).countDocuments(),
|
||
db2.collection(COLLECTIONS.CRAWLERS).countDocuments(),
|
||
db2.collection(COLLECTIONS.APPROVAL_CASES).countDocuments(),
|
||
db2.collection(COLLECTIONS.PROTOCOL_REGISTRY).countDocuments(),
|
||
getNacUserCount(),
|
||
db2.collection(COLLECTIONS.AUDIT_LOGS).countDocuments()
|
||
]);
|
||
const [activeCrawlers, pendingCases, approvedCases, activeProtocols] = await Promise.all([
|
||
db2.collection(COLLECTIONS.CRAWLERS).countDocuments({ status: "active" }),
|
||
db2.collection(COLLECTIONS.APPROVAL_CASES).countDocuments({ status: "pending_review" }),
|
||
db2.collection(COLLECTIONS.APPROVAL_CASES).countDocuments({ decision: "approved" }),
|
||
db2.collection(COLLECTIONS.PROTOCOL_REGISTRY).countDocuments({ status: "active" })
|
||
]);
|
||
const jurisdictionStats = await db2.collection(COLLECTIONS.COMPLIANCE_RULES).aggregate([
|
||
{ $group: { _id: "$jurisdiction", count: { $sum: 1 } } },
|
||
{ $sort: { count: -1 } }
|
||
]).toArray();
|
||
return {
|
||
knowledgeBase: { totalRules: ruleCount, activeProtocols, totalProtocols: protocolCount },
|
||
crawlers: { total: crawlerCount, active: activeCrawlers },
|
||
approvals: { total: caseCount, pending: pendingCases, approved: approvedCases, approvalRate: caseCount > 0 ? Math.round(approvedCases / caseCount * 100) : 0 },
|
||
users: { total: userCount },
|
||
audit: { total: auditCount },
|
||
jurisdictionCoverage: jurisdictionStats,
|
||
systemStatus: { mongodb: "connected", mysql: "connected", timestamp: /* @__PURE__ */ new Date() }
|
||
};
|
||
}),
|
||
recentActivity: nacAuthProcedure.query(async () => {
|
||
const db2 = await getMongoDb();
|
||
if (!db2) return [];
|
||
return db2.collection(COLLECTIONS.AUDIT_LOGS).find({}).sort({ timestamp: -1 }).limit(20).toArray();
|
||
})
|
||
}),
|
||
// ─── 知识库管理 ──────────────────────────────────────────────────
|
||
knowledgeBase: router({
|
||
list: nacAuthProcedure.input(z2.object({ jurisdiction: z2.string().optional(), assetType: z2.string().optional(), status: z2.string().optional(), page: z2.number().default(1), pageSize: z2.number().default(20) })).query(async ({ input }) => {
|
||
const db2 = await getMongoDb();
|
||
if (!db2) return { items: [], total: 0 };
|
||
const filter = {};
|
||
if (input.jurisdiction) filter.jurisdiction = input.jurisdiction;
|
||
if (input.assetType) filter.assetType = input.assetType;
|
||
if (input.status) filter.status = input.status;
|
||
const skip = (input.page - 1) * input.pageSize;
|
||
const [items, total] = await Promise.all([
|
||
db2.collection(COLLECTIONS.COMPLIANCE_RULES).find(filter).sort({ createdAt: -1 }).skip(skip).limit(input.pageSize).toArray(),
|
||
db2.collection(COLLECTIONS.COMPLIANCE_RULES).countDocuments(filter)
|
||
]);
|
||
return { items, total };
|
||
}),
|
||
create: nacAdminProcedure.input(z2.object({ jurisdiction: z2.string(), assetType: z2.string(), ruleName: z2.string(), description: z2.string(), required: z2.boolean(), tags: z2.array(z2.string()) })).mutation(async ({ input, ctx }) => {
|
||
const db2 = await getMongoDb();
|
||
if (!db2) throw new TRPCError3({ code: "INTERNAL_SERVER_ERROR" });
|
||
const result = await db2.collection(COLLECTIONS.COMPLIANCE_RULES).insertOne({ ...input, status: "active", createdAt: /* @__PURE__ */ new Date() });
|
||
await writeAuditLog("CREATE_RULE", ctx.nacUser.id, ctx.nacUser.email, { ruleId: result.insertedId, ruleName: input.ruleName });
|
||
return { id: result.insertedId };
|
||
}),
|
||
update: nacAdminProcedure.input(z2.object({ id: z2.string(), data: z2.object({ ruleName: z2.string().optional(), description: z2.string().optional(), status: z2.enum(["active", "disabled"]).optional(), tags: z2.array(z2.string()).optional() }) })).mutation(async ({ input, ctx }) => {
|
||
const db2 = await getMongoDb();
|
||
if (!db2) throw new TRPCError3({ code: "INTERNAL_SERVER_ERROR" });
|
||
await db2.collection(COLLECTIONS.COMPLIANCE_RULES).updateOne({ _id: new ObjectId(input.id) }, { $set: { ...input.data, updatedAt: /* @__PURE__ */ new Date() } });
|
||
await writeAuditLog("UPDATE_RULE", ctx.nacUser.id, ctx.nacUser.email, { ruleId: input.id });
|
||
return { success: true };
|
||
}),
|
||
toggleStatus: nacAdminProcedure.input(z2.object({ id: z2.string(), status: z2.enum(["active", "disabled"]) })).mutation(async ({ input, ctx }) => {
|
||
const db2 = await getMongoDb();
|
||
if (!db2) throw new TRPCError3({ code: "INTERNAL_SERVER_ERROR" });
|
||
await db2.collection(COLLECTIONS.COMPLIANCE_RULES).updateOne({ _id: new ObjectId(input.id) }, { $set: { status: input.status, updatedAt: /* @__PURE__ */ new Date() } });
|
||
await writeAuditLog("TOGGLE_RULE", ctx.nacUser.id, ctx.nacUser.email, { ruleId: input.id, newStatus: input.status });
|
||
return { success: true };
|
||
}),
|
||
delete: nacAdminProcedure.input(z2.object({ id: z2.string() })).mutation(async ({ input, ctx }) => {
|
||
const db2 = await getMongoDb();
|
||
if (!db2) throw new TRPCError3({ code: "INTERNAL_SERVER_ERROR" });
|
||
await db2.collection(COLLECTIONS.COMPLIANCE_RULES).deleteOne({ _id: new ObjectId(input.id) });
|
||
await writeAuditLog("DELETE_RULE", ctx.nacUser.id, ctx.nacUser.email, { ruleId: input.id });
|
||
return { success: true };
|
||
})
|
||
}),
|
||
// ─── 采集器监控与管理 ────────────────────────────────────────────
|
||
crawler: router({
|
||
list: nacAuthProcedure.query(async () => {
|
||
const db2 = await getMongoDb();
|
||
if (!db2) return [];
|
||
return db2.collection(COLLECTIONS.CRAWLERS).find({}).sort({ createdAt: -1 }).toArray();
|
||
}),
|
||
logs: nacAuthProcedure.input(z2.object({ crawlerId: z2.string().optional(), limit: z2.number().default(50) })).query(async ({ input }) => {
|
||
const db2 = await getMongoDb();
|
||
if (!db2) return [];
|
||
const filter = {};
|
||
if (input.crawlerId) filter.crawlerId = input.crawlerId;
|
||
return db2.collection(COLLECTIONS.CRAWLER_LOGS).find(filter).sort({ timestamp: -1 }).limit(input.limit).toArray();
|
||
}),
|
||
trigger: nacAdminProcedure.input(z2.object({ crawlerId: z2.string() })).mutation(async ({ input, ctx }) => {
|
||
const db2 = await getMongoDb();
|
||
if (!db2) throw new TRPCError3({ code: "INTERNAL_SERVER_ERROR" });
|
||
const crawler = await db2.collection(COLLECTIONS.CRAWLERS).findOne({ _id: new ObjectId(input.crawlerId) });
|
||
if (!crawler) throw new TRPCError3({ code: "NOT_FOUND", message: "\u91C7\u96C6\u5668\u4E0D\u5B58\u5728" });
|
||
await db2.collection(COLLECTIONS.CRAWLER_LOGS).insertOne({ crawlerId: input.crawlerId, crawlerName: crawler.name, action: "manual_trigger", status: "triggered", message: "\u7BA1\u7406\u5458\u624B\u52A8\u89E6\u53D1\u91C7\u96C6\u4EFB\u52A1", timestamp: /* @__PURE__ */ new Date() });
|
||
await db2.collection(COLLECTIONS.CRAWLERS).updateOne({ _id: new ObjectId(input.crawlerId) }, { $set: { lastRun: /* @__PURE__ */ new Date() } });
|
||
await writeAuditLog("TRIGGER_CRAWLER", ctx.nacUser.id, ctx.nacUser.email, { crawlerId: input.crawlerId, crawlerName: crawler.name });
|
||
return { success: true, message: `\u91C7\u96C6\u5668 "${crawler.name}" \u5DF2\u89E6\u53D1` };
|
||
}),
|
||
create: nacAdminProcedure.input(z2.object({ name: z2.string(), jurisdiction: z2.string(), type: z2.enum(["internal", "external"]), source: z2.string(), category: z2.string(), frequency: z2.string() })).mutation(async ({ input, ctx }) => {
|
||
const db2 = await getMongoDb();
|
||
if (!db2) throw new TRPCError3({ code: "INTERNAL_SERVER_ERROR" });
|
||
const result = await db2.collection(COLLECTIONS.CRAWLERS).insertOne({ ...input, status: "active", lastRun: null, successRate: 0, totalCollected: 0, createdAt: /* @__PURE__ */ new Date() });
|
||
await writeAuditLog("CREATE_CRAWLER", ctx.nacUser.id, ctx.nacUser.email, { crawlerName: input.name });
|
||
return { id: result.insertedId };
|
||
})
|
||
}),
|
||
// ─── AI审批案例审查 ──────────────────────────────────────────────
|
||
approvalCase: router({
|
||
list: nacAuthProcedure.input(z2.object({ status: z2.string().optional(), riskLevel: z2.string().optional(), page: z2.number().default(1), pageSize: z2.number().default(20) })).query(async ({ input }) => {
|
||
const db2 = await getMongoDb();
|
||
if (!db2) return { items: [], total: 0 };
|
||
const filter = {};
|
||
if (input.status) filter.status = input.status;
|
||
if (input.riskLevel) filter.riskLevel = input.riskLevel;
|
||
const skip = (input.page - 1) * input.pageSize;
|
||
const [items, total] = await Promise.all([
|
||
db2.collection(COLLECTIONS.APPROVAL_CASES).find(filter).sort({ createdAt: -1 }).skip(skip).limit(input.pageSize).toArray(),
|
||
db2.collection(COLLECTIONS.APPROVAL_CASES).countDocuments(filter)
|
||
]);
|
||
return { items, total };
|
||
}),
|
||
review: nacAuthProcedure.input(z2.object({ id: z2.string(), decision: z2.enum(["approved", "rejected"]), comment: z2.string().optional() })).mutation(async ({ input, ctx }) => {
|
||
const db2 = await getMongoDb();
|
||
if (!db2) throw new TRPCError3({ code: "INTERNAL_SERVER_ERROR" });
|
||
const nacUser = ctx.nacUser;
|
||
await db2.collection(COLLECTIONS.APPROVAL_CASES).updateOne(
|
||
{ _id: new ObjectId(input.id) },
|
||
{ $set: { status: "reviewed", decision: input.decision, reviewComment: input.comment, reviewedBy: nacUser.email, reviewedAt: /* @__PURE__ */ new Date() } }
|
||
);
|
||
await writeAuditLog("REVIEW_CASE", nacUser.id, nacUser.email, { caseId: input.id, decision: input.decision });
|
||
return { success: true };
|
||
})
|
||
}),
|
||
// ─── 标签与规则引擎治理 ──────────────────────────────────────────
|
||
tagEngine: router({
|
||
listRules: nacAuthProcedure.query(async () => {
|
||
const db2 = await getMongoDb();
|
||
if (!db2) return [];
|
||
return db2.collection(COLLECTIONS.TAG_RULES).find({}).sort({ createdAt: -1 }).toArray();
|
||
}),
|
||
correctTag: nacAuthProcedure.input(z2.object({ documentId: z2.string(), originalTags: z2.array(z2.string()), correctedTags: z2.array(z2.string()), reason: z2.string() })).mutation(async ({ input, ctx }) => {
|
||
const db2 = await getMongoDb();
|
||
if (!db2) throw new TRPCError3({ code: "INTERNAL_SERVER_ERROR" });
|
||
const nacUser = ctx.nacUser;
|
||
await db2.collection(COLLECTIONS.TAG_RULES).insertOne({ type: "correction", documentId: input.documentId, originalTags: input.originalTags, correctedTags: input.correctedTags, reason: input.reason, correctedBy: nacUser.email, isTrainingData: true, createdAt: /* @__PURE__ */ new Date() });
|
||
await writeAuditLog("CORRECT_TAG", nacUser.id, nacUser.email, { documentId: input.documentId });
|
||
return { success: true };
|
||
}),
|
||
createRule: nacAdminProcedure.input(z2.object({ keyword: z2.string(), tags: z2.array(z2.string()), dimension: z2.string(), description: z2.string() })).mutation(async ({ input, ctx }) => {
|
||
const db2 = await getMongoDb();
|
||
if (!db2) throw new TRPCError3({ code: "INTERNAL_SERVER_ERROR" });
|
||
const result = await db2.collection(COLLECTIONS.TAG_RULES).insertOne({ ...input, type: "rule", status: "active", createdAt: /* @__PURE__ */ new Date() });
|
||
await writeAuditLog("CREATE_TAG_RULE", ctx.nacUser.id, ctx.nacUser.email, { keyword: input.keyword });
|
||
return { id: result.insertedId };
|
||
})
|
||
}),
|
||
// ─── 协议族注册表管理 ────────────────────────────────────────────
|
||
protocolRegistry: router({
|
||
list: nacAuthProcedure.query(async () => {
|
||
const db2 = await getMongoDb();
|
||
if (!db2) return [];
|
||
return db2.collection(COLLECTIONS.PROTOCOL_REGISTRY).find({}).sort({ createdAt: -1 }).toArray();
|
||
}),
|
||
register: nacAdminProcedure.input(z2.object({ name: z2.string(), type: z2.string(), version: z2.string(), endpoint: z2.string(), trigger: z2.string(), description: z2.string().optional() })).mutation(async ({ input, ctx }) => {
|
||
const db2 = await getMongoDb();
|
||
if (!db2) throw new TRPCError3({ code: "INTERNAL_SERVER_ERROR" });
|
||
const result = await db2.collection(COLLECTIONS.PROTOCOL_REGISTRY).insertOne({ ...input, status: "active", createdAt: /* @__PURE__ */ new Date() });
|
||
await writeAuditLog("REGISTER_PROTOCOL", ctx.nacUser.id, ctx.nacUser.email, { protocolName: input.name });
|
||
return { id: result.insertedId };
|
||
}),
|
||
toggleStatus: nacAdminProcedure.input(z2.object({ id: z2.string(), status: z2.enum(["active", "disabled", "deprecated"]) })).mutation(async ({ input, ctx }) => {
|
||
const db2 = await getMongoDb();
|
||
if (!db2) throw new TRPCError3({ code: "INTERNAL_SERVER_ERROR" });
|
||
await db2.collection(COLLECTIONS.PROTOCOL_REGISTRY).updateOne({ _id: new ObjectId(input.id) }, { $set: { status: input.status, updatedAt: /* @__PURE__ */ new Date() } });
|
||
await writeAuditLog("TOGGLE_PROTOCOL", ctx.nacUser.id, ctx.nacUser.email, { protocolId: input.id, newStatus: input.status });
|
||
return { success: true };
|
||
}),
|
||
updateVersion: nacAdminProcedure.input(z2.object({ id: z2.string(), version: z2.string(), trigger: z2.string().optional() })).mutation(async ({ input, ctx }) => {
|
||
const db2 = await getMongoDb();
|
||
if (!db2) throw new TRPCError3({ code: "INTERNAL_SERVER_ERROR" });
|
||
const update = { version: input.version, updatedAt: /* @__PURE__ */ new Date() };
|
||
if (input.trigger) update.trigger = input.trigger;
|
||
await db2.collection(COLLECTIONS.PROTOCOL_REGISTRY).updateOne({ _id: new ObjectId(input.id) }, { $set: update });
|
||
await writeAuditLog("UPDATE_PROTOCOL_VERSION", ctx.nacUser.id, ctx.nacUser.email, { protocolId: input.id, version: input.version });
|
||
return { success: true };
|
||
})
|
||
}),
|
||
// ─── 权限与审计管理 ──────────────────────────────────────────────
|
||
rbac: router({
|
||
listUsers: nacAdminProcedure.input(z2.object({ page: z2.number().default(1), pageSize: z2.number().default(20) })).query(async ({ input }) => {
|
||
const offset = (input.page - 1) * input.pageSize;
|
||
const [users2, total] = await Promise.all([listNacUsers(input.pageSize, offset), getNacUserCount()]);
|
||
return { users: users2, total };
|
||
}),
|
||
auditLogs: nacAdminProcedure.input(z2.object({ action: z2.string().optional(), userId: z2.number().optional(), page: z2.number().default(1), pageSize: z2.number().default(50) })).query(async ({ input }) => {
|
||
const db2 = await getMongoDb();
|
||
if (!db2) return { items: [], total: 0 };
|
||
const filter = {};
|
||
if (input.action) filter.action = input.action;
|
||
if (input.userId) filter.userId = input.userId;
|
||
const skip = (input.page - 1) * input.pageSize;
|
||
const [items, total] = await Promise.all([
|
||
db2.collection(COLLECTIONS.AUDIT_LOGS).find(filter).sort({ timestamp: -1 }).skip(skip).limit(input.pageSize).toArray(),
|
||
db2.collection(COLLECTIONS.AUDIT_LOGS).countDocuments(filter)
|
||
]);
|
||
return { items, total };
|
||
})
|
||
})
|
||
});
|
||
|
||
// server/_core/context.ts
|
||
async function createContext(opts) {
|
||
let user = null;
|
||
try {
|
||
user = await sdk.authenticateRequest(opts.req);
|
||
} catch (error) {
|
||
user = null;
|
||
}
|
||
return {
|
||
req: opts.req,
|
||
res: opts.res,
|
||
user
|
||
};
|
||
}
|
||
|
||
// server/_core/index.ts
|
||
function isPortAvailable(port) {
|
||
return new Promise((resolve) => {
|
||
const server = net.createServer();
|
||
server.listen(port, () => {
|
||
server.close(() => resolve(true));
|
||
});
|
||
server.on("error", () => resolve(false));
|
||
});
|
||
}
|
||
async function findAvailablePort(startPort = 3e3) {
|
||
for (let port = startPort; port < startPort + 20; port++) {
|
||
if (await isPortAvailable(port)) {
|
||
return port;
|
||
}
|
||
}
|
||
throw new Error(`No available port found starting from ${startPort}`);
|
||
}
|
||
async function startServer() {
|
||
const app = express3();
|
||
const server = createServer(app);
|
||
app.set("trust proxy", 1);
|
||
app.use(cookieParser());
|
||
app.use(express3.json({ limit: "50mb" }));
|
||
app.use(express3.urlencoded({ limit: "50mb", extended: true }));
|
||
registerOAuthRoutes(app);
|
||
app.use(
|
||
"/api/trpc",
|
||
createExpressMiddleware({
|
||
router: appRouter,
|
||
createContext
|
||
})
|
||
);
|
||
if (process.env.NODE_ENV === "development") {
|
||
const { setupVite: setupVite2 } = await Promise.resolve().then(() => (init_vite(), vite_exports));
|
||
await setupVite2(app, server);
|
||
} else {
|
||
const { serveStatic: serveStatic3 } = await Promise.resolve().then(() => (init_static(), static_exports));
|
||
serveStatic3(app);
|
||
}
|
||
const preferredPort = parseInt(process.env.PORT || "3000");
|
||
const port = await findAvailablePort(preferredPort);
|
||
if (port !== preferredPort) {
|
||
console.log(`Port ${preferredPort} is busy, using port ${port} instead`);
|
||
}
|
||
server.listen(port, () => {
|
||
console.log(`Server running on http://localhost:${port}/`);
|
||
});
|
||
}
|
||
startServer().catch(console.error);
|