|
|
@@ -2,7 +2,7 @@
|
|
|
|
|
|
**Контекст**: Карта архитектуры и технического долга проекта PhotoPlaces. Используется для планирования рефакторинга и оценки жизнеспособности.
|
|
|
|
|
|
-**Статус**: Заметка актуализирована 2026-06-24 после code review. Предыдущая версия содержала устаревшие P0, которые уже исправлены в коде.
|
|
|
+**Статус**: Заметка актуализирована 2026-07-01 после повторного code review и аудита документации.
|
|
|
|
|
|
## Суть
|
|
|
|
|
|
@@ -17,7 +17,7 @@
|
|
|
- **Rate Limiting**: Redis (sliding window) + in-memory fallback
|
|
|
- **Validation**: go-playground/validator + кастомные правила
|
|
|
- **Architecture**: Layered (handlers → services → repository)
|
|
|
-- **Migrations**: golang-migrate (7 миграций)
|
|
|
+- **Migrations**: golang-migrate (12 миграций, 000001–000012)
|
|
|
|
|
|
### Frontend (Next.js 14 App Router)
|
|
|
- **State**: React Context (AuthProvider)
|
|
|
@@ -56,42 +56,47 @@ graph TB
|
|
|
| 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)
|
|
|
+| **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` |
|
|
|
+
|
|
|
+## Актуальные проблемы (на 2026-07)
|
|
|
|
|
|
### 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 |
|
|
|
+✅ Все P0 проблемы исправлены (см. таблицу выше).
|
|
|
|
|
|
### 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)` запаникует при не-строке |
|
|
|
+✅ Все P1 проблемы исправлены:
|
|
|
+- Лишние запросы при пагинации — обрезка списка до вызова batch-запросов (`services/places.go:148-152`)
|
|
|
+- N+1 в services/GetByID — LEFT JOIN + `json_agg` в одном запросе (`repository/services.go:50-82`)
|
|
|
|
|
|
### 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/` не оптимизируются |
|
|
|
+| Проблема | Файл | Статус | Описание |
|
|
|
+|---|---|---|---|
|
|
|
+| Полная перерисовка маркеров | `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)
|
|
|
|
|
|
@@ -100,6 +105,8 @@ graph TB
|
|
|
- [[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} обновляет теги и фичи
|
|
|
|
|
|
## Связанные заметки
|
|
|
|
|
|
@@ -115,6 +122,17 @@ graph TB
|
|
|
- [[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
|
|
|
|
|
|
## Теги
|
|
|
|