From fad036ef6c2cf727d7b3e1d021a44e14527665bc Mon Sep 17 00:00:00 2001 From: RedStar Date: Mon, 24 Aug 2026 14:24:04 +0200 Subject: [PATCH] feat(auth): add two-factor authentication --- README.md | 7 + apps/dashboard/app/auth.config.ts | 6 + apps/dashboard/app/pages/two-factor.vue | 269 ++++ .../auth/components/TwoFactorError.vue | 13 + .../modules/auth/components/UserMenu.vue | 3 + apps/dashboard/nuxt.config.ts | 3 + apps/dashboard/package.json | 3 + apps/dashboard/server/auth.config.ts | 1 + apps/dashboard/test/e2e/auth.spec.ts | 22 + .../test/nuxt/components/UserMenu.spec.ts | 9 + .../test/nuxt/pages/two-factor.spec.ts | 112 ++ docs/architecture.md | 12 +- packages/auth/package.json | 1 + packages/auth/src/auth.test.ts | 9 + packages/auth/src/auth.ts | 7 + .../0004_outstanding_mister_sinister.sql | 16 + .../database/drizzle/meta/0004_snapshot.json | 1313 +++++++++++++++++ packages/database/drizzle/meta/_journal.json | 7 + packages/database/src/client.test.ts | 1 + packages/database/src/schema/auth.test.ts | 29 + packages/database/src/schema/auth.ts | 34 +- packages/database/src/schema/index.ts | 5 +- packages/i18n/locales/en/auth.json | 29 + packages/i18n/locales/it/auth.json | 29 + packages/i18n/schemas/auth.schema.json | 87 ++ pnpm-lock.yaml | 167 ++- 26 files changed, 2183 insertions(+), 11 deletions(-) create mode 100644 apps/dashboard/app/pages/two-factor.vue create mode 100644 apps/dashboard/modules/auth/components/TwoFactorError.vue create mode 100644 apps/dashboard/test/nuxt/pages/two-factor.spec.ts create mode 100644 packages/database/drizzle/0004_outstanding_mister_sinister.sql create mode 100644 packages/database/drizzle/meta/0004_snapshot.json create mode 100644 packages/database/src/schema/auth.test.ts diff --git a/README.md b/README.md index 1e21d59..ce08a6d 100644 --- a/README.md +++ b/README.md @@ -191,6 +191,13 @@ change them. The flip side of that build-time capture is a deployment contract: those policy variables, rebuild the app, or the auth pages will keep advertising the old capabilities (the server still enforces its own policy either way). +Every account can enable TOTP two-factor authentication from the signed-in user menu. Setup at +`/two-factor` requires the account password, displays a QR code plus one-time backup codes, and +does not become active until the first authenticator code verifies. The same route handles the +second-factor challenge after password sign-in, including backup-code recovery and an optional +30-day trusted-device cookie. Apply the checked-in database migrations before deploying this +feature: the Better Auth plugin adds `user.two_factor_enabled` and the `two_factor` table. + The interface ships English and Italian through `@nuxtjs/i18n`, with dictionaries split by scope in `packages/i18n/locales//` and shared with the marketing site; each app loads only the scopes it renders. `aube run i18n:status` builds a diff --git a/apps/dashboard/app/auth.config.ts b/apps/dashboard/app/auth.config.ts index 5446f9d..355a01c 100644 --- a/apps/dashboard/app/auth.config.ts +++ b/apps/dashboard/app/auth.config.ts @@ -6,6 +6,7 @@ import { lastLoginMethodClient, multiSessionClient, organizationClient, + twoFactorClient, } from 'better-auth/client/plugins'; // Better Auth is mounted in this app's own server (`server/auth.config.ts`), so every request is @@ -39,6 +40,11 @@ export default defineClientAuth(() => { betterEnrollmentClient(), lastLoginMethodClient(), multiSessionClient(), + twoFactorClient({ + onTwoFactorRedirect: async () => { + await navigateTo('/two-factor'); + }, + }), deviceAuthorizationClient(), dashClient(), // The one entry that is gated, and the only one whose absence changes nothing about the diff --git a/apps/dashboard/app/pages/two-factor.vue b/apps/dashboard/app/pages/two-factor.vue new file mode 100644 index 0000000..2804971 --- /dev/null +++ b/apps/dashboard/app/pages/two-factor.vue @@ -0,0 +1,269 @@ + + + diff --git a/apps/dashboard/modules/auth/components/TwoFactorError.vue b/apps/dashboard/modules/auth/components/TwoFactorError.vue new file mode 100644 index 0000000..7577a33 --- /dev/null +++ b/apps/dashboard/modules/auth/components/TwoFactorError.vue @@ -0,0 +1,13 @@ + + + diff --git a/apps/dashboard/modules/auth/components/UserMenu.vue b/apps/dashboard/modules/auth/components/UserMenu.vue index 1479c85..e635a5e 100644 --- a/apps/dashboard/modules/auth/components/UserMenu.vue +++ b/apps/dashboard/modules/auth/components/UserMenu.vue @@ -6,6 +6,9 @@

{{ user?.name || user?.email }}

+ + {{ $t('auth.twoFactor.link') }} +