Files
pg-adminus/server.js
2026-03-18 17:48:16 +07:00

540 lines
18 KiB
JavaScript

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();
}
});
// Helper: get primary key column for a table (returns null if none)
async function getPrimaryKeyColumn(tableName) {
const result = await pool.query(`
SELECT kcu.column_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
AND tc.table_schema = kcu.table_schema
WHERE tc.constraint_type = 'PRIMARY KEY'
AND tc.table_name = $1
AND tc.table_schema = 'public'
LIMIT 1
`, [tableName]);
return result.rows[0]?.column_name || null;
}
// 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, search, filters and sort
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 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;
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;
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 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;
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');
});