Добавление админки
This commit is contained in:
279
index.html
279
index.html
@@ -183,6 +183,10 @@
|
|||||||
<option value="dark">Dark</option>
|
<option value="dark">Dark</option>
|
||||||
<option value="system">System</option>
|
<option value="system">System</option>
|
||||||
</select>
|
</select>
|
||||||
|
<button id="usersButton" onclick="app.showUsersModal()" class="hidden flex items-center gap-2 px-4 py-2 bg-slate-100 text-slate-700 rounded-lg hover:bg-slate-200 transition-colors text-sm font-medium">
|
||||||
|
<i data-lucide="users" class="w-4 h-4"></i>
|
||||||
|
Users
|
||||||
|
</button>
|
||||||
<button id="logsButton" onclick="app.showLogsPanel()" class="hidden flex items-center gap-2 px-4 py-2 bg-slate-100 text-slate-700 rounded-lg hover:bg-slate-200 transition-colors text-sm font-medium">
|
<button id="logsButton" onclick="app.showLogsPanel()" class="hidden flex items-center gap-2 px-4 py-2 bg-slate-100 text-slate-700 rounded-lg hover:bg-slate-200 transition-colors text-sm font-medium">
|
||||||
<i data-lucide="scroll-text" class="w-4 h-4"></i>
|
<i data-lucide="scroll-text" class="w-4 h-4"></i>
|
||||||
Logs
|
Logs
|
||||||
@@ -244,6 +248,10 @@
|
|||||||
<i data-lucide="list-tree" class="w-4 h-4"></i>
|
<i data-lucide="list-tree" class="w-4 h-4"></i>
|
||||||
Индексы
|
Индексы
|
||||||
</button>
|
</button>
|
||||||
|
<button onclick="app.showMoveTableModal()" class="flex items-center gap-2 px-3 py-1.5 bg-slate-100 text-slate-700 rounded-lg hover:bg-slate-200 transition-colors text-sm">
|
||||||
|
<i data-lucide="folder-input" class="w-4 h-4"></i>
|
||||||
|
Move
|
||||||
|
</button>
|
||||||
<button onclick="app.deleteTable()" class="flex items-center gap-2 px-3 py-1.5 bg-red-50 text-red-600 rounded-lg hover:bg-red-100 transition-colors text-sm">
|
<button onclick="app.deleteTable()" class="flex items-center gap-2 px-3 py-1.5 bg-red-50 text-red-600 rounded-lg hover:bg-red-100 transition-colors text-sm">
|
||||||
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
||||||
Удалить
|
Удалить
|
||||||
@@ -555,6 +563,93 @@ SELECT * FROM users LIMIT 10;"></textarea>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="moveTableModal" class="hidden fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
|
||||||
|
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-md fade-in">
|
||||||
|
<div class="p-6 border-b border-slate-200">
|
||||||
|
<h3 class="text-xl font-bold text-slate-800">Move table</h3>
|
||||||
|
</div>
|
||||||
|
<div class="p-6 space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Folder</label>
|
||||||
|
<input type="text" id="moveTableFolder" class="w-full px-4 py-2 border border-slate-300 rounded-lg" placeholder="frontend">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Table name</label>
|
||||||
|
<input type="text" id="moveTableName" class="w-full px-4 py-2 border border-slate-300 rounded-lg" placeholder="users">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-6 border-t border-slate-200 bg-slate-50 flex justify-end gap-3">
|
||||||
|
<button onclick="app.closeModal('moveTableModal')" class="px-4 py-2 text-slate-600 hover:bg-slate-200 rounded-lg">Cancel</button>
|
||||||
|
<button onclick="app.moveTable()" class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="usersModal" class="hidden fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
|
||||||
|
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-4xl max-h-[90vh] overflow-hidden flex flex-col fade-in">
|
||||||
|
<div class="p-6 border-b border-slate-200 flex items-center justify-between">
|
||||||
|
<h3 class="text-xl font-bold text-slate-800">Users & access</h3>
|
||||||
|
<button onclick="app.closeModal('usersModal')" class="p-2 hover:bg-slate-100 rounded-lg transition-colors">
|
||||||
|
<i data-lucide="x" class="w-5 h-5 text-slate-500"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="grid lg:grid-cols-[1.2fr,0.8fr] gap-0 flex-1 min-h-0">
|
||||||
|
<div class="border-r border-slate-200 overflow-auto">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead class="bg-slate-50 border-b border-slate-200">
|
||||||
|
<tr>
|
||||||
|
<th class="text-left p-3">Username</th>
|
||||||
|
<th class="text-left p-3">Role</th>
|
||||||
|
<th class="text-left p-3">Status</th>
|
||||||
|
<th class="text-left p-3">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="usersTableBody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="p-6 overflow-auto space-y-3">
|
||||||
|
<input type="hidden" id="userEditMode" value="">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Username</label>
|
||||||
|
<input type="text" id="userUsername" class="w-full px-4 py-2 border border-slate-300 rounded-lg">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Password</label>
|
||||||
|
<input type="text" id="userPassword" class="w-full px-4 py-2 border border-slate-300 rounded-lg" placeholder="Leave empty to keep current">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Role</label>
|
||||||
|
<select id="userRole" class="w-full px-4 py-2 border border-slate-300 rounded-lg">
|
||||||
|
<option value="viewer">viewer</option>
|
||||||
|
<option value="moderator">moderator</option>
|
||||||
|
<option value="admin">admin</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Readable folders</label>
|
||||||
|
<input type="text" id="userViewFolders" class="w-full px-4 py-2 border border-slate-300 rounded-lg" placeholder="frontend, backend">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Editable tables</label>
|
||||||
|
<input type="text" id="userEditTables" class="w-full px-4 py-2 border border-slate-300 rounded-lg" placeholder="users, frontend__users">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 mb-1">Deletable folders</label>
|
||||||
|
<input type="text" id="userDeleteFolders" class="w-full px-4 py-2 border border-slate-300 rounded-lg" placeholder="frontend">
|
||||||
|
</div>
|
||||||
|
<label class="flex items-center gap-2 text-sm text-slate-700">
|
||||||
|
<input type="checkbox" id="userDisabled" class="w-4 h-4">
|
||||||
|
Disable login
|
||||||
|
</label>
|
||||||
|
<div class="flex justify-end gap-3 pt-2">
|
||||||
|
<button onclick="app.resetUserForm()" class="px-4 py-2 text-slate-600 hover:bg-slate-100 rounded-lg">Reset</button>
|
||||||
|
<button onclick="app.saveUser()" class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg">Save user</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Toast Notifications -->
|
<!-- Toast Notifications -->
|
||||||
<div id="toastContainer" class="fixed bottom-6 right-6 z-50 flex flex-col gap-2"></div>
|
<div id="toastContainer" class="fixed bottom-6 right-6 z-50 flex flex-col gap-2"></div>
|
||||||
|
|
||||||
@@ -565,6 +660,7 @@ SELECT * FROM users LIMIT 10;"></textarea>
|
|||||||
this.currentUser = null;
|
this.currentUser = null;
|
||||||
this.currentTable = null;
|
this.currentTable = null;
|
||||||
this.tables = [];
|
this.tables = [];
|
||||||
|
this.currentRows = [];
|
||||||
this.folderState = JSON.parse(localStorage.getItem('pg_folder_state') || '{}');
|
this.folderState = JSON.parse(localStorage.getItem('pg_folder_state') || '{}');
|
||||||
this.themePreference = localStorage.getItem('pg_theme') || 'system';
|
this.themePreference = localStorage.getItem('pg_theme') || 'system';
|
||||||
this.currentContainer = '';
|
this.currentContainer = '';
|
||||||
@@ -710,6 +806,7 @@ SELECT * FROM users LIMIT 10;"></textarea>
|
|||||||
`${dbInfo.host}:${dbInfo.port}/${dbInfo.database}`;
|
`${dbInfo.host}:${dbInfo.port}/${dbInfo.database}`;
|
||||||
document.getElementById('roleBadge').textContent = this.currentUser.role || 'viewer';
|
document.getElementById('roleBadge').textContent = this.currentUser.role || 'viewer';
|
||||||
document.getElementById('logsButton').classList.toggle('hidden', !this.getPermissions().canViewLogs);
|
document.getElementById('logsButton').classList.toggle('hidden', !this.getPermissions().canViewLogs);
|
||||||
|
document.getElementById('usersButton').classList.toggle('hidden', !this.getPermissions().canManageUsers);
|
||||||
document.querySelector('button[onclick="app.showSQLPanel()"]').style.display = this.getPermissions().canRunSql ? '' : 'none';
|
document.querySelector('button[onclick="app.showSQLPanel()"]').style.display = this.getPermissions().canRunSql ? '' : 'none';
|
||||||
document.querySelector('button[onclick="app.showCreateTableModal()"]').style.display = this.getPermissions().canCreate ? '' : 'none';
|
document.querySelector('button[onclick="app.showCreateTableModal()"]').style.display = this.getPermissions().canCreate ? '' : 'none';
|
||||||
|
|
||||||
@@ -724,7 +821,9 @@ SELECT * FROM users LIMIT 10;"></textarea>
|
|||||||
canEdit: true,
|
canEdit: true,
|
||||||
canDelete: true,
|
canDelete: true,
|
||||||
canViewLogs: true,
|
canViewLogs: true,
|
||||||
canRunSql: true
|
canRunSql: true,
|
||||||
|
canManageUsers: true,
|
||||||
|
canMoveTables: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -734,7 +833,9 @@ SELECT * FROM users LIMIT 10;"></textarea>
|
|||||||
canEdit: false,
|
canEdit: false,
|
||||||
canDelete: false,
|
canDelete: false,
|
||||||
canViewLogs: false,
|
canViewLogs: false,
|
||||||
canRunSql: false
|
canRunSql: false,
|
||||||
|
canManageUsers: false,
|
||||||
|
canMoveTables: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -853,8 +954,10 @@ SELECT * FROM users LIMIT 10;"></textarea>
|
|||||||
// Update action buttons based on permissions
|
// Update action buttons based on permissions
|
||||||
const addBtn = document.querySelector('#tableActions button[onclick="app.showAddRecordModal()"]');
|
const addBtn = document.querySelector('#tableActions button[onclick="app.showAddRecordModal()"]');
|
||||||
const deleteTableBtn = document.querySelector('#tableActions button[onclick="app.deleteTable()"]');
|
const deleteTableBtn = document.querySelector('#tableActions button[onclick="app.deleteTable()"]');
|
||||||
|
const moveTableBtn = document.querySelector('#tableActions button[onclick="app.showMoveTableModal()"]');
|
||||||
if (addBtn) addBtn.style.display = this.canEditTable() ? '' : 'none';
|
if (addBtn) addBtn.style.display = this.canEditTable() ? '' : 'none';
|
||||||
if (deleteTableBtn) deleteTableBtn.style.display = this.canDeleteTable() ? '' : 'none';
|
if (deleteTableBtn) deleteTableBtn.style.display = this.canDeleteTable() ? '' : 'none';
|
||||||
|
if (moveTableBtn) moveTableBtn.style.display = this.getPermissions().canMoveTables ? '' : 'none';
|
||||||
|
|
||||||
// Load table structure to get primary key
|
// Load table structure to get primary key
|
||||||
await this.loadTableStructure();
|
await this.loadTableStructure();
|
||||||
@@ -890,7 +993,8 @@ SELECT * FROM users LIMIT 10;"></textarea>
|
|||||||
const body = document.getElementById('tableBody');
|
const body = document.getElementById('tableBody');
|
||||||
|
|
||||||
// Generate headers
|
// Generate headers
|
||||||
const columns = records.length > 0 ? Object.keys(records[0]) : this.tableStructure.map(col => col.name);
|
this.currentRows = records;
|
||||||
|
const columns = (records.length > 0 ? Object.keys(records[0]) : this.tableStructure.map(col => col.name)).filter(col => col !== '__rowid');
|
||||||
headers.innerHTML = columns.map(col => {
|
headers.innerHTML = columns.map(col => {
|
||||||
const isSorted = this.sortColumn === col;
|
const isSorted = this.sortColumn === col;
|
||||||
const arrow = isSorted ? (this.sortDirection === 'ASC' ? '↑' : '↓') : '';
|
const arrow = isSorted ? (this.sortDirection === 'ASC' ? '↑' : '↓') : '';
|
||||||
@@ -910,7 +1014,9 @@ SELECT * FROM users LIMIT 10;"></textarea>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
body.innerHTML = records.map(record => {
|
body.innerHTML = records.map(record => {
|
||||||
const pkValue = this.primaryKey ? record[this.primaryKey] : null;
|
const pkValue = this.primaryKey && record[this.primaryKey] !== undefined && record[this.primaryKey] !== null && record[this.primaryKey] !== ''
|
||||||
|
? record[this.primaryKey]
|
||||||
|
: record.__rowid;
|
||||||
const cells = columns.map(col => {
|
const cells = columns.map(col => {
|
||||||
const colDef = this.tableStructure.find(c => c.name === col);
|
const colDef = this.tableStructure.find(c => c.name === col);
|
||||||
let displayValue = record[col] || '';
|
let displayValue = record[col] || '';
|
||||||
@@ -921,6 +1027,7 @@ SELECT * FROM users LIMIT 10;"></textarea>
|
|||||||
}).join('');
|
}).join('');
|
||||||
const canEdit = this.canEditTable();
|
const canEdit = this.canEditTable();
|
||||||
const canDelete = this.canDeleteTable();
|
const canDelete = this.canDeleteTable();
|
||||||
|
const actionValue = JSON.stringify(String(pkValue));
|
||||||
const actions = pkValue ? `
|
const actions = pkValue ? `
|
||||||
<td class="p-3 border-b border-slate-100">
|
<td class="p-3 border-b border-slate-100">
|
||||||
${canEdit ? `<button onclick="app.editRecord('${pkValue}')" class="text-blue-600 hover:underline mr-2">Редактировать</button>` : ''}
|
${canEdit ? `<button onclick="app.editRecord('${pkValue}')" class="text-blue-600 hover:underline mr-2">Редактировать</button>` : ''}
|
||||||
@@ -1054,12 +1161,26 @@ SELECT * FROM users LIMIT 10;"></textarea>
|
|||||||
|
|
||||||
async loadRecordData(pkValue) {
|
async loadRecordData(pkValue) {
|
||||||
try {
|
try {
|
||||||
// Since we don't have a single record endpoint, we'll load all data and find the record
|
let record = this.currentRows.find(r => {
|
||||||
const params = new URLSearchParams({ page: 1, limit: 1000 }); // Load more to find the record
|
const rowKey = this.primaryKey && r[this.primaryKey] !== undefined && r[this.primaryKey] !== null && r[this.primaryKey] !== ''
|
||||||
|
? r[this.primaryKey]
|
||||||
|
: r.__rowid;
|
||||||
|
return String(rowKey) === String(pkValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!record) {
|
||||||
|
const params = new URLSearchParams({ page: 1, limit: 1000 });
|
||||||
const response = await fetch(`/api/tables/${this.currentTable}/data?${params}`);
|
const response = await fetch(`/api/tables/${this.currentTable}/data?${params}`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
this.currentRows = data.data || [];
|
||||||
|
record = this.currentRows.find(r => {
|
||||||
|
const rowKey = this.primaryKey && r[this.primaryKey] !== undefined && r[this.primaryKey] !== null && r[this.primaryKey] !== ''
|
||||||
|
? r[this.primaryKey]
|
||||||
|
: r.__rowid;
|
||||||
|
return String(rowKey) === String(pkValue);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const record = data.data.find(r => r[this.primaryKey] == pkValue);
|
|
||||||
if (record) {
|
if (record) {
|
||||||
this.tableStructure.forEach(col => {
|
this.tableStructure.forEach(col => {
|
||||||
const input = document.querySelector(`[name="${col.name}"]`);
|
const input = document.querySelector(`[name="${col.name}"]`);
|
||||||
@@ -1552,6 +1673,150 @@ SELECT * FROM users LIMIT 10;"></textarea>
|
|||||||
output.scrollTop = output.scrollHeight;
|
output.scrollTop = output.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parseList(value) {
|
||||||
|
return value.split(',').map(item => item.trim()).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
async showUsersModal() {
|
||||||
|
await this.loadUsers();
|
||||||
|
this.resetUserForm();
|
||||||
|
document.getElementById('usersModal').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadUsers() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/users');
|
||||||
|
const users = await response.json();
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(users.error || 'Failed to load users');
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('usersTableBody').innerHTML = users.map(user => `
|
||||||
|
<tr class="border-b border-slate-200">
|
||||||
|
<td class="p-3">${user.username}</td>
|
||||||
|
<td class="p-3">${user.role}</td>
|
||||||
|
<td class="p-3">${user.disabled ? 'disabled' : 'active'}</td>
|
||||||
|
<td class="p-3">
|
||||||
|
<button onclick='app.editUser(${JSON.stringify(JSON.stringify(user))})' class="text-blue-600 hover:underline mr-3">Edit</button>
|
||||||
|
<button onclick='app.deleteUser(${JSON.stringify(user.username)})' class="text-red-600 hover:underline">Delete</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
} catch (err) {
|
||||||
|
this.showToast(err.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
editUser(serializedUser) {
|
||||||
|
const user = JSON.parse(serializedUser);
|
||||||
|
document.getElementById('userEditMode').value = user.username;
|
||||||
|
document.getElementById('userUsername').value = user.username;
|
||||||
|
document.getElementById('userUsername').disabled = true;
|
||||||
|
document.getElementById('userPassword').value = '';
|
||||||
|
document.getElementById('userRole').value = user.role;
|
||||||
|
document.getElementById('userViewFolders').value = (user.access?.view?.folders || []).join(', ');
|
||||||
|
document.getElementById('userEditTables').value = (user.access?.edit?.tables || []).join(', ');
|
||||||
|
document.getElementById('userDeleteFolders').value = (user.access?.delete?.folders || []).join(', ');
|
||||||
|
document.getElementById('userDisabled').checked = Boolean(user.disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetUserForm() {
|
||||||
|
document.getElementById('userEditMode').value = '';
|
||||||
|
document.getElementById('userUsername').value = '';
|
||||||
|
document.getElementById('userUsername').disabled = false;
|
||||||
|
document.getElementById('userPassword').value = '';
|
||||||
|
document.getElementById('userRole').value = 'viewer';
|
||||||
|
document.getElementById('userViewFolders').value = '';
|
||||||
|
document.getElementById('userEditTables').value = '';
|
||||||
|
document.getElementById('userDeleteFolders').value = '';
|
||||||
|
document.getElementById('userDisabled').checked = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveUser() {
|
||||||
|
const username = document.getElementById('userUsername').value.trim();
|
||||||
|
const editMode = document.getElementById('userEditMode').value;
|
||||||
|
const password = document.getElementById('userPassword').value;
|
||||||
|
const payload = {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
role: document.getElementById('userRole').value,
|
||||||
|
disabled: document.getElementById('userDisabled').checked,
|
||||||
|
access: {
|
||||||
|
view: { folders: this.parseList(document.getElementById('userViewFolders').value), tables: [] },
|
||||||
|
create: { folders: [], tables: [] },
|
||||||
|
edit: { folders: [], tables: this.parseList(document.getElementById('userEditTables').value) },
|
||||||
|
delete: { folders: this.parseList(document.getElementById('userDeleteFolders').value), tables: [] },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(editMode ? `/api/users/${encodeURIComponent(editMode)}` : '/api/users', {
|
||||||
|
method: editMode ? 'PUT' : 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.error || 'Failed to save user');
|
||||||
|
}
|
||||||
|
this.showToast('User saved', 'success');
|
||||||
|
this.resetUserForm();
|
||||||
|
this.loadUsers();
|
||||||
|
} catch (err) {
|
||||||
|
this.showToast(err.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteUser(username) {
|
||||||
|
if (!confirm(`Delete user ${username}?`)) return;
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/users/${encodeURIComponent(username)}`, { method: 'DELETE' });
|
||||||
|
const data = await response.json();
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.error || 'Failed to delete user');
|
||||||
|
}
|
||||||
|
this.showToast('User deleted', 'success');
|
||||||
|
this.loadUsers();
|
||||||
|
} catch (err) {
|
||||||
|
this.showToast(err.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showMoveTableModal() {
|
||||||
|
const parts = (this.currentTable || '').split('__');
|
||||||
|
document.getElementById('moveTableFolder').value = parts.length > 1 ? parts[0] : '';
|
||||||
|
document.getElementById('moveTableName').value = parts.length > 1 ? parts.slice(1).join('__') : this.currentTable || '';
|
||||||
|
document.getElementById('moveTableModal').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
async moveTable() {
|
||||||
|
const folder = document.getElementById('moveTableFolder').value.trim();
|
||||||
|
const name = document.getElementById('moveTableName').value.trim();
|
||||||
|
if (!name) {
|
||||||
|
this.showToast('Table name is required', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/tables/${encodeURIComponent(this.currentTable)}/move`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ folder, name }),
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.error || 'Failed to move table');
|
||||||
|
}
|
||||||
|
this.currentTable = data.name;
|
||||||
|
this.closeModal('moveTableModal');
|
||||||
|
this.loadTables();
|
||||||
|
this.selectTable(data.name);
|
||||||
|
this.showToast('Table moved', 'success');
|
||||||
|
} catch (err) {
|
||||||
|
this.showToast(err.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
showIndexesModal() {
|
showIndexesModal() {
|
||||||
document.getElementById('indexesTableName').textContent = this.currentTable;
|
document.getElementById('indexesTableName').textContent = this.currentTable;
|
||||||
this.loadIndexes();
|
this.loadIndexes();
|
||||||
|
|||||||
353
server.js
353
server.js
@@ -90,6 +90,7 @@ function normalizeUser(user) {
|
|||||||
passwordHash: typeof user.passwordHash === 'string' ? user.passwordHash : undefined,
|
passwordHash: typeof user.passwordHash === 'string' ? user.passwordHash : undefined,
|
||||||
role: ['admin', 'moderator', 'viewer', 'superadmin'].includes(role) ? role : 'viewer',
|
role: ['admin', 'moderator', 'viewer', 'superadmin'].includes(role) ? role : 'viewer',
|
||||||
folders,
|
folders,
|
||||||
|
access: normalizeAccess(user.access, role, folders),
|
||||||
disabled: Boolean(user.disabled),
|
disabled: Boolean(user.disabled),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -98,33 +99,144 @@ function getUser(username) {
|
|||||||
return readUsersConfig().users.find((user) => user.username === username) || null;
|
return readUsersConfig().users.find((user) => user.username === username) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRolePermissions(role, folders = 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') {
|
if (role === 'superadmin') {
|
||||||
return { role, folders: null, canCreate: true, canEdit: true, canDelete: true, canViewLogs: true, canRunSql: true };
|
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') {
|
if (role === 'admin') {
|
||||||
return { role, folders: folders && folders.length ? folders : null, canCreate: true, canEdit: true, canDelete: true, canViewLogs: true, canRunSql: true };
|
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') {
|
if (role === 'moderator') {
|
||||||
return { role, folders: folders && folders.length ? folders : null, canCreate: true, canEdit: true, canDelete: false, canViewLogs: false, canRunSql: false };
|
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, canCreate: false, canEdit: false, canDelete: false, canViewLogs: false, canRunSql: 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 canAccessTable(permissionsOrRole, tableName, folders = null) {
|
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'
|
const perms = typeof permissionsOrRole === 'string'
|
||||||
? getRolePermissions(permissionsOrRole, folders)
|
? getRolePermissions(permissionsOrRole, folders, access)
|
||||||
: permissionsOrRole;
|
: permissionsOrRole;
|
||||||
if (!perms.folders) return true;
|
return isScopeAllowed(perms.access?.[action], tableName);
|
||||||
return perms.folders.includes(getTableFolder(tableName));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function canAccessFolder(permissions, folder) {
|
function canAccessFolder(permissions, folder, action = 'view') {
|
||||||
if (!permissions.folders) return true;
|
const scope = permissions.access?.[action];
|
||||||
return permissions.folders.includes(folder);
|
if (!scope) return false;
|
||||||
|
if (scope.folders === null) return true;
|
||||||
|
return scope.folders.includes(folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidIdentifier(value) {
|
function isValidIdentifier(value) {
|
||||||
@@ -138,11 +250,11 @@ function quoteIdentifier(identifier) {
|
|||||||
return `"${identifier}"`;
|
return `"${identifier}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSessionUser({ username, role, folders }) {
|
function createSessionUser({ username, role, folders, access }) {
|
||||||
return {
|
return {
|
||||||
username,
|
username,
|
||||||
role,
|
role,
|
||||||
permissions: getRolePermissions(role, folders),
|
permissions: getRolePermissions(role, folders, access),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,6 +270,82 @@ async function verifyPassword(user, password) {
|
|||||||
return user.password === password;
|
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 dockerRequest(requestPath, { stream = false } = {}) {
|
function dockerRequest(requestPath, { stream = false } = {}) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const req = http.request({
|
const req = http.request({
|
||||||
@@ -326,7 +514,7 @@ const requireTableAccess = (req, res, next) => {
|
|||||||
return res.status(400).json({ success: false, error: 'Invalid table name' });
|
return res.status(400).json({ success: false, error: 'Invalid table name' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!canAccessTable(req.currentUser.permissions, tableName)) {
|
if (!canAccessTable(req.currentUser.permissions, tableName, null, null, 'view')) {
|
||||||
return res.status(403).json({ success: false, error: 'Access denied' });
|
return res.status(403).json({ success: false, error: 'Access denied' });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,6 +617,83 @@ app.get('/api/session', (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get('/api/users', requireAuth, requirePermission(
|
||||||
|
(permissions) => permissions.canManageUsers,
|
||||||
|
'User management access denied'
|
||||||
|
), (req, res) => {
|
||||||
|
const config = readUsersConfig();
|
||||||
|
res.json(config.users.map(sanitizeUser));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/users', requireAuth, requirePermission(
|
||||||
|
(permissions) => permissions.canManageUsers,
|
||||||
|
'User management access denied'
|
||||||
|
), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const payload = validateUserPayload(req.body);
|
||||||
|
const config = readUsersConfig();
|
||||||
|
if (config.users.some((user) => user.username === payload.username)) {
|
||||||
|
return res.status(409).json({ success: false, error: 'User already exists' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordHash = await bcrypt.hash(payload.password, 10);
|
||||||
|
config.users.push({
|
||||||
|
username: payload.username,
|
||||||
|
passwordHash,
|
||||||
|
role: payload.role,
|
||||||
|
folders: payload.folders,
|
||||||
|
access: payload.access,
|
||||||
|
disabled: payload.disabled,
|
||||||
|
});
|
||||||
|
writeUsersConfig(config.users);
|
||||||
|
res.json({ success: true, user: sanitizeUser(config.users[config.users.length - 1]) });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(400).json({ success: false, error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.put('/api/users/:username', requireAuth, requirePermission(
|
||||||
|
(permissions) => permissions.canManageUsers,
|
||||||
|
'User management access denied'
|
||||||
|
), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const payload = validateUserPayload({ ...req.body, username: req.params.username }, { allowPasswordOptional: true });
|
||||||
|
const config = readUsersConfig();
|
||||||
|
const user = config.users.find((item) => item.username === req.params.username);
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({ success: false, error: 'User not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
user.role = payload.role;
|
||||||
|
user.folders = payload.folders;
|
||||||
|
user.access = payload.access;
|
||||||
|
user.disabled = payload.disabled;
|
||||||
|
if (payload.password) {
|
||||||
|
user.passwordHash = await bcrypt.hash(payload.password, 10);
|
||||||
|
delete user.password;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeUsersConfig(config.users);
|
||||||
|
res.json({ success: true, user: sanitizeUser(user) });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(400).json({ success: false, error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/api/users/:username', requireAuth, requirePermission(
|
||||||
|
(permissions) => permissions.canManageUsers,
|
||||||
|
'User management access denied'
|
||||||
|
), (req, res) => {
|
||||||
|
const config = readUsersConfig();
|
||||||
|
const nextUsers = config.users.filter((user) => user.username !== req.params.username);
|
||||||
|
if (nextUsers.length === config.users.length) {
|
||||||
|
return res.status(404).json({ success: false, error: 'User not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
writeUsersConfig(nextUsers);
|
||||||
|
res.json({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
// Get all tables
|
// Get all tables
|
||||||
app.get('/api/tables', requireAuth, async (req, res) => {
|
app.get('/api/tables', requireAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -440,7 +705,7 @@ app.get('/api/tables', requireAuth, async (req, res) => {
|
|||||||
ORDER BY table_name
|
ORDER BY table_name
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const accessibleTables = result.rows.filter(table => canAccessTable(req.currentUser.permissions, table.name));
|
const accessibleTables = result.rows.filter(table => canAccessTable(req.currentUser.permissions, table.name, null, null, 'view'));
|
||||||
const tablesWithCounts = await Promise.all(
|
const tablesWithCounts = await Promise.all(
|
||||||
accessibleTables.map(async (table) => {
|
accessibleTables.map(async (table) => {
|
||||||
try {
|
try {
|
||||||
@@ -512,7 +777,7 @@ app.get('/api/tables/:tableName/data', requireAuth, requireTableAccess, async (r
|
|||||||
const countResult = await pool.query(`SELECT COUNT(*)::int as total FROM ${quoteIdentifier(tableName)} ${whereClause}`, params);
|
const countResult = await pool.query(`SELECT COUNT(*)::int as total FROM ${quoteIdentifier(tableName)} ${whereClause}`, params);
|
||||||
const total = countResult.rows[0].total;
|
const total = countResult.rows[0].total;
|
||||||
const result = await pool.query(`
|
const result = await pool.query(`
|
||||||
SELECT * FROM ${quoteIdentifier(tableName)}
|
SELECT ctid::text AS "__rowid", * FROM ${quoteIdentifier(tableName)}
|
||||||
${whereClause}
|
${whereClause}
|
||||||
${orderBy}
|
${orderBy}
|
||||||
LIMIT $${params.length + 1} OFFSET $${params.length + 2}
|
LIMIT $${params.length + 1} OFFSET $${params.length + 2}
|
||||||
@@ -562,7 +827,7 @@ app.get('/api/tables/:tableName/structure', requireAuth, requireTableAccess, asy
|
|||||||
|
|
||||||
app.post('/api/tables', requireAuth, requirePermission((permissions, req) => {
|
app.post('/api/tables', requireAuth, requirePermission((permissions, req) => {
|
||||||
const folder = getTableFolder(req.body?.name);
|
const folder = getTableFolder(req.body?.name);
|
||||||
return permissions.canCreate && canAccessFolder(permissions, folder);
|
return permissions.canCreate && canAccessFolder(permissions, folder, 'create');
|
||||||
}, 'Access denied'), async (req, res) => {
|
}, 'Access denied'), async (req, res) => {
|
||||||
const { name, columns } = req.body || {};
|
const { name, columns } = req.body || {};
|
||||||
|
|
||||||
@@ -590,7 +855,7 @@ app.post('/api/tables', requireAuth, requirePermission((permissions, req) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.delete('/api/tables/:tableName', requireAuth, requireTableAccess, requirePermission(
|
app.delete('/api/tables/:tableName', requireAuth, requireTableAccess, requirePermission(
|
||||||
(permissions, req) => permissions.canDelete && canAccessTable(permissions, req.params.tableName),
|
(permissions, req) => permissions.canDelete && canAccessTable(permissions, req.params.tableName, null, null, 'delete'),
|
||||||
'Access denied'
|
'Access denied'
|
||||||
), async (req, res) => {
|
), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -601,8 +866,30 @@ app.delete('/api/tables/:tableName', requireAuth, requireTableAccess, requirePer
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.post('/api/tables/:tableName/move', requireAuth, requireTableAccess, requirePermission(
|
||||||
|
(permissions, req) => permissions.canMoveTables && canAccessTable(permissions, req.params.tableName, null, null, 'edit'),
|
||||||
|
'Access denied'
|
||||||
|
), async (req, res) => {
|
||||||
|
const { tableName } = req.params;
|
||||||
|
const { folder, name } = req.body || {};
|
||||||
|
const cleanName = String(name || '').trim();
|
||||||
|
const cleanFolder = String(folder || '').trim();
|
||||||
|
|
||||||
|
if (!isValidIdentifier(cleanName) || (cleanFolder && !isValidIdentifier(cleanFolder))) {
|
||||||
|
return res.status(400).json({ success: false, error: 'Invalid table or folder name' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextName = cleanFolder ? `${cleanFolder}__${cleanName}` : cleanName;
|
||||||
|
try {
|
||||||
|
await pool.query(`ALTER TABLE ${quoteIdentifier(tableName)} RENAME TO ${quoteIdentifier(nextName)}`);
|
||||||
|
res.json({ success: true, name: nextName });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ success: false, error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.post('/api/tables/:tableName/records', requireAuth, requireTableAccess, requirePermission(
|
app.post('/api/tables/:tableName/records', requireAuth, requireTableAccess, requirePermission(
|
||||||
(permissions, req) => permissions.canEdit && canAccessTable(permissions, req.params.tableName),
|
(permissions, req) => permissions.canEdit && canAccessTable(permissions, req.params.tableName, null, null, 'create'),
|
||||||
'Access denied'
|
'Access denied'
|
||||||
), async (req, res) => {
|
), async (req, res) => {
|
||||||
const { tableName } = req.params;
|
const { tableName } = req.params;
|
||||||
@@ -646,7 +933,7 @@ app.post('/api/tables/:tableName/records', requireAuth, requireTableAccess, requ
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.put('/api/tables/:tableName/records/:pk', requireAuth, requireTableAccess, requirePermission(
|
app.put('/api/tables/:tableName/records/:pk', requireAuth, requireTableAccess, requirePermission(
|
||||||
(permissions, req) => permissions.canEdit && canAccessTable(permissions, req.params.tableName),
|
(permissions, req) => permissions.canEdit && canAccessTable(permissions, req.params.tableName, null, null, 'edit'),
|
||||||
'Access denied'
|
'Access denied'
|
||||||
), async (req, res) => {
|
), async (req, res) => {
|
||||||
const { tableName, pk } = req.params;
|
const { tableName, pk } = req.params;
|
||||||
@@ -658,10 +945,13 @@ app.put('/api/tables/:tableName/records/:pk', requireAuth, requireTableAccess, r
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const primaryKey = await getPrimaryKeyColumn(tableName) || 'id';
|
const primaryKey = await getPrimaryKeyColumn(tableName);
|
||||||
const values = columns.map((column) => data[column]);
|
const values = columns.map((column) => data[column]);
|
||||||
const setClause = columns.map((col, i) => `${quoteIdentifier(col)} = $${i + 1}`).join(', ');
|
const setClause = columns.map((col, i) => `${quoteIdentifier(col)} = $${i + 1}`).join(', ');
|
||||||
const sql = `UPDATE ${quoteIdentifier(tableName)} SET ${setClause} WHERE ${quoteIdentifier(primaryKey)} = $${values.length + 1} RETURNING *`;
|
const whereClause = primaryKey
|
||||||
|
? `${quoteIdentifier(primaryKey)} = $${values.length + 1}`
|
||||||
|
: 'ctid::text = $' + (values.length + 1);
|
||||||
|
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]);
|
||||||
res.json({ success: true, data: result.rows[0] });
|
res.json({ success: true, data: result.rows[0] });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -670,14 +960,17 @@ app.put('/api/tables/:tableName/records/:pk', requireAuth, requireTableAccess, r
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.delete('/api/tables/:tableName/records/:pk', requireAuth, requireTableAccess, requirePermission(
|
app.delete('/api/tables/:tableName/records/:pk', requireAuth, requireTableAccess, requirePermission(
|
||||||
(permissions, req) => permissions.canDelete && canAccessTable(permissions, req.params.tableName),
|
(permissions, req) => permissions.canDelete && canAccessTable(permissions, req.params.tableName, null, null, 'delete'),
|
||||||
'Access denied'
|
'Access denied'
|
||||||
), async (req, res) => {
|
), async (req, res) => {
|
||||||
const { tableName, pk } = req.params;
|
const { tableName, pk } = req.params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const primaryKey = await getPrimaryKeyColumn(tableName) || 'id';
|
const primaryKey = await getPrimaryKeyColumn(tableName);
|
||||||
await pool.query(`DELETE FROM ${quoteIdentifier(tableName)} WHERE ${quoteIdentifier(primaryKey)} = $1`, [pk]);
|
const whereClause = primaryKey
|
||||||
|
? `${quoteIdentifier(primaryKey)} = $1`
|
||||||
|
: 'ctid::text = $1';
|
||||||
|
await pool.query(`DELETE FROM ${quoteIdentifier(tableName)} WHERE ${whereClause}`, [pk]);
|
||||||
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 });
|
||||||
@@ -685,7 +978,7 @@ app.delete('/api/tables/:tableName/records/:pk', requireAuth, requireTableAccess
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/tables/:tableName/columns', requireAuth, requireTableAccess, requirePermission(
|
app.post('/api/tables/:tableName/columns', requireAuth, requireTableAccess, requirePermission(
|
||||||
(permissions, req) => permissions.canEdit && canAccessTable(permissions, req.params.tableName),
|
(permissions, req) => permissions.canEdit && canAccessTable(permissions, req.params.tableName, null, null, 'edit'),
|
||||||
'Access denied'
|
'Access denied'
|
||||||
), async (req, res) => {
|
), async (req, res) => {
|
||||||
const { tableName } = req.params;
|
const { tableName } = req.params;
|
||||||
@@ -711,7 +1004,7 @@ app.post('/api/tables/:tableName/columns', requireAuth, requireTableAccess, requ
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.put('/api/tables/:tableName/columns/:columnName', requireAuth, requireTableAccess, requirePermission(
|
app.put('/api/tables/:tableName/columns/:columnName', requireAuth, requireTableAccess, requirePermission(
|
||||||
(permissions, req) => permissions.canEdit && canAccessTable(permissions, req.params.tableName),
|
(permissions, req) => permissions.canEdit && canAccessTable(permissions, req.params.tableName, null, null, 'edit'),
|
||||||
'Access denied'
|
'Access denied'
|
||||||
), async (req, res) => {
|
), async (req, res) => {
|
||||||
const { tableName, columnName } = req.params;
|
const { tableName, columnName } = req.params;
|
||||||
@@ -749,7 +1042,7 @@ app.put('/api/tables/:tableName/columns/:columnName', requireAuth, requireTableA
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.delete('/api/tables/:tableName/columns/:columnName', requireAuth, requireTableAccess, requirePermission(
|
app.delete('/api/tables/:tableName/columns/:columnName', requireAuth, requireTableAccess, requirePermission(
|
||||||
(permissions, req) => permissions.canDelete && canAccessTable(permissions, req.params.tableName),
|
(permissions, req) => permissions.canDelete && canAccessTable(permissions, req.params.tableName, null, null, 'delete'),
|
||||||
'Access denied'
|
'Access denied'
|
||||||
), async (req, res) => {
|
), async (req, res) => {
|
||||||
const { tableName, columnName } = req.params;
|
const { tableName, columnName } = req.params;
|
||||||
@@ -815,7 +1108,7 @@ app.get('/api/tables/:tableName/indexes', requireAuth, requireTableAccess, async
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/tables/:tableName/indexes', requireAuth, requireTableAccess, requirePermission(
|
app.post('/api/tables/:tableName/indexes', requireAuth, requireTableAccess, requirePermission(
|
||||||
(permissions, req) => permissions.canEdit && canAccessTable(permissions, req.params.tableName),
|
(permissions, req) => permissions.canEdit && canAccessTable(permissions, req.params.tableName, null, null, 'edit'),
|
||||||
'Access denied'
|
'Access denied'
|
||||||
), async (req, res) => {
|
), async (req, res) => {
|
||||||
const { tableName } = req.params;
|
const { tableName } = req.params;
|
||||||
|
|||||||
Reference in New Issue
Block a user