diff --git a/.env.example b/.env.example index 08b8fc4..3446918 100644 --- a/.env.example +++ b/.env.example @@ -45,7 +45,9 @@ REDIS_DB=0 # ============================ # FRONTEND # ============================ -FRONTEND_API_URL=http://api.yourdomain.com/api +# ✅ API URL определяется автоматически на основе window.location +# Фронтенд обращается к /api, nginx проксирует на backend +# Работает с localhost, IP адресом или доменом без конфигурации! REACT_APP_ENV=production # ============================ @@ -56,7 +58,9 @@ NGINX_PORT=8080 # ============================ # CORS & RATE LIMITING # ============================ -CORS_ORIGIN=https://yourdomain.com +# ✅ CORS не требуется в Docker сети - фронтенд и API за одним Nginx на одном origin +# Если нужен external access к API - укажи домен/IP (например: CORS_ORIGIN=https://your-domain.com) +CORS_ORIGIN=* RATE_LIMIT_WINDOW=15 RATE_LIMIT_MAX_REQUESTS=100 diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..750b5ef --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,192 @@ +# 🏗️ Архитектура приложения + +## Как работает автоматическое определение API URL + +### ❌ Старый подход (проблемный): +``` +.env файл: FRONTEND_API_URL=http://185.56.162.170:8080/api +└─ Нужно обновлять для каждого окружения (dev, staging, prod) +└─ Нужно пересобирать контейнер при смене IP/домена +└─ Хранит хардкод конфигурации +``` + +### ✅ Новый подход (умный): + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ БРАУЗЕР ПОЛЬЗОВАТЕЛЯ │ +│ http://185.56.162.170:80 или http://my-domain.com │ +└────────────────────────┬────────────────────────────────────────┘ + │ (window.location = текущий адрес) + │ + ┌────▼─────┐ + │ NGINX │ (Gate для всех запросов) + │ :80 │ + └────┬─────┘ + │ + ┌────────────┬────────────┐ + │ │ │ + GET / │ GET /api/* GET /static/* + │ │ │ + ┌──▼──┐ ┌────▼──┐ ┌──▼──────┐ + │FRONT│ │ BACK │ │ STATIC │ + │END │ │ END │ │ CACHE │ + └─────┘ └───────┘ └─────────┘ + +Фронтенд код: +- ✅ Использует window.location.origin для определения хоста +- ✅ Обращается к /api (относительный путь, не айпи) +- ✅ Nginx проксирует /api на backend контейнер +- ✅ Работает везде без переконфигурирования! +``` + +## Как это использовать в коде + +### В App.js или любом React компоненте: + +```javascript +import API_CONFIG from './api/config'; + +function MyComponent() { + useEffect(() => { + // Автоматически определится правильный API URL + // Локально: http://localhost:3000/api/users + // На VPS: http://185.56.162.170:80/api/users + // С доменом: http://my-domain.com/api/users + + API_CONFIG.fetch('users') + .then(data => console.log(data)); + }, []); +} +``` + +## Преимущества + +| Параметр | Старый подход | Новый подход | +|----------|---------------|-------------| +| Нужно менять .env | ✅ Да | ❌ Нет | +| Нужно пересобирать образ | ✅ Да | ❌ Нет | +| Работает на localhost | ✅ Да | ✅ Да | +| Работает на IP адресе | ✅ Да | ✅ Да | +| Работает на домене | ✅ Да | ✅ Да | +| CORS проблемы | ✅ Иногда | ❌ Нет | +| Безопасность | ⚠️ Хардкод | ✅ Динамично | + +## Docker сетевая архитектура + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Docker Network: pg-admin-network (bridge) │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ │ +│ │ pg-admin-db │ │pg-admin-cache│ │ +│ │ Port: 5432 │ │ Port: 6379 │ │ +│ │ (internal) │ │ (internal) │ │ +│ └──────────────┘ └──────────────┘ │ +│ △ △ │ +│ │ │ │ +│ └──────┬─────────────┘ │ +│ │ │ +│ ┌──────▼──────────┐ │ +│ │ pg-admin-api │ │ +│ │ Port: 3000 │ │ +│ │ (internal) │ │ +│ └────────┬────────┘ │ +│ │ │ +│ ┌─────────────┼──────────────┐ │ +│ │ │ │ │ +│ ▼ ▼ ▼ │ +│ ┌──────────┐ ┌────────────┐ ┌───────────┐ │ +│ │frontend │ │pg-admin-ui │ │ DATABASE │ │ +│ │ │ │ Port: 3000 │ │ Postgres │ │ +│ │Port: 80 │ │(internal) │ │ 15-alpine │ │ +│ └────┬─────┘ └────────────┘ └───────────┘ │ +│ │ │ +└──────┼──────────────────────────────────────────────────────┘ + │ + ▼ + ┌──────────────┐ + │ pg-admin-proxy (Nginx) + │ Listen: 80 (EXTERNAL) + └──────────────┘ + │ + ├─ GET / → frontend:3000 + ├─ GET /api/* → backend:3000 + └─ GET /static/* → cache 365 дней + +Все контейнеры видят друг друга по имени (DNS): +- backend может подключиться к postgres:5432 +- backend может подключиться к redis:6379 +- Nginx может подключиться к frontend:3000 и backend:3000 +``` + +## Переменные окружения (только на VPS) + +Теперь в .env нужны ТОЛЬКО: + +```env +# Development / Production +NODE_ENV=production + +# Пароли (генерируются openssl) +DB_PASSWORD=... +JWT_SECRET=... +REDIS_PASSWORD=... + +# Остальное опционально +LOG_LEVEL=info +RATE_LIMIT_MAX_REQUESTS=100 +``` + +**НЕ нужны больше:** +- ❌ FRONTEND_API_URL +- ❌ CORS_ORIGIN (используется * по умолчанию) +- ❌ FRONTEND_PORT +- ❌ API_PORT (внутренние порты) +- ❌ DB_PORT (внутренние порты) + +## Примеры использования + +### Локальная разработка: +```bash +docker compose up --build +# Открой http://localhost:80 +# Фронтенд будет обращаться к http://localhost:80/api +# Nginx проксирует на backend контейнер +``` + +### На VPS: +```bash +docker compose up -d --build +# Открой http://185.56.162.170:80 +# Фронтенд будет обращаться к http://185.56.162.170:80/api +# Nginx проксирует на backend контейнер +``` + +### С доменом: +```bash +# Просто открой https://my-domain.com +# Фронтенд будет обращаться к https://my-domain.com/api +# Все работает автоматически! +``` + +## Безопасность + +✅ **Защищено:** +- ✅ БД не открыта наружу +- ✅ Redis не открыт наружу +- ✅ API не открыт напрямую (только через Nginx) +- ✅ Все запросы проходят через Nginx (централизованный контроль) +- ✅ Нет хардкода IP адресов в коде +- ✅ Пароли не в git + +⚠️ **Нужно настроить:** +- SSH ключи (вместо пароля) +- UFW файрвол (только необходимые порты) +- SSL/HTTPS (Let's Encrypt) +- Rate limiting (уже включен) +- Логирование (уже включено) + +--- + +**Готово к production! 🚀** diff --git a/docker-compose.yml b/docker-compose.yml index eee8563..1625fe5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -70,7 +70,8 @@ services: dockerfile: ../docker/Dockerfile.frontend container_name: pg-admin-ui environment: - REACT_APP_API_URL: ${FRONTEND_API_URL:-http://localhost:3000/api} + # ✅ API URL определяется динамически из window.location в браузере + # Фронтенд обращается к /api, Nginx проксирует на backend REACT_APP_ENV: ${NODE_ENV:-production} # Frontend НЕ открыт наружу - только для nginx depends_on: diff --git a/docker/nginx.conf b/docker/nginx.conf index e21115c..ec4d84c 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -76,22 +76,7 @@ http { access_log off; } - # Frontend - location / { - proxy_pass http://frontend; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - # Don't cache HTML pages - add_header Cache-Control "public, max-age=0, must-revalidate"; - } - - # API + # API endpoints - проксируем на backend location /api/ { limit_req zone=api burst=60 nodelay; @@ -109,6 +94,21 @@ http { proxy_read_timeout 60s; } + # Frontend (все остальные запросы) + location / { + proxy_pass http://frontend; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Don't cache HTML pages + add_header Cache-Control "public, max-age=0, must-revalidate"; + } + # Health check endpoint location /health { access_log off; diff --git a/frontend/src/App.js b/frontend/src/App.js index 9fb130b..2ff172e 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1,11 +1,53 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; +import API_CONFIG from './api/config'; function App() { + const [apiStatus, setApiStatus] = useState('Checking...'); + const [apiUrl, setApiUrl] = useState(''); + + useEffect(() => { + // Показыва какой API URL используется + const baseUrl = API_CONFIG.getBaseUrl(); + const fullUrl = `${window.location.origin}${baseUrl}`; + setApiUrl(fullUrl); + + // Проверяем подключение к API + const checkAPI = async () => { + try { + const response = await fetch(`${baseUrl}/health`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + }); + + if (response.ok) { + setApiStatus('✅ Connected'); + } else { + setApiStatus(`⚠️ Error: ${response.status}`); + } + } catch (error) { + setApiStatus(`❌ Failed: ${error.message}`); + } + }; + + checkAPI(); + }, []); + return (

