require('dotenv').config(); 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 bcrypt = require('bcryptjs'); // Initialize Express app const app = express(); const PORT = process.env.PORT || 3000; // Middleware app.use(cors()); app.use(express.json()); app.use(express.static(path.join(__dirname, 'public'))); // Session configuration app.use(session({ secret: process.env.SESSION_SECRET || 'default-secret-change-this', resave: false, saveUninitialized: false, cookie: { secure: process.env.NODE_ENV === 'production', httpOnly: true, maxAge: 1000 * 60 * 60 * 24 * 7 // 7 days } })); // Database connection pool const pool = new Pool({ host: process.env.DB_HOST || 'localhost', port: process.env.DB_PORT || 5432, database: process.env.DB_NAME || 'postgres', user: process.env.DB_USER || 'postgres', password: process.env.DB_PASSWORD || 'postgres', }); // Test database connection pool.connect((err, client, release) => { if (err) { console.error('❌ Error connecting to PostgreSQL:', err.message); console.log('Проверьте настройки в .env файле'); } else { console.log('✅ Connected to PostgreSQL database'); console.log(` Host: ${process.env.DB_HOST || 'localhost'}:${process.env.DB_PORT || 5432}`); console.log(` Database: ${process.env.DB_NAME || 'postgres'}`); release(); } }); // Initialize database schema async function initializeDatabase() { const client = await pool.connect(); try { // Check if users table exists, if not create it await client.query(` CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, password VARCHAR(255) NOT NULL, role VARCHAR(50) NOT NULL DEFAULT 'viewer', active BOOLEAN DEFAULT true, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS activity_logs ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, action VARCHAR(255), details JSONB, created_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_activity_logs_user_id ON activity_logs(user_id); CREATE INDEX IF NOT EXISTS idx_activity_logs_created_at ON activity_logs(created_at); `); // Create default admin user if doesn't exist const adminExists = await client.query( 'SELECT id FROM users WHERE email = $1', ['admin@example.com'] ); if (adminExists.rows.length === 0) { const hashedPassword = await bcrypt.hash('admin123', 10); await client.query( `INSERT INTO users (name, email, password, role, active) VALUES ($1, $2, $3, $4, $5)`, ['Admin', 'admin@example.com', hashedPassword, 'superadmin', true] ); console.log('✅ Created default admin user: admin@example.com / admin123'); } console.log('✅ Database schema initialized'); } catch (error) { console.error('Database initialization error:', error); } finally { client.release(); } } // Initialize on startup initializeDatabase(); // Routes const authRoutes = require('./src/routes/auth'); const userRoutes = require('./src/routes/users'); const dbRoutes = require('./src/routes/db-tables'); const adminRoutes = require('./src/routes/admin'); // API Routes app.use('/api/auth', authRoutes(pool)); app.use('/api/users', userRoutes(pool)); app.use('/api/db', dbRoutes(pool)); app.use('/api/admin', adminRoutes(pool)); // SPA catch-all route app.get('*', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); // Error handling middleware app.use((err, req, res, next) => { console.error('Error:', err); res.status(500).json({ success: false, message: 'Internal server error', error: process.env.NODE_ENV === 'development' ? err.message : undefined }); }); // Start server app.listen(PORT, () => { console.log(`\n🚀 PostgreSQL Admin Panel running at http://localhost:${PORT}`); console.log(`📦 Environment: ${process.env.NODE_ENV || 'development'}`); console.log(`\n💾 Database Connection:`); console.log(` Host: ${process.env.DB_HOST || 'localhost'}`); console.log(` Database: ${process.env.DB_NAME || 'postgres'}`); console.log('\n'); }); // Graceful shutdown process.on('SIGTERM', () => { console.log('SIGTERM received. Shutting down gracefully...'); pool.end(() => { console.log('Connection pool closed'); process.exit(0); }); }); module.exports = app; app.post('/api/login', async (req, res) => { const { username, password } = req.body; // 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, 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; } } // 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 app.post('/api/logout', (req, res) => { req.session.destroy(); res.json({ success: true }); }); // Check session app.get('/api/session', (req, res) => { if (req.session.authenticated) { 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, database: process.env.DB_NAME } }); } else { res.json({ authenticated: false }); } }); // 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, (SELECT COUNT(*) FROM information_schema.columns WHERE table_name = t.table_name) as column_count FROM information_schema.tables t 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( accessibleTables.map(async (table) => { try { const countResult = await pool.query(`SELECT COUNT(*) as count FROM "${table.name}"`); return { ...table, rows: parseInt(countResult.rows[0].count), size: 'calculating...' // Would need pg_size_pretty for real size }; } catch (e) { return { ...table, rows: 0, size: '0 KB' }; } }) ); res.json(tablesWithCounts); } catch (err) { res.status(500).json({ error: err.message }); } }); // 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 || ''; const filters = req.query.filters ? JSON.parse(req.query.filters) : []; const sortColumn = req.query.sortColumn || ''; const sortDirection = req.query.sortDirection || 'ASC'; const offset = (page - 1) * limit; try { let whereClause = ''; let params = []; let paramIndex = 1; // Search if (search) { const columnsResult = await pool.query(` SELECT column_name FROM information_schema.columns WHERE table_name = $1 AND table_schema = 'public' ORDER BY ordinal_position `, [tableName]); const columns = columnsResult.rows.map(row => row.column_name); const searchConditions = columns.map(col => `CAST("${col}" AS TEXT) ILIKE $${paramIndex}`).join(' OR '); whereClause = `WHERE ${searchConditions}`; params.push(`%${search}%`); paramIndex++; } // Filters if (filters && typeof filters === 'object') { const filterConditions = Object.entries(filters).map(([column, value]) => { if (value && value.trim()) { params.push(`%${value}%`); paramIndex++; // Use CAST to TEXT to support UUID and other non-text column types return `CAST("${column}" AS TEXT) ILIKE $${paramIndex - 1}`; } return null; }).filter(c => c); if (filterConditions.length > 0) { whereClause = whereClause ? `${whereClause} AND ${filterConditions.join(' AND ')}` : `WHERE ${filterConditions.join(' AND ')}`; } } // Get total count const countResult = await pool.query(`SELECT COUNT(*) as total FROM "${tableName}" ${whereClause}`, params); const total = parseInt(countResult.rows[0].total); // Build ORDER BY let orderBy = 'ORDER BY (SELECT NULL)'; // Default no order if (sortColumn) { orderBy = `ORDER BY "${sortColumn}" ${sortDirection}`; } // Get data const result = await pool.query(` SELECT * FROM "${tableName}" ${whereClause} ${orderBy} LIMIT $${paramIndex} OFFSET $${paramIndex + 1} `, [...params, limit, offset]); res.json({ data: result.rows, total, page, limit, totalPages: Math.ceil(total / limit) }); } catch (err) { res.status(500).json({ error: err.message }); } }); // Get table structure app.get('/api/tables/:tableName/structure', requireAuth, async (req, res) => { const { tableName } = req.params; try { const result = await pool.query(` SELECT c.column_name as name, c.data_type as type, c.is_nullable as nullable, c.column_default as default_value, CASE WHEN kcu.column_name IS NOT NULL THEN true ELSE false END as is_primary FROM information_schema.columns c LEFT JOIN information_schema.table_constraints tc ON tc.table_name = c.table_name AND tc.table_schema = c.table_schema AND tc.constraint_type = 'PRIMARY KEY' LEFT JOIN information_schema.key_column_usage kcu ON kcu.constraint_name = tc.constraint_name AND kcu.table_schema = tc.table_schema AND kcu.column_name = c.column_name WHERE c.table_name = $1 AND c.table_schema = 'public' ORDER BY c.ordinal_position `, [tableName]); res.json(result.rows); } catch (err) { res.status(500).json({ error: err.message }); } }); // 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}`; if (col.pk) def += ' PRIMARY KEY'; if (!col.nullable && !col.pk) def += ' NOT NULL'; return def; }).join(', '); const sql = `CREATE TABLE "${name}" (${columnsSQL})`; await pool.query(sql); res.json({ success: true, message: 'Table created' }); } catch (err) { res.status(500).json({ error: err.message }); } }); // 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}"`); res.json({ success: true, message: 'Table deleted' }); } catch (err) { res.status(500).json({ error: err.message }); } }); // Insert record 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(` SELECT column_name, data_type FROM information_schema.columns WHERE table_name = $1 AND table_schema = 'public' `, [tableName]); const structure = structureResult.rows; // Filter out empty UUIDs and generate if needed const filteredData = {}; for (const [key, value] of Object.entries(data)) { const colInfo = structure.find(col => col.column_name === key); if (colInfo && colInfo.data_type === 'uuid') { if (!value || value.trim() === '') { // Generate UUID for empty UUID fields filteredData[key] = require('crypto').randomUUID(); } else { // Validate UUID const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; if (uuidRegex.test(value)) { filteredData[key] = value; } else { // Invalid UUID, generate new one filteredData[key] = require('crypto').randomUUID(); } } } else if (value !== '') { filteredData[key] = value; } } const columns = Object.keys(filteredData); const values = Object.values(filteredData); const placeholders = values.map((_, i) => `$${i + 1}`).join(', '); const sql = `INSERT INTO "${tableName}" (${columns.map(c => `"${c}"`).join(', ')}) VALUES (${placeholders}) RETURNING *`; const result = await pool.query(sql, values); res.json({ success: true, data: result.rows[0] }); } catch (err) { res.status(500).json({ error: err.message }); } }); // 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(', '); try { const primaryKey = await getPrimaryKeyColumn(tableName) || 'id'; const sql = `UPDATE "${tableName}" SET ${setClause} WHERE "${primaryKey}" = $${values.length + 1} RETURNING *`; const result = await pool.query(sql, [...values, pk]); res.json({ success: true, data: result.rows[0] }); } catch (err) { res.status(500).json({ error: err.message }); } }); // 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]); res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); // Add a new column app.post('/api/tables/:tableName/columns', requireAuth, async (req, res) => { const { tableName } = req.params; const { name, type, nullable = true, defaultValue, primaryKey } = req.body; if (!name || !type) { return res.status(400).json({ error: 'Column name and type are required' }); } const parts = [`"${name}" ${type}`]; if (primaryKey) parts.push('PRIMARY KEY'); if (!nullable) parts.push('NOT NULL'); if (defaultValue !== undefined && defaultValue !== null && defaultValue !== '') { parts.push(`DEFAULT ${defaultValue}`); } try { await pool.query(`ALTER TABLE "${tableName}" ADD COLUMN ${parts.join(' ')}`); res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); // Modify an existing column app.put('/api/tables/:tableName/columns/:columnName', requireAuth, async (req, res) => { const { tableName, columnName } = req.params; const { type, nullable, defaultValue } = req.body; try { if (type) { await pool.query(`ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}" TYPE ${type}`); } if (typeof nullable === 'boolean') { const nullSql = nullable ? 'DROP NOT NULL' : 'SET NOT NULL'; await pool.query(`ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}" ${nullSql}`); } if (defaultValue !== undefined) { if (defaultValue === null || defaultValue === '') { await pool.query(`ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}" DROP DEFAULT`); } else { await pool.query(`ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}" SET DEFAULT ${defaultValue}`); } } res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); // Drop a column app.delete('/api/tables/:tableName/columns/:columnName', requireAuth, async (req, res) => { const { tableName, columnName } = req.params; try { await pool.query(`ALTER TABLE "${tableName}" DROP COLUMN IF EXISTS "${columnName}"`); res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); // Execute SQL app.post('/api/query', requireAuth, async (req, res) => { const { sql } = req.body; try { const result = await pool.query(sql); res.json({ success: true, rows: result.rows, rowCount: result.rowCount, command: result.command }); } catch (err) { res.status(500).json({ error: err.message }); } }); // Get indexes app.get('/api/tables/:tableName/indexes', requireAuth, async (req, res) => { const { tableName } = req.params; try { const result = await pool.query(` SELECT indexname as name, indexdef as definition FROM pg_indexes WHERE tablename = $1 `, [tableName]); const indexes = result.rows.map(row => ({ name: row.name, columns: row.definition.match(/\((.*?)\)/)?.[1] || 'unknown', unique: row.definition.includes('UNIQUE'), type: 'btree' })); res.json(indexes); } catch (err) { res.status(500).json({ error: err.message }); } }); // Create index app.post('/api/tables/:tableName/indexes', requireAuth, async (req, res) => { const { tableName } = req.params; const { name, columns, unique } = req.body; try { const uniqueStr = unique ? 'UNIQUE' : ''; const sql = `CREATE ${uniqueStr} INDEX "${name}" ON "${tableName}" (${columns})`; await pool.query(sql); res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); // Drop index app.delete('/api/indexes/:indexName', requireAuth, async (req, res) => { const { indexName } = req.params; try { await pool.query(`DROP INDEX IF EXISTS "${indexName}"`); res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); // Start server const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`🚀 Server running on http://localhost:${PORT}`); console.log(''); console.log('🔑 Default login credentials:'); console.log(` Username: ${process.env.ADMIN_USERNAME || 'admin'}`); console.log(` Password: ${process.env.ADMIN_PASSWORD || 'admin'}`); console.log(''); console.log('📝 Make sure to configure your database in .env file'); });