Compare commits
10 Commits
c9b8be54c5
...
96635dbcf2
| Author | SHA1 | Date | |
|---|---|---|---|
| 96635dbcf2 | |||
| 6d7d86befd | |||
| ead769e9d1 | |||
| ad117fe837 | |||
| a9020b0388 | |||
| 443afe9f6a | |||
| 9442176e5c | |||
| d1ba0eb58b | |||
| 9fa65ba4af | |||
| 9f9d41f14a |
236
.dockerignore
236
.dockerignore
@@ -1,138 +1,156 @@
|
|||||||
# =============================================================================
|
# ============================================================================
|
||||||
# Git
|
# Git & Version Control
|
||||||
# =============================================================================
|
# ============================================================================
|
||||||
.git
|
.git
|
||||||
.gitea
|
.gitea
|
||||||
.github
|
.github
|
||||||
.gitlab
|
.gitlab
|
||||||
.gitlab-ci.yml
|
|
||||||
.gitattributes
|
.gitattributes
|
||||||
|
.gitignore
|
||||||
.pre-commit-config.yaml
|
.pre-commit-config.yaml
|
||||||
|
|
||||||
# =============================================================================
|
# ============================================================================
|
||||||
# Python virtual environments
|
# Node.js / npm
|
||||||
# =============================================================================
|
# ============================================================================
|
||||||
.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_modules/
|
node_modules/
|
||||||
.next/
|
npm-debug.log*
|
||||||
.nuxt/
|
yarn-debug.log*
|
||||||
out/
|
yarn-error.log*
|
||||||
coverage/
|
pnpm-debug.log*
|
||||||
*.tsbuildinfo
|
pnpm-lock.yaml
|
||||||
|
yarn.lock
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
# =============================================================================
|
# ============================================================================
|
||||||
# IDE / Editor
|
# IDE & Editor
|
||||||
# =============================================================================
|
# ============================================================================
|
||||||
.idea/
|
|
||||||
.vscode/
|
.vscode/
|
||||||
|
.idea/
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
*~
|
*~
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
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.*
|
.env.local
|
||||||
!.env.example
|
.env.*.local
|
||||||
!.env.sample
|
.env.test
|
||||||
|
.env.production
|
||||||
# =============================================================================
|
.envrc
|
||||||
# Databases
|
.env-cmdrc.json
|
||||||
# =============================================================================
|
.secrets/
|
||||||
*.db
|
|
||||||
*.sqlite
|
|
||||||
*.sqlite3
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# Secrets
|
|
||||||
# =============================================================================
|
|
||||||
*.pem
|
*.pem
|
||||||
*.key
|
*.key
|
||||||
*.crt
|
*.crt
|
||||||
*.p12
|
*.p12
|
||||||
*.pfx
|
*.pfx
|
||||||
secrets/
|
|
||||||
|
|
||||||
# =============================================================================
|
# ============================================================================
|
||||||
# Temporary
|
# Temporary & Cache Files
|
||||||
# =============================================================================
|
# ============================================================================
|
||||||
tmp/
|
tmp/
|
||||||
temp/
|
temp/
|
||||||
|
.tmp/
|
||||||
|
.cache/
|
||||||
*.tmp
|
*.tmp
|
||||||
*.temp
|
*.temp
|
||||||
.cache/
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
ehthumbs.db
|
||||||
|
|
||||||
# =============================================================================
|
# ============================================================================
|
||||||
# Jupyter
|
# Logs
|
||||||
# =============================================================================
|
# ============================================================================
|
||||||
.ipynb_checkpoints/
|
*.log
|
||||||
|
logs/
|
||||||
|
log/
|
||||||
|
|
||||||
# =============================================================================
|
# ============================================================================
|
||||||
# ML artifacts
|
# Database & Data
|
||||||
# =============================================================================
|
# ============================================================================
|
||||||
*.pt
|
postgres_data/
|
||||||
*.pth
|
database_backups/
|
||||||
*.onnx
|
*.db
|
||||||
*.h5
|
*.sqlite
|
||||||
*.ckpt
|
*.sqlite3
|
||||||
*.safetensors
|
*.sql
|
||||||
*.npy
|
*.sql.gz
|
||||||
*.npz
|
|
||||||
*.parquet
|
# ============================================================================
|
||||||
|
# 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
|
||||||
|
|||||||
17
.env.example
Normal file
17
.env.example
Normal file
@@ -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
|
||||||
326
ARCHITECTURE.md
Normal file
326
ARCHITECTURE.md
Normal file
@@ -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
|
||||||
391
DEVELOPMENT.md
Normal file
391
DEVELOPMENT.md
Normal file
@@ -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 <repo-url>
|
||||||
|
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 */
|
||||||
|
<button class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
|
||||||
|
|
||||||
|
/* Create custom utility classes in main.css */
|
||||||
|
.card {
|
||||||
|
@apply bg-white dark:bg-slate-900 border border-slate-200 rounded-xl shadow-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Use CSS custom properties for theming */
|
||||||
|
:root {
|
||||||
|
--color-primary: #3b82f6;
|
||||||
|
--color-success: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--color-primary: #60a5fa;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### SQL
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Use parameterized queries to prevent SQL injection
|
||||||
|
pool.query('SELECT * FROM users WHERE id = $1', [userId]);
|
||||||
|
|
||||||
|
-- Use clear naming conventions
|
||||||
|
SELECT user_id, created_at FROM activity_logs ORDER BY created_at DESC;
|
||||||
|
|
||||||
|
-- Create indexes for frequently queried columns
|
||||||
|
CREATE INDEX idx_activity_logs_user_id ON activity_logs(user_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🏗️ Adding New Features
|
||||||
|
|
||||||
|
### Adding a New Module
|
||||||
|
|
||||||
|
1. **Create module structure**
|
||||||
|
```
|
||||||
|
public/modules/new-feature/
|
||||||
|
├── new-feature.html # UI template
|
||||||
|
├── styles/
|
||||||
|
│ └── new-feature.css # Styles (optional)
|
||||||
|
└── js/
|
||||||
|
└── new-feature.js # Logic (optional)
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Update router.js**
|
||||||
|
```javascript
|
||||||
|
routes: {
|
||||||
|
'new-feature': {
|
||||||
|
module: 'newFeatureModule',
|
||||||
|
requireAuth: true,
|
||||||
|
handler: this.handleNewFeature.bind(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Add navigation item**
|
||||||
|
```html
|
||||||
|
<a href="#new-feature" class="nav-item">
|
||||||
|
<i data-lucide="icon" class="w-5 h-5"></i>
|
||||||
|
<span>New Feature</span>
|
||||||
|
</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding a New API Endpoint
|
||||||
|
|
||||||
|
1. **Create route file** in `src/routes/`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
module.exports = (pool) => {
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
const requireAuth = (req, res, next) => {
|
||||||
|
if (req.session && req.session.userId) {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
res.status(401).json({ success: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// GET endpoint
|
||||||
|
router.get('/', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await pool.query('SELECT * FROM table');
|
||||||
|
res.json({ success: true, data: result.rows });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Register in server.js**
|
||||||
|
```javascript
|
||||||
|
app.use('/api/endpoint', require('./src/routes/endpoint')(pool));
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Add API method** in `public/js/api.js`
|
||||||
|
```javascript
|
||||||
|
async getEndpoint() {
|
||||||
|
return this.request('/endpoint', { method: 'GET' });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐛 Debugging
|
||||||
|
|
||||||
|
### Frontend Debugging
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Use Chrome DevTools
|
||||||
|
// F12 → Sources tab for breakpoints
|
||||||
|
|
||||||
|
// Use console methods
|
||||||
|
console.log('Value:', value); // Info
|
||||||
|
console.error('Error:', error); // Error
|
||||||
|
console.warn('Warning:', warning); // Warning
|
||||||
|
console.table(array); // Table view
|
||||||
|
|
||||||
|
// Use debugger statement
|
||||||
|
debugger; // Pauses execution when DevTools open
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backend Debugging
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Use node --inspect for debugging
|
||||||
|
node --inspect server.js
|
||||||
|
|
||||||
|
// Or use VS Code debugger with .vscode/launch.json
|
||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${workspaceFolder}/server.js",
|
||||||
|
"restart": true,
|
||||||
|
"console": "integratedTerminal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Debugging
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Connect to PostgreSQL
|
||||||
|
psql -U postgres -d postgres -h localhost
|
||||||
|
|
||||||
|
# Useful queries
|
||||||
|
\dt # List tables
|
||||||
|
\d users # Describe table
|
||||||
|
SELECT COUNT(*) FROM table_name;
|
||||||
|
EXPLAIN ANALYZE SELECT ...;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Testing API
|
||||||
|
|
||||||
|
### Using curl
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Register
|
||||||
|
curl -X POST http://localhost:3000/api/auth/register \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"name":"Test","email":"test@example.com","password":"pass123"}'
|
||||||
|
|
||||||
|
# Login
|
||||||
|
curl -X POST http://localhost:3000/api/auth/login \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"email":"test@example.com","password":"pass123"}'
|
||||||
|
|
||||||
|
# Get data
|
||||||
|
curl http://localhost:3000/api/db/tables
|
||||||
|
|
||||||
|
# Execute query
|
||||||
|
curl -X POST http://localhost:3000/api/db/query \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"sql":"SELECT COUNT(*) FROM users"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Postman
|
||||||
|
|
||||||
|
1. Import requests from `postman_collection.json` (если имеется)
|
||||||
|
2. Set base URL: `http://localhost:3000`
|
||||||
|
3. Save responses in collection
|
||||||
|
|
||||||
|
## 🗂️ File Organization
|
||||||
|
|
||||||
|
**Keep related files together:**
|
||||||
|
- HTML structure
|
||||||
|
- CSS styling
|
||||||
|
- JavaScript logic
|
||||||
|
|
||||||
|
**Example - Auth Module:**
|
||||||
|
```
|
||||||
|
modules/auth/
|
||||||
|
├── login.html # Structure + CSS
|
||||||
|
└── (js in main app.js or auth.js)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Avoid:**
|
||||||
|
- Mixing concerns (HTML with business logic)
|
||||||
|
- Creating orphaned files
|
||||||
|
- Dead code without comments
|
||||||
|
|
||||||
|
## 📚 Best Practices
|
||||||
|
|
||||||
|
### General
|
||||||
|
|
||||||
|
1. **DRY (Don't Repeat Yourself)**
|
||||||
|
- Extract common functions to utilities
|
||||||
|
- Create reusable components
|
||||||
|
|
||||||
|
2. **KISS (Keep It Simple, Stupid)**
|
||||||
|
- Write clear, readable code
|
||||||
|
- Avoid over-engineering
|
||||||
|
|
||||||
|
3. **SOLID Principles**
|
||||||
|
- Single responsibility
|
||||||
|
- Open/closed (open for extension, closed for modification)
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
1. **Performance**
|
||||||
|
- Lazy load modules when possible
|
||||||
|
- Minimize DOM manipulation
|
||||||
|
- Use event delegation for dynamic elements
|
||||||
|
|
||||||
|
2. **Accessibility**
|
||||||
|
- Use semantic HTML
|
||||||
|
- Add ARIA labels where needed
|
||||||
|
- Test with keyboard navigation
|
||||||
|
|
||||||
|
3. **Responsive Design**
|
||||||
|
- Mobile-first approach
|
||||||
|
- Test on multiple devices
|
||||||
|
- Use relative units (rem, %, vw)
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
|
||||||
|
1. **Database**
|
||||||
|
- Always use parameterized queries
|
||||||
|
- Create appropriate indexes
|
||||||
|
- Regular backups
|
||||||
|
|
||||||
|
2. **Security**
|
||||||
|
- Validate all inputs
|
||||||
|
- Hash passwords properly
|
||||||
|
- Use HTTPS in production
|
||||||
|
- Implement rate limiting
|
||||||
|
|
||||||
|
3. **Error Handling**
|
||||||
|
- Provide meaningful error messages
|
||||||
|
- Log errors server-side
|
||||||
|
- Don't expose sensitive information
|
||||||
|
|
||||||
|
## 🔑 Environment Variables
|
||||||
|
|
||||||
|
Never commit `.env`! Use `.env.example` template.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Required for all environments
|
||||||
|
PORT=3000
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_USER=postgres
|
||||||
|
|
||||||
|
# Development only
|
||||||
|
NODE_ENV=development
|
||||||
|
DEBUG=*
|
||||||
|
|
||||||
|
# Production only (set via deployment system)
|
||||||
|
SESSION_SECRET=production-secret
|
||||||
|
SSL_CERT=/path/to/cert.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📦 Dependencies
|
||||||
|
|
||||||
|
### Why we use them
|
||||||
|
|
||||||
|
- **express** - Web framework
|
||||||
|
- **pg** - PostgreSQL driver
|
||||||
|
- **bcryptjs** - Password hashing
|
||||||
|
- **express-session** - Session management
|
||||||
|
- **cors** - Cross-origin requests
|
||||||
|
- **dotenv** - Environment variables
|
||||||
|
|
||||||
|
**Keeping dependencies updated:**
|
||||||
|
```bash
|
||||||
|
npm outdated # Check for updates
|
||||||
|
npm update # Update to latest
|
||||||
|
npm audit # Check for vulnerabilities
|
||||||
|
npm audit fix # Auto-fix vulnerabilities
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Performance Tips
|
||||||
|
|
||||||
|
1. **Frontend**
|
||||||
|
- Use production builds
|
||||||
|
- Minify CSS/JS
|
||||||
|
- Lazy load images
|
||||||
|
- Use CSS containment for heavy animations
|
||||||
|
|
||||||
|
2. **Backend**
|
||||||
|
- Use connection pooling
|
||||||
|
- Add database indexes
|
||||||
|
- Cache frequently accessed data
|
||||||
|
- Optimize queries with EXPLAIN
|
||||||
|
|
||||||
|
3. **Deployment**
|
||||||
|
- Use CDN for static files
|
||||||
|
- Enable gzip compression
|
||||||
|
- Set appropriate cache headers
|
||||||
|
- Use monitoring tools
|
||||||
|
|
||||||
|
## 📖 Further Reading
|
||||||
|
|
||||||
|
- [Express.js Documentation](https://expressjs.com/)
|
||||||
|
- [PostgreSQL Documentation](https://www.postgresql.org/docs/)
|
||||||
|
- [MDN Web Docs](https://developer.mozilla.org/)
|
||||||
|
- [Node.js Best Practices](https://github.com/goldbergyoni/nodebestpractices)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Last updated: January 2024
|
||||||
54
Dockerfile
54
Dockerfile
@@ -1,20 +1,54 @@
|
|||||||
# Используем легкий Node.js образ
|
# ============================================================================
|
||||||
FROM node:20-alpine
|
# Multi-stage build for optimized production Docker image
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Stage 1: Dependencies
|
||||||
|
FROM node:20-alpine AS dependencies
|
||||||
|
|
||||||
# Рабочая директория
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Копируем package.json
|
# Copy only package files
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
# Устанавливаем зависимости
|
# Install dependencies (using npm ci for production)
|
||||||
RUN npm install --production
|
RUN npm ci --prefer-offline --no-audit
|
||||||
|
|
||||||
# Копируем весь проект
|
# ============================================================================
|
||||||
|
# Stage 2: Production
|
||||||
|
FROM node:20-alpine AS production
|
||||||
|
|
||||||
|
# Install curl for healthcheck and dumb-init for signal handling
|
||||||
|
RUN apk add --no-cache curl dumb-init
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Set Node.js environment to production
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
# Copy node_modules from dependencies stage
|
||||||
|
COPY --from=dependencies /app/node_modules ./node_modules
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Открываем порт
|
# Create non-root user for security
|
||||||
|
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
|
||||||
|
|
||||||
|
USER nodejs
|
||||||
|
|
||||||
|
# Expose port
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
# Запуск сервера
|
# Health check
|
||||||
CMD ["npm", "start"]
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||||
|
CMD curl -f http://localhost:3000 || exit 1
|
||||||
|
|
||||||
|
# Use dumb-init to handle signals properly (graceful shutdown)
|
||||||
|
ENTRYPOINT ["/usr/sbin/dumb-init", "--"]
|
||||||
|
|
||||||
|
# Start application
|
||||||
|
CMD ["node", "server.js"]
|
||||||
|
|||||||
97
Makefile
97
Makefile
@@ -1,22 +1,95 @@
|
|||||||
# Имя проекта (можно менять)
|
.PHONY: help install dev start stop logs clean build up down restart
|
||||||
PROJECT_NAME=postgres_admin
|
|
||||||
|
|
||||||
# Файл compose
|
# Project settings
|
||||||
COMPOSE=docker-compose
|
PROJECT_NAME=pg-admin
|
||||||
|
DOCKER_COMPOSE=docker-compose
|
||||||
|
|
||||||
# === Основные команды ===
|
# Colors for output
|
||||||
|
BLUE=\033[0;34m
|
||||||
|
GREEN=\033[0;32m
|
||||||
|
YELLOW=\033[0;33m
|
||||||
|
NC=\033[0m # No Color
|
||||||
|
|
||||||
# Сборка контейнеров
|
help:
|
||||||
|
@echo "$(BLUE)=== PG-Admin - PostgreSQL Admin Panel ===$(NC)"
|
||||||
|
@echo "$(GREEN)Available commands:$(NC)"
|
||||||
|
@echo " make install - Install dependencies"
|
||||||
|
@echo " make dev - Run development server"
|
||||||
|
@echo " make start - Start application"
|
||||||
|
@echo " make stop - Stop application"
|
||||||
|
@echo " make restart - Restart application"
|
||||||
|
@echo " make build - Build Docker image"
|
||||||
|
@echo " make up - Start Docker containers"
|
||||||
|
@echo " make down - Stop Docker containers"
|
||||||
|
@echo " make logs - View Docker logs"
|
||||||
|
@echo " make clean - Remove containers and volumes"
|
||||||
|
@echo " make db-reset - Reset database"
|
||||||
|
@echo ""
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
install:
|
||||||
|
@echo "$(BLUE)Installing dependencies...$(NC)"
|
||||||
|
npm install
|
||||||
|
@echo "$(GREEN)✓ Dependencies installed$(NC)"
|
||||||
|
|
||||||
|
# Development server with hot reload
|
||||||
|
dev:
|
||||||
|
@echo "$(BLUE)Starting development server...$(NC)"
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Start production server
|
||||||
|
start:
|
||||||
|
@echo "$(BLUE)Starting server...$(NC)"
|
||||||
|
npm start
|
||||||
|
|
||||||
|
# Stop application
|
||||||
|
stop:
|
||||||
|
@echo "$(BLUE)Stopping server...$(NC)"
|
||||||
|
@pkill -f "node server.js" || true
|
||||||
|
|
||||||
|
# Restart application
|
||||||
|
restart: stop start
|
||||||
|
@echo "$(GREEN)✓ Server restarted$(NC)"
|
||||||
|
|
||||||
|
# Docker - Build image
|
||||||
build:
|
build:
|
||||||
$(COMPOSE) build
|
@echo "$(BLUE)Building Docker image...$(NC)"
|
||||||
|
$(DOCKER_COMPOSE) build
|
||||||
|
@echo "$(GREEN)✓ Image built$(NC)"
|
||||||
|
|
||||||
# Запуск (в фоне)
|
# Docker - Start containers
|
||||||
up:
|
up: build
|
||||||
$(COMPOSE) up -d
|
@echo "$(BLUE)Starting containers...$(NC)"
|
||||||
|
$(DOCKER_COMPOSE) up -d
|
||||||
|
@echo "$(GREEN)✓ Containers started$(NC)"
|
||||||
|
@echo "Application available at http://localhost:3000"
|
||||||
|
|
||||||
# Остановка
|
# Docker - Stop containers
|
||||||
down:
|
down:
|
||||||
$(COMPOSE) down
|
@echo "$(BLUE)Stopping containers...$(NC)"
|
||||||
|
$(DOCKER_COMPOSE) down
|
||||||
|
@echo "$(GREEN)✓ Containers stopped$(NC)"
|
||||||
|
|
||||||
|
# Docker - View logs
|
||||||
|
logs:
|
||||||
|
@echo "$(BLUE)Fetching logs...$(NC)"
|
||||||
|
$(DOCKER_COMPOSE) logs -f
|
||||||
|
|
||||||
|
# Docker - Clean (remove containers and volumes)
|
||||||
|
clean:
|
||||||
|
@echo "$(YELLOW)Cleaning Docker resources...$(NC)"
|
||||||
|
$(DOCKER_COMPOSE) down -v
|
||||||
|
@echo "$(GREEN)✓ Cleaned$(NC)"
|
||||||
|
|
||||||
|
# Database - Reset database
|
||||||
|
db-reset:
|
||||||
|
@echo "$(YELLOW)Resetting database...$(NC)"
|
||||||
|
$(DOCKER_COMPOSE) exec postgres psql -U postgres -d postgres -c "DROP DATABASE IF EXISTS postgres;"
|
||||||
|
$(DOCKER_COMPOSE) exec postgres psql -U postgres -d postgres -c "CREATE DATABASE postgres;"
|
||||||
|
@echo "$(GREEN)✓ Database reset$(NC)"
|
||||||
|
|
||||||
|
# Show all targets
|
||||||
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
# Перезапуск
|
# Перезапуск
|
||||||
restart:
|
restart:
|
||||||
|
|||||||
296
README.md
Normal file
296
README.md
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
# PostgreSQL Admin Panel (PG-Admin)
|
||||||
|
|
||||||
|
## 🎯 Описание
|
||||||
|
|
||||||
|
Современная, production-ready панель администратора для управления PostgreSQL базами данных с модульной архитектурой, поддержкой темной/светлой темы, адаптивным дизайном и полной функциональностью для администраторов.
|
||||||
|
|
||||||
|
## ✨ Особенности
|
||||||
|
|
||||||
|
### 🏗️ Архитектура
|
||||||
|
- **Модульная структура** - разделение на отдельные модули (auth, dashboard, admin)
|
||||||
|
- **Frontend и Backend разделены** - чистая архитектура
|
||||||
|
- **SPA (Single Page Application)** - быстрая навигация без перезагрузок
|
||||||
|
- **RESTful API** - стандартные API endpoints
|
||||||
|
|
||||||
|
### 🎨 Дизайн & UX
|
||||||
|
- **Темная/Светлая тема** - автоматическое определение системной темы
|
||||||
|
- **Адаптивный дизайн** - полная поддержка мобильных устройств
|
||||||
|
- **Современный UI** - использование Tailwind CSS и Lucide Icons
|
||||||
|
- **Плавные анимации** - интерактивные переходы и эффекты
|
||||||
|
- **Доступность** - поддержка клавиатурной навигации
|
||||||
|
|
||||||
|
### 👥 Управление пользователями
|
||||||
|
- **Разные роли** - superadmin, admin, moderator, viewer
|
||||||
|
- **Управление правами доступа** - контроль доступа к функциям
|
||||||
|
- **Активация/Деактивация** - управление состоянием пользователей
|
||||||
|
- **Логирование** - отслеживание всех действий в системе
|
||||||
|
|
||||||
|
### 💾 Работа с базой данных
|
||||||
|
- **Просмотр таблиц** - список всех таблиц в БД
|
||||||
|
- **Просмотр данных** - пагинация, поиск, фильтрация
|
||||||
|
- **SQL запросы** - выполнение произвольных SQL запросов
|
||||||
|
- **Статистика** - информация о размере и состоянии БД
|
||||||
|
|
||||||
|
### 🔒 Безопасность
|
||||||
|
- **Хеширование паролей** - bcryptjs для безопасного хранения
|
||||||
|
- **Session-based auth** - безопасное управление сеансами
|
||||||
|
- **CSRF защита** - встроенная в Express session
|
||||||
|
- **SQL injection protection** - параметризованные запросы
|
||||||
|
|
||||||
|
### 📱 Мобильная поддержка
|
||||||
|
- **Responsive Layout** - адаптащия для всех размеров экранов
|
||||||
|
- **Touch-friendly** - оптимизированные кнопки и элементы
|
||||||
|
- **Боковое меню** - сворачиваемое на мобильных устройствах
|
||||||
|
- **Компактная навигация** - эффективное использование пространства
|
||||||
|
|
||||||
|
## 🚀 Быстрый старт
|
||||||
|
|
||||||
|
### Требования
|
||||||
|
- Node.js 14+
|
||||||
|
- PostgreSQL 12+
|
||||||
|
- Docker & Docker Compose (опционально)
|
||||||
|
|
||||||
|
### Установка
|
||||||
|
|
||||||
|
1. **Клонируйте репозиторий**
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd pg-admin
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Установите зависимости**
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Создайте файл .env**
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Отредактируйте .env с вашими данными**
|
||||||
|
```env
|
||||||
|
NODE_ENV=development
|
||||||
|
PORT=3000
|
||||||
|
SESSION_SECRET=your-secret-key-change-this
|
||||||
|
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_NAME=postgres
|
||||||
|
DB_USER=postgres
|
||||||
|
DB_PASSWORD=postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Запустите приложение**
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
или для разработки с автоперезагрузкой:
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Откройте браузер**
|
||||||
|
```
|
||||||
|
http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐳 Docker запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Приложение станет доступно на `http://localhost:3000`
|
||||||
|
|
||||||
|
## 📁 Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
pg-admin/
|
||||||
|
├── public/ # Frontend код
|
||||||
|
│ ├── index.html # Главная страница
|
||||||
|
│ ├── modules/ # UI модули
|
||||||
|
│ │ ├── auth/ # Аутентификация
|
||||||
|
│ │ ├── dashboard/ # Основная панель
|
||||||
|
│ │ └── admin/ # Админ панель
|
||||||
|
│ ├── styles/ # CSS файлы
|
||||||
|
│ │ ├── main.css # Основные стили
|
||||||
|
│ │ ├── theme.css # Темы (свет/темнота)
|
||||||
|
│ │ ├── responsive.css # Адаптивность
|
||||||
|
│ │ └── animations.css # Анимации
|
||||||
|
│ └── js/ # JavaScript
|
||||||
|
│ ├── theme.js # Управление темой
|
||||||
|
│ ├── api.js # API запросы
|
||||||
|
│ ├── auth.js # Аутентификация
|
||||||
|
│ ├── router.js # SPA роутер
|
||||||
|
│ └── app.js # Главное приложение
|
||||||
|
│
|
||||||
|
├── src/ # Backend код
|
||||||
|
│ ├── routes/ # API маршруты
|
||||||
|
│ │ ├── auth.js # Аутентификация
|
||||||
|
│ │ ├── users.js # Управление пользователями
|
||||||
|
│ │ ├── db-tables.js # Работа с БД
|
||||||
|
│ │ └── admin.js # Админ функции
|
||||||
|
│ ├── middleware/ # Express middleware
|
||||||
|
│ ├── config/ # Конфиг файлы
|
||||||
|
│ └── database/ # Database helpers
|
||||||
|
│
|
||||||
|
├── server.js # Главный файл приложения
|
||||||
|
├── Dockerfile # Docker конфиг
|
||||||
|
├── docker-compose.yml # Docker Compose конфиг
|
||||||
|
├── package.json # NPM зависимости
|
||||||
|
├── .env.example # Пример .env файла
|
||||||
|
└── README.md # Этот файл
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔐 Учетные данные по умолчанию
|
||||||
|
|
||||||
|
При первом запуске создается администратор:
|
||||||
|
- **Email:** admin@example.com
|
||||||
|
- **Пароль:** admin123
|
||||||
|
|
||||||
|
⚠️ **Обязательно смените пароль после первого входа!**
|
||||||
|
|
||||||
|
## 🛣️ API Endpoints
|
||||||
|
|
||||||
|
### Аутентификация
|
||||||
|
- `POST /api/auth/register` - Регистрация
|
||||||
|
- `POST /api/auth/login` - Вход
|
||||||
|
- `POST /api/auth/logout` - Выход
|
||||||
|
- `GET /api/auth/me` - Текущий пользователь
|
||||||
|
|
||||||
|
### Пользователи
|
||||||
|
- `GET /api/users` - Список пользователей (админ)
|
||||||
|
- `POST /api/users` - Создать пользователя (админ)
|
||||||
|
- `PATCH /api/users/:userId` - Обновить пользователя
|
||||||
|
- `DELETE /api/users/:userId` - Удалить пользователя (админ)
|
||||||
|
|
||||||
|
### База данных
|
||||||
|
- `GET /api/db/tables` - Список таблиц
|
||||||
|
- `GET /api/db/tables/:tableName/data` - Данные таблицы
|
||||||
|
- `POST /api/db/query` - Выполнить SQL запрос
|
||||||
|
- `GET /api/db/stats` - Статистика БД
|
||||||
|
|
||||||
|
### Администрирование
|
||||||
|
- `GET /api/admin/stats` - Системная статистика
|
||||||
|
- `GET /api/admin/logs` - Логи активности
|
||||||
|
- `GET /api/admin/backups` - Список резервных копий
|
||||||
|
- `POST /api/admin/backups` - Создать резервную копию
|
||||||
|
|
||||||
|
## 🎨 Настройки темы
|
||||||
|
|
||||||
|
Приложение автоматически определяет предпочтение темы системы:
|
||||||
|
- **Темная тема** - если система установила темный режим
|
||||||
|
- **Светлая тема** - если система установила светлый режим
|
||||||
|
|
||||||
|
Пользователь может переключать тему вручную с помощью кнопки (🌙/☀️) в заголовке.
|
||||||
|
|
||||||
|
Выбор сохраняется в localStorage.
|
||||||
|
|
||||||
|
## 📱 Адаптивность
|
||||||
|
|
||||||
|
Приложение полностью адаптивно:
|
||||||
|
- **Mobile** (< 640px) - оптимизированный интерфейс
|
||||||
|
- **Tablet** (640-1024px) - промежуточный размер
|
||||||
|
- **Desktop** (> 1024px) - полный функционал
|
||||||
|
|
||||||
|
## 🚦 Роли и права доступа
|
||||||
|
|
||||||
|
### Superadmin
|
||||||
|
- Полный доступ ко всем функциям
|
||||||
|
- Управление пользователями
|
||||||
|
- Администрирование системы
|
||||||
|
- Просмотр логов
|
||||||
|
|
||||||
|
### Admin
|
||||||
|
- Управление таблицами
|
||||||
|
- Просмотр и редактирование данных
|
||||||
|
- Управление пользователями
|
||||||
|
- Просмотр статистики
|
||||||
|
|
||||||
|
### Moderator
|
||||||
|
- Просмотр и редактирование данных
|
||||||
|
- Ограниченный доступ к таблицам
|
||||||
|
- Без прав на удаление
|
||||||
|
|
||||||
|
### Viewer
|
||||||
|
- Только просмотр данных
|
||||||
|
- Без прав на редактирование
|
||||||
|
|
||||||
|
## 🔧 Разработка
|
||||||
|
|
||||||
|
### Горячая перезагрузка
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Структура кода
|
||||||
|
|
||||||
|
**Frontend:**
|
||||||
|
- Modular: каждый компонент в своем модуле
|
||||||
|
- API layer: централизованные запросы в `api.js`
|
||||||
|
- Router: SPA маршрутизация в `router.js`
|
||||||
|
- Theme: система тем в `theme.js`
|
||||||
|
|
||||||
|
**Backend:**
|
||||||
|
- Express.js фреймворк
|
||||||
|
- PostgreSQL драйвер `pg`
|
||||||
|
- Bcryptjs для паролей
|
||||||
|
- Session-based аутентификация
|
||||||
|
|
||||||
|
### Тестирование
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Регистрация нового пользователя
|
||||||
|
curl -X POST http://localhost:3000/api/auth/register \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"name":"Test","email":"test@example.com","password":"test123"}'
|
||||||
|
|
||||||
|
# Вход
|
||||||
|
curl -X POST http://localhost:3000/api/auth/login \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"email":"test@example.com","password":"test123"}'
|
||||||
|
|
||||||
|
# Список таблиц
|
||||||
|
curl http://localhost:3000/api/db/tables
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐛 Известные проблемы
|
||||||
|
|
||||||
|
- Функция дублирования таблиц требует реализации
|
||||||
|
- Экспорт данных в CSV/JSON планируется
|
||||||
|
- Мониторинг запросов к БД в разработке
|
||||||
|
|
||||||
|
## 🗺️ Дорожная карта
|
||||||
|
|
||||||
|
- [ ] Резервное копирование БД
|
||||||
|
- [ ] Экспорт/импорт данных
|
||||||
|
- [ ] Мониторинг производительности
|
||||||
|
- [ ] Интеграция с аналитикой
|
||||||
|
- [ ] WebSocket поддержка
|
||||||
|
- [ ] Offline режим
|
||||||
|
- [ ] PWA преобразование
|
||||||
|
|
||||||
|
## 📝 Лицензия
|
||||||
|
|
||||||
|
MIT License - смотрите LICENSE файл для деталей
|
||||||
|
|
||||||
|
## 👥 Поддержка
|
||||||
|
|
||||||
|
Обнаружили баг? Откройте issue на GitHub.
|
||||||
|
|
||||||
|
Есть идеи для улучшения? Создайте pull request.
|
||||||
|
|
||||||
|
## 📚 Дополнительные ресурсы
|
||||||
|
|
||||||
|
- [Express.js документация](https://expressjs.com/)
|
||||||
|
- [PostgreSQL документация](https://www.postgresql.org/docs/)
|
||||||
|
- [Tailwind CSS](https://tailwindcss.com/)
|
||||||
|
- [Lucide Icons](https://lucide.dev/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Версия:** 2.0.0
|
||||||
|
**Поддержка:** Node.js 14+
|
||||||
|
**Лицензия:** MIT
|
||||||
@@ -1,53 +1,153 @@
|
|||||||
|
version: '3.9'
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# PostgreSQL Admin Panel - Complete Stack
|
||||||
|
# ============================================================================
|
||||||
|
# Services:
|
||||||
|
# - postgres: PostgreSQL 16 database
|
||||||
|
# - pgadmin: Node.js application
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker-compose up --build # Build and start
|
||||||
|
# docker-compose down # Stop and remove
|
||||||
|
# docker-compose logs -f # View logs
|
||||||
|
# docker-compose ps # Status
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
services:
|
services:
|
||||||
db:
|
# =========================================================================
|
||||||
image: postgres:16
|
# PostgreSQL Database Service
|
||||||
container_name: app_postgres
|
# =========================================================================
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: pg-admin-postgres
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
environment:
|
# Network Settings
|
||||||
POSTGRES_USER: postgres
|
|
||||||
POSTGRES_PASSWORD: postgres
|
|
||||||
POSTGRES_DB: testdb
|
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
|
networks:
|
||||||
|
- pgadmin-network
|
||||||
|
|
||||||
|
# Environment Configuration
|
||||||
|
environment:
|
||||||
|
# Database initialization
|
||||||
|
POSTGRES_DB: ${DB_NAME:-postgres}
|
||||||
|
POSTGRES_USER: ${DB_USER:-postgres}
|
||||||
|
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
|
||||||
|
|
||||||
|
# PostgreSQL configuration
|
||||||
|
POSTGRES_INITDB_ARGS: >
|
||||||
|
-c max_connections=200
|
||||||
|
-c shared_buffers=256MB
|
||||||
|
-c effective_cache_size=1GB
|
||||||
|
-c log_statement=all
|
||||||
|
|
||||||
|
# Volume Management
|
||||||
volumes:
|
volumes:
|
||||||
|
# Persist database files
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
|
||||||
networks:
|
# Health Check
|
||||||
- app_network
|
|
||||||
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-postgres}"]
|
||||||
interval: 5s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# PostgreSQL Admin Panel Application
|
||||||
|
# =========================================================================
|
||||||
|
pgadmin:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
# Build arguments
|
||||||
|
args:
|
||||||
|
NODE_ENV: production
|
||||||
|
|
||||||
backend:
|
container_name: pg-admin-app
|
||||||
build: .
|
|
||||||
container_name: app_backend
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# Network Settings
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
|
networks:
|
||||||
|
- pgadmin-network
|
||||||
|
|
||||||
env_file:
|
# Depends On
|
||||||
- .env
|
|
||||||
|
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
||||||
networks:
|
# Environment Configuration
|
||||||
- app_network
|
environment:
|
||||||
|
# Application Settings
|
||||||
|
NODE_ENV: ${NODE_ENV:-production}
|
||||||
|
PORT: 3000
|
||||||
|
|
||||||
|
# Database Connection
|
||||||
|
DB_HOST: postgres
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_NAME: ${DB_NAME:-postgres}
|
||||||
|
DB_USER: ${DB_USER:-postgres}
|
||||||
|
DB_PASSWORD: ${DB_PASSWORD:-postgres}
|
||||||
|
|
||||||
|
# Security
|
||||||
|
SESSION_SECRET: ${SESSION_SECRET:-change-this-in-production-to-random-string}
|
||||||
|
|
||||||
|
# Volume Management (development - can be removed in production)
|
||||||
|
volumes:
|
||||||
|
# Code synchronization
|
||||||
|
- ./public:/app/public:ro
|
||||||
|
- ./src:/app/src:ro
|
||||||
|
# Node modules persistence
|
||||||
|
- /app/node_modules
|
||||||
|
|
||||||
|
# Resource Limits
|
||||||
|
# Uncomment for production to limit resources
|
||||||
|
# deploy:
|
||||||
|
# resources:
|
||||||
|
# limits:
|
||||||
|
# cpus: '1'
|
||||||
|
# memory: 512M
|
||||||
|
# reservations:
|
||||||
|
# cpus: '0.5'
|
||||||
|
# memory: 256M
|
||||||
|
|
||||||
|
# Health Check
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:3000"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
|
# Logging Configuration
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Volumes Definition
|
||||||
|
# ============================================================================
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
|
driver: local
|
||||||
|
driver_opts:
|
||||||
|
type: none
|
||||||
|
o: bind
|
||||||
|
device: ${PWD}/postgres_data
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Networks Definition
|
||||||
|
# ============================================================================
|
||||||
networks:
|
networks:
|
||||||
app_network:
|
pgadmin-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 172.20.0.0/16
|
||||||
|
|||||||
16
favicon.svg
Normal file
16
favicon.svg
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||||
|
<!-- Фон -->
|
||||||
|
<rect width="64" height="64" rx="16" fill="#1d4ed8"/>
|
||||||
|
|
||||||
|
<!-- Иконка базы данных -->
|
||||||
|
<g fill="none" stroke="white" stroke-width="3">
|
||||||
|
<!-- Верх -->
|
||||||
|
<ellipse cx="32" cy="20" rx="12" ry="6"/>
|
||||||
|
|
||||||
|
<!-- Боковые линии -->
|
||||||
|
<path d="M20 20v16c0 3.3 5.4 6 12 6s12-2.7 12-6V20"/>
|
||||||
|
|
||||||
|
<!-- Средние линии -->
|
||||||
|
<path d="M20 28c0 3.3 5.4 6 12 6s12-2.7 12-6"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 513 B |
1209
index.html
1209
index.html
File diff suppressed because it is too large
Load Diff
21
package.json
21
package.json
@@ -1,11 +1,24 @@
|
|||||||
{
|
{
|
||||||
"name": "postgres-admin-panel",
|
"name": "pg-admin-panel",
|
||||||
"version": "1.0.0",
|
"version": "2.0.0",
|
||||||
"description": "PostgreSQL Admin Panel with .env configuration",
|
"description": "Production-ready PostgreSQL Admin Panel with modular architecture, dark mode, and responsive design",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
|
"author": "Your Name",
|
||||||
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node server.js",
|
"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": {
|
"dependencies": {
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
|||||||
47
public/index.html
Normal file
47
public/index.html
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>PostgreSQL SensoLab Panel</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
||||||
|
|
||||||
|
<!-- Tailwind CSS -->
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: 'class',
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: 'var(--color-primary)',
|
||||||
|
'primary-dark': 'var(--color-primary-dark)',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Lucide Icons -->
|
||||||
|
<script src="https://unpkg.com/lucide@latest"></script>
|
||||||
|
|
||||||
|
<!-- Fonts -->
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Main Styles -->
|
||||||
|
<link rel="stylesheet" href="/styles/main.css">
|
||||||
|
<link rel="stylesheet" href="/styles/theme.css">
|
||||||
|
<link rel="stylesheet" href="/styles/responsive.css">
|
||||||
|
<link rel="stylesheet" href="/styles/animations.css">
|
||||||
|
</head>
|
||||||
|
<body class="bg-white dark:bg-slate-950 text-slate-800 dark:text-slate-100 transition-colors duration-300">
|
||||||
|
<div id="app"></div>
|
||||||
|
|
||||||
|
<!-- Scripts -->
|
||||||
|
<script src="/js/theme.js"></script>
|
||||||
|
<script src="/js/api.js"></script>
|
||||||
|
<script src="/js/auth.js"></script>
|
||||||
|
<script src="/js/router.js"></script>
|
||||||
|
<script src="/js/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
145
public/js/api.js
Normal file
145
public/js/api.js
Normal file
@@ -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();
|
||||||
228
public/js/app.js
Normal file
228
public/js/app.js
Normal file
@@ -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 = '<p class="text-slate-500 dark:text-slate-400 text-sm">Таблицы не найдены</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tablesList.innerHTML = tables.map(table => `
|
||||||
|
<div class="flex items-center justify-between p-3 bg-slate-50 dark:bg-slate-800 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors cursor-pointer" onclick="window.location.hash='#tables'">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<i data-lucide="table" class="w-4 h-4 text-blue-600 dark:text-blue-400"></i>
|
||||||
|
<span class="font-medium text-slate-900 dark:text-white">${table.name}</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs text-slate-500 dark:text-slate-400">${table.recordCount || 0} записей</span>
|
||||||
|
</div>
|
||||||
|
`).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 = '<tr><td colspan="5" class="py-8 px-4 text-center text-slate-500 dark:text-slate-400">Пользователи не найдены</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
usersList.innerHTML = users.map(user => `
|
||||||
|
<tr>
|
||||||
|
<td class="py-3 px-4 font-medium text-slate-900 dark:text-white">${user.name}</td>
|
||||||
|
<td class="py-3 px-4 text-slate-600 dark:text-slate-400 hidden sm:table-cell text-sm">${user.email}</td>
|
||||||
|
<td class="py-3 px-4 text-slate-600 dark:text-slate-400">
|
||||||
|
<span class="px-2 py-1 rounded bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 text-xs font-medium">${this.getRoleName(user.role)}</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-3 px-4">
|
||||||
|
<span class="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-semibold ${user.active ? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400' : 'bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-400'}">
|
||||||
|
<i data-lucide="circle" class="w-2 h-2"></i>
|
||||||
|
${user.active ? 'Активен' : 'Неактивен'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-3 px-4 text-right">
|
||||||
|
<div class="flex items-center justify-end gap-2">
|
||||||
|
<button onclick="app.editUser(${user.id})" class="p-1 hover:bg-blue-100 dark:hover:bg-blue-900/30 rounded text-blue-600 dark:text-blue-400" title="Редактировать">
|
||||||
|
<i data-lucide="edit" class="w-4 h-4"></i>
|
||||||
|
</button>
|
||||||
|
<button onclick="app.deleteUser(${user.id})" class="p-1 hover:bg-red-100 dark:hover:bg-red-900/30 rounded text-red-600 dark:text-red-400" title="Удалить">
|
||||||
|
<i data-lucide="trash" class="w-4 h-4"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
212
public/js/auth.js
Normal file
212
public/js/auth.js
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
205
public/js/router.js
Normal file
205
public/js/router.js
Normal file
@@ -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();
|
||||||
76
public/js/theme.js
Normal file
76
public/js/theme.js
Normal file
@@ -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());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
164
public/modules/admin/admin-panel.html
Normal file
164
public/modules/admin/admin-panel.html
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
<!-- Admin Panel Module -->
|
||||||
|
<div id="adminModule" class="admin-module hidden h-screen flex flex-col">
|
||||||
|
<!-- Header (same as dashboard) -->
|
||||||
|
<header class="bg-white dark:bg-slate-900 border-b border-slate-200 dark:border-slate-800 h-16 flex items-center justify-between px-4 sm:px-6 shadow-sm z-10">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<button id="toggleAdminSidebar" class="p-2 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-lg md:hidden">
|
||||||
|
<i data-lucide="menu" class="w-6 h-6"></i>
|
||||||
|
</button>
|
||||||
|
<div class="flex items-center gap-2 text-blue-600 dark:text-blue-400">
|
||||||
|
<i data-lucide="shield-admin" class="w-6 h-6"></i>
|
||||||
|
<span class="font-bold text-lg hidden sm:inline">Администрирование</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<button id="toggleAdminTheme" class="p-2 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-lg transition-colors" title="Переключить тему">
|
||||||
|
<i data-lucide="moon" class="w-5 h-5 dark:hidden"></i>
|
||||||
|
<i data-lucide="sun" class="w-5 h-5 hidden dark:block"></i>
|
||||||
|
</button>
|
||||||
|
<div class="h-6 w-px bg-slate-200 dark:bg-slate-700 hidden sm:block"></div>
|
||||||
|
<button id="adminLogoutBtn" class="p-2 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors" title="Выход">
|
||||||
|
<i data-lucide="log-out" class="w-5 h-5 text-red-500"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="flex flex-1 overflow-hidden">
|
||||||
|
<!-- Admin Sidebar -->
|
||||||
|
<aside id="adminSidebar" class="fixed md:static inset-y-16 left-0 w-64 bg-white dark:bg-slate-900 border-r border-slate-200 dark:border-slate-800 overflow-y-auto transform -translate-x-full md:translate-x-0 transition-transform z-40">
|
||||||
|
<nav class="p-4 space-y-2">
|
||||||
|
<h3 class="px-4 py-2 text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider">Управление</h3>
|
||||||
|
|
||||||
|
<a href="#admin-overview" class="nav-item active flex items-center gap-3 px-4 py-3 rounded-lg font-medium transition-colors">
|
||||||
|
<i data-lucide="layout-dashboard" class="w-5 h-5"></i>
|
||||||
|
<span>Обзор</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="#admin-users" class="nav-item flex items-center gap-3 px-4 py-3 rounded-lg font-medium transition-colors">
|
||||||
|
<i data-lucide="users" class="w-5 h-5"></i>
|
||||||
|
<span>Пользователи</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="#admin-roles" class="nav-item flex items-center gap-3 px-4 py-3 rounded-lg font-medium transition-colors">
|
||||||
|
<i data-lucide="shield" class="w-5 h-5"></i>
|
||||||
|
<span>Роли и права</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="#admin-logs" class="nav-item flex items-center gap-3 px-4 py-3 rounded-lg font-medium transition-colors">
|
||||||
|
<i data-lucide="file-text" class="w-5 h-5"></i>
|
||||||
|
<span>Логи</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="my-4 h-px bg-slate-200 dark:bg-slate-700"></div>
|
||||||
|
|
||||||
|
<h3 class="px-4 py-2 text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider">Система</h3>
|
||||||
|
|
||||||
|
<a href="#admin-database" class="nav-item flex items-center gap-3 px-4 py-3 rounded-lg font-medium transition-colors">
|
||||||
|
<i data-lucide="database" class="w-5 h-5"></i>
|
||||||
|
<span>База данных</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="#admin-settings" class="nav-item flex items-center gap-3 px-4 py-3 rounded-lg font-medium transition-colors">
|
||||||
|
<i data-lucide="settings" class="w-5 h-5"></i>
|
||||||
|
<span>Настройки</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="#admin-backups" class="nav-item flex items-center gap-3 px-4 py-3 rounded-lg font-medium transition-colors">
|
||||||
|
<i data-lucide="download" class="w-5 h-5"></i>
|
||||||
|
<span>Резервные копии</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="#dashboard" class="nav-item flex items-center gap-3 px-4 py-3 rounded-lg font-medium transition-colors text-blue-600 dark:text-blue-400">
|
||||||
|
<i data-lucide="arrow-left" class="w-5 h-5"></i>
|
||||||
|
<span>К панели</span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Admin Content -->
|
||||||
|
<main id="adminContent" class="flex-1 overflow-auto">
|
||||||
|
<div class="p-4 sm:p-6 space-y-6">
|
||||||
|
<!-- Users Section -->
|
||||||
|
<section id="usersSection" class="space-y-4">
|
||||||
|
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-4">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold text-slate-900 dark:text-white flex items-center gap-2">
|
||||||
|
<i data-lucide="users" class="w-6 h-6"></i>
|
||||||
|
Управление пользователями
|
||||||
|
</h2>
|
||||||
|
<p class="text-slate-500 dark:text-slate-400 text-sm mt-1">Добавляйте, редактируйте и удаляйте администраторов</p>
|
||||||
|
</div>
|
||||||
|
<button id="addUserBtn" class="btn btn-primary whitespace-nowrap flex items-center gap-2">
|
||||||
|
<i data-lucide="plus" class="w-5 h-5"></i>
|
||||||
|
<span class="hidden sm:inline">Добавить пользователя</span>
|
||||||
|
<span class="sm:hidden">Добавить</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Users Table -->
|
||||||
|
<div class="card overflow-x-auto">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead class="border-b border-slate-200 dark:border-slate-700">
|
||||||
|
<tr>
|
||||||
|
<th class="text-left py-3 px-4 font-semibold text-slate-600 dark:text-slate-300">Имя</th>
|
||||||
|
<th class="text-left py-3 px-4 font-semibold text-slate-600 dark:text-slate-300 hidden sm:table-cell">Email</th>
|
||||||
|
<th class="text-left py-3 px-4 font-semibold text-slate-600 dark:text-slate-300">Роль</th>
|
||||||
|
<th class="text-left py-3 px-4 font-semibold text-slate-600 dark:text-slate-300">Статус</th>
|
||||||
|
<th class="text-right py-3 px-4 font-semibold text-slate-600 dark:text-slate-300">Действия</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="usersList" class="divide-y divide-slate-200 dark:divide-slate-700">
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="py-8 px-4 text-center text-slate-500 dark:text-slate-400">
|
||||||
|
Загрузка пользователей...
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Add/Edit User Modal -->
|
||||||
|
<div id="userModal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50">
|
||||||
|
<div class="bg-white dark:bg-slate-900 rounded-2xl shadow-xl max-w-md w-full max-h-[90vh] overflow-y-auto">
|
||||||
|
<div class="sticky top-0 bg-white dark:bg-slate-900 p-6 border-b border-slate-200 dark:border-slate-700 flex items-center justify-between">
|
||||||
|
<h3 class="text-xl font-bold text-slate-900 dark:text-white" id="userModalTitle">Добавить пользователя</h3>
|
||||||
|
<button id="closeUserModal" class="p-1 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-lg">
|
||||||
|
<i data-lucide="x" class="w-6 h-6"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="userForm" class="p-6 space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Полное имя</label>
|
||||||
|
<input type="text" id="userNameInput" class="input-field" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Email</label>
|
||||||
|
<input type="email" id="userEmailInput" class="input-field" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Роль</label>
|
||||||
|
<select id="userRoleSelect" class="input-field">
|
||||||
|
<option value="superadmin">Суперадминистратор</option>
|
||||||
|
<option value="admin">Администратор</option>
|
||||||
|
<option value="moderator">Модератор</option>
|
||||||
|
<option value="viewer">Только просмотр</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Пароль</label>
|
||||||
|
<input type="password" id="userPasswordInput" class="input-field" placeholder="Оставить пусто, чтобы оставить без изменений">
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3 pt-4">
|
||||||
|
<button type="submit" class="flex-1 btn btn-primary">Сохранить</button>
|
||||||
|
<button type="button" id="cancelUserEdit" class="flex-1 btn btn-secondary">Отмена</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
98
public/modules/auth/login.html
Normal file
98
public/modules/auth/login.html
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
<!-- Auth Module - Login/Register Screen -->
|
||||||
|
<div id="authModule" class="auth-module hidden">
|
||||||
|
<!-- Login Screen -->
|
||||||
|
<div id="loginScreen" class="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
|
<div class="absolute inset-0 bg-gradient-to-br from-slate-900 via-blue-900 to-slate-900 dark:from-slate-950 dark:via-blue-950 dark:to-slate-950"></div>
|
||||||
|
|
||||||
|
<div class="relative w-full max-w-md px-4 sm:px-0">
|
||||||
|
<div class="glass-panel glass-panel-light dark:glass-panel-dark rounded-2xl shadow-2xl p-8">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="text-center mb-8">
|
||||||
|
<div class="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-blue-600 to-blue-700 rounded-2xl mb-4 shadow-lg shadow-blue-600/30">
|
||||||
|
<i data-lucide="database" class="w-8 h-8 text-white"></i>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-2xl sm:text-3xl font-bold text-slate-900 dark:text-white">PostgreSQL SensoLab</h1>
|
||||||
|
<p class="text-slate-500 dark:text-slate-400 mt-2 text-sm sm:text-base">Управление базой данных</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab Navigation -->
|
||||||
|
<div class="flex gap-2 mb-8 bg-slate-100 dark:bg-slate-800 p-1 rounded-lg">
|
||||||
|
<button class="auth-tab-btn active flex-1 py-2 px-4 rounded-md font-medium transition-all text-sm sm:text-base" data-tab="login">
|
||||||
|
Вход
|
||||||
|
</button>
|
||||||
|
<button class="auth-tab-btn flex-1 py-2 px-4 rounded-md font-medium transition-all text-sm sm:text-base" data-tab="register">
|
||||||
|
Регистрация
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Login Form -->
|
||||||
|
<form id="loginForm" class="auth-form space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||||
|
Email или логин
|
||||||
|
</label>
|
||||||
|
<input type="text" id="loginEmail" class="input-field" placeholder="admin@example.com" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||||
|
Пароль
|
||||||
|
</label>
|
||||||
|
<input type="password" id="loginPassword" class="input-field" placeholder="••••••••" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="w-full btn btn-primary mt-6">
|
||||||
|
<span class="login-text">Войти</span>
|
||||||
|
<div class="login-loader hidden" style="width: 20px; height: 20px; border: 2px solid rgba(255,255,255,0.3); border-top: 2px solid white; border-radius: 50%; animation: spin 1s linear infinite;"></div>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Register Form -->
|
||||||
|
<form id="registerForm" class="auth-form hidden space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||||
|
Полное имя
|
||||||
|
</label>
|
||||||
|
<input type="text" id="registerName" class="input-field" placeholder="Иван Петров" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<input type="email" id="registerEmail" class="input-field" placeholder="admin@example.com" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||||
|
Пароль
|
||||||
|
</label>
|
||||||
|
<input type="password" id="registerPassword" class="input-field" placeholder="••••••••" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||||
|
Подтверждение пароля
|
||||||
|
</label>
|
||||||
|
<input type="password" id="registerPasswordConfirm" class="input-field" placeholder="••••••••" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="w-full btn btn-primary mt-6">
|
||||||
|
<span class="register-text">Создать аккаунт</span>
|
||||||
|
<div class="register-loader hidden" style="width: 20px; height: 20px; border: 2px solid rgba(255,255,255,0.3); border-top: 2px solid white; border-radius: 50%; animation: spin 1s linear infinite;"></div>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Error Message -->
|
||||||
|
<div id="authError" class="mt-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg text-red-600 dark:text-red-400 text-sm hidden flex items-center gap-2">
|
||||||
|
<i data-lucide="alert-circle" class="w-4 h-4"></i>
|
||||||
|
<span id="authErrorText"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Demo Info -->
|
||||||
|
<div class="mt-6 p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg text-blue-600 dark:text-blue-400 text-xs sm:text-sm">
|
||||||
|
<p class="font-semibold mb-2 flex items-center gap-2">
|
||||||
|
<i data-lucide="info" class="w-4 h-4"></i>
|
||||||
|
Демо учетные данные:
|
||||||
|
</p>
|
||||||
|
<p>Email: <code class="bg-white/50 dark:bg-slate-900 px-2 py-1 rounded text-xs">admin@example.com</code></p>
|
||||||
|
<p>Пароль: <code class="bg-white/50 dark:bg-slate-900 px-2 py-1 rounded text-xs">admin123</code></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
146
public/modules/dashboard/dashboard.html
Normal file
146
public/modules/dashboard/dashboard.html
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
<!-- Dashboard Module -->
|
||||||
|
<div id="dashboardModule" class="dashboard-module hidden h-screen flex flex-col">
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="bg-white dark:bg-slate-900 border-b border-slate-200 dark:border-slate-800 h-16 flex items-center justify-between px-4 sm:px-6 shadow-sm z-10">
|
||||||
|
<div class="flex items-center gap-2 sm:gap-4">
|
||||||
|
<button id="toggleSidebar" class="p-2 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-lg md:hidden">
|
||||||
|
<i data-lucide="menu" class="w-6 h-6"></i>
|
||||||
|
</button>
|
||||||
|
<div class="flex items-center gap-2 text-blue-600 dark:text-blue-400">
|
||||||
|
<i data-lucide="database" class="w-6 h-6"></i>
|
||||||
|
<span class="font-bold text-lg hidden sm:inline">SensoLab</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<button id="toggleTheme" class="p-2 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-lg transition-colors" title="Переключить тему">
|
||||||
|
<i data-lucide="moon" class="w-5 h-5 dark:hidden"></i>
|
||||||
|
<i data-lucide="sun" class="w-5 h-5 hidden dark:block"></i>
|
||||||
|
</button>
|
||||||
|
<div class="h-6 w-px bg-slate-200 dark:bg-slate-700 hidden sm:block"></div>
|
||||||
|
<div class="flex items-center gap-2 text-sm">
|
||||||
|
<div class="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center text-white font-semibold hidden sm:flex" id="avatarCircle">A</div>
|
||||||
|
<div class="hidden sm:block">
|
||||||
|
<p class="font-semibold text-slate-900 dark:text-white text-sm" id="userName">Admin</p>
|
||||||
|
<p class="text-xs text-slate-500 dark:text-slate-400" id="userRole">Администратор</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button id="logoutBtn" class="p-2 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors" title="Выход">
|
||||||
|
<i data-lucide="log-out" class="w-5 h-5 text-red-500"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="flex flex-1 overflow-hidden">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<aside id="sidebar" class="fixed md:static inset-y-16 left-0 w-64 bg-white dark:bg-slate-900 border-r border-slate-200 dark:border-slate-800 overflow-y-auto transform -translate-x-full md:translate-x-0 transition-transform z-40">
|
||||||
|
<nav class="p-4 space-y-2">
|
||||||
|
<h3 class="px-4 py-2 text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider">Меню</h3>
|
||||||
|
|
||||||
|
<a href="#dashboard" class="nav-item active flex items-center gap-3 px-4 py-3 rounded-lg font-medium transition-colors">
|
||||||
|
<i data-lucide="home" class="w-5 h-5"></i>
|
||||||
|
<span>Панель управления</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="#databases" class="nav-item flex items-center gap-3 px-4 py-3 rounded-lg font-medium transition-colors">
|
||||||
|
<i data-lucide="database" class="w-5 h-5"></i>
|
||||||
|
<span>Базы данных</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="#tables" class="nav-item flex items-center gap-3 px-4 py-3 rounded-lg font-medium transition-colors">
|
||||||
|
<i data-lucide="table" class="w-5 h-5"></i>
|
||||||
|
<span>Таблицы</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="#queries" class="nav-item flex items-center gap-3 px-4 py-3 rounded-lg font-medium transition-colors">
|
||||||
|
<i data-lucide="terminal" class="w-5 h-5"></i>
|
||||||
|
<span>SQL Запросы</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="my-4 h-px bg-slate-200 dark:bg-slate-700"></div>
|
||||||
|
|
||||||
|
<h3 class="px-4 py-2 text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider">Администрирование</h3>
|
||||||
|
|
||||||
|
<a href="#admin-users" class="nav-item flex items-center gap-3 px-4 py-3 rounded-lg font-medium transition-colors">
|
||||||
|
<i data-lucide="users" class="w-5 h-5"></i>
|
||||||
|
<span>Пользователи</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="#admin-settings" class="nav-item flex items-center gap-3 px-4 py-3 rounded-lg font-medium transition-colors">
|
||||||
|
<i data-lucide="settings" class="w-5 h-5"></i>
|
||||||
|
<span>Настройки</span>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="flex-1 overflow-auto">
|
||||||
|
<div id="dashboardContent" class="p-4 sm:p-6 space-y-6">
|
||||||
|
<!-- Stats Row -->
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-slate-500 dark:text-slate-400">Таблиц</p>
|
||||||
|
<p class="text-2xl font-bold text-slate-900 dark:text-white" id="statsTableCount">0</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 bg-blue-100 dark:bg-blue-900/30 rounded-lg">
|
||||||
|
<i data-lucide="table" class="w-6 h-6 text-blue-600 dark:text-blue-400"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-slate-500 dark:text-slate-400">Записей</p>
|
||||||
|
<p class="text-2xl font-bold text-slate-900 dark:text-white" id="statsRecordCount">0</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 bg-green-100 dark:bg-green-900/30 rounded-lg">
|
||||||
|
<i data-lucide="database" class="w-6 h-6 text-green-600 dark:text-green-400"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-slate-500 dark:text-slate-400">Пользователей</p>
|
||||||
|
<p class="text-2xl font-bold text-slate-900 dark:text-white" id="statsUserCount">0</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 bg-purple-100 dark:bg-purple-900/30 rounded-lg">
|
||||||
|
<i data-lucide="users" class="w-6 h-6 text-purple-600 dark:text-purple-400"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-slate-500 dark:text-slate-400">Размер БД</p>
|
||||||
|
<p class="text-2xl font-bold text-slate-900 dark:text-white" id="statsDbSize">-</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 bg-orange-100 dark:bg-orange-900/30 rounded-lg">
|
||||||
|
<i data-lucide="hard-drive" class="w-6 h-6 text-orange-600 dark:text-orange-400"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tables Preview -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h2 class="text-lg font-semibold text-slate-900 dark:text-white flex items-center gap-2">
|
||||||
|
<i data-lucide="table" class="w-5 h-5"></i>
|
||||||
|
Таблицы
|
||||||
|
</h2>
|
||||||
|
<a href="#tables" class="text-sm text-blue-600 dark:text-blue-400 hover:underline">Все таблицы</a>
|
||||||
|
</div>
|
||||||
|
<div id="tablesList" class="space-y-2">
|
||||||
|
<p class="text-slate-500 dark:text-slate-400 text-sm">Загрузка...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
275
public/styles/animations.css
Normal file
275
public/styles/animations.css
Normal file
@@ -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%;
|
||||||
|
}
|
||||||
266
public/styles/main.css
Normal file
266
public/styles/main.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
279
public/styles/responsive.css
Normal file
279
public/styles/responsive.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
192
public/styles/theme.css
Normal file
192
public/styles/theme.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
338
server.js
338
server.js
@@ -4,77 +4,161 @@ 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');
|
||||||
|
|
||||||
|
// Initialize Express app
|
||||||
const app = express();
|
const app = express();
|
||||||
|
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('.'));
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
|
||||||
// Session configuration
|
// Session configuration
|
||||||
app.use(session({
|
app.use(session({
|
||||||
secret: process.env.SESSION_SECRET || 'default-secret-change-this',
|
secret: process.env.SESSION_SECRET || 'default-secret-change-this',
|
||||||
resave: false,
|
resave: false,
|
||||||
saveUninitialized: 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({
|
const pool = new Pool({
|
||||||
host: process.env.DB_HOST,
|
host: process.env.DB_HOST || 'localhost',
|
||||||
port: process.env.DB_PORT,
|
port: process.env.DB_PORT || 5432,
|
||||||
database: process.env.DB_NAME,
|
database: process.env.DB_NAME || 'postgres',
|
||||||
user: process.env.DB_USER,
|
user: process.env.DB_USER || 'postgres',
|
||||||
password: process.env.DB_PASSWORD,
|
password: process.env.DB_PASSWORD || 'postgres',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test database connection on startup
|
// 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('❌ Error connecting to PostgreSQL:', err.message);
|
||||||
console.log('Проверьте настройки в .env файле');
|
console.log('Проверьте настройки в .env файле');
|
||||||
} else {
|
} else {
|
||||||
console.log('✅ Connected to PostgreSQL database');
|
console.log('✅ Connected to PostgreSQL database');
|
||||||
console.log(` Host: ${process.env.DB_HOST}:${process.env.DB_PORT}`);
|
console.log(` Host: ${process.env.DB_HOST || 'localhost'}:${process.env.DB_PORT || 5432}`);
|
||||||
console.log(` Database: ${process.env.DB_NAME}`);
|
console.log(` Database: ${process.env.DB_NAME || 'postgres'}`);
|
||||||
release();
|
release();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Helper: get primary key column for a table (returns null if none)
|
// Initialize database schema
|
||||||
async function getPrimaryKeyColumn(tableName) {
|
async function initializeDatabase() {
|
||||||
const result = await pool.query(`
|
const client = await pool.connect();
|
||||||
SELECT kcu.column_name
|
try {
|
||||||
FROM information_schema.table_constraints tc
|
// Check if users table exists, if not create it
|
||||||
JOIN information_schema.key_column_usage kcu
|
await client.query(`
|
||||||
ON tc.constraint_name = kcu.constraint_name
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
AND tc.table_schema = kcu.table_schema
|
id SERIAL PRIMARY KEY,
|
||||||
WHERE tc.constraint_type = 'PRIMARY KEY'
|
name VARCHAR(255) NOT NULL,
|
||||||
AND tc.table_name = $1
|
email VARCHAR(255) UNIQUE NOT NULL,
|
||||||
AND tc.table_schema = 'public'
|
password VARCHAR(255) NOT NULL,
|
||||||
LIMIT 1
|
role VARCHAR(50) NOT NULL DEFAULT 'viewer',
|
||||||
`, [tableName]);
|
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
|
// Initialize on startup
|
||||||
const requireAuth = (req, res, next) => {
|
initializeDatabase();
|
||||||
if (req.session && req.session.authenticated) {
|
|
||||||
next();
|
|
||||||
} else {
|
|
||||||
res.status(401).json({ error: 'Unauthorized' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Login endpoint - checks admin credentials from .env
|
// 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) => {
|
app.post('/api/login', async (req, res) => {
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
|
|
||||||
// Check against .env credentials
|
// Try users.json first
|
||||||
if (username === process.env.ADMIN_USERNAME && password === process.env.ADMIN_PASSWORD) {
|
const user = getUser(username);
|
||||||
// Test database connection
|
if (user && password === user.password) {
|
||||||
try {
|
try {
|
||||||
const client = await pool.connect();
|
const client = await pool.connect();
|
||||||
const result = await client.query('SELECT NOW() as time');
|
const result = await client.query('SELECT NOW() as time');
|
||||||
@@ -82,10 +166,12 @@ app.post('/api/login', async (req, res) => {
|
|||||||
|
|
||||||
req.session.authenticated = true;
|
req.session.authenticated = true;
|
||||||
req.session.username = username;
|
req.session.username = username;
|
||||||
|
req.session.role = user.role;
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Login successful',
|
message: 'Login successful',
|
||||||
|
role: user.role,
|
||||||
dbInfo: {
|
dbInfo: {
|
||||||
host: process.env.DB_HOST,
|
host: process.env.DB_HOST,
|
||||||
port: process.env.DB_PORT,
|
port: process.env.DB_PORT,
|
||||||
@@ -94,19 +180,55 @@ app.post('/api/login', async (req, res) => {
|
|||||||
serverTime: result.rows[0].time
|
serverTime: result.rows[0].time
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Database connection failed',
|
error: 'Database connection failed',
|
||||||
details: err.message
|
details: err.message
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
res.status(401).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Invalid credentials'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// Logout
|
||||||
@@ -121,6 +243,8 @@ app.get('/api/session', (req, res) => {
|
|||||||
res.json({
|
res.json({
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
username: req.session.username,
|
username: req.session.username,
|
||||||
|
role: req.session.role || 'viewer',
|
||||||
|
permissions: getRolePermissions(req.session.role),
|
||||||
dbInfo: {
|
dbInfo: {
|
||||||
host: process.env.DB_HOST,
|
host: process.env.DB_HOST,
|
||||||
port: process.env.DB_PORT,
|
port: process.env.DB_PORT,
|
||||||
@@ -135,6 +259,7 @@ app.get('/api/session', (req, res) => {
|
|||||||
// Get all tables
|
// Get all tables
|
||||||
app.get('/api/tables', requireAuth, async (req, res) => {
|
app.get('/api/tables', requireAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const role = req.session.role || 'viewer';
|
||||||
const result = await pool.query(`
|
const result = await pool.query(`
|
||||||
SELECT
|
SELECT
|
||||||
table_name as name,
|
table_name as name,
|
||||||
@@ -144,9 +269,12 @@ app.get('/api/tables', requireAuth, async (req, res) => {
|
|||||||
ORDER BY table_name
|
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
|
// Get row counts for each table
|
||||||
const tablesWithCounts = await Promise.all(
|
const tablesWithCounts = await Promise.all(
|
||||||
result.rows.map(async (table) => {
|
accessibleTables.map(async (table) => {
|
||||||
try {
|
try {
|
||||||
const countResult = await pool.query(`SELECT COUNT(*) as count FROM "${table.name}"`);
|
const countResult = await pool.query(`SELECT COUNT(*) as count FROM "${table.name}"`);
|
||||||
return {
|
return {
|
||||||
@@ -166,20 +294,31 @@ app.get('/api/tables', requireAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get table data with pagination and search
|
// Get table data with pagination, search, filters and sort
|
||||||
app.get('/api/tables/:tableName/data', requireAuth, async (req, res) => {
|
app.get('/api/tables/:tableName/data', requireAuth, async (req, res) => {
|
||||||
const { tableName } = req.params;
|
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 page = parseInt(req.query.page) || 1;
|
||||||
const limit = parseInt(req.query.limit) || 10;
|
const limit = parseInt(req.query.limit) || 10;
|
||||||
const search = req.query.search || '';
|
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;
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let whereClause = '';
|
let whereClause = '';
|
||||||
let params = [limit, offset];
|
let params = [];
|
||||||
|
let paramIndex = 1;
|
||||||
|
|
||||||
|
// Search
|
||||||
if (search) {
|
if (search) {
|
||||||
// Get column names for search
|
|
||||||
const columnsResult = await pool.query(`
|
const columnsResult = await pool.query(`
|
||||||
SELECT column_name
|
SELECT column_name
|
||||||
FROM information_schema.columns
|
FROM information_schema.columns
|
||||||
@@ -188,22 +327,46 @@ app.get('/api/tables/:tableName/data', requireAuth, async (req, res) => {
|
|||||||
`, [tableName]);
|
`, [tableName]);
|
||||||
|
|
||||||
const columns = columnsResult.rows.map(row => row.column_name);
|
const columns = columnsResult.rows.map(row => row.column_name);
|
||||||
const searchConditions = columns.map(col => `CAST("${col}" AS TEXT) ILIKE $${params.length + 1}`).join(' OR ');
|
const searchConditions = columns.map(col => `CAST("${col}" AS TEXT) ILIKE $${paramIndex}`).join(' OR ');
|
||||||
whereClause = `WHERE ${searchConditions}`;
|
whereClause = `WHERE ${searchConditions}`;
|
||||||
params.push(`%${search}%`);
|
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
|
// Get total count
|
||||||
const countResult = await pool.query(`SELECT COUNT(*) as total FROM "${tableName}" ${whereClause}`, params.slice(2));
|
const countResult = await pool.query(`SELECT COUNT(*) as total FROM "${tableName}" ${whereClause}`, params);
|
||||||
const total = parseInt(countResult.rows[0].total);
|
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
|
// Get data
|
||||||
const result = await pool.query(`
|
const result = await pool.query(`
|
||||||
SELECT * FROM "${tableName}"
|
SELECT * FROM "${tableName}"
|
||||||
${whereClause}
|
${whereClause}
|
||||||
ORDER BY (SELECT NULL) -- No specific order, but consistent
|
${orderBy}
|
||||||
LIMIT $1 OFFSET $2
|
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
|
||||||
`, params);
|
`, [...params, limit, offset]);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
data: result.rows,
|
data: result.rows,
|
||||||
@@ -251,6 +414,13 @@ app.get('/api/tables/:tableName/structure', requireAuth, async (req, res) => {
|
|||||||
// Create table
|
// Create table
|
||||||
app.post('/api/tables', requireAuth, async (req, res) => {
|
app.post('/api/tables', requireAuth, async (req, res) => {
|
||||||
const { name, columns } = req.body;
|
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 {
|
try {
|
||||||
let columnsSQL = columns.map(col => {
|
let columnsSQL = columns.map(col => {
|
||||||
@@ -272,6 +442,13 @@ app.post('/api/tables', requireAuth, async (req, res) => {
|
|||||||
// Delete table
|
// Delete table
|
||||||
app.delete('/api/tables/:tableName', requireAuth, async (req, res) => {
|
app.delete('/api/tables/:tableName', requireAuth, async (req, res) => {
|
||||||
const { tableName } = req.params;
|
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 {
|
try {
|
||||||
await pool.query(`DROP TABLE IF EXISTS "${tableName}"`);
|
await pool.query(`DROP TABLE IF EXISTS "${tableName}"`);
|
||||||
@@ -285,12 +462,51 @@ app.delete('/api/tables/:tableName', requireAuth, async (req, res) => {
|
|||||||
app.post('/api/tables/:tableName/records', requireAuth, async (req, res) => {
|
app.post('/api/tables/:tableName/records', requireAuth, async (req, res) => {
|
||||||
const { tableName } = req.params;
|
const { tableName } = req.params;
|
||||||
const data = req.body;
|
const data = req.body;
|
||||||
|
const role = req.session.role || 'viewer';
|
||||||
|
const folder = getTableFolder(tableName);
|
||||||
|
const perms = getRolePermissions(role);
|
||||||
|
|
||||||
const columns = Object.keys(data);
|
if (!perms.canEdit || (perms.folders && !perms.folders.includes(folder))) {
|
||||||
const values = Object.values(data);
|
return res.status(403).json({ error: 'Access denied' });
|
||||||
const placeholders = values.map((_, i) => `$${i + 1}`).join(', ');
|
}
|
||||||
|
|
||||||
try {
|
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 sql = `INSERT INTO "${tableName}" (${columns.map(c => `"${c}"`).join(', ')}) VALUES (${placeholders}) RETURNING *`;
|
||||||
const result = await pool.query(sql, values);
|
const result = await pool.query(sql, values);
|
||||||
res.json({ success: true, data: result.rows[0] });
|
res.json({ success: true, data: result.rows[0] });
|
||||||
@@ -302,8 +518,14 @@ app.post('/api/tables/:tableName/records', requireAuth, async (req, res) => {
|
|||||||
// Update record
|
// Update record
|
||||||
app.put('/api/tables/:tableName/records/:pk', requireAuth, async (req, res) => {
|
app.put('/api/tables/:tableName/records/:pk', requireAuth, async (req, res) => {
|
||||||
const { tableName, pk } = req.params;
|
const { tableName, pk } = 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' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = req.body;
|
||||||
const columns = Object.keys(data);
|
const columns = Object.keys(data);
|
||||||
const values = Object.values(data);
|
const values = Object.values(data);
|
||||||
const setClause = columns.map((col, i) => `"${col}" = $${i + 1}`).join(', ');
|
const setClause = columns.map((col, i) => `"${col}" = $${i + 1}`).join(', ');
|
||||||
@@ -321,6 +543,12 @@ app.put('/api/tables/:tableName/records/:pk', requireAuth, async (req, res) => {
|
|||||||
// Delete record
|
// Delete record
|
||||||
app.delete('/api/tables/:tableName/records/:pk', requireAuth, async (req, res) => {
|
app.delete('/api/tables/:tableName/records/:pk', requireAuth, async (req, res) => {
|
||||||
const { tableName, pk } = req.params;
|
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 {
|
try {
|
||||||
const primaryKey = await getPrimaryKeyColumn(tableName) || 'id';
|
const primaryKey = await getPrimaryKeyColumn(tableName) || 'id';
|
||||||
|
|||||||
127
src/routes/admin.js
Normal file
127
src/routes/admin.js
Normal file
@@ -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;
|
||||||
|
};
|
||||||
197
src/routes/auth.js
Normal file
197
src/routes/auth.js
Normal file
@@ -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;
|
||||||
|
};
|
||||||
174
src/routes/db-tables.js
Normal file
174
src/routes/db-tables.js
Normal file
@@ -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;
|
||||||
|
};
|
||||||
233
src/routes/users.js
Normal file
233
src/routes/users.js
Normal file
@@ -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;
|
||||||
|
};
|
||||||
39
users.json
Normal file
39
users.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"role": "superadmin",
|
||||||
|
"permissions": {
|
||||||
|
"folders": null,
|
||||||
|
"canCreate": true,
|
||||||
|
"canEdit": true,
|
||||||
|
"canDelete": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "frontend_admin",
|
||||||
|
"password": "frontend",
|
||||||
|
"role": "frontend_admin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "backend_admin",
|
||||||
|
"password": "backend",
|
||||||
|
"role": "backend_admin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "frontend_moder",
|
||||||
|
"password": "mod123",
|
||||||
|
"role": "frontend_moder"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "backend_moder",
|
||||||
|
"password": "mod123",
|
||||||
|
"role": "backend_moder"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "viewer",
|
||||||
|
"password": "viewer",
|
||||||
|
"role": "viewer"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user