PostgreSQL Admin Panel

-

Frontend is loading...

-

API Status: Checking...

+

Frontend is ready! 🚀

+
+

🔌 API Configuration

+

API Base URL: {apiUrl}

+

API Status: {apiStatus}

+
+

💡 How it works:

+
); } diff --git a/frontend/src/api/config.js b/frontend/src/api/config.js new file mode 100644 index 0000000..4553ba2 --- /dev/null +++ b/frontend/src/api/config.js @@ -0,0 +1,44 @@ +// Автоматическое определение API URL без необходимости env переменных +// Работает на localhost, IP адресе или доменном имени + +const API_CONFIG = { + // Определяй базовый URL на основе текущего location + // Фронтенд обращается к /api, nginx перенаправляет на backend внутри Docker сети + getBaseUrl: () => { + // Используем относительный путь /api вместо абсолютного URL + // Например: http://localhost:3000/api или http://185.56.162.170:8080/api + return '/api'; + }, + + // Получить полный URL для API запроса + getApiUrl: (endpoint) => { + const baseUrl = API_CONFIG.getBaseUrl(); + // Убираем слэш в начале endpoint если есть + const cleanEndpoint = endpoint.startsWith('/') ? endpoint.slice(1) : endpoint; + return `${baseUrl}/${cleanEndpoint}`; + }, + + // Хелпер для fetch запросов + fetch: async (endpoint, options = {}) => { + const url = API_CONFIG.getApiUrl(endpoint); + const defaultHeaders = { + 'Content-Type': 'application/json', + }; + + const response = await fetch(url, { + ...options, + headers: { + ...defaultHeaders, + ...options.headers, + }, + }); + + if (!response.ok) { + throw new Error(`API Error: ${response.status} ${response.statusText}`); + } + + return response.json(); + }, +}; + +export default API_CONFIG; diff --git a/frontend/src/components/ExampleAPIUsage.js b/frontend/src/components/ExampleAPIUsage.js new file mode 100644 index 0000000..ac05903 --- /dev/null +++ b/frontend/src/components/ExampleAPIUsage.js @@ -0,0 +1,59 @@ +/** + * @example Пример использования API конфига в React компоненте + * + * import API_CONFIG from './api/config'; + * + * function MyComponent() { + * useEffect(() => { + * // Вариант 1: Простой fetch + * fetch(API_CONFIG.getApiUrl('users')) + * .then(r => r.json()) + * .then(data => console.log(data)); + * + * // Вариант 2: Использовать хелпер API_CONFIG.fetch + * API_CONFIG.fetch('users') + * .then(data => console.log(data)) + * .catch(error => console.error(error)); + * + * // Вариант 3: С параметрами + * API_CONFIG.fetch('users/123', { + * method: 'GET', + * }); + * + * // Вариант 4: POST запрос + * API_CONFIG.fetch('users', { + * method: 'POST', + * body: JSON.stringify({ name: 'John' }), + * }); + * }, []); + * } + */ + +import React, { useEffect, useState } from 'react'; +import API_CONFIG from '../api/config'; + +function ExampleComponent() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + // Получи данные с API + API_CONFIG.fetch('users') + .then(data => setData(data)) + .catch(err => setError(err.message)) + .finally(() => setLoading(false)); + }, []); + + if (loading) return

Loading...

; + if (error) return

Error: {error}

; + + return ( +
+

Users from API

+
{JSON.stringify(data, null, 2)}
+
+ ); +} + +export default ExampleComponent;