Add product editor audit log, fix bug that would overwrite editor fields if edited too soon after load, add audit log ui
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
-- Migration: Create product_editor_audit_log table
|
||||
-- Permanent audit trail of all product editor API submissions
|
||||
-- Run this against your PostgreSQL database
|
||||
|
||||
CREATE TABLE IF NOT EXISTS product_editor_audit_log (
|
||||
id SERIAL PRIMARY KEY,
|
||||
|
||||
-- Who made the edit
|
||||
user_id INTEGER NOT NULL,
|
||||
username VARCHAR(255),
|
||||
|
||||
-- Which product
|
||||
pid INTEGER NOT NULL,
|
||||
|
||||
-- What was submitted
|
||||
action VARCHAR(50) NOT NULL, -- 'product_edit', 'image_changes', 'taxonomy_set'
|
||||
request_payload JSONB NOT NULL, -- The exact payload sent to the external API
|
||||
target_endpoint VARCHAR(255), -- The API URL that was called
|
||||
|
||||
-- What came back
|
||||
success BOOLEAN NOT NULL,
|
||||
response_payload JSONB, -- Full API response
|
||||
error_message TEXT, -- Extracted error message on failure
|
||||
|
||||
-- Metadata
|
||||
duration_ms INTEGER, -- How long the API call took
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Index for looking up edits by product
|
||||
CREATE INDEX IF NOT EXISTS idx_pe_audit_log_pid
|
||||
ON product_editor_audit_log (pid);
|
||||
|
||||
-- Index for looking up edits by user
|
||||
CREATE INDEX IF NOT EXISTS idx_pe_audit_log_user_id
|
||||
ON product_editor_audit_log (user_id);
|
||||
|
||||
-- Index for time-based queries
|
||||
CREATE INDEX IF NOT EXISTS idx_pe_audit_log_created_at
|
||||
ON product_editor_audit_log (created_at DESC);
|
||||
|
||||
-- Composite index for product + time queries
|
||||
CREATE INDEX IF NOT EXISTS idx_pe_audit_log_pid_created
|
||||
ON product_editor_audit_log (pid, created_at DESC);
|
||||
|
||||
-- Composite index for user + time queries
|
||||
CREATE INDEX IF NOT EXISTS idx_pe_audit_log_user_created
|
||||
ON product_editor_audit_log (user_id, created_at DESC);
|
||||
|
||||
COMMENT ON TABLE product_editor_audit_log IS 'Permanent audit log of all product editor API submissions';
|
||||
COMMENT ON COLUMN product_editor_audit_log.action IS 'Type of edit: product_edit, image_changes, or taxonomy_set';
|
||||
COMMENT ON COLUMN product_editor_audit_log.request_payload IS 'Exact payload sent to the external API';
|
||||
COMMENT ON COLUMN product_editor_audit_log.response_payload IS 'Full response received from the external API';
|
||||
COMMENT ON COLUMN product_editor_audit_log.duration_ms IS 'Round-trip time of the API call in milliseconds';
|
||||
193
inventory-server/src/routes/product-editor-audit-log.js
Normal file
193
inventory-server/src/routes/product-editor-audit-log.js
Normal file
@@ -0,0 +1,193 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
// Create a new audit log entry
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
user_id,
|
||||
username,
|
||||
pid,
|
||||
action,
|
||||
request_payload,
|
||||
target_endpoint,
|
||||
success,
|
||||
response_payload,
|
||||
error_message,
|
||||
duration_ms,
|
||||
} = req.body;
|
||||
|
||||
// Validate required fields
|
||||
if (!user_id) {
|
||||
return res.status(400).json({ error: 'user_id is required' });
|
||||
}
|
||||
if (!pid) {
|
||||
return res.status(400).json({ error: 'pid is required' });
|
||||
}
|
||||
if (!action) {
|
||||
return res.status(400).json({ error: 'action is required' });
|
||||
}
|
||||
if (!request_payload) {
|
||||
return res.status(400).json({ error: 'request_payload is required' });
|
||||
}
|
||||
if (typeof success !== 'boolean') {
|
||||
return res.status(400).json({ error: 'success (boolean) is required' });
|
||||
}
|
||||
|
||||
const pool = req.app.locals.pool;
|
||||
if (!pool) {
|
||||
throw new Error('Database pool not initialized');
|
||||
}
|
||||
|
||||
const result = await pool.query(`
|
||||
INSERT INTO product_editor_audit_log (
|
||||
user_id,
|
||||
username,
|
||||
pid,
|
||||
action,
|
||||
request_payload,
|
||||
target_endpoint,
|
||||
success,
|
||||
response_payload,
|
||||
error_message,
|
||||
duration_ms
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
RETURNING id, created_at
|
||||
`, [
|
||||
user_id,
|
||||
username || null,
|
||||
pid,
|
||||
action,
|
||||
JSON.stringify(request_payload),
|
||||
target_endpoint || null,
|
||||
success,
|
||||
response_payload ? JSON.stringify(response_payload) : null,
|
||||
error_message || null,
|
||||
duration_ms || null,
|
||||
]);
|
||||
|
||||
res.status(201).json(result.rows[0]);
|
||||
} catch (error) {
|
||||
console.error('Error creating product editor audit log:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to create product editor audit log',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// List audit log entries (with pagination)
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const { user_id, pid, action, limit = 50, offset = 0, success: successFilter } = req.query;
|
||||
|
||||
const pool = req.app.locals.pool;
|
||||
if (!pool) {
|
||||
throw new Error('Database pool not initialized');
|
||||
}
|
||||
|
||||
const conditions = [];
|
||||
const params = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
if (user_id) {
|
||||
conditions.push(`user_id = $${paramIndex++}`);
|
||||
params.push(user_id);
|
||||
}
|
||||
|
||||
if (pid) {
|
||||
conditions.push(`pid = $${paramIndex++}`);
|
||||
params.push(pid);
|
||||
}
|
||||
|
||||
if (action) {
|
||||
conditions.push(`action = $${paramIndex++}`);
|
||||
params.push(action);
|
||||
}
|
||||
|
||||
if (successFilter !== undefined) {
|
||||
conditions.push(`success = $${paramIndex++}`);
|
||||
params.push(successFilter === 'true');
|
||||
}
|
||||
|
||||
const whereClause = conditions.length > 0
|
||||
? `WHERE ${conditions.join(' AND ')}`
|
||||
: '';
|
||||
|
||||
// Get total count
|
||||
const countResult = await pool.query(
|
||||
`SELECT COUNT(*) FROM product_editor_audit_log ${whereClause}`,
|
||||
params
|
||||
);
|
||||
|
||||
// Get paginated results (exclude large payload columns in list view)
|
||||
const dataParams = [...params, parseInt(limit, 10), parseInt(offset, 10)];
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
id,
|
||||
user_id,
|
||||
username,
|
||||
pid,
|
||||
action,
|
||||
target_endpoint,
|
||||
success,
|
||||
error_message,
|
||||
duration_ms,
|
||||
created_at
|
||||
FROM product_editor_audit_log
|
||||
${whereClause}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $${paramIndex++} OFFSET $${paramIndex++}
|
||||
`, dataParams);
|
||||
|
||||
res.json({
|
||||
total: parseInt(countResult.rows[0].count, 10),
|
||||
entries: result.rows,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching product editor audit log:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to fetch product editor audit log',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Get a single audit log entry (with full payloads)
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const pool = req.app.locals.pool;
|
||||
if (!pool) {
|
||||
throw new Error('Database pool not initialized');
|
||||
}
|
||||
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM product_editor_audit_log WHERE id = $1',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Audit log entry not found' });
|
||||
}
|
||||
|
||||
res.json(result.rows[0]);
|
||||
} catch (error) {
|
||||
console.error('Error fetching product editor audit log entry:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to fetch audit log entry',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Error handling middleware
|
||||
router.use((err, req, res, next) => {
|
||||
console.error('Product editor audit log route error:', err);
|
||||
res.status(500).json({
|
||||
error: 'Internal server error',
|
||||
details: err.message
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -25,6 +25,7 @@ const brandsAggregateRouter = require('./routes/brandsAggregate');
|
||||
const htsLookupRouter = require('./routes/hts-lookup');
|
||||
const importSessionsRouter = require('./routes/import-sessions');
|
||||
const importAuditLogRouter = require('./routes/import-audit-log');
|
||||
const productEditorAuditLogRouter = require('./routes/product-editor-audit-log');
|
||||
const newsletterRouter = require('./routes/newsletter');
|
||||
|
||||
// Get the absolute path to the .env file
|
||||
@@ -135,6 +136,7 @@ async function startServer() {
|
||||
app.use('/api/hts-lookup', htsLookupRouter);
|
||||
app.use('/api/import-sessions', importSessionsRouter);
|
||||
app.use('/api/import-audit-log', importAuditLogRouter);
|
||||
app.use('/api/product-editor-audit-log', productEditorAuditLogRouter);
|
||||
app.use('/api/newsletter', newsletterRouter);
|
||||
|
||||
// Basic health check route
|
||||
|
||||
Reference in New Issue
Block a user