import { IP_BAN_TTL_SECONDS, IP_RATE_LIMIT, IP_RATE_WINDOW_SECONDS, } from "@/lib/constants/cache"; import { withBoundedRedis } from "@/lib/redis"; import { writeAlert } from "@/modules/alert/service"; export type IpRateLimitResult = | { allowed: true } | { allowed: false; errorCode: "RATE_LIMITED"; banned: boolean }; function ipRateKey(ip: string): string { return `ratelimit:ip:${ip}`; } function ipBanKey(ip: string): string { return `ban:ip:${ip}`; } /** IP 级限流:1000 次/分钟,超限封禁 10 分钟 */ export async function checkIpRateLimit( ip: string, ): Promise { const result = await withBoundedRedis(async (redis) => { const banned = await redis.exists(ipBanKey(ip)); if (banned === 1) { return { allowed: false as const, errorCode: "RATE_LIMITED" as const, banned: true }; } const key = ipRateKey(ip); const count = await redis.incr(key); if (count === 1) { await redis.expire(key, IP_RATE_WINDOW_SECONDS); } if (count > IP_RATE_LIMIT) { await redis.set(ipBanKey(ip), "1", "EX", IP_BAN_TTL_SECONDS); void writeAlert("SECURITY", { detail: { reason: "ip_rate_limit_ban", ip, count, window_seconds: IP_RATE_WINDOW_SECONDS, }, }); return { allowed: false as const, errorCode: "RATE_LIMITED" as const, banned: true }; } return { allowed: true as const }; }); if (!result) { return { allowed: true }; } return result; }