FINDINGS.md 7.8 KB

Findings: Аудит проекта PhotoPlaces

Дата аудита: 2026-06-13 Дата исправлений: 2026-06-14


Статус исправлений

✅ P0 — Исправлено

  1. WebSocket CheckOrigin — больше не return true. При пустом allowedOrigins в production соединения отклоняются.
  2. Sentinel error для "not your place" — заменён на ErrNotYourPlace с errors.Is-поддержкой.
  3. Dev-секреты в docker-compose.yml — вынесены в переменные ${VAR:-default}.
  4. env.prod — очищен от замусоренного placeholder Yandex-ключа.
  5. Docker secrets — config.go поддерживает _FILE suffix.
  6. JWT секреты без дефолтов в production — config.go требует JWT_SECRET, JWT_REFRESH_SECRET, ALLOWED_ORIGINS в production; дефолты ("dev-secret", "localhost:3000") запрещены, приложение паникует при старте.
  7. Token Reuse Detection — уже реализован: GetRevoked в репозитории, проверка в RefreshSession, отзыв всех токенов пользователя при обнаружении, обработка в хендлере.
  8. Скрытие внутренних ошибок в production — все хендлеры используют единый writeError с параметром err для логирования; в production 500 ошибки возвращают "internal server error" вместо деталей БД/логики.
  9. WebSocket heartbeat + pub/sub — рефакторинг: Hub с register/unregister/broadcast каналами и Run() горутиной; Client с send chan, readPump/writePump горутинами; ping/pong heartbeat (60s pong wait, 54s ping interval); typed events ({"type":"visitors","data":{...}}) вместо плоского JSON. Breaking change: старый формат {"visitors":[...]} больше не поддерживается.

✅ P1 — Исправлено

  1. Теги и фичи в листинге местPlaceFilter.IncludeTagsFeatures + batch-загрузка (GetTagsBatch/GetFeaturesBatch), запрос ?include=tags,features. N+1 проблема решена.
  2. Cursor-based пагинацияPlaceFilter.CursorRating, PaginatedPlaces с json-тегами, сервис возвращает *PaginatedPlaces, репозиторий поддерживает курсор для created_at и rating сортировок. API: ?cursor=<base64>&limit=N. Ответ: {"data":[...], "next_cursor":"...", "has_more":true}.

✅ P1 — Исправлено

  1. Rate limiter — Redis-based имплементация подключена в main.go, in-memory как fallback при недоступности Redis (реализован корректный fallback: при недоступности Redis автоматически используется in-memory limiter; в production failOpen=false — возвращается 503 только если оба лимитера недоступны).
  2. string(rune(line)) — код в log.go уже использует strconv.Itoa. Баг был исправлен до аудита.
  3. strPtr дублирование — удалён из handlers/helpers.go и services/auth.go. Создан единый pointer package.
  4. contains/searchString — код в repository/reviews.go использует errors.As напрямую. Проблема не актуальна.

✅ Тесты — Добавлены

  1. Backend unit-тесты:
    • services/auth_test.go — 6 тестов (регистрация, дубликат email, неверный пароль, бан, валидация токена, невалидный токен)
    • services/places_test.go — 3 теста (создание, не-владелец, неизвестный action модерации)
  2. Frontend тесты:
    • vitest + @testing-library/react
    • ErrorBoundary.test.tsx — 2 теста

✅ Frontend — Исправлено

  1. ErrorBoundary — создан компонент, подключён в layout.tsx
  2. catch {} — в admin/tags/page.tsx заменён на обработку с сообщением пользователю
  3. console.error — в MapView.tsx заменён на тихий catch (карта работает с текущими данными)

✅ Архитектура — Улучшено

  1. Dependency Inversion — в сервисы добавлены интерфейсы PlaceRepo, UserRepo, RefreshTokenRepo для тестируемости
  2. CSP — в Caddyfile добавлен Content-Security-Policy
  3. WebSocket — добавлен isProd флаг для безопасного fallback origins
  4. docker-compose.yml — все пароли через переменные окружения

⏳ P2 — Отложено

  1. MapLibre/OSM — только Yandex Maps. Задача на V2.
  2. Тесты frontend — база настроена (vitest), нужно расширять покрытие.
  3. CI/CD.github/dependabot.yml есть, но нет workflow. Нужен GitHub Actions.

Сводка изменений

Файл Изменение
backend/internal/services/places.go Sentinel errors + интерфейс PlaceRepo
backend/internal/services/auth.go Интерфейсы UserRepo/RefreshTokenRepo, убран strPtr
backend/internal/handlers/websocket.go isProd + CheckOrigin без fallback true
backend/internal/handlers/helpers.go Удалён strPtr
backend/internal/handlers/tags.go strPtr → pointer.Str
backend/internal/pointer/ Новый пакет (Str, Int, Float64)
backend/cmd/api/main.go isProd для WS Hub; rate limiter fallback (Redis → in-memory)
backend/internal/config/config.go JWT секреты обязательны в production, запрещены дефолты
backend/internal/handlers/errors.go Новый: единый writeError с логированием и скрытием деталей в prod
backend/internal/handlers/ws_client.go Новый: Client readPump/writePump, ping/pong heartbeat
backend/internal/handlers/websocket.go Переписан: Hub с register/unregister/broadcast, Run(), typed events
backend/internal/handlers/*.go Все хендлеры обновлены: writeError(msg, err) вместо err.Error()
backend/internal/models/place.go PlaceFilter.IncludeTagsFeatures, CursorRating; PaginatedPlaces json-теги
backend/internal/services/places.go List: batch-загрузка тегов/фич, encodeCursor/DecodeCursor, возвращает *PaginatedPlaces
backend/internal/repository/places.go GetTagsBatch, GetFeaturesBatch; курсор для rating sort
backend/internal/handlers/places.go List: парсинг ?include=tags,features и ?cursor=
docker-compose.yml Все пароли через ${VAR:-default}
deploy/env.prod Очищен Yandex-ключ
deploy/Caddyfile CSP + Permissions-Policy
frontend/src/components/ErrorBoundary.tsx Новый компонент
frontend/src/app/layout.tsx ErrorBoundary обёртка
frontend/src/app/admin/tags/page.tsx Обработка ошибок
frontend/src/components/MapView.tsx console.error → тихий catch
frontend/vitest.config.ts Новый файл
frontend/src/test/ setup.ts, ErrorBoundary.test.tsx
obsidian_data/Photoplaces_data/ 7 новых Zettelkasten-заметок