This commit is contained in:
2026-03-18 16:09:47 +07:00
parent ab3d27a4f1
commit 1ad90395dc
7 changed files with 364 additions and 22 deletions

View File

@@ -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
View 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! 🚀**

View File

@@ -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:

View File

@@ -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;

View File

@@ -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>
); );
} }

View 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;

View 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;