Commit graph

3 commits

Author SHA1 Message Date
nns 03bdf4d4f1 rebrand: письма и серверные строки ALL MARKET
- EmailTemplates (invite/low-stock/weekly-summary): шапка с текстовым
  вордмарком «all market» (бренд-цвета, всегда рендерится — без внешних
  картинок и привязки к домену) + подпись «ALL MARKET — система учёта торговли».
- Имя отправителя по умолчанию, темы/тела системных писем, экспорт-README,
  дневная сводка, тест SMTP → ALL MARKET.
- Цвета писем и домены не менялись.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 17:34:38 +05:00
nns 8e54e2e0d6 feat(s13): security headers + rate-limits + sensitive-ops audit + session revoke + Grafana
Sprint 13 — security + observability deep. 7 пунктов чек-листа ✓.

Подробности — docs/sprint13-progress.md и docs/food-market-server-postgres-role.md.

Главное:
- food-market-server (back.food-market.kz, legacy backend) теперь
  работает на dedicated PG-роли food_market_server_app (NOSUPERUSER /
  NOCREATEDB / NOCREATEROLE / NOREPLICATION / NOBYPASSRLS) с CRUD-only
  грантами. Раньше использовался postgres-superuser с паролем 1q2w3e4r.
  Бэкап конфига сохранён, rollback одной командой.
- SecurityHeadersMiddleware навешивает CSP / X-Frame-Options DENY /
  X-Content-Type-Options nosniff / Referrer-Policy strict-origin /
  Permissions-Policy. HSTS 365d + includeSubDomains + preload.
  Те же заголовки в deploy/nginx.conf для SPA HTML.
- Rate-limit:
  • Signup-IP — 3/час + 10/день (на stage'е переопределено через
    .env RATE_SIGNUP_HOUR=30 чтобы не ломать e2e).
  • Forgot-password — per-email 3/час + per-IP 10/час.
- SensitiveOpsAudit сервис, wired в:
  • TwoFactor enroll/disable
  • Employees.Update при смене RoleId (action=AssignRole,
    payload с prev/next role + полный RolePermissions)
  • MeAccount.ChangePassword (новый endpoint)
  • MeSessions.RevokeAll (новый endpoint)
- POST /api/me/sessions/revoke-all — через
  IOpenIddictAuthorizationManager.FindBySubjectAsync + TryRevokeAsync.
  Integration-тест: refresh после revoke → 400/401.
- Hangfire dashboard — nginx-route добавлен (раньше /hangfire ловился
  SPA-fallback'ом). Фильтр SuperAdmin'ом уже был. Тест: anon/tenant →
  401/403/404.
- Grafana dashboard JSON (deploy/grafana/dashboards/food-market.json,
  9 панелей) + инструкции импорта в docs/observability.md.

Проверено на stage'е: все 6 security-заголовков видны на /;
/hangfire → 401 (закрыт); 4-я форгот → 429; stage-smoke (5 этапов) ✓.

Тесты: 68 unit + 9 integration (включая 3 новых: SessionRevokeTests,
HangfireAccessTests).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 12:30:10 +05:00
nns e38a360e54 feat(auth): forgot/reset password — endpoints + UI + IP rate-limit
Some checks failed
CI / Backend (.NET 8) (push) Successful in 1m6s
CI / Web (React + Vite) (push) Successful in 39s
Docker API / Build + push API (push) Successful in 1m17s
Docker Web / Build + push Web (push) Successful in 36s
Docker API / Deploy API on stage (push) Failing after 37s
Docker Web / Deploy Web on stage (push) Successful in 12s
CI / POS (WPF, Windows) (push) Has been cancelled
Пункты 5 + 6 пакета SMTP-настроек.

API (AuthForgotPasswordController, anonymous):
- POST /api/auth/forgot-password { email }
  · IP rate-limit: 3 попытки в час (in-memory ConcurrentDictionary
    с per-IP списком timestamps; для одного API-инстанса хватает,
    при scale-out — Redis).
  · ВСЕГДА возвращает 200 (анти-юзер-энумерация). Реально шлёт письмо
    только если юзер найден И активен И имеет email; иначе тихо
    логирует и отдаёт 200.
  · Использует UserManager.GeneratePasswordResetTokenAsync (Identity
    AddDefaultTokenProviders уже подключён в Program.cs).
  · Письмо: ссылка вида https://admin.food-market.kz/reset-password
    ?email=...&token=... (1 час валидна).
  · Если SMTP не настроен — ловит EmailNotConfiguredException,
    логирует и всё равно отдаёт 200 (UX-friendly).
- POST /api/auth/reset-password { email, token, newPassword }
  · UserManager.ResetPasswordAsync. На InvalidToken — понятный
    «Ссылка недействительна или истекла».
  · После успеха revoke всех valid-OpenIddict tokens юзера
    (UPDATE OpenIddictTokens SET Status='revoked' WHERE Subject=...).

UI:
- /forgot-password — anonymous, форма с email; submit → 200 «проверьте
  почту» (одинаковый текст независимо от существования email).
- /reset-password — anonymous, читает email/token из query-string;
  поля «новый пароль» + «повторите»; после успеха — auto-redirect
  через 2.5 секунды на /login.
- LoginPage: добавлена ссылка «Забыли пароль?» под кнопкой «Войти».

Smoke-флоу:
1. SuperAdmin → /super-admin/platform-settings → SMTP creds + test-send.
2. Юзер → /login → «Забыли пароль?» → /forgot-password → email.
3. Письмо с ссылкой → /reset-password?email&token → новый пароль.
4. Login со старым паролем — отказ (revoked refresh + новый pwd).
5. Login с новым паролем → норма.
2026-05-06 12:45:38 +05:00