ебать что это блять
This commit is contained in:
612
server.js
612
server.js
@@ -3,502 +3,48 @@ const express = require('express');
|
||||
const { Pool } = require('pg');
|
||||
const session = require('express-session');
|
||||
const cors = require('cors');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const http = require('http');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const crypto = require('crypto');
|
||||
|
||||
let usersConfig = { users: [] };
|
||||
try {
|
||||
usersConfig = JSON.parse(fs.readFileSync(path.join(__dirname, 'users.json'), 'utf8'));
|
||||
} catch (err) {
|
||||
console.warn('⚠️ users.json not found or invalid JSON. Falling back to env-based admin only.');
|
||||
}
|
||||
|
||||
const rolePermissions = {
|
||||
superadmin: { folders: null, canCreate: true, canEdit: true, canDelete: true },
|
||||
frontend_admin: { folders: ['frontend'], canCreate: true, canEdit: true, canDelete: true },
|
||||
backend_admin: { folders: ['backend'], canCreate: true, canEdit: true, canDelete: true },
|
||||
frontend_moder: { folders: ['frontend'], canCreate: true, canEdit: true, canDelete: false },
|
||||
backend_moder: { folders: ['backend'], canCreate: true, canEdit: true, canDelete: false },
|
||||
viewer: { folders: null, canCreate: false, canEdit: false, canDelete: false },
|
||||
};
|
||||
|
||||
function getUser(username) {
|
||||
return usersConfig.users.find(u => u.username === username);
|
||||
}
|
||||
|
||||
function getTableFolder(tableName) {
|
||||
if (!tableName) return 'default';
|
||||
const parts = tableName.split('__');
|
||||
return parts.length > 1 ? parts[0] : 'default';
|
||||
}
|
||||
|
||||
function getRolePermissions(role) {
|
||||
return rolePermissions[role] || rolePermissions.viewer;
|
||||
}
|
||||
|
||||
function canAccessTable(role, tableName) {
|
||||
const perms = getRolePermissions(role);
|
||||
if (!perms.folders) return true;
|
||||
const folder = getTableFolder(tableName);
|
||||
return perms.folders.includes(folder);
|
||||
}
|
||||
|
||||
const USERS_FILE = path.join(__dirname, 'users.json');
|
||||
const AUDIT_LOG_FILE = path.join(__dirname, 'audit.log');
|
||||
const DOCKER_SOCKET_PATH = process.env.DOCKER_SOCKET_PATH || '/var/run/docker.sock';
|
||||
const DOCKER_API_PREFIX = process.env.DOCKER_API_PREFIX || '/v1.41';
|
||||
const SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
||||
const ALLOWED_SQL_TYPES = new Set(['VARCHAR(255)', 'TEXT', 'INTEGER', 'BIGINT', 'DECIMAL', 'BOOLEAN', 'DATE', 'TIMESTAMP', 'UUID', 'JSON', 'JSONB']);
|
||||
const LEGACY_ROLE_MAP = {
|
||||
frontend_admin: { role: 'admin', folders: ['frontend'] },
|
||||
backend_admin: { role: 'admin', folders: ['backend'] },
|
||||
frontend_moder: { role: 'moderator', folders: ['frontend'] },
|
||||
backend_moder: { role: 'moderator', folders: ['backend'] },
|
||||
viewer: { role: 'viewer', folders: null },
|
||||
superadmin: { role: 'superadmin', folders: null },
|
||||
};
|
||||
|
||||
function readUsersConfig() {
|
||||
try {
|
||||
const parsed = JSON.parse(fs.readFileSync(USERS_FILE, 'utf8'));
|
||||
const users = Array.isArray(parsed.users) ? parsed.users : [];
|
||||
return { users: users.map(normalizeUser).filter(Boolean) };
|
||||
} catch (err) {
|
||||
console.warn('users.json not found or invalid JSON. Falling back to env-based superadmin only.');
|
||||
return { users: [] };
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeUser(user) {
|
||||
if (!user || typeof user.username !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const legacy = LEGACY_ROLE_MAP[user.role];
|
||||
const role = legacy ? legacy.role : user.role;
|
||||
const folders = Array.isArray(user.folders)
|
||||
? user.folders.filter(Boolean)
|
||||
: legacy
|
||||
? legacy.folders
|
||||
: null;
|
||||
|
||||
return {
|
||||
username: user.username,
|
||||
password: typeof user.password === 'string' ? user.password : undefined,
|
||||
passwordHash: typeof user.passwordHash === 'string' ? user.passwordHash : undefined,
|
||||
role: ['admin', 'moderator', 'viewer', 'superadmin'].includes(role) ? role : 'viewer',
|
||||
folders,
|
||||
access: normalizeAccess(user.access, role, folders),
|
||||
disabled: Boolean(user.disabled),
|
||||
};
|
||||
}
|
||||
|
||||
function getUser(username) {
|
||||
return readUsersConfig().users.find((user) => user.username === username) || null;
|
||||
}
|
||||
|
||||
function normalizeScope(scope, fallbackFolders = null) {
|
||||
if (scope === null) {
|
||||
return { folders: null, tables: null };
|
||||
}
|
||||
|
||||
return {
|
||||
folders: Array.isArray(scope?.folders)
|
||||
? scope.folders.filter(Boolean)
|
||||
: fallbackFolders,
|
||||
tables: Array.isArray(scope?.tables)
|
||||
? scope.tables.filter(Boolean)
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeAccess(access, role, folders = null) {
|
||||
if (role === 'superadmin') {
|
||||
return {
|
||||
view: { folders: null, tables: null },
|
||||
create: { folders: null, tables: null },
|
||||
edit: { folders: null, tables: null },
|
||||
delete: { folders: null, tables: null },
|
||||
};
|
||||
}
|
||||
|
||||
const baseFolders = folders && folders.length ? folders : null;
|
||||
const defaultsByRole = {
|
||||
admin: {
|
||||
view: { folders: baseFolders, tables: [] },
|
||||
create: { folders: baseFolders, tables: [] },
|
||||
edit: { folders: baseFolders, tables: [] },
|
||||
delete: { folders: baseFolders, tables: [] },
|
||||
},
|
||||
moderator: {
|
||||
view: { folders: baseFolders, tables: [] },
|
||||
create: { folders: baseFolders, tables: [] },
|
||||
edit: { folders: baseFolders, tables: [] },
|
||||
delete: { folders: [], tables: [] },
|
||||
},
|
||||
viewer: {
|
||||
view: { folders: baseFolders, tables: [] },
|
||||
create: { folders: [], tables: [] },
|
||||
edit: { folders: [], tables: [] },
|
||||
delete: { folders: [], tables: [] },
|
||||
},
|
||||
};
|
||||
|
||||
const defaults = defaultsByRole[role] || defaultsByRole.viewer;
|
||||
const source = access && typeof access === 'object' ? access : {};
|
||||
|
||||
return {
|
||||
view: normalizeScope(source.view, defaults.view.folders),
|
||||
create: normalizeScope(source.create, defaults.create.folders),
|
||||
edit: normalizeScope(source.edit, defaults.edit.folders),
|
||||
delete: normalizeScope(source.delete, defaults.delete.folders),
|
||||
};
|
||||
}
|
||||
|
||||
function getRolePermissions(role, folders = null, access = null) {
|
||||
const normalizedAccess = normalizeAccess(access, role, folders);
|
||||
if (role === 'superadmin') {
|
||||
return {
|
||||
role,
|
||||
folders: null,
|
||||
access: normalizedAccess,
|
||||
canCreate: true,
|
||||
canEdit: true,
|
||||
canDelete: true,
|
||||
canViewLogs: true,
|
||||
canRunSql: true,
|
||||
canManageUsers: true,
|
||||
canMoveTables: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (role === 'admin') {
|
||||
return {
|
||||
role,
|
||||
folders: folders && folders.length ? folders : null,
|
||||
access: normalizedAccess,
|
||||
canCreate: true,
|
||||
canEdit: true,
|
||||
canDelete: true,
|
||||
canViewLogs: true,
|
||||
canRunSql: true,
|
||||
canManageUsers: true,
|
||||
canMoveTables: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (role === 'moderator') {
|
||||
return {
|
||||
role,
|
||||
folders: folders && folders.length ? folders : null,
|
||||
access: normalizedAccess,
|
||||
canCreate: true,
|
||||
canEdit: true,
|
||||
canDelete: false,
|
||||
canViewLogs: false,
|
||||
canRunSql: false,
|
||||
canManageUsers: false,
|
||||
canMoveTables: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
role: 'viewer',
|
||||
folders: folders && folders.length ? folders : null,
|
||||
access: normalizedAccess,
|
||||
canCreate: false,
|
||||
canEdit: false,
|
||||
canDelete: false,
|
||||
canViewLogs: false,
|
||||
canRunSql: false,
|
||||
canManageUsers: false,
|
||||
canMoveTables: false,
|
||||
};
|
||||
}
|
||||
|
||||
function isScopeAllowed(scope, tableName) {
|
||||
if (!scope) return false;
|
||||
if (scope.tables === null || scope.folders === null) return true;
|
||||
const folder = getTableFolder(tableName);
|
||||
return scope.tables.includes(tableName) || scope.folders.includes(folder);
|
||||
}
|
||||
|
||||
function canAccessTable(permissionsOrRole, tableName, folders = null, access = null, action = 'view') {
|
||||
const perms = typeof permissionsOrRole === 'string'
|
||||
? getRolePermissions(permissionsOrRole, folders, access)
|
||||
: permissionsOrRole;
|
||||
return isScopeAllowed(perms.access?.[action], tableName);
|
||||
}
|
||||
|
||||
function canAccessFolder(permissions, folder, action = 'view') {
|
||||
const scope = permissions.access?.[action];
|
||||
if (!scope) return false;
|
||||
if (scope.folders === null) return true;
|
||||
return scope.folders.includes(folder);
|
||||
}
|
||||
|
||||
function isValidIdentifier(value) {
|
||||
return SAFE_IDENTIFIER.test(value);
|
||||
}
|
||||
|
||||
function quoteIdentifier(identifier) {
|
||||
if (!isValidIdentifier(identifier)) {
|
||||
throw new Error(`Unsafe identifier: ${identifier}`);
|
||||
}
|
||||
return `"${identifier}"`;
|
||||
}
|
||||
|
||||
function createSessionUser({ username, role, folders, access }) {
|
||||
return {
|
||||
username,
|
||||
role,
|
||||
permissions: getRolePermissions(role, folders, access),
|
||||
};
|
||||
}
|
||||
|
||||
async function verifyPassword(user, password) {
|
||||
if (!user || user.disabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.passwordHash) {
|
||||
return bcrypt.compare(password, user.passwordHash);
|
||||
}
|
||||
|
||||
return user.password === password;
|
||||
}
|
||||
|
||||
function sanitizeUser(user) {
|
||||
return {
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
folders: user.folders,
|
||||
access: user.access,
|
||||
disabled: user.disabled,
|
||||
};
|
||||
}
|
||||
|
||||
function validateScopeInput(scope) {
|
||||
if (scope === null) {
|
||||
return { folders: null, tables: null };
|
||||
}
|
||||
|
||||
return {
|
||||
folders: Array.isArray(scope?.folders) ? scope.folders.filter(Boolean) : [],
|
||||
tables: Array.isArray(scope?.tables) ? scope.tables.filter(Boolean) : [],
|
||||
};
|
||||
}
|
||||
|
||||
function validateUserPayload(payload, { allowPasswordOptional = false } = {}) {
|
||||
if (!payload || typeof payload.username !== 'string' || !payload.username.trim()) {
|
||||
throw new Error('Username is required');
|
||||
}
|
||||
|
||||
if (!['admin', 'moderator', 'viewer'].includes(payload.role)) {
|
||||
throw new Error('Invalid role');
|
||||
}
|
||||
|
||||
if (!allowPasswordOptional && (!payload.password || typeof payload.password !== 'string')) {
|
||||
throw new Error('Password is required');
|
||||
}
|
||||
|
||||
const folders = Array.isArray(payload.folders) ? payload.folders.filter(Boolean) : null;
|
||||
const access = payload.access && typeof payload.access === 'object'
|
||||
? {
|
||||
view: validateScopeInput(payload.access.view),
|
||||
create: validateScopeInput(payload.access.create),
|
||||
edit: validateScopeInput(payload.access.edit),
|
||||
delete: validateScopeInput(payload.access.delete),
|
||||
}
|
||||
: normalizeAccess(null, payload.role, folders);
|
||||
|
||||
return {
|
||||
username: payload.username.trim(),
|
||||
password: typeof payload.password === 'string' ? payload.password : undefined,
|
||||
role: payload.role,
|
||||
folders,
|
||||
access,
|
||||
disabled: Boolean(payload.disabled),
|
||||
};
|
||||
}
|
||||
|
||||
function writeUsersConfig(users) {
|
||||
const serialized = JSON.stringify({
|
||||
users: users.map((user) => {
|
||||
const payload = {
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
folders: user.folders,
|
||||
access: user.access,
|
||||
disabled: Boolean(user.disabled),
|
||||
};
|
||||
if (user.passwordHash) {
|
||||
payload.passwordHash = user.passwordHash;
|
||||
} else if (user.password) {
|
||||
payload.password = user.password;
|
||||
}
|
||||
return payload;
|
||||
}),
|
||||
}, null, 2);
|
||||
|
||||
fs.writeFileSync(USERS_FILE, serialized, 'utf8');
|
||||
}
|
||||
|
||||
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 [];
|
||||
}
|
||||
|
||||
return fs.readFileSync(AUDIT_LOG_FILE, 'utf8')
|
||||
.split(/\r?\n/)
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
try {
|
||||
return JSON.parse(line);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
.slice(-limit)
|
||||
.reverse();
|
||||
}
|
||||
|
||||
function dockerRequest(requestPath, { stream = false } = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = http.request({
|
||||
socketPath: DOCKER_SOCKET_PATH,
|
||||
path: `${DOCKER_API_PREFIX}${requestPath}`,
|
||||
method: 'GET',
|
||||
}, (response) => {
|
||||
if (stream) {
|
||||
if (response.statusCode >= 400) {
|
||||
const chunks = [];
|
||||
response.on('data', (chunk) => chunks.push(chunk));
|
||||
response.on('end', () => reject(new Error(Buffer.concat(chunks).toString('utf8') || 'Docker stream error')));
|
||||
return;
|
||||
}
|
||||
resolve(response);
|
||||
return;
|
||||
}
|
||||
|
||||
const chunks = [];
|
||||
response.on('data', (chunk) => chunks.push(chunk));
|
||||
response.on('end', () => {
|
||||
const body = Buffer.concat(chunks);
|
||||
if (response.statusCode >= 400) {
|
||||
reject(new Error(body.toString('utf8') || 'Docker API error'));
|
||||
return;
|
||||
}
|
||||
resolve(body);
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
function demuxDockerChunk(buffer) {
|
||||
let offset = 0;
|
||||
let output = '';
|
||||
|
||||
while (offset + 8 <= buffer.length) {
|
||||
const payloadLength = buffer.readUInt32BE(offset + 4);
|
||||
const payloadStart = offset + 8;
|
||||
const payloadEnd = payloadStart + payloadLength;
|
||||
|
||||
if (payloadEnd > buffer.length) {
|
||||
output += buffer.slice(offset).toString('utf8');
|
||||
return output;
|
||||
}
|
||||
|
||||
output += buffer.slice(payloadStart, payloadEnd).toString('utf8');
|
||||
offset = payloadEnd;
|
||||
}
|
||||
|
||||
if (offset < buffer.length) {
|
||||
output += buffer.slice(offset).toString('utf8');
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
async function listContainers() {
|
||||
const body = await dockerRequest('/containers/json?all=1');
|
||||
const containers = JSON.parse(body.toString('utf8'));
|
||||
return containers.map((container) => ({
|
||||
id: container.Id,
|
||||
name: container.Names?.[0]?.replace(/^\//, '') || container.Id.slice(0, 12),
|
||||
state: container.State,
|
||||
status: container.Status,
|
||||
image: container.Image,
|
||||
}));
|
||||
}
|
||||
|
||||
async function resolveContainer(nameOrId) {
|
||||
const containers = await listContainers();
|
||||
const container = containers.find((item) =>
|
||||
item.id === nameOrId || item.id.startsWith(nameOrId) || item.name === nameOrId
|
||||
);
|
||||
|
||||
if (!container) {
|
||||
throw new Error('Container not found');
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
const {
|
||||
ALLOWED_SQL_TYPES,
|
||||
canAccessFolder,
|
||||
canAccessTable,
|
||||
createSessionUser,
|
||||
getTableFolder,
|
||||
getUser,
|
||||
isValidIdentifier,
|
||||
quoteIdentifier,
|
||||
readUsersConfig,
|
||||
sanitizeUser,
|
||||
validateUserPayload,
|
||||
verifyPassword,
|
||||
writeUsersConfig,
|
||||
} = require('./src/lib/access');
|
||||
const {
|
||||
appendAudit,
|
||||
getAuditSource,
|
||||
readAuditLog,
|
||||
} = require('./src/lib/audit');
|
||||
const {
|
||||
demuxDockerChunk,
|
||||
dockerRequest,
|
||||
listContainers,
|
||||
resolveContainer,
|
||||
} = require('./src/lib/docker');
|
||||
const {
|
||||
createBackup,
|
||||
getBackupPath,
|
||||
listBackups,
|
||||
} = require('./src/services/backups');
|
||||
const {
|
||||
notifyError,
|
||||
} = require('./src/services/notifications');
|
||||
|
||||
const app = express();
|
||||
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: '1mb' }));
|
||||
app.use(express.static('.'));
|
||||
app.use(express.static('./public'));
|
||||
|
||||
// Session configuration
|
||||
app.use(session({
|
||||
@@ -534,6 +80,29 @@ pool.connect((err, client, release) => {
|
||||
}
|
||||
});
|
||||
|
||||
function applyRecordMetadata(structure, payload, currentUser, { isCreate = false } = {}) {
|
||||
const data = { ...payload };
|
||||
const hasColumn = (name) => structure.some((col) => col.column_name === name);
|
||||
|
||||
if (isCreate) {
|
||||
if (hasColumn('created_by') && !data.created_by) {
|
||||
data.created_by = currentUser.username;
|
||||
}
|
||||
if (hasColumn('created_at') && !data.created_at) {
|
||||
data.created_at = new Date().toISOString();
|
||||
}
|
||||
}
|
||||
|
||||
if (hasColumn('updated_by')) {
|
||||
data.updated_by = currentUser.username;
|
||||
}
|
||||
if (hasColumn('updated_at')) {
|
||||
data.updated_at = new Date().toISOString();
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Helper: get primary key column for a table (returns null if none)
|
||||
async function getPrimaryKeyColumn(tableName) {
|
||||
const result = await pool.query(`
|
||||
@@ -772,6 +341,39 @@ app.get('/api/audit', requireAuth, requirePermission(
|
||||
res.json(readAuditLog(limit));
|
||||
});
|
||||
|
||||
app.get('/api/backups', requireAuth, requirePermission(
|
||||
(permissions) => permissions.canManageUsers,
|
||||
'Backup access denied'
|
||||
), (req, res) => {
|
||||
res.json(listBackups());
|
||||
});
|
||||
|
||||
app.post('/api/backups', requireAuth, requirePermission(
|
||||
(permissions) => permissions.canManageUsers,
|
||||
'Backup access denied'
|
||||
), async (req, res) => {
|
||||
try {
|
||||
const backup = await createBackup(pool, req.currentUser.username);
|
||||
appendAudit('backup.created', req.currentUser.username, { filename: backup.filename, source: getAuditSource(req) });
|
||||
res.json({ success: true, backup });
|
||||
} catch (err) {
|
||||
notifyError('Backup creation failed', err, { actor: req.currentUser.username }).catch(() => {});
|
||||
res.status(500).json({ success: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/backups/:filename/download', requireAuth, requirePermission(
|
||||
(permissions) => permissions.canManageUsers,
|
||||
'Backup access denied'
|
||||
), (req, res) => {
|
||||
try {
|
||||
const filePath = getBackupPath(req.params.filename);
|
||||
res.download(filePath, req.params.filename);
|
||||
} catch (err) {
|
||||
res.status(404).json({ success: false, error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get all tables
|
||||
app.get('/api/tables', requireAuth, async (req, res) => {
|
||||
try {
|
||||
@@ -914,6 +516,7 @@ app.post('/api/tables', requireAuth, requirePermission((permissions, req) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const reservedColumns = new Set(columns.map((col) => col.name));
|
||||
const columnsSQL = columns.map((col) => {
|
||||
if (!isValidIdentifier(col.name) || !ALLOWED_SQL_TYPES.has(col.type)) {
|
||||
throw new Error('Invalid column definition');
|
||||
@@ -923,7 +526,12 @@ app.post('/api/tables', requireAuth, requirePermission((permissions, req) => {
|
||||
if (col.pk) def += ' PRIMARY KEY';
|
||||
if (!col.nullable && !col.pk) def += ' NOT NULL';
|
||||
return def;
|
||||
}).join(', ');
|
||||
}).concat([
|
||||
!reservedColumns.has('created_at') ? `"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP` : null,
|
||||
!reservedColumns.has('created_by') ? `"created_by" VARCHAR(255)` : null,
|
||||
!reservedColumns.has('updated_at') ? `"updated_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP` : null,
|
||||
!reservedColumns.has('updated_by') ? `"updated_by" VARCHAR(255)` : null,
|
||||
].filter(Boolean)).join(', ');
|
||||
|
||||
await pool.query(`CREATE TABLE ${quoteIdentifier(name)} (${columnsSQL})`);
|
||||
appendAudit('table.created', req.currentUser.username, { table: name, source: getAuditSource(req) });
|
||||
@@ -985,7 +593,8 @@ app.post('/api/tables/:tableName/records', requireAuth, requireTableAccess, requ
|
||||
|
||||
const structure = structureResult.rows;
|
||||
const filteredData = {};
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
const dataWithMetadata = applyRecordMetadata(structure, data, req.currentUser, { isCreate: true });
|
||||
for (const [key, value] of Object.entries(dataWithMetadata)) {
|
||||
const colInfo = structure.find(col => col.column_name === key);
|
||||
if (!colInfo || value === '') {
|
||||
continue;
|
||||
@@ -1020,15 +629,22 @@ app.put('/api/tables/:tableName/records/:pk', requireAuth, requireTableAccess, r
|
||||
), async (req, res) => {
|
||||
const { tableName, pk } = req.params;
|
||||
const data = req.body || {};
|
||||
const columns = Object.keys(data).filter(isValidIdentifier);
|
||||
|
||||
if (!columns.length) {
|
||||
return res.status(400).json({ success: false, error: 'No valid fields to update' });
|
||||
}
|
||||
|
||||
try {
|
||||
const primaryKey = await getPrimaryKeyColumn(tableName);
|
||||
const values = columns.map((column) => data[column]);
|
||||
const structure = await pool.query(`
|
||||
SELECT column_name, data_type
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = $1 AND table_schema = 'public'
|
||||
`, [tableName]).then((result) => result.rows);
|
||||
const dataWithMetadata = applyRecordMetadata(structure, data, req.currentUser, { isCreate: false });
|
||||
const columns = Object.keys(dataWithMetadata).filter(isValidIdentifier);
|
||||
|
||||
if (!columns.length) {
|
||||
return res.status(400).json({ success: false, error: 'No valid fields to update' });
|
||||
}
|
||||
|
||||
const values = columns.map((column) => dataWithMetadata[column]);
|
||||
const setClause = columns.map((col, i) => `${quoteIdentifier(col)} = $${i + 1}`).join(', ');
|
||||
const whereClause = primaryKey
|
||||
? `${quoteIdentifier(primaryKey)} = $${values.length + 1}`
|
||||
@@ -1326,3 +942,5 @@ app.listen(PORT, () => {
|
||||
console.log('');
|
||||
console.log('📝 Make sure to configure your database in .env file');
|
||||
});
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user