Implement PostgreSQL history with user isolation

This commit is contained in:
2025-12-28 00:13:43 +11:00
parent d83a93da7e
commit 5ae3b0d036
6 changed files with 130 additions and 21 deletions

1
.gitignore vendored
View File

@@ -29,6 +29,7 @@ config.json
config*.json
settings.json
settings*.json
lib/db-config.js
# ===========================================
# SSH & AUTHENTICATION

33
lib/db.js Normal file
View File

@@ -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
};

View File

@@ -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 = {

27
main.js
View File

@@ -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();

View File

@@ -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": {

View File

@@ -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);
});