Контекст: Карта архитектуры и технического долга проекта PhotoPlaces. Используется для планирования рефакторинга и оценки жизнеспособности.
Статус: Заметка актуализирована 2026-07-04 после рефакторинга модалок.
Проект — fullstack приложение (Next.js + Go) для карты мест фотосъёмок, аренды студий и найма фотографов. Использует PostgreSQL + PostGIS, Redis, MinIO. Развертывание через Docker Compose.
docker-compose.yml (postgres, redis, minio, backend, frontend)deploy/docker-compose.prod.yml + Caddy reverse proxyCI/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.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 |
✅ Все P0 проблемы исправлены (см. таблицу выше).
✅ Все P1 проблемы исправлены:
services/places.go:148-152)json_agg в одном запросе (repository/services.go:50-82)| Проблема | Файл | Статус | Описание |
|---|---|---|---|
| Полная перерисовка маркеров | 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 |
#architecture #backend #frontend #security #technical-debt #moc