mirror of
https://github.com/DeNNiiInc/Web-Page-Performance-Test.git
synced 2026-04-17 20:05:58 +00:00
Implement PostgreSQL history with user isolation
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -29,6 +29,7 @@ config.json
|
|||||||
config*.json
|
config*.json
|
||||||
settings.json
|
settings.json
|
||||||
settings*.json
|
settings*.json
|
||||||
|
lib/db-config.js
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# SSH & AUTHENTICATION
|
# SSH & AUTHENTICATION
|
||||||
|
|||||||
33
lib/db.js
Normal file
33
lib/db.js
Normal 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
|
||||||
|
};
|
||||||
@@ -87,6 +87,34 @@ async function runTest(url, options = {}) {
|
|||||||
|
|
||||||
await chrome.kill();
|
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;
|
return summary;
|
||||||
|
|
||||||
} catch (error) {
|
} 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() {
|
async function getHistory(userUuid, userIp) {
|
||||||
const reportDir = path.join(__dirname, '..', 'reports');
|
const db = require('../lib/db');
|
||||||
if (!fs.existsSync(reportDir)) return [];
|
|
||||||
|
// 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 {
|
try {
|
||||||
return JSON.parse(fs.readFileSync(path.join(reportDir, file), 'utf8'));
|
const query = `
|
||||||
} catch (e) {
|
SELECT * FROM test_results
|
||||||
return null;
|
WHERE user_uuid = $1 AND user_ip = $2
|
||||||
}
|
ORDER BY timestamp DESC
|
||||||
}).filter(Boolean);
|
LIMIT 50
|
||||||
|
`;
|
||||||
|
const res = await db.pool.query(query, [userUuid, userIp]);
|
||||||
|
|
||||||
// Sort by newest first
|
// Convert DB rows back to simplified history objects
|
||||||
return history.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
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 = {
|
module.exports = {
|
||||||
|
|||||||
27
main.js
27
main.js
@@ -61,7 +61,10 @@ async function runTest() {
|
|||||||
try {
|
try {
|
||||||
const response = await fetch('/api/run-test', {
|
const response = await fetch('/api/run-test', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-user-uuid': getUserUuid()
|
||||||
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
url: url,
|
url: url,
|
||||||
isMobile: currentDevice === 'mobile'
|
isMobile: currentDevice === 'mobile'
|
||||||
@@ -131,7 +134,11 @@ function showError(msg) {
|
|||||||
|
|
||||||
async function loadHistory() {
|
async function loadHistory() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/history');
|
const response = await fetch('/api/history', {
|
||||||
|
headers: {
|
||||||
|
'x-user-uuid': getUserUuid()
|
||||||
|
}
|
||||||
|
});
|
||||||
const history = await response.json();
|
const history = await response.json();
|
||||||
|
|
||||||
const container = document.getElementById('history-list');
|
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
|
// Initialization
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// Ensure we have an identity
|
||||||
|
const userUuid = getUserUuid();
|
||||||
|
console.log('User Identity:', userUuid);
|
||||||
|
|
||||||
updateVersionBadge();
|
updateVersionBadge();
|
||||||
loadHistory();
|
loadHistory();
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"chrome-launcher": "^1.2.1",
|
"chrome-launcher": "^1.2.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"lighthouse": "^13.0.1",
|
"lighthouse": "^13.0.1",
|
||||||
|
"pg": "^8.16.3",
|
||||||
"uuid": "^13.0.0"
|
"uuid": "^13.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
13
server.js
13
server.js
@@ -38,10 +38,14 @@ app.get("/api/git-info", (req, res) => {
|
|||||||
// API Endpoint: Run Test
|
// API Endpoint: Run Test
|
||||||
app.post("/api/run-test", async (req, res) => {
|
app.post("/api/run-test", async (req, res) => {
|
||||||
const { url, isMobile } = req.body;
|
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" });
|
if (!url) return res.status(400).json({ error: "URL is required" });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await runner.runTest(url, { isMobile });
|
const result = await runner.runTest(url, { isMobile, userUuid, userIp });
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Test failed:", error);
|
console.error("Test failed:", error);
|
||||||
@@ -50,8 +54,11 @@ app.post("/api/run-test", async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// API Endpoint: History
|
// API Endpoint: History
|
||||||
app.get("/api/history", (req, res) => {
|
app.get("/api/history", async (req, res) => {
|
||||||
const history = runner.getHistory();
|
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);
|
res.json(history);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user