create-nuxt-base 1.0.3 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -34,102 +34,199 @@ export interface AuthResponse {
34
34
  };
35
35
  }
36
36
 
37
- // =============================================================================
38
- // Base Client Configuration
39
- // =============================================================================
40
-
41
- const baseClient = createAuthClient({
42
- basePath: '/iam', // IMPORTANT: Must match nest-server betterAuth.basePath, default: '/iam'
43
- baseURL: import.meta.env?.VITE_API_URL || process.env.API_URL || 'http://localhost:3000',
44
- plugins: [
45
- adminClient(),
46
- twoFactorClient({
47
- onTwoFactorRedirect() {
48
- navigateTo('/auth/2fa');
49
- },
50
- }),
51
- passkeyClient(),
52
- ],
53
- });
37
+ /**
38
+ * Configuration options for the auth client factory
39
+ * All options have sensible defaults for nest-server compatibility
40
+ */
41
+ export interface AuthClientConfig {
42
+ /** API base URL (default: from env or http://localhost:3000) */
43
+ baseURL?: string;
44
+ /** Auth API base path (default: '/iam' - must match nest-server betterAuth.basePath) */
45
+ basePath?: string;
46
+ /** 2FA redirect path (default: '/auth/2fa') */
47
+ twoFactorRedirectPath?: string;
48
+ /** Enable admin plugin (default: true) */
49
+ enableAdmin?: boolean;
50
+ /** Enable 2FA plugin (default: true) */
51
+ enableTwoFactor?: boolean;
52
+ /** Enable passkey plugin (default: true) */
53
+ enablePasskey?: boolean;
54
+ }
54
55
 
55
56
  // =============================================================================
56
- // Auth Client with Password Hashing
57
+ // Auth Client Factory
57
58
  // =============================================================================
58
59
 
59
60
  /**
60
- * Extended auth client that hashes passwords before transmission.
61
+ * Creates a configured Better-Auth client with password hashing
62
+ *
63
+ * This factory function allows creating auth clients with custom configuration,
64
+ * making it reusable across different projects.
65
+ *
66
+ * @example
67
+ * ```typescript
68
+ * // Default configuration (works with nest-server defaults)
69
+ * const authClient = createBetterAuthClient();
70
+ *
71
+ * // Custom configuration
72
+ * const authClient = createBetterAuthClient({
73
+ * baseURL: 'https://api.example.com',
74
+ * basePath: '/auth',
75
+ * twoFactorRedirectPath: '/login/2fa',
76
+ * });
77
+ * ```
61
78
  *
62
79
  * SECURITY: Passwords are hashed with SHA256 client-side to prevent
63
80
  * plain text password transmission over the network.
64
- *
65
- * The server's normalizePasswordForIam() detects SHA256 hashes (64 hex chars)
66
- * and processes them correctly.
67
81
  */
