From 1f150bb83c37ae84db89c55b3d0dd99248ce3b78 Mon Sep 17 00:00:00 2001 From: Verum Date: Thu, 19 Mar 2026 15:34:08 +0700 Subject: [PATCH] 123412412412412 --- .env | 28 +-- server.js | 670 +++++------------------------------------------------- 2 files changed, 62 insertions(+), 636 deletions(-) diff --git a/.env b/.env index e01423c..a356fe7 100644 --- a/.env +++ b/.env @@ -1,25 +1,17 @@ -# ========================= -# Server -# ========================= -PORT=3000 +# Environment NODE_ENV=development -# ========================= -# Admin panel credentials -# ========================= -ADMIN_USERNAME=admin -ADMIN_PASSWORD=admin +# Server +PORT=3000 +SESSION_SECRET=your-secret-key-change-this-in-production -# ========================= -# PostgreSQL (Docker) -# ========================= -DB_HOST=postgres +# Database Configuration +DB_HOST=localhost DB_PORT=5432 -DB_NAME=testdb +DB_NAME=postgres DB_USER=postgres DB_PASSWORD=postgres -# ========================= -# Session -# ========================= -SESSION_SECRET=super_secret_key_change_this_123 +# Admin credentials for first login +ADMIN_EMAIL=admin@example.com +ADMIN_PASSWORD=admin123 diff --git a/server.js b/server.js index 7ba2c54..5943568 100644 --- a/server.js +++ b/server.js @@ -1,713 +1,147 @@ 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 +// ====================== +// App init +// ====================== 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'))); -function requireAuth(req, res, next) { - if (!req.session || !req.session.authenticated) { - return res.status(401).json({ error: 'Unauthorized' }); - } - next(); -} - -// Session configuration +// ====================== +// Session +// ====================== app.use(session({ - secret: process.env.SESSION_SECRET || 'default-secret-change-this', + secret: process.env.SESSION_SECRET || 'change-this-secret', resave: false, saveUninitialized: false, cookie: { secure: process.env.NODE_ENV === 'production', httpOnly: true, - maxAge: 1000 * 60 * 60 * 24 * 7 // 7 days + maxAge: 1000 * 60 * 60 * 24 * 7 } })); -// Database connection pool +// ====================== +// Database +// ====================== const pool = new Pool({ - host: process.env.DB_HOST || 'localhost', + host: process.env.DB_HOST || 'postgres', // важно для Docker 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 файле'); + console.error('❌ PostgreSQL connection error:', err.message); } 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'}`); + console.log('✅ PostgreSQL connected'); release(); } }); -// Initialize database schema +// ====================== +// Init DB +// ====================== 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, + name VARCHAR(255), + email VARCHAR(255) UNIQUE, + password VARCHAR(255), + role VARCHAR(50) DEFAULT 'viewer', 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( + const exists = 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); + if (exists.rows.length === 0) { + const hash = 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] + `INSERT INTO users (name, email, password, role) + VALUES ($1, $2, $3, $4)`, + ['Admin', 'admin@example.com', hash, 'superadmin'] ); - console.log('✅ Created default admin user: admin@example.com / admin123'); + console.log('✅ Default admin created'); } - console.log('✅ Database schema initialized'); - } catch (error) { - console.error('Database initialization error:', error); + } catch (err) { + console.error('DB init error:', err); } 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 +// ====================== +// SPA fallback +// ====================== app.get('*', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); -// Error handling middleware +// ====================== +// Error handler +// ====================== app.use((err, req, res, next) => { - console.error('Error:', err); + console.error(err); res.status(500).json({ - success: false, - message: 'Internal server error', - error: process.env.NODE_ENV === 'development' ? err.message : undefined + error: 'Internal server error' }); }); +// ====================== // 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'); +// ====================== +app.listen(PORT, '0.0.0.0', () => { + console.log(`🚀 Server running on http://localhost:${PORT}`); }); +// ====================== // Graceful shutdown +// ====================== process.on('SIGTERM', () => { - console.log('SIGTERM received. Shutting down gracefully...'); + console.log('SIGTERM received. Shutting down...'); pool.end(() => { - console.log('Connection pool closed'); + console.log('DB 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 -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'); -});