Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b6f30a838f | |||
| 8922a3eef6 | |||
| 0fd56f8841 | |||
| cf82d606aa |
@@ -45,6 +45,7 @@ database.db
|
|||||||
|
|
||||||
src/lang/source-use.json
|
src/lang/source-use.json
|
||||||
/data
|
/data
|
||||||
|
/local-auth-data/
|
||||||
/_delete
|
/_delete
|
||||||
.vscode
|
.vscode
|
||||||
|
|
||||||
|
|||||||
+37
-5
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
这是从原目录整理出的独立副本,后续开发、启动和构建请在本目录执行。
|
这是从原目录整理出的独立副本,后续开发、启动和构建请在本目录执行。
|
||||||
|
|
||||||
## 启动
|
## 启动应用
|
||||||
|
|
||||||
```bat
|
```bat
|
||||||
start-local.bat
|
start-local.bat
|
||||||
@@ -14,14 +14,46 @@ start-local.bat
|
|||||||
npm.cmd run start:local
|
npm.cmd run start:local
|
||||||
```
|
```
|
||||||
|
|
||||||
|
默认会同时启动本地认证服务、Vite 和 Electron。
|
||||||
|
|
||||||
|
- 认证服务:`http://127.0.0.1:3302`
|
||||||
|
- 应用认证 API:`http://127.0.0.1:3302/api`
|
||||||
|
- 管理后台:`http://127.0.0.1:3302/admin/`
|
||||||
|
- 默认账号:`13800138000`
|
||||||
|
- 默认密码:`123456`
|
||||||
|
|
||||||
|
## 单独启动认证/管理服务
|
||||||
|
|
||||||
|
```bat
|
||||||
|
npm.cmd run auth:local
|
||||||
|
```
|
||||||
|
|
||||||
|
管理后台是从遗失代码中恢复的原版纯静态页面,源码位于 `admin-panel/`;服务端代码位于 `server/local-auth/`。当前为了本地测试,认证服务会顺带托管 `admin-panel/`,之后可以把 `admin-panel/` 单独部署到任意静态站点。
|
||||||
|
|
||||||
|
独立部署后台时,默认请求同域 `/api`。如果后台和认证服务不在同一个域名,可以通过 `api` 参数指定认证服务地址:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://admin.example.com/?api=https://auth.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
本地测试也可以这样写:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://127.0.0.1:3302/admin/?api=http://127.0.0.1:3302
|
||||||
|
```
|
||||||
|
|
||||||
## 自检
|
## 自检
|
||||||
|
|
||||||
```bat
|
```bat
|
||||||
npm.cmd run start:check
|
npm.cmd run start:check
|
||||||
```
|
```
|
||||||
|
|
||||||
## 说明
|
## 系统 API Key 配置
|
||||||
|
|
||||||
- 默认开发端口已改为 `3354`,避免和原目录的 `3344` 服务冲突。
|
进入 `http://127.0.0.1:3302/admin/` 后,在“系统配置”里填写:
|
||||||
- 已排除旧目录中的构建产物、日志、缓存、用户数据和 Git 信息。
|
|
||||||
- `node_modules` 是实拷贝,不是软链接;同时排除了旧 Electron 目录中误混入的巨大运行数据。
|
- `aliyun_bailian_api_key`:QwenTTS / DashScope API Key
|
||||||
|
- `aliyun_bailian_base_url`:默认 `https://dashscope.aliyuncs.com/compatible-mode/v1`
|
||||||
|
- 其他 OSS、RunningHub、火山引擎、云美颜配置也可以在这里维护
|
||||||
|
|
||||||
|
这些配置会通过 `/api/system/config/public` 提供给本地应用作为兜底配置。
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# 管理后台
|
||||||
|
|
||||||
|
这是从遗失服务端代码中恢复的原版静态管理后台,可以和认证服务分开部署。
|
||||||
|
|
||||||
|
## 本地访问
|
||||||
|
|
||||||
|
认证服务启动后访问:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://127.0.0.1:3302/admin/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 独立部署
|
||||||
|
|
||||||
|
把本目录作为静态站点部署即可。默认情况下后台会请求同域 `/api`。
|
||||||
|
|
||||||
|
如果后台和认证服务不在同一个域名,通过 `api` 参数指定认证服务地址:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://admin.example.com/?api=https://auth.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
本地测试示例:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://127.0.0.1:3302/admin/?api=http://127.0.0.1:3302
|
||||||
|
```
|
||||||
|
|
||||||
|
认证服务需要开放 CORS;本地服务默认允许跨域。
|
||||||
|
|
||||||
|
## 资源
|
||||||
|
|
||||||
|
- `index.html`:原版管理后台页面
|
||||||
|
- `login-bg.png`:原版登录背景图
|
||||||
|
- `logo.png`:原版 Logo
|
||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 444 KiB |
+4
-2
@@ -14,7 +14,9 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start:local": "node scripts/start-local.cjs",
|
"start:local": "node scripts/start-local.cjs",
|
||||||
"start:check": "node scripts/start-local.cjs --check",
|
"start:check": "node scripts/start-local.cjs --check",
|
||||||
"dev": "set VITE_DEV_SERVER_URL=http://127.0.0.1:3354&& vite --host 127.0.0.1 --port 3354 --strictPort",
|
"auth:local": "node scripts/local-auth-server.cjs",
|
||||||
|
"admin:local": "node server/local-auth/index.cjs",
|
||||||
|
"dev": "set AUTH_SERVER_URL=http://127.0.0.1:3302&& set VITE_DEV_SERVER_URL=http://127.0.0.1:3354&& vite --host 127.0.0.1 --port 3354 --strictPort",
|
||||||
"typecheck": "vue-tsc --noEmit",
|
"typecheck": "vue-tsc --noEmit",
|
||||||
"build": "node scripts/clean-build-artifacts.cjs && vite build",
|
"build": "node scripts/clean-build-artifacts.cjs && vite build",
|
||||||
"build:dev": "node scripts/clean-build-artifacts.cjs && vite build --mode development",
|
"build:dev": "node scripts/clean-build-artifacts.cjs && vite build --mode development",
|
||||||
@@ -22,7 +24,7 @@
|
|||||||
"version:sync": "node scripts/sync-version.cjs",
|
"version:sync": "node scripts/sync-version.cjs",
|
||||||
"dev:all": "npm run dev:electron",
|
"dev:all": "npm run dev:electron",
|
||||||
"dev:win": "npm run dev:electron",
|
"dev:win": "npm run dev:electron",
|
||||||
"dev:electron": "concurrently \"set VITE_DEV_SERVER_URL=http://127.0.0.1:3354&& set EXTERNAL_ELECTRON_DEV=1&& vite --host 127.0.0.1 --port 3354 --strictPort\" \"set VITE_DEV_SERVER_URL=http://127.0.0.1:3354&& node scripts/dev-electron.cjs\"",
|
"dev:electron": "concurrently \"set AUTH_SERVER_URL=http://127.0.0.1:3302&& set VITE_DEV_SERVER_URL=http://127.0.0.1:3354&& set EXTERNAL_ELECTRON_DEV=1&& vite --host 127.0.0.1 --port 3354 --strictPort\" \"set AUTH_SERVER_URL=http://127.0.0.1:3302&& set VITE_DEV_SERVER_URL=http://127.0.0.1:3354&& node scripts/dev-electron.cjs\"",
|
||||||
"prepare-package": "node apply-oem.cjs && py -3 scripts/export-runtime-system-templates.py && node scripts/prepare-package.cjs",
|
"prepare-package": "node apply-oem.cjs && py -3 scripts/export-runtime-system-templates.py && node scripts/prepare-package.cjs",
|
||||||
"build:win": "npm run prepare-package && npm run build && node scripts/build-resource-artifacts.cjs && electron-builder --win --x64 && node scripts/fix-latest-yml.cjs && node scripts/assemble-release-artifacts.cjs",
|
"build:win": "npm run prepare-package && npm run build && node scripts/build-resource-artifacts.cjs && electron-builder --win --x64 && node scripts/fix-latest-yml.cjs && node scripts/assemble-release-artifacts.cjs",
|
||||||
"build:win:only": "electron-builder --win --x64",
|
"build:win:only": "electron-builder --win --x64",
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
require("../server/local-auth/index.cjs");
|
||||||
+77
-13
@@ -5,6 +5,7 @@ const { spawn, spawnSync } = require("node:child_process");
|
|||||||
|
|
||||||
const projectRoot = path.resolve(__dirname, "..");
|
const projectRoot = path.resolve(__dirname, "..");
|
||||||
const serverUrl = process.env.VITE_DEV_SERVER_URL || "http://127.0.0.1:3354";
|
const serverUrl = process.env.VITE_DEV_SERVER_URL || "http://127.0.0.1:3354";
|
||||||
|
const authServerUrl = process.env.AUTH_SERVER_URL || "http://127.0.0.1:3302";
|
||||||
const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
|
const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
|
||||||
const requiredNodeMajor = 20;
|
const requiredNodeMajor = 20;
|
||||||
const checkOnly = process.argv.includes("--check");
|
const checkOnly = process.argv.includes("--check");
|
||||||
@@ -18,11 +19,11 @@ function fail(message) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseServerUrl(url) {
|
function parseUrl(url, name) {
|
||||||
try {
|
try {
|
||||||
return new URL(url);
|
return new URL(url);
|
||||||
} catch {
|
} catch {
|
||||||
fail(`VITE_DEV_SERVER_URL 无效:${url}`);
|
fail(`${name} 无效:${url}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,13 +58,13 @@ function checkDependencies() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertPortAvailable(host, port) {
|
function assertPortAvailable(host, port, label) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const server = net.createServer();
|
const server = net.createServer();
|
||||||
|
|
||||||
server.once("error", error => {
|
server.once("error", error => {
|
||||||
if (error.code === "EADDRINUSE") {
|
if (error.code === "EADDRINUSE") {
|
||||||
reject(new Error(`端口 ${port} 已被占用,请先关闭已有 Vite/Electron 开发进程。`));
|
reject(new Error(`${label} 端口 ${port} 已被占用,请先关闭已有进程。`));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
reject(error);
|
reject(error);
|
||||||
@@ -77,24 +78,75 @@ function assertPortAvailable(host, port) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function canConnect(host, port) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const socket = net.createConnection({ host, port });
|
||||||
|
const finish = ok => {
|
||||||
|
socket.destroy();
|
||||||
|
resolve(ok);
|
||||||
|
};
|
||||||
|
socket.setTimeout(500);
|
||||||
|
socket.once("connect", () => finish(true));
|
||||||
|
socket.once("timeout", () => finish(false));
|
||||||
|
socket.once("error", () => finish(false));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startLocalAuth(authUrl) {
|
||||||
|
const authHost = authUrl.hostname || "127.0.0.1";
|
||||||
|
const authPort = Number(authUrl.port || 3302);
|
||||||
|
|
||||||
|
if (await canConnect(authHost, authPort)) {
|
||||||
|
log(`本地认证服务已在运行:${authServerUrl}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const child = spawn(process.execPath, ["scripts/local-auth-server.cjs"], {
|
||||||
|
cwd: projectRoot,
|
||||||
|
stdio: "inherit",
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
LOCAL_AUTH_HOST: authHost,
|
||||||
|
LOCAL_AUTH_PORT: String(authPort),
|
||||||
|
},
|
||||||
|
shell: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("error", error => {
|
||||||
|
fail(`本地认证服务启动失败:${error.message}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt < 40; attempt++) {
|
||||||
|
if (await canConnect(authHost, authPort)) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 250));
|
||||||
|
}
|
||||||
|
|
||||||
|
child.kill();
|
||||||
|
fail(`本地认证服务未能在 ${authServerUrl} 启动。`);
|
||||||
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
process.chdir(projectRoot);
|
process.chdir(projectRoot);
|
||||||
|
|
||||||
const parsedUrl = parseServerUrl(serverUrl);
|
const parsedServerUrl = parseUrl(serverUrl, "VITE_DEV_SERVER_URL");
|
||||||
const host = parsedUrl.hostname || "127.0.0.1";
|
const parsedAuthUrl = parseUrl(authServerUrl, "AUTH_SERVER_URL");
|
||||||
const port = Number(parsedUrl.port || 3344);
|
const viteHost = parsedServerUrl.hostname || "127.0.0.1";
|
||||||
|
const vitePort = Number(parsedServerUrl.port || 3354);
|
||||||
|
|
||||||
log("========================================");
|
log("========================================");
|
||||||
log(" 本地快速启动:Vite + Electron");
|
log(" 本地快速启动:Local Auth + Vite + Electron");
|
||||||
log("========================================");
|
log("========================================");
|
||||||
log(`项目目录:${projectRoot}`);
|
log(`项目目录:${projectRoot}`);
|
||||||
|
log(`认证服务:${authServerUrl}`);
|
||||||
log(`开发地址:${serverUrl}`);
|
log(`开发地址:${serverUrl}`);
|
||||||
log("");
|
log("");
|
||||||
|
|
||||||
checkNodeVersion();
|
checkNodeVersion();
|
||||||
const npmVersion = checkCommand(npmCommand, ["--version"], "npm");
|
const npmVersion = checkCommand(npmCommand, ["--version"], "npm");
|
||||||
checkDependencies();
|
checkDependencies();
|
||||||
await assertPortAvailable(host, port);
|
await assertPortAvailable(viteHost, vitePort, "Vite");
|
||||||
|
|
||||||
log(`Node.js:${process.version}`);
|
log(`Node.js:${process.version}`);
|
||||||
log(`npm:${npmVersion}`);
|
log(`npm:${npmVersion}`);
|
||||||
@@ -105,24 +157,36 @@ async function main() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const authChild = await startLocalAuth(parsedAuthUrl);
|
||||||
|
|
||||||
|
log("");
|
||||||
log("正在启动应用,关闭此窗口即可停止开发服务。");
|
log("正在启动应用,关闭此窗口即可停止开发服务。");
|
||||||
|
log("默认本地账号:13800138000 / 123456");
|
||||||
log("");
|
log("");
|
||||||
|
|
||||||
const child = spawn(npmCommand, ["run", "dev:electron"], {
|
const appChild = spawn(npmCommand, ["run", "dev:electron"], {
|
||||||
cwd: projectRoot,
|
cwd: projectRoot,
|
||||||
stdio: "inherit",
|
stdio: "inherit",
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
VITE_DEV_SERVER_URL: serverUrl,
|
VITE_DEV_SERVER_URL: serverUrl,
|
||||||
|
AUTH_SERVER_URL: authServerUrl,
|
||||||
},
|
},
|
||||||
shell: process.platform === "win32",
|
shell: process.platform === "win32",
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on("exit", code => {
|
const shutdown = code => {
|
||||||
|
if (authChild && !authChild.killed) {
|
||||||
|
authChild.kill();
|
||||||
|
}
|
||||||
process.exit(code ?? 0);
|
process.exit(code ?? 0);
|
||||||
});
|
};
|
||||||
|
|
||||||
child.on("error", error => {
|
process.on("SIGINT", () => shutdown(0));
|
||||||
|
process.on("SIGTERM", () => shutdown(0));
|
||||||
|
|
||||||
|
appChild.on("exit", shutdown);
|
||||||
|
appChild.on("error", error => {
|
||||||
fail(`启动失败:${error.message}`);
|
fail(`启动失败:${error.message}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,354 @@
|
|||||||
|
const { publicUser, requireAdmin } = require("./auth.cjs");
|
||||||
|
const { loadDb, saveDb, getSubscription, addUserLog } = require("./db.cjs");
|
||||||
|
const { sendJson } = require("./http-utils.cjs");
|
||||||
|
const { nowIso, addHours, passwordHash } = require("./security.cjs");
|
||||||
|
const { systemConfigDescriptions } = require("./config.cjs");
|
||||||
|
|
||||||
|
function isValidSubscription(subscription) {
|
||||||
|
return subscription && new Date(subscription.expires_at).getTime() > Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
function userRow(db, user) {
|
||||||
|
const subscription = getSubscription(db, user.id);
|
||||||
|
const owner = db.users.find(item => Number(item.id) === Number(user.owner_admin_id));
|
||||||
|
const deviceCount = db.devices.filter(item => Number(item.user_id) === Number(user.id)).length || (user.device_fingerprint ? 1 : 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...publicUser(user),
|
||||||
|
avatar: user.avatar || null,
|
||||||
|
device_fingerprint: user.device_fingerprint || "",
|
||||||
|
current_token: null,
|
||||||
|
subscription_type: subscription.subscription_type,
|
||||||
|
expires_at: subscription.expires_at,
|
||||||
|
owner_admin_name: owner?.username || null,
|
||||||
|
device_count: deviceCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function configRows(db) {
|
||||||
|
return Object.entries(db.systemConfigs).map(([config_key, config_value], index) => ({
|
||||||
|
id: index + 1,
|
||||||
|
config_key,
|
||||||
|
config_value: String(config_value ?? ""),
|
||||||
|
description: systemConfigDescriptions[config_key] || "",
|
||||||
|
updated_at: nowIso(),
|
||||||
|
updated_by: "local-admin",
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function logRows(db, logs) {
|
||||||
|
return logs
|
||||||
|
.slice()
|
||||||
|
.sort((left, right) => new Date(right.created_at) - new Date(left.created_at))
|
||||||
|
.map(log => ({
|
||||||
|
...log,
|
||||||
|
username: db.users.find(user => Number(user.id) === Number(log.user_id))?.username || null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function generationStats(db, userId) {
|
||||||
|
const logs = userId
|
||||||
|
? db.generationLogs.filter(log => Number(log.user_id) === Number(userId))
|
||||||
|
: db.generationLogs;
|
||||||
|
const stats = { script: 0, voice: 0, video: 0, subtitle: 0, total: logs.length };
|
||||||
|
for (const log of logs) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(stats, log.type)) stats[log.type] += 1;
|
||||||
|
}
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUserRole(user, role, inviteCode) {
|
||||||
|
user.role = role === "admin" ? "admin" : "user";
|
||||||
|
user.is_super_admin = user.is_super_admin || 0;
|
||||||
|
if (inviteCode !== undefined) user.invite_code = inviteCode || null;
|
||||||
|
user.updated_at = nowIso();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAdminApi(req, res, pathname, body, requestUrl) {
|
||||||
|
if (!pathname.startsWith("/api/admin")) return false;
|
||||||
|
|
||||||
|
const db = loadDb();
|
||||||
|
const admin = requireAdmin(db, req);
|
||||||
|
if (!admin) {
|
||||||
|
sendJson(res, 401, { success: false, message: "需要管理员登录" });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "GET" && pathname === "/api/admin/stats") {
|
||||||
|
const activeUsers = db.users.filter(user => user.status === "active").length;
|
||||||
|
const adminCount = db.users.filter(user => user.role === "admin" || Number(user.is_super_admin) === 1).length;
|
||||||
|
const validSubscriptions = db.subscriptions.filter(isValidSubscription).length;
|
||||||
|
sendJson(res, 200, {
|
||||||
|
success: true,
|
||||||
|
stats: {
|
||||||
|
total_users: db.users.length,
|
||||||
|
active_users: activeUsers,
|
||||||
|
admin_count: adminCount,
|
||||||
|
valid_subscriptions: validSubscriptions,
|
||||||
|
expired_subscriptions: db.subscriptions.length - validSubscriptions,
|
||||||
|
total_devices: db.devices.length,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "GET" && pathname === "/api/admin/users") {
|
||||||
|
const search = String(requestUrl.searchParams.get("search") || "").trim().toLowerCase();
|
||||||
|
const status = String(requestUrl.searchParams.get("status") || "").trim();
|
||||||
|
const subscription = String(requestUrl.searchParams.get("subscription") || "").trim();
|
||||||
|
|
||||||
|
let users = db.users;
|
||||||
|
if (search) {
|
||||||
|
users = users.filter(user =>
|
||||||
|
String(user.username || "").toLowerCase().includes(search) ||
|
||||||
|
String(user.email || "").toLowerCase().includes(search),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (status) users = users.filter(user => user.status === status);
|
||||||
|
if (subscription === "valid") users = users.filter(user => isValidSubscription(getSubscription(db, user.id)));
|
||||||
|
if (subscription === "expired") users = users.filter(user => !isValidSubscription(getSubscription(db, user.id)));
|
||||||
|
|
||||||
|
saveDb(db);
|
||||||
|
sendJson(res, 200, { success: true, users: users.map(user => userRow(db, user)) });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "GET" && pathname === "/api/admin/admins") {
|
||||||
|
const admins = db.users
|
||||||
|
.filter(user => user.role === "admin" || Number(user.is_super_admin) === 1)
|
||||||
|
.map(user => ({
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
invite_code: user.invite_code || null,
|
||||||
|
is_super_admin: Number(user.is_super_admin || 0),
|
||||||
|
status: user.status,
|
||||||
|
owned_user_count: db.users.filter(item => Number(item.owner_admin_id) === Number(user.id)).length,
|
||||||
|
}));
|
||||||
|
sendJson(res, 200, { success: true, admins });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "POST" && pathname === "/api/admin/add-time") {
|
||||||
|
const user = db.users.find(item => Number(item.id) === Number(body.user_id));
|
||||||
|
if (!user) {
|
||||||
|
sendJson(res, 200, { success: false, message: "用户不存在" });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const subscription = getSubscription(db, user.id);
|
||||||
|
const base = new Date(subscription.expires_at).getTime() > Date.now() ? new Date(subscription.expires_at) : new Date();
|
||||||
|
subscription.expires_at = addHours(base, Number(body.hours || 0)).toISOString();
|
||||||
|
subscription.updated_at = nowIso();
|
||||||
|
addUserLog(db, user.id, "add_time", body.reason || `管理员增加 ${body.hours || 0} 小时`);
|
||||||
|
saveDb(db);
|
||||||
|
sendJson(res, 200, { success: true, subscription });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "POST" && pathname === "/api/admin/user/status") {
|
||||||
|
const user = db.users.find(item => Number(item.id) === Number(body.user_id));
|
||||||
|
if (!user) {
|
||||||
|
sendJson(res, 200, { success: false, message: "用户不存在" });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
user.status = body.status === "disabled" ? "disabled" : "active";
|
||||||
|
user.updated_at = nowIso();
|
||||||
|
addUserLog(db, user.id, "status", `管理员修改状态为 ${user.status}`);
|
||||||
|
saveDb(db);
|
||||||
|
sendJson(res, 200, { success: true, user: userRow(db, user) });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "POST" && pathname === "/api/admin/device/unbind") {
|
||||||
|
const user = db.users.find(item => Number(item.id) === Number(body.user_id));
|
||||||
|
if (!user) {
|
||||||
|
sendJson(res, 200, { success: false, message: "用户不存在" });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
user.device_fingerprint = "";
|
||||||
|
user.last_login_device = null;
|
||||||
|
db.devices = db.devices.filter(item => Number(item.user_id) !== Number(user.id));
|
||||||
|
addUserLog(db, user.id, "device_unbind", "管理员解绑设备");
|
||||||
|
saveDb(db);
|
||||||
|
sendJson(res, 200, { success: true });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "POST" && pathname === "/api/admin/user/reset-password") {
|
||||||
|
const user = db.users.find(item => Number(item.id) === Number(body.user_id));
|
||||||
|
if (!user || !body.new_password) {
|
||||||
|
sendJson(res, 200, { success: false, message: "用户或新密码无效" });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
user.password = passwordHash(body.new_password);
|
||||||
|
user.updated_at = nowIso();
|
||||||
|
addUserLog(db, user.id, "reset_password", "管理员重置密码");
|
||||||
|
saveDb(db);
|
||||||
|
sendJson(res, 200, { success: true });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "POST" && pathname === "/api/admin/create-admin") {
|
||||||
|
const email = String(body.email || "").trim();
|
||||||
|
const username = String(body.username || "").trim();
|
||||||
|
const password = String(body.password || "");
|
||||||
|
if (!email || !username || !password) {
|
||||||
|
sendJson(res, 200, { success: false, message: "管理员账号信息不完整" });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (db.users.some(user => user.email === email || user.username === username)) {
|
||||||
|
sendJson(res, 200, { success: false, message: "账号已存在" });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const user = {
|
||||||
|
id: db.nextUserId++,
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password: passwordHash(password),
|
||||||
|
role: "admin",
|
||||||
|
status: "active",
|
||||||
|
points: 999999,
|
||||||
|
is_super_admin: 0,
|
||||||
|
owner_admin_id: admin.id,
|
||||||
|
invite_code: body.invite_code || null,
|
||||||
|
max_devices: 99,
|
||||||
|
device_fingerprint: "",
|
||||||
|
created_at: nowIso(),
|
||||||
|
updated_at: nowIso(),
|
||||||
|
last_login_at: null,
|
||||||
|
last_login_device: null,
|
||||||
|
current_token: "",
|
||||||
|
cloud_digital_human_disabled: 0,
|
||||||
|
};
|
||||||
|
db.users.push(user);
|
||||||
|
getSubscription(db, user.id);
|
||||||
|
addUserLog(db, user.id, "create_admin", `由 ${admin.username} 创建管理员`);
|
||||||
|
saveDb(db);
|
||||||
|
sendJson(res, 200, { success: true, admin: userRow(db, user) });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "POST" && pathname === "/api/admin/points/modify") {
|
||||||
|
const user = db.users.find(item => Number(item.id) === Number(body.user_id));
|
||||||
|
if (!user) {
|
||||||
|
sendJson(res, 200, { success: false, message: "用户不存在" });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
user.points = Number(user.points || 0) + Number(body.points_change || 0);
|
||||||
|
user.updated_at = nowIso();
|
||||||
|
addUserLog(db, user.id, "points_modify", body.reason || `积分变更 ${body.points_change || 0}`);
|
||||||
|
saveDb(db);
|
||||||
|
sendJson(res, 200, { success: true, points: user.points });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userLogMatch = pathname.match(/^\/api\/admin\/logs\/user\/(\d+)$/);
|
||||||
|
if (req.method === "GET" && userLogMatch) {
|
||||||
|
const logs = db.userLogs.filter(log => Number(log.user_id) === Number(userLogMatch[1]));
|
||||||
|
sendJson(res, 200, { success: true, logs: logRows(db, logs) });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "GET" && pathname === "/api/admin/logs/all") {
|
||||||
|
const limit = Number(requestUrl.searchParams.get("limit") || 100);
|
||||||
|
sendJson(res, 200, { success: true, logs: logRows(db, db.userLogs).slice(0, limit), page: 1, limit });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "POST" && pathname === "/api/admin/user/assign-owner") {
|
||||||
|
const user = db.users.find(item => Number(item.id) === Number(body.user_id));
|
||||||
|
if (!user) {
|
||||||
|
sendJson(res, 200, { success: false, message: "用户不存在" });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
user.owner_admin_id = body.owner_admin_id ? Number(body.owner_admin_id) : null;
|
||||||
|
user.updated_at = nowIso();
|
||||||
|
addUserLog(db, user.id, "assign_owner", `管理员归属变更为 ${user.owner_admin_id || "无"}`);
|
||||||
|
saveDb(db);
|
||||||
|
sendJson(res, 200, { success: true, user: userRow(db, user) });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "PUT" && pathname === "/api/admin/admin/invite-code") {
|
||||||
|
const user = db.users.find(item => Number(item.id) === Number(body.admin_id));
|
||||||
|
if (!user) {
|
||||||
|
sendJson(res, 200, { success: false, message: "管理员不存在" });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
user.invite_code = body.invite_code || null;
|
||||||
|
user.updated_at = nowIso();
|
||||||
|
addUserLog(db, user.id, "invite_code", "管理员邀请码变更");
|
||||||
|
saveDb(db);
|
||||||
|
sendJson(res, 200, { success: true, admin: userRow(db, user) });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "POST" && pathname === "/api/admin/user/role") {
|
||||||
|
const user = db.users.find(item => Number(item.id) === Number(body.user_id));
|
||||||
|
if (!user) {
|
||||||
|
sendJson(res, 200, { success: false, message: "用户不存在" });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
updateUserRole(user, body.role, body.invite_code);
|
||||||
|
addUserLog(db, user.id, "role", `角色变更为 ${user.role}`);
|
||||||
|
saveDb(db);
|
||||||
|
sendJson(res, 200, { success: true, user: userRow(db, user) });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "GET" && pathname === "/api/admin/generation-stats") {
|
||||||
|
sendJson(res, 200, {
|
||||||
|
success: true,
|
||||||
|
stats: generationStats(db, requestUrl.searchParams.get("userId")),
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "GET" && pathname === "/api/admin/generation-logs") {
|
||||||
|
const userId = requestUrl.searchParams.get("userId");
|
||||||
|
const limit = Number(requestUrl.searchParams.get("limit") || 100);
|
||||||
|
const logs = userId ? db.generationLogs.filter(log => Number(log.user_id) === Number(userId)) : db.generationLogs;
|
||||||
|
const rows = logRows(db, logs).slice(0, limit);
|
||||||
|
sendJson(res, 200, { success: true, logs: rows, total: logs.length });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "GET" && pathname === "/api/admin/system/config") {
|
||||||
|
sendJson(res, 200, { success: true, configs: configRows(db) });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "PUT" && pathname === "/api/admin/system/config") {
|
||||||
|
const updates = Array.isArray(body.configs)
|
||||||
|
? body.configs.reduce((acc, item) => ({ ...acc, [item.config_key]: item.config_value }), {})
|
||||||
|
: body.configs || body;
|
||||||
|
for (const [key, value] of Object.entries(updates || {})) {
|
||||||
|
if (key in db.systemConfigs) db.systemConfigs[key] = String(value ?? "");
|
||||||
|
}
|
||||||
|
addUserLog(db, admin.id, "system_config", "管理员更新系统配置");
|
||||||
|
saveDb(db);
|
||||||
|
sendJson(res, 200, { success: true, configs: configRows(db) });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "PUT" && pathname === "/api/admin/user/cloud-digital-human") {
|
||||||
|
const user = db.users.find(item => Number(item.id) === Number(body.user_id));
|
||||||
|
if (!user) {
|
||||||
|
sendJson(res, 200, { success: false, message: "用户不存在" });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
user.cloud_digital_human_disabled = body.disabled ? 1 : 0;
|
||||||
|
user.updated_at = nowIso();
|
||||||
|
addUserLog(db, user.id, "cloud_digital_human", `云数字人状态 ${user.cloud_digital_human_disabled ? "禁用" : "启用"}`);
|
||||||
|
saveDb(db);
|
||||||
|
sendJson(res, 200, { success: true, user: userRow(db, user) });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendJson(res, 404, { success: false, message: `Admin endpoint not found: ${req.method} ${pathname}` });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
handleAdminApi,
|
||||||
|
};
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
const { publicUser, findUserByToken } = require("./auth.cjs");
|
||||||
|
const { loadDb, saveDb, getSubscription, addUserLog, addGenerationLog } = require("./db.cjs");
|
||||||
|
const { sendJson } = require("./http-utils.cjs");
|
||||||
|
const { nowIso, createToken, checkPassword, passwordHash, daysRemaining } = require("./security.cjs");
|
||||||
|
|
||||||
|
function findLoginUser(db, account) {
|
||||||
|
const value = String(account || "").trim();
|
||||||
|
return db.users.find(user => user.email === value || user.username === value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAppApi(req, res, pathname, body) {
|
||||||
|
const db = loadDb();
|
||||||
|
|
||||||
|
if (req.method === "GET" && pathname === "/api/system/config/public") {
|
||||||
|
sendJson(res, 200, { success: true, config: db.systemConfigs });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "POST" && pathname === "/api/register") {
|
||||||
|
const username = String(body.username || "").trim();
|
||||||
|
const email = String(body.email || "").trim();
|
||||||
|
const password = String(body.password || "");
|
||||||
|
|
||||||
|
if (!username || !email || !password) {
|
||||||
|
sendJson(res, 200, { success: false, message: "用户名、手机号和密码不能为空" });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (db.users.some(user => user.email === email || user.username === username)) {
|
||||||
|
sendJson(res, 200, { success: false, message: "用户名或手机号已存在" });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFirstUser = db.users.length === 0;
|
||||||
|
const user = {
|
||||||
|
id: db.nextUserId++,
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password: passwordHash(password),
|
||||||
|
role: isFirstUser ? "admin" : "user",
|
||||||
|
status: "active",
|
||||||
|
points: isFirstUser ? 999999 : 1000,
|
||||||
|
is_super_admin: isFirstUser ? 1 : 0,
|
||||||
|
owner_admin_id: null,
|
||||||
|
invite_code: body.invite_code || null,
|
||||||
|
max_devices: 99,
|
||||||
|
device_fingerprint: body.device_fingerprint || "",
|
||||||
|
created_at: nowIso(),
|
||||||
|
updated_at: nowIso(),
|
||||||
|
last_login_at: nowIso(),
|
||||||
|
last_login_device: body.device_fingerprint || null,
|
||||||
|
current_token: "",
|
||||||
|
cloud_digital_human_disabled: 0,
|
||||||
|
};
|
||||||
|
const token = createToken(user.id);
|
||||||
|
user.current_token = token;
|
||||||
|
db.users.push(user);
|
||||||
|
const subscription = getSubscription(db, user.id);
|
||||||
|
addUserLog(db, user.id, "register", "本地注册成功", { device_fingerprint: body.device_fingerprint });
|
||||||
|
saveDb(db);
|
||||||
|
|
||||||
|
sendJson(res, 200, {
|
||||||
|
success: true,
|
||||||
|
token,
|
||||||
|
userId: user.id,
|
||||||
|
user: publicUser(user),
|
||||||
|
subscription,
|
||||||
|
message: "本地注册成功",
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "POST" && pathname === "/api/login") {
|
||||||
|
const account = body.email || body.username || body.phone;
|
||||||
|
const password = String(body.password || "");
|
||||||
|
const user = findLoginUser(db, account);
|
||||||
|
|
||||||
|
if (!user || !checkPassword(password, user.password)) {
|
||||||
|
sendJson(res, 200, { success: false, message: "账号或密码错误" });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.status !== "active") {
|
||||||
|
sendJson(res, 200, { success: false, message: "账户已被禁用" });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = createToken(user.id);
|
||||||
|
user.current_token = token;
|
||||||
|
user.last_login_at = nowIso();
|
||||||
|
user.last_login_device = body.device_fingerprint || null;
|
||||||
|
const subscription = getSubscription(db, user.id);
|
||||||
|
addUserLog(db, user.id, "login", "本地登录成功", { device_fingerprint: body.device_fingerprint });
|
||||||
|
saveDb(db);
|
||||||
|
|
||||||
|
sendJson(res, 200, {
|
||||||
|
success: true,
|
||||||
|
token,
|
||||||
|
user: publicUser(user),
|
||||||
|
subscription,
|
||||||
|
message: "本地登录成功",
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "POST" && pathname === "/api/verify") {
|
||||||
|
const user = findUserByToken(db, body.token);
|
||||||
|
if (!user || user.status !== "active") {
|
||||||
|
sendJson(res, 200, { valid: false, success: false, message: "Token 无效" });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscription = getSubscription(db, user.id);
|
||||||
|
saveDb(db);
|
||||||
|
sendJson(res, 200, {
|
||||||
|
valid: true,
|
||||||
|
success: true,
|
||||||
|
payload: { userId: user.id, role: user.role },
|
||||||
|
user: publicUser(user),
|
||||||
|
subscription,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "POST" && pathname === "/api/subscription/check") {
|
||||||
|
const userId = Number(body.user_id || body.userId);
|
||||||
|
const subscription = getSubscription(db, userId);
|
||||||
|
const remainingDays = daysRemaining(subscription.expires_at);
|
||||||
|
saveDb(db);
|
||||||
|
sendJson(res, 200, {
|
||||||
|
success: true,
|
||||||
|
hasAccess: remainingDays > 0,
|
||||||
|
expiresAt: subscription.expires_at,
|
||||||
|
remainingDays,
|
||||||
|
isExpired: remainingDays <= 0,
|
||||||
|
subscription,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "POST" && pathname === "/api/device/checkin") {
|
||||||
|
const userId = Number(body.user_id || body.userId || 0);
|
||||||
|
if (userId) {
|
||||||
|
const user = db.users.find(item => Number(item.id) === userId);
|
||||||
|
if (user) {
|
||||||
|
user.device_fingerprint = body.device_fingerprint || user.device_fingerprint || "";
|
||||||
|
user.last_login_device = body.device_fingerprint || user.last_login_device || null;
|
||||||
|
user.updated_at = nowIso();
|
||||||
|
}
|
||||||
|
if (body.device_fingerprint && !db.devices.some(item => item.user_id === userId && item.device_fingerprint === body.device_fingerprint)) {
|
||||||
|
db.devices.push({ user_id: userId, device_fingerprint: body.device_fingerprint, created_at: nowIso() });
|
||||||
|
}
|
||||||
|
saveDb(db);
|
||||||
|
}
|
||||||
|
sendJson(res, 200, { success: true, message: "本地设备校验通过" });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "POST" && pathname === "/api/user/generation-log") {
|
||||||
|
addGenerationLog(
|
||||||
|
db,
|
||||||
|
body.user_id || body.userId,
|
||||||
|
body.type,
|
||||||
|
body.detail || body.message || body,
|
||||||
|
{ device_fingerprint: body.device_fingerprint },
|
||||||
|
);
|
||||||
|
saveDb(db);
|
||||||
|
sendJson(res, 200, { success: true });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
handleAppApi,
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
const { verifyToken } = require("./security.cjs");
|
||||||
|
|
||||||
|
function publicUser(user) {
|
||||||
|
if (!user) return null;
|
||||||
|
const { password, current_token, ...safeUser } = user;
|
||||||
|
return safeUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findUserByToken(db, token) {
|
||||||
|
const payload = verifyToken(token);
|
||||||
|
if (!payload?.userId) return null;
|
||||||
|
return db.users.find(user => Number(user.id) === Number(payload.userId)) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBearerToken(req) {
|
||||||
|
const header = req.headers.authorization || "";
|
||||||
|
const match = String(header).match(/^Bearer\s+(.+)$/i);
|
||||||
|
return match ? match[1] : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireAdmin(db, req) {
|
||||||
|
const user = findUserByToken(db, getBearerToken(req));
|
||||||
|
if (!user || user.status !== "active") return null;
|
||||||
|
if (user.role !== "admin" && Number(user.is_super_admin || 0) !== 1) return null;
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
publicUser,
|
||||||
|
findUserByToken,
|
||||||
|
getBearerToken,
|
||||||
|
requireAdmin,
|
||||||
|
};
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
const path = require("node:path");
|
||||||
|
const os = require("node:os");
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, "../..");
|
||||||
|
|
||||||
|
const defaultSystemConfig = {
|
||||||
|
aliyun_bailian_api_key: "",
|
||||||
|
aliyun_bailian_base_url: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||||
|
aliyun_oss_access_key_id: "",
|
||||||
|
aliyun_oss_access_key_secret: "",
|
||||||
|
aliyun_oss_bucket: "",
|
||||||
|
aliyun_oss_region: "oss-cn-beijing",
|
||||||
|
volcengine_api_key: "",
|
||||||
|
volcengine_model: "doubao-seed-2-0-mini-260215",
|
||||||
|
runninghub_api_key: "",
|
||||||
|
runninghub_workflow_id_1: "2013514129943826433",
|
||||||
|
runninghub_workflow_id_2: "2013514129943826433",
|
||||||
|
runninghub_tts_webapp_id_v2: "1965614643077070850",
|
||||||
|
runninghub_tts_webapp_id_v3: "2028779949334728706",
|
||||||
|
runninghub_tts_v2_audio_node: "13",
|
||||||
|
runninghub_tts_v2_text_node: "14",
|
||||||
|
runninghub_tts_v2_emotion_node: "15",
|
||||||
|
cloud_beauty_base_url: "",
|
||||||
|
cloud_beauty_api_key: "",
|
||||||
|
cloud_beauty_default_mode: "256m",
|
||||||
|
cloud_beauty_enabled: "false",
|
||||||
|
trial_duration_hours: "168",
|
||||||
|
update_url: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const systemConfigDescriptions = {
|
||||||
|
aliyun_bailian_api_key: "阿里云百炼 / DashScope API Key",
|
||||||
|
aliyun_bailian_base_url: "阿里云百炼兼容 OpenAI Base URL",
|
||||||
|
aliyun_oss_access_key_id: "阿里云 OSS AccessKey ID",
|
||||||
|
aliyun_oss_access_key_secret: "阿里云 OSS AccessKey Secret",
|
||||||
|
aliyun_oss_bucket: "阿里云 OSS Bucket",
|
||||||
|
aliyun_oss_region: "阿里云 OSS Region",
|
||||||
|
volcengine_api_key: "火山引擎 API Key",
|
||||||
|
volcengine_model: "火山引擎默认模型",
|
||||||
|
runninghub_api_key: "RunningHub API Key",
|
||||||
|
runninghub_workflow_id_1: "RunningHub 工作流 ID 1",
|
||||||
|
runninghub_workflow_id_2: "RunningHub 工作流 ID 2",
|
||||||
|
runninghub_tts_webapp_id_v2: "RunningHub TTS WebApp ID V2",
|
||||||
|
runninghub_tts_webapp_id_v3: "RunningHub TTS WebApp ID V3",
|
||||||
|
runninghub_tts_v2_audio_node: "RunningHub TTS V2 音频节点",
|
||||||
|
runninghub_tts_v2_text_node: "RunningHub TTS V2 文本节点",
|
||||||
|
runninghub_tts_v2_emotion_node: "RunningHub TTS V2 情绪节点",
|
||||||
|
cloud_beauty_base_url: "云端美颜 Base URL",
|
||||||
|
cloud_beauty_api_key: "云端美颜 API Key",
|
||||||
|
cloud_beauty_default_mode: "云端美颜默认模式",
|
||||||
|
cloud_beauty_enabled: "是否启用云端美颜",
|
||||||
|
trial_duration_hours: "试用时长(小时)",
|
||||||
|
update_url: "软件更新地址",
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveDataDir() {
|
||||||
|
if (process.env.LOCAL_AUTH_DATA_DIR) return path.resolve(process.env.LOCAL_AUTH_DATA_DIR);
|
||||||
|
return path.join(process.env.APPDATA || os.homedir() || root, "aIzhinengti-clean-local-auth");
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
root,
|
||||||
|
host: process.env.LOCAL_AUTH_HOST || "127.0.0.1",
|
||||||
|
port: Number(process.env.LOCAL_AUTH_PORT || process.env.PORT || 3302),
|
||||||
|
dataDir: resolveDataDir(),
|
||||||
|
tokenSecret: process.env.LOCAL_AUTH_SECRET || "local-auth-dev-secret",
|
||||||
|
adminPanelDir: process.env.ADMIN_PANEL_DIR || path.join(root, "admin-panel"),
|
||||||
|
defaultUser: {
|
||||||
|
username: process.env.LOCAL_AUTH_DEFAULT_USERNAME || "本地管理员",
|
||||||
|
email: process.env.LOCAL_AUTH_DEFAULT_EMAIL || "13800138000",
|
||||||
|
password: process.env.LOCAL_AUTH_DEFAULT_PASSWORD || "123456",
|
||||||
|
},
|
||||||
|
defaultSystemConfig,
|
||||||
|
systemConfigDescriptions,
|
||||||
|
};
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
const fs = require("node:fs");
|
||||||
|
const path = require("node:path");
|
||||||
|
const { dataDir, defaultUser, defaultSystemConfig } = require("./config.cjs");
|
||||||
|
const { addYears, nowIso, passwordHash } = require("./security.cjs");
|
||||||
|
|
||||||
|
const dbPath = path.join(dataDir, "users.json");
|
||||||
|
let memoryDb = null;
|
||||||
|
|
||||||
|
function createInitialDb() {
|
||||||
|
const expiresAt = addYears(new Date(), 100).toISOString();
|
||||||
|
const user = {
|
||||||
|
id: 1,
|
||||||
|
username: defaultUser.username,
|
||||||
|
email: defaultUser.email,
|
||||||
|
password: passwordHash(defaultUser.password),
|
||||||
|
role: "admin",
|
||||||
|
status: "active",
|
||||||
|
points: 999999,
|
||||||
|
is_super_admin: 1,
|
||||||
|
owner_admin_id: null,
|
||||||
|
invite_code: "LOCALADMIN",
|
||||||
|
max_devices: 99,
|
||||||
|
device_fingerprint: "",
|
||||||
|
created_at: nowIso(),
|
||||||
|
updated_at: nowIso(),
|
||||||
|
last_login_at: null,
|
||||||
|
last_login_device: null,
|
||||||
|
current_token: "",
|
||||||
|
cloud_digital_human_disabled: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
nextUserId: 2,
|
||||||
|
nextSubscriptionId: 2,
|
||||||
|
nextUserLogId: 1,
|
||||||
|
nextGenerationLogId: 1,
|
||||||
|
users: [user],
|
||||||
|
subscriptions: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
user_id: 1,
|
||||||
|
subscription_type: "local",
|
||||||
|
expires_at: expiresAt,
|
||||||
|
trial_started_at: nowIso(),
|
||||||
|
created_at: nowIso(),
|
||||||
|
updated_at: nowIso(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
generationLogs: [],
|
||||||
|
userLogs: [],
|
||||||
|
devices: [],
|
||||||
|
systemConfigs: { ...defaultSystemConfig },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSystemConfigs(value) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.reduce((configs, item) => {
|
||||||
|
if (item?.config_key) configs[item.config_key] = String(item.config_value ?? "");
|
||||||
|
return configs;
|
||||||
|
}, { ...defaultSystemConfig });
|
||||||
|
}
|
||||||
|
return { ...defaultSystemConfig, ...(value || {}) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDb(db) {
|
||||||
|
const initial = createInitialDb();
|
||||||
|
const normalized = { ...initial, ...(db || {}) };
|
||||||
|
normalized.users = Array.isArray(normalized.users) ? normalized.users : initial.users;
|
||||||
|
normalized.subscriptions = Array.isArray(normalized.subscriptions) ? normalized.subscriptions : initial.subscriptions;
|
||||||
|
normalized.generationLogs = Array.isArray(normalized.generationLogs) ? normalized.generationLogs : [];
|
||||||
|
normalized.userLogs = Array.isArray(normalized.userLogs) ? normalized.userLogs : [];
|
||||||
|
normalized.devices = Array.isArray(normalized.devices) ? normalized.devices : [];
|
||||||
|
normalized.systemConfigs = normalizeSystemConfigs(normalized.systemConfigs);
|
||||||
|
|
||||||
|
for (const user of normalized.users) {
|
||||||
|
user.role = user.role || "user";
|
||||||
|
user.status = user.status || "active";
|
||||||
|
user.points = Number(user.points || 0);
|
||||||
|
user.max_devices = Number(user.max_devices || 1);
|
||||||
|
user.is_super_admin = Number(user.is_super_admin || 0);
|
||||||
|
user.cloud_digital_human_disabled = Number(user.cloud_digital_human_disabled || 0);
|
||||||
|
user.created_at = user.created_at || nowIso();
|
||||||
|
user.updated_at = user.updated_at || nowIso();
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized.nextUserId = Math.max(
|
||||||
|
Number(normalized.nextUserId || 1),
|
||||||
|
...normalized.users.map(user => Number(user.id || 0) + 1),
|
||||||
|
);
|
||||||
|
normalized.nextSubscriptionId = Math.max(
|
||||||
|
Number(normalized.nextSubscriptionId || 1),
|
||||||
|
...normalized.subscriptions.map(item => Number(item.id || 0) + 1),
|
||||||
|
);
|
||||||
|
normalized.nextUserLogId = Math.max(
|
||||||
|
Number(normalized.nextUserLogId || 1),
|
||||||
|
...normalized.userLogs.map(item => Number(item.id || 0) + 1),
|
||||||
|
);
|
||||||
|
normalized.nextGenerationLogId = Math.max(
|
||||||
|
Number(normalized.nextGenerationLogId || 1),
|
||||||
|
...normalized.generationLogs.map(item => Number(item.id || 0) + 1),
|
||||||
|
);
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureDb() {
|
||||||
|
if (memoryDb) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(dataDir, { recursive: true });
|
||||||
|
|
||||||
|
if (!fs.existsSync(dbPath)) {
|
||||||
|
fs.writeFileSync(dbPath, JSON.stringify(createInitialDb(), null, 2), "utf8");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
memoryDb = createInitialDb();
|
||||||
|
console.warn(`[local-auth] cannot write ${dbPath}; using in-memory database: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadDb() {
|
||||||
|
ensureDb();
|
||||||
|
if (memoryDb) return memoryDb;
|
||||||
|
return normalizeDb(JSON.parse(fs.readFileSync(dbPath, "utf8")));
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveDb(db) {
|
||||||
|
const normalized = normalizeDb(db);
|
||||||
|
if (memoryDb) {
|
||||||
|
memoryDb = normalized;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fs.writeFileSync(dbPath, JSON.stringify(normalized, null, 2), "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSubscription(db, userId) {
|
||||||
|
const existing = db.subscriptions.find(item => Number(item.user_id) === Number(userId));
|
||||||
|
if (existing) return existing;
|
||||||
|
|
||||||
|
const subscription = {
|
||||||
|
id: db.nextSubscriptionId++,
|
||||||
|
user_id: Number(userId),
|
||||||
|
subscription_type: "local",
|
||||||
|
expires_at: addYears(new Date(), 100).toISOString(),
|
||||||
|
trial_started_at: nowIso(),
|
||||||
|
created_at: nowIso(),
|
||||||
|
updated_at: nowIso(),
|
||||||
|
};
|
||||||
|
db.subscriptions.push(subscription);
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addUserLog(db, userId, action, message, extra = {}) {
|
||||||
|
db.userLogs.push({
|
||||||
|
id: db.nextUserLogId++,
|
||||||
|
user_id: Number(userId || 0) || null,
|
||||||
|
action,
|
||||||
|
message,
|
||||||
|
device_fingerprint: extra.device_fingerprint || null,
|
||||||
|
ip_address: extra.ip_address || "127.0.0.1",
|
||||||
|
created_at: nowIso(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addGenerationLog(db, userId, type, detail, extra = {}) {
|
||||||
|
db.generationLogs.push({
|
||||||
|
id: db.nextGenerationLogId++,
|
||||||
|
user_id: Number(userId || 0) || null,
|
||||||
|
type: type || "unknown",
|
||||||
|
detail: typeof detail === "string" ? detail : JSON.stringify(detail || {}),
|
||||||
|
device_fingerprint: extra.device_fingerprint || null,
|
||||||
|
ip_address: extra.ip_address || "127.0.0.1",
|
||||||
|
created_at: nowIso(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
dbPath,
|
||||||
|
ensureDb,
|
||||||
|
loadDb,
|
||||||
|
saveDb,
|
||||||
|
getSubscription,
|
||||||
|
addUserLog,
|
||||||
|
addGenerationLog,
|
||||||
|
};
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
const fs = require("node:fs");
|
||||||
|
const path = require("node:path");
|
||||||
|
|
||||||
|
function sendJson(res, statusCode, body) {
|
||||||
|
res.writeHead(statusCode, {
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
||||||
|
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
||||||
|
"Cache-Control": "no-store",
|
||||||
|
});
|
||||||
|
res.end(JSON.stringify(body));
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendText(res, statusCode, text, contentType = "text/plain; charset=utf-8") {
|
||||||
|
res.writeHead(statusCode, {
|
||||||
|
"Content-Type": contentType,
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Cache-Control": "no-store",
|
||||||
|
});
|
||||||
|
res.end(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function redirect(res, location) {
|
||||||
|
res.writeHead(302, { Location: location });
|
||||||
|
res.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readBody(req) {
|
||||||
|
const chunks = [];
|
||||||
|
for await (const chunk of req) chunks.push(chunk);
|
||||||
|
const text = Buffer.concat(chunks).toString("utf8");
|
||||||
|
if (!text) return {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(text);
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function serveFile(res, filePath, contentType = "text/html; charset=utf-8") {
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
sendText(res, 404, "Not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(200, {
|
||||||
|
"Content-Type": contentType,
|
||||||
|
"Cache-Control": "no-store",
|
||||||
|
});
|
||||||
|
fs.createReadStream(filePath).pipe(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeJoin(baseDir, requestPath) {
|
||||||
|
const resolved = path.resolve(baseDir, requestPath.replace(/^\/+/, ""));
|
||||||
|
if (!resolved.startsWith(path.resolve(baseDir))) return null;
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
sendJson,
|
||||||
|
sendText,
|
||||||
|
redirect,
|
||||||
|
readBody,
|
||||||
|
serveFile,
|
||||||
|
safeJoin,
|
||||||
|
};
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
const http = require("node:http");
|
||||||
|
const { host, port, defaultUser } = require("./config.cjs");
|
||||||
|
const { dbPath, ensureDb } = require("./db.cjs");
|
||||||
|
const { handleAppApi } = require("./app-api.cjs");
|
||||||
|
const { handleAdminApi } = require("./admin-api.cjs");
|
||||||
|
const { handleAdminStatic } = require("./static-admin.cjs");
|
||||||
|
const { sendJson } = require("./http-utils.cjs");
|
||||||
|
const { nowIso } = require("./security.cjs");
|
||||||
|
const { readBody } = require("./http-utils.cjs");
|
||||||
|
|
||||||
|
function createLocalAuthServer() {
|
||||||
|
ensureDb();
|
||||||
|
|
||||||
|
return http.createServer(async (req, res) => {
|
||||||
|
const requestUrl = new URL(req.url, `http://${req.headers.host || `${host}:${port}`}`);
|
||||||
|
const pathname = requestUrl.pathname.replace(/\/+$/, "") || "/";
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (req.method === "OPTIONS") {
|
||||||
|
sendJson(res, 200, { success: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "GET" && (pathname === "/" || pathname === "/health" || pathname === "/api/health")) {
|
||||||
|
sendJson(res, 200, { success: true, service: "local-auth", admin: "/admin/", time: nowIso() });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handleAdminStatic(req, res, requestUrl.pathname)) return;
|
||||||
|
|
||||||
|
const body = ["POST", "PUT", "PATCH"].includes(req.method) ? await readBody(req) : {};
|
||||||
|
if (await handleAdminApi(req, res, pathname, body, requestUrl)) return;
|
||||||
|
if (await handleAppApi(req, res, pathname, body, requestUrl)) return;
|
||||||
|
|
||||||
|
sendJson(res, 404, { success: false, message: `Local auth endpoint not found: ${req.method} ${pathname}` });
|
||||||
|
} catch (error) {
|
||||||
|
sendJson(res, 500, { success: false, message: error.message || "Local auth server error" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function start() {
|
||||||
|
const server = createLocalAuthServer();
|
||||||
|
server.listen(port, host, () => {
|
||||||
|
console.log(`[local-auth] running at http://${host}:${port}`);
|
||||||
|
console.log(`[local-auth] admin panel: http://${host}:${port}/admin/`);
|
||||||
|
console.log(`[local-auth] default account: ${defaultUser.email} / ${defaultUser.password}`);
|
||||||
|
console.log(`[local-auth] data file: ${dbPath}`);
|
||||||
|
});
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module || module.parent?.filename?.endsWith("local-auth-server.cjs")) {
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createLocalAuthServer,
|
||||||
|
start,
|
||||||
|
};
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
const crypto = require("node:crypto");
|
||||||
|
const { tokenSecret } = require("./config.cjs");
|
||||||
|
|
||||||
|
function nowIso() {
|
||||||
|
return new Date().toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addYears(date, years) {
|
||||||
|
const next = new Date(date);
|
||||||
|
next.setFullYear(next.getFullYear() + years);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addHours(date, hours) {
|
||||||
|
return new Date(date.getTime() + Number(hours || 0) * 3600000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sign(value) {
|
||||||
|
return crypto.createHmac("sha256", tokenSecret).update(value).digest("base64url");
|
||||||
|
}
|
||||||
|
|
||||||
|
function createToken(userId) {
|
||||||
|
const payload = Buffer.from(JSON.stringify({ userId, issuedAt: Date.now() })).toString("base64url");
|
||||||
|
return `${payload}.${sign(payload)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyToken(token) {
|
||||||
|
if (!token || typeof token !== "string" || !token.includes(".")) return null;
|
||||||
|
const [payload, signature] = token.split(".");
|
||||||
|
if (!payload || !signature || sign(payload) !== signature) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function passwordHash(password, salt = crypto.randomBytes(16).toString("hex")) {
|
||||||
|
const hash = crypto.pbkdf2Sync(String(password), salt, 120000, 32, "sha256").toString("hex");
|
||||||
|
return `${salt}:${hash}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkPassword(password, stored) {
|
||||||
|
const [salt] = String(stored || "").split(":");
|
||||||
|
if (!salt) return false;
|
||||||
|
return passwordHash(password, salt) === stored;
|
||||||
|
}
|
||||||
|
|
||||||
|
function daysRemaining(expiresAt) {
|
||||||
|
return Math.max(0, Math.ceil((new Date(expiresAt).getTime() - Date.now()) / 86400000));
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
nowIso,
|
||||||
|
addYears,
|
||||||
|
addHours,
|
||||||
|
createToken,
|
||||||
|
verifyToken,
|
||||||
|
passwordHash,
|
||||||
|
checkPassword,
|
||||||
|
daysRemaining,
|
||||||
|
};
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
const path = require("node:path");
|
||||||
|
const { adminPanelDir } = require("./config.cjs");
|
||||||
|
const { redirect, serveFile, safeJoin } = require("./http-utils.cjs");
|
||||||
|
|
||||||
|
function handleAdminStatic(req, res, pathname) {
|
||||||
|
if (req.method !== "GET") return false;
|
||||||
|
|
||||||
|
if (pathname === "/admin") {
|
||||||
|
redirect(res, "/admin/");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathname === "/admin/") {
|
||||||
|
serveFile(res, path.join(adminPanelDir, "index.html"));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathname.startsWith("/admin/")) {
|
||||||
|
const filePath = safeJoin(adminPanelDir, pathname.replace(/^\/admin\//, ""));
|
||||||
|
if (!filePath) return false;
|
||||||
|
const ext = path.extname(filePath).toLowerCase();
|
||||||
|
const contentType = {
|
||||||
|
".css": "text/css; charset=utf-8",
|
||||||
|
".js": "text/javascript; charset=utf-8",
|
||||||
|
".json": "application/json; charset=utf-8",
|
||||||
|
".png": "image/png",
|
||||||
|
".jpg": "image/jpeg",
|
||||||
|
".jpeg": "image/jpeg",
|
||||||
|
".svg": "image/svg+xml",
|
||||||
|
}[ext] || "application/octet-stream";
|
||||||
|
serveFile(res, filePath, contentType);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
handleAdminStatic,
|
||||||
|
};
|
||||||
+1
-1
@@ -12,7 +12,7 @@ export const AppConfig = {
|
|||||||
website: ``,
|
website: ``,
|
||||||
websiteGithub: "",
|
websiteGithub: "",
|
||||||
websiteGitee: "",
|
websiteGitee: "",
|
||||||
apiBaseUrl: `http://49.233.157.120:3002/api`,
|
apiBaseUrl: `http://127.0.0.1:3302/api`,
|
||||||
updaterUrl: ``,
|
updaterUrl: ``,
|
||||||
downloadUrl: ``,
|
downloadUrl: ``,
|
||||||
feedbackUrl: ``,
|
feedbackUrl: ``,
|
||||||
|
|||||||
Reference in New Issue
Block a user