76 lines
2.4 KiB
TypeScript
76 lines
2.4 KiB
TypeScript
import { NextResponse } from 'next/server';
|
|
import type { NextRequest } from 'next/server';
|
|
|
|
const LOCALES = ['id', 'en'];
|
|
|
|
// =====================================================================
|
|
// PANOPTICON LIVE KILL ENFORCEMENT (In-Memory LRU Cache)
|
|
// Pengguna yang sudah di-kill akan langsung ditolak di middleware layer
|
|
// tanpa menyentuh database (zero-latency 403).
|
|
// =====================================================================
|
|
const killCache = new Set<string>();
|
|
let lastKillSync = 0;
|
|
const KILL_SYNC_INTERVAL_MS = 10_000; // Sinkronisasi tiap 10 detik
|
|
|
|
async function syncKillList() {
|
|
const now = Date.now();
|
|
if (now - lastKillSync < KILL_SYNC_INTERVAL_MS) return;
|
|
lastKillSync = now;
|
|
|
|
try {
|
|
// Fetch kill list from internal API (server-side only)
|
|
const baseUrl = `http://127.0.0.1:${process.env.PORT || 3005}`;
|
|
const resp = await fetch(`${baseUrl}/api/telemetry/kill-list`, {
|
|
headers: { 'x-internal-secret': process.env.JWT_SECRET || '' },
|
|
signal: AbortSignal.timeout(3000),
|
|
});
|
|
if (resp.ok) {
|
|
const data = await resp.json();
|
|
killCache.clear();
|
|
if (Array.isArray(data.targets)) {
|
|
data.targets.forEach((id: string) => killCache.add(id));
|
|
}
|
|
}
|
|
} catch (_e) {
|
|
// Gagal sinkronisasi = tetap gunakan cache lama. Zero-Downtime.
|
|
}
|
|
}
|
|
|
|
function extractUserFromJWT(token: string): string | null {
|
|
try {
|
|
// Decode payload tanpa verifikasi (hanya untuk baca email di middleware)
|
|
const parts = token.split('.');
|
|
if (parts.length !== 3) return null;
|
|
const payload = JSON.parse(atob(parts[1]));
|
|
return payload.email || payload.userId || null;
|
|
} catch (_e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function middleware(request: NextRequest) {
|
|
const pathname = request.nextUrl.pathname;
|
|
|
|
// ---- PANOPTICON KILL CHECK ----
|
|
await syncKillList();
|
|
const token = request.cookies.get('jumpa_token')?.value;
|
|
if (token && killCache.size > 0) {
|
|
const userIdentity = extractUserFromJWT(token);
|
|
if (userIdentity && killCache.has(userIdentity)) {
|
|
return new NextResponse(
|
|
JSON.stringify({ error: 'Akses Dihanguskan oleh Otoritas Puncak. Sesi Anda telah dimusnahkan.' }),
|
|
{ status: 403, headers: { 'Content-Type': 'application/json' } }
|
|
);
|
|
}
|
|
}
|
|
|
|
// ---- NO MORE LOCALE REWRITE ----
|
|
return NextResponse.next();
|
|
}
|
|
|
|
export default middleware;
|
|
|
|
export const config = {
|
|
matcher: ['/((?!api|_next|_vercel|.*\\..*).*)', '/']
|
|
};
|