// 认证:scrypt 口令散列 + Bearer token + 设备登记 import crypto from 'node:crypto'; import { getDb } from './db.mjs'; export function register({ username, password, device_name, invite_code, seed }) { const d = getDb(); // 多用户管控:管理员颁发邀请码;env INVITE_CODE 作为静态兜底 const maxUsers = seed ? 0 : Number(process.env.MAX_USERS || 0); // seed 播种不受名额限制 if (maxUsers > 0) { const cnt = d.prepare('SELECT COUNT(*) AS c FROM users').get().c; if (cnt >= maxUsers) throw new Error('注册名额已满,请联系管理员'); } const userCount = d.prepare('SELECT COUNT(*) AS c FROM users').get().c; let isAdmin = 0; if (userCount === 0) { isAdmin = 1; // 首位注册者自动成为管理员 } else if (seed) { // 内置演示账号播种:不要求邀请码 } else { const code = String(invite_code || '').trim().toUpperCase(); const envCode = String(process.env.INVITE_CODE || '').trim().toUpperCase(); const row = code ? d.prepare('SELECT id, valid FROM invite_codes WHERE code=?').get(code) : null; if (row && row.valid) { var __usedInviteId = row.id; // eslint-disable-line no-var } else if (code && envCode && code === envCode) { // 静态兜底码(env INVITE_CODE),与动态邀请码等价 } else { throw new Error(code ? '邀请码无效或已使用' : '需要邀请码,请联系管理员'); } } if (!/^[a-zA-Z0-9_-]{3,32}$/.test(username || '')) throw new Error('用户名需为 3-32 位字母数字-_'); if (!password || password.length < 8) throw new Error('密码至少 8 位'); const exists = d.prepare('SELECT 1 FROM users WHERE username=?').get(username); if (exists) throw new Error('用户名已存在'); const salt = crypto.randomBytes(16).toString('hex'); const hash = crypto.scryptSync(password, salt, 64).toString('hex'); const user_id = crypto.randomUUID(); d.prepare('INSERT INTO users (id, username, password_hash, salt, is_admin) VALUES (?,?,?,?,?)') .run(user_id, username, hash, salt, isAdmin); if (typeof __usedInviteId === 'number') { d.prepare("UPDATE invite_codes SET used_by=?, used_at=datetime('now','localtime'), valid=0 WHERE id=?") .run(user_id, __usedInviteId); } return issueToken(user_id, device_name); } export function login({ username, password, device_name }) { const d = getDb(); const row = d.prepare('SELECT * FROM users WHERE username=?').get(username || ''); if (!row) throw new Error('用户名或密码错误'); const hash = crypto.scryptSync(password || '', row.salt, 64); const expect = Buffer.from(row.password_hash, 'hex'); if (hash.length !== expect.length || !crypto.timingSafeEqual(hash, expect)) { throw new Error('用户名或密码错误'); } return issueToken(row.id, device_name); } function issueToken(user_id, device_name) { const d = getDb(); const name = (device_name || '未知设备').slice(0, 64); let device = d.prepare('SELECT id FROM devices WHERE user_id=? AND name=?').get(user_id, name); let device_id; if (device) { device_id = device.id; d.prepare("UPDATE devices SET last_seen_at=datetime('now','localtime') WHERE id=?").run(device_id); } else { device_id = crypto.randomUUID(); d.prepare('INSERT INTO devices (id, user_id, name) VALUES (?,?,?)').run(device_id, user_id, name); } const token = crypto.randomBytes(32).toString('hex'); const expires = new Date(Date.now() + 365 * 86400_000).toISOString().slice(0, 19).replace('T', ' '); d.prepare('INSERT INTO tokens (token, user_id, device_id, expires_at) VALUES (?,?,?,?)') .run(token, user_id, device_id, expires); return { token, user_id, device_id, device_name: name, expires_at: expires }; } export function isUserAdmin(user_id) { const d = getDb(); const row = d.prepare('SELECT is_admin FROM users WHERE id=?').get(user_id); return !!row && !!row.is_admin; } export function authUser(req) { const d = getDb(); const header = req.headers['authorization'] || ''; const token = header.startsWith('Bearer ') ? header.slice(7) : ''; if (!token) throw Object.assign(new Error('未提供凭据'), { code: 401 }); const row = d.prepare(` SELECT t.user_id, t.device_id, t.expires_at FROM tokens t WHERE t.token=?`).get(token); if (!row) throw Object.assign(new Error('凭据无效'), { code: 401 }); if (row.expires_at < new Date().toISOString().slice(0, 19).replace('T', ' ')) { throw Object.assign(new Error('凭据已过期,请重新登录'), { code: 401 }); } d.prepare("UPDATE devices SET last_seen_at=datetime('now','localtime') WHERE id=?").run(row.device_id); return { user_id: row.user_id, device_id: row.device_id }; }