/** * 登录流程:防爆破前置检查 → hasher 验密 → 失败计数落库 / 成功签发会话。 * * 防爆破计数走注入 repo(不靠进程内 Map,多实例下仍生效);判定纯逻辑在 bruteforce.js。 * 失败一律 INVALID_CREDENTIALS(不区分「用户不存在」与「密码错」以免账号枚举), * 超阈值返 TOO_MANY_ATTEMPTS + retryAfterMs。 */ import { AuthError } from './errors.js' import { issueSession } from './session.js' import { attemptKey, evaluateAttempts, DEFAULT_MAX_ATTEMPTS, DEFAULT_WINDOW_MS } from './bruteforce.js' /** * @param {Object} deps * @param {import('./index.js').UserRepo} deps.userRepo * @param {import('./index.js').TokenSigner} deps.tokenSigner * @param {import('./index.js').Hasher} deps.hasher * @param {number} [deps.maxAttempts] * @param {number} [deps.windowMs] * @param {number} [deps.accessTtlMs] * @param {number} [deps.refreshTtlMs] * @returns {(args: { email: string, password: string, ip?: string, device_id?: string }) => Promise} */ export function makeLogin(deps) { const { userRepo, hasher, maxAttempts = DEFAULT_MAX_ATTEMPTS, windowMs = DEFAULT_WINDOW_MS, } = deps return async function login({ email, password, ip }) { const normalized = String(email ?? '').trim().toLowerCase() const key = attemptKey(normalized, ip) const recentCount = await userRepo.countRecentAttempts({ key, ip, windowMs }) const gate = evaluateAttempts({ recentCount, maxAttempts, windowMs }) if (gate.locked) { throw new AuthError({ code: 'TOO_MANY_ATTEMPTS', retryAfterMs: gate.retryAfterMs }) } const user = await userRepo.findByEmail(normalized) const ok = user && user.password_hash ? await hasher.verify(password, user.password_hash) : false if (!ok || !user) { await userRepo.recordAttempt({ key, ip, at: new Date() }) throw new AuthError({ code: 'INVALID_CREDENTIALS' }) } const issued = await issueSession(deps, { user_id: user.id }) return { user, access_token: issued.access_token, refresh_token: issued.refresh_token } } }