This commit is contained in:
2026-03-20 15:45:24 +07:00
parent 1ee888e929
commit d969ac594e
2 changed files with 72 additions and 18 deletions

View File

@@ -348,15 +348,46 @@ function writeUsersConfig(users) {
}
function appendAudit(event, actor, details = {}) {
const source = details.source || 'WEB';
const entry = {
timestamp: new Date().toISOString(),
event,
actor: actor || 'system',
source,
summary: formatAuditSummary(event, details),
details,
};
fs.appendFileSync(AUDIT_LOG_FILE, `${JSON.stringify(entry)}\n`, 'utf8');
}
function formatAuditSummary(event, details = {}) {
const summaries = {
'login.success': `successful login`,
'login.failed': `failed login attempt`,
'logout': `logout`,
'user.created': `created user ${details.username || ''}`.trim(),
'user.updated': `updated user ${details.username || ''}`.trim(),
'user.deleted': `deleted user ${details.username || ''}`.trim(),
'table.created': `created table ${details.table || ''}`.trim(),
'table.deleted': `deleted table ${details.table || ''}`.trim(),
'table.moved': `moved table ${details.from || ''} to ${details.to || ''}`.trim(),
'record.created': `created record in ${details.table || ''}`.trim(),
'record.updated': `updated record in ${details.table || ''}`.trim(),
'record.deleted': `deleted record from ${details.table || ''}`.trim(),
'sql.executed': `executed ${details.command || 'SQL'} query`,
};
return summaries[event] || event;
}
function getAuditSource(req, fallback = 'WEB') {
const header = String(req?.headers?.['x-request-source'] || '').trim().toUpperCase();
if (header === 'AI' || header === 'WEB') {
return header;
}
return fallback;
}
function readAuditLog(limit = 200) {
if (!fs.existsSync(AUDIT_LOG_FILE)) {
return [];
@@ -566,7 +597,7 @@ app.post('/api/login', async (req, res) => {
const sessionUser = createSessionUser(user);
req.session.user = sessionUser;
appendAudit('login.success', sessionUser.username, { role: sessionUser.role });
appendAudit('login.success', sessionUser.username, { role: sessionUser.role, source: getAuditSource(req) });
return res.json({
success: true,
username: sessionUser.username,
@@ -595,7 +626,7 @@ app.post('/api/login', async (req, res) => {
const sessionUser = createSessionUser({ username, role: 'superadmin', folders: null });
req.session.user = sessionUser;
appendAudit('login.success', sessionUser.username, { role: sessionUser.role, source: 'env' });
appendAudit('login.success', sessionUser.username, { role: sessionUser.role, source: getAuditSource(req) });
return res.json({
success: true,
username: sessionUser.username,
@@ -618,7 +649,7 @@ app.post('/api/login', async (req, res) => {
}
}
appendAudit('login.failed', username, {});
appendAudit('login.failed', username, { source: getAuditSource(req) });
return res.status(401).json({
success: false,
error: 'Invalid credentials'
@@ -629,7 +660,7 @@ app.post('/api/login', async (req, res) => {
app.post('/api/logout', (req, res) => {
const actor = req.session?.user?.username;
req.session.destroy(() => {
appendAudit('logout', actor, {});
appendAudit('logout', actor, { source: 'WEB' });
res.json({ success: true });
});
});
@@ -682,7 +713,7 @@ app.post('/api/users', requireAuth, requirePermission(
disabled: payload.disabled,
});
writeUsersConfig(config.users);
appendAudit('user.created', req.currentUser.username, { username: payload.username, role: payload.role });
appendAudit('user.created', req.currentUser.username, { username: payload.username, role: payload.role, source: getAuditSource(req) });
res.json({ success: true, user: sanitizeUser(config.users[config.users.length - 1]) });
} catch (err) {
res.status(400).json({ success: false, error: err.message });
@@ -711,7 +742,7 @@ app.put('/api/users/:username', requireAuth, requirePermission(
}
writeUsersConfig(config.users);
appendAudit('user.updated', req.currentUser.username, { username: user.username, role: user.role });
appendAudit('user.updated', req.currentUser.username, { username: user.username, role: user.role, source: getAuditSource(req) });
res.json({ success: true, user: sanitizeUser(user) });
} catch (err) {
res.status(400).json({ success: false, error: err.message });
@@ -729,7 +760,7 @@ app.delete('/api/users/:username', requireAuth, requirePermission(
}
writeUsersConfig(nextUsers);
appendAudit('user.deleted', req.currentUser.username, { username: req.params.username });
appendAudit('user.deleted', req.currentUser.username, { username: req.params.username, source: getAuditSource(req) });
res.json({ success: true });
});
@@ -895,7 +926,7 @@ app.post('/api/tables', requireAuth, requirePermission((permissions, req) => {
}).join(', ');
await pool.query(`CREATE TABLE ${quoteIdentifier(name)} (${columnsSQL})`);
appendAudit('table.created', req.currentUser.username, { table: name });
appendAudit('table.created', req.currentUser.username, { table: name, source: getAuditSource(req) });
res.json({ success: true, message: 'Table created' });
} catch (err) {
res.status(500).json({ success: false, error: err.message });
@@ -908,7 +939,7 @@ app.delete('/api/tables/:tableName', requireAuth, requireTableAccess, requirePer
), async (req, res) => {
try {
await pool.query(`DROP TABLE IF EXISTS ${quoteIdentifier(req.params.tableName)}`);
appendAudit('table.deleted', req.currentUser.username, { table: req.params.tableName });
appendAudit('table.deleted', req.currentUser.username, { table: req.params.tableName, source: getAuditSource(req) });
res.json({ success: true, message: 'Table deleted' });
} catch (err) {
res.status(500).json({ success: false, error: err.message });
@@ -931,7 +962,7 @@ app.post('/api/tables/:tableName/move', requireAuth, requireTableAccess, require
const nextName = cleanFolder ? `${cleanFolder}__${cleanName}` : cleanName;
try {
await pool.query(`ALTER TABLE ${quoteIdentifier(tableName)} RENAME TO ${quoteIdentifier(nextName)}`);
appendAudit('table.moved', req.currentUser.username, { from: tableName, to: nextName, folder: cleanFolder || 'default' });
appendAudit('table.moved', req.currentUser.username, { from: tableName, to: nextName, folder: cleanFolder || 'default', source: getAuditSource(req) });
res.json({ success: true, name: nextName });
} catch (err) {
res.status(500).json({ success: false, error: err.message });
@@ -976,7 +1007,7 @@ app.post('/api/tables/:tableName/records', requireAuth, requireTableAccess, requ
const placeholders = values.map((_, i) => `$${i + 1}`).join(', ');
const sql = `INSERT INTO ${quoteIdentifier(tableName)} (${columns.map(quoteIdentifier).join(', ')}) VALUES (${placeholders}) RETURNING *`;
const result = await pool.query(sql, values);
appendAudit('record.created', req.currentUser.username, { table: tableName });
appendAudit('record.created', req.currentUser.username, { table: tableName, source: getAuditSource(req) });
res.json({ success: true, data: result.rows[0] });
} catch (err) {
res.status(500).json({ success: false, error: err.message });
@@ -1004,7 +1035,7 @@ app.put('/api/tables/:tableName/records/:pk', requireAuth, requireTableAccess, r
: 'ctid::text = $' + (values.length + 1);
const sql = `UPDATE ${quoteIdentifier(tableName)} SET ${setClause} WHERE ${whereClause} RETURNING *`;
const result = await pool.query(sql, [...values, pk]);
appendAudit('record.updated', req.currentUser.username, { table: tableName, key: pk });
appendAudit('record.updated', req.currentUser.username, { table: tableName, key: pk, source: getAuditSource(req) });
res.json({ success: true, data: result.rows[0] });
} catch (err) {
res.status(500).json({ success: false, error: err.message });
@@ -1023,7 +1054,7 @@ app.delete('/api/tables/:tableName/records/:pk', requireAuth, requireTableAccess
? `${quoteIdentifier(primaryKey)} = $1`
: 'ctid::text = $1';
await pool.query(`DELETE FROM ${quoteIdentifier(tableName)} WHERE ${whereClause}`, [pk]);
appendAudit('record.deleted', req.currentUser.username, { table: tableName, key: pk });
appendAudit('record.deleted', req.currentUser.username, { table: tableName, key: pk, source: getAuditSource(req) });
res.json({ success: true });
} catch (err) {
res.status(500).json({ success: false, error: err.message });
@@ -1124,7 +1155,7 @@ app.post('/api/query', requireAuth, requirePermission(
try {
const result = await pool.query(sql);
appendAudit('sql.executed', req.currentUser.username, { command: result.command, sql: sql.slice(0, 400) });
appendAudit('sql.executed', req.currentUser.username, { command: result.command, sql: sql.slice(0, 400), source: getAuditSource(req, 'WEB') });
res.json({
success: true,
rows: result.rows,