Дмитрий
9fa187780b
style+fix(auth): pint formatting + nullsafe.neverNull fix + P1 plan DONE marker
2026-05-22 17:43:18 +03:00
Дмитрий
cf9c082af1
test(auth): full auth-flow integration test for auth_log coverage
2026-05-22 17:43:17 +03:00
Дмитрий
f64c70501d
feat(auth): password reset writes auth_log (requested/completed/failed)
2026-05-22 17:43:15 +03:00
Дмитрий
b7f65865b1
feat(auth): 2FA setup events write auth_log (init/confirm/disable/regen)
2026-05-22 17:43:15 +03:00
Дмитрий
06df563ddf
feat(auth): 2FA verify+recovery write auth_log (success/fail)
2026-05-22 17:43:14 +03:00
Дмитрий
c1e7384437
feat(auth): AuthController uses WritesAuthLog trait + logs logout + register_success
2026-05-22 17:43:14 +03:00
Дмитрий
b40a76e0ff
test(auth): UpdateProfileTest — 422 coverage for empty last_name (review M1)
...
Code-quality review of Task 1: first_name had a 422 test but last_name
(identical required rule) did not. Adds the symmetric test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-15 21:30:13 +03:00
Дмитрий
d8d2f37598
feat(auth): PATCH /api/auth/me profile update endpoint (closes J6)
...
Audit J6: ProfileTab needs a full-profile update endpoint. Adds
AuthController::updateProfile (first_name/last_name/phone/timezone),
routed in the existing /api/auth auth:sanctum group; mirrors the
sibling updateNotificationPreferences. userResource() now also returns
phone + timezone so the GET /me round-trip carries them.
phpstan-baseline.neon updated for Pest PendingCalls false positives
in the new test file (same pattern as all other Feature test files).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-15 20:58:00 +03:00
Дмитрий
f55b91cfa4
phase2(notifications-stage3): NotificationsTab schema-aligned + prefs API
...
Закрывает архитектурное расхождение v1.28 — Tab сохранял prefs только
локально без API. Backend events не совпадали с handoff'ом.
Backend:
- PATCH /api/auth/me/notification-preferences под auth:sanctum.
- Replace-семантика: незадекларированные events/channels отбрасываются.
- userResource расширен: notification_preferences + sound_enabled.
- UserFactory с schema-default JSON (Eloquent не перечитывает после INSERT,
DB-DEFAULT JSONB виден как null без явного override).
- Pest +10: 401 / replace / неизвестные events/channels отбрасываются /
422 без prefs / sound_enabled опционален / bool-cast 1/'1' / replace-
семантика (отсутствующие events исчезают).
Frontend:
- api/auth.ts: типы NotificationChannel/EventKey/Preferences +
updateNotificationPreferences helper. AuthUser получил optional поля.
- NotificationsTab.vue переписан под schema:
8 событий (new_lead/reminder/low_balance/zero_balance/topup_success/
invoice_paid/new_device_login/marketing) × 3 канала (inapp/push/email,
НЕ sms). Sync-init prefs (без onMounted — иначе v-if блокирует рендер
и тесты mount-then-find падают). dirty через computed-сравнение с
originalPrefs snapshot. save async + success/error alerts.
- SettingsView.spec.ts: legacy event-имена → schema-aligned.
- Vitest +10: 8 schema events / 3 channels (НЕ sms) / legacy отсутствуют /
читает prefs из user / save calls API + alerts / Отменить возвращает.
cspell-words: +prefs.
PHPStan baseline регенерирован.
Pest 315/315 (+10) за 36.73 сек, 1130 assertions.
Vitest 349/349 (+10) за 20.42 сек.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-09 11:41:35 +03:00
Дмитрий
2e91469a07
phase2(suspicious-login): email при 3 неудачах login (ТЗ §22.4.4 п.3)
...
- App\Mail\SuspiciousLoginNotification Mailable + emails.suspicious_login Blade
- maybeNotifySuspiciousLogin срабатывает ровно при count==3 (защита от спама на 4-5)
- Для unknown email — skip (некому)
- На dev MAIL_MAILER=log → письмо в storage/logs
ТЗ §22.4.4 анти-брутфорс закрыт полностью:
- email-rate-limit 5/15мин (v1.36)
- IP-lockout 10/час (v1.41)
- email-notify при 3 неудачах (this commit)
- Pest +4 SuspiciousLoginNotificationTest (111/111 за 14.32с, 401 assertions)
- Регресс: Pint+Stan passed
- CLAUDE.md v1.41→v1.42, реестр v1.50→v1.51
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-09 04:10:58 +03:00
Дмитрий
d1e4237e2f
phase2(ip-lockout): auth_log записи + 10 неудач/час с IP → 429 (ТЗ §22.4.4 п.2)
...
- AuthController::isIpLockedOut: count login_failed за час с IP, ≥10 → 429 + Retry-After: 3600
- logAuthEvent: 3 ветки failure_reason (invalid_password / unknown_email / account_locked)
- DB::table('auth_log')->insert; hash-chain trigger заполняет log_hash (OPEN-И-15)
- Защита поверх email-rate-limit: один IP не сможет перебирать множество email'ов
- Pest +6 IpLockoutTest (107/107 за 13.86с, 380 assertions)
- Регресс: Pint+Stan passed
- CLAUDE.md v1.40→v1.41, реестр v1.49→v1.50
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-09 04:07:17 +03:00
Дмитрий
73e64128dc
phase2(2fa-setup): wizard init+confirm+disable+regenerate в SettingsView/SecurityTab
...
- TwoFactorSetupController (auth:sanctum): /api/2fa/{init,confirm,disable,regenerate-recovery-codes}
- init секрет в session (не в БД), QR-URL otpauth://; confirm активирует 2FA + 8 recovery codes
- disable/regenerate требуют password-confirmation
- User.casts: totp_secret => encrypted
Schema v8.7→v8.8: users.totp_secret VARCHAR(255) → TEXT (encrypted ~256 chars)
Migration fix: explicit ALTER TABLE webhook_dedup_keys ADD FK после DB::unprepared (PDO глотал FK на partitioned)
PartitionsCreateMonthsTest fix: DETACH PARTITION + DROP вместо DROP CASCADE
Frontend: SecurityTab реальная логика (setup wizard 3 шага, disable, regenerate dialogs)
- Pest +10 (101/101 за 13.37с, 364 assertions)
- Vitest 166/166
- CLAUDE.md v1.39→v1.40, реестр v1.48→v1.49, schema v8.7→v8.8
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-09 04:03:02 +03:00
Дмитрий
c39d555e6f
phase2(recovery-code): POST /api/auth/2fa/recovery-use + UseRecoveryCodeView
...
- AuthController::useRecoveryCode перебирает unused codes через Hash::check, нормализация (lowercase + remove dash/space)
- UserRecoveryCode Eloquent (UPDATED_AT=null — schema без updated_at)
- Rate-limit auth:recovery:{pending_user_id}|{ip} (5/15мин)
- Returns recovery_codes_remaining для UI-warning'а (sessionStorage на frontend)
- UseRecoveryCodeView.vue → POST /api/auth/2fa/recovery-use, /recovery-use route, autocomplete=one-time-code
- TwoFactorView "резервный код" ссылка /recovery → /recovery-use
- Pest +6 RecoveryCodeTest (91/91 за 12.77с, 319 assertions)
- Vitest +6 (166/166 за 11.47с)
- TODO: #3 2FA setup wizard (после этого /recovery view получит реальный source данных)
- Регресс: lint+type+format OK; build 849ms; story:build 21/28 за 30.36с; Pint+Stan passed
- CLAUDE.md v1.38→v1.39, реестр v1.47→v1.48
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-09 03:43:58 +03:00
Дмитрий
9c488122a1
phase2(reset-password): POST /api/auth/reset-password + ResetPasswordView + DB timezone fix
...
- AuthController::resetPassword через Password::reset() (callback пишет password_hash)
- ResetPasswordRequest: token + email + password (min 10 по ТЗ §22.4.1) + confirmed
- Rate-limit auth:reset:{sha256(token)[0..16]}|{ip} (5/15мин)
- ResetPasswordView для deep-link /reset/:token?email=...; pre-fill email из query; success → redirect /login через 3 сек
- Vue Router /reset/:token (guestOnly); web.php /reset SPA-path
- DB FIX: config/database.php pgsql.timezone=UTC — без него PG TIMESTAMPTZ +03 терялся при Carbon::parse и tokenExpired ошибочно срабатывал
- Pest +6 ResetPasswordTest (85/85 за 11.50с, 291 assertions)
- Vitest +7 (160/160 за 11.02с)
- Регресс: lint+type+format OK; build 784ms; story:build 21/28 за 30.74с; Pint+Stan passed
- CLAUDE.md v1.37→v1.38, реестр v1.46→v1.47
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-09 03:36:27 +03:00
Дмитрий
170382878b
phase2(forgot-password): POST /api/auth/forgot + ForgotPasswordView интеграция
...
- AuthController::forgotPassword использует Password::sendResetLink (anti-enumeration: всегда 200)
- AUTH_PASSWORD_RESET_TOKEN_TABLE=password_resets — указывает на нашу таблицу из schema v8.7
- Rate-limit 5/15мин по auth:forgot:{email}|{ip} — hit ставится ДО sendResetLink (защита перебора через unknown email)
- Frontend: authApi.forgotPassword, auth-store.requestPasswordReset, ForgotPasswordView success-state
- Pest +6 в ForgotPasswordTest (79/79 за 10.55с, 273 assertions)
- Vitest +4 (153/153 за 11.11с)
- TODO: POST /api/auth/reset-password + UI-форма new_password (deep-link)
- Регресс: lint+type+format OK; build 862ms; story:build 21/28 за 32с; Pint+Stan passed
- CLAUDE.md v1.36→v1.37, реестр v1.45→v1.46
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-08 21:10:28 +03:00
Дмитрий
75897b1636
phase2(rate-limit): login + 2FA verify (5/15min) + frontend lockout
...
- AuthController: RateLimiter::hit/clear на login + verifyTwoFactor по ключу email|ip / pending_user_id|ip
- 429 + Retry-After header + JSON retry_after (lockoutResponse helper)
- ТЗ §22.4.4: 5 попыток / 15 мин; success чистит throttle; inactive user тоже расходует попытки
- extractRateLimitRetry в api/client.ts; auth-store.lockoutSeconds; v-alert в LoginView/TwoFactorView
- Pest +6 в RateLimitTest.php (73/73 за 8.07с, 246 assertions)
- Vitest +4 в auth-store + LoginView (149/149 за 12.31с)
- Quirk: wrong-password в тестах ≥8 символов (LoginRequest::min:8) — иначе валидация падает до controller
- Quirk: vi.mock api/client в auth-store.spec — иначе axios.isAxiosError в jsdom возвращает false для plain Error
- TODO (отдельные коммиты): IP-lockout 10/час через auth_log + email при 3 неудачах
- Регресс: lint+type+format OK; build 886ms; story:build 21/28 за 37.19с; Pint+Stan passed
- CLAUDE.md v1.35→v1.36, реестр v1.44→v1.45
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-08 20:49:47 +03:00
Дмитрий
374724a7a3
phase2(auth-2fa): TOTP-verify endpoint + TwoFactorView интеграция
...
- pragmarx/google2fa@^9.0 для TOTP RFC 6238.
- AuthController::login изменён: при totp_enabled=true НЕ делает Auth::login,
сохраняет auth.pending_user_id+pending_remember в session, возвращает
requires_2fa=true. /me=401 пока 2FA не пройдена.
- AuthController::verifyTwoFactor: читает pending_user_id, верифицирует TOTP
через Google2FA::verifyKey($secret, $code, window: 1) (окно ±1 = 30s).
Success → Auth::login + regenerate + clear pending + last_login_at.
- VerifyTwoFactorRequest: regex /^\d{6}$/.
- /api/auth/2fa/verify публичный (нет session-auth до verify).
Frontend:
- auth-store::login: при requires_2fa=true user остаётся null (иначе
isAuthenticated=true и guard пустит на /dashboard минуя 2FA).
- auth-store::verifyTwoFactor action.
- api/auth.ts::verifyTwoFactor(code).
- TwoFactorView: onMounted redirect на /login если нет pending state;
submit → verify → /dashboard; на error - clear code + focus first cell.
userEmail из auth.user?.email.
Pest +6 (всего 67/67 за 6.97s, 194 assertions): login для 2FA НЕ создаёт
session + verify success/неверный код/без login/валидация формата +
после verify /me=200.
Vitest +3 (всего 142/142 за 10.75s): login pending vs success state +
verifyTwoFactor success/reject. TwoFactorView spec получил setActivePinia
+ requires2fa=true для bypass onMounted-redirect.
PHPStan baseline +26 Pest TestCall warnings (накопительно).
Регресс: pint+stan passed; vitest 142/142; vite build 908ms;
story:build 21/28 за 31.28s; Pest 67/67 за 6.97s.
CLAUDE.md v1.33->v1.34, реестр Открытых_вопросов v1.42->v1.43.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-08 20:14:33 +03:00
Дмитрий
04b90afda4
phase2(auth-backend): Sanctum SPA mode + AuthController + 13 Pest tests
...
- laravel/sanctum@^4.3 install. SPA mode (cookie-based session, не tokens).
personal_access_tokens migration удалена (для SPA не нужна).
- AuthController (Api/): login + register + me + logout с детальной валидацией
+ кастомные русские error-messages.
- LoginRequest + RegisterRequest Form Requests. Register требует
accept_offer:accepted + accept_pdn:accepted (по ТЗ §1.5/§4.1, БЕЗ
маркетингового click-wrap'а - расхождение #2 handoff vs ТЗ).
- User::fillable += last_login_at, last_active_at.
- Auth-routes в web.php (НЕ api.php): Sanctum SPA нуждается в session-cookie
middleware из web-группы (laravel.com/docs/sanctum#spa-authentication).
- cspell-words.txt: pdn, залогинен.
Pest +13 (всего 61/61 за 6.22s):
- login success + 2FA-flag + invalid pass + missing email + blocked + format
validation + last_login_at update + register success/duplicate/без accept +
me 401/200 + logout 200.
- Logout-test упрощён до 200+message - Pest cookie-jar держит session между
запросами теста, full flow через browser-mode (отдельный коммит).
- phpstan-baseline: +25 ignored Pest TestCall warnings (Larastan+Pest quirk).
Регресс: pint+stan passed; vitest 129/129 за 9.59s; vite build 802ms;
story:build 21/28 за 30.39s; Pest 61/61 за 6.22s.
CLAUDE.md v1.31->v1.32, реестр Открытых_вопросов v1.40->v1.41.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-08 19:41:35 +03:00