From 5cb59a64d52301a8202e1fd6eab698f12ec8993a Mon Sep 17 00:00:00 2001 From: Verum Date: Wed, 18 Mar 2026 14:24:58 +0700 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B2=D1=8B=D0=B9=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B5=D0=BA=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 17 + .env.example | 17 + docker-compose.yml | 23 + index.html | 1152 ++++++++++++++++++++++++++++++++++++++++++++ package.json | 21 + server.js | 366 ++++++++++++++ 6 files changed, 1596 insertions(+) create mode 100644 .env create mode 100644 .env.example create mode 100644 docker-compose.yml create mode 100644 index.html create mode 100644 package.json create mode 100644 server.js diff --git a/.env b/.env new file mode 100644 index 0000000..f0a7314 --- /dev/null +++ b/.env @@ -0,0 +1,17 @@ +# Сервер +PORT=3000 +NODE_ENV=development + +# Админ-панель (логин/пароль для входа в веб-интерфейс) +ADMIN_USERNAME=admin +ADMIN_PASSWORD=admin + +# PostgreSQL Database Configuration (скрыто от пользователей) +DB_HOST=postgres +DB_PORT=5432 +DB_NAME=testdb +DB_USER=postgres +DB_PASSWORD=postgres + +# Сессия +SESSION_SECRET=your_random_secret_key_change_this diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..4e915ed --- /dev/null +++ b/.env.example @@ -0,0 +1,17 @@ +# Сервер +PORT=3000 +NODE_ENV=development + +# Админ-панель (логин/пароль для входа в веб-интерфейс) +ADMIN_USERNAME=admin +ADMIN_PASSWORD=admin + +# PostgreSQL Database Configuration (скрыто от пользователей) +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=postgres +DB_USER=postgres +DB_PASSWORD=your_database_password_here + +# Сессия +SESSION_SECRET=your_random_secret_key_change_this diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1c72c4e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +services: + postgres: + image: postgres:16 + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: testdb + ports: + - "5432:5432" + volumes: + - pg_data:/var/lib/postgresql/data + + app: + build: . + ports: + - "3000:3000" + env_file: + - .env + depends_on: + - postgres + +volumes: + pg_data: diff --git a/index.html b/index.html new file mode 100644 index 0000000..b9a831c --- /dev/null +++ b/index.html @@ -0,0 +1,1152 @@ + + + + + + PostgreSQL Admin Panel + + + + + + + + +
+
+
+
+ +
+

PostgreSQL Admin

+

Войдите для управления базой данных

+
+

🔒 Настройки БД скрыты в .env файле сервера

+
+
+ +
+
+ + +
+
+ + +
+ +
+ +
+

По умолчанию: admin / admin

+

Настройки подключения к БД в .env

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..8b5231b --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "postgres-admin-panel", + "version": "1.0.0", + "description": "PostgreSQL Admin Panel with .env configuration", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js" + }, + "dependencies": { + "express": "^4.18.2", + "pg": "^8.11.3", + "dotenv": "^16.3.1", + "cors": "^2.8.5", + "express-session": "^1.17.3", + "bcryptjs": "^2.4.3" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } +} \ No newline at end of file diff --git a/server.js b/server.js new file mode 100644 index 0000000..535a74d --- /dev/null +++ b/server.js @@ -0,0 +1,366 @@ +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 app = express(); + +// Middleware +app.use(cors()); +app.use(express.json()); +app.use(express.static('.')); + +// Session configuration +app.use(session({ + secret: process.env.SESSION_SECRET || 'default-secret-change-this', + resave: false, + saveUninitialized: false, + cookie: { secure: false } // Set to true if using HTTPS +})); + +// Database connection pool (uses .env configuration) +const pool = new Pool({ + host: process.env.DB_HOST, + port: process.env.DB_PORT, + database: process.env.DB_NAME, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, +}); + +// Test database connection on startup +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}:${process.env.DB_PORT}`); + console.log(` Database: ${process.env.DB_NAME}`); + release(); + } +}); + +// Middleware to check if user is authenticated +const requireAuth = (req, res, next) => { + if (req.session && req.session.authenticated) { + next(); + } else { + res.status(401).json({ error: 'Unauthorized' }); + } +}; + +// Login endpoint - checks admin credentials from .env +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 { + const client = await pool.connect(); + const result = await client.query('SELECT NOW() as time'); + client.release(); + + req.session.authenticated = true; + req.session.username = username; + + res.json({ + success: true, + message: 'Login successful', + dbInfo: { + host: process.env.DB_HOST, + port: process.env.DB_PORT, + database: process.env.DB_NAME, + connected: true, + serverTime: result.rows[0].time + } + }); + } catch (err) { + res.status(500).json({ + success: false, + error: 'Database connection failed', + details: err.message + }); + } + } else { + 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, + 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 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 + `); + + // Get row counts for each table + const tablesWithCounts = await Promise.all( + result.rows.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 +app.get('/api/tables/:tableName/data', requireAuth, async (req, res) => { + const { tableName } = req.params; + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 10; + const offset = (page - 1) * limit; + + try { + // Get total count + const countResult = await pool.query(`SELECT COUNT(*) as total FROM "${tableName}"`); + const total = parseInt(countResult.rows[0].total); + + // Get data + const result = await pool.query(` + SELECT * FROM "${tableName}" + LIMIT $1 OFFSET $2 + `, [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 + column_name as name, + data_type as type, + is_nullable as nullable, + column_default as default_value + FROM information_schema.columns + WHERE table_name = $1 AND table_schema = 'public' + ORDER BY 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; + + 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; + + 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 columns = Object.keys(data); + const values = Object.values(data); + const placeholders = values.map((_, i) => `$${i + 1}`).join(', '); + + try { + 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/:id', requireAuth, async (req, res) => { + const { tableName, id } = req.params; + 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 { + // Assuming id column is named 'id' - in production you'd need to detect PK + const sql = `UPDATE "${tableName}" SET ${setClause} WHERE id = $${values.length + 1} RETURNING *`; + const result = await pool.query(sql, [...values, id]); + 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/:id', requireAuth, async (req, res) => { + const { tableName, id } = req.params; + + try { + await pool.query(`DELETE FROM "${tableName}" WHERE id = $1`, [id]); + 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'); +}); \ No newline at end of file