Skip to content

feat: gap-12 ghost-lock fix + gap-13 SSO MFA (amr claim, grace period, confirm modal)#151

Merged
NovakPAai merged 21 commits into
mainfrom
claude/jack-gap-13-sso-totp-mfa
May 8, 2026
Merged

feat: gap-12 ghost-lock fix + gap-13 SSO MFA (amr claim, grace period, confirm modal)#151
NovakPAai merged 21 commits into
mainfrom
claude/jack-gap-13-sso-totp-mfa

Conversation

@NovakPAai
Copy link
Copy Markdown
Owner

@NovakPAai NovakPAai commented May 7, 2026

Summary

  • gap-12: фикс ghost-lock в WorkspaceSettingsPage — три состояния загрузки (skeleton / error+retry / read-only) для Workflows, Members, Labels; ошибка API больше не приводит к тихому read-only режиму
  • gap-13: SSO 2FA/TOTP через amr claim — workspaceMfaGuard / boardMfaGuard / taskMfaGuard / checklistMfaGuard на всех защищённых маршрутах; grace-period на включение MFA и добавление новых участников; banner в AppLayout; Security-вкладка в настройках workspace
  • Ревью-правки: confirm modal вместо browser confirm(), дубль кнопки удаления, layout shift nav, emailVerified === true fail-closed, @keyframes вынесены из StrictMode

Key changes

  • backend/src/shared/middleware/workspace-mfa-guard.ts — factory guards для workspace / board / task / checklist / checklistItem
  • backend/src/modules/auth/sso/claims-mapper.tsemailVerified === true (fail-closed)
  • backend/src/modules/auth/auth.service.ts — сохранение amr при refresh token
  • backend/src/modules/workspaces/workspaces.service.tsgetMfaGraceUntil, grace при добавлении участников
  • frontend/src/pages/WorkspaceSettingsPage.tsx — skeleton/error states, confirm modal, Security tab
  • frontend/src/components/AppLayout.tsx — grace-period banner с правильным склонением
  • frontend/src/global.css@keyframes вынесены из инлайн JSX

Test plan

  • Войти как SSO-юзер в workspace с requireMfa: false — доступ открыт
  • Включить MFA в настройках → все участники получают grace period
  • Войти без TOTP в MFA-workspace после истечения grace → 403 MFA_REQUIRED
  • Войти с TOTP → amr: ['totp'] → доступ; banner исчезает
  • Открыть WorkspaceSettings как MEMBER → Workflows в read-only с баннером
  • Симулировать ошибку API → Members/Labels/Workflows показывают error+retry
  • Удалить участника/метку/workflow → кастомный confirm-modal, не browser confirm()
  • Обновить страницу — History/Security вкладки видны сразу (нет layout shift)

🤖 Generated with Claude Code

@NovakPAai NovakPAai changed the title feat: SSO 2FA/TOTP — workspace-level MFA через amr claim (gap-13) feat: gap-12 ghost-lock fix + gap-13 SSO MFA + specs BDD/SDD + backlog May 7, 2026
@NovakPAai NovakPAai changed the title feat: gap-12 ghost-lock fix + gap-13 SSO MFA + specs BDD/SDD + backlog feat: gap-12 ghost-lock fix + gap-13 SSO MFA (amr claim, grace period, confirm modal) May 7, 2026
…ояния загрузки и ошибок для workflows

Немой .catch(() => {}) в Promise.all заставлял members оставаться пустым,
что давало isOwner=false без какого-либо сообщения пользователю.

Теперь renderWorkflows показывает три состояния:
- скелетон при загрузке
- сообщение с кнопкой повтора при ошибке API
- read-only список с пояснением «Редактирование workflow доступно только владельцу воркспейса»
- loadingData теперь инициализируется false (loadWorkspaceData ставит true сам)
  → устраняет бесконечный скелетон при hard-refresh когда wsId ещё не известен
- разделить эффект синхронизации формы и эффект загрузки данных
  → сохранение настроек workspace больше не вызывает лишний reload API
- кнопка «Повторить» теперь вызывает load() если wsId undefined
  → убирает молчаливый no-op на кнопке в граничном случае
Backend:
- schema: Workspace.requireMfa, Workspace.mfaGraceDays, WorkspaceMember.mfaGraceUntil
- prisma migrate dev gap13_mfa_fields
- claims-mapper: извлекает amr[] из OIDC id_token (RFC 8176)
- sso.service: сохраняет amr в Redis-сессию при SSO-входе
- redis: добавлен getUserSession, amr?: string[] в UserSession
- workspace-mfa-guard: middleware проверяет requireMfa + amr + grace period
  - local-пользователи не затрагиваются (authProvider = local)
  - 403 MFA_REQUIRED / MFA_GRACE_EXPIRED при нарушении
  - X-MFA-Grace-Days header во время grace period
- workspaces.service: при включении requireMfa выставляет mfaGraceUntil всем участникам
- workspaces.dto: requireMfa, mfaGraceDays поля
- boards.router + workspaces.router: workspaceMfaGuard('wid') / workspaceMfaGuard()

Frontend:
- Workspace type: requireMfa, mfaGraceDays, mfaGraceUntil
- WorkspaceSettingsPage: вкладка «Безопасность» с toggle requireMfa + grace period
- AppLayout: баннер «Требуется настроить 2FA — осталось N дней» при активном grace period
- api/workspaces: updateWorkspace принимает requireMfa, mfaGraceDays
- boardMfaGuard: новый вариант охраны для маршрутов /boards/:id и /tasks
  → разрешает workspaceId через board.workspaceId, устраняет обход через прямые ID
  → применён на boards.router (/:id) и boardTasksRouter (/boards/:bid/tasks)
