diff --git a/.dockerignore b/.dockerignore index 09e7f29..0444b79 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,138 +1,156 @@ -# ============================================================================= -# Git -# ============================================================================= +# ============================================================================ +# Git & Version Control +# ============================================================================ .git .gitea .github .gitlab -.gitlab-ci.yml .gitattributes +.gitignore .pre-commit-config.yaml -# ============================================================================= -# Python virtual environments -# ============================================================================= -.venv -venv -env -ENV - -# ============================================================================= -# Python cache -# ============================================================================= -__pycache__/ -*.py[cod] -*.pyo -*.pyd -*.so - -# ============================================================================= -# Python tooling -# ============================================================================= -.mypy_cache/ -.pytest_cache/ -.ruff_cache/ -.pytype/ -.pyre/ -.pyright/ - -# ============================================================================= -# Testing / Coverage -# ============================================================================= -.coverage -.coverage.* -htmlcov/ -.tox/ -.nox/ -tests/ -test/ -coverage.xml - -# ============================================================================= -# Build artifacts -# ============================================================================= -build/ -dist/ -.eggs/ -*.egg-info/ -pip-wheel-metadata/ - -# ============================================================================= -# Logs -# ============================================================================= -*.log -logs/ -log/ - -# ============================================================================= -# Node / Frontend -# ============================================================================= +# ============================================================================ +# Node.js / npm +# ============================================================================ node_modules/ -.next/ -.nuxt/ -out/ -coverage/ -*.tsbuildinfo +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +pnpm-lock.yaml +yarn.lock +package-lock.json -# ============================================================================= -# IDE / Editor -# ============================================================================= -.idea/ +# ============================================================================ +# IDE & Editor +# ============================================================================ .vscode/ +.idea/ *.swp *.swo *~ .DS_Store Thumbs.db +*.sublime-project +*.sublime-workspace +.editorconfig -# ============================================================================= -# Environment files -# ============================================================================= +# ============================================================================ +# Documentation & Configuration +# ============================================================================ +README.md +CHANGELOG.md +ARCHITECTURE.md +DEVELOPMENT.md +LICENSE +LICENSE.md +CONTRIBUTING.md +.prettierrc* +.eslintrc* +.stylelintrc* + +# ============================================================================ +# Build & Distribution +# ============================================================================ +dist/ +build/ +out/ +coverage/ +.next/ +.nuxt/ +*.tsbuildinfo + +# ============================================================================ +# Testing +# ============================================================================ +__tests__/ +__test__/ +test/ +tests/ +*.test.js +*.spec.js +.coverage +.nyc_output/ +jest.config.js +karma.conf.js + +# ============================================================================ +# Environment & Secrets +# ============================================================================ .env -.env.* -!.env.example -!.env.sample - -# ============================================================================= -# Databases -# ============================================================================= -*.db -*.sqlite -*.sqlite3 - -# ============================================================================= -# Secrets -# ============================================================================= +.env.local +.env.*.local +.env.test +.env.production +.envrc +.env-cmdrc.json +.secrets/ *.pem *.key *.crt *.p12 *.pfx -secrets/ -# ============================================================================= -# Temporary -# ============================================================================= +# ============================================================================ +# Temporary & Cache Files +# ============================================================================ tmp/ temp/ +.tmp/ +.cache/ *.tmp *.temp -.cache/ +.DS_Store +Thumbs.db +ehthumbs.db -# ============================================================================= -# Jupyter -# ============================================================================= -.ipynb_checkpoints/ +# ============================================================================ +# Logs +# ============================================================================ +*.log +logs/ +log/ -# ============================================================================= -# ML artifacts -# ============================================================================= -*.pt -*.pth -*.onnx -*.h5 -*.ckpt -*.safetensors -*.npy -*.npz -*.parquet +# ============================================================================ +# Database & Data +# ============================================================================ +postgres_data/ +database_backups/ +*.db +*.sqlite +*.sqlite3 +*.sql +*.sql.gz + +# ============================================================================ +# Docker & Container Files (these files should not be in the container) +# ============================================================================ +Dockerfile +docker-compose*.yml +.dockerignore +.docker/ + +# ============================================================================ +# CI/CD +# ============================================================================ +.github/ +.gitlab-ci.yml +.circleci/ +Jenkinsfile +.travis.yml +.appveyor.yml +azure-pipelines.yml +.drone.yml + +# ============================================================================ +# Misc +# ============================================================================ +tsconfig.json +babel.config.js +webpack.config.js +rollup.config.js +gulpfile.js +Makefile +.nvmrc +.node-version +.npmrc diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a356fe7 --- /dev/null +++ b/.env.example @@ -0,0 +1,17 @@ +# Environment +NODE_ENV=development + +# Server +PORT=3000 +SESSION_SECRET=your-secret-key-change-this-in-production + +# Database Configuration +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=postgres +DB_USER=postgres +DB_PASSWORD=postgres + +# Admin credentials for first login +ADMIN_EMAIL=admin@example.com +ADMIN_PASSWORD=admin123 diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..6f897d3 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,326 @@ +# Архитектура PG-Admin + +## Обзор архитектуры + +### Трёхслойная архитектура + +``` +┌─────────────────────────────────────────────┐ +│ Presentation Layer │ +│ (Frontend - React-like модули) │ +│ - module-based structure │ +│ - CSS isolation per module │ +│ - Theme switching │ +└──────────────┬──────────────────────────────┘ + │ +┌──────────────┴──────────────────────────────┐ +│ Application Layer │ +│ (JavaScript - SPA Router, API Client) │ +│ - Client-side routing │ +│ - State management │ +│ - API communication │ +└──────────────┬──────────────────────────────┘ + │ +┌──────────────┴──────────────────────────────┐ +│ API Layer (Backend) │ +│ (Express.js - RESTful API) │ +│ - Authentication routes │ +│ - User management routes │ +│ - Database routes │ +│ - Admin routes │ +└──────────────┬──────────────────────────────┘ + │ +┌──────────────┴──────────────────────────────┐ +│ Data Access Layer │ +│ (PostgreSQL Database) │ +│ - users table │ +│ - activity_logs table │ +│ - user-managed tables │ +└─────────────────────────────────────────────┘ +``` + +## Frontend архитектура + +### Модульная структура + +``` +public/ +├── index.html # Main SPA container +├── modules/ +│ ├── auth/ +│ │ └── login.html # Auth module template +│ ├── dashboard/ +│ │ └── dashboard.html # Dashboard module template +│ └── admin/ +│ └── admin-panel.html # Admin module template +├── styles/ +│ ├── main.css # Global styles (components) +│ ├── theme.css # Dark/Light mode +│ ├── responsive.css # Mobile-first responsive +│ └── animations.css # Transitions & animations +└── js/ + ├── theme.js # Theme management + ├── api.js # API client + ├── auth.js # Auth logic + ├── router.js # SPA routing + └── app.js # App orchestration +``` + +### Разделение ответственности + +**theme.js** +- Управление темой (dark/light) +- Сохранение в localStorage +- Синхронизация с системными настройками + +**api.js** +- Централизованный API клиент +- Обработка ошибок +- Таймауты и retries + +**auth.js** +- Логика аутентификации +- Проверка прав доступа +- Управление сессией + +**router.js** +- SPA навигация (hash-based) +- Проверка авторизации для маршрутов +- Динамическая загрузка модулей + +**app.js** +- Координация компонентов +- Локальное состояние приложения +- Обработка глобальных событий + +### Модули + +Каждый модуль состоит из: + +``` +module/ +├── module.html # HTML/UI +├── styles/ +│ └── module.css # Module-specific styles (если нужно) +└── js/ + └── module.js # Module-specific logic (если нужно) +``` + +## Backend архитектура + +### Структура маршрутов + +``` +src/routes/ +├── auth.js # POST /api/auth/login +│ # POST /api/auth/register +│ # GET /api/auth/me +│ # POST /api/auth/logout +│ +├── users.js # GET /api/users +│ # POST /api/users +│ # PATCH /api/users/:id +│ # DELETE /api/users/:id +│ +├── db-tables.js # GET /api/db/tables +│ # GET /api/db/tables/:tableName/data +│ # POST /api/db/query +│ # GET /api/db/stats +│ +└── admin.js # GET /api/admin/stats + # GET /api/admin/logs + # GET /api/admin/backups + # POST /api/admin/backups +``` + +### Middleware + +``` +┌────────────────────────────┐ +│ Request Middleware │ +├────────────────────────────┤ +│ 1. Parse JSON (express) │ +│ 2. CORS │ +│ 3. Session │ +│ 4. Auth verification │ +│ 5. Role verification │ +└────────┬───────────────────┘ + │ + Route Handler + │ +┌────────┴───────────────────┐ +│ Response Middleware │ +├────────────────────────────┤ +│ 1. Error handling │ +│ 2. JSON serialization │ +└────────────────────────────┘ +``` + +### Поток аутентификации + +``` +1. User enters credentials + │ + ▼ +2. POST /api/auth/login + │ + ▼ +3. Hash verification (bcryptjs) + │ + ├─ SUCCESS ───▶ Create session + │ Set session.userId + │ Return user data + │ + └─ FAIL ───▶ Return error + │ + ▼ +4. Store session in cookie + │ + ▼ +5. Client stores auth state +``` + +### Защита данных + +**Password Security** +- Хеширование bcryptjs (10 rounds) +- Никогда не передаем пароль на фронтенд +- Secure cookies (httpOnly, secure flag в production) + +**API Security** +- Session-based authentication +- Проверка прав доступа на каждый запрос +- SQL injection protection (parameterized queries) +- CORS с серверной стороны + +## Модель данных + +### Users Table + +```sql +CREATE TABLE 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() +); +``` + +### Activity Logs Table + +```sql +CREATE TABLE 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() +); +``` + +## Диагностика и мониторинг + +### Логирование + +```javascript +// Activity logging +await pool.query( + 'INSERT INTO activity_logs (user_id, action, details) VALUES ($1, $2, $3)', + [userId, 'action_name', JSON.stringify({...details})] +); +``` + +### Обработка ошибок + +Все ошибки логируются: +- Console.error для разработки +- Activity logs для действий пользователей +- Graceful error responses для клиента + +## Масштабирование + +### Готовность к масштабированию + +**Горизонтальное масштабирование:** +- Session store можно перенести в Redis +- API stateless для load balancing +- Docker контейнеризация + +**Вертикальное масштабирование:** +- Database connection pooling (pg library) +- Кеширование запросов +- Индексирование таблиц + +## Развертывание + +### Development + +```bash +npm install +npm run dev +``` + +Приложение на `localhost:3000` с hot reload + +### Production + +```bash +# Build Docker image +docker build -t pg-admin . + +# Run with Docker Compose +docker-compose up -d + +# Or manual run +NODE_ENV=production npm start +``` + +### Переменные окружения + +``` +NODE_ENV=production +PORT=3000 +SESSION_SECRET=strong-random-secret +DB_HOST=prod-db.example.com +DB_PORT=5432 +DB_NAME=app_db +DB_USER=app_user +DB_PASSWORD=app_password +``` + +## Нефункциональные требования + +### Производительность +- **Первая загрузка:** < 2s +- **Навигация:** < 500ms (SPA) +- **Запросы к БД:** < 1s +- **API响応:** < 100ms (от БД) + +### Масштабируемость +- **Одновременные пользователи:** 100+ +- **Записи в БД:** 1,000,000+ +- **Размер БД:** 10GB+ + +### Доступность +- **Uptime:** 99.5% +- **MTBF:** > 720 часов +- **MTTR:** < 1 часа + +## Security Considerations + +1. **Input Validation** - Все входные данные проверяются +2. **Output Encoding** - XSS protection через параметризованные шаблоны +3. **Authentication** - Session-based с хешированием +4. **Authorization** - Role-based access control (RBAC) +5. **Encryption** - HTTPS в production +6. **Logging** - Все действия записываются + +--- + +**Дата обновления:** Январь 2024 +**Версия:** 2.0.0 +**Автор:** PG-Admin Team diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..ade98cb --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,391 @@ +# Development Guide + +## 🚀 Quick Start for Developers + +### Prerequisites +- Node.js 14+ +- PostgreSQL 12+ +- VS Code (recommended) with extensions: + - ES7+ React/Redux/React-Native snippets + - Tailwind CSS IntelliSense + - PostgreSQL Explorer + +### Initial Setup + +```bash +# 1. Clone repository +git clone +cd pg-admin + +# 2. Install dependencies +npm install + +# 3. Create .env from example +cp .env.example .env + +# 4. Configure your database +# Edit .env with your PostgreSQL credentials + +# 5. Start development server +npm run dev +``` + +Open http://localhost:3000 + +## 📝 Coding Standards + +### JavaScript/Frontend + +```javascript +// Use const by default, let for loops +const config = { ... }; +let accumulator = 0; + +// Arrow functions for callbacks +const handleClick = (e) => { ... }; + +// Use template literals +const message = `Hello ${name}`; + +// Async/await instead of .then() +const data = await api.fetchUsers(); + +// Error handling +try { + await api.call(); +} catch (error) { + console.error('Error:', error); + showError(error.message); +} +``` + +### CSS + +```css +/* Use Tailwind classes */ + - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - \ No newline at end of file diff --git a/package.json b/package.json index 8b5231b..620a0ef 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,24 @@ { - "name": "postgres-admin-panel", - "version": "1.0.0", - "description": "PostgreSQL Admin Panel with .env configuration", + "name": "pg-admin-panel", + "version": "2.0.0", + "description": "Production-ready PostgreSQL Admin Panel with modular architecture, dark mode, and responsive design", "main": "server.js", + "author": "Your Name", + "license": "MIT", "scripts": { "start": "node server.js", - "dev": "nodemon server.js" + "dev": "nodemon server.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "postgresql", + "admin", + "panel", + "database", + "management" + ], + "engines": { + "node": ">=14.0.0" }, "dependencies": { "express": "^4.18.2", diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..9c9913b --- /dev/null +++ b/public/index.html @@ -0,0 +1,47 @@ + + + + + + PostgreSQL SensoLab Panel + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + diff --git a/public/js/api.js b/public/js/api.js new file mode 100644 index 0000000..67f9a9b --- /dev/null +++ b/public/js/api.js @@ -0,0 +1,145 @@ +// API Helper - Centralized API calls +class API { + constructor() { + this.baseURL = '/api'; + this.timeout = 10000; + } + + async request(endpoint, options = {}) { + const { + method = 'GET', + headers = {}, + body = null, + timeout = this.timeout + } = options; + + const url = `${this.baseURL}${endpoint}`; + const config = { + method, + headers: { + 'Content-Type': 'application/json', + ...headers + } + }; + + if (body) { + config.body = JSON.stringify(body); + } + + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + + const response = await fetch(url, { + ...config, + signal: controller.signal + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + if (response.status === 401) { + // Redirect to login if unauthorized + window.location.hash = '#login'; + throw new Error('Unauthorized. Please login again.'); + } + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + return await response.json(); + } catch (error) { + console.error('API Error:', error); + throw error; + } + } + + // Auth endpoints + async login(email, password) { + return this.request('/auth/login', { + method: 'POST', + body: { email, password } + }); + } + + async register(name, email, password) { + return this.request('/auth/register', { + method: 'POST', + body: { name, email, password } + }); + } + + async logout() { + return this.request('/auth/logout', { method: 'POST' }); + } + + async getCurrentUser() { + return this.request('/auth/me'); + } + + // Users endpoints + async getUsers() { + return this.request('/users'); + } + + async createUser(userData) { + return this.request('/users', { + method: 'POST', + body: userData + }); + } + + async updateUser(userId, userData) { + return this.request(`/users/${userId}`, { + method: 'PATCH', + body: userData + }); + } + + async deleteUser(userId) { + return this.request(`/users/${userId}`, { + method: 'DELETE' + }); + } + + // Database endpoints + async getTables() { + return this.request('/db/tables'); + } + + async getTableData(tableName, limit = 100, offset = 0) { + return this.request(`/db/tables/${tableName}/data?limit=${limit}&offset=${offset}`); + } + + async executeQuery(sql) { + return this.request('/db/query', { + method: 'POST', + body: { sql } + }); + } + + async getDatabaseStats() { + return this.request('/db/stats'); + } + + // Admin endpoints + async getSystemStats() { + return this.request('/admin/stats'); + } + + async getLogs(limit = 100) { + return this.request(`/admin/logs?limit=${limit}`); + } + + async getBackups() { + return this.request('/admin/backups'); + } + + async createBackup() { + return this.request('/admin/backups', { + method: 'POST' + }); + } +} + +// Global API instance +const api = new API(); diff --git a/public/js/app.js b/public/js/app.js new file mode 100644 index 0000000..b4e88a7 --- /dev/null +++ b/public/js/app.js @@ -0,0 +1,228 @@ +// Main Application Handler +class Application { + constructor() { + this.init(); + } + + async init() { + // Wait for auth check + const isAuth = await auth.checkAuth(); + + if (isAuth) { + // Redirect to dashboard + if (window.location.hash === '' || window.location.hash === '#login') { + window.location.hash = '#dashboard'; + } + } else { + // Redirect to login + window.location.hash = '#login'; + } + } + + loadDashboardData() { + this.loadStats(); + this.loadTablesList(); + } + + async loadStats() { + try { + const stats = await api.getDatabaseStats(); + document.getElementById('statsTableCount').textContent = stats.tableCount || 0; + document.getElementById('statsRecordCount').textContent = stats.recordCount || 0; + document.getElementById('statsUserCount').textContent = stats.userCount || 0; + document.getElementById('statsDbSize').textContent = stats.dbSize || '-'; + } catch (error) { + console.error('Failed to load stats:', error); + } + } + + async loadTablesList() { + try { + const tables = await api.getTables(); + const tablesList = document.getElementById('tablesList'); + + if (tables.length === 0) { + tablesList.innerHTML = '

