For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Добавить двухфакторную аутентификацию для входа в админку с выбором способа — код по email или TOTP (Google Authenticator). Пользователь без 2FA логинится как сейчас; с включённым 2FA после ввода пароля перенаправляется на ввод кода, затем создаётся сессия.
Architecture: Двухшаговый логин: API login-step1 проверяет пароль и при 2FA создаёт запись TwoFactorPending и cookie; страница /login/2fa отправляет код в verify-2fa; при успехе создаётся одноразовый TwoFactorGrant; клиент вызывает signIn с grantToken; Credentials provider принимает либо email+password (для пользователей без 2FA), либо grantToken (после 2FA). Настройка 2FA (включение/выбор способа/отключение) — в админке, защищённые API с проверкой сессии.
Tech Stack: NextAuth (Credentials), Prisma, otplib (TOTP), Node crypto (AES-256-GCM для секрета TOTP), существующий SMTP/nodemailer.
Design doc: docs/plans/2026-02-24-2fa-design.md
Files:
- Modify:
nextjs-project/prisma/schema.prisma(модель User и две новые модели)
Step 1: Добавить поля и модели в schema.prisma
В модели User после updatedAt добавить:
twoFactorEnabled Boolean @default(false)twoFactorMethod String? // 'email' | 'totp'totpSecretEncrypted String?В секцию relations добавить:twoFactorPending TwoFactorPending?иtwoFactorGrants TwoFactorGrant[].
Добавить модели (по образцу PasswordResetToken / SetInitialPasswordToken):
model TwoFactorPending {
id String @id @default(cuid())
userId String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
tokenHash String
expiresAt DateTime
emailCodeHash String?
emailCodeExpiresAt DateTime?
createdAt DateTime @default(now())
@@index([userId])
@@index([expiresAt])
}
model TwoFactorGrant {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
usedAt DateTime?
expiresAt DateTime
createdAt DateTime @default(now())
@@index([userId])
@@index([expiresAt])
}Step 2: Создать миграцию
Run: cd nextjs-project && npx prisma migrate dev --name add_2fa_tables
Expected: миграция создана и применена.
Step 3: Commit
git add nextjs-project/prisma/schema.prisma nextjs-project/prisma/migrations/
git commit -m "feat(2fa): add User 2FA fields and TwoFactorPending, TwoFactorGrant tables"Files:
- Modify:
nextjs-project/src/lib/email.ts
Step 1: Добавить send2FACodeEmail
По образцу sendInitialPasswordCodeEmail: функция send2FACodeEmail(to: string, code: string): Promise<{ ok: boolean; error?: string }>. Тема письма: «Код для входа — Inner Health», текст: «Ваш код для входа: {code}. Действует 5 минут.» Использовать тот же transporter (SMTP_HOST и т.д.) и SUPPORT_FROM, если есть, иначе как в sendPasswordResetEmail.
Step 2: Commit
git add nextjs-project/src/lib/email.ts
git commit -m "feat(2fa): add send2FACodeEmail for email 2FA"Files:
- Create:
nextjs-project/src/lib/totp.ts - Modify:
nextjs-project/package.json(добавить зависимость otplib)
Step 1: Установить otplib
Run: cd nextjs-project && npm install otplib
(Или authenticator — по предпочтению; в плане используем otplib: authenticator.generateSecret(), authenticator.verifyToken().)
Step 2: Создать nextjs-project/src/lib/totp.ts
- Функция
generateTotpSecret(): string— генерирует секрет (otplib). - Функция
encryptTotpSecret(plainSecret: string): string— шифрует AES-256-GCM, ключ изprocess.env.TOTP_SECRET_ENCRYPTION_KEY(32 байта base64 или hex). Возвращать base64 ciphertext+iv+authTag. - Функция
decryptTotpSecret(encrypted: string): string— расшифровка. - Функция
verifyTotpCode(encryptedSecret: string, code: string): boolean— расшифровать секрет, проверить code через otplib (окно ±1 при необходимости). - Функция
getTotpUri(secret: string, email: string, issuer: string): string— для QR (otpauth://...).
Если TOTP_SECRET_ENCRYPTION_KEY не задан — в encrypt/decrypt бросать понятную ошибку или возвращать null и проверять в вызывающем коде.
Step 3: Commit
git add nextjs-project/package.json nextjs-project/package-lock.json nextjs-project/src/lib/totp.ts
git commit -m "feat(2fa): add TOTP secret encryption and verification (otplib)"Files:
- Create:
nextjs-project/src/lib/two-factor.ts
Step 1: Реализовать хелперы
- Константы:
TWO_FACTOR_PENDING_COOKIE = 'two_factor_pending', TTL pending 10 мин, TTL grant 2 мин. createPendingToken(): { token: string; hash: string }— случайный token (crypto.randomBytes(32).toString('hex')), hash = bcrypt.hash(token).verifyPendingTokenHash(plainToken: string, hash: string): Promise<boolean>— bcrypt.compare.- Подпись cookie: хранить в cookie значение
{ id: pendingRecordId }или сам token; на сервере ищем pending по userId из cookie или по id. Проще: в cookie кладём только подписанный payload{ pendingId }(jwt или HMAC). Либо хранить в БД tokenHash и отдавать клиенту в cookie неподписанный id записи + подпись (HMAC). Рекомендация: cookie =base64url({ id: string }) + '.' + HMAC(id, NEXTAUTH_SECRET); при чтении проверяем подпись и ищем TwoFactorPending по id. - Функции:
setPendingCookie(id: string, res: NextResponse)— установить httpOnly, secure, sameSite, path=/, maxAge=600.getPendingIdFromCookie(cookieHeader: string | null): string | null— разобрать cookie, проверить подпись, вернуть id. createGrant(userId: string): Promise<{ grantId: string }>— создать TwoFactorGrant, вернуть id.consumeGrant(grantId: string): Promise<{ userId: string } | null>— найти по id, проверить usedAt === null и expiresAt > now, обновить usedAt, вернуть userId или null.
Step 2: Commit
git add nextjs-project/src/lib/two-factor.ts
git commit -m "feat(2fa): add two-factor cookie and grant helpers"Files:
- Create:
nextjs-project/src/app/api/auth/login-step1/route.ts
Step 1: Реализовать route
- POST, body:
{ email: string, password: string }(Zod). - Найти user по email; проверить пароль (как в auth.ts: isBcryptHash + verifyPassword или plain).
- Если неверно — return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 }).
- Если верно и
!user.twoFactorEnabled— return NextResponse.json({ success: true }). - Если верно и user.twoFactorEnabled: создать или обновить TwoFactorPending (upsert по userId: tokenHash, expiresAt = now + 10 min). Если twoFactorMethod === 'email' — сгенерировать 6-значный код, сохранить bcrypt hash в emailCodeHash, emailCodeExpiresAt = now + 5 min, вызвать send2FACodeEmail(user.email, code). Установить cookie two_factor_pending с подписанным pendingId (через setPendingCookie). Вернуть NextResponse.json({ need2FA: true, method: user.twoFactorMethod }, { status: 200 }) и в заголовках Set-Cookie от cookie.
- Использовать NextResponse с cookies: после создания response вызвать setPendingCookie(pending.id, response) и вернуть response.
Step 2: Commit
git add nextjs-project/src/app/api/auth/login-step1/route.ts
git commit -m "feat(2fa): add login-step1 API (password check, create pending if 2FA)"Files:
- Create:
nextjs-project/src/app/api/auth/2fa/send-code/route.ts
Step 1: Реализовать route
- POST, без body (или пустой). Читать cookie two_factor_pending, получить pendingId через getPendingIdFromCookie.
- Найти TwoFactorPending по id, проверить expiresAt > now. Загрузить user; проверить user.twoFactorMethod === 'email' (иначе 400).
- Rate limit: если emailCodeExpiresAt не null и прошло меньше 60 сек с предыдущей отправки — 429 Too Many Requests.
- Сгенерировать новый 6-значный код, обновить emailCodeHash и emailCodeExpiresAt в записи, отправить send2FACodeEmail(user.email, code). Вернуть 200 { ok: true }.
Step 2: Commit
git add nextjs-project/src/app/api/auth/2fa/send-code/route.ts
git commit -m "feat(2fa): add 2fa send-code API for email method"Files:
- Create:
nextjs-project/src/app/api/auth/verify-2fa/route.ts
Step 1: Реализовать route
- POST, body:
{ code: string }(Zod, строка 6 цифр для email или 6 цифр для TOTP). - Из cookie two_factor_pending получить pendingId, найти TwoFactorPending, проверить expiresAt.
- Загрузить user с полями twoFactorMethod, totpSecretEncrypted.
- Если method === 'email': сравнить code с emailCodeHash (bcrypt). Если method === 'totp': verifyTotpCode(user.totpSecretEncrypted!, code).
- При неверном коде — 401 { error: 'Invalid code' }. При верном: создать TwoFactorGrant (createGrant(user.id)), удалить или обновить TwoFactorPending (удалить запись), очистить cookie two_factor_pending (maxAge=0), вернуть 200 { grantToken: grantId }.
- В ответе установить Set-Cookie на two_factor_pending с пустым значением и maxAge=0 для очистки.
Step 2: Commit
git add nextjs-project/src/app/api/auth/verify-2fa/route.ts
git commit -m "feat(2fa): add verify-2fa API and grant token issue"Files:
- Modify:
nextjs-project/src/lib/auth.ts
Step 1: Расширить credentials и authorize
- В credentials добавить:
grantToken: { label: "Grant Token", type: "text" }(опционально). - В authorize: если передан
credentials.grantToken: вызвать consumeGrant(credentials.grantToken); если вернулся userId — загрузить user по id, вернуть объект { id, email, name, role, mustChangePassword }. Если не вернулся — return null. - Если grantToken не передан: текущая логика email+password. После успешной проверки пароля: если user.twoFactorEnabled === true — return null (чтобы нельзя было обойти 2FA прямым вызовом signIn с паролем). Иначе вернуть user как сейчас.
Step 2: Commit
git add nextjs-project/src/lib/auth.ts
git commit -m "feat(2fa): Credentials provider accept grantToken and reject password-only when 2FA on"Files:
- Modify:
nextjs-project/src/app/login/page.tsx
Step 1: Изменить поток входа
- Вместо прямого signIn('credentials', { email, password }): сначала fetch POST /api/auth/login-step1 с { email, password } (credentials: 'include' для cookie).
- Если ответ 401 — показать «Неверные учетные данные».
- Если ответ 200 и body.success === true — вызвать signIn('credentials', { email, password }), затем редирект на change-password или /admin/catalog как сейчас.
- Если ответ 200 и body.need2FA === true — редирект на
/login/2fa?method=${body.method}(или сохранить method в cookie/state). Не вызывать signIn.
Step 2: Commit
git add nextjs-project/src/app/login/page.tsx
git commit -m "feat(2fa): login page use login-step1 and redirect to 2fa when needed"Files:
- Create:
nextjs-project/src/app/login/2fa/page.tsx
Step 1: Реализовать страницу
- Client component. Читать method из searchParams (email | totp). Поле ввода кода (6 цифр), кнопка «Войти». При method === 'email' — кнопка «Отправить код повторно» (вызов POST /api/auth/2fa/send-code с credentials: 'include'), disable на 60 сек после отправки.
- Submit: POST /api/auth/verify-2fa с { code }, credentials: 'include'. При 401 — показать ошибку. При 200 — вызвать signIn('credentials', { grantToken: body.grantToken }), затем редирект на /login/change-password если mustChangePassword, иначе /admin/catalog; router.refresh().
- Оформление: в духе текущей страницы логина (фон, форма), заголовок «Введите код из письма» или «Введите код из приложения».
Step 2: Commit
git add nextjs-project/src/app/login/2fa/page.tsx
git commit -m "feat(2fa): add /login/2fa page for code entry and grant sign-in"Files:
- Create:
nextjs-project/src/app/api/auth/2fa/setup/route.ts
Step 1: Реализовать POST /api/auth/2fa/setup
- Получить сессию (getServerSession(authOptions)); без сессии — 401.
- Body:
{ action: 'enable' | 'disable', method?: 'email' | 'totp', code?: string }. Для enable при method 'totp' при первом запросе можно вернуть { secret, uri } без установки в БД до проверки code. - enable + method email: установить user.twoFactorEnabled = true, twoFactorMethod = 'email', totpSecretEncrypted = null. Вернуть 200.
- enable + method totp: если code не передан — сгенерировать секрет, зашифровать, сохранить во временное поле или вернуть uri для QR и сохранить секрет в сессии/времени; при следующем запросе с code — проверить TOTP, сохранить totpSecretEncrypted, twoFactorEnabled = true, twoFactorMethod = 'totp'. Упрощение: один запрос с code — генерируем секрет, показываем uri в ответе, сохраняем зашифрованный секрет, проверяем code и только тогда twoFactorEnabled = true (иначе злоумышленник не сможет завершить). Либо два шага: POST с action=enable, method=totp без code → 200 { secret, uri }; фронт показывает QR; POST с action=enable, method=totp, code → проверка, сохранение.
- disable: проверить текущий пароль или код 2FA (body.password или body.code). Если верно — twoFactorEnabled = false, twoFactorMethod = null, totpSecretEncrypted = null. Вернуть 200.
Step 2: Commit
git add nextjs-project/src/app/api/auth/2fa/setup/route.ts
git commit -m "feat(2fa): add 2fa setup API (enable email/totp, disable)"Files:
- Create or modify: страница настроек/профиля в админке (например
nextjs-project/src/app/admin/settings/page.tsxили раздел в существующей странице профиля).
Step 1: Добавить блок «Двухфакторная аутентификация»
- Показать текущий статус: выключено / email / приложение.
- Если выключено: кнопки «Включить по email» и «Включить через приложение». По клику — вызов setup с action=enable, method=email или totp (для totp сначала запрос без code для получения uri, отобразить QR и поле ввода кода, затем запрос с code).
- Если включено: кнопка «Отключить 2FA» — модалка с вводом пароля или кода, затем POST setup action=disable.
- Расположение: по существующей структуре админки (например ссылка «Настройки» в сайдбаре, страница с формой).
Step 2: Commit
git add nextjs-project/src/app/admin/settings/page.tsx # или изменённые файлы
git commit -m "feat(2fa): admin UI for enable/disable and method selection"Files:
- Create or modify:
nextjs-project/docs/password-reset-env.mdили отдельныйnextjs-project/docs/2fa-env.md - Modify:
.env.example(если есть) — добавитьTOTP_SECRET_ENCRYPTION_KEY=(32 bytes base64).
Step 1: Описать переменные
TOTP_SECRET_ENCRYPTION_KEY— ключ для шифрования TOTP-секрета (32 байта, base64). Генерация:node -e "console.log(require('crypto').randomBytes(32).toString('base64'))".- Для email 2FA используется существующий SMTP (SMTP_HOST и т.д.).
Step 2: Commit
git add nextjs-project/docs/2fa-env.md nextjs-project/.env.example
git commit -m "docs(2fa): env and 2FA setup documentation"- Задачи 1–10 обеспечивают полный поток входа с 2FA (email и TOTP).
- Задачи 11–12 — настройка 2FA в админке.
- Задача 13 — документация.
После выполнения плана: проверить сценарии «логин без 2FA», «логин с 2FA email», «логин с 2FA TOTP», «включение/отключение 2FA в настройках».
Plan complete and saved to docs/plans/2026-02-24-2fa.md.
Two execution options:
- Subagent-Driven (this session) — dispatch a fresh subagent per task, review between tasks, fast iteration.
- Parallel Session (separate) — open a new session with executing-plans, batch execution with checkpoints.
Which approach?