feat(server): 管理面与成员视图 API + Scalar 在线文档
契约 19 路径全量:引导/会话/改密、engine 桩、keys、overview/logs、payments、 qpu、members、audit、成员视图。鉴权优先、422 信封、登录节流、上游验证接线。
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 登录节流(契约:429,最小实现)
|
||||
* 同一用户名失败计数 + 固定退避;进程内存即可(单机母体)。
|
||||
*/
|
||||
|
||||
const WINDOW_MS = 5 * 60_000;
|
||||
const MAX_FAILURES = 5;
|
||||
const LOCK_MS = 60_000;
|
||||
|
||||
type Entry = { count: number; firstAt: number; lockedUntil: number };
|
||||
|
||||
const buckets = new Map<string, Entry>();
|
||||
|
||||
export function isLoginThrottled(username: string): boolean {
|
||||
const e = buckets.get(username);
|
||||
if (!e) return false;
|
||||
if (e.lockedUntil > Date.now()) return true;
|
||||
if (Date.now() - e.firstAt > WINDOW_MS) buckets.delete(username);
|
||||
return false;
|
||||
}
|
||||
|
||||
export function recordLoginFailure(username: string): void {
|
||||
const now = Date.now();
|
||||
const e = buckets.get(username);
|
||||
if (!e || now - e.firstAt > WINDOW_MS) {
|
||||
buckets.set(username, { count: 1, firstAt: now, lockedUntil: 0 });
|
||||
return;
|
||||
}
|
||||
e.count += 1;
|
||||
if (e.count >= MAX_FAILURES) {
|
||||
e.lockedUntil = now + LOCK_MS;
|
||||
e.count = 0;
|
||||
e.firstAt = now;
|
||||
}
|
||||
}
|
||||
|
||||
export function recordLoginSuccess(username: string): void {
|
||||
buckets.delete(username);
|
||||
}
|
||||
|
||||
/** 测试用 */
|
||||
export function resetLoginThrottle(): void {
|
||||
buckets.clear();
|
||||
}
|
||||
Reference in New Issue
Block a user