- amr sanitize: фильтровать только string-значения ≤32 символов, max 10 элементов
  → защита от инъекции из скомпрометированного IdP в Redis
- updateWorkspace: обернуть workspace.update + updateMany в prisma.$transaction
  → устраняет гонку при одновременных PATCH-запросах на включение MFA
Backend — ИБ и код:
- auth.service: сохранять amr при refresh-токене (читаем prevSession)
- workspace-mfa-guard: fail-closed при Redis=null для SSO+requireMfa (503 SESSION_UNAVAILABLE)
- workspace-mfa-guard: добавлен taskMfaGuard; guard покрывает tasks/:id, comments, labels, workflows
- workspaces.router: asyncHandler вместо двойного authHandler вокруг guard
- boards.router: asyncHandler вместо authHandler для guard-middleware
- workspaces.service: getMfaGraceUntil helper — addMember и inviteByEmail выставляют mfaGraceUntil при requireMfa=true
- members/labels/comments: asyncHandler(guard) на все соответствующие роутеры

Frontend — UX/UI:
- AppLayout: корректный pluralization (день/дня/дней) для 11-14, 21+ дней
- AppLayout: safe-area-inset padding в баннере, flexWrap для узких экранов
- WorkspaceSettingsPage: aria-label на toggle MFA (WCAG accessibility)
- WorkspaceSettingsPage: Space Grotesk на заголовке MFA-карточки
- WorkspaceSettingsPage: GRACE PERIOD → ПЕРИОД ОТСРОЧКИ (единый язык)
- WorkspaceSettingsPage: onBlur-валидация grace days (NaN → 1)
- WorkspaceSettingsPage: read-only баннер — accent border + c.text для WCAG AA контраста
- WorkspaceSettingsPage: сброс members/labels/workflows=[] при старте loadWorkspaceData
HIGH
- mfa-guard: сбрасывать mfaGraceUntil когда пользователь прошёл с amr:totp —
  без этого баннер не гас даже после успешной настройки TOTP
- checklists: добавить taskMfaGuard/checklistMfaGuard/checklistItemMfaGuard —
  роуты /tasks/:tid/checklists, /checklists/:id, /checklist-items/:id не
  были защищены; SSO без TOTP мог писать чеклисты
- workspaces: добавить workspaceMfaGuard на GET /:id/history —
  история воркспейса отдавалась без проверки MFA

MEDIUM
- WorkspaceSettingsPage: loadingData инициализируется true (было false) —
  OWNER кратко видел read-only вьюху до загрузки данных участников

LOW
- AppLayout: заменить IIFE на именованные функции mfaGraceBanner/pluralDays,
  добавить role="alert" aria-live="polite" для доступности
- WorkspaceSettingsPage: Math.round в onChange (было только в onBlur) для
  mfaGraceDays input — дробные значения сохранялись до потери фокуса
- WorkspaceSettingsPage: хинт-текст "При включении" скрывать когда MFA уже
  включена — вводил в заблуждение при изменении grace period
… дубль кнопки, emailVerified

- Убрана дублирующая кнопка «Удалить workspace» из сайдбара (осталась только в Опасной зоне General)
- Все 4 вызова native confirm() заменены на кастомный <Modal> с кнопками Отмена / Удалить
- renderMembers и renderLabels получили skeleton-загрузку и error+retry (как в renderWorkflows)
- emailVerified: `!== false` → `=== true` — fail-closed для account-linking через email
@NovakPAai NovakPAai force-pushed the claude/jack-gap-13-sso-totp-mfa branch from 73bc1ac to 9d8a5a4 Compare May 7, 2026 19:10
…enumeration)

gap-14: добавить rate-limit middleware на /login, /register, /forgot-password
с ключом по email (не IP) — смена X-Forwarded-For больше не обходит лимит.
Fallback для запросов без body — 'no-email' bucket вместо req.ip.

gap-15: унифицировать ответ POST /api/auth/register для всех случаев —
существующий email, pending заявка и новый email теперь возвращают
одинаковый message (200). hashPassword вынесен в Promise.all для
защиты от timing side-channel.

Тесты: обновлены auth.test.ts и ib-authentication.test.ts,
добавлены сценарии gap-14 (IP bypass) и gap-15 (enumeration).
Спеки: specs/gaps/gap-14-*, gap-15-*, BACKLOG.md, ib-authentication.feature.
- Use getByText for Comments tab click (cleaner selector)
- Remove unnecessary waitForLoadState calls after tab click
- Reduce commentInput.waitFor timeout to 15s (was 30s)
- Conflict from stash pop resolved: took stashed version without networkidle waits
… pattern

- Wait for taskCard to be in DOM before dispatchEvent (avoids race after quick-add)
- Wait for 'Детали' tab to confirm drawer opened (matches task-drawer.spec.ts pattern)
- Use getByPlaceholder for comment input (more resilient than textarea[placeholder=...])
- Use getByRole('button', { name: 'Отправить' }) instead of has-text locator
- Remove explicit waitFor: fill() waits internally for element to be actionable
@NovakPAai NovakPAai merged commit 655ad2b into main May 8, 2026
8 checks passed
@NovakPAai NovakPAai deleted the claude/jack-gap-13-sso-totp-mfa branch May 8, 2026 07:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants