123412412412412
This commit is contained in:
28
.env
28
.env
@@ -1,25 +1,17 @@
|
|||||||
# =========================
|
# Environment
|
||||||
# Server
|
|
||||||
# =========================
|
|
||||||
PORT=3000
|
|
||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
|
|
||||||
# =========================
|
# Server
|
||||||
# Admin panel credentials
|
PORT=3000
|
||||||
# =========================
|
SESSION_SECRET=your-secret-key-change-this-in-production
|
||||||
ADMIN_USERNAME=admin
|
|
||||||
ADMIN_PASSWORD=admin
|
|
||||||
|
|
||||||
# =========================
|
# Database Configuration
|
||||||
# PostgreSQL (Docker)
|
DB_HOST=localhost
|
||||||
# =========================
|
|
||||||
DB_HOST=postgres
|
|
||||||
DB_PORT=5432
|
DB_PORT=5432
|
||||||
DB_NAME=testdb
|
DB_NAME=postgres
|
||||||
DB_USER=postgres
|
DB_USER=postgres
|
||||||
DB_PASSWORD=postgres
|
DB_PASSWORD=postgres
|
||||||
|
|
||||||
# =========================
|
# Admin credentials for first login
|
||||||
# Session
|
ADMIN_EMAIL=admin@example.com
|
||||||
# =========================
|
ADMIN_PASSWORD=admin123
|
||||||
SESSION_SECRET=super_secret_key_change_this_123
|
|
||||||
|
|||||||
670
server.js
670
server.js
@@ -1,713 +1,147 @@
|
|||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { Pool } = require('pg');
|
const { Pool } = require('pg');
|
||||||
const session = require('express-session');
|
const session = require('express-session');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
|
||||||
const bcrypt = require('bcryptjs');
|
const bcrypt = require('bcryptjs');
|
||||||
|
|
||||||
// Initialize Express app
|
// ======================
|
||||||
|
// App init
|
||||||
|
// ======================
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
// ======================
|
||||||
// Middleware
|
// Middleware
|
||||||
|
// ======================
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.static(path.join(__dirname, 'public')));
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
|
||||||
function requireAuth(req, res, next) {
|
// ======================
|
||||||
if (!req.session || !req.session.authenticated) {
|
// Session
|
||||||
return res.status(401).json({ error: 'Unauthorized' });
|
// ======================
|
||||||
}
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Session configuration
|
|
||||||
app.use(session({
|
app.use(session({
|
||||||
secret: process.env.SESSION_SECRET || 'default-secret-change-this',
|
secret: process.env.SESSION_SECRET || 'change-this-secret',
|
||||||
resave: false,
|
resave: false,
|
||||||
saveUninitialized: false,
|
saveUninitialized: false,
|
||||||
cookie: {
|
cookie: {
|
||||||
secure: process.env.NODE_ENV === 'production',
|
secure: process.env.NODE_ENV === 'production',
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
maxAge: 1000 * 60 * 60 * 24 * 7 // 7 days
|
maxAge: 1000 * 60 * 60 * 24 * 7
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Database connection pool
|
// ======================
|
||||||
|
// Database
|
||||||
|
// ======================
|
||||||
const pool = new Pool({
|
const pool = new Pool({
|
||||||
host: process.env.DB_HOST || 'localhost',
|
host: process.env.DB_HOST || 'postgres', // важно для Docker
|
||||||
port: process.env.DB_PORT || 5432,
|
port: process.env.DB_PORT || 5432,
|
||||||
database: process.env.DB_NAME || 'postgres',
|
database: process.env.DB_NAME || 'postgres',
|
||||||
user: process.env.DB_USER || 'postgres',
|
user: process.env.DB_USER || 'postgres',
|
||||||
password: process.env.DB_PASSWORD || 'postgres',
|
password: process.env.DB_PASSWORD || 'postgres',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test database connection
|
// Проверка подключения
|
||||||
pool.connect((err, client, release) => {
|
pool.connect((err, client, release) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error('❌ Error connecting to PostgreSQL:', err.message);
|
console.error('❌ PostgreSQL connection error:', err.message);
|
||||||
console.log('Проверьте настройки в .env файле');
|
|
||||||
} else {
|
} else {
|
||||||
console.log('✅ Connected to PostgreSQL database');
|
console.log('✅ PostgreSQL connected');
|
||||||
console.log(` Host: ${process.env.DB_HOST || 'localhost'}:${process.env.DB_PORT || 5432}`);
|
|
||||||
console.log(` Database: ${process.env.DB_NAME || 'postgres'}`);
|
|
||||||
release();
|
release();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize database schema
|
// ======================
|
||||||
|
// Init DB
|
||||||
|
// ======================
|
||||||
async function initializeDatabase() {
|
async function initializeDatabase() {
|
||||||
const client = await pool.connect();
|
const client = await pool.connect();
|
||||||
try {
|
try {
|
||||||
// Check if users table exists, if not create it
|
|
||||||
await client.query(`
|
await client.query(`
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
name VARCHAR(255) NOT NULL,
|
name VARCHAR(255),
|
||||||
email VARCHAR(255) UNIQUE NOT NULL,
|
email VARCHAR(255) UNIQUE,
|
||||||
password VARCHAR(255) NOT NULL,
|
password VARCHAR(255),
|
||||||
role VARCHAR(50) NOT NULL DEFAULT 'viewer',
|
role VARCHAR(50) 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()
|
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 exists = await client.query(
|
||||||
const adminExists = await client.query(
|
|
||||||
'SELECT id FROM users WHERE email = $1',
|
'SELECT id FROM users WHERE email = $1',
|
||||||
['admin@example.com']
|
['admin@example.com']
|
||||||
);
|
);
|
||||||
|
|
||||||
if (adminExists.rows.length === 0) {
|
if (exists.rows.length === 0) {
|
||||||
const hashedPassword = await bcrypt.hash('admin123', 10);
|
const hash = await bcrypt.hash('admin123', 10);
|
||||||
await client.query(
|
await client.query(
|
||||||
`INSERT INTO users (name, email, password, role, active)
|
`INSERT INTO users (name, email, password, role)
|
||||||
VALUES ($1, $2, $3, $4, $5)`,
|
VALUES ($1, $2, $3, $4)`,
|
||||||
['Admin', 'admin@example.com', hashedPassword, 'superadmin', true]
|
['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 (err) {
|
||||||
} catch (error) {
|
console.error('DB init error:', err);
|
||||||
console.error('Database initialization error:', error);
|
|
||||||
} finally {
|
} finally {
|
||||||
client.release();
|
client.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize on startup
|
|
||||||
initializeDatabase();
|
initializeDatabase();
|
||||||
|
|
||||||
|
// ======================
|
||||||
// Routes
|
// Routes
|
||||||
|
// ======================
|
||||||
const authRoutes = require('./src/routes/auth');
|
const authRoutes = require('./src/routes/auth');
|
||||||
const userRoutes = require('./src/routes/users');
|
const userRoutes = require('./src/routes/users');
|
||||||
const dbRoutes = require('./src/routes/db-tables');
|
const dbRoutes = require('./src/routes/db-tables');
|
||||||
const adminRoutes = require('./src/routes/admin');
|
const adminRoutes = require('./src/routes/admin');
|
||||||
|
|
||||||
// API Routes
|
|
||||||
app.use('/api/auth', authRoutes(pool));
|
app.use('/api/auth', authRoutes(pool));
|
||||||
app.use('/api/users', userRoutes(pool));
|
app.use('/api/users', userRoutes(pool));
|
||||||
app.use('/api/db', dbRoutes(pool));
|
app.use('/api/db', dbRoutes(pool));
|
||||||
app.use('/api/admin', adminRoutes(pool));
|
app.use('/api/admin', adminRoutes(pool));
|
||||||
|
|
||||||
// SPA catch-all route
|
// ======================
|
||||||
|
// SPA fallback
|
||||||
|
// ======================
|
||||||
app.get('*', (req, res) => {
|
app.get('*', (req, res) => {
|
||||||
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Error handling middleware
|
// ======================
|
||||||
|
// Error handler
|
||||||
|
// ======================
|
||||||
app.use((err, req, res, next) => {
|
app.use((err, req, res, next) => {
|
||||||
console.error('Error:', err);
|
console.error(err);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
error: 'Internal server error'
|
||||||
message: 'Internal server error',
|
|
||||||
error: process.env.NODE_ENV === 'development' ? err.message : undefined
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ======================
|
||||||
// Start server
|
// Start server
|
||||||
app.listen(PORT, () => {
|
// ======================
|
||||||
console.log(`\n🚀 PostgreSQL Admin Panel running at http://localhost:${PORT}`);
|
app.listen(PORT, '0.0.0.0', () => {
|
||||||
console.log(`📦 Environment: ${process.env.NODE_ENV || 'development'}`);
|
console.log(`🚀 Server running on http://localhost:${PORT}`);
|
||||||
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
|
// Graceful shutdown
|
||||||
|
// ======================
|
||||||
process.on('SIGTERM', () => {
|
process.on('SIGTERM', () => {
|
||||||
console.log('SIGTERM received. Shutting down gracefully...');
|
console.log('SIGTERM received. Shutting down...');
|
||||||
pool.end(() => {
|
pool.end(() => {
|
||||||
console.log('Connection pool closed');
|
console.log('DB pool closed');
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = app;
|
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');
|
|
||||||
});
|
|
||||||
|
|||||||
Reference in New Issue
Block a user