From cbe8a0a96f51a9d8fe7f57ff36e23ea36aa40872 Mon Sep 17 00:00:00 2001 From: PUNITH KUMAR B S Date: Tue, 7 Jul 2026 15:53:11 +0530 Subject: [PATCH] feat: implement threat analytics dashboard and backend rate limiting (#172) --- README.md | 2 +- dashboard.html | 135 +---------------- dashboard.js | 176 ++++++++++++++++++++++ package-lock.json | 13 ++ package.json | 6 +- script.js | 34 ++++- server.js | 363 ++++++++++++++++++++++++++------------------- test-rate-limit.js | 33 +++++ 8 files changed, 478 insertions(+), 284 deletions(-) create mode 100644 dashboard.js create mode 100644 test-rate-limit.js diff --git a/README.md b/README.md index c66865f..05c9eb6 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ node server.js ## πŸš€ Roadmap - [ ] πŸ” Email breach checker (HIBP API) -- [ ] πŸ“Š Threat analytics dashboard with charts +- [x] πŸ“Š Threat analytics dashboard with charts - [ ] 🌍 Browser extension - [ ] πŸ€– AI-based threat detection - [ ] πŸ“€ Export scan logs for offline analysis diff --git a/dashboard.html b/dashboard.html index 5b4f774..d437f43 100644 --- a/dashboard.html +++ b/dashboard.html @@ -78,8 +78,13 @@

Threat
Analytics Dashboard

Threat Distribution

-
- +
+
+ +
+
+ +
@@ -123,130 +128,6 @@

Navigation

- + diff --git a/dashboard.js b/dashboard.js new file mode 100644 index 0000000..77f18b8 --- /dev/null +++ b/dashboard.js @@ -0,0 +1,176 @@ +// Dashboard JS: reads localStorage `cybershield_history` and renders charts + table + +// Theme toggle handler +const themeToggleBtn = document.getElementById('themeToggle'); +if (themeToggleBtn) { + themeToggleBtn.addEventListener('click', () => { + const isLight = document.documentElement.classList.toggle('light-mode'); + localStorage.setItem('theme', isLight ? 'light' : 'dark'); + }); +} + +function formatDate(iso) { + try { + return iso ? new Date(iso).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : 'β€”'; + } catch (e) { return 'β€”'; } +} + +function safeInt(n){ return Number.isFinite(n) ? n : 0; } + +function getHistory() { + try { + return JSON.parse(localStorage.getItem('cybershield_history') || '[]'); + } catch (err) { + console.warn('Unable to parse scan history:', err); + return []; + } +} + +document.addEventListener('DOMContentLoaded', () => { + const history = getHistory(); + + const total = history.length; + const safeSc = history.filter(r => r.status === 'safe').length; + const dangerSc = history.filter(r => r.status === 'danger').length; + const safeRatio = total > 0 ? Math.round((safeSc / total) * 100) : 0; + + const elTotal = document.getElementById('dbTotalScans'); + const elThreats = document.getElementById('dbThreatsBlocked'); + const elRatio = document.getElementById('dbSafeRatio'); + if (elTotal) elTotal.textContent = total; + if (elThreats) elThreats.textContent = dangerSc; + if (elRatio) elRatio.textContent = `${safeRatio}%`; + + // Threat breakdown counts + let malware = 0, phishing = 0, unwanted = 0, harmful = 0; + history.forEach(r => { + if (r.threats && Array.isArray(r.threats)) { + r.threats.forEach(t => { + if (!t) return; + const key = String(t).toUpperCase(); + if (key.includes('MALWARE')) malware++; + else if (key.includes('SOCIAL_ENGINEERING') || key.includes('PHISHING')) phishing++; + else if (key.includes('UNWANTED')) unwanted++; + else if (key.includes('POTENTIALLY_HARMFUL') || key.includes('HARMFUL')) harmful++; + }); + } + }); + + // Resolve color hints + const isLight = document.documentElement.classList.contains('light-mode'); + const gridColor = isLight ? 'rgba(0,0,0,0.06)' : 'rgba(255,255,255,0.06)'; + const tickColor = isLight ? '#3a5580' : '#4a6580'; + + // Pie chart: Safe vs Threat + const statusCtx = document.getElementById('statusChart'); + if (statusCtx && window.Chart) { + const ctx1 = statusCtx.getContext('2d'); + new Chart(ctx1, { + type: 'doughnut', + data: { + labels: ['Safe', 'Threats'], + datasets: [{ + data: [safeInt(safeSc), safeInt(dangerSc)], + backgroundColor: ['rgba(0, 200, 120, 0.9)', 'rgba(255, 70, 90, 0.95)'], + borderWidth: 0, + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { position: 'bottom', labels: { color: tickColor } } + } + } + }); + } + + // Bar chart: Threat types + const barCtx = document.getElementById('threatBarChart'); + if (barCtx && window.Chart) { + const ctx2 = barCtx.getContext('2d'); + new Chart(ctx2, { + type: 'bar', + data: { + labels: ['Malware', 'Phishing', 'Unwanted Software', 'Potentially Harmful'], + datasets: [{ + label: 'Count', + data: [malware, phishing, unwanted, harmful], + backgroundColor: [ + 'rgba(255, 51, 102, 0.6)', + 'rgba(255, 204, 0, 0.6)', + 'rgba(0, 245, 255, 0.6)', + 'rgba(120, 120, 255, 0.6)' + ], + borderColor: [ + 'rgba(255, 51, 102, 1)', + 'rgba(255, 204, 0, 1)', + 'rgba(0, 245, 255, 1)', + 'rgba(120, 120, 255, 1)' + ], + borderWidth: 1, + borderRadius: 6 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { legend: { display: false } }, + scales: { + y: { beginAtZero: true, ticks: { color: tickColor }, grid: { color: gridColor } }, + x: { ticks: { color: tickColor }, grid: { display: false } } + } + } + }); + } + + // Recent scans table + const recentEl = document.getElementById('recentScans'); + if (recentEl) { + if (!history || history.length === 0) { + recentEl.innerHTML = '

No scans recorded yet. Scan a URL to get started.

'; + } else { + const recent = [...history].reverse().slice(0, 10); + recentEl.innerHTML = ` + + + + + + + + + + ${recent.map(r => ` + + + + + `).join('')} + +
URLResultScanned
+ ${r.url || 'β€”'} + + ${r.status === 'safe' ? 'SAFE' : 'DANGER'} + + ${r.timestamp ? formatDate(r.timestamp) : 'β€”'} +
`; + } + } + + // Clear history button + const clearBtn = document.getElementById('clearHistoryBtn'); + if (clearBtn) { + clearBtn.addEventListener('click', () => { + if (confirm('Permanently delete your entire scan history?')) { + localStorage.removeItem('cybershield_history'); + window.location.reload(); + } + }); + } +}); diff --git a/package-lock.json b/package-lock.json index 2e2ef3e..8c71f47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "cors": "^2.8.6", "dotenv": "^17.4.2", "express": "^5.2.1", + "express-rate-limit": "^6.7.0", "node-fetch": "^2.7.0" } }, @@ -302,6 +303,18 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.11.2.tgz", + "integrity": "sha512-a7uwwfNTh1U60ssiIkuLFWHt4hAC5yxlLGU2VP0X4YNlyEDZAqF4tK3GD3NSitVBrCQmQ0++0uOyFOgC2y4DDw==", + "license": "MIT", + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "express": "^4 || ^5" + } + }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", diff --git a/package.json b/package.json index c5a6b35..abf4aec 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "description": "", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "echo \"Error: no test specified\" && exit 1", + "test-rate-limit": "node test-rate-limit.js" }, "keywords": [], "author": "", @@ -14,6 +15,7 @@ "cors": "^2.8.6", "dotenv": "^17.4.2", "express": "^5.2.1", - "node-fetch": "^2.7.0" + "node-fetch": "^2.7.0", + "express-rate-limit": "^6.7.0" } } diff --git a/script.js b/script.js index 2c902f0..2818774 100644 --- a/script.js +++ b/script.js @@ -103,8 +103,21 @@ function toggleTeam() { // ═══════════════════════════════════ let totalScans = 0, safeCount = 0, dangerCount = 0; +function getHistory() { + try { + return JSON.parse(localStorage.getItem('cybershield_history') || '[]'); + } catch (err) { + console.warn('Unable to parse scan history:', err); + return []; + } +} + +function setHistory(history) { + localStorage.setItem('cybershield_history', JSON.stringify(history)); +} + function loadStats() { - const history = JSON.parse(localStorage.getItem('cybershield_history') || '[]'); + const history = getHistory(); totalScans = history.length; safeCount = history.filter(r => r.status === 'safe').length; dangerCount = history.filter(r => r.status === 'danger').length; @@ -126,9 +139,12 @@ function fillExample(url) { } function saveToHistory(url, status, threats) { - const history = JSON.parse(localStorage.getItem('cybershield_history') || '[]'); + const history = getHistory(); history.push({ url, status, threats, timestamp: new Date().toISOString() }); - localStorage.setItem('cybershield_history', JSON.stringify(history)); + if (history.length > 200) { + history.splice(0, history.length - 200); + } + setHistory(history); } function showResult(type, title, desc, url, threats) { @@ -171,7 +187,17 @@ async function checkSecurity() { showResult('loading', 'Scanning...', 'Checking against threat databases…', url, []); try { - const apiHost = 'https://cybershield-30a3.onrender.com'; + const apiHost = (function() { + // Prefer local backend during development or when opened from file:// + try { + if (location.protocol === 'file:' || location.hostname === 'localhost' || location.hostname === '127.0.0.1') { + console.debug('[Scanner] Using local API host http://localhost:3000'); + return 'http://localhost:3000'; + } + } catch (e) { /* ignore */ } + return 'https://cybershield-30a3.onrender.com'; + })(); + console.debug('[Scanner] apiHost =', apiHost); const response = await fetch(`${apiHost}/check`, { method: 'POST', diff --git a/server.js b/server.js index 4f41058..11fde46 100644 --- a/server.js +++ b/server.js @@ -1,134 +1,185 @@ const express = require('express'); const cors = require('cors'); const dotenv = require('dotenv'); +const fetch = require('node-fetch'); +const path = require('path'); + dotenv.config(); -console.log("API_KEY:", process.env.API_KEY); -console.log("GEMINI_API_KEY:", process.env.GEMINI_API_KEY); +const DEFAULT_ALLOWED_ORIGINS = [ + 'http://localhost:3000', + 'http://localhost:5500', + 'http://127.0.0.1:5500', + 'https://cybershield-sxz0.onrender.com', + 'https://mrinalray.github.io', + 'null' +]; + +function parseOrigins(origins) { + if (!origins) return DEFAULT_ALLOWED_ORIGINS; + return origins.split(',').map(o => o.trim()).filter(Boolean); +} + +function createApp(options = {}) { + const app = express(); -const app = express(); -const PORT = process.env.PORT || 3000; + const SAFE_BROWSING_KEY = options.apiKey || process.env.API_KEY; + const GEMINI_KEY = options.geminiKey || process.env.GEMINI_API_KEY; + const ALLOWED_ORIGINS = options.allowedOrigins || parseOrigins(process.env.CORS_ORIGINS); + const FETCH_IMPL = options.fetchImpl || fetch; + const MAX_RETRIES = Number.isFinite(options.retries) ? options.retries : parseInt(process.env.REQUEST_RETRIES || '2', 10); + const TIMEOUT_MS = Number.isFinite(options.timeoutMs) ? options.timeoutMs : parseInt(process.env.REQUEST_TIMEOUT_MS || '8000', 10); -// ─── CORS ─── -const ALLOWED_ORIGINS = process.env.CORS_ORIGINS - ? process.env.CORS_ORIGINS.split(',').map(o => o.trim()) - : ['http://localhost:3000', 'http://localhost:5500', 'http://127.0.0.1:5500', - 'https://cybershield-sxz0.onrender.com','https://mrinalray.github.io', 'null']; + // Rate limiting + let rateLimit; + try { + rateLimit = require('express-rate-limit'); + } catch (e) { + console.warn('express-rate-limit not installed; /check will not be rate-limited'); + } -app.use(cors({ - origin: (origin, cb) => { - // Allow requests with no origin (Postman, file://) or matching allowed list - if (!origin || ALLOWED_ORIGINS.includes(origin) || ALLOWED_ORIGINS.includes('*')) { - return cb(null, true); + const checkLimiter = rateLimit ? rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per windowMs + standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers + legacyHeaders: false, // Disable the `X-RateLimit-*` headers + message: { error: 'Rate limit exceeded. Try again later.' } + }) : (req, res, next) => next(); + + // ─── CORS ─── + app.use(cors({ + origin: (origin, cb) => { + if (!origin || ALLOWED_ORIGINS.includes(origin) || ALLOWED_ORIGINS.includes('*')) { + return cb(null, true); + } + cb(new Error(`CORS blocked: ${origin}`)); + }, + methods: ['GET', 'POST'], + allowedHeaders: ['Content-Type'] + })); + + app.use(express.json({ limit: '1mb' })); + app.use(express.static(path.join(__dirname))); + + // ─── Timeout + Retry helper ─── + async function fetchWithRetry(url, options, retries = MAX_RETRIES) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), TIMEOUT_MS); + + try { + const res = await FETCH_IMPL(url, { ...options, signal: controller.signal }); + clearTimeout(timer); + return res; + } catch (err) { + clearTimeout(timer); + if (retries > 0 && (err.name === 'AbortError' || err.message.includes('fetch'))) { + console.warn(`[RETRY] ${retries} left β€” ${err.message}`); + await new Promise(r => setTimeout(r, 400)); + return fetchWithRetry(url, options, retries - 1); + } + throw err; } - cb(new Error(`CORS blocked: ${origin}`)); - }, - methods: ['GET', 'POST'], - allowedHeaders: ['Content-Type'] -})); + } -app.use(express.json({ limit: '1mb' })); + // ─── Health check ─── + app.get('/api/status', (req, res) => { + res.json({ status: 'CyberShield backend running', version: '2.0' }); + }); -const SAFE_BROWSING_KEY = process.env.API_KEY; -const GEMINI_KEY = process.env.GEMINI_API_KEY; + app.get('/', (req, res) => { + res.sendFile(path.join(__dirname, 'index.html')); + }); -// ─── Timeout + Retry helper ─── -const MAX_RETRIES = parseInt(process.env.REQUEST_RETRIES || '2', 10); -const TIMEOUT_MS = parseInt(process.env.REQUEST_TIMEOUT_MS || '8000', 10); + app.get('/index.html', (req, res) => { + res.sendFile(path.join(__dirname, 'index.html')); + }); -async function fetchWithRetry(url, options, retries = MAX_RETRIES) { - const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), TIMEOUT_MS); + // ─── URL Safe Browsing check ─── + app.post('/check', checkLimiter, async (req, res) => { + const userUrl = req.body.url; + if (!userUrl) return res.status(400).json({ error: 'No URL provided' }); - try { - const res = await fetch(url, { ...options, signal: controller.signal }); - clearTimeout(timer); - return res; - } catch (err) { - clearTimeout(timer); - if (retries > 0 && (err.name === 'AbortError' || err.message.includes('fetch'))) { - console.warn(`[RETRY] ${retries} left β€” ${err.message}`); - await new Promise(r => setTimeout(r, 400)); - return fetchWithRetry(url, options, retries - 1); + try { new URL(userUrl); } catch { + return res.status(400).json({ error: 'Invalid URL format' }); } - throw err; - } -} -// ─── Health check ─── -app.get('/', (req, res) => { - res.json({ status: 'CyberShield backend running', port: PORT, version: '2.0' }); -}); + if (!SAFE_BROWSING_KEY) { + console.warn('SAFE_BROWSING API key not configured; returning demo scan response.'); + const lowerUrl = userUrl.toLowerCase(); + const matches = []; + + if (lowerUrl.includes('testsafebrowsing.appspot.com/s/phishing')) { + matches.push({ + threatType: 'SOCIAL_ENGINEERING', + platformType: 'ANY_PLATFORM', + threatEntryType: 'URL', + threat: { url: userUrl } + }); + } else if (lowerUrl.includes('testsafebrowsing.appspot.com/s/malware')) { + matches.push({ + threatType: 'MALWARE', + platformType: 'ANY_PLATFORM', + threatEntryType: 'URL', + threat: { url: userUrl } + }); + } -// ─── URL Safe Browsing check ─── -app.post('/check', async (req, res) => { - const userUrl = req.body.url; - if (!userUrl) return res.status(400).json({ error: 'No URL provided' }); + return res.json({ matches }); + } - try { new URL(userUrl); } catch { - return res.status(400).json({ error: 'Invalid URL format' }); - } + console.log(`[SCAN] ${userUrl}`); - if (!SAFE_BROWSING_KEY) { - return res.status(503).json({ error: 'SAFE_BROWSING API key not configured (set API_KEY in .env)' }); - } + const body = { + client: { clientId: 'cybershield', clientVersion: '2.0' }, + threatInfo: { + threatTypes: ['MALWARE', 'SOCIAL_ENGINEERING', 'UNWANTED_SOFTWARE', 'POTENTIALLY_HARMFUL_APPLICATION'], + platformTypes: ['ANY_PLATFORM'], + threatEntryTypes: ['URL'], + threatEntries: [{ url: userUrl }] + } + }; + + try { + const response = await fetchWithRetry( + `https://safebrowsing.googleapis.com/v4/threatMatches:find?key=${SAFE_BROWSING_KEY}`, + { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) } + ); + + if (!response.ok) { + const errText = await response.text(); + console.error(`[API ERROR] ${response.status}:`, errText); + return res.status(502).json({ error: `Google API error: ${response.status}`, detail: errText }); + } - console.log(`[SCAN] ${userUrl}`); + const data = await response.json(); + console.log(`[RESULT] Matches: ${data.matches ? data.matches.length : 0}`); + res.json(data); - const body = { - client: { clientId: 'cybershield', clientVersion: '2.0' }, - threatInfo: { - threatTypes: ['MALWARE', 'SOCIAL_ENGINEERING', 'UNWANTED_SOFTWARE', 'POTENTIALLY_HARMFUL_APPLICATION'], - platformTypes: ['ANY_PLATFORM'], - threatEntryTypes: ['URL'], - threatEntries: [{ url: userUrl }] + } catch (err) { + console.error('[FETCH ERROR]', err.message); + res.status(500).json({ error: 'Backend fetch failed', detail: err.message }); } - }; - - try { - const response = await fetchWithRetry( - `https://safebrowsing.googleapis.com/v4/threatMatches:find?key=${SAFE_BROWSING_KEY}`, - { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) } - ); - - if (!response.ok) { - const errText = await response.text(); - console.error(`[API ERROR] ${response.status}:`, errText); - return res.status(502).json({ error: `Google API error: ${response.status}`, detail: errText }); + }); + + // ─── Scam Text Detection (AI) ─── + app.post('/api/scam-detect', async (req, res) => { + console.log('[SCAM-DETECT] Request received:', req.body.message?.slice(0, 50)); + + const { message, history } = req.body; + if (!message) return res.status(400).json({ error: 'No message provided' }); + + if (!GEMINI_KEY) { + console.warn('[SCAM-DETECT] GEMINI_API_KEY not set β€” returning mock response'); + return res.json({ + classification: 'suspicious', + scamType: 'other', + confidence: 60, + explanation: 'AI analysis unavailable (GEMINI_API_KEY not configured). Based on pattern matching, this message contains elements that warrant caution.', + actionableAdvice: 'Do not share personal details or send money. Configure GEMINI_API_KEY in your .env file to enable full AI analysis.' + }); } - const data = await response.json(); - console.log(`[RESULT] Matches: ${data.matches ? data.matches.length : 0}`); - res.json(data); - - } catch (err) { - console.error('[FETCH ERROR]', err.message); - res.status(500).json({ error: 'Backend fetch failed', detail: err.message }); - } -}); - - - -// ─── Scam Text Detection (AI) ─── -app.post('/api/scam-detect', async (req, res) => { - console.log('[SCAM-DETECT] Request received:', req.body.message?.slice(0, 50)); // ← add this - - const { message, history } = req.body; - if (!message) return res.status(400).json({ error: 'No message provided' }); - - if (!GEMINI_KEY) { - // Fallback mock response when key not configured - console.warn('[SCAM-DETECT] GEMINI_API_KEY not set β€” returning mock response'); - return res.json({ - classification: 'suspicious', - scamType: 'other', - confidence: 60, - explanation: 'AI analysis unavailable (GEMINI_API_KEY not configured). Based on pattern matching, this message contains elements that warrant caution.', - actionableAdvice: 'Do not share personal details or send money. Configure GEMINI_API_KEY in your .env file to enable full AI analysis.' - }); - } - - const systemPrompt = `You are CyberShield, an expert scam and fraud detection AI. Analyze the provided message and return ONLY a valid JSON object (no markdown, no code fences) with exactly these fields: + const systemPrompt = `You are CyberShield, an expert scam and fraud detection AI. Analyze the provided message and return ONLY a valid JSON object (no markdown, no code fences) with exactly these fields: { "classification": "safe" | "suspicious" | "danger", "scamType": "phishing" | "fake_job" | "otp_fraud" | "investment_scam" | "impersonation" | "other" | "none", @@ -137,51 +188,63 @@ app.post('/api/scam-detect', async (req, res) => { "actionableAdvice": "<1-2 sentence concrete advice for the user>" }`; - const conversationMessages = []; - if (history && Array.isArray(history)) { - history.forEach(turn => { - if (turn.role === 'user') conversationMessages.push({ role: 'user', parts: [{ text: turn.text }] }); - if (turn.role === 'model') conversationMessages.push({ role: 'model', parts: [{ text: turn.text }] }); - }); - } - conversationMessages.push({ role: 'user', parts: [{ text: `Analyze this message:\n\n${message}` }] }); - - try { - const response = await fetchWithRetry( - `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${GEMINI_KEY}`, - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - systemInstruction: { parts: [{ text: systemPrompt }] }, - contents: conversationMessages, - generationConfig: { temperature: 0.1, maxOutputTokens: 1024 } - }) + const conversationMessages = []; + if (history && Array.isArray(history)) { + history.forEach(turn => { + if (turn.role === 'user') conversationMessages.push({ role: 'user', parts: [{ text: turn.text }] }); + if (turn.role === 'model') conversationMessages.push({ role: 'model', parts: [{ text: turn.text }] }); + }); + } + conversationMessages.push({ role: 'user', parts: [{ text: `Analyze this message:\n\n${message}` }] }); + + try { + const response = await fetchWithRetry( + `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${GEMINI_KEY}`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + systemInstruction: { parts: [{ text: systemPrompt }] }, + contents: conversationMessages, + generationConfig: { temperature: 0.1, maxOutputTokens: 1024 } + }) + } + ); + + if (!response.ok) { + const errText = await response.text(); + console.error('[GEMINI ERROR]', response.status, errText); + return res.status(502).json({ error: 'Gemini API error', detail: errText }); } - ); - if (!response.ok) { - const errText = await response.text(); - console.error('[GEMINI ERROR]', response.status, errText); // ← add this line - return res.status(502).json({ error: 'Gemini API error', detail: errText }); + const data = await response.json(); + const rawText = data.candidates?.[0]?.content?.parts?.[0]?.text || '{}'; + const cleaned = rawText.replace(/```json|```/g, '').trim(); + const parsed = JSON.parse(cleaned); + res.json(parsed); + + } catch (err) { + console.error('[SCAM-DETECT ERROR]', err.message); + res.status(500).json({ error: 'Scam analysis failed', detail: err.message }); + } + }); + + return app; } - const data = await response.json(); - const rawText = data.candidates?.[0]?.content?.parts?.[0]?.text || '{}'; - const cleaned = rawText.replace(/```json|```/g, '').trim(); - const parsed = JSON.parse(cleaned); - res.json(parsed); +if (require.main === module) { + const app = createApp(); + const PORT = process.env.PORT || 3000; - } catch (err) { - console.error('[SCAM-DETECT ERROR]', err.message); - res.status(500).json({ error: 'Scam analysis failed', detail: err.message }); - } -}); - -// ─── Start ─── -app.listen(PORT, () => { - console.log(`\nπŸ›‘οΈ CyberShield Backend v2.0`); - console.log(`πŸš€ http://localhost:${PORT}`); - console.log(`πŸ“‘ POST /check β€” URL scan`); - console.log(`πŸ€– POST /api/scam-detect β€” AI scam detection\n`); -}); + console.log("API_KEY:", process.env.API_KEY); + console.log("GEMINI_API_KEY:", process.env.GEMINI_API_KEY); + + app.listen(PORT, () => { + console.log(`\nπŸ›‘οΈ CyberShield Backend v2.0`); + console.log(`πŸš€ http://localhost:${PORT}`); + console.log(`πŸ“‘ POST /check β€” URL scan`); + console.log(`πŸ€– POST /api/scam-detect β€” AI scam detection\n`); + }); +} + +module.exports = { createApp }; diff --git a/test-rate-limit.js b/test-rate-limit.js new file mode 100644 index 0000000..79a6a88 --- /dev/null +++ b/test-rate-limit.js @@ -0,0 +1,33 @@ +// Simple rate limit tester β€” sends a burst of POST /check to local server +const fetch = require('node-fetch'); + +const TARGET = process.env.TARGET || 'http://localhost:3000/check'; +const TOTAL = parseInt(process.env.TOTAL || '120', 10); +const DELAY_MS = parseInt(process.env.DELAY_MS || '80', 10); // inter-request delay + +async function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } + +async function run() { + console.log(`Sending ${TOTAL} POSTs to ${TARGET} (delay ${DELAY_MS}ms)`); + let got429 = 0; + for (let i = 1; i <= TOTAL; i++) { + try { + const res = await fetch(TARGET, { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url: 'https://example.com' }) + }); + const txt = await res.text(); + console.log(`#${i} -> ${res.status}${res.status===429 ? ' (RATE LIMITED)' : ''}`); + if (res.status === 429) { + got429++; + try { console.log('Body:', JSON.parse(txt)); } catch (e) { console.log('Body:', txt); } + } + } catch (err) { + console.error(`#${i} ERROR`, err.message); + } + await sleep(DELAY_MS); + } + console.log(`Done. Observed ${got429} rate-limited responses.`); +} + +run().catch(e => { console.error('Test failed', e); process.exit(1); });