54545
This commit is contained in:
@@ -45,7 +45,9 @@ REDIS_DB=0
|
|||||||
# ============================
|
# ============================
|
||||||
# FRONTEND
|
# FRONTEND
|
||||||
# ============================
|
# ============================
|
||||||
FRONTEND_API_URL=http://api.yourdomain.com/api
|
# ✅ API URL определяется автоматически на основе window.location
|
||||||
|
# Фронтенд обращается к /api, nginx проксирует на backend
|
||||||
|
# Работает с localhost, IP адресом или доменом без конфигурации!
|
||||||
REACT_APP_ENV=production
|
REACT_APP_ENV=production
|
||||||
|
|
||||||
# ============================
|
# ============================
|
||||||
@@ -56,7 +58,9 @@ NGINX_PORT=8080
|
|||||||
# ============================
|
# ============================
|
||||||
# CORS & RATE LIMITING
|
# 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_WINDOW=15
|
||||||
RATE_LIMIT_MAX_REQUESTS=100
|
RATE_LIMIT_MAX_REQUESTS=100
|
||||||
|
|
||||||
|
|||||||
192
ARCHITECTURE.md
Normal file
192
ARCHITECTURE.md
Normal file
@@ -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! 🚀**
|
||||||
@@ -70,7 +70,8 @@ services:
|
|||||||
dockerfile: ../docker/Dockerfile.frontend
|
dockerfile: ../docker/Dockerfile.frontend
|
||||||
container_name: pg-admin-ui
|
container_name: pg-admin-ui
|
||||||
environment:
|
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}
|
REACT_APP_ENV: ${NODE_ENV:-production}
|
||||||
# Frontend НЕ открыт наружу - только для nginx
|
# Frontend НЕ открыт наружу - только для nginx
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@@ -76,22 +76,7 @@ http {
|
|||||||
access_log off;
|
access_log off;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Frontend
|
# API endpoints - проксируем на backend
|
||||||
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
|
|
||||||
location /api/ {
|
location /api/ {
|
||||||
limit_req zone=api burst=60 nodelay;
|
limit_req zone=api burst=60 nodelay;
|
||||||
|
|
||||||
@@ -109,6 +94,21 @@ http {
|
|||||||
proxy_read_timeout 60s;
|
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
|
# Health check endpoint
|
||||||
location /health {
|
location /health {
|
||||||
access_log off;
|
access_log off;
|
||||||
|
|||||||
@@ -1,11 +1,53 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import API_CONFIG from './api/config';
|
||||||
|
|
||||||
function App() {
|
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 (
|
return (
|
||||||
<div style={{ padding: '20px', fontFamily: 'Arial' }}>
|
<div style={{ padding: '20px', fontFamily: 'Arial' }}>
|
||||||
<h1>PostgreSQL Admin Panel</h1>
|
<h1>PostgreSQL Admin Panel</h1>
|
||||||
<p>Frontend is loading...</p>
|
<p>Frontend is ready! 🚀</p>
|
||||||
<p>API Status: <span id="status">Checking...</span></p>
|
<hr />
|
||||||
|
<h3>🔌 API Configuration</h3>
|
||||||
|
<p><strong>API Base URL:</strong> {apiUrl}</p>
|
||||||
|
<p><strong>API Status:</strong> {apiStatus}</p>
|
||||||
|
<hr />
|
||||||
|
<h3>💡 How it works:</h3>
|
||||||
|
<ul>
|
||||||
|
<li>✅ Frontend automatically detects API URL from window.location</li>
|
||||||
|
<li>✅ Nginx proxies /api/* to backend internally</li>
|
||||||
|
<li>✅ Works on localhost, IP, or domain without configuration</li>
|
||||||
|
<li>✅ No hardcoded environment variables needed for API URL</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
44
frontend/src/api/config.js
Normal file
44
frontend/src/api/config.js
Normal file
@@ -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;
|
||||||
59
frontend/src/components/ExampleAPIUsage.js
Normal file
59
frontend/src/components/ExampleAPIUsage.js
Normal file
@@ -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 <p>Loading...</p>;
|
||||||
|
if (error) return <p>Error: {error}</p>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>Users from API</h2>
|
||||||
|
<pre>{JSON.stringify(data, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExampleComponent;
|
||||||
Reference in New Issue
Block a user