From 5ae3b0d03660fa5e4cab785d46b953f907b97a3a Mon Sep 17 00:00:00 2001 From: DeNNiiInc Date: Sun, 28 Dec 2025 00:13:43 +1100 Subject: [PATCH] Implement PostgreSQL history with user isolation --- .gitignore | 1 + lib/db.js | 33 ++++++++++++++++++++++ lib/runner.js | 76 ++++++++++++++++++++++++++++++++++++++++----------- main.js | 27 ++++++++++++++++-- package.json | 1 + server.js | 13 +++++++-- 6 files changed, 130 insertions(+), 21 deletions(-) create mode 100644 lib/db.js diff --git a/.gitignore b/.gitignore index ce8c6ca..4ca65c5 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ config.json config*.json settings.json settings*.json +lib/db-config.js # =========================================== # SSH & AUTHENTICATION diff --git a/lib/db.js b/lib/db.js new file mode 100644 index 0000000..db88ca0 --- /dev/null +++ b/lib/db.js @@ -0,0 +1,33 @@ +const { Pool } = require('pg'); +const dbConfig = require('./db-config'); + +const pool = new Pool(dbConfig); + +async function initSchema() { + const client = await pool.connect(); + try { + const query = ` + CREATE TABLE IF NOT EXISTS test_results ( + id UUID PRIMARY KEY, + url TEXT NOT NULL, + timestamp TIMESTAMPTZ NOT NULL, + is_mobile BOOLEAN NOT NULL, + scores JSONB NOT NULL, + metrics JSONB NOT NULL, + user_uuid TEXT NOT NULL, + user_ip TEXT NOT NULL + ); + `; + await client.query(query); + console.log("Schema initialized: test_results table ready."); + } catch (err) { + console.error("Error initializing schema:", err); + } finally { + client.release(); + } +} + +module.exports = { + pool, + initSchema +}; diff --git a/lib/runner.js b/lib/runner.js index d6d7a01..eab325d 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -81,12 +81,40 @@ async function runTest(url, options = {}) { isMobile: isMobile }; - // Save JSON Summary + // Save JSON Summary const jsonPath = path.join(reportDir, `${testId}.json`); fs.writeFileSync(jsonPath, JSON.stringify(summary, null, 2)); await chrome.kill(); + // Insert into Database + // We expect user_uuid and user_ip to be passed in options, or handle gracefully if not + const userUuid = options.userUuid || 'anonymous'; + const userIp = options.userIp || '0.0.0.0'; + + const insertQuery = ` + INSERT INTO test_results (id, url, timestamp, is_mobile, scores, metrics, user_uuid, user_ip) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + `; + const values = [ + testId, + summary.url, + summary.timestamp, + isMobile, + summary.scores, + summary.metrics, + userUuid, + userIp + ]; + + try { + const db = require('../lib/db'); + await db.pool.query(insertQuery, values); + } catch (dbErr) { + console.error('Failed to save result to DB:', dbErr); + // Don't fail the whole test if DB save fails, just log it + } + return summary; } catch (error) { @@ -96,23 +124,39 @@ async function runTest(url, options = {}) { } /** - * Get list of all test results + * Get list of all test results (Scoped to User) + * @param {string} userUuid - User's UUID from client + * @param {string} userIp - User's IP address */ -function getHistory() { - const reportDir = path.join(__dirname, '..', 'reports'); - if (!fs.existsSync(reportDir)) return []; +async function getHistory(userUuid, userIp) { + const db = require('../lib/db'); + + // If no identifiers provided, return empty or limit to anonymous? + // For strict isolation, we require at least one. + if (!userUuid && !userIp) return []; - const files = fs.readdirSync(reportDir).filter(f => f.endsWith('.json')); - const history = files.map(file => { - try { - return JSON.parse(fs.readFileSync(path.join(reportDir, file), 'utf8')); - } catch (e) { - return null; - } - }).filter(Boolean); - - // Sort by newest first - return history.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); + try { + const query = ` + SELECT * FROM test_results + WHERE user_uuid = $1 AND user_ip = $2 + ORDER BY timestamp DESC + LIMIT 50 + `; + const res = await db.pool.query(query, [userUuid, userIp]); + + // Convert DB rows back to simplified history objects + return res.rows.map(row => ({ + id: row.id, + url: row.url, + timestamp: row.timestamp, // JS Date + isMobile: row.is_mobile, + scores: row.scores, + metrics: row.metrics + })); + } catch (err) { + console.error('Error fetching history from DB:', err); + return []; + } } module.exports = { diff --git a/main.js b/main.js index e7c88e9..b40e349 100644 --- a/main.js +++ b/main.js @@ -61,7 +61,10 @@ async function runTest() { try { const response = await fetch('/api/run-test', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { + 'Content-Type': 'application/json', + 'x-user-uuid': getUserUuid() + }, body: JSON.stringify({ url: url, isMobile: currentDevice === 'mobile' @@ -131,7 +134,11 @@ function showError(msg) { async function loadHistory() { try { - const response = await fetch('/api/history'); + const response = await fetch('/api/history', { + headers: { + 'x-user-uuid': getUserUuid() + } + }); const history = await response.json(); const container = document.getElementById('history-list'); @@ -209,8 +216,24 @@ async function updateVersionBadge() { } } +// ============================================================================ +// Identity Management +// ============================================================================ +function getUserUuid() { + let uuid = localStorage.getItem('user_uuid'); + if (!uuid) { + uuid = crypto.randomUUID(); + localStorage.setItem('user_uuid', uuid); + } + return uuid; +} + // Initialization document.addEventListener('DOMContentLoaded', () => { + // Ensure we have an identity + const userUuid = getUserUuid(); + console.log('User Identity:', userUuid); + updateVersionBadge(); loadHistory(); diff --git a/package.json b/package.json index 727949c..f6d1500 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "chrome-launcher": "^1.2.1", "express": "^4.18.2", "lighthouse": "^13.0.1", + "pg": "^8.16.3", "uuid": "^13.0.0" }, "devDependencies": { diff --git a/server.js b/server.js index e61a57c..3e9de17 100644 --- a/server.js +++ b/server.js @@ -38,10 +38,14 @@ app.get("/api/git-info", (req, res) => { // API Endpoint: Run Test app.post("/api/run-test", async (req, res) => { const { url, isMobile } = req.body; + const userUuid = req.headers['x-user-uuid']; + // Use header for IP if behind proxy, fallback to socket address + const userIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress; + if (!url) return res.status(400).json({ error: "URL is required" }); try { - const result = await runner.runTest(url, { isMobile }); + const result = await runner.runTest(url, { isMobile, userUuid, userIp }); res.json(result); } catch (error) { console.error("Test failed:", error); @@ -50,8 +54,11 @@ app.post("/api/run-test", async (req, res) => { }); // API Endpoint: History -app.get("/api/history", (req, res) => { - const history = runner.getHistory(); +app.get("/api/history", async (req, res) => { + const userUuid = req.headers['x-user-uuid']; + const userIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress; + + const history = await runner.getHistory(userUuid, userIp); res.json(history); });