create-nuxt-base 1.1.1 → 1.2.0

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.
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Shared authentication state for Cookie/JWT dual-mode authentication
3
+ *
4
+ * This module provides a reactive state that is shared between:
5
+ * - auth-client.ts (uses it for customFetch)
6
+ * - use-better-auth.ts (manages the state)
7
+ *
8
+ * Auth Mode Strategy:
9
+ * 1. Primary: Session cookies (more secure, HttpOnly)
10
+ * 2. Fallback: JWT tokens (when cookies are not available/working)
11
+ *
12
+ * The state is persisted in cookies for SSR compatibility.
13
+ */
14
+
15
+ export type AuthMode = 'cookie' | 'jwt';
16
+
17
+ /**
18
+ * Get the current auth mode from cookie
19
+ */
20
+ export function getAuthMode(): AuthMode {
21
+ if (import.meta.server) return 'cookie';
22
+
23
+ try {
24
+ const cookie = document.cookie.split('; ').find((row) => row.startsWith('auth-state='));
25
+ if (cookie) {
26
+ const parts = cookie.split('=');
27
+ const value = parts.length > 1 ? decodeURIComponent(parts.slice(1).join('=')) : '';
28
+ const state = JSON.parse(value);
29
+ return state?.authMode || 'cookie';
30
+ }
31
+ } catch {
32
+ // Ignore parse errors
33
+ }
34
+ return 'cookie';
35
+ }
36
+
37
+ /**
38
+ * Get the JWT token from cookie
39
+ */
40
+ export function getJwtToken(): string | null {
41
+ if (import.meta.server) return null;
42
+
43
+ try {
44
+ const cookie = document.cookie.split('; ').find((row) => row.startsWith('jwt-token='));
45
+ if (cookie) {
46
+ const parts = cookie.split('=');
47
+ const value = parts.length > 1 ? decodeURIComponent(parts.slice(1).join('=')) : '';
48
+ // Handle JSON-encoded string (useCookie stores as JSON)
49
+ if (value.startsWith('"') && value.endsWith('"')) {
50
+ return JSON.parse(value);
51
+ }
52
+ return value || null;
53
+ }
54
+ } catch {
55
+ // Ignore parse errors
56
+ }
57
+ return null;
58
+ }
59
+
60
+ /**
61
+ * Set JWT token in cookie
62
+ */
63
+ export function setJwtToken(token: string | null): void {
64
+ if (import.meta.server) return;
65
+
66
+ const maxAge = 60 * 60 * 24 * 7; // 7 days
67
+ if (token) {
68
+ document.cookie = `jwt-token=${encodeURIComponent(JSON.stringify(token))}; path=/; max-age=${maxAge}; samesite=lax`;
69
+ } else {
70
+ document.cookie = `jwt-token=; path=/; max-age=0`;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Update auth mode in the auth-state cookie
76
+ */
77
+ export function setAuthMode(mode: AuthMode): void {
78
+ if (import.meta.server) return;
79
+
80
+ try {
81
+ const cookie = document.cookie.split('; ').find((row) => row.startsWith('auth-state='));
82
+
83
+ let state = { user: null, authMode: mode };
84
+ if (cookie) {
85
+ const parts = cookie.split('=');
86
+ const value = parts.length > 1 ? decodeURIComponent(parts.slice(1).join('=')) : '';
87
+ state = { ...JSON.parse(value), authMode: mode };
88
+ }
89
+
90
+ const maxAge = 60 * 60 * 24 * 7; // 7 days
91
+ document.cookie = `auth-state=${encodeURIComponent(JSON.stringify(state))}; path=/; max-age=${maxAge}; samesite=lax`;
92
+ } catch {
93
+ // Ignore errors
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Get the API base URL
99
+ */
100
+ export function getApiBase(): string {
101
+ const isDev = import.meta.dev;
102
+ if (isDev) {
103
+ return '/api/iam';
104
+ }
105
+ // In production, try to get from runtime config or fall back to default
106
+ if (typeof window !== 'undefined' && (window as any).__NUXT__?.config?.public?.apiUrl) {
107
+ return `${(window as any).__NUXT__.config.public.apiUrl}/iam`;
108
+ }
109
+ return 'http://localhost:3000/iam';
110
+ }
111
+
112
+ /**
113
+ * Attempt to switch to JWT mode by fetching a token
114
+ */
115
+ export async function attemptJwtSwitch(): Promise<boolean> {
116
+ try {
117
+ const apiBase = getApiBase();
118
+ const response = await fetch(`${apiBase}/token`, {
119
+ method: 'GET',
120
+ credentials: 'include',
121
+ });
122
+
123
+ if (response.ok) {
124
+ const data = await response.json();
125
+ if (data.token) {
126
+ setJwtToken(data.token);
127
+ setAuthMode('jwt');
128
+ console.debug('[Auth] Switched to JWT mode');
129
+ return true;
130
+ }
131
+ }
132
+ return false;
133
+ } catch {
134
+ return false;
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Check if user is authenticated (has auth-state with user)
140
+ */
141
+ export function isAuthenticated(): boolean {
142
+ if (import.meta.server) return false;
143
+
144
+ try {
145
+ const cookie = document.cookie.split('; ').find((row) => row.startsWith('auth-state='));
146
+ if (cookie) {
147
+ const parts = cookie.split('=');
148
+ const value = parts.length > 1 ? decodeURIComponent(parts.slice(1).join('=')) : '';
149
+ const state = JSON.parse(value);
150
+ return !!state?.user;
151
+ }
152
+ } catch {
153
+ // Ignore parse errors
154
+ }
155
+ return false;
156
+ }
157
+
158
+ /**
159
+ * Custom fetch function that handles Cookie/JWT dual-mode authentication
160
+ *
161
+ * This function:
162
+ * 1. In cookie mode: Uses credentials: 'include'
163
+ * 2. In JWT mode: Adds Authorization header
164
+ * 3. On 401 in cookie mode: Attempts to switch to JWT and retries
165
+ */
166
+ export async function authFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
167
+ const authMode = getAuthMode();
168
+ const jwtToken = getJwtToken();
169
+
170
+ const headers = new Headers(init?.headers);
171
+
172
+ // In JWT mode, add Authorization header
173
+ if (authMode === 'jwt' && jwtToken) {
174
+ headers.set('Authorization', `Bearer ${jwtToken}`);
175
+ }
176
+
177
+ // Always include credentials for cookie-based session auth
178
+ // In JWT mode, cookies are sent but ignored by the server (Authorization header is used instead)
179
+ // This is more robust than conditionally omitting cookies
180
+ const response = await fetch(input, {
181
+ ...init,
182
+ headers,
183
+ credentials: 'include',
184
+ });
185
+
186
+ // If we get 401 in cookie mode and user is authenticated, try JWT fallback
187
+ if (response.status === 401 && authMode === 'cookie' && isAuthenticated()) {
188
+ console.debug('[Auth] Cookie auth failed, attempting JWT fallback...');
189
+ const switched = await attemptJwtSwitch();
190
+
191
+ if (switched) {
192
+ // Retry the request with JWT
193
+ const newToken = getJwtToken();
194
+ if (newToken) {
195
+ headers.set('Authorization', `Bearer ${newToken}`);
196
+ return fetch(input, {
197
+ ...init,
198
+ headers,
199
+ credentials: 'include',
200
+ });
201
+ }
202
+ }
203
+ }
204
+
205
+ return response;
206
+ }
@@ -4,20 +4,40 @@ export default defineNuxtRouteMiddleware(async (to) => {
4
4
  return;
5
5
  }
6
6
 
7
- const { isAdmin, isAuthenticated, isLoading } = useBetterAuth();
7
+ let isAuthenticated = false;
8
+ let isAdmin = false;
8
9
 
9
- // Wait for session to load
10
- if (isLoading.value) {
11
- return;
10
+ // On client, read directly from document.cookie for accurate state
11
+ if (import.meta.client) {
12
+ try {
13
+ const cookie = document.cookie.split('; ').find((row) => row.startsWith('auth-state='));
14
+ if (cookie) {
15
+ const parts = cookie.split('=');
16
+ const value = parts.length > 1 ? decodeURIComponent(parts.slice(1).join('=')) : '';
17
+ const state = JSON.parse(value);
18
+ isAuthenticated = !!state?.user;
19
+ isAdmin = state?.user?.role === 'admin';
20
+ }
21
+ } catch {
22
+ // Ignore parse errors
23
+ }
24
+ } else {
25
+ // On server, use useCookie
26
+ const authStateCookie = useCookie<{ user: { role?: string } | null; authMode: string } | null>('auth-state');
27
+ isAuthenticated = !!authStateCookie.value?.user;
28
+ isAdmin = authStateCookie.value?.user?.role === 'admin';
12
29
  }
13
30
 
14
31
  // Redirect to login if not authenticated
15
- if (!isAuthenticated.value) {
16
- return navigateTo('/auth/login');
32
+ if (!isAuthenticated) {
33
+ return navigateTo({
34
+ path: '/auth/login',
35
+ query: { redirect: to.fullPath },
36
+ });
17
37
  }
18
38
 
19
39
  // Redirect to /app if authenticated but not admin
20
- if (!isAdmin.value) {
40
+ if (!isAdmin) {
21
41
  return navigateTo('/app');
22
42
  }
23
43
  });
@@ -4,15 +4,32 @@ export default defineNuxtRouteMiddleware(async (to) => {
4
4
  return;
5
5
  }
6
6
 
7
- const { isAuthenticated, isLoading } = useBetterAuth();
7
+ let isAuthenticated = false;
8
8
 
9
- // Wait for session to load
10
- if (isLoading.value) {
11
- return;
9
+ // On client, read directly from document.cookie for accurate state
10
+ if (import.meta.client) {
11
+ try {
12
+ const cookie = document.cookie.split('; ').find((row) => row.startsWith('auth-state='));
13
+ if (cookie) {
14
+ const parts = cookie.split('=');
15
+ const value = parts.length > 1 ? decodeURIComponent(parts.slice(1).join('=')) : '';
16
+ const state = JSON.parse(value);
17
+ isAuthenticated = !!state?.user;
18
+ }
19
+ } catch {
20
+ // Ignore parse errors
21
+ }
22
+ } else {
23
+ // On server, use useCookie
24
+ const authStateCookie = useCookie<{ user: unknown; authMode: string } | null>('auth-state');
25
+ isAuthenticated = !!authStateCookie.value?.user;
12
26
  }
13
27
 
14
28
  // Redirect to login if not authenticated
15
- if (!isAuthenticated.value) {
16
- return navigateTo('/auth/login');
29
+ if (!isAuthenticated) {
30
+ return navigateTo({
31
+ path: '/auth/login',
32
+ query: { redirect: to.fullPath },
33
+ });
17
34
  }
18
35
  });
@@ -4,15 +4,30 @@ export default defineNuxtRouteMiddleware(async (to) => {
4
4
  return;
5
5
  }
6
6
 
7
- const { isAuthenticated, isLoading } = useBetterAuth();
7
+ let isAuthenticated = false;
8
8
 
9
- // Wait for session to load
10
- if (isLoading.value) {
11
- return;
9
+ // On client, read directly from document.cookie for accurate state
10
+ if (import.meta.client) {
11
+ try {
12
+ const cookie = document.cookie.split('; ').find((row) => row.startsWith('auth-state='));
13
+ if (cookie) {
14
+ const parts = cookie.split('=');
15
+ const value = parts.length > 1 ? decodeURIComponent(parts.slice(1).join('=')) : '';
16
+ const state = JSON.parse(value);
17
+ isAuthenticated = !!state?.user;
18
+ }
19
+ } catch {
20
+ // Ignore parse errors
21
+ }
22
+ } else {
23
+ // On server, use useCookie
24
+ const authStateCookie = useCookie<{ user: unknown; authMode: string } | null>('auth-state');
25
+ isAuthenticated = !!authStateCookie.value?.user;
12
26
  }
13
27
 
14
- // Redirect to /app if already authenticated
15
- if (isAuthenticated.value) {
16
- return navigateTo('/app');
28
+ // Redirect to /app (or redirect query) if already authenticated
29
+ if (isAuthenticated) {
30
+ const redirect = to.query.redirect as string;
31
+ return navigateTo(redirect || '/app');
17
32
  }
18
33
  });
@@ -29,9 +29,7 @@ async function handleSignOut(): Promise<void> {
29
29
  <div class="mx-auto max-w-4xl px-4 py-8">
30
30
  <!-- Welcome Header -->
31
31
  <div class="mb-8">
32
- <h1 class="text-3xl font-bold">
33
- Willkommen{{ user?.name ? `, ${user.name}` : '' }}!
34
- </h1>
32
+ <h1 class="text-3xl font-bold">Willkommen{{ user?.name ? `, ${user.name}` : '' }}!</h1>
35
33
  <p class="mt-2 text-muted">
36
34
  {{ user?.email }}
37
35
  </p>
@@ -41,12 +39,7 @@ async function handleSignOut(): Promise<void> {
41
39
  <div class="mb-8">
42
40
  <h2 class="mb-4 text-xl font-semibold">Schnellzugriff</h2>
43
41
  <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
- >
42
+ <UCard v-for="page in pages" :key="page.to" class="cursor-pointer transition-shadow hover:shadow-lg" @click="navigateTo(page.to)">
50
43
  <div class="flex items-start gap-4">
51
44
  <div class="rounded-lg bg-primary/10 p-3">
52
45
  <UIcon :name="page.icon" class="size-6 text-primary" />
@@ -87,14 +80,7 @@ async function handleSignOut(): Promise<void> {
87
80
  </div>
88
81
 
89
82
  <template #footer>
90
- <UButton
91
- color="error"
92
- variant="outline"
93
- icon="i-lucide-log-out"
94
- @click="handleSignOut"
95
- >
96
- Abmelden
97
- </UButton>
83
+ <UButton color="error" variant="outline" icon="i-lucide-log-out" @click="handleSignOut"> Abmelden </UButton>
98
84
  </template>
99
85
  </UCard>
100
86
  </div>
@@ -13,7 +13,7 @@ import { authClient } from '~/lib/auth-client';
13
13
  // Composables
14
14
  // ============================================================================
15
15
  const toast = useToast();
16
- const { setUser, validateSession } = useBetterAuth();
16
+ const { fetchWithAuth, setUser, switchToJwtMode, jwtToken } = useBetterAuth();
17
17
 
18
18
  // ============================================================================
19
19
  // Page Meta
@@ -76,13 +76,45 @@ async function onSubmit(payload: FormSubmitEvent<Schema>): Promise<void> {
76
76
  }
77
77
  }
78
78
 
79
- // Update auth state with user data from response
79
+ // Extract token and user data from response (JWT mode: cookies: false)
80
+ const token = result?.token || result?.data?.token;
80
81
  const userData = result?.data?.user || result?.user;
81
- if (userData) {
82
- setUser(userData);
82
+
83
+ if (token) {
84
+ // JWT mode: Token is in the response
85
+ jwtToken.value = token;
86
+ if (userData) {
87
+ setUser(userData, 'jwt');
88
+ }
89
+ console.debug('[Auth] JWT token received from 2FA response');
90
+ } else if (userData) {
91
+ // Cookie mode: No token in response, use cookies
92
+ setUser(userData, 'cookie');
93
+ // Try to get JWT token for fallback
94
+ switchToJwtMode().catch(() => {});
83
95
  } else {
84
- // Fallback: validate session to get user data
85
- await validateSession();
96
+ // Fallback: fetch session data from API using authenticated fetch
97
+ try {
98
+ const isDev = import.meta.dev;
99
+ const runtimeConfig = useRuntimeConfig();
100
+ const apiBase = isDev ? '/api/iam' : `${runtimeConfig.public.apiUrl || 'http://localhost:3000'}/iam`;
101
+ const sessionResponse = await fetchWithAuth(`${apiBase}/get-session`);
102
+ if (sessionResponse.ok) {
103
+ const sessionData = await sessionResponse.json();
104
+ const sessionToken = sessionData?.token;
105
+ if (sessionToken) {
106
+ jwtToken.value = sessionToken;
107
+ if (sessionData?.user) {
108
+ setUser(sessionData.user, 'jwt');
109
+ }
110
+ } else if (sessionData?.user) {
111
+ setUser(sessionData.user, 'cookie');
112
+ switchToJwtMode().catch(() => {});
113
+ }
114
+ }
115
+ } catch {
116
+ // Ignore session fetch errors
117
+ }
86
118
  }
87
119
 
88
120
  await navigateTo('/app');
@@ -7,13 +7,11 @@ import type { InferOutput } from 'valibot';
7
7
 
8
8
  import * as v from 'valibot';
9
9
 
10
- import { authClient } from '~/lib/auth-client';
11
-
12
10
  // ============================================================================
13
11
  // Composables
14
12
  // ============================================================================
15
13
  const toast = useToast();
16
- const { signIn, setUser, isLoading, validateSession } = useBetterAuth();
14
+ const { signIn, setUser, isLoading, validateSession, authenticateWithPasskey } = useBetterAuth();
17
15
 
18
16
  // ============================================================================
19
17
  // Page Meta
@@ -54,32 +52,30 @@ type Schema = InferOutput<typeof schema>;
54
52
 
55
53
  /**
56
54
  * Handle passkey authentication
57
- * Uses official Better Auth signIn.passkey() method
58
- * @see https://www.better-auth.com/docs/plugins/passkey
55
+ * Uses authenticateWithPasskey from composable which supports JWT mode (challengeId)
59
56
  */
60
57
  async function onPasskeyLogin(): Promise<void> {
61
58
  passkeyLoading.value = true;
62
59
 
63
60
  try {
64
- // Use official Better Auth client method
65
- // This calls: GET /passkey/generate-authenticate-options → POST /passkey/verify-authentication
66
- const result = await authClient.signIn.passkey();
61
+ // Use composable method which handles challengeId for JWT mode
62
+ const result = await authenticateWithPasskey();
67
63
 
68
- // Check for error in response
69
- if (result.error) {
64
+ // Check for error in response (authenticateWithPasskey returns { success, error?, user? })
65
+ if (!result.success) {
70
66
  toast.add({
71
67
  color: 'error',
72
- description: result.error.message || 'Passkey-Anmeldung fehlgeschlagen',
68
+ description: result.error || 'Passkey-Anmeldung fehlgeschlagen',
73
69
  title: 'Fehler',
74
70
  });
75
71
  return;
76
72
  }
77
73
 
78
74
  // Update auth state with user data if available
79
- if (result.data?.user) {
80
- setUser(result.data.user as any);
81
- } else if (result.data?.session) {
82
- // Passkey auth returns session without user - fetch user via session validation
75
+ if (result.user) {
76
+ setUser(result.user as any);
77
+ } else {
78
+ // Passkey auth may return success without user - fetch user via session validation
83
79
  await validateSession();
84
80
  }
85
81
 
@@ -129,10 +125,7 @@ async function onSubmit(payload: FormSubmitEvent<Schema>): Promise<void> {
129
125
  // Check if 2FA is required
130
126
  // Better-Auth native uses 'twoFactorRedirect', nest-server REST API uses 'requiresTwoFactor'
131
127
  const resultData = 'data' in result ? result.data : result;
132
- const requires2FA = resultData && (
133
- ('twoFactorRedirect' in resultData && resultData.twoFactorRedirect) ||
134
- ('requiresTwoFactor' in resultData && resultData.requiresTwoFactor)
135
- );
128
+ const requires2FA = resultData && (('twoFactorRedirect' in resultData && resultData.twoFactorRedirect) || ('requiresTwoFactor' in resultData && resultData.requiresTwoFactor));
136
129
  if (requires2FA) {
137
130
  // Redirect to 2FA page
138
131
  await navigateTo('/auth/2fa');
@@ -140,7 +133,7 @@ async function onSubmit(payload: FormSubmitEvent<Schema>): Promise<void> {
140
133
  }
141
134
 
142
135
  // Check if login was successful (user data in response)
143
- const userData = 'user' in result ? result.user : ('data' in result ? result.data?.user : null);
136
+ const userData = 'user' in result ? result.user : 'data' in result ? result.data?.user : null;
144
137
  if (userData) {
145
138
  // Auth state is already stored by useBetterAuth
146
139
  // Navigate to app
@@ -11,7 +11,7 @@ import * as v from 'valibot';
11
11
  // Composables
12
12
  // ============================================================================
13
13
  const toast = useToast();
14
- const { signUp, signIn } = useBetterAuth();
14
+ const { signUp, signIn, registerPasskey } = useBetterAuth();
15
15
 
16
16
  // ============================================================================
17
17
  // Page Meta
@@ -24,6 +24,8 @@ definePageMeta({
24
24
  // Variables
25
25
  // ============================================================================
26
26
  const loading = ref<boolean>(false);
27
+ const showPasskeyPrompt = ref<boolean>(false);
28
+ const passkeyLoading = ref<boolean>(false);
27
29
 
28
30
  const fields: AuthFormField[] = [
29
31
  {
@@ -117,40 +119,89 @@ async function onSubmit(payload: FormSubmitEvent<Schema>): Promise<void> {
117
119
 
118
120
  toast.add({
119
121
  color: 'success',
120
- description: 'Dein Konto wurde erfolgreich erstellt. Du kannst Passkeys später in den Sicherheitseinstellungen hinzufügen.',
122
+ description: 'Dein Konto wurde erfolgreich erstellt',
121
123
  title: 'Willkommen!',
122
124
  });
123
125
 
124
- // Navigate to app - passkeys can be added later in security settings
125
- // Note: Immediate passkey registration after sign-up is currently not supported
126
- // due to session handling differences between nest-server and Better Auth
127
- await navigateTo('/app', { external: true });
126
+ // Show passkey prompt after successful registration + login
127
+ showPasskeyPrompt.value = true;
128
128
  } finally {
129
129
  loading.value = false;
130
130
  }
131
131
  }
132
+
133
+ async function addPasskey(): Promise<void> {
134
+ passkeyLoading.value = true;
135
+
136
+ try {
137
+ // Use registerPasskey from composable which properly handles challengeId
138
+ const result = await registerPasskey('Mein Gerät');
139
+
140
+ if (!result.success) {
141
+ toast.add({
142
+ color: 'error',
143
+ description: result.error || 'Passkey konnte nicht hinzugefügt werden',
144
+ title: 'Fehler',
145
+ });
146
+ // Still navigate to app even if passkey failed
147
+ await navigateTo('/app');
148
+ return;
149
+ }
150
+
151
+ toast.add({
152
+ color: 'success',
153
+ description: 'Passkey wurde erfolgreich hinzugefügt',
154
+ title: 'Erfolg',
155
+ });
156
+
157
+ await navigateTo('/app');
158
+ } finally {
159
+ passkeyLoading.value = false;
160
+ }
161
+ }
162
+
163
+ async function skipPasskey(): Promise<void> {
164
+ await navigateTo('/app');
165
+ }
132
166
  </script>
133
167
 
134
168
  <template>
135
169
  <UPageCard class="w-md" variant="naked">
136
- <UAuthForm
137
- :schema="schema"
138
- title="Registrieren"
139
- icon="i-lucide-user-plus"
140
- :fields="fields"
141
- :loading="loading"
142
- :submit="{
143
- label: 'Konto erstellen',
144
- block: true,
145
- }"
146
- @submit="onSubmit"
147
- >
148
- <template #footer>
149
- <p class="text-center text-sm text-muted">
150
- Bereits ein Konto?
151
- <ULink to="/auth/login" class="text-primary font-medium">Anmelden</ULink>
152
- </p>
153
- </template>
154
- </UAuthForm>
170
+ <template v-if="!showPasskeyPrompt">
171
+ <UAuthForm
172
+ :schema="schema"
173
+ title="Registrieren"
174
+ icon="i-lucide-user-plus"
175
+ :fields="fields"
176
+ :loading="loading"
177
+ :submit="{
178
+ label: 'Konto erstellen',
179
+ block: true,
180
+ }"
181
+ @submit="onSubmit"
182
+ >
183
+ <template #footer>
184
+ <p class="text-center text-sm text-muted">
185
+ Bereits ein Konto?
186
+ <ULink to="/auth/login" class="text-primary font-medium">Anmelden</ULink>
187
+ </p>
188
+ </template>
189
+ </UAuthForm>
190
+ </template>
191
+
192
+ <template v-else>
193
+ <div class="flex flex-col items-center gap-6">
194
+ <UIcon name="i-lucide-key" class="size-16 text-primary" />
195
+ <div class="text-center">
196
+ <h2 class="text-xl font-semibold">Passkey hinzufügen?</h2>
197
+ <p class="mt-2 text-sm text-muted">Mit einem Passkey kannst du dich schnell und sicher ohne Passwort anmelden.</p>
198
+ </div>
199
+
200
+ <div class="flex w-full flex-col gap-3">
201
+ <UButton block :loading="passkeyLoading" @click="addPasskey"> Passkey hinzufügen </UButton>
202
+ <UButton block variant="outline" color="neutral" @click="skipPasskey"> Später einrichten </UButton>
203
+ </div>
204
+ </div>
205
+ </template>
155
206
  </UPageCard>
156
207
  </template>