SPB Git
1.7 KB · 26 lines typescript
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
Files · main
Menu

Theme