architecture-overview.md 10 KB

Архитектура PhotoPlaces — Обзор (MOC)

Контекст: Карта архитектуры и технического долга проекта PhotoPlaces. Используется для планирования рефакторинга и оценки жизнеспособности.

Статус: Заметка актуализирована 2026-07-01 после повторного code review и аудита документации.

Суть

Проект — 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 (12 миграций, 000001–000012)

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)

    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.goatomic.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
Mutex без defer (P0) ✅ Исправлено middleware/ratelimit.go:62 — добавлен defer rl.mu.Unlock()
Эскалация привилегий (P0) ✅ Исправлено handlers/users.go:162-183 — проверка роли только superadmin, защита единственного SA
TOCTOU race регистрации (P0) ✅ Исправлено services/auth.go:99 — INSERT напрямую с обработкой unique constraint (23505)
Retry loop /auth/logout (P1) ✅ Исправлено lib/api.ts:87/auth/logout добавлен в noRefreshPaths
Default status пользователя (P1) ✅ Исправлено repository/users.go:29 — INSERT с status = 'active'
Проверка статуса place при бронировании (P1) ✅ Исправлено repository/bookings.go:43-45 — проверка status = 'published'
RoleMiddleware unsafe assertion (P1) ✅ Исправлено middleware/auth.go:54 — безопасный v, _ := ...(string)
Парсинг bounds без проверки ошибки (P1) ✅ Исправлено handlers/places.go:81-83 — добавлен slog.Warn при ошибке
ErrorBoundary в production (P2) ✅ Исправлено components/ErrorBoundary.tsx:26console.error без условий
node_modules volume в Go backend (P2) ✅ Исправлено docker-compose.yml — удалён backend-modules volume
DeleteTag: парсинг body (P2) ✅ Исправлено handlers/tags.go:80 — ID берётся из query, не из body
next.config.js: no S3 proxy (P2) ✅ Исправлено next.config.js — добавлен remotePatterns для S3
Лишние запросы при пагинации (P1) ✅ Исправлено services/places.go:148-152 — обрезка списка до batch-запросов
N+1 в services/GetByID (P1) ✅ Исправлено repository/services.go:50-82 — LEFT JOIN + json_agg в одном запросе
Полная перерисовка маркеров (P2) ✅ Исправлено components/MapView.tsx — diff-обновление через markerIdsRef
broadcastVisitors двойной lock (P2) ✅ Исправлено handlers/websocket.go:91-106 — один проход по h.clients
logout — подавление ошибок (P2) ✅ Исправлено hooks/useAuth.tsx:74catch (e) { console.warn(...) }
deploy-remote.sh: остатки Yandex (P2) ✅ Исправлено deploy/deploy-remote.sh — удалена prompt_for_yandex_key

Актуальные проблемы (на 2026-07)

P0 — Критические

✅ Все P0 проблемы исправлены (см. таблицу выше).

P1 — Высокие

✅ Все P1 проблемы исправлены:

  • Лишние запросы при пагинации — обрезка списка до вызова batch-запросов (services/places.go:148-152)
  • N+1 в services/GetByID — LEFT JOIN + json_agg в одном запросе (repository/services.go:50-82)

P2 — Умеренные

Проблема Файл Статус Описание
Полная перерисовка маркеров components/MapView.tsx:63-79 ✅ Исправлено Diff-обновление через markerIdsRef — только новые/удалённые
CSP с unsafe-inline deploy/Caddyfile:39 ⏳ Принятый риск Next.js требует unsafe-inline; для strict CSP нужен nonce + серверные изменения
broadcastVisitors: двойной lock handlers/websocket.go:91-127 ✅ Исправлено Один проход по h.clients вместо двух
logout — подавление ошибок hooks/useAuth.tsx:73-74 ✅ Исправлено catch {}catch (e) { console.warn(...) }
fetchPlaces — подавление ошибок components/MapView.tsx:29-31 ✅ Уже исправлено console.warn + setMapError(true) с UI-уведомлением
deploy-remote.sh: остатки Yandex deploy/deploy-remote.sh ✅ Исправлено Удалена функция prompt_for_yandex_key

Архитектурные решения (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]] — Стратегия тестирования
  • [[decision-jwt-refresh-storage]] — Grace period для refresh token при ротации
  • [[decision-patch-tags-features-fix]] — PATCH /places/{id} обновляет теги и фичи

Связанные заметки

  • [[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
  • [[atomic-code-audit-2026-06-30]] — Аудит и исправление 14 багов
  • [[atomic-rate-limit-429-deauth-loop]] — 429 deauth loop fix
  • [[atomic-refresh-token-race-condition]] — Grace period refresh token
  • [[atomic-fetch-retry-infinite-loop]] — Retry loop infinite fix
  • [[atomic-csp-configuration]] — CSP без unsafe-eval
  • [[atomic-ghost-markers]] — Унификация стиля маркеров на карте
  • [[atomic-session-cookie-mechanism]] — Session cookie как признак сессии
  • [[atomic-cookie-secure-via-protocol]] — Secure flag по протоколу, а не APP_ENV
  • [[atomic-extra-batch-pagination]] — Лишние batch-запросы при пагинации
  • [[atomic-n-plus-one-getbyid]] — N+1 в GetByID (places + services)
  • [[atomic-websocket-broadcast-double-lock]] — Двойной проход по clients в broadcastVisitors

Теги

#architecture #backend #frontend #security #technical-debt #moc