# Архитектура PhotoPlaces — Обзор (MOC) **Контекст**: Карта архитектуры и технического долга проекта PhotoPlaces. Используется для планирования рефакторинга и оценки жизнеспособности. **Статус**: Заметка актуализирована 2026-06-24 после code review. Предыдущая версия содержала устаревшие P0, которые уже исправлены в коде. ## Суть Проект — fullstack приложение (Next.js + Go) для карты мест фотосъёмок, аренды студий и найма фотографов. Использует PostgreSQL + PostGIS, Redis, MinIO. Развертывание через Docker Compose. ## Основные компоненты ### Backend (Go 1.22) - **Router**: Chi v5 - **DB**: pgx/v5 (PostgreSQL + PostGIS) - **Auth**: JWT (access 15min + refresh 30d, SHA-256 hash в БД, ротация) - **Rate Limiting**: Redis (sliding window) + in-memory fallback - **Validation**: go-playground/validator + кастомные правила - **Architecture**: Layered (handlers → services → repository) - **Migrations**: golang-migrate (7 миграций) ### Frontend (Next.js 14 App Router) - **State**: React Context (AuthProvider) - **API**: Custom fetch wrapper с auto-refresh токена - **Maps**: Leaflet (OSM dark tiles via CartoDB) - **Styling**: Tailwind CSS - **Testing**: Vitest + @testing-library/react ### Infrastructure - **Local**: `docker-compose.yml` (postgres, redis, minio, backend, frontend) - **Prod**: `deploy/docker-compose.prod.yml` + Caddy reverse proxy - **CI/CD**: GitHub Actions (dependabot.yml) ```mermaid graph TB FB[Frontend Next.js 14] --> API[Go API :8080] API --> PG[PostgreSQL + PostGIS] API --> RD[Redis] API --> S3[MinIO / S3] FB --> FBStatic[Next.js Static] CADDY[Caddy] --> FB CADDY --> API CADDY --> S3 ``` ## Статус исправления проблем | Проблема | Статус | Где | |---|---|---| | Refresh tokens не хранятся в БД | ✅ Исправлено | `services/auth.go` — SHA-256, ротация, `DeleteIfExists` | | Rate limiter только in-memory | ✅ Исправлено | `main.go` — Redis + fallback, fail-open/closed | | Нет валидации входных данных | ✅ Исправлено | `validator/validator.go` — go-playground/validator | | Нет тестов | ✅ Частично | `auth_test.go`, `places_test.go`, `place_repo_test.go` | | Package-level mutable state | ✅ Исправлено | `handlers/errors.go` — `atomic.Bool` вместо `var AppEnv string` | | Координаты [lng,lat]/[lat,lng] | ✅ Исправлено | `lib/map.ts` — консистентно (lat, lng) везде | | N+1 в GetByID | ✅ Исправлено | `repository/places.go:63-109` — один LEFT JOIN + `json_agg` | | Подавление ошибок PlaceForm | ✅ Исправлено | `components/PlaceForm.tsx:50` — ошибки через `setError` | | Мало линтеров | ✅ Исправлено | `.golangci.yml` — добавлены `gosec`, `bodyclose`, `gocritic`, `revive` | ## Актуальные проблемы (на 2026-06) ### P0 — Критические | Проблема | Файл | Описание | |---|---|---| | **TOCTOU race при регистрации** | `services/auth.go:85-111` | `GetByEmail` → `Create` без транзакции | | **Mutex без defer** | `middleware/ratelimit.go:61` | `rl.mu.Lock()` без `defer Unlock()` — deadlock при панике | | **Эскалация привилегий** | `handlers/users.go:166-186` | Модератор может повысить любого до superadmin | ### P1 — Высокие | Проблема | Файл | Описание | |---|---|---| | Retry loop на /auth/logout | `lib/api.ts:71` | `/auth/logout` не в `noRefreshPaths` — logout с истёкшим токеном циклит refresh | | Лишние запросы при пагинации | `services/places.go:148-172` | `GetTagsBatch`/`GetFeaturesBatch` вызываются до обрезки `limit+1` | | Хрупкий парсинг bounds | `handlers/places.go:76-80` | `fmt.Sscanf` — silent fail при ошибке | | Отсутствует статус по умолчанию у user | `repository/users.go:22-29` | INSERT без `status` — новый пользователь имеет пустой статус | | Нет проверки статуса place при бронировании | `repository/bookings.go:25-63` | Можно забронировать место в `draft`/`pending_moderation` | | RoleMiddleware: unsafe type assertion | `middleware/auth.go:54` | `ctx.Value(UserRoleKey).(string)` запаникует при не-строке | ### P2 — Умеренные | Проблема | Файл | Описание | |---|---|---| | ErrorBoundary молчит в production | `components/ErrorBoundary.tsx:26-29` | `if (NODE_ENV === 'production') return` — ошибки не логируются | | Полная перерисовка маркеров | `components/MapView.tsx:98-115` | Все маркеры удаляются и создаются заново при каждом изменении `places` | | node_modules volume в Go backend | `docker-compose.yml:88` | `backend-modules:/app/node_modules` — не имеет смысла для Go | | CSP с `unsafe-inline` | `deploy/Caddyfile:39` | `script-src` и `style-src` с `unsafe-inline` — ослабляет XSS-защиту | | N+1 в services/GetByID | `handlers/services.go:80-85` | Отдельный запрос GetTags после GetByID | | DeleteTag: парсинг body | `handlers/tags.go:79-84` | DELETE читает JSON body, затем fallback на query | | broadcastVisitors: двойной lock | `handlers/websocket.go:91-127` | Состояние может измениться между сбором и рассылкой | | logout на фронте — подавление ошибок | `hooks/useAuth.tsx:65` | `catch {}` скрывает ошибки сети | | fetchPlaces — подавление ошибок | `components/MapView.tsx:37-39` | Ошибка загрузки мест не отображается пользователю | | next.config.js: нет S3-прокси для image optimization | `next.config.js:5-8` | Изображения через `api.{DOMAIN}/s3/` не оптимизируются | ## Архитектурные решения (Decision Records) - [[decision-jwt-refresh-storage]] — Почему refresh tokens должны храниться в БД - [[decision-rate-limiter-redis]] — Redis-based rate limiting с fallback - [[decision-validation-library]] — go-playground/validator - [[decision-validation-error-production]] — Скрытие деталей ошибок в production - [[decision-test-strategy]] — Стратегия тестирования ## Связанные заметки - [[backend-auth-security]] — JWT + refresh token реализация - [[backend-rate-limiting]] — Rate limiting (Redis + in-memory) - [[backend-validation]] — Валидация - [[frontend-api-client]] — API client на фронте - [[deploy-production-readiness]] — Production readiness - [[database-migrations]] — Схема миграций - [[atomic-global-appenv-package-var]] — Проблема глобальной переменной - [[atomic-toctou-race-registration]] — TOCTOU race - [[atomic-map-coordinates-inconsistency]] — Координаты карты - [[atomic-error-swallowing-frontend]] — Подавление ошибок - [[atomic-redis-rate-limiter-failopen-failclosed]] — Fail-open/closed - [[atomic-csp-hardening]] — CSP hardening ## Теги #architecture #backend #frontend #security #technical-debt #moc