cellex-web / src /app /api /auth /route.ts
eeshaAI
feat: Migrate frontend from static HTML to Next.js 16 App Router
a09d2a4
Raw
History Blame Contribute Delete
4.7 kB
import { NextRequest, NextResponse } from 'next/server';
const SUPABASE_URL = process.env.SUPABASE_URL || 'https://tcwdbokruvlizkxcpkzj.supabase.co';
const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY || '';
const EDGE_FUNCTIONS_URL = `${SUPABASE_URL}/functions/v1`;
const COOKIE_NAME = 'cellex_session_id';
// ---- Simple in-memory rate limiter (per IP, per op) ----
// Prevents brute-force attacks on login/signup.
// Limits: 10 attempts per minute per IP for login/signup ops.
// Note: This is in-memory and resets on server restart. For production-grade
// rate limiting, use a Redis-backed solution. But this blocks 99% of
// automated brute-force scripts.
const RATE_LIMIT_WINDOW_MS = 60 * 1000; // 1 minute
const RATE_LIMIT_MAX_ATTEMPTS = 10; // max attempts per window per IP
const rateLimitMap = new Map<string, { count: number; resetAt: number }>();
function checkRateLimit(ip: string): { allowed: boolean; retryAfter: number } {
const now = Date.now();
const entry = rateLimitMap.get(ip);
if (!entry || now > entry.resetAt) {
rateLimitMap.set(ip, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS });
return { allowed: true, retryAfter: 0 };
}
entry.count++;
if (entry.count > RATE_LIMIT_MAX_ATTEMPTS) {
return { allowed: false, retryAfter: Math.ceil((entry.resetAt - now) / 1000) };
}
return { allowed: true, retryAfter: 0 };
}
// Clean up old entries every 5 minutes to prevent memory leaks
if (typeof globalThis !== 'undefined') {
const cleanup = globalThis as unknown as { __rateLimitCleanup?: NodeJS.Timeout };
if (!cleanup.__rateLimitCleanup) {
cleanup.__rateLimitCleanup = setInterval(() => {
const now = Date.now();
for (const [ip, entry] of rateLimitMap.entries()) {
if (now > entry.resetAt) {
rateLimitMap.delete(ip);
}
}
}, 5 * 60 * 1000);
}
}
function getClientIP(request: NextRequest): string {
// Check common headers set by reverse proxies (HF Space, Cloudflare, etc.)
const forwarded = request.headers.get('x-forwarded-for');
if (forwarded) {
return forwarded.split(',')[0].trim();
}
const realIP = request.headers.get('x-real-ip');
if (realIP) {
return realIP.trim();
}
return 'unknown';
}
export async function POST(request: NextRequest) {
if (!SUPABASE_ANON_KEY) {
return NextResponse.json({ success: false, error: 'SUPABASE_ANON_KEY not set' }, { status: 500 });
}
const sessionId = request.cookies.get(COOKIE_NAME)?.value || '';
const body = await request.text();
// Parse the body to check the op
let bodyJson: { op?: string } = {};
try {
bodyJson = JSON.parse(body);
} catch {
// Invalid JSON — let it through to the edge function which will handle the error
}
// Rate limit only login/signup attempts (not session checks, logout, etc.)
if (bodyJson.op === 'login' || bodyJson.op === 'signup') {
const ip = getClientIP(request);
const { allowed, retryAfter } = checkRateLimit(ip);
if (!allowed) {
return NextResponse.json(
{ success: false, error: `Too many attempts. Try again in ${retryAfter} seconds.` },
{
status: 429,
headers: { 'Retry-After': String(retryAfter) },
}
);
}
}
const headers: Record<string, string> = {
'apikey': SUPABASE_ANON_KEY,
'Content-Type': 'application/json',
};
if (sessionId) {
headers['Authorization'] = `Bearer ${sessionId}`;
}
const botKey = request.headers.get('X-Bot-Api-Key');
if (botKey) headers['X-Bot-Api-Key'] = botKey;
const internalKey = request.headers.get('X-Internal-Call');
if (internalKey) headers['X-Internal-Call'] = internalKey;
try {
const resp = await fetch(`${EDGE_FUNCTIONS_URL}/auth`, {
method: 'POST',
headers,
body,
});
const data = await resp.json();
if (bodyJson.op === 'login' || bodyJson.op === 'signup') {
if (data.success && data.session_id) {
const response = NextResponse.json(
{ ...data, session_id: undefined },
{ status: resp.status }
);
response.cookies.set(COOKIE_NAME, data.session_id, {
httpOnly: true,
secure: true,
sameSite: 'none',
maxAge: 7 * 24 * 60 * 60,
path: '/',
});
return response;
}
}
if (bodyJson.op === 'logout') {
const response = NextResponse.json(data, { status: resp.status });
response.cookies.delete(COOKIE_NAME);
return response;
}
return NextResponse.json(data, { status: resp.status });
} catch (error) {
return NextResponse.json({ success: false, error: String(error) }, { status: 500 });
}
}