feat: gap-12 ghost-lock fix + gap-13 SSO MFA (amr claim, grace period, confirm modal)#151
Merged
Merged
Conversation
NovakPAai
pushed a commit
that referenced
this pull request
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
…енный открытый gap-13
- 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
…s (fix TS2307 в CI)
73bc1ac to
9d8a5a4
Compare
…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.
…сты gap-14 должны работать
- 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
amrclaim —workspaceMfaGuard/boardMfaGuard/taskMfaGuard/checklistMfaGuardна всех защищённых маршрутах; grace-period на включение MFA и добавление новых участников; banner в AppLayout; Security-вкладка в настройках workspaceconfirm(), дубль кнопки удаления, layout shift nav,emailVerified === truefail-closed,@keyframesвынесены из StrictModeKey changes
backend/src/shared/middleware/workspace-mfa-guard.ts— factory guards для workspace / board / task / checklist / checklistItembackend/src/modules/auth/sso/claims-mapper.ts—emailVerified === true(fail-closed)backend/src/modules/auth/auth.service.ts— сохранениеamrпри refresh tokenbackend/src/modules/workspaces/workspaces.service.ts—getMfaGraceUntil, grace при добавлении участниковfrontend/src/pages/WorkspaceSettingsPage.tsx— skeleton/error states, confirm modal, Security tabfrontend/src/components/AppLayout.tsx— grace-period banner с правильным склонениемfrontend/src/global.css—@keyframesвынесены из инлайн JSXTest plan
requireMfa: false— доступ открытamr: ['totp']→ доступ; banner исчезает🤖 Generated with Claude Code