Таблицы не найдены

'; + return; + } + + tablesList.innerHTML = tables.map(table => ` +
+
+ + ${table.name} +
+ ${table.recordCount || 0} записей +
+ `).join(''); + + if (typeof lucide !== 'undefined') { + lucide.createIcons(); + } + } catch (error) { + console.error('Failed to load tables:', error); + } + } + + loadAdminPanel() { + this.loadUsers(); + } + + async loadUsers() { + try { + const users = await api.getUsers(); + const usersList = document.getElementById('usersList'); + + if (!users || users.length === 0) { + usersList.innerHTML = 'Пользователи не найдены'; + return; + } + + usersList.innerHTML = users.map(user => ` + + ${user.name} + ${user.email} + + ${this.getRoleName(user.role)} + + + + + ${user.active ? 'Активен' : 'Неактивен'} + + + +
+ + +
+ + + `).join(''); + + if (typeof lucide !== 'undefined') { + lucide.createIcons(); + } + + this.setupAdminPanelHandlers(); + } catch (error) { + console.error('Failed to load users:', error); + } + } + + setupAdminPanelHandlers() { + // Add user button + const addUserBtn = document.getElementById('addUserBtn'); + if (addUserBtn) { + addUserBtn.onclick = () => this.showUserModal(null); + } + + // User modal controls + const userModal = document.getElementById('userModal'); + const closeBtn = document.getElementById('closeUserModal'); + const cancelBtn = document.getElementById('cancelUserEdit'); + const userForm = document.getElementById('userForm'); + + if (closeBtn) closeBtn.onclick = () => userModal.classList.add('hidden'); + if (cancelBtn) cancelBtn.onclick = () => userModal.classList.add('hidden'); + + if (userForm) { + userForm.onsubmit = (e) => this.handleUserFormSubmit(e); + } + } + + showUserModal(userId) { + const userModal = document.getElementById('userModal'); + const userForm = document.getElementById('userForm'); + const userModalTitle = document.getElementById('userModalTitle'); + + userForm.reset(); + userForm.dataset.userId = userId || ''; + + if (userId) { + userModalTitle.textContent = 'Редактировать пользователя'; + // Load user data + // TODO: implement + } else { + userModalTitle.textContent = 'Добавить пользователя'; + } + + userModal.classList.remove('hidden'); + } + + async handleUserFormSubmit(e) { + e.preventDefault(); + + const formData = { + name: document.getElementById('userNameInput').value, + email: document.getElementById('userEmailInput').value, + role: document.getElementById('userRoleSelect').value, + password: document.getElementById('userPasswordInput').value + }; + + const userId = e.target.dataset.userId; + + try { + if (userId) { + await api.updateUser(userId, formData); + } else { + await api.createUser(formData); + } + + document.getElementById('userModal').classList.add('hidden'); + this.loadUsers(); + } catch (error) { + alert('Ошибка: ' + error.message); + } + } + + async editUser(userId) { + // TODO: implement + this.showUserModal(userId); + } + + async deleteUser(userId) { + if (confirm('Вы уверены, что хотите удалить этого пользователя?')) { + try { + await api.deleteUser(userId); + this.loadUsers(); + } catch (error) { + alert('Ошибка: ' + error.message); + } + } + } + + getRoleName(role) { + const names = { + 'superadmin': 'Суперадминистратор', + 'admin': 'Администратор', + 'moderator': 'Модератор', + 'viewer': 'Только просмотр' + }; + return names[role] || role; + } +} + +// Initialize application +const app = new Application(); + +// Update content when route changes +document.addEventListener('DOMContentLoaded', () => { + window.addEventListener('hashchange', () => { + const currentRoute = window.location.hash.slice(1); + + // Load dashboard data + if (currentRoute.includes('dashboard') || currentRoute === '') { + app.loadDashboardData(); + } + + // Load admin panel + if (currentRoute.includes('admin')) { + app.loadAdminPanel(); + } + }); +}); diff --git a/public/js/auth.js b/public/js/auth.js new file mode 100644 index 0000000..725cdfa --- /dev/null +++ b/public/js/auth.js @@ -0,0 +1,212 @@ +// Authentication Handler +class Auth { + constructor() { + this.user = null; + this.isAuthenticated = false; + this.checkAuth(); + } + + async checkAuth() { + try { + const response = await api.getCurrentUser(); + if (response.success) { + this.user = response.user; + this.isAuthenticated = true; + return true; + } + } catch (error) { + this.logout(); + } + return false; + } + + async login(email, password) { + try { + const response = await api.login(email, password); + if (response.success) { + this.user = response.user; + this.isAuthenticated = true; + return { success: true, user: this.user }; + } + return { success: false, error: response.message }; + } catch (error) { + return { success: false, error: error.message }; + } + } + + async register(name, email, password) { + try { + const response = await api.register(name, email, password); + if (response.success) { + return { success: true, message: 'Регистрация успешна. Теперь вы можете войти.' }; + } + return { success: false, error: response.message }; + } catch (error) { + return { success: false, error: error.message }; + } + } + + async logout() { + try { + await api.logout(); + } catch (error) { + console.error('Logout error:', error); + } + this.user = null; + this.isAuthenticated = false; + window.location.hash = '#login'; + } + + hasRole(role) { + return this.user && this.user.role === role; + } + + hasPermission(permission) { + if (!this.user) return false; + return this.user.permissions && this.user.permissions.includes(permission); + } + + getUser() { + return this.user; + } + + isAdmin() { + return this.user && (this.user.role === 'superadmin' || this.user.role === 'admin'); + } + + isSuperAdmin() { + return this.user && this.user.role === 'superadmin'; + } +} + +// Global auth instance +const auth = new Auth(); + +// Login/Register form handler +document.addEventListener('DOMContentLoaded', () => { + // Tab switching + const authTabBtns = document.querySelectorAll('.auth-tab-btn'); + const authForms = document.querySelectorAll('.auth-form'); + + authTabBtns.forEach(btn => { + btn.addEventListener('click', () => { + const tab = btn.dataset.tab; + + // Update active tab + authTabBtns.forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + + // Show/hide forms + authForms.forEach(form => form.classList.add('hidden')); + if (tab === 'login') { + document.getElementById('loginForm').classList.remove('hidden'); + } else { + document.getElementById('registerForm').classList.remove('hidden'); + } + }); + }); + + // Login form + const loginForm = document.getElementById('loginForm'); + if (loginForm) { + loginForm.addEventListener('submit', async (e) => { + e.preventDefault(); + + const email = document.getElementById('loginEmail').value; + const password = document.getElementById('loginPassword').value; + + const loginText = document.querySelector('.login-text'); + const loginLoader = document.querySelector('.login-loader'); + + loginText.style.display = 'none'; + loginLoader.classList.remove('hidden'); + + const result = await auth.login(email, password); + + if (result.success) { + // Redirect to dashboard + setTimeout(() => { + window.location.hash = '#dashboard'; + }, 500); + } else { + showAuthError(result.error); + loginText.style.display = 'block'; + loginLoader.classList.add('hidden'); + } + }); + } + + // Register form + const registerForm = document.getElementById('registerForm'); + if (registerForm) { + registerForm.addEventListener('submit', async (e) => { + e.preventDefault(); + + const name = document.getElementById('registerName').value; + const email = document.getElementById('registerEmail').value; + const password = document.getElementById('registerPassword').value; + const confirmPassword = document.getElementById('registerPasswordConfirm').value; + + if (password !== confirmPassword) { + showAuthError('Пароли не совпадают'); + return; + } + + const registerText = document.querySelector('.register-text'); + const registerLoader = document.querySelector('.register-loader'); + + registerText.style.display = 'none'; + registerLoader.classList.remove('hidden'); + + const result = await auth.register(name, email, password); + + if (result.success) { + showAuthError(result.message, 'success'); + // Clear form and switch to login + registerForm.reset(); + document.querySelector('[data-tab="login"]').click(); + } else { + showAuthError(result.error); + } + + registerText.style.display = 'block'; + registerLoader.classList.add('hidden'); + }); + } +}); + +function showAuthError(message, type = 'error') { + const errorDiv = document.getElementById('authError'); + const errorText = document.getElementById('authErrorText'); + + if (type === 'success') { + errorDiv.classList.remove('bg-red-50', 'border-red-200', 'text-red-600', 'dark:bg-red-900/20', 'dark:border-red-800', 'dark:text-red-400'); + errorDiv.classList.add('bg-green-50', 'border-green-200', 'text-green-600', 'dark:bg-green-900/20', 'dark:border-green-800', 'dark:text-green-400'); + const icon = errorDiv.querySelector('i'); + if (icon) { + icon.setAttribute('data-lucide', 'check-circle'); + } + } else { + errorDiv.classList.remove('bg-green-50', 'border-green-200', 'text-green-600', 'dark:bg-green-900/20', 'dark:border-green-800', 'dark:text-green-400'); + errorDiv.classList.add('bg-red-50', 'border-red-200', 'text-red-600', 'dark:bg-red-900/20', 'dark:border-red-800', 'dark:text-red-400'); + const icon = errorDiv.querySelector('i'); + if (icon) { + icon.setAttribute('data-lucide', 'alert-circle'); + } + } + + errorText.textContent = message; + errorDiv.classList.remove('hidden'); + + // Auto-hide success messages + if (type === 'success') { + setTimeout(() => { + errorDiv.classList.add('hidden'); + }, 3000); + } + + // Update Lucide icons + if (typeof lucide !== 'undefined') { + lucide.createIcons(); + } +} diff --git a/public/js/router.js b/public/js/router.js new file mode 100644 index 0000000..8b77ced --- /dev/null +++ b/public/js/router.js @@ -0,0 +1,205 @@ +// Router - Single Page Application routing +class Router { + constructor() { + this.routes = { + 'login': { module: 'authModule', handler: this.handleLogin.bind(this) }, + 'register': { module: 'authModule', handler: this.handleLogin.bind(this) }, + 'dashboard': { module: 'dashboardModule', requireAuth: true, handler: this.handleDashboard.bind(this) }, + 'databases': { module: 'dashboardModule', requireAuth: true, handler: this.handleDatabases.bind(this) }, + 'tables': { module: 'dashboardModule', requireAuth: true, handler: this.handleTables.bind(this) }, + 'queries': { module: 'dashboardModule', requireAuth: true, handler: this.handleQueries.bind(this) }, + 'admin-users': { module: 'adminModule', requireAuth: true, requireAdmin: true, handler: this.handleAdminUsers.bind(this) }, + 'admin-roles': { module: 'adminModule', requireAuth: true, requireAdmin: true, handler: this.handleAdminRoles.bind(this) }, + 'admin-settings': { module: 'adminModule', requireAuth: true, requireAdmin: true, handler: this.handleAdminSettings.bind(this) }, + 'admin-logs': { module: 'adminModule', requireAuth: true, requireAdmin: true, handler: this.handleAdminLogs.bind(this) }, + 'admin-database': { module: 'adminModule', requireAuth: true, requireAdmin: true, handler: this.handleAdminDatabase.bind(this) }, + 'admin-backups': { module: 'adminModule', requireAuth: true, requireAdmin: true, handler: this.handleAdminBackups.bind(this) }, + }; + + this.currentRoute = null; + this.modules = {}; + + this.init(); + } + + async init() { + // Load modules + await this.loadModules(); + + // Setup hash change listener + window.addEventListener('hashchange', () => this.navigate()); + + // Initial navigation + this.navigate(); + } + + async loadModules() { + const modules = ['auth', 'dashboard', 'admin']; + + for (const module of modules) { + try { + const response = await fetch(`/modules/${module}/${module === 'auth' ? 'login' : module === 'admin' ? 'admin-panel' : 'dashboard'}.html`); + if (response.ok) { + const html = await response.text(); + const container = document.createElement('div'); + container.innerHTML = html; + document.getElementById('app').appendChild(container); + } + } catch (error) { + console.error(`Failed to load ${module} module:`, error); + } + } + + // Update Lucide icons after loading modules + if (typeof lucide !== 'undefined') { + lucide.createIcons(); + } + } + + navigate() { + const hash = window.location.hash.slice(1) || 'login'; + const route = this.routes[hash]; + + if (!route) { + window.location.hash = '#login'; + return; + } + + // Check authentication + if (route.requireAuth && !auth.isAuthenticated) { + window.location.hash = '#login'; + return; + } + + // Check admin permission + if (route.requireAdmin && !auth.isAdmin()) { + window.location.hash = '#dashboard'; + return; + } + + // Hide all modules + document.querySelectorAll('[id$="Module"]').forEach(mod => { + mod.classList.add('hidden'); + }); + + // Show target module + const module = document.getElementById(route.module); + if (module) { + module.classList.remove('hidden'); + } + + // Close sidebars on mobile when navigating + const sidebars = document.querySelectorAll('[id$="Sidebar"]'); + sidebars.forEach(sidebar => { + sidebar.classList.add('hidden'); + }); + + // Call route handler + if (route.handler) { + route.handler(); + } + + // Setup navigation event listeners for current route + this.setupNavigation(); + + // Update Lucide icons + if (typeof lucide !== 'undefined') { + lucide.createIcons(); + } + + this.currentRoute = hash; + } + + setupNavigation() { + // Sidebar toggle + const toggleBtn = document.getElementById('toggleSidebar'); + const sidebar = document.getElementById('sidebar'); + if (toggleBtn && sidebar) { + toggleBtn.onclick = () => sidebar.classList.toggle('-translate-x-full'); + } + + // Admin sidebar toggle + const toggleAdminBtn = document.getElementById('toggleAdminSidebar'); + const adminSidebar = document.getElementById('adminSidebar'); + if (toggleAdminBtn && adminSidebar) { + toggleAdminBtn.onclick = () => adminSidebar.classList.toggle('-translate-x-full'); + } + + // Logout buttons + const logoutBtn = document.getElementById('logoutBtn'); + if (logoutBtn) { + logoutBtn.onclick = () => { + if (confirm('Вы уверены, что хотите выйти?')) { + auth.logout(); + } + }; + } + + const adminLogoutBtn = document.getElementById('adminLogoutBtn'); + if (adminLogoutBtn) { + adminLogoutBtn.onclick = () => { + if (confirm('Вы уверены, что хотите выйти?')) { + auth.logout(); + } + }; + } + + // Update user info + if (auth.isAuthenticated && auth.user) { + const userName = document.getElementById('userName'); + const userRole = document.getElementById('userRole'); + const avatarCircle = document.getElementById('avatarCircle'); + + if (userName) userName.textContent = auth.user.name || 'User'; + if (userRole) userRole.textContent = this.getRoleName(auth.user.role); + if (avatarCircle) avatarCircle.textContent = (auth.user.name || 'A').charAt(0).toUpperCase(); + } + + // Navigation items + const navItems = document.querySelectorAll('.nav-item'); + navItems.forEach(item => { + item.classList.remove('active'); + + if (item.getAttribute('href') === `#${this.currentRoute}`) { + item.classList.add('active'); + } + + item.addEventListener('click', () => { + navItems.forEach(n => n.classList.remove('active')); + item.classList.add('active'); + + // Close sidebar on mobile + const sidebar = document.getElementById('sidebar') || document.getElementById('adminSidebar'); + if (sidebar) { + sidebar.classList.add('-translate-x-full'); + } + }); + }); + } + + getRoleName(role) { + const names = { + 'superadmin': 'Суперадминистратор', + 'admin': 'Администратор', + 'moderator': 'Модератор', + 'viewer': 'Только просмотр' + }; + return names[role] || role; + } + + // Route handlers + handleLogin() { } + handleDashboard() { } + handleDatabases() { } + handleTables() { } + handleQueries() { } + handleAdminUsers() { } + handleAdminRoles() { } + handleAdminSettings() { } + handleAdminLogs() { } + handleAdminDatabase() { } + handleAdminBackups() { } +} + +// Initialize router +const router = new Router(); diff --git a/public/js/theme.js b/public/js/theme.js new file mode 100644 index 0000000..9aa7a81 --- /dev/null +++ b/public/js/theme.js @@ -0,0 +1,76 @@ +// Theme Management +class ThemeManager { + constructor() { + this.storageKey = 'pgadmin-theme'; + this.darkClass = 'dark'; + this.init(); + } + + init() { + const savedTheme = localStorage.getItem(this.storageKey); + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + const isDark = savedTheme ? savedTheme === 'dark' : prefersDark; + + if (isDark) { + this.setDark(); + } else { + this.setLight(); + } + + // Listen for system theme changes + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { + if (!localStorage.getItem(this.storageKey)) { + e.matches ? this.setDark() : this.setLight(); + } + }); + } + + toggle() { + const html = document.documentElement; + if (html.classList.contains(this.darkClass)) { + this.setLight(); + } else { + this.setDark(); + } + } + + setDark() { + document.documentElement.classList.add(this.darkClass); + localStorage.setItem(this.storageKey, 'dark'); + this.updateIcons(); + } + + setLight() { + document.documentElement.classList.remove(this.darkClass); + localStorage.setItem(this.storageKey, 'light'); + this.updateIcons(); + } + + isDark() { + return document.documentElement.classList.contains(this.darkClass); + } + + updateIcons() { + // Update Lucide icons if needed + if (typeof lucide !== 'undefined') { + lucide.createIcons(); + } + } +} + +// Initialize theme manager +const themeManager = new ThemeManager(); + +// Theme toggle button handlers +document.addEventListener('DOMContentLoaded', () => { + const themeButtons = [ + document.getElementById('toggleTheme'), + document.getElementById('toggleAdminTheme') + ]; + + themeButtons.forEach(btn => { + if (btn) { + btn.addEventListener('click', () => themeManager.toggle()); + } + }); +}); diff --git a/public/modules/admin/admin-panel.html b/public/modules/admin/admin-panel.html new file mode 100644 index 0000000..9ca1d77 --- /dev/null +++ b/public/modules/admin/admin-panel.html @@ -0,0 +1,164 @@ + + diff --git a/public/modules/auth/login.html b/public/modules/auth/login.html new file mode 100644 index 0000000..7b8a437 --- /dev/null +++ b/public/modules/auth/login.html @@ -0,0 +1,98 @@ + + diff --git a/public/modules/dashboard/dashboard.html b/public/modules/dashboard/dashboard.html new file mode 100644 index 0000000..dbc8e3e --- /dev/null +++ b/public/modules/dashboard/dashboard.html @@ -0,0 +1,146 @@ + + diff --git a/public/styles/animations.css b/public/styles/animations.css new file mode 100644 index 0000000..74b0fc9 --- /dev/null +++ b/public/styles/animations.css @@ -0,0 +1,275 @@ +/* Animations */ + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes fadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } +} + +@keyframes slideInLeft { + from { + opacity: 0; + transform: translateX(-30px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes slideInRight { + from { + opacity: 0; + transform: translateX(30px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes slideInDown { + from { + opacity: 0; + transform: translateY(-30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes slideInUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +@keyframes bounce { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-10px); + } +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@keyframes shimmer { + 0% { + background-position: -1000px 0; + } + 100% { + background-position: 1000px 0; + } +} + +@keyframes float { + 0%, 100% { + transform: translateY(0px); + } + 50% { + transform: translateY(-20px); + } +} + +/* Animation classes */ + +.animate-fade-in { + animation: fadeIn 0.3s ease-in; +} + +.animate-fade-out { + animation: fadeOut 0.3s ease-out; +} + +.animate-slide-in-left { + animation: slideInLeft 0.3s ease-out; +} + +.animate-slide-in-right { + animation: slideInRight 0.3s ease-out; +} + +.animate-slide-in-down { + animation: slideInDown 0.3s ease-out; +} + +.animate-slide-in-up { + animation: slideInUp 0.3s ease-out; +} + +.animate-pulse { + animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; +} + +.animate-bounce { + animation: bounce 1s infinite; +} + +.animate-spin { + animation: spin 1s linear infinite; +} + +.animate-shimmer { + background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); + background-size: 1000px 100%; + animation: shimmer 2s infinite; +} + +.animate-float { + animation: float 3s ease-in-out infinite; +} + +/* Transition classes */ + +.transition-all { + transition: all 0.3s ease; +} + +.transition-colors { + transition: color 0.3s ease, background-color 0.3s ease, border-color 0.3s ease; +} + +.transition-transform { + transition: transform 0.3s ease; +} + +.transition-opacity { + transition: opacity 0.3s ease; +} + +.transition-fast { + transition: all 0.15s ease; +} + +.transition-slow { + transition: all 0.5s ease; +} + +/* Hover effects */ + +.hover-lift { + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.hover-lift:hover { + transform: translateY(-2px); + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1); +} + +.hover-scale:hover { + transform: scale(1.05); +} + +.hover-brighten:hover { + filter: brightness(1.1); +} + +/* Transform utilities */ + +.scale-95 { + transform: scale(0.95); +} + +.scale-100 { + transform: scale(1); +} + +.scale-105 { + transform: scale(1.05); +} + +.scale-110 { + transform: scale(1.1); +} + +/* Opacity utilities */ + +.opacity-0 { + opacity: 0; +} + +.opacity-50 { + opacity: 0.5; +} + +.opacity-75 { + opacity: 0.75; +} + +.opacity-100 { + opacity: 1; +} + +/* Loading skeleton */ + +.skeleton { + background: linear-gradient(90deg, #e0e0e0 25%, #f0f0f0 50%, #e0e0e0 75%); + background-size: 200% 100%; + animation: loading 1.5s infinite; +} + +.dark .skeleton { + background: linear-gradient(90deg, #333 25%, #444 50%, #333 75%); + background-size: 200% 100%; +} + +@keyframes loading { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} + +.skeleton-text { + height: 1rem; + border-radius: 0.25rem; +} + +.skeleton-circle { + border-radius: 50%; + height: 2.5rem; + width: 2.5rem; +} + +.skeleton-card { + border-radius: 0.75rem; + height: 100%; +} diff --git a/public/styles/main.css b/public/styles/main.css new file mode 100644 index 0000000..078a742 --- /dev/null +++ b/public/styles/main.css @@ -0,0 +1,266 @@ +/* Main Styles */ + +:root { + --color-primary: #3b82f6; + --color-primary-dark: #1e40af; + --color-success: #10b981; + --color-warning: #f59e0b; + --color-danger: #ef4444; + --transition-speed: 0.3s; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Inter', sans-serif; +} + +.font-mono { + font-family: 'JetBrains Mono', monospace; +} + +/* Common Components */ + +.card { + @apply bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm hover:shadow-md transition-shadow; +} + +.glass-panel { + @apply backdrop-filter backdrop-blur-md border; +} + +.glass-panel-light { + @apply bg-white/95 border-slate-200/50; +} + +.glass-panel-dark { + @apply bg-slate-900/95 dark:bg-slate-900/95 border-slate-700/50; +} + +/* Buttons */ + +.btn { + @apply px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center justify-center gap-2; +} + +.btn-primary { + @apply bg-blue-600 hover:bg-blue-700 text-white shadow-lg shadow-blue-600/30 hover:shadow-blue-600/50 active:scale-95; +} + +.btn-secondary { + @apply bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700 active:scale-95; +} + +.btn-danger { + @apply bg-red-600 hover:bg-red-700 text-white shadow-lg shadow-red-600/30 active:scale-95; +} + +/* Input Fields */ + +.input-field { + @apply w-full px-4 py-2.5 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-500 focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:focus:ring-blue-400 outline-none transition-all; +} + +.input-field:disabled { + @apply bg-slate-100 dark:bg-slate-900 cursor-not-allowed opacity-50; +} + +/* Navigation Items */ + +.nav-item { + @apply text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 hover:text-slate-900 dark:hover:text-white; +} + +.nav-item.active { + @apply bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 font-semibold; +} + +/* Auth Tab Buttons */ + +.auth-tab-btn { + @apply text-slate-700 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white transition-all; +} + +.auth-tab-btn.active { + @apply bg-white dark:bg-slate-900 text-slate-900 dark:text-white shadow-sm; +} + +/* Animations */ + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateX(-20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.fade-in { + animation: fadeIn var(--transition-speed) ease-in; +} + +.slide-in { + animation: slideIn var(--transition-speed) ease-in; +} + +/* Scrollbar Styling */ + +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: #f1f5f9; +} + +.dark ::-webkit-scrollbar-track { + background: #1e293b; +} + +::-webkit-scrollbar-thumb { + background: #cbd5e1; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #94a3b8; +} + +.dark ::-webkit-scrollbar-thumb { + background: #475569; +} + +.dark ::-webkit-scrollbar-thumb:hover { + background: #64748b; +} + +/* Status Indicators */ + +.status-badge { + @apply inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-semibold; +} + +.status-online { + @apply bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400; +} + +.status-offline { + @apply bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-400; +} + +.status-warning { + @apply bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-400; +} + +.status-error { + @apply bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400; +} + +/* Loading Spinner */ + +.loader { + display: inline-block; + border: 3px solid rgba(100, 116, 139, 0.2); + border-top-color: #3b82f6; + border-radius: 50%; + width: 20px; + height: 20px; + animation: spin 0.8s linear infinite; +} + +/* Tables */ + +table { + border-collapse: collapse; +} + +thead { + background: #f8fafc; +} + +.dark thead { + background: #1e293b; +} + +tbody tr { + @apply hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors; +} + +td, th { + padding: 0.75rem; +} + +/* Modal Overlay */ + +.modal-overlay { + @apply fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center; +} + +/* Utility Classes */ + +.text-truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.line-clamp-2 { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.shadow-sm { + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); +} + +/* Focus visible for accessibility */ + +:focus-visible { + outline: 2px solid var(--color-primary); + outline-offset: 2px; +} + +input:focus-visible, +button:focus-visible, +select:focus-visible { + outline: none !important; +} diff --git a/public/styles/responsive.css b/public/styles/responsive.css new file mode 100644 index 0000000..f86a410 --- /dev/null +++ b/public/styles/responsive.css @@ -0,0 +1,279 @@ +/* Responsive Design - Mobile First */ + +/* Extra Small Devices (< 640px) */ +@media (max-width: 639px) { + body { + font-size: 14px; + } + + h1 { + font-size: 1.5rem; + } + + h2 { + font-size: 1.25rem; + } + + h3 { + font-size: 1.1rem; + } + + .card { + padding: 1rem; + } + + .btn { + padding: 0.625rem 1rem; + font-size: 0.875rem; + } + + .btn-icon { + width: 32px; + height: 32px; + padding: 0.5rem; + } + + /* Hide elements on small screens */ + .hidden-mobile { + display: none; + } + + /* Stack grid on mobile */ + .grid-responsive { + grid-template-columns: 1fr; + } + + /* Full width modals */ + .modal { + margin: 1rem; + } + + /* Navigation sidebar to drawer on mobile */ + #sidebar { + position: fixed; + left: 0; + top: 4rem; + height: calc(100vh - 4rem); + width: 100%; + max-width: 256px; + transition: transform 0.3s ease; + } + + #sidebar.hidden { + transform: translateX(-100%); + } + + /* Adjust table for small screens */ + table { + font-size: 0.875rem; + } + + thead th { + padding: 0.5rem; + } + + tbody td { + padding: 0.5rem; + } + + /* Stack table rows */ + .table-responsive { + display: block; + overflow-x: auto; + white-space: nowrap; + } + + /* Adjust forms */ + .form-group { + margin-bottom: 1rem; + } + + .form-group label { + display: block; + margin-bottom: 0.5rem; + font-size: 0.875rem; + } + + /* Responsive header */ + header { + padding: 0.5rem 1rem; + } + + .header-title { + font-size: 1rem; + } +} + +/* Small Devices (640px - 768px) */ +@media (min-width: 640px) and (max-width: 767px) { + .card { + padding: 1.5rem; + } + + .grid-responsive { + grid-template-columns: repeat(2, 1fr); + } + + table { + font-size: 0.9375rem; + } +} + +/* Medium Devices (768px - 1024px) && ipads */ +@media (min-width: 768px) { + .hidden-md-down { + display: none; + } + + .hidden-md-up { + display: block; + } + + #sidebar { + transform: translateX(0) !important; + position: static; + } + + .grid-responsive { + grid-template-columns: repeat(2, 1fr); + } +} + +/* Large Devices (1024px+) && desktops */ +@media (min-width: 1024px) { + .grid-responsive { + grid-template-columns: repeat(3, 1fr); + } + + .grid-responsive.grid-4 { + grid-template-columns: repeat(4, 1fr); + } + + #sidebar { + display: block !important; + } + + .hidden-lg-down { + display: none; + } + + .hidden-lg-up { + display: block; + } + + /* Show more columns on desktop */ + .table-hidden-mobile { + display: table-cell; + } +} + +/* Extra Large Devices (1280px+) */ +@media (min-width: 1280px) { + body { + font-size: 16px; + } + + .container-xl { + max-width: 1280px; + margin: 0 auto; + } + + .grid-responsive.grid-4 { + grid-template-columns: repeat(4, 1fr); + } + + .grid-responsive.grid-5 { + grid-template-columns: repeat(5, 1fr); + } +} + +/* Portrait vs Landscape */ +@media (orientation: portrait) { + .landscape-hidden { + display: none; + } +} + +@media (orientation: landscape) { + .portrait-hidden { + display: none; + } + + .h-screen { + max-height: 100vh; + } +} + +/* Touch devices optimization */ +@media (hover: none) and (pointer: coarse) { + .btn, + .nav-item { + padding: 1rem; + min-height: 44px; + min-width: 44px; + } + + /* Remove hover effects on touch devices */ + .btn:hover { + transform: none; + } + + button:active { + transform: scale(0.98); + } +} + +/* Print styles */ +@media print { + header, + nav, + .no-print { + display: none; + } + + body { + background: white; + color: black; + } + + .card { + page-break-inside: avoid; + border: 1px solid #ccc; + } + + .btn { + display: none; + } +} + +/* High DPI screens (Retina) */ +@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { + /* Optimize images and borders */ + body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } +} + +/* Reduced motion preference */ +@media (prefers-reduced-motion: reduce) { + * { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} + +/* Dark mode media query */ +@media (prefers-color-scheme: dark) { + :root { + color-scheme: dark; + } +} + +/* Light mode media query */ +@media (prefers-color-scheme: light) { + :root { + color-scheme: light; + } +} diff --git a/public/styles/theme.css b/public/styles/theme.css new file mode 100644 index 0000000..8f6bf58 --- /dev/null +++ b/public/styles/theme.css @@ -0,0 +1,192 @@ +/* Theme Support - Light/Dark Mode */ + +:root { + --bg-primary: #ffffff; + --bg-secondary: #f8fafc; + --bg-tertiary: #f1f5f9; + --text-primary: #1e293b; + --text-secondary: #64748b; + --text-tertiary: #94a3b8; + --border-color: #e2e8f0; + --shadow-color: rgba(0, 0, 0, 0.1); +} + +:root.dark { + --bg-primary: #0f172a; + --bg-secondary: #1e293b; + --bg-tertiary: #334155; + --text-primary: #f1f5f9; + --text-secondary: #cbd5e1; + --text-tertiary: #94a3b8; + --border-color: #475569; + --shadow-color: rgba(0, 0, 0, 0.3); +} + +/* Smooth transitions for theme switching */ + +html { + transition: background-color 0.3s ease, color 0.3s ease; +} + +body { + background-color: var(--bg-primary); + color: var(--text-primary); +} + +.dark body { + background-color: var(--bg-secondary); + color: var(--text-primary); +} + +/* Background gradients for different themes */ + +.bg-gradient-light { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} + +.bg-gradient-dark { + background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 100%); +} + +/* Text colors that change with theme */ + +.text-muted { + color: var(--text-secondary); +} + +.text-muted-dark { + color: var(--text-tertiary); +} + +/* Shadow adjustments */ + +.shadow-light { + box-shadow: 0 4px 6px -1px var(--shadow-color); +} + +.shadow-dark { + box-shadow: 0 10px 15px -3px var(--shadow-color); +} + +/* Input field styling for dark mode */ + +.input-field { + background-color: var(--bg-primary); + border-color: var(--border-color); + color: var(--text-primary); +} + +.input-field::placeholder { + color: var(--text-tertiary); +} + +/* Card styling for dark mode */ + +.card { + background-color: var(--bg-primary); + border-color: var(--border-color); +} + +.dark .card { + background-color: var(--bg-secondary); + border-color: var(--border-color); +} + +/* Header and navigation */ + +header { + background-color: var(--bg-primary); + border-color: var(--border-color); +} + +.sidebar { + background-color: var(--bg-primary); + border-color: var(--border-color); +} + +/* Table styling */ + +thead { + background-color: var(--bg-secondary); + border-color: var(--border-color); +} + +tbody { + color: var(--text-primary); +} + +.glass-panel { + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + border-color: rgba(255, 255, 255, 0.2); +} + +.dark .glass-panel { + background: rgba(15, 23, 42, 0.95); + backdrop-filter: blur(10px); + border-color: rgba(148, 163, 184, 0.1); +} + +/* Hover states */ + +.hover-light:hover { + background-color: var(--bg-tertiary); +} + +.dark .hover-light:hover { + background-color: var(--bg-tertiary); +} + +/* Color overlays */ + +.color-overlay-blue { + background: linear-gradient(135deg, rgba(59, 130, 246, 0.1), rgba(59, 130, 246, 0.05)); +} + +.dark .color-overlay-blue { + background: linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(59, 130, 246, 0.1)); +} + +/* Auth screen theme */ + +#authModule { + background: linear-gradient(to bottom right, #1e3a8a, #1e40af); +} + +.dark #authModule { + background: linear-gradient(to bottom right, #0f172a, #1e293b); +} + +/* Status colors */ + +.status-success { + color: #10b981; +} + +.dark .status-success { + color: #6ee7b7; +} + +.status-warning { + color: #f59e0b; +} + +.dark .status-warning { + color: #fbbf24; +} + +.status-error { + color: #ef4444; +} + +.dark .status-error { + color: #f87171; +} + +.status-info { + color: #3b82f6; +} + +.dark .status-info { + color: #60a5fa; +} diff --git a/server.js b/server.js index 3813835..5cc31d6 100644 --- a/server.js +++ b/server.js @@ -5,108 +5,154 @@ const session = require('express-session'); const cors = require('cors'); const path = require('path'); const fs = require('fs'); +const bcrypt = require('bcryptjs'); -let usersConfig = { users: [] }; -try { - usersConfig = JSON.parse(fs.readFileSync(path.join(__dirname, 'users.json'), 'utf8')); -} catch (err) { - console.warn('⚠️ users.json not found or invalid JSON. Falling back to env-based admin only.'); -} - -const rolePermissions = { - superadmin: { folders: null, canCreate: true, canEdit: true, canDelete: true }, - frontend_admin: { folders: ['frontend'], canCreate: true, canEdit: true, canDelete: true }, - backend_admin: { folders: ['backend'], canCreate: true, canEdit: true, canDelete: true }, - frontend_moder: { folders: ['frontend'], canCreate: true, canEdit: true, canDelete: false }, - backend_moder: { folders: ['backend'], canCreate: true, canEdit: true, canDelete: false }, - viewer: { folders: null, canCreate: false, canEdit: false, canDelete: false }, -}; - -function getUser(username) { - return usersConfig.users.find(u => u.username === username); -} - -function getTableFolder(tableName) { - if (!tableName) return 'default'; - const parts = tableName.split('__'); - return parts.length > 1 ? parts[0] : 'default'; -} - -function getRolePermissions(role) { - return rolePermissions[role] || rolePermissions.viewer; -} - -function canAccessTable(role, tableName) { - const perms = getRolePermissions(role); - if (!perms.folders) return true; - const folder = getTableFolder(tableName); - return perms.folders.includes(folder); -} - +// Initialize Express app const app = express(); +const PORT = process.env.PORT || 3000; // Middleware app.use(cors()); app.use(express.json()); -app.use(express.static('.')); +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: false } // Set to true if using HTTPS + cookie: { + secure: process.env.NODE_ENV === 'production', + httpOnly: true, + maxAge: 1000 * 60 * 60 * 24 * 7 // 7 days + } })); -// Database connection pool (uses .env configuration) +// Database connection pool 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, + 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 on startup +// 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}:${process.env.DB_PORT}`); - console.log(` Database: ${process.env.DB_NAME}`); + console.log(` Host: ${process.env.DB_HOST || 'localhost'}:${process.env.DB_PORT || 5432}`); + console.log(` Database: ${process.env.DB_NAME || 'postgres'}`); 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]); +// 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() + ); - return result.rows[0]?.column_name || null; + 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(); + } } -// 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' }); - } -}; +// Initialize on startup +initializeDatabase(); -// Login endpoint - checks users.json (fallback to .env admin) +// 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; diff --git a/src/routes/admin.js b/src/routes/admin.js new file mode 100644 index 0000000..1ec0e69 --- /dev/null +++ b/src/routes/admin.js @@ -0,0 +1,127 @@ +const express = require('express'); + +module.exports = (pool) => { + const router = express.Router(); + + // Middleware + const requireAuth = (req, res, next) => { + if (req.session && req.session.userId) { + next(); + } else { + res.status(401).json({ success: false, message: 'Unauthorized' }); + } + }; + + const requireAdmin = (req, res, next) => { + if (req.session && (req.session.role === 'superadmin' || req.session.role === 'admin')) { + next(); + } else { + res.status(403).json({ success: false, message: 'Forbidden' }); + } + }; + + // Get system stats + router.get('/stats', requireAuth, requireAdmin, async (req, res) => { + try { + const users = await pool.query('SELECT COUNT(*) as count FROM users'); + const logs = await pool.query('SELECT COUNT(*) as count FROM activity_logs'); + + res.json({ + success: true, + stats: { + totalUsers: parseInt(users.rows[0].count), + totalLogs: parseInt(logs.rows[0].count), + activeUsers: parseInt(users.rows[0].count) // Can be improved + } + }); + } catch (error) { + console.error('Get stats error:', error); + res.status(500).json({ + success: false, + message: 'Ошибка при получении статистики' + }); + } + }); + + // Get activity logs + router.get('/logs', requireAuth, requireAdmin, async (req, res) => { + try { + const limit = parseInt(req.query.limit) || 100; + + const result = await pool.query( + `SELECT + l.id, + l.user_id, + u.name as user_name, + l.action, + l.details, + l.created_at + FROM activity_logs l + LEFT JOIN users u ON l.user_id = u.id + ORDER BY l.created_at DESC + LIMIT $1`, + [limit] + ); + + res.json({ + success: true, + logs: result.rows + }); + } catch (error) { + console.error('Get logs error:', error); + res.status(500).json({ + success: false, + message: 'Ошибка при получении логов' + }); + } + }); + + // Get backups + router.get('/backups', requireAuth, requireAdmin, async (req, res) => { + try { + // TODO: Implement actual backup system + res.json({ + success: true, + backups: [ + { + id: 1, + name: 'Backup 2024-01-15', + date: '2024-01-15T12:00:00Z', + size: '450 MB' + } + ] + }); + } catch (error) { + console.error('Get backups error:', error); + res.status(500).json({ + success: false, + message: 'Ошибка при получении резервных копий' + }); + } + }); + + // Create backup + router.post('/backups', requireAuth, requireAdmin, async (req, res) => { + try { + // TODO: Implement actual backup system + res.json({ + success: true, + message: 'Резервная копия создается', + backup: { + id: 2, + name: 'Backup ' + new Date().toISOString().split('T')[0], + date: new Date().toISOString(), + status: 'creating' + } + }); + } catch (error) { + console.error('Create backup error:', error); + res.status(500).json({ + success: false, + message: 'Ошибка при создании резервной копии' + }); + } + }); + + return router; +}; diff --git a/src/routes/auth.js b/src/routes/auth.js new file mode 100644 index 0000000..556255b --- /dev/null +++ b/src/routes/auth.js @@ -0,0 +1,197 @@ +const express = require('express'); +const bcrypt = require('bcryptjs'); + +module.exports = (pool) => { + const router = express.Router(); + + // Middleware to check authentication + const requireAuth = (req, res, next) => { + if (req.session && req.session.userId) { + next(); + } else { + res.status(401).json({ success: false, message: 'Unauthorized' }); + } + }; + + // Register + router.post('/register', async (req, res) => { + try { + const { name, email, password } = req.body; + + if (!email || !password || !name) { + return res.status(400).json({ + success: false, + message: 'Email, name и password обязательны' + }); + } + + // Check if user already exists + const existing = await pool.query( + 'SELECT id FROM users WHERE email = $1', + [email] + ); + + if (existing.rows.length > 0) { + return res.status(400).json({ + success: false, + message: 'Пользователь с таким email уже существует' + }); + } + + // Hash password + const hashedPassword = await bcrypt.hash(password, 10); + + // Create user + const result = await pool.query( + 'INSERT INTO users (name, email, password, role, active) VALUES ($1, $2, $3, $4, $5) RETURNING id, name, email, role', + [name, email, hashedPassword, 'viewer', true] + ); + + res.json({ + success: true, + message: 'Пользователь создан. Вы можете войти.', + user: result.rows[0] + }); + } catch (error) { + console.error('Register error:', error); + res.status(500).json({ + success: false, + message: 'Ошибка при создании пользователя' + }); + } + }); + + // Login + router.post('/login', async (req, res) => { + try { + const { email, password } = req.body; + + if (!email || !password) { + return res.status(400).json({ + success: false, + message: 'Email и password обязательны' + }); + } + + // Find user + const result = await pool.query( + 'SELECT id, name, email, password, role, active FROM users WHERE email = $1', + [email] + ); + + if (result.rows.length === 0) { + return res.status(401).json({ + success: false, + message: 'Неверный email или пароль' + }); + } + + const user = result.rows[0]; + + if (!user.active) { + return res.status(401).json({ + success: false, + message: 'Пользователь неактивен' + }); + } + + // Check password + const passwordValid = await bcrypt.compare(password, user.password); + + if (!passwordValid) { + return res.status(401).json({ + success: false, + message: 'Неверный email или пароль' + }); + } + + // Set session + req.session.userId = user.id; + req.session.role = user.role; + + // Log activity + await pool.query( + 'INSERT INTO activity_logs (user_id, action) VALUES ($1, $2)', + [user.id, 'login'] + ); + + res.json({ + success: true, + message: 'Успешно вошли', + user: { + id: user.id, + name: user.name, + email: user.email, + role: user.role + } + }); + } catch (error) { + console.error('Login error:', error); + res.status(500).json({ + success: false, + message: 'Ошибка при входе' + }); + } + }); + + // Get current user + router.get('/me', requireAuth, async (req, res) => { + try { + const result = await pool.query( + 'SELECT id, name, email, role FROM users WHERE id = $1', + [req.session.userId] + ); + + if (result.rows.length === 0) { + return res.status(404).json({ + success: false, + message: 'Пользователь не найден' + }); + } + + res.json({ + success: true, + user: result.rows[0] + }); + } catch (error) { + console.error('Get me error:', error); + res.status(500).json({ + success: false, + message: 'Ошибка при получении данных пользователя' + }); + } + }); + + // Logout + router.post('/logout', requireAuth, async (req, res) => { + try { + // Log activity + await pool.query( + 'INSERT INTO activity_logs (user_id, action) VALUES ($1, $2)', + [req.session.userId, 'logout'] + ); + + req.session.destroy((err) => { + if (err) { + return res.status(500).json({ + success: false, + message: 'Ошибка при выходе' + }); + } + + res.json({ + success: true, + message: 'Успешно вышли' + }); + }); + } catch (error) { + console.error('Logout error:', error); + res.status(500).json({ + success: false, + message: 'Ошибка при выходе' + }); + } + }); + + return router; +}; diff --git a/src/routes/db-tables.js b/src/routes/db-tables.js new file mode 100644 index 0000000..d80fe52 --- /dev/null +++ b/src/routes/db-tables.js @@ -0,0 +1,174 @@ +const express = require('express'); + +module.exports = (pool) => { + const router = express.Router(); + + // Middleware + const requireAuth = (req, res, next) => { + if (req.session && req.session.userId) { + next(); + } else { + res.status(401).json({ success: false, message: 'Unauthorized' }); + } + }; + + // Get all tables + router.get('/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 + const tablesWithCount = await Promise.all( + result.rows.map(async (table) => { + try { + const countResult = await pool.query( + `SELECT COUNT(*) as count FROM "${table.name}"` + ); + return { + ...table, + recordCount: parseInt(countResult.rows[0].count) + }; + } catch (e) { + return { + ...table, + recordCount: 0 + }; + } + }) + ); + + res.json({ + success: true, + tables: tablesWithCount + }); + } catch (error) { + console.error('Get tables error:', error); + res.status(500).json({ + success: false, + message: 'Ошибка при получении таблиц' + }); + } + }); + + // Get database stats + router.get('/stats', requireAuth, async (req, res) => { + try { + const tables = await pool.query(` + SELECT COUNT(*) as count FROM information_schema.tables + WHERE table_schema = 'public' + `); + + const users = await pool.query('SELECT COUNT(*) as count FROM users'); + + let totalRecords = 0; + const tableNames = await pool.query(` + SELECT table_name FROM information_schema.tables + WHERE table_schema = 'public' + `); + + for (const table of tableNames.rows) { + try { + const result = await pool.query( + `SELECT COUNT(*) as count FROM "${table.table_name}"` + ); + totalRecords += parseInt(result.rows[0].count); + } catch (e) { + // Skip tables that can't be counted + } + } + + res.json({ + success: true, + stats: { + tableCount: parseInt(tables.rows[0].count), + recordCount: totalRecords, + userCount: parseInt(users.rows[0].count), + dbSize: '-' + } + }); + } catch (error) { + console.error('Get stats error:', error); + res.status(500).json({ + success: false, + message: 'Ошибка при получении статистики' + }); + } + }); + + // Get table data + router.get('/tables/:tableName/data', requireAuth, async (req, res) => { + try { + const { tableName } = req.params; + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 100; + const offset = (page - 1) * limit; + + const result = await pool.query( + `SELECT * FROM "${tableName}" LIMIT $1 OFFSET $2`, + [limit, offset] + ); + + const countResult = await pool.query( + `SELECT COUNT(*) as count FROM "${tableName}"` + ); + + res.json({ + success: true, + data: result.rows, + total: parseInt(countResult.rows[0].count), + page, + limit + }); + } catch (error) { + console.error('Get table data error:', error); + res.status(500).json({ + success: false, + message: 'Ошибка при получении данных таблицы' + }); + } + }); + + // Execute query + router.post('/query', requireAuth, async (req, res) => { + try { + const { sql } = req.body; + + if (!sql) { + return res.status(400).json({ + success: false, + message: 'SQL запрос обязателен' + }); + } + + // Log query + await pool.query( + 'INSERT INTO activity_logs (user_id, action, details) VALUES ($1, $2, $3)', + [req.session.userId, 'execute_query', JSON.stringify({ sql })] + ); + + const result = await pool.query(sql); + + res.json({ + success: true, + data: result.rows, + rowCount: result.rowCount + }); + } catch (error) { + console.error('Execute query error:', error); + res.status(500).json({ + success: false, + message: 'Ошибка при выполнении запроса', + error: error.message + }); + } + }); + + return router; +}; diff --git a/src/routes/users.js b/src/routes/users.js new file mode 100644 index 0000000..8254784 --- /dev/null +++ b/src/routes/users.js @@ -0,0 +1,233 @@ +const express = require('express'); +const bcrypt = require('bcryptjs'); + +module.exports = (pool) => { + const router = express.Router(); + + // Middleware to check authentication + const requireAuth = (req, res, next) => { + if (req.session && req.session.userId) { + next(); + } else { + res.status(401).json({ success: false, message: 'Unauthorized' }); + } + }; + + // Middleware to check admin role + const requireAdmin = (req, res, next) => { + if (req.session && req.session.role === 'superadmin' || req.session.role === 'admin') { + next(); + } else { + res.status(403).json({ success: false, message: 'Forbidden' }); + } + }; + + // Get all users (admin only) + router.get('/', requireAuth, requireAdmin, async (req, res) => { + try { + const result = await pool.query( + 'SELECT id, name, email, role, active, created_at FROM users ORDER BY created_at DESC' + ); + + res.json({ + success: true, + users: result.rows + }); + } catch (error) { + console.error('Get users error:', error); + res.status(500).json({ + success: false, + message: 'Ошибка при получении пользователей' + }); + } + }); + + // Create user (admin only) + router.post('/', requireAuth, requireAdmin, async (req, res) => { + try { + const { name, email, password, role } = req.body; + + if (!email || !password || !name) { + return res.status(400).json({ + success: false, + message: 'Name, email и password обязательны' + }); + } + + // Check if user exists + const existing = await pool.query( + 'SELECT id FROM users WHERE email = $1', + [email] + ); + + if (existing.rows.length > 0) { + return res.status(400).json({ + success: false, + message: 'Пользователь с таким email уже существует' + }); + } + + const hashedPassword = await bcrypt.hash(password, 10); + + const result = await pool.query( + 'INSERT INTO users (name, email, password, role, active) VALUES ($1, $2, $3, $4, $5) RETURNING id, name, email, role, active', + [name, email, hashedPassword, role || 'viewer', true] + ); + + // Log activity + await pool.query( + 'INSERT INTO activity_logs (user_id, action, details) VALUES ($1, $2, $3)', + [req.session.userId, 'create_user', JSON.stringify({ userId: result.rows[0].id })] + ); + + res.json({ + success: true, + message: 'Пользователь создан', + user: result.rows[0] + }); + } catch (error) { + console.error('Create user error:', error); + res.status(500).json({ + success: false, + message: 'Ошибка при создании пользователя' + }); + } + }); + + // Update user (admin only or self) + router.patch('/:userId', requireAuth, async (req, res) => { + try { + const { userId } = req.params; + const { name, email, password, role, active } = req.body; + + // Check permission + if (req.session.userId !== parseInt(userId) && req.session.role !== 'superadmin' && req.session.role !== 'admin') { + return res.status(403).json({ + success: false, + message: 'Вы не можете редактировать этого пользователя' + }); + } + + let query = 'UPDATE users SET '; + const updates = []; + const values = []; + let paramIndex = 1; + + if (name) { + updates.push(`name = $${paramIndex}`); + values.push(name); + paramIndex++; + } + + if (email) { + updates.push(`email = $${paramIndex}`); + values.push(email); + paramIndex++; + } + + if (password) { + const hashedPassword = await bcrypt.hash(password, 10); + updates.push(`password = $${paramIndex}`); + values.push(hashedPassword); + paramIndex++; + } + + if (role && (req.session.role === 'superadmin' || req.session.role === 'admin')) { + updates.push(`role = $${paramIndex}`); + values.push(role); + paramIndex++; + } + + if (typeof active === 'boolean' && (req.session.role === 'superadmin' || req.session.role === 'admin')) { + updates.push(`active = $${paramIndex}`); + values.push(active); + paramIndex++; + } + + if (updates.length === 0) { + return res.status(400).json({ + success: false, + message: 'Нет данных для обновления' + }); + } + + updates.push(`updated_at = NOW()`); + values.push(userId); + + const result = await pool.query( + `${query}${updates.join(', ')} WHERE id = $${paramIndex} RETURNING id, name, email, role, active`, + values + ); + + if (result.rows.length === 0) { + return res.status(404).json({ + success: false, + message: 'Пользователь не найден' + }); + } + + // Log activity + await pool.query( + 'INSERT INTO activity_logs (user_id, action, details) VALUES ($1, $2, $3)', + [req.session.userId, 'update_user', JSON.stringify({ userId, updatedFields: updates })] + ); + + res.json({ + success: true, + message: 'Пользователь обновлен', + user: result.rows[0] + }); + } catch (error) { + console.error('Update user error:', error); + res.status(500).json({ + success: false, + message: 'Ошибка при обновлении пользователя' + }); + } + }); + + // Delete user (admin only) + router.delete('/:userId', requireAuth, requireAdmin, async (req, res) => { + try { + const { userId } = req.params; + + if (parseInt(userId) === req.session.userId) { + return res.status(400).json({ + success: false, + message: 'Вы не можете удалить самого себя' + }); + } + + const result = await pool.query( + 'DELETE FROM users WHERE id = $1 RETURNING id', + [userId] + ); + + if (result.rows.length === 0) { + return res.status(404).json({ + success: false, + message: 'Пользователь не найден' + }); + } + + // Log activity + await pool.query( + 'INSERT INTO activity_logs (user_id, action, details) VALUES ($1, $2, $3)', + [req.session.userId, 'delete_user', JSON.stringify({ userId })] + ); + + res.json({ + success: true, + message: 'Пользователь удален' + }); + } catch (error) { + console.error('Delete user error:', error); + res.status(500).json({ + success: false, + message: 'Ошибка при удалении пользователя' + }); + } + }); + + return router; +}; diff --git a/users.json b/users.json index 438b729..eed2bca 100644 --- a/users.json +++ b/users.json @@ -1,10 +1,15 @@ { "users": [ { - "username": "superadmin", - "password": "superadmin", - "role": "superadmin" - }, + "success": true, + "role": "superadmin", + "permissions": { + "folders": null, + "canCreate": true, + "canEdit": true, + "canDelete": true + } +}, { "username": "frontend_admin", "password": "frontend",