68
- export const authClient = {
69
- // Spread all base client properties and methods
70
- ...baseClient,
71
-
72
- /**
73
- * Change password for an authenticated user (both passwords are hashed)
74
- */
75
- changePassword: async (params: { currentPassword: string; newPassword: string }, options?: any) => {
76
- const [hashedCurrent, hashedNew] = await Promise.all([sha256(params.currentPassword), sha256(params.newPassword)]);
77
- return baseClient.changePassword?.({ currentPassword: hashedCurrent, newPassword: hashedNew }, options);
78
- },
79
-
80
- /**
81
- * Reset password with token (new password is hashed before sending)
82
- */
83
- resetPassword: async (params: { newPassword: string; token: string }, options?: any) => {
84
- const hashedPassword = await sha256(params.newPassword);
85
- return baseClient.resetPassword?.({ newPassword: hashedPassword, token: params.token }, options);
86
- },
87
-
88
- // Override signIn to hash password
89
- signIn: {
90
- ...baseClient.signIn,
91
- /**
92
- * Sign in with email and password (password is hashed before sending)
93
- */
94
- email: async (params: { email: string; password: string; rememberMe?: boolean }, options?: any) => {
95
- const hashedPassword = await sha256(params.password);
96
- return baseClient.signIn.email({ ...params, password: hashedPassword }, options);
82
+ export function createBetterAuthClient(config: AuthClientConfig = {}) {
83
+ // In development, use empty baseURL and /api/iam path to leverage Nuxt server proxy
84
+ // This is REQUIRED for WebAuthn/Passkey to work correctly because:
85
+ // - Frontend runs on localhost:3002, API on localhost:3000
86
+ // - WebAuthn validates the origin, which must be consistent
87
+ // - The Nuxt server proxy ensures requests come from the frontend origin
88
+ const isDev = import.meta.env?.DEV || process.env.NODE_ENV === 'development';
89
+ const defaultBaseURL = isDev ? '' : (import.meta.env?.VITE_API_URL || process.env.API_URL || 'http://localhost:3000');
90
+ const defaultBasePath = isDev ? '/api/iam' : '/iam';
91
+
92
+ const {
93
+ baseURL = defaultBaseURL,
94
+ basePath = defaultBasePath,
95
+ twoFactorRedirectPath = '/auth/2fa',
96
+ enableAdmin = true,
97
+ enableTwoFactor = true,
98
+ enablePasskey = true,
99
+ } = config;
100
+
101
+ // Build plugins array based on configuration
102
+ const plugins: any[] = [];
103
+
104
+ if (enableAdmin) {
105
+ plugins.push(adminClient());
106
+ }
107
+
108
+ if (enableTwoFactor) {
109
+ plugins.push(
110
+ twoFactorClient({
111
+ onTwoFactorRedirect() {
112
+ navigateTo(twoFactorRedirectPath);
113
+ },
114
+ }),
115
+ );
116
+ }
117
+
118
+ if (enablePasskey) {
119
+ plugins.push(passkeyClient());
120
+ }
121
+
122
+ // Create base client with configuration
123
+ const baseClient = createAuthClient({
124
+ basePath,
125
+ baseURL,
126
+ fetchOptions: {
127
+ credentials: 'include', // Required for cross-origin cookie handling
97
128
  },
98
- },
129
+ plugins,
130
+ });
131
+
132
+ // Return extended client with password hashing
133
+ return {
134
+ // Spread all base client properties and methods
135
+ ...baseClient,
99
136
 
100
- // Explicitly pass through signOut (not captured by spread operator)
101
- signOut: baseClient.signOut,
137
+ // Explicitly pass through methods not captured by spread operator
138
+ useSession: baseClient.useSession,
139
+ passkey: (baseClient as any).passkey,
140
+ admin: (baseClient as any).admin,
141
+ $Infer: baseClient.$Infer,
142
+ $fetch: baseClient.$fetch,
143
+ $store: baseClient.$store,
102
144
 
103
- // Override signUp to hash password
104
- signUp: {
105
- ...baseClient.signUp,
106
145
  /**
107
- * Sign up with email and password (password is hashed before sending)
146
+ * Change password for an authenticated user (both passwords are hashed)
108
147
  */
109
- email: async (params: { email: string; name: string; password: string }, options?: any) => {
110
- const hashedPassword = await sha256(params.password);
111
- return baseClient.signUp.email({ ...params, password: hashedPassword }, options);
148
+ changePassword: async (params: { currentPassword: string; newPassword: string }, options?: any) => {
149
+ const [hashedCurrent, hashedNew] = await Promise.all([sha256(params.currentPassword), sha256(params.newPassword)]);
150
+ return baseClient.changePassword?.({ currentPassword: hashedCurrent, newPassword: hashedNew }, options);
112
151
  },
113
- },
114
152
 
115
- // Override twoFactor to hash passwords
116
- twoFactor: {
117
- ...baseClient.twoFactor,
118
153
  /**
119
- * Disable 2FA (password is hashed before sending)
154
+ * Reset password with token (new password is hashed before sending)
120
155
  */
121
- disable: async (params: { password: string }, options?: any) => {
122
- const hashedPassword = await sha256(params.password);
123
- return baseClient.twoFactor.disable({ password: hashedPassword }, options);
156
+ resetPassword: async (params: { newPassword: string; token: string }, options?: any) => {
157
+ const hashedPassword = await sha256(params.newPassword);
158
+ return baseClient.resetPassword?.({ newPassword: hashedPassword, token: params.token }, options);
124
159
  },
125
- /**
126
- * Enable 2FA (password is hashed before sending)
127
- */
128
- enable: async (params: { password: string }, options?: any) => {
129
- const hashedPassword = await sha256(params.password);
130
- return baseClient.twoFactor.enable({ password: hashedPassword }, options);
160
+
161
+ // Override signIn to hash password (keep passkey method from plugin)
162
+ signIn: {
163
+ ...baseClient.signIn,
164
+ /**
165
+ * Sign in with email and password (password is hashed before sending)
166
+ */
167
+ email: async (params: { email: string; password: string; rememberMe?: boolean }, options?: any) => {
168
+ const hashedPassword = await sha256(params.password);
169
+ return baseClient.signIn.email({ ...params, password: hashedPassword }, options);
170
+ },
171
+ /**
172
+ * Sign in with passkey (pass through to base client - provided by passkeyClient plugin)
173
+ * @see https://www.better-auth.com/docs/plugins/passkey
174
+ */
175
+ passkey: (baseClient.signIn as any).passkey,
176
+ },
177
+
178
+ // Explicitly pass through signOut (not captured by spread operator)
179
+ signOut: baseClient.signOut,
180
+
181
+ // Override signUp to hash password
182
+ signUp: {
183
+ ...baseClient.signUp,
184
+ /**
185
+ * Sign up with email and password (password is hashed before sending)
186
+ */
187
+ email: async (params: { email: string; name: string; password: string }, options?: any) => {
188
+ const hashedPassword = await sha256(params.password);
189
+ return baseClient.signUp.email({ ...params, password: hashedPassword }, options);
190
+ },
191
+ },
192
+
193
+ // Override twoFactor to hash passwords (provided by twoFactorClient plugin)
194
+ twoFactor: {
195
+ ...(baseClient as any).twoFactor,
196
+ /**
197
+ * Disable 2FA (password is hashed before sending)
198
+ */
199
+ disable: async (params: { password: string }, options?: any) => {
200
+ const hashedPassword = await sha256(params.password);
201
+ return (baseClient as any).twoFactor.disable({ password: hashedPassword }, options);
202
+ },
203
+ /**
204
+ * Enable 2FA (password is hashed before sending)
205
+ */
206
+ enable: async (params: { password: string }, options?: any) => {
207
+ const hashedPassword = await sha256(params.password);
208
+ return (baseClient as any).twoFactor.enable({ password: hashedPassword }, options);
209
+ },
210
+ /**
211
+ * Verify TOTP code (pass through to base client)
212
+ */
213
+ verifyTotp: (baseClient as any).twoFactor.verifyTotp,
214
+ /**
215
+ * Verify backup code (pass through to base client)
216
+ */
217
+ verifyBackupCode: (baseClient as any).twoFactor.verifyBackupCode,
131
218
  },
132
- },
133
- };
219
+ };
220
+ }
221
+
222
+ // =============================================================================
223
+ // Default Auth Client Instance
224
+ // =============================================================================
225
+
226
+ /**
227
+ * Default auth client instance with standard nest-server configuration
228
+ * Use createBetterAuthClient() for custom configuration
229
+ */
230
+ export const authClient = createBetterAuthClient();
134
231
 
135
- export type AuthClient = typeof authClient;
232
+ export type AuthClient = ReturnType<typeof createBetterAuthClient>;
@@ -0,0 +1,101 @@
1
+ <script setup lang="ts">
2
+ // ============================================================================
3
+ // Composables
4
+ // ============================================================================
5
+ const { user, signOut } = useBetterAuth();
6
+
7
+ // ============================================================================
8
+ // Variables
9
+ // ============================================================================
10
+ const pages = [
11
+ {
12
+ title: 'Sicherheit',
13
+ description: 'Verwalte 2FA, Passkeys und Kontosicherheit',
14
+ icon: 'i-lucide-shield-check',
15
+ to: '/app/settings/security',
16
+ },
17
+ ];
18
+
19
+ // ============================================================================
20
+ // Functions
21
+ // ============================================================================
22
+ async function handleSignOut(): Promise<void> {
23
+ await signOut();
24
+ await navigateTo('/auth/login');
25
+ }
26
+ </script>
27
+
28
+ <template>
29
+ <div class="mx-auto max-w-4xl px-4 py-8">
30
+ <!-- Welcome Header -->
31
+ <div class="mb-8">
32
+ <h1 class="text-3xl font-bold">
33
+ Willkommen{{ user?.name ? `, ${user.name}` : '' }}!
34
+ </h1>
35
+ <p class="mt-2 text-muted">
36
+ {{ user?.email }}
37
+ </p>
38
+ </div>
39
+
40
+ <!-- Quick Actions -->
41
+ <div class="mb-8">
42
+ <h2 class="mb-4 text-xl font-semibold">Schnellzugriff</h2>
43
+ <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
44
+ <UCard
45
+ v-for="page in pages"
46
+ :key="page.to"
47
+ class="cursor-pointer transition-shadow hover:shadow-lg"
48
+ @click="navigateTo(page.to)"
49
+ >
50
+ <div class="flex items-start gap-4">
51
+ <div class="rounded-lg bg-primary/10 p-3">
52
+ <UIcon :name="page.icon" class="size-6 text-primary" />
53
+ </div>
54
+ <div>
55
+ <h3 class="font-semibold">{{ page.title }}</h3>
56
+ <p class="mt-1 text-sm text-muted">{{ page.description }}</p>
57
+ </div>
58
+ </div>
59
+ </UCard>
60
+ </div>
61
+ </div>
62
+
63
+ <!-- User Info Card -->
64
+ <UCard>
65
+ <template #header>
66
+ <div class="flex items-center gap-3">
67
+ <UIcon name="i-lucide-user" class="size-6 text-primary" />
68
+ <h2 class="font-semibold">Dein Konto</h2>
69
+ </div>
70
+ </template>
71
+
72
+ <div class="space-y-3">
73
+ <div class="flex justify-between">
74
+ <span class="text-muted">Name</span>
75
+ <span class="font-medium">{{ user?.name || '-' }}</span>
76
+ </div>
77
+ <div class="flex justify-between">
78
+ <span class="text-muted">E-Mail</span>
79
+ <span class="font-medium">{{ user?.email || '-' }}</span>
80
+ </div>
81
+ <div class="flex justify-between">
82
+ <span class="text-muted">E-Mail verifiziert</span>
83
+ <UBadge :color="user?.emailVerified ? 'success' : 'warning'">
84
+ {{ user?.emailVerified ? 'Ja' : 'Nein' }}
85
+ </UBadge>
86
+ </div>
87
+ </div>
88
+
89
+ <template #footer>
90
+ <UButton
91
+ color="error"
92
+ variant="outline"
93
+ icon="i-lucide-log-out"
94
+ @click="handleSignOut"
95
+ >
96
+ Abmelden
97
+ </UButton>
98
+ </template>
99
+ </UCard>
100
+ </div>
101
+ </template>
@@ -24,7 +24,7 @@ interface Passkey {
24
24
  // ============================================================================
25
25
  const toast = useToast();
26
26
  const overlay = useOverlay();
27
- const { is2FAEnabled, user } = useBetterAuth();
27
+ const { is2FAEnabled, registerPasskey, setUser, user } = useBetterAuth();
28
28
 
29
29
  // ============================================================================
30
30
  // Variables
@@ -39,6 +39,11 @@ const passkeyLoading = ref<boolean>(false);
39
39
  const newPasskeyName = ref<string>('');
40
40
  const showAddPasskey = ref<boolean>(false);
41
41
 
42
+ // Form states for UForm (required for proper data binding)
43
+ const enable2FAForm = reactive({ password: '' });
44
+ const disable2FAForm = reactive({ password: '' });
45
+ const totpForm = reactive({ code: '' });
46
+
42
47
  const passwordSchema = v.object({
43
48
  password: v.pipe(v.string('Passwort ist erforderlich'), v.minLength(1, 'Passwort ist erforderlich')),
44
49
  });
@@ -70,14 +75,13 @@ async function addPasskey(): Promise<void> {
70
75
  passkeyLoading.value = true;
71
76
 
72
77
  try {
73
- const { error } = await authClient.passkey.addPasskey({
74
- name: newPasskeyName.value,
75
- });
78
+ // Use custom registerPasskey method with direct API calls
79
+ const result = await registerPasskey(newPasskeyName.value);
76
80
 
77
- if (error) {
81
+ if (!result.success) {
78
82
  toast.add({
79
83
  color: 'error',
80
- description: error.message || 'Passkey konnte nicht hinzugefügt werden',
84
+ description: result.error || 'Passkey konnte nicht hinzugefügt werden',
81
85
  title: 'Fehler',
82
86
  });
83
87
  return;
@@ -143,6 +147,11 @@ async function disable2FA(payload: FormSubmitEvent<PasswordSchema>): Promise<voi
143
147
  return;
144
148
  }
145
149
 
150
+ // Update user state to reflect 2FA disabled
151
+ if (user.value) {
152
+ setUser({ ...user.value, twoFactorEnabled: false });
153
+ }
154
+
146
155
  toast.add({
147
156
  color: 'success',
148
157
  description: '2FA wurde deaktiviert',
@@ -150,6 +159,7 @@ async function disable2FA(payload: FormSubmitEvent<PasswordSchema>): Promise<voi
150
159
  });
151
160
 
152
161
  show2FADisable.value = false;
162
+ disable2FAForm.password = '';
153
163
  } finally {
154
164
  loading.value = false;
155
165
  }
@@ -175,6 +185,7 @@ async function enable2FA(payload: FormSubmitEvent<PasswordSchema>): Promise<void
175
185
  totpUri.value = data?.totpURI ?? '';
176
186
  backupCodes.value = data?.backupCodes ?? [];
177
187
  showTotpSetup.value = true;
188
+ enable2FAForm.password = '';
178
189
  } finally {
179
190
  loading.value = false;
180
191
  }
@@ -224,6 +235,11 @@ async function verifyTotp(payload: FormSubmitEvent<TotpSchema>): Promise<void> {
224
235
  return;
225
236
  }
226
237
 
238
+ // Update user state to reflect 2FA enabled
239
+ if (user.value) {
240
+ setUser({ ...user.value, twoFactorEnabled: true });
241
+ }
242
+
227
243
  toast.add({
228
244
  color: 'success',
229
245
  description: '2FA wurde erfolgreich aktiviert',
@@ -231,6 +247,7 @@ async function verifyTotp(payload: FormSubmitEvent<TotpSchema>): Promise<void> {
231
247
  });
232
248
 
233
249
  showTotpSetup.value = false;
250
+ totpForm.code = '';
234
251
  await openBackupCodesModal(backupCodes.value);
235
252
  } finally {
236
253
  loading.value = false;
@@ -271,9 +288,9 @@ async function verifyTotp(payload: FormSubmitEvent<TotpSchema>): Promise<void> {
271
288
  </div>
272
289
 
273
290
  <template v-if="!is2FAEnabled && !showTotpSetup">
274
- <UForm :schema="passwordSchema" class="space-y-4" @submit="enable2FA">
291
+ <UForm :schema="passwordSchema" :state="enable2FAForm" class="space-y-4" @submit="enable2FA">
275
292
  <UFormField label="Passwort bestätigen" name="password">
276
- <UInput name="password" type="password" placeholder="Dein Passwort" />
293
+ <UInput v-model="enable2FAForm.password" type="password" placeholder="Dein Passwort" />
277
294
  </UFormField>
278
295
  <UButton type="submit" :loading="loading"> 2FA aktivieren </UButton>
279
296
  </UForm>
@@ -281,15 +298,15 @@ async function verifyTotp(payload: FormSubmitEvent<TotpSchema>): Promise<void> {
281
298
 
282
299
  <template v-if="showTotpSetup">
283
300
  <div class="space-y-4">
284
- <UAlert color="info" icon="i-lucide-info"> Scanne den QR-Code mit deiner Authenticator-App (z.B. Google Authenticator, Authy) und gib den Code ein. </UAlert>
301
+ <p class="text-sm text-muted">Scanne den QR-Code mit deiner Authenticator-App (z.B. Google Authenticator, Authy) und gib den Code ein.</p>
285
302
 
286
303
  <div class="flex justify-center">
287
304
  <img v-if="totpUri" :src="`https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(totpUri)}`" alt="TOTP QR Code" class="rounded-lg" />
288
305
  </div>
289
306
 
290
- <UForm :schema="totpSchema" class="space-y-4" @submit="verifyTotp">
307
+ <UForm :schema="totpSchema" :state="totpForm" class="space-y-4" @submit="verifyTotp">
291
308
  <UFormField label="Verifizierungscode" name="code">
292
- <UInput name="code" placeholder="000000" class="text-center font-mono" />
309
+ <UInput v-model="totpForm.code" placeholder="000000" class="text-center font-mono" />
293
310
  </UFormField>
294
311
  <div class="flex gap-2">
295
312
  <UButton type="submit" :loading="loading"> Verifizieren </UButton>
@@ -307,10 +324,10 @@ async function verifyTotp(payload: FormSubmitEvent<TotpSchema>): Promise<void> {
307
324
  </template>
308
325
 
309
326
  <template v-if="show2FADisable">
310
- <UForm :schema="passwordSchema" class="space-y-4" @submit="disable2FA">
327
+ <UForm :schema="passwordSchema" :state="disable2FAForm" class="space-y-4" @submit="disable2FA">
311
328
  <UAlert color="warning" icon="i-lucide-alert-triangle"> 2FA zu deaktivieren verringert die Sicherheit deines Kontos. </UAlert>
312
329
  <UFormField label="Passwort bestätigen" name="password">
313
- <UInput name="password" type="password" placeholder="Dein Passwort" />
330
+ <UInput v-model="disable2FAForm.password" type="password" placeholder="Dein Passwort" />
314
331
  </UFormField>
315
332
  <div class="flex gap-2">
316
333
  <UButton type="submit" color="error" :loading="loading"> 2FA deaktivieren </UButton>
@@ -13,6 +13,7 @@ import { authClient } from '~/lib/auth-client';
13
13
  // Composables
14
14
  // ============================================================================
15
15
  const toast = useToast();
16
+ const { setUser, validateSession } = useBetterAuth();
16
17
 
17
18
  // ============================================================================
18
19
  // Page Meta
@@ -28,6 +29,9 @@ const loading = ref<boolean>(false);
28
29
  const useBackupCode = ref<boolean>(false);
29
30
  const trustDevice = ref<boolean>(false);
30
31
 
32
+ // Form state for UForm
33
+ const formState = reactive({ code: '' });
34
+
31
35
  const schema = v.object({
32
36
  code: v.pipe(v.string('Code ist erforderlich'), v.minLength(6, 'Code muss mindestens 6 Zeichen haben')),
33
37
  });
@@ -41,35 +45,46 @@ async function onSubmit(payload: FormSubmitEvent<Schema>): Promise<void> {
41
45
  loading.value = true;
42
46
 
43
47
  try {
48
+ let result: any;
49
+
44
50
  if (useBackupCode.value) {
45
- const { error } = await authClient.twoFactor.verifyBackupCode({
51
+ result = await authClient.twoFactor.verifyBackupCode({
46
52
  code: payload.data.code,
47
53
  });
48
54
 
49
- if (error) {
55
+ if (result.error) {
50
56
  toast.add({
51
57
  color: 'error',
52
- description: error.message || 'Backup-Code ungültig',
58
+ description: result.error.message || 'Backup-Code ungültig',
53
59
  title: 'Fehler',
54
60
  });
55
61
  return;
56
62
  }
57
63
  } else {
58
- const { error } = await authClient.twoFactor.verifyTotp({
64
+ result = await authClient.twoFactor.verifyTotp({
59
65
  code: payload.data.code,
60
66
  trustDevice: trustDevice.value,
61
67
  });
62
68
 
63
- if (error) {
69
+ if (result.error) {
64
70
  toast.add({
65
71
  color: 'error',
66
- description: error.message || 'Code ungültig',
72
+ description: result.error.message || 'Code ungültig',
67
73
  title: 'Fehler',
68
74
  });
69
75
  return;
70
76
  }
71
77
  }
72
78
 
79
+ // Update auth state with user data from response
80
+ const userData = result?.data?.user || result?.user;
81
+ if (userData) {
82
+ setUser(userData);
83
+ } else {
84
+ // Fallback: validate session to get user data
85
+ await validateSession();
86
+ }
87
+
73
88
  await navigateTo('/app');
74
89
  } finally {
75
90
  loading.value = false;
@@ -92,10 +107,10 @@ function toggleBackupCode(): void {
92
107
  </p>
93
108
  </div>
94
109
 
95
- <UForm :schema="schema" class="flex flex-col gap-4" @submit="onSubmit">
110
+ <UForm :schema="schema" :state="formState" class="flex flex-col gap-4" @submit="onSubmit">
96
111
  <UFormField :label="useBackupCode ? 'Backup-Code' : 'Authentifizierungscode'" name="code">
97
112
  <UInput
98
- name="code"
113
+ v-model="formState.code"
99
114
  :placeholder="useBackupCode ? 'Backup-Code eingeben' : '000000'"
100
115
  size="lg"
101
116
  class="text-center font-mono text-lg tracking-widest"