login.js
2.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* 登录流程:防爆破前置检查 → 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<import('./index.js').AuthTokens>}
*/
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 }
}
}