Files
omniai-server/src/auth.js
T

323 lines
9.2 KiB
JavaScript
Raw Normal View History

2026-06-02 13:14:10 +08:00
const jwt = require("jsonwebtoken");
const bcrypt = require("bcryptjs");
const crypto = require("node:crypto");
const { pool } = require("./db");
const { getJwtSecret } = require("./securityConfig");
const JWT_SECRET = getJwtSecret();
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "7d";
const MAX_CONCURRENT_SESSIONS = 2;
const USER_CONTEXT_SELECT = `
SELECT
u.id,
u.username,
u.avatar_url,
u.bio,
u.profile_background_url,
u.email,
u.email_verified,
u.phone,
u.auth_provider,
u.current_session_id,
u.current_session_started_at,
u.role,
u.max_concurrency,
u.enabled,
u.enterprise_id,
u.is_enterprise_admin,
u.balance_cents AS user_balance_cents,
u.billing_mode,
u.beta_expires_at,
e.name AS enterprise_name,
e.enabled AS enterprise_enabled,
e.balance_cents AS enterprise_balance_cents,
e.enterprise_code,
e.admin_user_id AS enterprise_admin_user_id,
COALESCE(
em.role,
CASE
WHEN u.is_enterprise_admin = 1 THEN 'admin'
WHEN u.enterprise_id IS NOT NULL THEN 'employee'
ELSE NULL
END
) AS enterprise_role
FROM users u
LEFT JOIN enterprises e ON e.id = u.enterprise_id
LEFT JOIN enterprise_members em ON em.enterprise_id = u.enterprise_id AND em.user_id = u.id
`;
function generateEnterpriseCode() {
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
let code = "ENT-";
for (let i = 0; i < 6; i++) {
code += chars.charAt(Math.floor(Math.random() * chars.length));
}
return code;
}
async function generateUniqueEnterpriseCode() {
for (let attempt = 0; attempt < 10; attempt++) {
const code = generateEnterpriseCode();
const { rows } = await pool.query("SELECT 1 FROM enterprises WHERE enterprise_code = $1", [
code,
]);
if (rows.length === 0) return code;
}
throw new Error("Failed to generate unique enterprise code");
}
async function toUserResponse(user) {
if (!user) return null;
const enterpriseId = user.enterprise_id == null ? null : Number(user.enterprise_id);
const isEnterpriseAdmin = !!user.is_enterprise_admin;
const enterpriseRole = enterpriseId
? (user.enterprise_role || (isEnterpriseAdmin ? "admin" : "employee"))
: undefined;
const result = {
id: Number(user.id),
username: user.username,
avatarUrl: user.avatar_url || null,
bio: user.bio || null,
profileBackgroundUrl: user.profile_background_url || null,
email: user.email || null,
emailVerified: !!user.email_verified,
phone: user.phone || null,
authProvider: user.auth_provider || "password",
sessionId: user.current_session_id || null,
sessionStartedAt: user.current_session_started_at || null,
role: user.role,
maxConcurrency: Number(user.max_concurrency || 0),
enabled: !!user.enabled,
enterpriseId,
enterpriseName: user.enterprise_name || null,
isEnterpriseAdmin,
enterpriseRole,
enterpriseAdminUserId:
user.enterprise_admin_user_id == null ? null : Number(user.enterprise_admin_user_id),
accountType: enterpriseId ? "enterprise" : "personal",
balanceCents: user.user_balance_cents != null ? Number(user.user_balance_cents) : 0,
billingMode: user.billing_mode || "credits",
betaExpiresAt: user.beta_expires_at || null,
enterpriseCode: user.enterprise_code || null,
};
if (enterpriseId) {
result.enterpriseBalanceCents =
user.enterprise_balance_cents != null ? Number(user.enterprise_balance_cents) : 0;
const now = new Date().toISOString();
try {
const { rows: activePackages } = await pool.query(
`
SELECT ep.remaining_image, ep.remaining_video, ep.remaining_text, ep.expires_at, p.name AS package_name
FROM enterprise_packages ep
JOIN packages p ON ep.package_id = p.id
WHERE ep.enterprise_id = $1 AND ep.expires_at > $2
ORDER BY ep.expires_at ASC
`,
[enterpriseId, now],
);
result.activePackages = activePackages.map((pkg) => ({
name: pkg.package_name,
expiresAt: pkg.expires_at,
remainingImage: pkg.remaining_image,
remainingVideo: pkg.remaining_video,
remainingText: pkg.remaining_text,
}));
} catch {
result.activePackages = [];
}
}
return result;
}
async function getUserContextById(userId) {
const { rows } = await pool.query(`${USER_CONTEXT_SELECT} WHERE u.id = $1 LIMIT 1`, [userId]);
return toUserResponse(rows[0]);
}
function isSystemAdmin(user) {
return user?.role === "admin";
}
function isEnterpriseAdmin(user) {
return Boolean(user?.enterpriseId && user?.isEnterpriseAdmin);
}
function generateToken(user, sessionId = user.sessionId) {
return jwt.sign(
{
userId: user.id,
sessionId,
username: user.username,
role: user.role,
enterpriseId: user.enterpriseId,
isEnterpriseAdmin: user.isEnterpriseAdmin,
},
JWT_SECRET,
{ expiresIn: JWT_EXPIRES_IN },
);
}
function verifyToken(token) {
return jwt.verify(token, JWT_SECRET);
}
async function startUserSession(userId, userAgent) {
const sessionId = crypto.randomUUID();
await pool.query(
"INSERT INTO user_sessions (id, user_id, user_agent, created_at) VALUES ($1, $2, $3, NOW())",
[sessionId, userId, userAgent || null],
);
await pool.query(
`DELETE FROM user_sessions
WHERE user_id = $1
AND id NOT IN (
SELECT id FROM user_sessions
WHERE user_id = $1
ORDER BY created_at DESC
LIMIT $2
)`,
[userId, MAX_CONCURRENT_SESSIONS],
);
await pool.query(
"UPDATE users SET current_session_id = $1, current_session_started_at = NOW(), updated_at = NOW() WHERE id = $2",
[sessionId, userId],
);
return sessionId;
}
async function clearUserSession(userId, sessionId) {
if (!userId || !sessionId) return;
await pool.query(
"DELETE FROM user_sessions WHERE user_id = $1 AND id = $2",
[userId, sessionId],
);
await pool.query(
`UPDATE users
SET current_session_id = NULL,
current_session_started_at = NULL,
updated_at = NOW()
WHERE id = $1 AND current_session_id = $2`,
[userId, sessionId],
);
}
async function isSessionValid(userId, sessionId) {
if (!userId || !sessionId) return false;
const { rows } = await pool.query(
"SELECT 1 FROM user_sessions WHERE user_id = $1 AND id = $2 LIMIT 1",
[userId, sessionId],
);
if (rows.length > 0) return true;
const { rows: legacy } = await pool.query(
"SELECT 1 FROM users WHERE id = $1 AND current_session_id = $2 LIMIT 1",
[userId, sessionId],
);
if (legacy.length > 0) {
await pool.query(
"INSERT INTO user_sessions (id, user_id, created_at) VALUES ($1, $2, NOW()) ON CONFLICT (id) DO NOTHING",
[sessionId, userId],
);
return true;
}
return false;
}
async function requireAuth(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith("Bearer ")) {
return res.status(401).json({ error: "未登录" });
}
try {
const token = authHeader.slice(7);
const payload = verifyToken(token);
const user = await getUserContextById(payload.userId);
if (!user?.enabled) {
return res.status(403).json({ error: "账号已禁用" });
}
const sessionOk = await isSessionValid(payload.userId, payload.sessionId);
if (!sessionOk) {
return res.status(401).json({ error: "您已在别处登录", code: "SESSION_REPLACED" });
}
if (user.enterpriseId && user.enterpriseName == null) {
return res.status(403).json({ error: "企业信息不存在" });
}
req.user = user;
req.auth = { token, sessionId: payload.sessionId };
next();
} catch {
return res.status(401).json({ error: "登录已过期" });
}
}
function requireAdmin(req, res, next) {
if (!isSystemAdmin(req.user)) {
return res.status(403).json({ error: "需要系统管理员权限" });
}
next();
}
function requireEnterpriseAdmin(req, res, next) {
if (!isEnterpriseAdmin(req.user)) {
return res.status(403).json({ error: "需要企业管理员权限" });
}
next();
}
function requireManagementAccess(req, res, next) {
if (!isSystemAdmin(req.user) && !isEnterpriseAdmin(req.user)) {
return res.status(403).json({ error: "需要管理权限" });
}
next();
}
async function login(username, password, userAgent) {
const { rows } = await pool.query(
`${USER_CONTEXT_SELECT} WHERE u.username = $1 AND u.enabled = 1 LIMIT 1`,
[username],
);
const user = rows[0];
if (!user) return null;
const { rows: pwRows } = await pool.query("SELECT password_hash FROM users WHERE id = $1", [
user.id,
]);
if (!pwRows[0] || !(await bcrypt.compare(password, pwRows[0].password_hash))) return null;
const safeUser = await toUserResponse(user);
const sessionId = await startUserSession(safeUser.id, userAgent);
const userWithSession = {
...safeUser,
sessionId,
sessionStartedAt: new Date().toISOString(),
};
return {
token: generateToken(userWithSession, sessionId),
user: userWithSession,
};
}
module.exports = {
generateToken,
verifyToken,
startUserSession,
clearUserSession,
isSessionValid,
login,
getUserContextById,
isSystemAdmin,
isEnterpriseAdmin,
requireAuth,
requireAdmin,
requireEnterpriseAdmin,
requireManagementAccess,
generateUniqueEnterpriseCode,
};