# 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" вместо деталей БД/логики. ### ✅ P1 — Исправлено 9. **Теги и фичи в листинге мест** — `PlaceFilter.IncludeTagsFeatures` + batch-загрузка (`GetTagsBatch`/`GetFeaturesBatch`), запрос `?include=tags,features`. N+1 проблема решена. 10. **Cursor-based пагинация** — `PlaceFilter.CursorRating`, `PaginatedPlaces` с json-тегами, сервис возвращает `*PaginatedPlaces`, репозиторий поддерживает курсор для `created_at` и `rating` сортировок. API: `?cursor=&limit=N`. Ответ: `{"data":[...], "next_cursor":"...", "has_more":true}`. ### ✅ P1 — Исправлено 6. **Rate limiter** — Redis-based имплементация подключена в main.go, in-memory как fallback при недоступности Redis (реализован корректный fallback: при недоступности Redis автоматически используется in-memory limiter; в production `failOpen=false` — возвращается 503 только если оба лимитера недоступны). 7. **`string(rune(line))`** — код в `log.go` уже использует `strconv.Itoa`. Баг был исправлен до аудита. 8. **`strPtr` дублирование** — удалён из `handlers/helpers.go` и `services/auth.go`. Создан единый `pointer` package. 9. **`contains`/`searchString`** — код в `repository/reviews.go` использует `errors.As` напрямую. Проблема не актуальна. ### ✅ Тесты — Добавлены 10. **Backend unit-тесты:** - `services/auth_test.go` — 6 тестов (регистрация, дубликат email, неверный пароль, бан, валидация токена, невалидный токен) - `services/places_test.go` — 3 теста (создание, не-владелец, неизвестный action модерации) 11. **Frontend тесты:** - `vitest` + `@testing-library/react` - `ErrorBoundary.test.tsx` — 2 теста ### ✅ Frontend — Исправлено 12. **ErrorBoundary** — создан компонент, подключён в layout.tsx 13. **`catch {}`** — в admin/tags/page.tsx заменён на обработку с сообщением пользователю 14. **`console.error`** — в MapView.tsx заменён на тихий catch (карта работает с текущими данными) ### ✅ Архитектура — Улучшено 15. **Dependency Inversion** — в сервисы добавлены интерфейсы `PlaceRepo`, `UserRepo`, `RefreshTokenRepo` для тестируемости 16. **CSP** — в Caddyfile добавлен Content-Security-Policy 17. **WebSocket** — добавлен `isProd` флаг для безопасного fallback origins 18. **docker-compose.yml** — все пароли через переменные окружения ### ⏳ P2 — Отложено 19. **MapLibre/OSM** — только Yandex Maps. Задача на V2. 20. **Тесты frontend** — база настроена (vitest), нужно расширять покрытие. 21. **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/*.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-заметок |