create-nuxt-base 1.1.1 → 1.1.2

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [1.1.2](https://github.com/lenneTech/nuxt-base-starter/compare/v1.1.1...v1.1.2) (2026-01-22)
6
+
5
7
  ### [1.1.1](https://github.com/lenneTech/nuxt-base-starter/compare/v1.1.0...v1.1.1) (2026-01-20)
6
8
 
7
9
 
@@ -205,12 +205,14 @@ export function useBetterAuth() {
205
205
  isLoading.value = true;
206
206
 
207
207
  try {
208
- // Direct API access (CORS is configured on the server for trusted origins)
208
+ // In development, use the Nuxt proxy to ensure cookies are sent correctly
209
+ // In production, use the direct API URL
210
+ const isDev = import.meta.dev;
209
211
  const runtimeConfig = useRuntimeConfig();
210
- const apiUrl = runtimeConfig.public.apiUrl || 'http://localhost:3000';
212
+ const apiBase = isDev ? '/api/iam' : `${runtimeConfig.public.apiUrl || 'http://localhost:3000'}/iam`;
211
213
 
212
214
  // Step 1: Get authentication options from server
213
- const optionsResponse = await fetch(`${apiUrl}/iam/passkey/generate-authenticate-options`, {
215
+ const optionsResponse = await fetch(`${apiBase}/passkey/generate-authenticate-options`, {
214
216
  method: 'GET',
215
217
  credentials: 'include',
216
218
  });
@@ -255,11 +257,12 @@ export function useBetterAuth() {
255
257
  };
256
258
 
257
259
  // Step 5: Verify with server
258
- const authResponse = await fetch(`${apiUrl}/iam/passkey/verify-authentication`, {
260
+ // Note: The server expects { response: credentialData } format (matching @simplewebauthn/browser output)
261
+ const authResponse = await fetch(`${apiBase}/passkey/verify-authentication`, {
259
262
  method: 'POST',
260
263
  credentials: 'include',
261
264
  headers: { 'Content-Type': 'application/json' },
262
- body: JSON.stringify(credentialBody),
265
+ body: JSON.stringify({ response: credentialBody }),
263
266
  });
264
267
 
265
268
  const result = await authResponse.json();
@@ -300,12 +303,14 @@ export function useBetterAuth() {
300
303
  isLoading.value = true;
301
304
 
302
305
  try {
303
- // Direct API access (CORS is configured on the server for trusted origins)
306
+ // In development, use the Nuxt proxy to ensure cookies are sent correctly
307
+ // In production, use the direct API URL
308
+ const isDev = import.meta.dev;
304
309
  const runtimeConfig = useRuntimeConfig();
305
- const apiUrl = runtimeConfig.public.apiUrl || 'http://localhost:3000';
310
+ const apiBase = isDev ? '/api/iam' : `${runtimeConfig.public.apiUrl || 'http://localhost:3000'}/iam`;
306
311
 
307
312
  // Step 1: Get registration options from server
308
- const optionsResponse = await fetch(`${apiUrl}/iam/passkey/generate-register-options`, {
313
+ const optionsResponse = await fetch(`${apiBase}/passkey/generate-register-options`, {
309
314
  method: 'GET',
310
315
  credentials: 'include',
311
316
  });
@@ -365,7 +370,7 @@ export function useBetterAuth() {
365
370
  };
366
371
 
367
372
  // Step 6: Send to server for verification and storage
368
- const registerResponse = await fetch(`${apiUrl}/iam/passkey/verify-registration`, {
373
+ const registerResponse = await fetch(`${apiBase}/passkey/verify-registration`, {
369
374
  method: 'POST',
370
375
  credentials: 'include',
371
376
  headers: { 'Content-Type': 'application/json' },
@@ -1,6 +1,13 @@
1
1
  <script setup lang="ts">
2
2
  import type { NavigationMenuItem } from '@nuxt/ui';
3
3
 
4
+ const { isAuthenticated, signOut, user } = useBetterAuth();
5
+
6
+ async function handleLogout() {
7
+ await signOut();
8
+ await navigateTo('/auth/login');
9
+ }
10
+
4
11
  const headerItems = computed<NavigationMenuItem[]>(() => [
5
12
  {
6
13
  label: 'Docs',
@@ -51,6 +58,16 @@ const footerItems: NavigationMenuItem[] = [
51
58
  <UNavigationMenu :items="headerItems" />
52
59
 
53
60
  <template #right>
61
+ <template v-if="isAuthenticated">
62
+ <span class="text-sm text-muted hidden sm:inline">{{ user?.email }}</span>
63
+ <UTooltip text="Logout">
64
+ <UButton color="neutral" variant="ghost" icon="i-lucide-log-out" aria-label="Logout" @click="handleLogout" />
65
+ </UTooltip>
66
+ </template>
67
+ <template v-else>
68
+ <UButton color="primary" variant="soft" to="/auth/login" icon="i-lucide-log-in" label="Login" />
69
+ </template>
70
+
54
71
  <UColorModeButton />
55
72
 
56
73
  <UTooltip text="Open on GitHub" :kbds="['meta', 'G']">
@@ -7,6 +7,8 @@ import type { InferOutput } from 'valibot';
7
7
 
8
8
  import * as v from 'valibot';
9
9
 
10
+ import { authClient } from '~/lib/auth-client';
11
+
10
12
  // ============================================================================
11
13
  // Composables
12
14
  // ============================================================================
@@ -24,6 +26,8 @@ definePageMeta({
24
26
  // Variables
25
27
  // ============================================================================
26
28
  const loading = ref<boolean>(false);
29
+ const showPasskeyPrompt = ref<boolean>(false);
30
+ const passkeyLoading = ref<boolean>(false);
27
31
 
28
32
  const fields: AuthFormField[] = [
29
33
  {
@@ -117,40 +121,90 @@ async function onSubmit(payload: FormSubmitEvent<Schema>): Promise<void> {
117
121
 
118
122
  toast.add({
119
123
  color: 'success',
120
- description: 'Dein Konto wurde erfolgreich erstellt. Du kannst Passkeys später in den Sicherheitseinstellungen hinzufügen.',
124
+ description: 'Dein Konto wurde erfolgreich erstellt',
121
125
  title: 'Willkommen!',
122
126
  });
123
127
 
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 });
128
+ // Show passkey prompt after successful registration + login
129
+ showPasskeyPrompt.value = true;
128
130
  } finally {
129
131
  loading.value = false;
130
132
  }
131
133
  }
134
+
135
+ async function addPasskey(): Promise<void> {
136
+ passkeyLoading.value = true;
137
+
138
+ try {
139
+ const { error } = await authClient.passkey.addPasskey({
140
+ name: 'Mein Gerät',
141
+ });
142
+
143
+ if (error) {
144
+ toast.add({
145
+ color: 'error',
146
+ description: error.message || 'Passkey konnte nicht hinzugefügt werden',
147
+ title: 'Fehler',
148
+ });
149
+ // Still navigate to app even if passkey failed
150
+ await navigateTo('/app');
151
+ return;
152
+ }
153
+
154
+ toast.add({
155
+ color: 'success',
156
+ description: 'Passkey wurde erfolgreich hinzugefügt',
157
+ title: 'Erfolg',
158
+ });
159
+
160
+ await navigateTo('/app');
161
+ } finally {
162
+ passkeyLoading.value = false;
163
+ }
164
+ }
165
+
166
+ async function skipPasskey(): Promise<void> {
167
+ await navigateTo('/app');
168
+ }
132
169
  </script>
133
170
 
134
171
  <template>
135
172
  <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>
173
+ <template v-if="!showPasskeyPrompt">
174
+ <UAuthForm
175
+ :schema="schema"
176
+ title="Registrieren"
177
+ icon="i-lucide-user-plus"
178
+ :fields="fields"
179
+ :loading="loading"
180
+ :submit="{
181
+ label: 'Konto erstellen',
182
+ block: true,
183
+ }"
184
+ @submit="onSubmit"
185
+ >
186
+ <template #footer>
187
+ <p class="text-center text-sm text-muted">
188
+ Bereits ein Konto?
189
+ <ULink to="/auth/login" class="text-primary font-medium">Anmelden</ULink>
190
+ </p>
191
+ </template>
192
+ </UAuthForm>
193
+ </template>
194
+
195
+ <template v-else>
196
+ <div class="flex flex-col items-center gap-6">
197
+ <UIcon name="i-lucide-key" class="size-16 text-primary" />
198
+ <div class="text-center">
199
+ <h2 class="text-xl font-semibold">Passkey hinzufügen?</h2>
200
+ <p class="mt-2 text-sm text-muted">Mit einem Passkey kannst du dich schnell und sicher ohne Passwort anmelden.</p>
201
+ </div>
202
+
203
+ <div class="flex w-full flex-col gap-3">
204
+ <UButton block :loading="passkeyLoading" @click="addPasskey"> Passkey hinzufügen </UButton>
205
+ <UButton block variant="outline" color="neutral" @click="skipPasskey"> Später einrichten </UButton>
206
+ </div>
207
+ </div>
208
+ </template>
155
209
  </UPageCard>
156
210
  </template>
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Auth Interceptor Plugin
3
+ *
4
+ * This plugin intercepts all API responses and handles session expiration.
5
+ * When a 401 (Unauthorized) response is received, it automatically:
6
+ * 1. Clears the user session state
7
+ * 2. Redirects to the login page
8
+ *
9
+ * Note: This is a client-only plugin (.client.ts) since auth state
10
+ * management only makes sense in the browser context.
11
+ */
12
+ export default defineNuxtPlugin(() => {
13
+ const { clearUser, isAuthenticated } = useBetterAuth();
14
+ const route = useRoute();
15
+
16
+ // Track if we're already handling a 401 to prevent multiple redirects
17
+ let isHandling401 = false;
18
+
19
+ // Paths that should not trigger auto-logout on 401
20
+ // (public auth endpoints where 401 is expected)
21
+ const publicAuthPaths = [
22
+ '/auth/login',
23
+ '/auth/register',
24
+ '/auth/forgot-password',
25
+ '/auth/reset-password',
26
+ '/auth/2fa',
27
+ ];
28
+
29
+ /**
30
+ * Check if current route is a public auth route
31
+ */
32
+ function isPublicAuthRoute(): boolean {
33
+ return publicAuthPaths.some((path) => route.path.startsWith(path));
34
+ }
35
+
36
+ /**
37
+ * Check if URL is an auth-related endpoint that shouldn't trigger logout
38
+ * (e.g., login, register, password reset endpoints)
39
+ */
40
+ function isAuthEndpoint(url: string): boolean {
41
+ const authEndpoints = [
42
+ '/sign-in',
43
+ '/sign-up',
44
+ '/sign-out',
45
+ '/forgot-password',
46
+ '/reset-password',
47
+ '/verify-email',
48
+ '/session',
49
+ ];
50
+ return authEndpoints.some((endpoint) => url.includes(endpoint));
51
+ }
52
+
53
+ /**
54
+ * Handle 401 Unauthorized responses
55
+ * Clears user state and redirects to login page
56
+ */
57
+ async function handleUnauthorized(requestUrl?: string): Promise<void> {
58
+ // Prevent multiple simultaneous 401 handling
59
+ if (isHandling401) {
60
+ return;
61
+ }
62
+
63
+ // Don't handle 401 for auth endpoints (expected behavior)
64
+ if (requestUrl && isAuthEndpoint(requestUrl)) {
65
+ return;
66
+ }
67
+
68
+ // Don't handle 401 on public auth pages
69
+ if (isPublicAuthRoute()) {
70
+ return;
71
+ }
72
+
73
+ isHandling401 = true;
74
+
75
+ try {
76
+ // Only handle if user was authenticated (prevents redirect loops)
77
+ if (isAuthenticated.value) {
78
+ console.debug('[Auth Interceptor] Session expired, logging out...');
79
+
80
+ // Clear user state
81
+ clearUser();
82
+
83
+ // Redirect to login page with return URL
84
+ await navigateTo({
85
+ path: '/auth/login',
86
+ query: {
87
+ redirect: route.fullPath !== '/auth/login' ? route.fullPath : undefined,
88
+ },
89
+ }, {
90
+ replace: true,
91
+ });
92
+ }
93
+ } finally {
94
+ // Reset flag after a short delay to allow navigation to complete
95
+ setTimeout(() => {
96
+ isHandling401 = false;
97
+ }, 1000);
98
+ }
99
+ }
100
+
101
+ // Override the default $fetch to add response error handling
102
+ const originalFetch = globalThis.$fetch;
103
+
104
+ // Use a wrapper to intercept responses
105
+ globalThis.$fetch = ((url: string, options?: any) => {
106
+ return originalFetch(url, {
107
+ ...options,
108
+ onResponseError: (context: any) => {
109
+ // Call original onResponseError if provided
110
+ if (options?.onResponseError) {
111
+ options.onResponseError(context);
112
+ }
113
+
114
+ // Handle 401 errors
115
+ if (context.response?.status === 401) {
116
+ handleUnauthorized(url);
117
+ }
118
+ },
119
+ });
120
+ }) as typeof globalThis.$fetch;
121
+
122
+ // Also intercept native fetch for manual API calls
123
+ const originalNativeFetch = globalThis.fetch;
124
+
125
+ globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
126
+ const response = await originalNativeFetch(input, init);
127
+
128
+ // Handle 401 errors from native fetch
129
+ if (response.status === 401) {
130
+ const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
131
+ handleUnauthorized(url);
132
+ }
133
+
134
+ return response;
135
+ };
136
+
137
+ // Provide a manual method to trigger logout on 401
138
+ return {
139
+ provide: {
140
+ handleUnauthorized,
141
+ },
142
+ };
143
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nuxt-base",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "Starter to generate a configured environment with VueJS, Nuxt, Tailwind, Eslint, Unit Tests, Playwright etc.",
5
5
  "license": "MIT",
6
6
  "repository": {