feat: complete deployment codebase for BackOne DPI dashboard with dynamic telemetry, rebranding, database size tracking, and modular design
This commit is contained in:
1 parent
6cf3c6c7e5
commit
c316f3171b
226 files changed
+21152
-8397
No files matched your search
@@ -0,0 +1,117 @@
|
||||
// test/architecture_test.js
|
||||
// TDD: Verifikasi arsitektur 2 container BackOne DPI
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
|
||||
function readFile(relPath) {
|
||||
return fs.readFileSync(path.join(ROOT, relPath), 'utf8');
|
||||
}
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function test(name, condition, detail = '') {
|
||||
if (condition) {
|
||||
console.log(` ✓ PASS: ${name}`);
|
||||
passed++;
|
||||
} else {
|
||||
console.log(` ✗ FAIL: ${name}${detail ? ' — ' + detail : ''}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n══════════════════════════════════════════════════════');
|
||||
console.log(' BackOne DPI — TDD Architecture Verification');
|
||||
console.log('══════════════════════════════════════════════════════\n');
|
||||
|
||||
// ─── TEST GROUP 1: Backend bersih dari SQLite/Netify API ───────────────────
|
||||
console.log('[GROUP 1] Backend — Isolasi dari SQLite & Netify API');
|
||||
const serverCode = readFile('backend/server.js');
|
||||
const dashCode = readFile('backend/routes/dashboard.js') +
|
||||
readFile('backend/routes/dashboard/helpers.js') +
|
||||
readFile('backend/routes/dashboard/summary.js') +
|
||||
readFile('backend/routes/dashboard/apps.js') +
|
||||
readFile('backend/routes/dashboard/devices.js') +
|
||||
readFile('backend/routes/dashboard/flows.js') +
|
||||
readFile('backend/routes/dashboard/threats.js') +
|
||||
readFile('backend/routes/dashboard/geo.js') +
|
||||
readFile('backend/routes/dashboard/tls.js') +
|
||||
readFile('backend/routes/dashboard/telemetry.js') +
|
||||
readFile('backend/routes/dashboard/events.js');
|
||||
const authCode = readFile('backend/routes/auth.js');
|
||||
|
||||
test('server.js tidak memanggil ingestionService', !serverCode.includes('ingestionService'));
|
||||
test('server.js tidak import database.js (SQLite)', !serverCode.includes("require('./database')") && !serverCode.includes("require('../database')"));
|
||||
test('server.js tidak memanggil startScheduler', !serverCode.includes('startScheduler'));
|
||||
test('dashboard.js tidak import database.js', !dashCode.includes("require('../database')"));
|
||||
test('dashboard.js tidak import scheduler.js lama', !dashCode.includes("require('../scheduler')"));
|
||||
test('dashboard.js tidak memanggil netify.ai API', !dashCode.includes('netify.ai') && !dashCode.includes('fetchTopApps'));
|
||||
test('auth.js tidak import database.js', !authCode.includes("require('../database')"));
|
||||
test('auth.js tidak menggunakan better-sqlite3', !authCode.includes('better-sqlite3') && !authCode.includes('getDB()'));
|
||||
console.log('');
|
||||
|
||||
// ─── TEST GROUP 2: Multi-tenant isolation ──────────────────────────────────
|
||||
console.log('[GROUP 2] Dashboard — Multi-Tenant Agent UUID Isolation');
|
||||
test('dashboard.js punya getBaseFilter() helper', dashCode.includes('function getBaseFilter'));
|
||||
test('getBaseFilter menambahkan agent_uuid filter', dashCode.includes('filter.agent_uuid = req.user.agent_uuid'));
|
||||
test('getBaseFilter menambahkan site_uuid filter', dashCode.includes('filter.site_uuid = req.user.site_uuid') || dashCode.includes('filter.site_uuid ='));
|
||||
test('/apps pakai getBaseFilter', dashCode.includes("router.get('/apps'") && dashCode.includes('getBaseFilter'));
|
||||
test('/devices pakai getBaseFilter', dashCode.includes("router.get('/devices'") && dashCode.includes('getBaseFilter'));
|
||||
test('/flows pakai getBaseFilter', dashCode.includes("router.get('/flows'") && dashCode.includes('getBaseFilter'));
|
||||
test('/threats pakai getBaseFilter', dashCode.includes("router.get('/threats'") && dashCode.includes('getBaseFilter'));
|
||||
test('/summary pakai getBaseFilter', dashCode.includes("router.get('/summary'") && dashCode.includes('getBaseFilter'));
|
||||
test('/agents endpoint hanya untuk SUPER_ADMIN', dashCode.includes("SUPER_ADMIN") && dashCode.includes("router.get('/agents'"));
|
||||
console.log('');
|
||||
|
||||
// ─── TEST GROUP 3: Proxy 2 mode ────────────────────────────────────────────
|
||||
console.log('[GROUP 3] Proxy — 2 Mode Pengambilan Data');
|
||||
const schedulerCode = readFile('proxy/scheduler.js');
|
||||
const collectorCode = readFile('proxy/collector.js');
|
||||
|
||||
test('scheduler.js membaca PROXY_COLLECT_MODE env', schedulerCode.includes('PROXY_COLLECT_MODE'));
|
||||
test('scheduler.js membaca PROXY_AGENT_UUID env', schedulerCode.includes('PROXY_AGENT_UUID'));
|
||||
test("scheduler.js mendukung mode 'all'", schedulerCode.includes("COLLECT_MODE === 'agent'"));
|
||||
test('collector.js export collectAllAgents()', collectorCode.includes('collectAllAgents'));
|
||||
test('collector.js export collectSpecificAgent()', collectorCode.includes('collectSpecificAgent'));
|
||||
test('collector.js tag data dengan agent_uuid', collectorCode.includes('agent_uuid: agentUuid'));
|
||||
test('collector.js tag data dengan site_uuid', collectorCode.includes('site_uuid: SITE_UUID') || collectorCode.includes('site_uuid: SITE_UUID'));
|
||||
console.log('');
|
||||
|
||||
// ─── TEST GROUP 4: Docker Compose 2 Container Groups ─────────────────────
|
||||
console.log('[GROUP 4] Docker Compose — 2 Container Groups + 2 Networks');
|
||||
const composeCode = readFile('docker-compose.yml');
|
||||
|
||||
test('docker-compose.yml punya service: mongodb', composeCode.includes('mongodb:'));
|
||||
test('docker-compose.yml punya service: proxy', composeCode.includes('proxy:'));
|
||||
test('docker-compose.yml punya service: backend', composeCode.includes('backend:'));
|
||||
test('docker-compose.yml punya service: frontend', composeCode.includes('frontend:'));
|
||||
test('docker-compose.yml punya network: backone-infra', composeCode.includes('backone-infra'));
|
||||
test('docker-compose.yml punya network: backone-app', composeCode.includes('backone-app'));
|
||||
test('proxy connect ke backone-infra (Container 1)', composeCode.includes('backone-infra'));
|
||||
test('backend connect ke backone-app (Container 2)', composeCode.includes('backone-app'));
|
||||
test('proxy depends on mongodb', composeCode.includes('service_healthy'));
|
||||
test('env PROXY_COLLECT_MODE di docker-compose', composeCode.includes('PROXY_COLLECT_MODE'));
|
||||
test('env PROXY_AGENT_UUID di docker-compose', composeCode.includes('PROXY_AGENT_UUID'));
|
||||
test('PROXY_URL dikirim ke backend', composeCode.includes('PROXY_URL=http://proxy:4000'));
|
||||
console.log('');
|
||||
|
||||
// ─── TEST GROUP 5: MongoDB Schemas sinkron ────────────────────────────────
|
||||
console.log('[GROUP 5] MongoDB Schemas — Proxy & Backend Sinkron');
|
||||
const proxySchema = readFile('proxy/models/Schemas.js');
|
||||
const backendSchema = readFile('backend/models/Schemas.js');
|
||||
|
||||
const sharedFields = ['agent_uuid', 'site_uuid', 'timestamp', 'bandwidth_down', 'bandwidth_up', 'ip_address', 'mac_address', 'flow_id', 'src_ip', 'dst_ip', 'threat_type', 'severity'];
|
||||
for (const field of sharedFields) {
|
||||
test(`Field '${field}' ada di kedua schema`, proxySchema.includes(field) && backendSchema.includes(field));
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// ─── SUMMARY ──────────────────────────────────────────────────────────────
|
||||
console.log('══════════════════════════════════════════════════════');
|
||||
console.log(` Results: ${passed} PASSED | ${failed} FAILED`);
|
||||
console.log('══════════════════════════════════════════════════════\n');
|
||||
|
||||
if (failed > 0) process.exit(1);
|
||||
@@ -0,0 +1,103 @@
|
||||
// test/field_alignment.js
|
||||
// Check field name alignment between backend and frontend
|
||||
const fs = require('fs');
|
||||
|
||||
const dash = fs.readFileSync('backend/routes/dashboard.js','utf8') +
|
||||
fs.readFileSync('backend/routes/dashboard/helpers.js','utf8') +
|
||||
fs.readFileSync('backend/routes/dashboard/summary.js','utf8') +
|
||||
fs.readFileSync('backend/routes/dashboard/apps.js','utf8') +
|
||||
fs.readFileSync('backend/routes/dashboard/devices.js','utf8') +
|
||||
fs.readFileSync('backend/routes/dashboard/flows.js','utf8') +
|
||||
fs.readFileSync('backend/routes/dashboard/threats.js','utf8') +
|
||||
fs.readFileSync('backend/routes/dashboard/geo.js','utf8') +
|
||||
fs.readFileSync('backend/routes/dashboard/tls.js','utf8') +
|
||||
fs.readFileSync('backend/routes/dashboard/telemetry.js','utf8') +
|
||||
fs.readFileSync('backend/routes/dashboard/events.js','utf8');
|
||||
const api = fs.readFileSync('src/lib/api.ts','utf8');
|
||||
|
||||
console.log('=== Field Name Alignment: Backend ↔ Frontend ===\n');
|
||||
|
||||
function check(label, backendHas, frontendExpects, mismatchDesc = '') {
|
||||
if (backendHas && frontendExpects) {
|
||||
console.log(` ✓ PASS: ${label} is aligned`);
|
||||
} else if (backendHas && !frontendExpects) {
|
||||
console.log(` ⚠ WARNING: Backend defines ${label} but Frontend doesn't expect it`);
|
||||
} else if (!backendHas && frontendExpects) {
|
||||
console.log(` ✗ FAIL: ${label} is missing in backend routing!`);
|
||||
if (mismatchDesc) console.log(` → ${mismatchDesc}`);
|
||||
}
|
||||
}
|
||||
|
||||
// /protocols
|
||||
console.log('[/protocols]');
|
||||
check(
|
||||
'protocol_label',
|
||||
dash.includes('protocol_label'),
|
||||
api.includes('protocol_label'),
|
||||
'Backend doesn\'t return protocol_label to frontend'
|
||||
);
|
||||
console.log('');
|
||||
|
||||
// /timeline
|
||||
console.log('[/timeline]');
|
||||
check(
|
||||
'fetched_at',
|
||||
dash.includes('fetched_at'),
|
||||
api.includes('fetched_at'),
|
||||
'Backend sends timestamp, frontend reads fetched_at'
|
||||
);
|
||||
console.log('');
|
||||
|
||||
// /countries
|
||||
console.log('[/countries]');
|
||||
check(
|
||||
'country_name',
|
||||
dash.includes('country_name'),
|
||||
api.includes('country_name'),
|
||||
'Backend does not return country_name'
|
||||
);
|
||||
check(
|
||||
'country_code',
|
||||
dash.includes('country_code'),
|
||||
api.includes('country_code'),
|
||||
'Backend does not return country_code'
|
||||
);
|
||||
console.log('');
|
||||
|
||||
// /dns
|
||||
console.log('[/dns]');
|
||||
check(
|
||||
'domain',
|
||||
dash.includes('domain'),
|
||||
api.includes('domain'),
|
||||
'Backend does not map domain field'
|
||||
);
|
||||
console.log('');
|
||||
|
||||
// /summary
|
||||
console.log('[/summary]');
|
||||
check('total_devices', dash.includes('total_devices'), api.includes('total_devices'));
|
||||
check('bandwidth_down', dash.includes('bandwidth_down'), api.includes('bandwidth_down'));
|
||||
check('download_speed', dash.includes('download_speed'), api.includes('download_speed'));
|
||||
console.log('');
|
||||
|
||||
// /apps
|
||||
console.log('[/apps]');
|
||||
check('app_label', dash.includes('app_label'), api.includes('app_label'));
|
||||
console.log('');
|
||||
|
||||
// /flows
|
||||
console.log('[/flows]');
|
||||
check('src_ip', dash.includes('src_ip'), api.includes('src_ip'));
|
||||
check('dst_port', dash.includes('dst_port'), api.includes('dst_port'));
|
||||
console.log('');
|
||||
|
||||
// app-categories
|
||||
console.log('[/app-categories]');
|
||||
check(
|
||||
'category_label',
|
||||
dash.includes('category_label'),
|
||||
api.includes('category_label'),
|
||||
'Backend sends label, frontend expects category_label'
|
||||
);
|
||||
console.log('');
|
||||
@@ -0,0 +1,137 @@
|
||||
// test/final_review.js
|
||||
// Final Review — Verifikasi semua deliverable dari original request
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
|
||||
function r(f) { return fs.readFileSync(path.join(ROOT, f), 'utf8'); }
|
||||
|
||||
let ok = 0, fail = 0;
|
||||
function check(label, condition) {
|
||||
const mark = condition ? '✓' : '✗ FAIL';
|
||||
console.log(` ${mark}: ${label}`);
|
||||
condition ? ok++ : fail++;
|
||||
}
|
||||
|
||||
console.log('\n╔══════════════════════════════════════════════════════╗');
|
||||
console.log('║ FINAL REVIEW — Original Request Deliverables ║');
|
||||
console.log('╚══════════════════════════════════════════════════════╝\n');
|
||||
|
||||
const dash = r('backend/routes/dashboard.js');
|
||||
const auth = r('backend/routes/auth.js');
|
||||
const server = r('backend/server.js');
|
||||
const sched = r('proxy/scheduler.js');
|
||||
const coll = r('proxy/collector.js');
|
||||
const compose = r('docker-compose.yml');
|
||||
const envFile = r('.env.local');
|
||||
const proxyIdx = r('proxy/index.js');
|
||||
const proxySchema = r('proxy/models/Schemas.js');
|
||||
const pkg = JSON.parse(r('backend/package.json'));
|
||||
|
||||
// ─── DELIVERABLE 1: Proxy 2 Mode ───────────────────────────────────────────
|
||||
console.log('📦 DELIVERABLE 1: Proxy — 2 Mode Pengambilan Data');
|
||||
check('Mode ALL: collectAllAgents() ada', coll.includes('collectAllAgents'));
|
||||
check('Mode AGENT: collectSpecificAgent() ada', coll.includes('collectSpecificAgent'));
|
||||
check('Scheduler baca PROXY_COLLECT_MODE env', sched.includes('PROXY_COLLECT_MODE'));
|
||||
check('Scheduler baca PROXY_AGENT_UUID env', sched.includes('PROXY_AGENT_UUID'));
|
||||
check('Jika mode=agent → collectSpecificAgent', sched.includes("COLLECT_MODE === 'agent'"));
|
||||
check('Jika mode=all → collectAllAgents', sched.includes('collectAllAgents'));
|
||||
console.log('');
|
||||
|
||||
// ─── DELIVERABLE 2: Data Isolation per Agent UUID ──────────────────────────
|
||||
console.log('📦 DELIVERABLE 2: Isolasi Data per Network Agent UUID di MongoDB');
|
||||
check('Proxy tag dokumen dengan agent_uuid', coll.includes('agent_uuid: agentUuid'));
|
||||
check('Proxy tag dokumen dengan site_uuid', coll.includes('site_uuid'));
|
||||
check('Proxy simpan IP address per agent', coll.includes('ip_address'));
|
||||
check('Proxy simpan MAC address per agent', coll.includes('mac_address'));
|
||||
check('Proxy simpan timestamp per agent', coll.includes('timestamp'));
|
||||
check('Proxy simpan bandwidth per agent', coll.includes('bandwidth_down') || coll.includes('download'));
|
||||
check('Summary schema punya agent_uuid', proxySchema.includes('agent_uuid'));
|
||||
check('Flow schema punya agent_uuid', proxySchema.includes('FlowSchema'));
|
||||
check('Device schema punya mac_address', proxySchema.includes('mac_address'));
|
||||
check('Threat schema ada', proxySchema.includes('ThreatSchema'));
|
||||
console.log('');
|
||||
|
||||
// ─── DELIVERABLE 3: Backend READ-ONLY dari MongoDB ─────────────────────────
|
||||
console.log('📦 DELIVERABLE 3: Backend Hanya Baca MongoDB (bukan tulis/ingestion)');
|
||||
check('server.js tidak ada ingestionService', !server.includes('ingestionService'));
|
||||
check('server.js tidak ada startScheduler', !server.includes('startScheduler'));
|
||||
check('server.js tidak ada import SQLite database.js', !server.includes("'./database'"));
|
||||
check('dashboard.js tidak ada db.getStats SQLite', !dash.includes('db.getStats'));
|
||||
check('dashboard.js tidak ada db.getBandwidthTimeline', !dash.includes('getBandwidthTimeline'));
|
||||
check('dashboard.js tidak ada db.getLatest SQLite', !dash.includes('db.getLatest'));
|
||||
check('auth.js tidak import database.js', !auth.includes("require('../database')"));
|
||||
check('auth.js tidak ada better-sqlite3', !auth.includes('better-sqlite3'));
|
||||
check('package.json tidak ada sqlite3', !pkg.dependencies['sqlite3'] && !pkg.dependencies['better-sqlite3']);
|
||||
console.log('');
|
||||
|
||||
// ─── DELIVERABLE 4: Multi-tenant isolasi per agent ─────────────────────────
|
||||
console.log('📦 DELIVERABLE 4: Multi-Tenant Isolation (Backend filter per agent_uuid)');
|
||||
check('getBaseFilter() helper ada', dash.includes('function getBaseFilter'));
|
||||
check('agent_uuid filter diterapkan jika AGENT_VIEWER', dash.includes('filter.agent_uuid = req.user.agent_uuid'));
|
||||
check('site_uuid filter diterapkan', dash.includes('filter.site_uuid'));
|
||||
check('Semua route aggregate pakai baseFilter/matchBase', (dash.match(/matchBase|getBaseFilter/g) || []).length > 15);
|
||||
check('/agents hanya SUPER_ADMIN', dash.includes("role !== 'SUPER_ADMIN'"));
|
||||
check('SUPER_ADMIN lihat semua data (no agent filter)', dash.includes('AGENT_VIEWER'));
|
||||
check('View-As mode didukung di server.js', server.includes('AGENT_VIEWER') && server.includes('view-as'));
|
||||
console.log('');
|
||||
|
||||
// ─── DELIVERABLE 5: 2 Docker Container Groups ──────────────────────────────
|
||||
console.log('📦 DELIVERABLE 5: 2 Docker Container Groups (Container 1: Proxy+MongoDB, Container 2: Backend+Frontend)');
|
||||
check('Container 1: Service mongodb', compose.includes('mongodb:'));
|
||||
check('Container 1: Service proxy', compose.includes('proxy:'));
|
||||
check('Container 2: Service backend', compose.includes('backend:'));
|
||||
check('Container 2: Service frontend', compose.includes('frontend:'));
|
||||
check('Network backone-infra (Container 1)', compose.includes('backone-infra'));
|
||||
check('Network backone-app (Container 2)', compose.includes('backone-app'));
|
||||
check('Proxy di backone-infra (isolated)', compose.includes('backone-infra'));
|
||||
check('Backend di backone-app (isolated)', compose.includes('backone-app'));
|
||||
check('MongoDB di kedua network (shared)', compose.includes('backone-infra') && compose.includes('backone-app'));
|
||||
check('Proxy depends on mongodb healthcheck', compose.includes('service_healthy'));
|
||||
check('PROXY_COLLECT_MODE di docker-compose', compose.includes('PROXY_COLLECT_MODE'));
|
||||
check('PROXY_AGENT_UUID di docker-compose', compose.includes('PROXY_AGENT_UUID'));
|
||||
check('PROXY_URL ke backend (untuk manual refresh)', compose.includes('PROXY_URL'));
|
||||
console.log('');
|
||||
|
||||
// ─── DELIVERABLE 6: Proxy API Endpoints ────────────────────────────────────
|
||||
console.log('📦 DELIVERABLE 6: Proxy REST API (port 4000)');
|
||||
check('GET /health', proxyIdx.includes("'/health'"));
|
||||
check('GET /status', proxyIdx.includes("'/status'"));
|
||||
check('GET /agents (list all agent UUIDs di MongoDB)', proxyIdx.includes("'/agents'"));
|
||||
check('POST /collect/all (trigger semua agent)', proxyIdx.includes("'/collect/all'"));
|
||||
check('POST /collect/:agentUuid (trigger spesifik)', proxyIdx.includes("'/collect/:agentUuid'"));
|
||||
console.log('');
|
||||
|
||||
// ─── DELIVERABLE 7: Proxy pisah dari backend ───────────────────────────────
|
||||
console.log('📦 DELIVERABLE 7: Proxy Terpisah dari Backend (sesuai pertanyaan user)');
|
||||
check('Proxy punya Dockerfile sendiri', fs.existsSync(path.join(ROOT, 'proxy/Dockerfile')));
|
||||
check('Proxy punya package.json sendiri', fs.existsSync(path.join(ROOT, 'proxy/package.json')));
|
||||
check('Proxy di folder terpisah (/proxy/)', fs.existsSync(path.join(ROOT, 'proxy/index.js')));
|
||||
check('Backend tidak mengimport dari proxy/', !server.includes("'../proxy'") && !dash.includes("'../proxy'"));
|
||||
check('Proxy tidak mengimport dari backend/', !r('proxy/index.js').includes("'../backend'"));
|
||||
check('Proxy hanya komunikasi ke MongoDB (tidak ke backend)', !r('proxy/collector.js').includes('3001'));
|
||||
check('Backend hanya POST ke proxy (bukan import)', dash.includes('axios.post') || server.includes('PROXY_URL'));
|
||||
console.log('');
|
||||
|
||||
// ─── DELIVERABLE 8: .env.local dokumentasi ─────────────────────────────────
|
||||
console.log('📦 DELIVERABLE 8: Environment Variable Dokumentasi');
|
||||
check('PROXY_COLLECT_MODE terdokumentasi', envFile.includes('PROXY_COLLECT_MODE'));
|
||||
check('PROXY_AGENT_UUID terdokumentasi', envFile.includes('PROXY_AGENT_UUID'));
|
||||
check('Penjelasan Mode 1 (all)', envFile.includes('Mode 1'));
|
||||
check('Penjelasan Mode 2 (agent spesifik)', envFile.includes('Mode 2'));
|
||||
check('MONGODB_URI ada', envFile.includes('MONGODB_URI'));
|
||||
check('PROXY_CRON_SCHEDULE ada', envFile.includes('PROXY_CRON_SCHEDULE'));
|
||||
console.log('');
|
||||
|
||||
// ─── SUMMARY ───────────────────────────────────────────────────────────────
|
||||
console.log('══════════════════════════════════════════════════════');
|
||||
console.log(` FINAL: ${ok} PASSED | ${fail} FAILED`);
|
||||
if (fail === 0) {
|
||||
console.log(' STATUS: ✓ SEMUA DELIVERABLE TERPENUHI');
|
||||
} else {
|
||||
console.log(' STATUS: ✗ ADA YANG PERLU DIPERBAIKI — lihat item FAIL di atas');
|
||||
}
|
||||
console.log('══════════════════════════════════════════════════════\n');
|
||||
|
||||
if (fail > 0) process.exit(1);
|
||||
@@ -0,0 +1,67 @@
|
||||
// Script to patch api.ts with timeRange support
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const filePath = path.join(__dirname, '..', 'src', 'lib', 'api.ts');
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// 1. Fix fetcher to accept timeRange
|
||||
const oldFetcher = `// Fetcher Function
|
||||
export async function fetcher<T>(endpoint: string): Promise<T> {
|
||||
const url = \`/api/dashboard\${endpoint}\`;
|
||||
// Tambahkan X-View-As-Agent header jika admin dalam mode view-as
|
||||
const viewAsHeaders = getViewAsHeaders();
|
||||
const res = await fetch(url, { headers: viewAsHeaders });
|
||||
if (!res.ok) {
|
||||
throw new Error(\`API error: \${res.status} \${res.statusText}\`);
|
||||
}
|
||||
const json = await res.json();
|
||||
if (!json.ok) {
|
||||
throw new Error(json.message || 'API responded with ok: false');
|
||||
}
|
||||
return json.data;
|
||||
}`;
|
||||
|
||||
const newFetcher = `// Fetcher Function — supports optional timeRange for time-filtered queries
|
||||
export async function fetcher<T>(endpoint: string, timeRange?: string): Promise<T> {
|
||||
// Append timeRange param if provided (appended before X-View-As header)
|
||||
let url = \`/api/dashboard\${endpoint}\`;
|
||||
if (timeRange) {
|
||||
const sep = url.includes('?') ? '&' : '?';
|
||||
url += sep + 'timeRange=' + timeRange;
|
||||
}
|
||||
// X-View-As-Agent header for admin view-as mode
|
||||
const viewAsHeaders = getViewAsHeaders();
|
||||
const res = await fetch(url, { headers: viewAsHeaders });
|
||||
if (!res.ok) {
|
||||
throw new Error(\`API error: \${res.status} \${res.statusText}\`);
|
||||
}
|
||||
const json = await res.json();
|
||||
if (!json.ok) {
|
||||
throw new Error(json.message || 'API responded with ok: false');
|
||||
}
|
||||
return json.data;
|
||||
}`;
|
||||
|
||||
// Normalize CRLF → LF for matching
|
||||
const normalized = content.replace(/\r\n/g, '\n');
|
||||
const oldNorm = oldFetcher.replace(/\r\n/g, '\n');
|
||||
const newNorm = newFetcher.replace(/\r\n/g, '\n');
|
||||
|
||||
if (!normalized.includes(oldNorm)) {
|
||||
console.error('ERROR: Could not find fetcher function in api.ts');
|
||||
console.log('Content around line 170:');
|
||||
console.log(normalized.split('\n').slice(168, 186).join('\n'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const patched = normalized.replace(oldNorm, newNorm);
|
||||
|
||||
// Write back with CRLF (Windows)
|
||||
fs.writeFileSync(filePath, patched.replace(/\n/g, '\r\n'), 'utf8');
|
||||
console.log('✓ api.ts patched successfully');
|
||||
console.log('✓ fetcher now accepts optional timeRange parameter');
|
||||
|
||||
// Verify
|
||||
const verify = fs.readFileSync(filePath, 'utf8');
|
||||
console.log('✓ timeRange in fetcher:', verify.includes('timeRange?: string') ? 'YES' : 'NO');
|
||||
@@ -0,0 +1,273 @@
|
||||
// Script to update all API hooks to accept timeRange parameter
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const filePath = path.join(__dirname, '..', 'src', 'lib', 'api.ts');
|
||||
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
||||
|
||||
let count = 0;
|
||||
|
||||
// Helper to replace hook signatures and add timeRange to useEffect deps
|
||||
function patchHook(content, hookSignature, fetchCall, newFetchCall, depArray, newDepArray) {
|
||||
if (content.includes(hookSignature)) {
|
||||
content = content.replace(hookSignature, hookSignature.replace(')', ', timeRange?: string)'));
|
||||
if (fetchCall && newFetchCall) {
|
||||
content = content.replace(fetchCall, newFetchCall);
|
||||
}
|
||||
if (depArray && newDepArray) {
|
||||
content = content.replace(depArray, newDepArray);
|
||||
}
|
||||
count++;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
// useDashboardSummary
|
||||
content = content.replace(
|
||||
'export function useDashboardSummary() {',
|
||||
'export function useDashboardSummary(timeRange?: string) {'
|
||||
);
|
||||
content = content.replace(
|
||||
" fetcher<DashboardSummary>('/summary')\n .then(setData)",
|
||||
" fetcher<DashboardSummary>('/summary', timeRange)\n .then(setData)"
|
||||
);
|
||||
content = content.replace(
|
||||
" }, []);\n\n return { data, error, isLoading };\n}\n\nexport function useTimeline",
|
||||
" }, [timeRange]);\n\n return { data, error, isLoading };\n}\n\nexport function useTimeline"
|
||||
);
|
||||
|
||||
// useTimeline
|
||||
content = content.replace(
|
||||
'export function useTimeline(points: number = 60) {',
|
||||
'export function useTimeline(points: number = 60, timeRange?: string) {'
|
||||
);
|
||||
content = content.replace(
|
||||
" fetcher<TimelinePoint[]>(`/timeline?points=${points}`)",
|
||||
" fetcher<TimelinePoint[]>(`/timeline?points=${points}`, timeRange)"
|
||||
);
|
||||
content = content.replace(
|
||||
" }, [points]);\n\n return { data, error, isLoading };\n}\n\nexport function useTopApps",
|
||||
" }, [points, timeRange]);\n\n return { data, error, isLoading };\n}\n\nexport function useTopApps"
|
||||
);
|
||||
|
||||
// useTopApps
|
||||
content = content.replace(
|
||||
'export function useTopApps(limit: number = 10) {',
|
||||
'export function useTopApps(limit: number = 10, timeRange?: string) {'
|
||||
);
|
||||
content = content.replace(
|
||||
" fetcher<AppStat[]>(`/apps?limit=${limit}`)",
|
||||
" fetcher<AppStat[]>(`/apps?limit=${limit}`, timeRange)"
|
||||
);
|
||||
content = content.replace(
|
||||
" }, [limit]);\n\n return { data, error, isLoading };\n}\n\nexport function useTopProtocols",
|
||||
" }, [limit, timeRange]);\n\n return { data, error, isLoading };\n}\n\nexport function useTopProtocols"
|
||||
);
|
||||
|
||||
// useTopProtocols
|
||||
content = content.replace(
|
||||
'export function useTopProtocols() {',
|
||||
'export function useTopProtocols(timeRange?: string) {'
|
||||
);
|
||||
content = content.replace(
|
||||
" fetcher<ProtocolStat[]>('/protocols')",
|
||||
" fetcher<ProtocolStat[]>('/protocols', timeRange)"
|
||||
);
|
||||
content = content.replace(
|
||||
" }, []);\n\n return { data, error, isLoading };\n}\n\nexport function useDevices",
|
||||
" }, [timeRange]);\n\n return { data, error, isLoading };\n}\n\nexport function useDevices"
|
||||
);
|
||||
|
||||
// useDevices
|
||||
content = content.replace(
|
||||
'export function useDevices(limit: number = 50) {',
|
||||
'export function useDevices(limit: number = 50, timeRange?: string) {'
|
||||
);
|
||||
content = content.replace(
|
||||
" fetcher<DeviceStat[]>(`/devices?limit=${limit}`)",
|
||||
" fetcher<DeviceStat[]>(`/devices?limit=${limit}`, timeRange)"
|
||||
);
|
||||
content = content.replace(
|
||||
" }, [limit]);\n\n return { data, error, isLoading };\n}\n\nexport function useThreats",
|
||||
" }, [limit, timeRange]);\n\n return { data, error, isLoading };\n}\n\nexport function useThreats"
|
||||
);
|
||||
|
||||
// useThreats
|
||||
content = content.replace(
|
||||
'export function useThreats(limit: number = 20) {',
|
||||
'export function useThreats(limit: number = 20, timeRange?: string) {'
|
||||
);
|
||||
content = content.replace(
|
||||
" fetcher<ThreatStat[]>(`/threats?limit=${limit}`)",
|
||||
" fetcher<ThreatStat[]>(`/threats?limit=${limit}`, timeRange)"
|
||||
);
|
||||
content = content.replace(
|
||||
" }, [limit]);\n\n return { data, error, isLoading };\n}\n\nexport function useAppCategories",
|
||||
" }, [limit, timeRange]);\n\n return { data, error, isLoading };\n}\n\nexport function useAppCategories"
|
||||
);
|
||||
|
||||
// useAppCategories
|
||||
content = content.replace(
|
||||
'export function useAppCategories() {',
|
||||
'export function useAppCategories(timeRange?: string) {'
|
||||
);
|
||||
content = content.replace(
|
||||
" fetcher<AppCategoryStat[]>('/app-categories')",
|
||||
" fetcher<AppCategoryStat[]>('/app-categories', timeRange)"
|
||||
);
|
||||
content = content.replace(
|
||||
" }, []);\n\n return { data, error, isLoading };\n}\n\nexport function useContinents",
|
||||
" }, [timeRange]);\n\n return { data, error, isLoading };\n}\n\nexport function useContinents"
|
||||
);
|
||||
|
||||
// useContinents
|
||||
content = content.replace(
|
||||
'export function useContinents() {',
|
||||
'export function useContinents(timeRange?: string) {'
|
||||
);
|
||||
content = content.replace(
|
||||
" fetcher<ContinentStat[]>('/continents')",
|
||||
" fetcher<ContinentStat[]>('/continents', timeRange)"
|
||||
);
|
||||
content = content.replace(
|
||||
" }, []);\n\n return { data, error, isLoading };\n}\n\nexport function useFlows",
|
||||
" }, [timeRange]);\n\n return { data, error, isLoading };\n}\n\nexport function useFlows"
|
||||
);
|
||||
|
||||
// useFlows
|
||||
content = content.replace(
|
||||
'export function useFlows(limit: number = 50) {',
|
||||
'export function useFlows(limit: number = 50, timeRange?: string) {'
|
||||
);
|
||||
content = content.replace(
|
||||
" fetcher<FlowStat[]>(`/flows?limit=${limit}`)",
|
||||
" fetcher<FlowStat[]>(`/flows?limit=${limit}`, timeRange)"
|
||||
);
|
||||
content = content.replace(
|
||||
" }, [limit]);\n\n return { data, error, isLoading };\n}\n\nexport function useCountries",
|
||||
" }, [limit, timeRange]);\n\n return { data, error, isLoading };\n}\n\nexport function useCountries"
|
||||
);
|
||||
|
||||
// useCountries
|
||||
content = content.replace(
|
||||
'export function useCountries(limit: number = 50) {',
|
||||
'export function useCountries(limit: number = 50, timeRange?: string) {'
|
||||
);
|
||||
content = content.replace(
|
||||
" fetcher<CountryStat[]>(`/countries?limit=${limit}`)",
|
||||
" fetcher<CountryStat[]>(`/countries?limit=${limit}`, timeRange)"
|
||||
);
|
||||
content = content.replace(
|
||||
" }, [limit]);\n\n return { data, error, isLoading };\n}\n\nexport function useDNS",
|
||||
" }, [limit, timeRange]);\n\n return { data, error, isLoading };\n}\n\nexport function useDNS"
|
||||
);
|
||||
|
||||
// useDNS
|
||||
content = content.replace(
|
||||
'export function useDNS(limit: number = 50) {',
|
||||
'export function useDNS(limit: number = 50, timeRange?: string) {'
|
||||
);
|
||||
content = content.replace(
|
||||
" fetcher<DnsStat[]>(`/dns?limit=${limit}`)",
|
||||
" fetcher<DnsStat[]>(`/dns?limit=${limit}`, timeRange)"
|
||||
);
|
||||
content = content.replace(
|
||||
" }, [limit]);\n\n return { data, error, isLoading };\n}\n\nexport function useEvents",
|
||||
" }, [limit, timeRange]);\n\n return { data, error, isLoading };\n}\n\nexport function useEvents"
|
||||
);
|
||||
|
||||
// useEvents
|
||||
content = content.replace(
|
||||
'export function useEvents(limit: number = 50) {',
|
||||
'export function useEvents(limit: number = 50, timeRange?: string) {'
|
||||
);
|
||||
content = content.replace(
|
||||
" fetcher<EventStat[]>(`/events?limit=${limit}`)",
|
||||
" fetcher<EventStat[]>(`/events?limit=${limit}`, timeRange)"
|
||||
);
|
||||
content = content.replace(
|
||||
" }, [limit]);\n\n return { data, error, isLoading };\n}\n\nexport function useTlsVersions",
|
||||
" }, [limit, timeRange]);\n\n return { data, error, isLoading };\n}\n\nexport function useTlsVersions"
|
||||
);
|
||||
|
||||
// useTlsVersions
|
||||
content = content.replace(
|
||||
'export function useTlsVersions(limit: number = 10) {',
|
||||
'export function useTlsVersions(limit: number = 10, timeRange?: string) {'
|
||||
);
|
||||
content = content.replace(
|
||||
" fetcher<TlsVersionStat[]>(`/tls-versions?limit=${limit}`)",
|
||||
" fetcher<TlsVersionStat[]>(`/tls-versions?limit=${limit}`, timeRange)"
|
||||
);
|
||||
content = content.replace(
|
||||
" }, [limit]);\n\n return { data, error, isLoading };\n}\n\nexport function useTlsCiphers",
|
||||
" }, [limit, timeRange]);\n\n return { data, error, isLoading };\n}\n\nexport function useTlsCiphers"
|
||||
);
|
||||
|
||||
// useTlsCiphers
|
||||
content = content.replace(
|
||||
'export function useTlsCiphers(limit: number = 15) {',
|
||||
'export function useTlsCiphers(limit: number = 15, timeRange?: string) {'
|
||||
);
|
||||
content = content.replace(
|
||||
" fetcher<TlsCipherStat[]>(`/tls-ciphers?limit=${limit}`)",
|
||||
" fetcher<TlsCipherStat[]>(`/tls-ciphers?limit=${limit}`, timeRange)"
|
||||
);
|
||||
content = content.replace(
|
||||
" }, [limit]);\n\n return { data, error, isLoading };\n}\n\nexport function useTlsSecurity",
|
||||
" }, [limit, timeRange]);\n\n return { data, error, isLoading };\n}\n\nexport function useTlsSecurity"
|
||||
);
|
||||
|
||||
// useTlsSecurity
|
||||
content = content.replace(
|
||||
'export function useTlsSecurity(limit: number = 10) {',
|
||||
'export function useTlsSecurity(limit: number = 10, timeRange?: string) {'
|
||||
);
|
||||
content = content.replace(
|
||||
" fetcher<TlsSecurityStat[]>(`/tls-security?limit=${limit}`)",
|
||||
" fetcher<TlsSecurityStat[]>(`/tls-security?limit=${limit}`, timeRange)"
|
||||
);
|
||||
content = content.replace(
|
||||
" }, [limit]);\n\n return { data, error, isLoading };\n}\n\nexport function useIntelligenceStats",
|
||||
" }, [limit, timeRange]);\n\n return { data, error, isLoading };\n}\n\nexport function useIntelligenceStats"
|
||||
);
|
||||
|
||||
// useIntelligenceStats
|
||||
content = content.replace(
|
||||
'export function useIntelligenceStats() {',
|
||||
'export function useIntelligenceStats(timeRange?: string) {'
|
||||
);
|
||||
content = content.replace(
|
||||
" fetcher<IntelligenceStats>('/intelligence/stats')",
|
||||
" fetcher<IntelligenceStats>('/intelligence/stats', timeRange)"
|
||||
);
|
||||
content = content.replace(
|
||||
" }, []);\n\n return { data, error, isLoading };\n}\n\n// ─── LOOKUP APPLICATIONS",
|
||||
" }, [timeRange]);\n\n return { data, error, isLoading };\n}\n\n// ─── LOOKUP APPLICATIONS"
|
||||
);
|
||||
|
||||
// Write back with CRLF
|
||||
fs.writeFileSync(filePath, content.replace(/\n/g, '\r\n'), 'utf8');
|
||||
|
||||
// Verify
|
||||
const verify = fs.readFileSync(filePath, 'utf8');
|
||||
const hooks = [
|
||||
'useDashboardSummary', 'useTimeline', 'useTopApps', 'useTopProtocols',
|
||||
'useDevices', 'useThreats', 'useAppCategories', 'useContinents',
|
||||
'useFlows', 'useCountries', 'useDNS', 'useEvents',
|
||||
'useTlsVersions', 'useTlsCiphers', 'useTlsSecurity', 'useIntelligenceStats'
|
||||
];
|
||||
|
||||
console.log('\n=== api.ts Patch Results ===');
|
||||
let allOk = true;
|
||||
for (const hook of hooks) {
|
||||
const pattern = hook + '(';
|
||||
const idx = verify.indexOf('export function ' + hook);
|
||||
if (idx < 0) { console.log('✗ MISSING:', hook); allOk = false; continue; }
|
||||
const slice = verify.slice(idx, idx + 80);
|
||||
const hasTimeRange = slice.includes('timeRange');
|
||||
console.log((hasTimeRange ? '✓' : '✗') + ' ' + hook + ' timeRange param');
|
||||
if (!hasTimeRange) allOk = false;
|
||||
}
|
||||
|
||||
console.log('\n' + (allOk ? '✓ ALL HOOKS UPDATED' : '✗ SOME HOOKS MISSING'));
|
||||
@@ -0,0 +1,92 @@
|
||||
// Script to update page files to use api-with-context instead of api
|
||||
// This ensures all page hooks automatically use timeRange from TimeFilterContext
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Files that import hooks from @/lib/api (not fetch functions, just hooks)
|
||||
const pageFiles = [
|
||||
'src/app/(dashboard)/page.tsx',
|
||||
'src/app/(dashboard)/threats/page.tsx',
|
||||
'src/app/(dashboard)/flows/page.tsx',
|
||||
'src/app/(dashboard)/devices/page.tsx',
|
||||
'src/app/(dashboard)/apps/page.tsx',
|
||||
'src/app/(dashboard)/dns/page.tsx',
|
||||
'src/app/(dashboard)/events/page.tsx',
|
||||
'src/app/(dashboard)/geography/page.tsx',
|
||||
'src/app/(dashboard)/intelligence/page.tsx',
|
||||
'src/app/(dashboard)/network-intelligence/page.tsx',
|
||||
'src/app/(dashboard)/security-audit/page.tsx',
|
||||
];
|
||||
|
||||
// Hooks that are context-aware (exist in api-with-context.ts)
|
||||
const contextHooks = [
|
||||
'useDashboardSummary', 'useTimeline', 'useTopApps', 'useTopProtocols',
|
||||
'useDevices', 'useThreats', 'useAppCategories', 'useContinents',
|
||||
'useFlows', 'useCountries', 'useDNS', 'useEvents',
|
||||
'useTlsVersions', 'useTlsCiphers', 'useTlsSecurity', 'useIntelligenceStats',
|
||||
];
|
||||
|
||||
// Types/fetch functions that should remain in @/lib/api
|
||||
const apiOnly = [
|
||||
'fetcher', 'fetchAgentDetails', 'fetchDeviceDetails', 'fetchAppDetails',
|
||||
'fetchSecurityDevices', 'useLookupApplications',
|
||||
// Type re-exports
|
||||
'DashboardSummary', 'TimelinePoint', 'AppStat', 'ProtocolStat', 'DeviceStat',
|
||||
'ThreatStat', 'AppCategoryStat', 'ContinentStat', 'FlowStat', 'CountryStat',
|
||||
'DnsStat', 'EventStat', 'TlsVersionStat', 'TlsCipherStat', 'TlsSecurityStat',
|
||||
'IntelligenceStats', 'AgentDetails', 'DeviceDetails', 'AppDetails', 'SecurityDevice',
|
||||
];
|
||||
|
||||
let totalChanges = 0;
|
||||
|
||||
for (const relPath of pageFiles) {
|
||||
const filePath = path.join(process.cwd(), relPath);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.log('SKIP (not found):', relPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
let content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
||||
const original = content;
|
||||
|
||||
// Find all imports from @/lib/api (not api-with-context, not api-advanced)
|
||||
const importRegex = /import \{([^}]+)\} from ["']@\/lib\/api["'];?/g;
|
||||
let match;
|
||||
|
||||
while ((match = importRegex.exec(content)) !== null) {
|
||||
const fullImport = match[0];
|
||||
const importedItems = match[1].split(',').map(s => s.trim()).filter(Boolean);
|
||||
|
||||
// Categorize: hooks go to api-with-context, types/fetchers stay in api
|
||||
const hooksToMove = importedItems.filter(item =>
|
||||
contextHooks.includes(item) || contextHooks.includes(item.split(' ')[0]) // handle "as" aliases
|
||||
);
|
||||
const typesToKeep = importedItems.filter(item =>
|
||||
!contextHooks.includes(item) && !contextHooks.includes(item.split(' ')[0])
|
||||
);
|
||||
|
||||
if (hooksToMove.length === 0) continue; // nothing to move
|
||||
|
||||
let newImports = '';
|
||||
|
||||
if (typesToKeep.length > 0) {
|
||||
newImports += `import { ${typesToKeep.join(', ')} } from "@/lib/api";\n`;
|
||||
}
|
||||
if (hooksToMove.length > 0) {
|
||||
newImports += `import { ${hooksToMove.join(', ')} } from "@/lib/api-with-context";`;
|
||||
}
|
||||
|
||||
content = content.replace(fullImport, newImports);
|
||||
totalChanges++;
|
||||
}
|
||||
|
||||
if (content !== original) {
|
||||
fs.writeFileSync(filePath, content.replace(/\n/g, '\r\n'), 'utf8');
|
||||
console.log('✓ Updated:', relPath);
|
||||
} else {
|
||||
console.log(' No changes needed:', relPath);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nTotal files updated: ${totalChanges}`);
|
||||
Reference in new issue
Block a user