Дата аудита: 2026-06-13 Дата исправлений: 2026-06-14
return true. При пустом allowedOrigins в production соединения отклоняются.ErrNotYourPlace с errors.Is-поддержкой.${VAR:-default}._FILE suffix.GetRevoked в репозитории, проверка в RefreshSession, отзыв всех токенов пользователя при обнаружении, обработка в хендлере.writeError с параметром err для логирования; в production 500 ошибки возвращают "internal server error" вместо деталей БД/логики.register/unregister/broadcast, Run() горутина; Client с send chan, readPump/writePump горутинами; ping/pong heartbeat (60s pong wait, 54s ping interval); typed events ({"type":"visitors","data":{...}}).useWebSocket с auto-reconnect (exponential backoff 1→30s); MapView заменяет polling 60s на WS для отображения точек других посетителей.{"visitors":[...]} не поддерживается.SELECT ... FOR UPDATE на places внутри транзакции в BookingRepo.Create(); цена рассчитывается атомарно (подзапрос внутри INSERT); убран placeRepo из BookingHandler; удалены мёртвые методы CancelOverlapping и CalculateTotalPrice.PlaceFilter.IncludeTagsFeatures + batch-загрузка (GetTagsBatch/GetFeaturesBatch), запрос ?include=tags,features. N+1 проблема решена.PlaceFilter.CursorRating, PaginatedPlaces с json-тегами, сервис возвращает *PaginatedPlaces, репозиторий поддерживает курсор для created_at и rating сортировок. API: ?cursor=<base64>&limit=N. Ответ: {"data":[...], "next_cursor":"...", "has_more":true}.TestMain с подключением по DATABASE_URL_TEST, фикстуры (user, place, tag), автономные тесты с cleanTables. Booking: цена, overlap, place not found, same slot different place. Place: status filter, cursor pagination, batch tags.failOpen=false — возвращается 503 только если оба лимитера недоступны).string(rune(line)) — код в log.go уже использует strconv.Itoa. Баг был исправлен до аудита.strPtr дублирование — удалён из handlers/helpers.go и services/auth.go. Создан единый pointer package.contains/searchString — код в repository/reviews.go использует errors.As напрямую. Проблема не актуальна.services/auth_test.go — 6 тестов (регистрация, дубликат email, неверный пароль, бан, валидация токена, невалидный токен)services/places_test.go — 3 теста (создание, не-владелец, неизвестный action модерации)vitest + @testing-library/reactErrorBoundary.test.tsx — 2 тестаcatch {} — в admin/tags/page.tsx заменён на обработку с сообщением пользователюconsole.error — в MapView.tsx заменён на тихий catch (карта работает с текущими данными)PlaceRepo, UserRepo, RefreshTokenRepo для тестируемостиisProd флаг для безопасного fallback origins.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= |
backend/internal/repository/bookings.go |
Create: транзакция + SELECT ... FOR UPDATE + атомарный расчёт цены; удалены мёртвые методы |
backend/internal/handlers/bookings.go |
Убран placeRepo; ErrPlaceNotFound в хендлере; цена в репозитории |
backend/internal/repository/integration_test.go |
Новый: TestMain, тестовые фикстуры (insertTestUser, insertTestPlace, insertTestTag, cleanTables) |
backend/internal/repository/booking_repo_test.go |
Новый: booking create (price, overlap, place not found, same slot different place) |
backend/internal/repository/place_repo_test.go |
Новый: list with filters, cursor pagination, GetTagsBatch |
docker-compose.yml |
Все пароли через ${VAR:-default} |
deploy/env.prod |
Очищены placeholder-ключи |
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; polling → WebSocket visitor dots |
frontend/src/hooks/useWebSocket.ts |
Новый: хук с auto-reconnect, отправкой позиции, приёмом списка посетителей |
frontend/vitest.config.ts |
Новый файл |
frontend/src/test/ |
setup.ts, ErrorBoundary.test.tsx |
obsidian_data/Photoplaces_data/ |
7 новых Zettelkasten-заметок |
| # | Проблема | Решение |
|---|---|---|
| 1 | Тесты не компилируются — mockPlaceRepo.SetTags принимал []string, интерфейс ожидает []Tag; mockRefreshTokenRepo без GetRevoked |
Исправлены сигнатуры, добавлены недостающие методы |
| 2 | writeValidationError утекал детали — err.Error() всегда возвращался в production |
В production: details: "validation failed" |
| 3 | SameSite=LaxMode — refresh token уязвим к CSRF | В production: SameSite=StrictMode |
| 4 | WebSocket без auth и rate limit — анонимные visitor_update без ограничений |
Добавлен per-connection rate limiter (10/s burst 1), глобальный лимит 1000 соединений; логин из JWT |
| 5 | Race condition refresh — конкурентные refresh сбрасывают все сессии | DeleteIfExists с проверкой RowsAffected; ErrTokenReused без RevokeAll |
| 6 | In-memory rate limiter использует RemoteAddr — за Caddy все IP одинаковы | Переключён на GetClientIP() (читает X-Forwarded-For) |
| # | Проблема | Решение |
|---|---|---|
| 7 | appEnv не используется в 6 хендлерах | Параметр удалён из конструкторов |
| 8 | Расчёт цены брони — int() урезал дробные часы |
math.Ceil() |
| 9 | Нет проверки min_hours в бронированиях | GetPlaceMinHours, валидация в Create |
| 10 | Race condition cancel брони | UpdateStatusIfPending — WHERE status='pending' |
| 11 | Refresh token cleanup не вызывался | Background goroutine каждые 6 часов |
| 12 | Frontend Place тип не совпадает с backend | coordinates → lat/lng; ApiError → {"error":"..."} |
| # | Проблема | Решение |
|---|---|---|
| 13 | Нет лимита тела запроса | http.MaxBytesReader(w, r.Body, 1MB) во всех хендлерах |
| 14 | Cleanup горутина RateLimiter не останавливается | Добавлен stopCh канал + Stop() метод |
| 15 | DeleteTag использует body + query param | Упрощено, но сохранено для обратной совместимости |
| # | Проблема | Решение | Файл |
|---|---|---|---|
| 1 | Бесконечный цикл при upload на 401 — requestFormData повторял запрос после refresh без guard'а; при повторном 401 уходил в плотный цикл к /auth/refresh |
Добавлен флаг isRetry (как в request) — однократный повтор |
frontend/src/lib/api.ts |
| 2 | PATCH /places/{id} отдавал 500 вместо 403 — sentinel ErrNotYourPlace не мапился в хендлере |
errors.Is(err, services.ErrNotYourPlace) → 403 |
handlers/places.go |
| 3 | Confirm брони без проверки статуса — можно подтвердить отменённую бронь / 200 на несуществующий id |
Переход через UpdateStatusIfPending; 409 если не pending. Удалён мёртвый UpdateStatus |
handlers/bookings.go, repository/bookings.go |
| 4 | Upload доверял Content-Type клиента — можно залить произвольный файл под видом изображения | Проверка magic-bytes (http.DetectContentType + ручной ISO-BMFF для heic), seek в начало |
handlers/upload.go |
| 5 | Мёртвый код — IsTimeSlotAvailable (не вызывается, tsrange vs tstzrange) |
Удалён; защита от двойных броней работает через DB exclusion-constraint | repository/bookings.go |
| 6 | ALLOWED_ORIGINS без trim — origin с пробелом не совпадал в CORS |
strings.TrimSpace + отброс пустых |
config/config.go |
| 7 | geocode светил upstream-тело в raw_error |
Тело отбрасывается, наружу только display_name: null |
handlers/geocode.go |