architecture-overview.md 7.9 KB

Архитектура 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)

    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

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

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

Проблема Файл Описание
TOCTOU race при регистрации services/auth.go:85-111 GetByEmailCreate без транзакции
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