import crypto from 'node:crypto'; const SESSION_TTL_MS = 1000 * 60 * 60 * 24 * 30; function now() { return new Date(); } function hashPassword(password, salt = crypto.randomBytes(16).toString('hex')) { const digest = crypto.scryptSync(String(password), salt, 64).toString('hex'); return `scrypt$${salt}$${digest}`; } function verifyPassword(password, stored) { const [scheme, salt, digest] = String(stored ?? '').split('$'); if (scheme !== 'scrypt' || !salt || !digest) return false; const next = crypto.scryptSync(String(password), salt, 64).toString('hex'); return crypto.timingSafeEqual(Buffer.from(next, 'hex'), Buffer.from(digest, 'hex')); } function rowToUser(row) { if (!row) return null; return { id: String(row.id), username: row.username, displayName: row.display_name ?? row.username, role: row.role, status: row.status, balanceCents: Number(row.balance_cents ?? 0), workspaceRoot: row.workspace_root ?? '', createdAt: row.created_at ? new Date(row.created_at).getTime() : Date.now(), updatedAt: row.updated_at ? new Date(row.updated_at).getTime() : Date.now(), }; } function defaultCapabilities() { return {}; } export function parseCookies(cookieHeader = '') { return Object.fromEntries( cookieHeader.split(';').map((part) => { const index = part.indexOf('='); if (index < 0) return ['', '']; return [decodeURIComponent(part.slice(0, index).trim()), decodeURIComponent(part.slice(index + 1).trim())]; }).filter(([k]) => k), ); } export const USER_COOKIE = 'tkmind_admin_token'; export function userLoginCookies(token, secure, domain) { const parts = [ `${USER_COOKIE}=${encodeURIComponent(token)}`, 'Path=/', 'HttpOnly', 'SameSite=Lax', secure ? 'Secure' : null, domain ? `Domain=${domain}` : null, `Max-Age=${SESSION_TTL_MS / 1000}`, ].filter(Boolean); return `${parts.join('; ')}`; } export function clearUserSessionCookie(secure, domain) { const parts = [ `${USER_COOKIE}=`, 'Path=/', 'HttpOnly', 'SameSite=Lax', secure ? 'Secure' : null, domain ? `Domain=${domain}` : null, 'Max-Age=0', ].filter(Boolean); return `${parts.join('; ')}`; } export function resolveCookieDomainForRequest(_req) { return ''; } export function createLocalUserAuth(pool) { const capabilityCatalog = []; const policyCatalog = []; const skillCatalog = []; async function ensureTables() { await pool.execute(` CREATE TABLE IF NOT EXISTS auth_users ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, username VARCHAR(191) NOT NULL UNIQUE, display_name VARCHAR(191) NULL, role VARCHAR(32) NOT NULL DEFAULT 'user', status VARCHAR(32) NOT NULL DEFAULT 'active', password_hash VARCHAR(255) NOT NULL, balance_cents BIGINT NOT NULL DEFAULT 0, workspace_root VARCHAR(255) NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) `); await pool.execute(` CREATE TABLE IF NOT EXISTS auth_sessions ( token VARCHAR(191) NOT NULL PRIMARY KEY, user_id BIGINT UNSIGNED NOT NULL, expires_at TIMESTAMP NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX idx_auth_sessions_user_id (user_id), INDEX idx_auth_sessions_expires_at (expires_at) ) `); } async function ensureAdminUser() { await ensureTables(); const [rows] = await pool.execute('SELECT id FROM auth_users WHERE username = ? LIMIT 1', ['admin']); if (rows.length) return; throw new Error('admin 账号未初始化,请先运行 npm run admin:init'); } async function getUserByUsername(username) { const [rows] = await pool.execute('SELECT * FROM auth_users WHERE username = ? LIMIT 1', [username]); return rowToUser(rows[0]); } async function getUserById(id) { const [rows] = await pool.execute('SELECT * FROM auth_users WHERE id = ? LIMIT 1', [id]); return rowToUser(rows[0]); } async function getMe(token) { if (!token) return null; const [rows] = await pool.execute( 'SELECT u.* FROM auth_sessions s JOIN auth_users u ON u.id = s.user_id WHERE s.token = ? AND s.expires_at > NOW() LIMIT 1', [token], ); return rowToUser(rows[0]); } async function revoke(token) { if (!token) return; await pool.execute('DELETE FROM auth_sessions WHERE token = ?', [token]); } async function login({ username, password }) { await ensureTables(); const [rows] = await pool.execute('SELECT * FROM auth_users WHERE username = ? LIMIT 1', [username]); const row = rows[0]; if (!row) return { ok: false, message: '账号或密码错误', retryAfterMs: 0 }; if (!verifyPassword(password, row.password_hash)) return { ok: false, message: '账号或密码错误', retryAfterMs: 0 }; const token = crypto.randomUUID().replace(/-/g, ''); const expiresAt = new Date(Date.now() + SESSION_TTL_MS); await pool.execute('INSERT INTO auth_sessions (token, user_id, expires_at) VALUES (?, ?, ?)', [token, row.id, expiresAt]); return { ok: true, token, user: rowToUser(row) }; } async function listUsers({ page = 1, pageSize = 20, search = '', role = '', status = '' }) { await ensureTables(); const clauses = []; const params = []; if (search) { clauses.push('(username LIKE ? OR display_name LIKE ?)'); params.push(`%${search}%`, `%${search}%`); } if (role) { clauses.push('role = ?'); params.push(role); } if (status) { clauses.push('status = ?'); params.push(status); } const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : ''; const [[countRow]] = await pool.execute(`SELECT COUNT(*) AS total FROM auth_users ${where}`, params); const safePage = Math.max(Number(page) || 1, 1); const safePageSize = Math.max(Number(pageSize) || 20, 1); const offset = (safePage - 1) * safePageSize; const [rows] = await pool.execute( `SELECT * FROM auth_users ${where} ORDER BY id DESC LIMIT ${safePageSize} OFFSET ${offset}`, params, ); return { items: rows.map((row) => rowToUser(row)), total: Number(countRow.total ?? 0), page: safePage, pageSize: safePageSize, totalPages: Math.max(1, Math.ceil(Number(countRow.total ?? 0) / safePageSize)), }; } async function createUser(payload) { if (!payload?.username || !payload?.password) return { ok: false, message: '用户名和密码不能为空' }; const [exists] = await pool.execute('SELECT id FROM auth_users WHERE username = ? LIMIT 1', [payload.username]); if (exists.length) return { ok: false, message: '用户名已存在' }; await pool.execute( 'INSERT INTO auth_users (username, display_name, role, status, password_hash, balance_cents, workspace_root) VALUES (?, ?, ?, ?, ?, ?, ?)', [ payload.username, payload.displayName || payload.username, payload.role === 'admin' ? 'admin' : 'user', 'active', hashPassword(payload.password), Number(payload.balanceCents ?? 0), payload.workspaceRoot ?? null, ], ); return { ok: true, user: await getUserByUsername(payload.username) }; } async function updateUser(userId, payload) { const user = await getUserById(userId); if (!user) return { ok: false, message: '用户不存在' }; const displayName = payload.displayName ?? user.displayName; const workspaceRoot = payload.workspaceRoot ?? user.workspaceRoot; const status = payload.status ?? user.status; const role = payload.role === 'admin' ? 'admin' : user.role; const balanceCents = payload.balanceCents ?? user.balanceCents; await pool.execute( 'UPDATE auth_users SET display_name = ?, workspace_root = ?, status = ?, role = ?, balance_cents = ? WHERE id = ?', [displayName, workspaceRoot || null, status, role, balanceCents, userId], ); return { ok: true, user: await getUserById(userId) }; } async function recharge(userId, amountCents) { const user = await getUserById(userId); if (!user) return { ok: false, message: '用户不存在' }; await pool.execute('UPDATE auth_users SET balance_cents = balance_cents + ? WHERE id = ?', [Number(amountCents ?? 0), userId]); return { ok: true, user: await getUserById(userId) }; } async function getAdminSummary() { const [[row]] = await pool.execute( `SELECT COUNT(*) AS total, SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) AS active, SUM(CASE WHEN balance_cents < 0 THEN 1 ELSE 0 END) AS lowBalance, SUM(balance_cents) AS totalBalanceCents FROM auth_users`, ); return { users: { total: Number(row.total ?? 0), active: Number(row.active ?? 0), lowBalance: Number(row.lowBalance ?? 0), totalBalanceCents: Number(row.totalBalanceCents ?? 0), }, usage24h: { count: 0, costCents: 0 }, lowBalanceUsers: [], recentUsage: [], recentLedger: [], llm: { keyCount: 0, selectedKeyName: null, globalModel: null }, }; } return { hashPassword, capabilityCatalog, policyCatalog, skillCatalog, ensureAdminUser, login, getMe, revoke, listUsers, createUser, updateUser, recharge, getAdminSummary, getRoleCapabilities: async () => ({ ok: true, role: 'user', capabilities: defaultCapabilities() }), setRoleCapabilities: async () => ({ ok: true, role: 'user', capabilities: defaultCapabilities() }), getUserCapabilities: async () => ({ ok: true, userId: null, capabilities: defaultCapabilities() }), setUserCapabilities: async () => ({ ok: true }), clearUserCapabilityOverrides: async () => ({ ok: true }), getRolePolicies: async () => ({ ok: true, role: 'user', policies: {} }), setRolePolicies: async () => ({ ok: true, role: 'user', policies: {} }), getUserPolicies: async () => ({ ok: true, userId: null, policies: {} }), setUserPolicies: async () => ({ ok: true }), clearUserPolicyOverrides: async () => ({ ok: true }), getRoleSkills: async () => ({ ok: true, role: 'user', skills: {} }), setRoleSkills: async () => ({ ok: true, role: 'user', skills: {} }), getUserSkills: async () => ({ ok: true, userId: null, skills: {} }), setUserSkills: async () => ({ ok: true }), clearUserSkillOverrides: async () => ({ ok: true }), }; }