新增本地认证与管理接口服务

This commit is contained in:
cat-shark
2026-06-20 10:07:44 +08:00
parent a13b804c7a
commit cf82d606aa
10 changed files with 1057 additions and 0 deletions
+354
View File
@@ -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,
};
+177
View File
@@ -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,
};
+33
View File
@@ -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,
};
+75
View File
@@ -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,
};
+186
View File
@@ -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,
};
+68
View File
@@ -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,
};
+60
View File
@@ -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,
};
+63
View File
@@ -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,
};
+40
View File
@@ -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,
};