spb/note-spboucher-ai
Public
1import { and, eq, gt, count, sql as dsql } from "drizzle-orm";2import { db, schema } from "@/db";34/** Login limiter: max 5 failures per 15 min per IP, and per username; lockout grows with repeated failures. */5export async function loginAllowed(ip: string, username: string): Promise<{ ok: true } | { ok: false; retryAfterSec: number }> {6 const since = new Date(Date.now() - 15 * 60_000);7 const [byIp] = await db.select({ n: count() }).from(schema.loginAttempts).where(and(eq(schema.loginAttempts.ip, ip), eq(schema.loginAttempts.success, false), gt(schema.loginAttempts.createdAt, since)));8 const [byUser] = await db.select({ n: count() }).from(schema.loginAttempts).where(and(eq(schema.loginAttempts.username, username), eq(schema.loginAttempts.success, false), gt(schema.loginAttempts.createdAt, since)));9 const failures = Math.max(byIp.n, byUser.n);10 if (failures < 5) return { ok: true };11 const lockMin = Math.min(60, 2 ** (failures - 4)); // 2,4,8,...,60 minutes12 return { ok: false, retryAfterSec: lockMin * 60 };13}14export async function recordAttempt(ip: string, username: string | null, success: boolean) {15 await db.insert(schema.loginAttempts).values({ ip, username, success });16 if (Math.random() < 0.05) await db.delete(schema.loginAttempts).where(dsql`created_at < now() - interval '2 days'`);17}1819/** Generic in-memory limiter for API routes (per key, sliding window). */20const buckets = new Map<string, number[]>();21export function rateLimit(key: string, max: number, windowMs: number) {22 const now = Date.now(); const arr = (buckets.get(key) ?? []).filter((t) => now - t < windowMs);23 if (arr.length >= max) { buckets.set(key, arr); return false; }24 arr.push(now); buckets.set(key, arr); return true;25}26