# Архитектура PhotoPlaces — Обзор (MOC) **Контекст**: Карта архитектуры и технического долга проекта PhotoPlaces. Используется для планирования рефакторинга и оценки жизнеспособности. **Статус**: Заметка актуализирована 2026-07-04 после рефакторинга модалок. ## Суть Проект — 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) ```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` | | **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:26` — `console.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:74` — `catch (e) { console.warn(...) }` | | **deploy-remote.sh: остатки Yandex** (P2) | ✅ Исправлено | `deploy/deploy-remote.sh` — удалены `prompt_for_yandex_key` и `YANDEX_MAPS_API_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` и `YANDEX_MAPS_API_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 - [[atomic-modal-scrollbar-inside]] — Модалка со скроллбаром внутри контента - [[decision-modal-component-extraction]] — Извлечение общего компонента Modal - [[MOC-frontend-patterns]] — Фронтенд-паттерны ## Теги #architecture #backend #frontend #security #technical-debt #moc