// proxy.ts - API Proxy Route
import { cookies } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
const API_URL = process.env.BACKEND_API_URL;
const TOKEN_COOKIE = 'auth_token';
const REFRESH_COOKIE = 'refresh_token';
const GUEST_COOKIE = 'guest_token';
// Token validation cache to reduce backend calls
const tokenCache = new Map<string, { valid: boolean; exp: number }>();
const pendingValidations = new Map<string, Promise<boolean>>();
export async function proxy(request: NextRequest) {
const cookieStore = cookies();
let token = cookieStore.get(TOKEN_COOKIE)?.value;
// Check cache first
if (token && tokenCache.has(token)) {
const cached = tokenCache.get(token)!;
if (cached.exp > Date.now()) {
if (!cached.valid) {
token = await tryRefreshToken(cookieStore);
}
}
}
// Validate token with backend
if (token) {
const isValid = await validateTokenWithBackend(token);
if (!isValid) {
token = await tryRefreshToken(cookieStore);
}
}
// Fallback to guest token
if (!token) {
token = await getOrCreateGuestToken(cookieStore);
}
// Forward request to backend
const backendUrl = new URL(request.url);
backendUrl.host = new URL(API_URL).host;
const headers = new Headers(request.headers);
headers.set('Authorization', `Bearer ${token}`);
headers.set('Accept-Language', getLocaleFromRequest(request));
const response = await fetch(backendUrl, {
method: request.method,
headers,
body: request.body,
});
// Handle 401 - try refresh once
if (response.status === 401) {
const newToken = await tryRefreshToken(cookieStore);
if (newToken) {
headers.set('Authorization', `Bearer ${newToken}`);
return fetch(backendUrl, {
method: request.method,
headers,
body: request.body,
});
}
}
// Clone response and sanitize
const data = await response.json();
const sanitized = sanitizeResponse(data);
return NextResponse.json(sanitized, {
status: response.status,
headers: response.headers,
});
}
async function validateTokenWithBackend(token: string): Promise<boolean> {
// Deduplicate concurrent validation requests
if (pendingValidations.has(token)) {
return pendingValidations.get(token)!;
}
const promise = fetch(API_URL + '/auth/me', {
headers: { 'Authorization': `Bearer ${token}` },
}).then(res => res.ok);
pendingValidations.set(token, promise);
const result = await promise;
pendingValidations.delete(token);
// Cache result
tokenCache.set(token, { valid: result, exp: Date.now() + 2000 });
return result;
}
async function tryRefreshToken(cookieStore: any): Promise<string | null> {
const refreshToken = cookieStore.get(REFRESH_COOKIE)?.value;
if (!refreshToken) return null;
try {
const res = await fetch(API_URL + '/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: refreshToken }),
});
if (!res.ok) throw new Error('Refresh failed');
const { access_token, refresh_token } = await res.json();
cookieStore.set(TOKEN_COOKIE, access_token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
});
cookieStore.set(REFRESH_COOKIE, refresh_token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
});
return access_token;
} catch {
// Clear invalid tokens
cookieStore.delete(TOKEN_COOKIE);
cookieStore.delete(REFRESH_COOKIE);
return null;
}
}
async function getOrCreateGuestToken(cookieStore: any): Promise<string> {
const existing = cookieStore.get(GUEST_COOKIE)?.value;
if (existing) {
const isValid = await validateTokenWithBackend(existing);
if (isValid) return existing;
}
const res = await fetch(API_URL + '/auth/guest', { method: 'POST' });
const { token } = await res.json();
cookieStore.set(GUEST_COOKIE, token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
});
return token;
}
// sanitize.ts - XSS Protection
import sanitizeHtml from 'sanitize-html';
function sanitizeResponse<T>(data: T): T {
if (typeof data === 'string') {
return sanitizeHtml(data) as T;
}
if (Array.isArray(data)) {
return data.map(sanitizeResponse) as T;
}
if (typeof data === 'object' && data !== null) {
return Object.fromEntries(
Object.entries(data).map(([k, v]) => [k, sanitizeResponse(v)])
) as T;
}
return data;
}
// And you STILL need:
// - Rate limit handling
// - Request timeout
// - Error boundaries
// - Loading states
// - i18n header injection
// - PUT/PATCH method spoofing
// - Response caching
// - Retry with backoff...