feat: Complete assets proxy implementation with documentation
- Add comprehensive documentation in docs/ASSETS_PROXY.md - Add contract comments in normalizeAssetUrl and proxy_asset - Verify all components use normalizeAssetUrl - Verify ENV variables are correctly set - Add troubleshooting guide
This commit is contained in:
@@ -6,6 +6,17 @@
|
|||||||
*
|
*
|
||||||
* IMPORTANT: Do NOT manually concatenate /api/static anywhere in the codebase.
|
* IMPORTANT: Do NOT manually concatenate /api/static anywhere in the codebase.
|
||||||
* Always use this function instead.
|
* Always use this function instead.
|
||||||
|
*
|
||||||
|
* CONTRACT with Asset Proxy (/city/assets/proxy/{path}):
|
||||||
|
*
|
||||||
|
* Input from DB: https://assets.daarion.space/daarion-assets/microdao/logo/2025/12/02/abc123.png
|
||||||
|
* Output: /api/city/assets/proxy/microdao/logo/2025/12/02/abc123.png
|
||||||
|
*
|
||||||
|
* The proxy endpoint receives: microdao/logo/2025/12/02/abc123.png
|
||||||
|
* And prepends ASSETS_BUCKET (daarion-assets) internally.
|
||||||
|
*
|
||||||
|
* @param url - Asset URL from database (can be MinIO URL, legacy /static/ path, etc.)
|
||||||
|
* @returns Normalized URL for use in <img src> or CSS backgroundImage
|
||||||
*/
|
*/
|
||||||
export function normalizeAssetUrl(url: string | null | undefined): string | null {
|
export function normalizeAssetUrl(url: string | null | undefined): string | null {
|
||||||
if (!url) return null;
|
if (!url) return null;
|
||||||
@@ -17,6 +28,7 @@ export function normalizeAssetUrl(url: string | null | undefined): string | null
|
|||||||
if (url.includes('assets.daarion.space')) {
|
if (url.includes('assets.daarion.space')) {
|
||||||
// Extract path after /daarion-assets/: https://assets.daarion.space/daarion-assets/microdao/logo/...
|
// Extract path after /daarion-assets/: https://assets.daarion.space/daarion-assets/microdao/logo/...
|
||||||
// Convert to: /api/city/assets/proxy/microdao/logo/...
|
// Convert to: /api/city/assets/proxy/microdao/logo/...
|
||||||
|
// Note: /daarion-assets/ prefix is removed - proxy will add ASSETS_BUCKET internally
|
||||||
const match = url.match(/\/daarion-assets\/(.+)$/);
|
const match = url.match(/\/daarion-assets\/(.+)$/);
|
||||||
if (match) {
|
if (match) {
|
||||||
return `/api/city/assets/proxy/${match[1]}`;
|
return `/api/city/assets/proxy/${match[1]}`;
|
||||||
|
|||||||
198
docs/ASSETS_PROXY.md
Normal file
198
docs/ASSETS_PROXY.md
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
# Assets Proxy — Документація
|
||||||
|
|
||||||
|
## Огляд
|
||||||
|
|
||||||
|
DAARION використовує **MinIO** (S3-compatible object storage) для зберігання assets (логотипи, банери, аватарки). Для доступу до assets без налаштування DNS для `assets.daarion.space` використовується **Asset Proxy** через `city-service`.
|
||||||
|
|
||||||
|
## Архітектура
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐
|
||||||
|
│ Frontend │ → /api/city/assets/proxy/microdao/logo/...
|
||||||
|
└─────────────┘
|
||||||
|
│
|
||||||
|
↓
|
||||||
|
┌─────────────┐
|
||||||
|
│ city-service│ → /city/assets/proxy/{path}
|
||||||
|
│ (FastAPI) │
|
||||||
|
└─────────────┘
|
||||||
|
│
|
||||||
|
↓
|
||||||
|
┌─────────────┐
|
||||||
|
│ MinIO │ → daarion-assets/microdao/logo/...
|
||||||
|
│ (Docker) │
|
||||||
|
└─────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Контракт normalizeAssetUrl ↔ Asset Proxy
|
||||||
|
|
||||||
|
### Вхідний URL з БД
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://assets.daarion.space/daarion-assets/microdao/logo/2025/12/02/abc123.png
|
||||||
|
```
|
||||||
|
|
||||||
|
### Вихідний URL для фронта (після normalizeAssetUrl)
|
||||||
|
|
||||||
|
```text
|
||||||
|
/api/city/assets/proxy/microdao/logo/2025/12/02/abc123.png
|
||||||
|
```
|
||||||
|
|
||||||
|
### Шлях до MinIO (всередині proxy)
|
||||||
|
|
||||||
|
```text
|
||||||
|
daarion-assets/microdao/logo/2025/12/02/abc123.png
|
||||||
|
```
|
||||||
|
|
||||||
|
**Важливо:** `normalizeAssetUrl` відрізає `daarion-assets/` з URL, тому proxy отримує шлях без префіксу бакету. Proxy додає `ASSETS_BUCKET` автоматично.
|
||||||
|
|
||||||
|
## Використання в компонентах
|
||||||
|
|
||||||
|
**ЗАВЖДИ** використовуйте `normalizeAssetUrl` для всіх asset URLs:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { normalizeAssetUrl } from '@/lib/utils/assetUrl';
|
||||||
|
|
||||||
|
// ✅ ПРАВИЛЬНО
|
||||||
|
<img src={normalizeAssetUrl(logo_url)!} alt="Logo" />
|
||||||
|
|
||||||
|
// ❌ НЕПРАВИЛЬНО
|
||||||
|
<img src={logo_url} alt="Logo" />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Endpoint
|
||||||
|
|
||||||
|
### City Service
|
||||||
|
|
||||||
|
- **Path:** `/city/assets/proxy/{path:path}`
|
||||||
|
- **Method:** `GET`
|
||||||
|
- **Router prefix:** `/city` (монтується в `main.py` без додаткового префіксу)
|
||||||
|
- **Повний URL:** `/api/city/assets/proxy/{path}`
|
||||||
|
|
||||||
|
### Параметри
|
||||||
|
|
||||||
|
- `path` — шлях до файлу в MinIO (без префіксу бакету)
|
||||||
|
- Приклад: `microdao/logo/2025/12/02/abc123.png`
|
||||||
|
|
||||||
|
### Відповідь
|
||||||
|
|
||||||
|
- **200 OK** — файл знайдено
|
||||||
|
- `Content-Type`: `image/png`, `image/jpeg`, тощо
|
||||||
|
- `Cache-Control`: `public, max-age=86400, immutable`
|
||||||
|
- `Access-Control-Allow-Origin`: `*`
|
||||||
|
|
||||||
|
- **404 Not Found** — файл не знайдено в MinIO
|
||||||
|
|
||||||
|
## ENV змінні
|
||||||
|
|
||||||
|
### City Service
|
||||||
|
|
||||||
|
```env
|
||||||
|
MINIO_ENDPOINT=http://minio:9000
|
||||||
|
MINIO_ROOT_USER=assets-admin
|
||||||
|
MINIO_ROOT_PASSWORD=<password>
|
||||||
|
ASSETS_BUCKET=daarion-assets
|
||||||
|
ASSETS_PUBLIC_BASE_URL=https://assets.daarion.space/daarion-assets
|
||||||
|
```
|
||||||
|
|
||||||
|
**Примітка:** `ASSETS_PUBLIC_BASE_URL` використовується тільки для генерації URL при завантаженні. Для читання використовується proxy.
|
||||||
|
|
||||||
|
## Приклади
|
||||||
|
|
||||||
|
### Завантаження asset
|
||||||
|
|
||||||
|
```python
|
||||||
|
from lib.assets_client import upload_asset
|
||||||
|
|
||||||
|
url = upload_asset(
|
||||||
|
file_obj,
|
||||||
|
content_type="image/png",
|
||||||
|
prefix="microdao/logo",
|
||||||
|
filename="logo.png"
|
||||||
|
)
|
||||||
|
# Повертає: https://assets.daarion.space/daarion-assets/microdao/logo/2025/12/02/abc123.png
|
||||||
|
```
|
||||||
|
|
||||||
|
### Відображення asset на фронті
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const logoUrl = "https://assets.daarion.space/daarion-assets/microdao/logo/2025/12/02/abc123.png";
|
||||||
|
const normalized = normalizeAssetUrl(logoUrl);
|
||||||
|
// normalized = "/api/city/assets/proxy/microdao/logo/2025/12/02/abc123.png"
|
||||||
|
|
||||||
|
<img src={normalized} alt="Logo" />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### 404 Not Found
|
||||||
|
|
||||||
|
1. Перевірте чи файл існує в MinIO:
|
||||||
|
```bash
|
||||||
|
docker exec daarion-minio mc ls minio/daarion-assets/microdao/logo/2025/12/02/
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Перевірте чи правильний шлях передається в proxy:
|
||||||
|
```bash
|
||||||
|
curl -I "https://daarion.space/api/city/assets/proxy/microdao/logo/2025/12/02/abc123.png"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Перевірте логи city-service:
|
||||||
|
```bash
|
||||||
|
docker logs daarion-city-service | grep "assets/proxy"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 500 Internal Server Error
|
||||||
|
|
||||||
|
1. Перевірте ENV змінні в docker-compose:
|
||||||
|
```bash
|
||||||
|
docker exec daarion-city-service env | grep MINIO
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Перевірте чи MinIO доступний:
|
||||||
|
```bash
|
||||||
|
docker exec daarion-city-service curl -I http://minio:9000/minio/health/live
|
||||||
|
```
|
||||||
|
|
||||||
|
### Assets не відображаються на фронті
|
||||||
|
|
||||||
|
1. Перевірте чи компонент використовує `normalizeAssetUrl`:
|
||||||
|
```typescript
|
||||||
|
// ✅ ПРАВИЛЬНО
|
||||||
|
src={normalizeAssetUrl(url)}
|
||||||
|
|
||||||
|
// ❌ НЕПРАВИЛЬНО
|
||||||
|
src={url}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Перевірте в DevTools Network tab:
|
||||||
|
- Чи запити йдуть на `/api/city/assets/proxy/...`?
|
||||||
|
- Який статус відповіді (200/404/500)?
|
||||||
|
|
||||||
|
## Додавання нових компонентів
|
||||||
|
|
||||||
|
При додаванні нового компонента, який відображає assets:
|
||||||
|
|
||||||
|
1. **Імпортуйте** `normalizeAssetUrl`:
|
||||||
|
```typescript
|
||||||
|
import { normalizeAssetUrl } from '@/lib/utils/assetUrl';
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Використовуйте** для всіх asset URLs:
|
||||||
|
```typescript
|
||||||
|
<img src={normalizeAssetUrl(logo_url)!} alt="Logo" />
|
||||||
|
<div style={{ backgroundImage: `url(${normalizeAssetUrl(banner_url)})` }} />
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **НЕ використовуйте** прямі URLs з БД без нормалізації.
|
||||||
|
|
||||||
|
## Міграція зі старих URL
|
||||||
|
|
||||||
|
Якщо в БД є старі формати URL:
|
||||||
|
|
||||||
|
- `/static/uploads/logo.png` → автоматично конвертується в `/api/static/uploads/logo.png`
|
||||||
|
- `/assets/logos/logo.png` → залишається як є
|
||||||
|
- `https://assets.daarion.space/...` → конвертується в `/api/city/assets/proxy/...`
|
||||||
|
|
||||||
|
`normalizeAssetUrl` обробляє всі ці випадки автоматично.
|
||||||
|
|
||||||
135
docs/tasks/TASK_PHASE_ASSETS_PROXY_DEBUG_v2.md
Normal file
135
docs/tasks/TASK_PHASE_ASSETS_PROXY_DEBUG_v2.md
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
# TASK_PHASE_ASSETS_PROXY_DEBUG_v2 — MinIO Asset Proxy (логотипи/банери)
|
||||||
|
|
||||||
|
## Контекст
|
||||||
|
|
||||||
|
- MinIO використовується для зберігання assets (логотипи, банери, зображення).
|
||||||
|
- Раніше доступ йшов через `https://assets.daarion.space/...` (NGINX → MinIO).
|
||||||
|
- Було додано proxy в city-service:
|
||||||
|
- endpoint: `/city/assets/proxy/{path}` (далі — Asset Proxy).
|
||||||
|
- `normalizeAssetUrl` перетворює `https://assets.daarion.space/daarion-assets/...` у `/api/city/assets/proxy/...`.
|
||||||
|
- Cursor повідомляв:
|
||||||
|
- Proxy endpoint працює, локально повертає PNG.
|
||||||
|
- Компоненти використовують `normalizeAssetUrl`.
|
||||||
|
- На проді фактично:
|
||||||
|
- логотипи/банери на `daarion.space` знову не відображаються,
|
||||||
|
- у браузері видно або «биті» /api-URL, або прямі `assets.daarion.space`, які не відкриваються.
|
||||||
|
|
||||||
|
Ціль — зробити так, щоб:
|
||||||
|
- на проді логотипи/банери стабільно відображалися,
|
||||||
|
- всі UI-компоненти використовували один механізм нормалізації URL,
|
||||||
|
- Asset Proxy коректно працював із MinIO в прод-оточенні.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Завдання
|
||||||
|
|
||||||
|
### 1. Перевірити, що саме зараз працює на проді
|
||||||
|
|
||||||
|
1. Зайти на продовий `daarion.space` (через SSH-tunel / curl).
|
||||||
|
2. Для декількох сторінок (мікроDAO, ноди, банери):
|
||||||
|
- подивитись `img.src` у HTML/DevTools,
|
||||||
|
- зафіксувати: чи це `/api/city/assets/proxy/...` чи `https://assets.daarion.space/...`.
|
||||||
|
3. Якщо хоч один компонент ще використовує сирий URL:
|
||||||
|
- знайти всі місця у фронті, де рендеряться логотипи/банери (microdao, nodes, dashboards, тощо),
|
||||||
|
- переконатися, що всюди використовується **одна** функція `normalizeAssetUrl` (або аналогічний helper),
|
||||||
|
- виправити імпорти/використання (ніяких `src={logoUrl}` напряму).
|
||||||
|
|
||||||
|
### 2. Перевірити конфіг маршруту Asset Proxy у city-service
|
||||||
|
|
||||||
|
1. Знайти в `services/city-service/routes_city.py` (або відповідному файлі) endpoint типу:
|
||||||
|
```python
|
||||||
|
@router.get("/city/assets/proxy/{path:path}")
|
||||||
|
```
|
||||||
|
Важливо: має бути `{path:path}`, щоб підтримувати вкладені шляхи з `/`.
|
||||||
|
|
||||||
|
2. Переконатися, що:
|
||||||
|
* route справді висить під `/city/assets/proxy/{path}` (не `/assets/proxy` без `/city`),
|
||||||
|
* в main-файлі `city-service` router змонтовано як `/api/city` (або відповідно до NGINX/Caddy маршрутизації),
|
||||||
|
* результатом на проді стає **саме той шлях**, який очікує фронт (наприклад `/api/city/assets/proxy/...`).
|
||||||
|
|
||||||
|
3. Перехрестити це з проксі-конфігом NGINX/Caddy:
|
||||||
|
* `/api/city` → `city-service`,
|
||||||
|
* нічого не обрізається/не додається зайве (щоб не вийшло `/api/api/city/...` або `/city/city/...`).
|
||||||
|
|
||||||
|
### 3. Перевірити ENV для доступу до MinIO в Asset Proxy
|
||||||
|
|
||||||
|
1. Знайти, які ENV використовує proxy для MinIO (наприклад):
|
||||||
|
```python
|
||||||
|
MINIO_PUBLIC_ENDPOINT
|
||||||
|
MINIO_BUCKET
|
||||||
|
MINIO_ASSETS_PREFIX # typ. "daarion-assets"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Переконатися, що:
|
||||||
|
* у prod-docker-compose для `city-service` ці ENV присутні й коректні,
|
||||||
|
* в коді proxy шлях будується правильно:
|
||||||
|
```python
|
||||||
|
# псевдокод
|
||||||
|
target_url = f"{MINIO_PUBLIC_ENDPOINT}/{MINIO_BUCKET}/{path}"
|
||||||
|
```
|
||||||
|
якщо `normalizeAssetUrl` відрізає `daarion-assets/`, то тут це потрібно додати вручну.
|
||||||
|
|
||||||
|
3. Написати невеликий unit/integration-тест або просто `curl` у коді:
|
||||||
|
* викликати Asset Proxy з конкретним шляхом, який точно існує в MinIO (`microdao/logo/...`),
|
||||||
|
* переконатися, що код отримує 200 і `Content-Type: image/png`.
|
||||||
|
|
||||||
|
### 4. Вирівняти контракт normalizeAssetUrl ↔ Asset Proxy
|
||||||
|
|
||||||
|
1. В коді `normalizeAssetUrl` явно зафіксувати контракт:
|
||||||
|
* Приклад вхідного URL з БД:
|
||||||
|
```text
|
||||||
|
https://assets.daarion.space/daarion-assets/microdao/logo/123.png
|
||||||
|
```
|
||||||
|
* Вихідний URL для фронта:
|
||||||
|
```text
|
||||||
|
/api/city/assets/proxy/microdao/logo/123.png
|
||||||
|
```
|
||||||
|
|
||||||
|
2. В коді proxy за цим шляхом (`microdao/logo/123.png`) формувати запит до MinIO:
|
||||||
|
```text
|
||||||
|
<MINIO_PUBLIC_ENDPOINT>/daarion-assets/microdao/logo/123.png
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Якщо в БД іноді зберігаються шляхи без `daarion-assets/` або без домену:
|
||||||
|
* додати обробку в normalizeAssetUrl:
|
||||||
|
* якщо шлях починається з `https://assets.daarion.space/` → вирізати домен і префікс бакету,
|
||||||
|
* якщо з `/daarion-assets/` → вирізати `/daarion-assets/`,
|
||||||
|
* якщо просто `microdao/logo/...` → використовувати як є.
|
||||||
|
|
||||||
|
4. Додати короткий коментар у коді normalizeAssetUrl і Asset Proxy, щоб наступні зміни не ламали цей контракт.
|
||||||
|
|
||||||
|
### 5. Перевірка на проді
|
||||||
|
|
||||||
|
1. Після оновлення `city-service` і `web` (перезбір образів і `docker-compose up -d`):
|
||||||
|
* Зайти на основні сторінки з логотипами/банерами:
|
||||||
|
* список MicroDAO,
|
||||||
|
* сторінка MicroDAO,
|
||||||
|
* сторінка нод,
|
||||||
|
* dashboard, де є банери.
|
||||||
|
|
||||||
|
2. У DevTools:
|
||||||
|
* перевірити, що всі картинки ведуть на `/api/city/assets/proxy/...`,
|
||||||
|
* статус усіх запитів 200,
|
||||||
|
* `Content-Type` відповідає типу зображення.
|
||||||
|
|
||||||
|
3. Зафіксувати в короткому звіті:
|
||||||
|
* приклади реальних URL (до/після),
|
||||||
|
* що саме було змінено в коді / docker-конфігах,
|
||||||
|
* інструкцію, як додавати нові компоненти з логотипами, щоб не обійти `normalizeAssetUrl`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
* На проді:
|
||||||
|
* всі логотипи й банери на `daarion.space` завантажуються,
|
||||||
|
* немає жодних прямих `https://assets.daarion.space/...` у HTML,
|
||||||
|
* всі запити до зображень йдуть через `/api/city/assets/proxy/...` і повертають 200.
|
||||||
|
|
||||||
|
* В репозиторії:
|
||||||
|
* є один централізований helper (`normalizeAssetUrl`), який використовують усі компоненти для assets,
|
||||||
|
* Asset Proxy має чіткий контракт з цим helper'ом і коректні ENV для MinIO,
|
||||||
|
* коротка документація (наприклад, у `docs/ASSETS_DNS_SETUP.md` або новому `docs/ASSETS_PROXY.md`) описує поточну схему.
|
||||||
|
|
||||||
|
* Перезапуск сервісів (web + city-service) після змін обов'язково задокументований у звіті Cursor.
|
||||||
|
|
||||||
@@ -328,7 +328,22 @@ async def update_agent_visibility_endpoint(
|
|||||||
async def proxy_asset(path: str):
|
async def proxy_asset(path: str):
|
||||||
"""
|
"""
|
||||||
Proxy endpoint for serving MinIO assets.
|
Proxy endpoint for serving MinIO assets.
|
||||||
Allows serving assets through /api/assets/... instead of requiring assets.daarion.space DNS.
|
|
||||||
|
Allows serving assets through /api/city/assets/proxy/... instead of requiring
|
||||||
|
assets.daarion.space DNS setup.
|
||||||
|
|
||||||
|
CONTRACT with normalizeAssetUrl:
|
||||||
|
- Input from normalizeAssetUrl: /api/city/assets/proxy/microdao/logo/2025/12/02/abc123.png
|
||||||
|
- Path parameter (after /city/assets/proxy/): microdao/logo/2025/12/02/abc123.png
|
||||||
|
- MinIO object key: daarion-assets/microdao/logo/2025/12/02/abc123.png
|
||||||
|
(ASSETS_BUCKET is prepended automatically)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: Object path in MinIO (without bucket prefix)
|
||||||
|
Example: "microdao/logo/2025/12/02/abc123.png"
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
StreamingResponse with image data and appropriate Content-Type
|
||||||
"""
|
"""
|
||||||
from lib.assets_client import get_minio_client, ASSETS_BUCKET
|
from lib.assets_client import get_minio_client, ASSETS_BUCKET
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
@@ -338,6 +353,8 @@ async def proxy_asset(path: str):
|
|||||||
client = get_minio_client()
|
client = get_minio_client()
|
||||||
|
|
||||||
# Get object from MinIO
|
# Get object from MinIO
|
||||||
|
# path already contains the full object key (e.g., "microdao/logo/2025/12/02/abc123.png")
|
||||||
|
# ASSETS_BUCKET is prepended by get_object()
|
||||||
response = client.get_object(ASSETS_BUCKET, path)
|
response = client.get_object(ASSETS_BUCKET, path)
|
||||||
|
|
||||||
# Read data
|
# Read data
|
||||||
|
|||||||
Reference in New Issue
Block a user