feat: streamline platform and secure data access
- move the relational data center to MySQL and a standalone workbench\n- add Interface Center API credentials, policies, logs, and DBeaver SSH guidance\n- harden authentication and deployment while retiring unused management surfaces
This commit is contained in:
1 parent
15368f2779
commit
3dd5731751
137 files changed
+8571
-14151
No files matched your search
@@ -0,0 +1,89 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
const appSource = readFileSync(new URL("../src/App.tsx", import.meta.url), "utf8");
|
||||
const panelSource = readFileSync(
|
||||
new URL("../src/panels/system/InterfaceCenterPanel.tsx", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const apiSource = readFileSync(new URL("../src/api.ts", import.meta.url), "utf8");
|
||||
const composeSource = readFileSync(new URL("../../docker-compose.yml", import.meta.url), "utf8");
|
||||
const serverComposeSource = readFileSync(
|
||||
new URL("../../docker-compose.server.yml", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const dockerfileSource = readFileSync(new URL("../../Dockerfile", import.meta.url), "utf8");
|
||||
const dataCenterSource = readFileSync(
|
||||
new URL("../src/panels/data-platform/DataCenterPanel.tsx", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const interfaceServiceSource = readFileSync(
|
||||
new URL("../../app/data_platform/interface_service.py", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
test("Interface Center is available under System", () => {
|
||||
assert.match(appSource, /key: "\/system\/interfaces"[\s\S]*label: "接口中心"/);
|
||||
assert.match(appSource, /path="\/admin\/system\/interfaces"/);
|
||||
assert.match(appSource, /<InterfaceCenterPanel \/>/);
|
||||
});
|
||||
|
||||
test("Interface Center is designed for inbound server access", () => {
|
||||
for (const label of ["DBeaver 管理接入", "HTTPS API", "接入信息", "安全设置"]) {
|
||||
assert.match(panelSource, new RegExp(label));
|
||||
}
|
||||
assert.match(panelSource, /SSH 隧道 \+ 独立 MySQL 账号/);
|
||||
assert.match(panelSource, /不要在 Main 页填写服务器公网 IP/);
|
||||
assert.match(panelSource, /Show all databases/);
|
||||
assert.doesNotMatch(panelSource, /JDBC URL/);
|
||||
assert.doesNotMatch(panelSource, /测试连接/);
|
||||
});
|
||||
|
||||
test("DBeaver access defaults to a loopback-only MySQL endpoint", () => {
|
||||
assert.match(composeSource, /MYSQL_HOST_BIND:-127\.0\.0\.1/);
|
||||
assert.match(composeSource, /--local-infile=OFF/);
|
||||
assert.match(composeSource, /DATA_MYSQL_SSH_TUNNEL_REQUIRED/);
|
||||
assert.match(interfaceServiceSource, /"mysql_publicly_bound": publicly_bound/);
|
||||
assert.match(interfaceServiceSource, /"database_host"/);
|
||||
assert.match(interfaceServiceSource, /"ssh_host"/);
|
||||
});
|
||||
|
||||
test("production deployment fails closed and preserves recoverability", () => {
|
||||
for (const setting of [
|
||||
"--secure-file-priv=NULL",
|
||||
"--log-bin=mysql-bin",
|
||||
"--binlog-format=ROW",
|
||||
"--sync-binlog=1",
|
||||
"--innodb-flush-log-at-trx-commit=1",
|
||||
]) {
|
||||
assert.match(composeSource, new RegExp(setting.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
||||
}
|
||||
assert.match(serverComposeSource, /SECURITY_STRICT_MODE: "true"/);
|
||||
assert.match(serverComposeSource, /API_BIND_HOST:-127\.0\.0\.1/);
|
||||
assert.match(serverComposeSource, /必须设置至少 32 位 AUTH_SECRET/);
|
||||
assert.match(dockerfileSource, /USER appuser/);
|
||||
assert.match(dataCenterSource, /安全只读模式/);
|
||||
});
|
||||
|
||||
test("Interface Center keeps clients, credentials, policies, docs and logs", () => {
|
||||
for (const label of ["客户端与密钥", "权限策略", "接口说明", "调用日志"]) {
|
||||
assert.match(panelSource, new RegExp(label));
|
||||
}
|
||||
assert.match(panelSource, /完整密钥只在签发时显示一次/);
|
||||
assert.match(panelSource, /留空默认 30 天,最长 90 天/);
|
||||
assert.match(panelSource, /加密备份待部署/);
|
||||
assert.match(panelSource, /接口不接受任意 SQL/);
|
||||
});
|
||||
|
||||
test("frontend uses the administrative Interface Center API", () => {
|
||||
for (const path of [
|
||||
"/interface-center/summary",
|
||||
"/interface-center/clients",
|
||||
"/interface-center/credentials",
|
||||
"/interface-center/policies",
|
||||
"/interface-center/logs",
|
||||
]) {
|
||||
assert.match(apiSource, new RegExp(path));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
const loginSource = readFileSync(new URL("../src/AdminLogin.tsx", import.meta.url), "utf8");
|
||||
const brandSource = readFileSync(new URL("../src/Brand.tsx", import.meta.url), "utf8");
|
||||
const indexSource = readFileSync(new URL("../index.html", import.meta.url), "utf8");
|
||||
const logo = readFileSync(
|
||||
new URL("../public/assets/nianxx-wordmark-black-transparent.png", import.meta.url),
|
||||
);
|
||||
|
||||
test("the login page uses the nian.xx company logo and digital asset product name", () => {
|
||||
assert.match(loginSource, /<LoginCompanyLogo \/>/);
|
||||
assert.match(loginSource, /智念数字资产管理系统/);
|
||||
assert.match(loginSource, /DIGITAL ASSET MANAGEMENT PLATFORM/);
|
||||
assert.doesNotMatch(loginSource, /智念城市知识图谱管理系统/);
|
||||
assert.match(brandSource, />xx\s*</);
|
||||
assert.doesNotMatch(brandSource, />xxx\s*</);
|
||||
assert.match(indexSource, /<title>智念数字资产管理系统<\/title>/);
|
||||
assert.ok(logo.byteLength > 0);
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
const appSource = readFileSync(new URL("../src/App.tsx", import.meta.url), "utf8");
|
||||
const dataCenterSource = readFileSync(
|
||||
new URL("../src/panels/data-platform/DataCenterPanel.tsx", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
test("the schema viewer remains available as the only modeling page", () => {
|
||||
assert.match(appSource, /key: "\/modeling\/schema", label: "本体 Schema"/);
|
||||
assert.match(appSource, /path="\/admin\/modeling\/schema"/);
|
||||
assert.doesNotMatch(appSource, /\/modeling\/(vocabulary|health)/);
|
||||
});
|
||||
|
||||
test("retired management sections are absent from navigation and routes", () => {
|
||||
for (const section of ["acquisition", "review", "publish", "workbench"]) {
|
||||
assert.doesNotMatch(appSource, new RegExp(`/admin/${section}`));
|
||||
assert.doesNotMatch(appSource, new RegExp(`key: "/${section}`));
|
||||
}
|
||||
|
||||
for (const label of ["数据采集", "审核入藏", "图谱发布", "治理工作台"]) {
|
||||
assert.doesNotMatch(appSource, new RegExp(label));
|
||||
}
|
||||
});
|
||||
|
||||
test("retired plaza pages are absent while the core plaza remains", () => {
|
||||
for (const page of ["manual-ingest", "audit", "usage", "alerts"]) {
|
||||
assert.doesNotMatch(appSource, new RegExp(`/admin/plaza/${page}`));
|
||||
assert.doesNotMatch(appSource, new RegExp(`key: "/plaza/${page}`));
|
||||
}
|
||||
|
||||
for (const page of ["overview", "user", "graph"]) {
|
||||
assert.match(appSource, new RegExp(`/admin/plaza/${page}`));
|
||||
}
|
||||
});
|
||||
|
||||
test("retired system pages and the notification bell are absent", () => {
|
||||
for (const page of ["areas", "notifications", "settings"]) {
|
||||
assert.doesNotMatch(appSource, new RegExp(`/admin/system/${page}`));
|
||||
assert.doesNotMatch(appSource, new RegExp(`key: "/system/${page}`));
|
||||
}
|
||||
|
||||
for (const component of ["AreaManagement", "Notifications", "AgentSettings", "NotificationBell"]) {
|
||||
assert.doesNotMatch(appSource, new RegExp(component));
|
||||
}
|
||||
});
|
||||
|
||||
test("the data center uses its own full-width workbench surface", () => {
|
||||
assert.match(appSource, /const showDataCenterWorkbench = location\.pathname === "\/admin\/data"/);
|
||||
assert.match(appSource, /\{!showDataCenterWorkbench && \(\s*<Sider/);
|
||||
assert.match(appSource, /path="\/admin\/data" element=\{<DataCenterPanel \/>\}/);
|
||||
});
|
||||
|
||||
test("the data center entry opens the standalone URL in a new browser tab", () => {
|
||||
assert.match(
|
||||
appSource,
|
||||
/if \(key === "\/data"\) \{\s*window\.open\("\/admin\/data", "_blank", "noopener,noreferrer"\)/,
|
||||
);
|
||||
});
|
||||
|
||||
test("database creation is a root action inside the data center tree", () => {
|
||||
assert.doesNotMatch(dataCenterSource, /data-center-toolbar/);
|
||||
assert.doesNotMatch(dataCenterSource, /PostgreSQL · 已连接/);
|
||||
assert.match(dataCenterSource, /<Text strong>数据中心<\/Text>/);
|
||||
|
||||
const treeStart = dataCenterSource.indexOf('<div className="data-center-tree">');
|
||||
const projectNode = dataCenterSource.indexOf("data-center-project-node", treeStart);
|
||||
const createDatabase = dataCenterSource.indexOf("data-center-create-database", projectNode);
|
||||
|
||||
assert.ok(treeStart >= 0);
|
||||
assert.ok(projectNode > treeStart);
|
||||
assert.ok(createDatabase > projectNode);
|
||||
assert.match(
|
||||
dataCenterSource.slice(createDatabase, createDatabase + 260),
|
||||
/onClick=\{openDatabaseCreate\}[\s\S]*新建数据库/,
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user