新增本地认证与管理接口服务
This commit is contained in:
@@ -0,0 +1 @@
|
||||
require("../server/local-auth/index.cjs");
|
||||
@@ -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,
|
||||
};
|
||||
Reference in New Issue
Block a user