@@ -510,6 +515,8 @@ SELECT * FROM users LIMIT 10;">
if (data.success) {
this.currentUser = {
username,
+ role: data.role,
+ permissions: data.permissions,
dbInfo: data.dbInfo
};
localStorage.setItem('pg_admin_session', JSON.stringify(this.currentUser));
@@ -545,6 +552,37 @@ SELECT * FROM users LIMIT 10;">
this.loadTables();
}
+ getPermissions() {
+ return this.currentUser?.permissions || { folders: null, canCreate: false, canEdit: false, canDelete: false };
+ }
+
+ getCurrentTableFolder() {
+ if (!this.currentTable) return null;
+ const parts = this.currentTable.split('__');
+ return parts.length > 1 ? parts[0] : 'default';
+ }
+
+ canCreate() {
+ const perms = this.getPermissions();
+ return perms.canCreate;
+ }
+
+ canEditTable() {
+ const perms = this.getPermissions();
+ const folder = this.getCurrentTableFolder();
+ if (!perms.canEdit) return false;
+ if (!perms.folders) return true;
+ return perms.folders.includes(folder);
+ }
+
+ canDeleteTable() {
+ const perms = this.getPermissions();
+ const folder = this.getCurrentTableFolder();
+ if (!perms.canDelete) return false;
+ if (!perms.folders) return true;
+ return perms.folders.includes(folder);
+ }
+
// Data Loading
async loadTables() {
try {
@@ -563,20 +601,38 @@ SELECT * FROM users LIMIT 10;">
renderTableList() {
const container = document.getElementById('tableList');
const search = document.getElementById('tableSearch').value.toLowerCase();
-
- container.innerHTML = this.tables
- .filter(t => t.name.toLowerCase().includes(search))
- .map(table => `
+
+ const filtered = this.tables.filter(t => t.name.toLowerCase().includes(search));
+ const grouped = filtered.reduce((acc, table) => {
+ const parts = table.name.split('__');
+ const folder = parts.length > 1 ? parts[0] : 'default';
+ if (!acc[folder]) acc[folder] = [];
+ acc[folder].push(table);
+ return acc;
+ }, {});
+
+ const folderOrder = Object.keys(grouped).sort();
+ container.innerHTML = folderOrder.map(folder => {
+ const label = folder === 'default' ? 'Общие' : folder;
+ const tablesHtml = grouped[folder].map(table => `
`).join('');
-
+
+ return `
+
+
${label}
+ ${tablesHtml}
+
+ `;
+ }).join('');
+
lucide.createIcons();
}
@@ -594,6 +650,12 @@ SELECT * FROM users LIMIT 10;">
document.getElementById('emptyState').classList.add('hidden');
document.getElementById('sqlPanel').classList.add('hidden');
document.getElementById('dataGrid').classList.remove('hidden');
+
+ // Update action buttons based on permissions
+ const addBtn = document.querySelector('#tableActions button[onclick="app.showAddRecordModal()"]');
+ const deleteTableBtn = document.querySelector('#tableActions button[onclick="app.deleteTable()"]');
+ if (addBtn) addBtn.style.display = this.canEditTable() ? '' : 'none';
+ if (deleteTableBtn) deleteTableBtn.style.display = this.canDeleteTable() ? '' : 'none';
// Load table structure to get primary key
await this.loadTableStructure();
@@ -658,10 +720,12 @@ SELECT * FROM users LIMIT 10;">
}
return `
${displayValue} | `;
}).join('');
+ const canEdit = this.canEditTable();
+ const canDelete = this.canDeleteTable();
const actions = pkValue ? `
-
-
+ ${canEdit ? `` : ''}
+ ${canDelete ? `` : ''}
|
` : '
- | ';
@@ -986,6 +1050,7 @@ SELECT * FROM users LIMIT 10;">
}
showCreateTableModal() {
+ document.getElementById('newTableFolder').value = '';
document.getElementById('newTableName').value = '';
document.getElementById('columnsContainer').innerHTML = '';
this.addColumnField(); // Add one default column
@@ -1026,12 +1091,15 @@ SELECT * FROM users LIMIT 10;">
}
createTable() {
- const name = document.getElementById('newTableName').value;
+ const folder = document.getElementById('newTableFolder').value.trim();
+ const name = document.getElementById('newTableName').value.trim();
if (!name) {
this.showToast('Введите название таблицы', 'error');
return;
}
+ const tableName = folder ? `${folder}__${name}` : name;
+
const columnElements = document.querySelectorAll('#columnsContainer > div');
const columns = Array.from(columnElements).map(div => {
const inputs = div.querySelectorAll('input, select');
@@ -1051,7 +1119,7 @@ SELECT * FROM users LIMIT 10;">
fetch('/api/tables', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ name, columns })
+ body: JSON.stringify({ name: tableName, columns })
})
.then(response => response.json())
.then(data => {
diff --git a/server.js b/server.js
index 86555ed..3813835 100644
--- a/server.js
+++ b/server.js
@@ -4,6 +4,44 @@ const { Pool } = require('pg');
const session = require('express-session');
const cors = require('cors');
const path = require('path');
+const fs = require('fs');
+
+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 app = express();
@@ -68,24 +106,26 @@ const requireAuth = (req, res, next) => {
}
};
-// Login endpoint - checks admin credentials from .env
+// Login endpoint - checks users.json (fallback to .env admin)
app.post('/api/login', async (req, res) => {
const { username, password } = req.body;
-
- // Check against .env credentials
- if (username === process.env.ADMIN_USERNAME && password === process.env.ADMIN_PASSWORD) {
- // Test database connection
+
+ // Try users.json first
+ const user = getUser(username);
+ if (user && password === user.password) {
try {
const client = await pool.connect();
const result = await client.query('SELECT NOW() as time');
client.release();
-
+
req.session.authenticated = true;
req.session.username = username;
-
+ req.session.role = user.role;
+
res.json({
success: true,
message: 'Login successful',
+ role: user.role,
dbInfo: {
host: process.env.DB_HOST,
port: process.env.DB_PORT,
@@ -94,19 +134,55 @@ app.post('/api/login', async (req, res) => {
serverTime: result.rows[0].time
}
});
+ return;
} catch (err) {
res.status(500).json({
success: false,
error: 'Database connection failed',
details: err.message
});
+ return;
}
- } else {
- res.status(401).json({
- success: false,
- error: 'Invalid credentials'
- });
}
+
+ // Fallback to env-based admin
+ if (username === process.env.ADMIN_USERNAME && password === process.env.ADMIN_PASSWORD) {
+ try {
+ const client = await pool.connect();
+ const result = await client.query('SELECT NOW() as time');
+ client.release();
+
+ req.session.authenticated = true;
+ req.session.username = username;
+ req.session.role = 'superadmin';
+
+ res.json({
+ success: true,
+ message: 'Login successful',
+ role: 'superadmin',
+ dbInfo: {
+ host: process.env.DB_HOST,
+ port: process.env.DB_PORT,
+ database: process.env.DB_NAME,
+ connected: true,
+ serverTime: result.rows[0].time
+ }
+ });
+ return;
+ } catch (err) {
+ res.status(500).json({
+ success: false,
+ error: 'Database connection failed',
+ details: err.message
+ });
+ return;
+ }
+ }
+
+ res.status(401).json({
+ success: false,
+ error: 'Invalid credentials'
+ });
});
// Logout
@@ -121,6 +197,8 @@ app.get('/api/session', (req, res) => {
res.json({
authenticated: true,
username: req.session.username,
+ role: req.session.role || 'viewer',
+ permissions: getRolePermissions(req.session.role),
dbInfo: {
host: process.env.DB_HOST,
port: process.env.DB_PORT,
@@ -135,6 +213,7 @@ app.get('/api/session', (req, res) => {
// Get all tables
app.get('/api/tables', requireAuth, async (req, res) => {
try {
+ const role = req.session.role || 'viewer';
const result = await pool.query(`
SELECT
table_name as name,
@@ -143,10 +222,13 @@ app.get('/api/tables', requireAuth, async (req, res) => {
WHERE table_schema = 'public'
ORDER BY table_name
`);
-
+
+ // Filter tables by access rights (folders)
+ const accessibleTables = result.rows.filter(table => canAccessTable(role, table.name));
+
// Get row counts for each table
const tablesWithCounts = await Promise.all(
- result.rows.map(async (table) => {
+ accessibleTables.map(async (table) => {
try {
const countResult = await pool.query(`SELECT COUNT(*) as count FROM "${table.name}"`);
return {
@@ -169,6 +251,13 @@ app.get('/api/tables', requireAuth, async (req, res) => {
// Get table data with pagination, search, filters and sort
app.get('/api/tables/:tableName/data', requireAuth, async (req, res) => {
const { tableName } = req.params;
+ const role = req.session.role || 'viewer';
+
+ // Check access for this table
+ if (!canAccessTable(role, tableName)) {
+ return res.status(403).json({ error: 'Access denied' });
+ }
+
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const search = req.query.search || '';
@@ -279,7 +368,14 @@ app.get('/api/tables/:tableName/structure', requireAuth, async (req, res) => {
// Create table
app.post('/api/tables', requireAuth, async (req, res) => {
const { name, columns } = req.body;
-
+ const role = req.session.role || 'viewer';
+ const folder = getTableFolder(name);
+
+ const perms = getRolePermissions(role);
+ if (!perms.canCreate || (perms.folders && !perms.folders.includes(folder))) {
+ return res.status(403).json({ error: 'Access denied' });
+ }
+
try {
let columnsSQL = columns.map(col => {
let def = `"${col.name}" ${col.type}`;
@@ -300,6 +396,13 @@ app.post('/api/tables', requireAuth, async (req, res) => {
// Delete table
app.delete('/api/tables/:tableName', requireAuth, async (req, res) => {
const { tableName } = req.params;
+ const role = req.session.role || 'viewer';
+ const folder = getTableFolder(tableName);
+ const perms = getRolePermissions(role);
+
+ if (!perms.canDelete || (perms.folders && !perms.folders.includes(folder))) {
+ return res.status(403).json({ error: 'Access denied' });
+ }
try {
await pool.query(`DROP TABLE IF EXISTS "${tableName}"`);
@@ -313,7 +416,14 @@ app.delete('/api/tables/:tableName', requireAuth, async (req, res) => {
app.post('/api/tables/:tableName/records', requireAuth, async (req, res) => {
const { tableName } = req.params;
const data = req.body;
-
+ const role = req.session.role || 'viewer';
+ const folder = getTableFolder(tableName);
+ const perms = getRolePermissions(role);
+
+ if (!perms.canEdit || (perms.folders && !perms.folders.includes(folder))) {
+ return res.status(403).json({ error: 'Access denied' });
+ }
+
try {
// Get table structure to check types
const structureResult = await pool.query(`
@@ -362,8 +472,14 @@ app.post('/api/tables/:tableName/records', requireAuth, async (req, res) => {
// Update record
app.put('/api/tables/:tableName/records/:pk', requireAuth, async (req, res) => {
const { tableName, pk } = req.params;
+ const role = req.session.role || 'viewer';
+ const folder = getTableFolder(tableName);
+ const perms = getRolePermissions(role);
+ if (!perms.canEdit || (perms.folders && !perms.folders.includes(folder))) {
+ return res.status(403).json({ error: 'Access denied' });
+ }
+
const data = req.body;
-
const columns = Object.keys(data);
const values = Object.values(data);
const setClause = columns.map((col, i) => `"${col}" = $${i + 1}`).join(', ');
@@ -381,7 +497,13 @@ app.put('/api/tables/:tableName/records/:pk', requireAuth, async (req, res) => {
// Delete record
app.delete('/api/tables/:tableName/records/:pk', requireAuth, async (req, res) => {
const { tableName, pk } = req.params;
-
+ const role = req.session.role || 'viewer';
+ const folder = getTableFolder(tableName);
+ const perms = getRolePermissions(role);
+ if (!perms.canDelete || (perms.folders && !perms.folders.includes(folder))) {
+ return res.status(403).json({ error: 'Access denied' });
+ }
+
try {
const primaryKey = await getPrimaryKeyColumn(tableName) || 'id';
await pool.query(`DELETE FROM "${tableName}" WHERE "${primaryKey}" = $1`, [pk]);
diff --git a/users.json b/users.json
new file mode 100644
index 0000000..438b729
--- /dev/null
+++ b/users.json
@@ -0,0 +1,34 @@
+{
+ "users": [
+ {
+ "username": "superadmin",
+ "password": "superadmin",
+ "role": "superadmin"
+ },
+ {
+ "username": "frontend_admin",
+ "password": "frontend",
+ "role": "frontend_admin"
+ },
+ {
+ "username": "backend_admin",
+ "password": "backend",
+ "role": "backend_admin"
+ },
+ {
+ "username": "frontend_moder",
+ "password": "mod123",
+ "role": "frontend_moder"
+ },
+ {
+ "username": "backend_moder",
+ "password": "mod123",
+ "role": "backend_moder"
+ },
+ {
+ "username": "viewer",
+ "password": "viewer",
+ "role": "viewer"
+ }
+ ]
+}