fix
This commit is contained in:
31
index.html
31
index.html
@@ -335,6 +335,14 @@
|
|||||||
<textarea id="sqlEditor" class="flex-1 bg-slate-900 text-slate-300 p-4 font-mono text-sm resize-none outline-none" placeholder="-- Введите SQL запрос здесь
|
<textarea id="sqlEditor" class="flex-1 bg-slate-900 text-slate-300 p-4 font-mono text-sm resize-none outline-none" placeholder="-- Введите SQL запрос здесь
|
||||||
SELECT * FROM users LIMIT 10;"></textarea>
|
SELECT * FROM users LIMIT 10;"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<button onclick="app.applySQLTemplate('select')" class="px-3 py-2 bg-slate-800 text-slate-200 rounded-lg text-xs">SELECT *</button>
|
||||||
|
<button onclick="app.applySQLTemplate('count')" class="px-3 py-2 bg-slate-800 text-slate-200 rounded-lg text-xs">COUNT rows</button>
|
||||||
|
<button onclick="app.applySQLTemplate('insert')" class="px-3 py-2 bg-slate-800 text-slate-200 rounded-lg text-xs">INSERT row</button>
|
||||||
|
<button onclick="app.applySQLTemplate('update')" class="px-3 py-2 bg-slate-800 text-slate-200 rounded-lg text-xs">UPDATE row</button>
|
||||||
|
<button onclick="app.applySQLTemplate('delete')" class="px-3 py-2 bg-slate-800 text-slate-200 rounded-lg text-xs">DELETE row</button>
|
||||||
|
<button onclick="app.applySQLTemplate('schema')" class="px-3 py-2 bg-slate-800 text-slate-200 rounded-lg text-xs">Describe table</button>
|
||||||
|
</div>
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
<button onclick="app.executeSQL()" class="flex items-center gap-2 px-6 py-2.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-all shadow-lg shadow-blue-600/20">
|
<button onclick="app.executeSQL()" class="flex items-center gap-2 px-6 py-2.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-all shadow-lg shadow-blue-600/20">
|
||||||
<i data-lucide="play" class="w-4 h-4"></i>
|
<i data-lucide="play" class="w-4 h-4"></i>
|
||||||
@@ -1532,7 +1540,7 @@ SELECT * FROM users LIMIT 10;"></textarea>
|
|||||||
|
|
||||||
fetch('/api/query', {
|
fetch('/api/query', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json', 'X-Request-Source': 'WEB' },
|
||||||
body: JSON.stringify({ sql })
|
body: JSON.stringify({ sql })
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
@@ -1587,6 +1595,20 @@ SELECT * FROM users LIMIT 10;"></textarea>
|
|||||||
editor.value = sql.trim();
|
editor.value = sql.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applySQLTemplate(type) {
|
||||||
|
const table = this.currentTable || 'your_table';
|
||||||
|
const templates = {
|
||||||
|
select: `SELECT *\nFROM "${table}"\nLIMIT 50;`,
|
||||||
|
count: `SELECT COUNT(*) AS total\nFROM "${table}";`,
|
||||||
|
insert: `INSERT INTO "${table}" ("column_name")\nVALUES ('value')\nRETURNING *;`,
|
||||||
|
update: `UPDATE "${table}"\nSET "column_name" = 'value'\nWHERE "id" = 'your-id'\nRETURNING *;`,
|
||||||
|
delete: `DELETE FROM "${table}"\nWHERE "id" = 'your-id'\nRETURNING *;`,
|
||||||
|
schema: `SELECT column_name, data_type, is_nullable, column_default\nFROM information_schema.columns\nWHERE table_schema = 'public' AND table_name = '${table}'\nORDER BY ordinal_position;`,
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('sqlEditor').value = templates[type] || '';
|
||||||
|
}
|
||||||
|
|
||||||
clearSQL() {
|
clearSQL() {
|
||||||
document.getElementById('sqlEditor').value = '';
|
document.getElementById('sqlEditor').value = '';
|
||||||
document.getElementById('sqlResults').classList.add('hidden');
|
document.getElementById('sqlResults').classList.add('hidden');
|
||||||
@@ -1801,7 +1823,7 @@ SELECT * FROM users LIMIT 10;"></textarea>
|
|||||||
disabled: document.getElementById('userDisabled').checked,
|
disabled: document.getElementById('userDisabled').checked,
|
||||||
access: {
|
access: {
|
||||||
view: { folders: this.getCheckedValues('userViewFoldersList'), tables: [] },
|
view: { folders: this.getCheckedValues('userViewFoldersList'), tables: [] },
|
||||||
create: { folders: [], tables: [] },
|
create: { folders: this.getCheckedValues('userViewFoldersList'), tables: [] },
|
||||||
edit: { folders: [], tables: this.getCheckedValues('userEditTablesList') },
|
edit: { folders: [], tables: this.getCheckedValues('userEditTablesList') },
|
||||||
delete: { folders: this.getCheckedValues('userDeleteFoldersList'), tables: [] },
|
delete: { folders: this.getCheckedValues('userDeleteFoldersList'), tables: [] },
|
||||||
},
|
},
|
||||||
@@ -1897,10 +1919,11 @@ SELECT * FROM users LIMIT 10;"></textarea>
|
|||||||
? entries.map(entry => `
|
? entries.map(entry => `
|
||||||
<div class="border border-slate-200 rounded-xl p-4">
|
<div class="border border-slate-200 rounded-xl p-4">
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex items-center justify-between gap-4">
|
||||||
<div class="font-medium text-slate-800">${entry.event}</div>
|
<div class="font-medium text-slate-800">${entry.summary || entry.event}</div>
|
||||||
<div class="text-xs text-slate-500">${entry.timestamp}</div>
|
<div class="text-xs text-slate-500">${entry.timestamp}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-slate-600 mt-1">Actor: ${entry.actor}</div>
|
<div class="text-sm text-slate-600 mt-1">Who: ${entry.actor} · Source: ${entry.source || 'WEB'}</div>
|
||||||
|
<div class="text-xs text-slate-500 mt-2">${entry.event}</div>
|
||||||
<pre class="mt-3 text-xs bg-slate-50 rounded-lg p-3 overflow-auto">${JSON.stringify(entry.details, null, 2)}</pre>
|
<pre class="mt-3 text-xs bg-slate-50 rounded-lg p-3 overflow-auto">${JSON.stringify(entry.details, null, 2)}</pre>
|
||||||
</div>
|
</div>
|
||||||
`).join('')
|
`).join('')
|
||||||
|
|||||||
59
server.js
59
server.js
@@ -348,15 +348,46 @@ function writeUsersConfig(users) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function appendAudit(event, actor, details = {}) {
|
function appendAudit(event, actor, details = {}) {
|
||||||
|
const source = details.source || 'WEB';
|
||||||
const entry = {
|
const entry = {
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
event,
|
event,
|
||||||
actor: actor || 'system',
|
actor: actor || 'system',
|
||||||
|
source,
|
||||||
|
summary: formatAuditSummary(event, details),
|
||||||
details,
|
details,
|
||||||
};
|
};
|
||||||
fs.appendFileSync(AUDIT_LOG_FILE, `${JSON.stringify(entry)}\n`, 'utf8');
|
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) {
|
function readAuditLog(limit = 200) {
|
||||||
if (!fs.existsSync(AUDIT_LOG_FILE)) {
|
if (!fs.existsSync(AUDIT_LOG_FILE)) {
|
||||||
return [];
|
return [];
|
||||||
@@ -566,7 +597,7 @@ app.post('/api/login', async (req, res) => {
|
|||||||
const sessionUser = createSessionUser(user);
|
const sessionUser = createSessionUser(user);
|
||||||
req.session.user = sessionUser;
|
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({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
username: sessionUser.username,
|
username: sessionUser.username,
|
||||||
@@ -595,7 +626,7 @@ app.post('/api/login', async (req, res) => {
|
|||||||
const sessionUser = createSessionUser({ username, role: 'superadmin', folders: null });
|
const sessionUser = createSessionUser({ username, role: 'superadmin', folders: null });
|
||||||
req.session.user = sessionUser;
|
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({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
username: sessionUser.username,
|
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({
|
return res.status(401).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Invalid credentials'
|
error: 'Invalid credentials'
|
||||||
@@ -629,7 +660,7 @@ app.post('/api/login', async (req, res) => {
|
|||||||
app.post('/api/logout', (req, res) => {
|
app.post('/api/logout', (req, res) => {
|
||||||
const actor = req.session?.user?.username;
|
const actor = req.session?.user?.username;
|
||||||
req.session.destroy(() => {
|
req.session.destroy(() => {
|
||||||
appendAudit('logout', actor, {});
|
appendAudit('logout', actor, { source: 'WEB' });
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -682,7 +713,7 @@ app.post('/api/users', requireAuth, requirePermission(
|
|||||||
disabled: payload.disabled,
|
disabled: payload.disabled,
|
||||||
});
|
});
|
||||||
writeUsersConfig(config.users);
|
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]) });
|
res.json({ success: true, user: sanitizeUser(config.users[config.users.length - 1]) });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(400).json({ success: false, error: err.message });
|
res.status(400).json({ success: false, error: err.message });
|
||||||
@@ -711,7 +742,7 @@ app.put('/api/users/:username', requireAuth, requirePermission(
|
|||||||
}
|
}
|
||||||
|
|
||||||
writeUsersConfig(config.users);
|
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) });
|
res.json({ success: true, user: sanitizeUser(user) });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(400).json({ success: false, error: err.message });
|
res.status(400).json({ success: false, error: err.message });
|
||||||
@@ -729,7 +760,7 @@ app.delete('/api/users/:username', requireAuth, requirePermission(
|
|||||||
}
|
}
|
||||||
|
|
||||||
writeUsersConfig(nextUsers);
|
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 });
|
res.json({ success: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -895,7 +926,7 @@ app.post('/api/tables', requireAuth, requirePermission((permissions, req) => {
|
|||||||
}).join(', ');
|
}).join(', ');
|
||||||
|
|
||||||
await pool.query(`CREATE TABLE ${quoteIdentifier(name)} (${columnsSQL})`);
|
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' });
|
res.json({ success: true, message: 'Table created' });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(500).json({ success: false, error: err.message });
|
res.status(500).json({ success: false, error: err.message });
|
||||||
@@ -908,7 +939,7 @@ app.delete('/api/tables/:tableName', requireAuth, requireTableAccess, requirePer
|
|||||||
), async (req, res) => {
|
), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
await pool.query(`DROP TABLE IF EXISTS ${quoteIdentifier(req.params.tableName)}`);
|
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' });
|
res.json({ success: true, message: 'Table deleted' });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(500).json({ success: false, error: err.message });
|
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;
|
const nextName = cleanFolder ? `${cleanFolder}__${cleanName}` : cleanName;
|
||||||
try {
|
try {
|
||||||
await pool.query(`ALTER TABLE ${quoteIdentifier(tableName)} RENAME TO ${quoteIdentifier(nextName)}`);
|
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 });
|
res.json({ success: true, name: nextName });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(500).json({ success: false, error: err.message });
|
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 placeholders = values.map((_, i) => `$${i + 1}`).join(', ');
|
||||||
const sql = `INSERT INTO ${quoteIdentifier(tableName)} (${columns.map(quoteIdentifier).join(', ')}) VALUES (${placeholders}) RETURNING *`;
|
const sql = `INSERT INTO ${quoteIdentifier(tableName)} (${columns.map(quoteIdentifier).join(', ')}) VALUES (${placeholders}) RETURNING *`;
|
||||||
const result = await pool.query(sql, values);
|
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] });
|
res.json({ success: true, data: result.rows[0] });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(500).json({ success: false, error: err.message });
|
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);
|
: 'ctid::text = $' + (values.length + 1);
|
||||||
const sql = `UPDATE ${quoteIdentifier(tableName)} SET ${setClause} WHERE ${whereClause} RETURNING *`;
|
const sql = `UPDATE ${quoteIdentifier(tableName)} SET ${setClause} WHERE ${whereClause} RETURNING *`;
|
||||||
const result = await pool.query(sql, [...values, pk]);
|
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] });
|
res.json({ success: true, data: result.rows[0] });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(500).json({ success: false, error: err.message });
|
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`
|
? `${quoteIdentifier(primaryKey)} = $1`
|
||||||
: 'ctid::text = $1';
|
: 'ctid::text = $1';
|
||||||
await pool.query(`DELETE FROM ${quoteIdentifier(tableName)} WHERE ${whereClause}`, [pk]);
|
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 });
|
res.json({ success: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(500).json({ success: false, error: err.message });
|
res.status(500).json({ success: false, error: err.message });
|
||||||
@@ -1124,7 +1155,7 @@ app.post('/api/query', requireAuth, requirePermission(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await pool.query(sql);
|
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({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
rows: result.rows,
|
rows: result.rows,
|
||||||
|
|||||||
Reference in New Issue
Block a user