New card in Настройки → Аккаунт: scan a QR code (otplib + qrcode), confirm with a live code, get 8 one-time recovery codes shown once. Only offered for authSource="local" — LDAP accounts already have their own MFA story at the directory level and are turned away with a clear message if they somehow hit the setup endpoint directly. Login flow: a 2FA account's password check now creates a short-lived "login challenge" (separate table from `sessions`, 5-minute TTL, capped at 6 verify attempts) instead of a real session, and the login page swaps to a second screen asking for a code — TOTP or a recovery code, either works. The LDAP branch of the login route is untouched; it returns before ever reaching the 2FA check. totpSecret is encrypted at rest via the existing lib/crypto/credentials.ts helper (same one already used for mailbox/LDAP bind passwords) rather than adding a second encryption scheme. Recovery codes are hashed, not stored plaintext, and each is single-use (marked usedAt, not deleted). Verified against the real deployment end-to-end with Playwright against a throwaway test account (created via the real admin-users API, fully deleted after): setup → confirm → recovery-code issuance → second login correctly prompted for a code → wrong code rejected → correct code and a recovery code both worked → a reused recovery code was correctly rejected → disable (password-gated) → subsequent login went straight through again. Also added unit tests for the TOTP/recovery-code helpers. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GteWhnWKTmnXcsd5jx6H7u
59 lines
1.5 KiB
JSON
59 lines
1.5 KiB
JSON
{
|
|
"name": "top-tickets",
|
|
"version": "0.1.0",
|
|
"private": true,
|
|
"scripts": {
|
|
"dev": "next dev",
|
|
"build": "next build",
|
|
"start": "next start -p ${PORT:-8081}",
|
|
"lint": "eslint",
|
|
"test": "vitest run",
|
|
"db:generate": "drizzle-kit generate",
|
|
"db:migrate": "drizzle-kit migrate",
|
|
"generate-key": "tsx scripts/generate-key.ts",
|
|
"bootstrap-admin": "tsx scripts/bootstrap-admin.ts"
|
|
},
|
|
"dependencies": {
|
|
"argon2": "^0.45.1",
|
|
"better-sqlite3": "^12.11.1",
|
|
"cheerio": "^1.2.0",
|
|
"drizzle-orm": "^0.45.2",
|
|
"framer-motion": "^12.4.7",
|
|
"html-to-text": "^10.0.0",
|
|
"imapflow": "^1.5.0",
|
|
"ldapjs": "^3.0.7",
|
|
"lucide-react": "^0.545.0",
|
|
"mailparser": "^3.9.14",
|
|
"next": "16.2.12",
|
|
"nodemailer": "^9.0.3",
|
|
"otplib": "^13.4.1",
|
|
"qrcode": "^1.5.4",
|
|
"react": "19.2.4",
|
|
"react-dom": "19.2.4",
|
|
"sanitize-html": "^2.13.1",
|
|
"telegraf": "^4.16.3",
|
|
"web-push": "^3.6.7",
|
|
"zod": "^4.4.3"
|
|
},
|
|
"devDependencies": {
|
|
"@tailwindcss/postcss": "^4.1.16",
|
|
"@types/better-sqlite3": "^7.6.13",
|
|
"@types/ldapjs": "^3.0.6",
|
|
"@types/mailparser": "^3.4.6",
|
|
"@types/node": "^20",
|
|
"@types/nodemailer": "^8.0.1",
|
|
"@types/qrcode": "^1.5.6",
|
|
"@types/react": "^19",
|
|
"@types/react-dom": "^19",
|
|
"@types/sanitize-html": "^2.16.1",
|
|
"@types/web-push": "^3.6.4",
|
|
"drizzle-kit": "^0.31.10",
|
|
"eslint": "^9",
|
|
"eslint-config-next": "16.2.12",
|
|
"tailwindcss": "^4.1.16",
|
|
"tsx": "^4.23.1",
|
|
"typescript": "^5",
|
|
"vitest": "^3.2.7"
|
|
}
|
|
}
|