Compare commits
4 Commits
a13b804c7a
...
b6f30a838f
| Author | SHA1 | Date | |
|---|---|---|---|
| b6f30a838f | |||
| 8922a3eef6 | |||
| 0fd56f8841 | |||
| cf82d606aa |
@@ -45,6 +45,7 @@ database.db
|
||||
|
||||
src/lang/source-use.json
|
||||
/data
|
||||
/local-auth-data/
|
||||
/_delete
|
||||
.vscode
|
||||
|
||||
|
||||
+37
-5
@@ -2,7 +2,7 @@
|
||||
|
||||
这是从原目录整理出的独立副本,后续开发、启动和构建请在本目录执行。
|
||||
|
||||
## 启动
|
||||
## 启动应用
|
||||
|
||||
```bat
|
||||
start-local.bat
|
||||
@@ -14,14 +14,46 @@ start-local.bat
|
||||
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
|
||||
npm.cmd run start:check
|
||||
```
|
||||
|
||||
## 说明
|
||||
## 系统 API Key 配置
|
||||
|
||||
- 默认开发端口已改为 `3354`,避免和原目录的 `3344` 服务冲突。
|
||||
- 已排除旧目录中的构建产物、日志、缓存、用户数据和 Git 信息。
|
||||
- `node_modules` 是实拷贝,不是软链接;同时排除了旧 Electron 目录中误混入的巨大运行数据。
|
||||
进入 `http://127.0.0.1:3302/admin/` 后,在“系统配置”里填写:
|
||||
|
||||
- `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": {
|
||||
"start:local": "node scripts/start-local.cjs",
|
||||
"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",
|
||||
"build": "node scripts/clean-build-artifacts.cjs && vite build",
|
||||
"build:dev": "node scripts/clean-build-artifacts.cjs && vite build --mode development",
|
||||
@@ -22,7 +24,7 @@
|
||||
"version:sync": "node scripts/sync-version.cjs",
|
||||
"dev:all": "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",
|
||||
"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",
|
||||
|
||||
@@ -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 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 requiredNodeMajor = 20;
|
||||
const checkOnly = process.argv.includes("--check");
|
||||
@@ -18,11 +19,11 @@ function fail(message) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function parseServerUrl(url) {
|
||||
function parseUrl(url, name) {
|
||||
try {
|
||||
return new URL(url);
|
||||
} 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) => {
|
||||
const server = net.createServer();
|
||||
|
||||
server.once("error", error => {
|
||||
if (error.code === "EADDRINUSE") {
|
||||
reject(new Error(`端口 ${port} 已被占用,请先关闭已有 Vite/Electron 开发进程。`));
|
||||
reject(new Error(`${label} 端口 ${port} 已被占用,请先关闭已有进程。`));
|
||||
return;
|
||||
}
|
||||
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() {
|
||||
process.chdir(projectRoot);
|
||||
|
||||
const parsedUrl = parseServerUrl(serverUrl);
|
||||
const host = parsedUrl.hostname || "127.0.0.1";
|
||||
const port = Number(parsedUrl.port || 3344);
|
||||
const parsedServerUrl = parseUrl(serverUrl, "VITE_DEV_SERVER_URL");
|
||||
const parsedAuthUrl = parseUrl(authServerUrl, "AUTH_SERVER_URL");
|
||||
const viteHost = parsedServerUrl.hostname || "127.0.0.1";
|
||||
const vitePort = Number(parsedServerUrl.port || 3354);
|
||||
|
||||
log("========================================");
|
||||
log(" 本地快速启动:Vite + Electron");
|
||||
log(" 本地快速启动:Local Auth + Vite + Electron");
|
||||
log("========================================");
|
||||
log(`项目目录:${projectRoot}`);
|
||||
log(`认证服务:${authServerUrl}`);
|
||||
log(`开发地址:${serverUrl}`);
|
||||
log("");
|
||||
|
||||
checkNodeVersion();
|
||||
const npmVersion = checkCommand(npmCommand, ["--version"], "npm");
|
||||
checkDependencies();
|
||||
await assertPortAvailable(host, port);
|
||||
await assertPortAvailable(viteHost, vitePort, "Vite");
|
||||
|
||||
log(`Node.js:${process.version}`);
|
||||
log(`npm:${npmVersion}`);
|
||||
@@ -105,24 +157,36 @@ async function main() {
|
||||
return;
|
||||
}
|
||||
|
||||
const authChild = await startLocalAuth(parsedAuthUrl);
|
||||
|
||||
log("");
|
||||
log("正在启动应用,关闭此窗口即可停止开发服务。");
|
||||
log("默认本地账号:13800138000 / 123456");
|
||||
log("");
|
||||
|
||||
const child = spawn(npmCommand, ["run", "dev:electron"], {
|
||||
const appChild = spawn(npmCommand, ["run", "dev:electron"], {
|
||||
cwd: projectRoot,
|
||||
stdio: "inherit",
|
||||
env: {
|
||||
...process.env,
|
||||
VITE_DEV_SERVER_URL: serverUrl,
|
||||
AUTH_SERVER_URL: authServerUrl,
|
||||
},
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
|
||||
child.on("exit", code => {
|
||||
const shutdown = code => {
|
||||
if (authChild && !authChild.killed) {
|
||||
authChild.kill();
|
||||
}
|
||||
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}`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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: ``,
|
||||
websiteGithub: "",
|
||||
websiteGitee: "",
|
||||
apiBaseUrl: `http://49.233.157.120:3002/api`,
|
||||
apiBaseUrl: `http://127.0.0.1:3302/api`,
|
||||
updaterUrl: ``,
|
||||
downloadUrl: ``,
|
||||
feedbackUrl: ``,
|
||||
|
||||
Reference in New Issue
Block a user