Files
GibSend/apps/web/src/app/api/auth/[...nextauth]/route.ts
2025-09-09 23:08:59 +10:00

86 lines
2.3 KiB
TypeScript

import NextAuth from "next-auth";
import { authOptions } from "~/server/auth";
import { env } from "~/env";
import { getRedis } from "~/server/redis";
import { logger } from "~/server/logger/log";
const handler = NextAuth(authOptions);
export { handler as GET };
function getClientIp(req: Request): string | null {
const h = req.headers;
const direct =
h.get("x-forwarded-for") ??
h.get("x-real-ip") ??
h.get("cf-connecting-ip") ??
h.get("x-client-ip") ??
h.get("true-client-ip") ??
h.get("fastly-client-ip") ??
h.get("x-cluster-client-ip") ??
null;
let ip = direct?.split(",")[0]?.trim() ?? "";
if (!ip) {
const fwd = h.get("forwarded");
if (fwd) {
const first = fwd.split(",")[0];
const match = first?.match(/for=([^;]+)/i);
if (match && match[1]) {
const raw = match[1].trim().replace(/^"|"$/g, "");
if (raw.startsWith("[")) {
const end = raw.indexOf("]");
ip = end !== -1 ? raw.slice(1, end) : raw;
} else {
const parts = raw.split(":");
if (parts.length > 0 && parts[0]) {
ip =
parts.length === 2 && /^\d+(?:\.\d+){3}$/.test(parts[0])
? parts[0]
: raw;
}
}
}
}
}
return ip || null;
}
export async function POST(req: Request, ctx: any) {
if (env.AUTH_EMAIL_RATE_LIMIT > 0) {
const url = new URL(req.url);
if (url.pathname.endsWith("/signin/email")) {
try {
const ip = getClientIp(req);
if (!ip) {
logger.warn("Auth email rate limit skipped: missing client IP");
return handler(req, ctx);
}
const redis = getRedis();
const key = `auth-rl:${ip}`;
const ttl = 60;
const count = await redis.incr(key);
if (count === 1) await redis.expire(key, ttl);
if (count > env.AUTH_EMAIL_RATE_LIMIT) {
logger.warn({ ip }, "Auth email rate limit exceeded");
return Response.json(
{
error: {
code: "RATE_LIMITED",
message: "Too many requests",
},
},
{ status: 429 }
);
}
} catch (error) {
logger.error({ err: error }, "Auth email rate limit failed");
}
}
}
return handler(req, ctx);
}