diff --git a/index.html b/index.html index daff040..45e5fb6 100644 --- a/index.html +++ b/index.html @@ -257,6 +257,11 @@ SELECT * FROM users LIMIT 10;">
+
+ + +

Если указано, имя таблицы будет создано как папка__имя. Оставьте пустым для создания в корне.

+
@@ -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" + } + ] +}