create-nuxt-base 2.1.3 → 2.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
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
+ ## [2.2.0](https://github.com/lenneTech/nuxt-base-starter/compare/v2.1.4...v2.2.0) (2026-02-04)
6
+
7
+
8
+ ### Features
9
+
10
+ * improve registration and login with email verification, terms checkbox, and TS fixes ([dee6b95](https://github.com/lenneTech/nuxt-base-starter/commit/dee6b95b29890bdccf09570192f869397694b61f))
11
+
12
+ ### [2.1.4](https://github.com/lenneTech/nuxt-base-starter/compare/v2.1.3...v2.1.4) (2026-01-26)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * generate QR code locally as SVG for password manager compatibility ([f4256e9](https://github.com/lenneTech/nuxt-base-starter/commit/f4256e94baf839b800cd5420c49ad9ed14a50f01))
18
+
5
19
  ### [2.1.3](https://github.com/lenneTech/nuxt-base-starter/compare/v2.1.2...v2.1.3) (2026-01-26)
6
20
 
7
21
 
@@ -1,7 +1,11 @@
1
1
  <script setup lang="ts">
2
2
  import type { NavigationMenuItem } from '@nuxt/ui';
3
3
 
4
- const { isAuthenticated, signOut, user } = useLtAuth();
4
+ const { isAuthenticated, signOut, user, validateSession } = useLtAuth();
5
+
6
+ onMounted(() => {
7
+ validateSession();
8
+ });
5
9
 
6
10
  async function handleLogout() {
7
11
  await signOut();
@@ -5,6 +5,7 @@
5
5
  import type { FormSubmitEvent } from '@nuxt/ui';
6
6
  import type { InferOutput } from 'valibot';
7
7
 
8
+ import QRCode from 'qrcode';
8
9
  import * as v from 'valibot';
9
10
 
10
11
  import ModalBackupCodes from '~/components/Modal/ModalBackupCodes.vue';
@@ -31,6 +32,7 @@ const authClient = useLtAuthClient();
31
32
  // ============================================================================
32
33
  const loading = ref<boolean>(false);
33
34
  const totpUri = ref<string>('');
35
+ const qrCodeSvg = ref<string>('');
34
36
  const backupCodes = ref<string[]>([]);
35
37
  const showTotpSetup = ref<boolean>(false);
36
38
  const show2FADisable = ref<boolean>(false);
@@ -184,6 +186,17 @@ async function enable2FA(payload: FormSubmitEvent<PasswordSchema>): Promise<void
184
186
 
185
187
  totpUri.value = data?.totpURI ?? '';
186
188
  backupCodes.value = data?.backupCodes ?? [];
189
+
190
+ // Generate QR code as SVG for better password manager compatibility
191
+ if (totpUri.value) {
192
+ qrCodeSvg.value = await QRCode.toString(totpUri.value, {
193
+ type: 'svg',
194
+ width: 200,
195
+ margin: 2,
196
+ errorCorrectionLevel: 'M',
197
+ });
198
+ }
199
+
187
200
  showTotpSetup.value = true;
188
201
  enable2FAForm.password = '';
189
202
  } finally {
@@ -209,6 +222,13 @@ async function loadPasskeys(): Promise<void> {
209
222
  }
210
223
  }
211
224
 
225
+ function resetTotpSetup(): void {
226
+ showTotpSetup.value = false;
227
+ totpForm.code = '';
228
+ totpUri.value = '';
229
+ qrCodeSvg.value = '';
230
+ }
231
+
212
232
  async function openBackupCodesModal(codes: string[] = []): Promise<void> {
213
233
  const modal = overlay.create(ModalBackupCodes, {
214
234
  props: {
@@ -246,8 +266,7 @@ async function verifyTotp(payload: FormSubmitEvent<TotpSchema>): Promise<void> {
246
266
  title: 'Erfolg',
247
267
  });
248
268
 
249
- showTotpSetup.value = false;
250
- totpForm.code = '';
269
+ resetTotpSetup();
251
270
  await openBackupCodesModal(backupCodes.value);
252
271
  } finally {
253
272
  loading.value = false;
@@ -301,7 +320,8 @@ async function verifyTotp(payload: FormSubmitEvent<TotpSchema>): Promise<void> {
301
320
  <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>
302
321
 
303
322
  <div class="flex justify-center">
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" />
323
+ <!-- eslint-disable-next-line vue/no-v-html -->
324
+ <div v-if="qrCodeSvg" class="rounded-lg bg-white p-2" v-html="qrCodeSvg" />
305
325
  </div>
306
326
 
307
327
  <UForm :schema="totpSchema" :state="totpForm" class="space-y-4" @submit="verifyTotp">
@@ -310,7 +330,7 @@ async function verifyTotp(payload: FormSubmitEvent<TotpSchema>): Promise<void> {
310
330
  </UFormField>
311
331
  <div class="flex gap-2">
312
332
  <UButton type="submit" :loading="loading"> Verifizieren </UButton>
313
- <UButton variant="outline" color="neutral" @click="showTotpSetup = false"> Abbrechen </UButton>
333
+ <UButton variant="outline" color="neutral" @click="resetTotpSetup"> Abbrechen </UButton>
314
334
  </div>
315
335
  </UForm>
316
336
  </div>
@@ -325,7 +345,7 @@ async function verifyTotp(payload: FormSubmitEvent<TotpSchema>): Promise<void> {
325
345
 
326
346
  <template v-if="show2FADisable">
327
347
  <UForm :schema="passwordSchema" :state="disable2FAForm" class="space-y-4" @submit="disable2FA">
328
- <UAlert color="warning" icon="i-lucide-alert-triangle"> 2FA zu deaktivieren verringert die Sicherheit deines Kontos. </UAlert>
348
+ <UAlert color="warning" icon="i-lucide-alert-triangle" description="2FA zu deaktivieren verringert die Sicherheit deines Kontos." />
329
349
  <UFormField label="Passwort bestätigen" name="password">
330
350
  <UInput v-model="disable2FAForm.password" type="password" placeholder="Dein Passwort" />
331
351
  </UFormField>
@@ -361,7 +381,7 @@ async function verifyTotp(payload: FormSubmitEvent<TotpSchema>): Promise<void> {
361
381
  <p class="text-xs text-muted">Erstellt am {{ new Date(passkey.createdAt).toLocaleDateString('de-DE') }}</p>
362
382
  </div>
363
383
  </div>
364
- <UButton variant="ghost" color="error" icon="i-lucide-trash-2" size="sm" :loading="passkeyLoading" @click="deletePasskey(passkey.id)" />
384
+ <UButton variant="outline" color="error" icon="i-lucide-trash" size="sm" :loading="passkeyLoading" @click="deletePasskey(passkey.id)">Löschen</UButton>
365
385
  </div>
366
386
  </div>
367
387
  </template>
@@ -7,8 +7,8 @@ import type { InferOutput } from 'valibot';
7
7
 
8
8
  import * as v from 'valibot';
9
9
 
10
- // Auth client from @lenne.tech/nuxt-extensions (auto-imported as ltAuthClient)
11
- const authClient = ltAuthClient;
10
+ // Auth client from @lenne.tech/nuxt-extensions
11
+ const authClient = useLtAuthClient();
12
12
 
13
13
  // ============================================================================
14
14
  // Composables
@@ -7,11 +7,30 @@ import type { InferOutput } from 'valibot';
7
7
 
8
8
  import * as v from 'valibot';
9
9
 
10
+ // ============================================================================
11
+ // Types
12
+ // ============================================================================
13
+ interface SignInResponse {
14
+ data?: {
15
+ redirect?: boolean;
16
+ requiresTwoFactor?: boolean;
17
+ token?: string | null;
18
+ twoFactorRedirect?: boolean;
19
+ url?: string;
20
+ user?: Record<string, unknown>;
21
+ } | null;
22
+ error?: {
23
+ code?: string;
24
+ message?: string;
25
+ status?: number;
26
+ } | null;
27
+ }
28
+
10
29
  // ============================================================================
11
30
  // Composables
12
31
  // ============================================================================
13
32
  const toast = useToast();
14
- const { signIn, setUser, isLoading, validateSession, authenticateWithPasskey } = useLtAuth();
33
+ const { signIn, setUser, validateSession, authenticateWithPasskey } = useLtAuth();
15
34
  const { translateError } = useLtErrorTranslation();
16
35
 
17
36
  // ============================================================================
@@ -108,14 +127,26 @@ async function onSubmit(payload: FormSubmitEvent<Schema>): Promise<void> {
108
127
  loading.value = true;
109
128
 
110
129
  try {
111
- const result = await signIn.email({
130
+ const result = (await signIn.email({
112
131
  email: payload.data.email,
113
132
  password: payload.data.password,
114
- });
133
+ })) as SignInResponse;
115
134
 
116
135
  // Check for error in response
117
- if ('error' in result && result.error) {
118
- const errorMessage = (result.error as { message?: string }).message || 'Anmeldung fehlgeschlagen';
136
+ if (result.error) {
137
+ const errorMessage = result.error.message || 'Anmeldung fehlgeschlagen';
138
+
139
+ // Check if email verification is required → redirect to verify-email page
140
+ if (errorMessage.includes('LTNS_0023') || errorMessage.toLowerCase().includes('email verification required')) {
141
+ toast.add({
142
+ color: 'warning',
143
+ description: 'Bitte bestätige zuerst deine E-Mail-Adresse.',
144
+ title: 'E-Mail nicht verifiziert',
145
+ });
146
+ await navigateTo({ path: '/auth/verify-email', query: { email: payload.data.email } });
147
+ return;
148
+ }
149
+
119
150
  toast.add({
120
151
  color: 'error',
121
152
  description: translateError(errorMessage),
@@ -126,8 +157,8 @@ async function onSubmit(payload: FormSubmitEvent<Schema>): Promise<void> {
126
157
 
127
158
  // Check if 2FA is required
128
159
  // Better-Auth native uses 'twoFactorRedirect', nest-server REST API uses 'requiresTwoFactor'
129
- const resultData = 'data' in result ? result.data : result;
130
- const requires2FA = resultData && (('twoFactorRedirect' in resultData && resultData.twoFactorRedirect) || ('requiresTwoFactor' in resultData && resultData.requiresTwoFactor));
160
+ const resultData = result.data as Record<string, unknown> | null | undefined;
161
+ const requires2FA = resultData && (resultData.twoFactorRedirect || resultData.requiresTwoFactor || resultData.redirect);
131
162
  if (requires2FA) {
132
163
  // Redirect to 2FA page
133
164
  await navigateTo('/auth/2fa');
@@ -135,7 +166,7 @@ async function onSubmit(payload: FormSubmitEvent<Schema>): Promise<void> {
135
166
  }
136
167
 
137
168
  // Check if login was successful (user data in response)
138
- const userData = 'user' in result ? result.user : 'data' in result ? result.data?.user : null;
169
+ const userData = result.data?.user;
139
170
  if (userData) {
140
171
  // Auth state is already stored by useLtAuth
141
172
  // Navigate to app
@@ -7,11 +7,26 @@ import type { InferOutput } from 'valibot';
7
7
 
8
8
  import * as v from 'valibot';
9
9
 
10
+ // ============================================================================
11
+ // Types
12
+ // ============================================================================
13
+ interface AuthResponse {
14
+ data?: {
15
+ token?: string | null;
16
+ user?: Record<string, unknown>;
17
+ } | null;
18
+ error?: {
19
+ code?: string;
20
+ message?: string;
21
+ status?: number;
22
+ } | null;
23
+ }
24
+
10
25
  // ============================================================================
11
26
  // Composables
12
27
  // ============================================================================
13
28
  const toast = useToast();
14
- const { signUp, signIn, registerPasskey } = useLtAuth();
29
+ const { signUp, signIn, registerPasskey, features, clearUser } = useLtAuth();
15
30
  const { translateError } = useLtErrorTranslation();
16
31
 
17
32
  // ============================================================================
@@ -28,7 +43,9 @@ const loading = ref<boolean>(false);
28
43
  const showPasskeyPrompt = ref<boolean>(false);
29
44
  const passkeyLoading = ref<boolean>(false);
30
45
 
31
- const fields: AuthFormField[] = [
46
+ const requireTerms = computed(() => features.value.signUpChecks === true);
47
+
48
+ const baseFields: AuthFormField[] = [
32
49
  {
33
50
  label: 'Name',
34
51
  name: 'name',
@@ -59,12 +76,41 @@ const fields: AuthFormField[] = [
59
76
  },
60
77
  ];
61
78
 
62
- const schema = v.pipe(
79
+ const fields = computed<AuthFormField[]>(() => {
80
+ if (!requireTerms.value) {
81
+ return baseFields;
82
+ }
83
+ return [
84
+ ...baseFields,
85
+ {
86
+ label: '',
87
+ name: 'termsAccepted',
88
+ required: true,
89
+ type: 'checkbox',
90
+ },
91
+ ];
92
+ });
93
+
94
+ const baseSchema = v.pipe(
95
+ v.object({
96
+ confirmPassword: v.pipe(v.string('Passwortbestätigung ist erforderlich'), v.minLength(8, 'Mindestens 8 Zeichen erforderlich')),
97
+ email: v.pipe(v.string('E-Mail ist erforderlich'), v.email('Bitte eine gültige E-Mail eingeben')),
98
+ name: v.pipe(v.string('Name ist erforderlich'), v.minLength(2, 'Mindestens 2 Zeichen erforderlich')),
99
+ password: v.pipe(v.string('Passwort ist erforderlich'), v.minLength(8, 'Mindestens 8 Zeichen erforderlich')),
100
+ }),
101
+ v.forward(
102
+ v.partialCheck([['password'], ['confirmPassword']], (input) => input.password === input.confirmPassword, 'Passwörter stimmen nicht überein'),
103
+ ['confirmPassword'],
104
+ ),
105
+ );
106
+
107
+ const termsSchema = v.pipe(
63
108
  v.object({
64
109
  confirmPassword: v.pipe(v.string('Passwortbestätigung ist erforderlich'), v.minLength(8, 'Mindestens 8 Zeichen erforderlich')),
65
110
  email: v.pipe(v.string('E-Mail ist erforderlich'), v.email('Bitte eine gültige E-Mail eingeben')),
66
111
  name: v.pipe(v.string('Name ist erforderlich'), v.minLength(2, 'Mindestens 2 Zeichen erforderlich')),
67
112
  password: v.pipe(v.string('Passwort ist erforderlich'), v.minLength(8, 'Mindestens 8 Zeichen erforderlich')),
113
+ termsAccepted: v.pipe(v.optional(v.boolean(), false), v.literal(true, 'Bitte akzeptiere die AGB und Datenschutzerklärung')),
68
114
  }),
69
115
  v.forward(
70
116
  v.partialCheck([['password'], ['confirmPassword']], (input) => input.password === input.confirmPassword, 'Passwörter stimmen nicht überein'),
@@ -72,7 +118,9 @@ const schema = v.pipe(
72
118
  ),
73
119
  );
74
120
 
75
- type Schema = InferOutput<typeof schema>;
121
+ const schema = computed(() => requireTerms.value ? termsSchema : baseSchema);
122
+
123
+ type Schema = InferOutput<typeof baseSchema> | InferOutput<typeof termsSchema>;
76
124
 
77
125
  // ============================================================================
78
126
  // Functions
@@ -82,16 +130,15 @@ async function onSubmit(payload: FormSubmitEvent<Schema>): Promise<void> {
82
130
 
83
131
  try {
84
132
  // Step 1: Sign up
85
- const signUpResult = await signUp.email({
133
+ const signUpResult = (await signUp.email({
86
134
  email: payload.data.email,
87
135
  name: payload.data.name,
88
136
  password: payload.data.password,
89
- });
137
+ ...(requireTerms.value ? { termsAndPrivacyAccepted: true } : {}),
138
+ })) as AuthResponse;
90
139
 
91
- const signUpError = 'error' in signUpResult ? signUpResult.error : null;
92
-
93
- if (signUpError) {
94
- const errorMessage = signUpError.message || 'Registrierung fehlgeschlagen';
140
+ if (signUpResult.error) {
141
+ const errorMessage = signUpResult.error.message || 'Registrierung fehlgeschlagen';
95
142
  toast.add({
96
143
  color: 'error',
97
144
  description: translateError(errorMessage),
@@ -100,13 +147,27 @@ async function onSubmit(payload: FormSubmitEvent<Schema>): Promise<void> {
100
147
  return;
101
148
  }
102
149
 
150
+ // If email verification is enabled, clear auth state and redirect to verify-email page
151
+ // The backend revokes the session, but we also clear the frontend state to prevent
152
+ // manual navigation to protected routes
153
+ if (features.value.emailVerification) {
154
+ clearUser();
155
+ toast.add({
156
+ color: 'success',
157
+ description: 'Bitte überprüfe dein Postfach und bestätige deine E-Mail-Adresse.',
158
+ title: 'Konto erstellt!',
159
+ });
160
+ await navigateTo({ path: '/auth/verify-email', query: { email: payload.data.email, fromRegister: 'true' } });
161
+ return;
162
+ }
163
+
103
164
  // Step 2: Sign in to create session (required for passkey registration)
104
- const signInResult = await signIn.email({
165
+ const signInResult = (await signIn.email({
105
166
  email: payload.data.email,
106
167
  password: payload.data.password,
107
- });
168
+ })) as AuthResponse;
108
169
 
109
- const signInError = 'error' in signInResult ? signInResult.error : null;
170
+ const signInError = signInResult.error;
110
171
 
111
172
  if (signInError) {
112
173
  // Sign-up was successful but sign-in failed - still show success and redirect
@@ -182,6 +243,19 @@ async function skipPasskey(): Promise<void> {
182
243
  }"
183
244
  @submit="onSubmit"
184
245
  >
246
+ <template v-if="requireTerms" #termsAccepted-field="{ state: formState }">
247
+ <UCheckbox v-model="formState.termsAccepted">
248
+ <template #label>
249
+ <span class="text-sm">
250
+ Ich akzeptiere die
251
+ <ULink to="/legal/terms" class="text-primary font-medium" target="_blank">AGB</ULink>
252
+ und
253
+ <ULink to="/legal/privacy" class="text-primary font-medium" target="_blank">Datenschutzerklärung</ULink>
254
+ </span>
255
+ </template>
256
+ </UCheckbox>
257
+ </template>
258
+
185
259
  <template #footer>
186
260
  <p class="text-center text-sm text-muted">
187
261
  Bereits ein Konto?
@@ -7,8 +7,8 @@ import type { InferOutput } from 'valibot';
7
7
 
8
8
  import * as v from 'valibot';
9
9
 
10
- // Auth client from @lenne.tech/nuxt-extensions (auto-imported as ltAuthClient)
11
- const authClient = ltAuthClient;
10
+ // Auth client from @lenne.tech/nuxt-extensions
11
+ const authClient = useLtAuthClient();
12
12
 
13
13
  // ============================================================================
14
14
  // Composables
@@ -0,0 +1,209 @@
1
+ <script setup lang="ts">
2
+ // ============================================================================
3
+ // Composables
4
+ // ============================================================================
5
+ const route = useRoute();
6
+ const toast = useToast();
7
+ const runtimeConfig = useRuntimeConfig();
8
+ const apiBase = import.meta.dev ? '/api/iam' : `${runtimeConfig.public.apiUrl || 'http://localhost:3000'}/iam`;
9
+
10
+ // ============================================================================
11
+ // Page Meta
12
+ // ============================================================================
13
+ definePageMeta({
14
+ layout: 'slim',
15
+ });
16
+
17
+ // ============================================================================
18
+ // Variables
19
+ // ============================================================================
20
+ const email = computed(() => (route.query.email as string) || '');
21
+ const token = computed(() => (route.query.token as string) || '');
22
+
23
+ const verifying = ref(false);
24
+ const verified = ref(false);
25
+ const verifyError = ref('');
26
+ const resending = ref(false);
27
+ const resendCooldown = ref(0);
28
+ const cooldownSeconds = ref(60);
29
+ let cooldownInterval: ReturnType<typeof setInterval> | null = null;
30
+
31
+ // ============================================================================
32
+ // Functions
33
+ // ============================================================================
34
+ async function verifyEmailWithToken(verificationToken: string): Promise<void> {
35
+ verifying.value = true;
36
+ verifyError.value = '';
37
+
38
+ try {
39
+ const result = await $fetch<{ status: boolean }>(`${apiBase}/verify-email`, {
40
+ params: { token: verificationToken },
41
+ redirect: 'manual',
42
+ });
43
+
44
+ if (!result?.status) {
45
+ verifyError.value = 'Verifizierung fehlgeschlagen';
46
+ return;
47
+ }
48
+
49
+ verified.value = true;
50
+ toast.add({
51
+ color: 'success',
52
+ description: 'Deine E-Mail-Adresse wurde erfolgreich verifiziert.',
53
+ title: 'E-Mail bestätigt',
54
+ });
55
+ } catch {
56
+ verifyError.value = 'Die Verifizierung ist fehlgeschlagen. Der Link ist möglicherweise abgelaufen.';
57
+ } finally {
58
+ verifying.value = false;
59
+ }
60
+ }
61
+
62
+ async function resendVerificationEmail(): Promise<void> {
63
+ if (!email.value || resendCooldown.value > 0) return;
64
+
65
+ resending.value = true;
66
+
67
+ try {
68
+ await $fetch(`${apiBase}/send-verification-email`, {
69
+ body: {
70
+ callbackURL: '/auth/verify-email',
71
+ email: email.value,
72
+ },
73
+ method: 'POST',
74
+ });
75
+
76
+ toast.add({
77
+ color: 'success',
78
+ description: 'Eine neue Bestätigungs-E-Mail wurde gesendet.',
79
+ title: 'E-Mail gesendet',
80
+ });
81
+
82
+ startCooldown(cooldownSeconds.value);
83
+ } catch {
84
+ toast.add({
85
+ color: 'error',
86
+ description: 'Die E-Mail konnte nicht gesendet werden. Bitte versuche es später erneut.',
87
+ title: 'Fehler',
88
+ });
89
+ } finally {
90
+ resending.value = false;
91
+ }
92
+ }
93
+
94
+ function startCooldown(seconds: number): void {
95
+ resendCooldown.value = seconds;
96
+ if (cooldownInterval) clearInterval(cooldownInterval);
97
+ cooldownInterval = setInterval(() => {
98
+ resendCooldown.value--;
99
+ if (resendCooldown.value <= 0) {
100
+ if (cooldownInterval) clearInterval(cooldownInterval);
101
+ cooldownInterval = null;
102
+ }
103
+ }, 1000);
104
+ }
105
+
106
+ // ============================================================================
107
+ // Lifecycle
108
+ // ============================================================================
109
+ onMounted(async () => {
110
+ // Fetch dynamic cooldown configuration from backend
111
+ try {
112
+ const features = await $fetch<Record<string, boolean | number | string[]>>(`${apiBase}/features`);
113
+ if (typeof features?.resendCooldownSeconds === 'number') {
114
+ cooldownSeconds.value = features.resendCooldownSeconds;
115
+ }
116
+ } catch {
117
+ // Use default cooldown if features endpoint is unavailable
118
+ }
119
+
120
+ if (token.value) {
121
+ await verifyEmailWithToken(token.value);
122
+ } else if (route.query.fromRegister === 'true') {
123
+ // Email was just sent during registration, start initial cooldown
124
+ startCooldown(cooldownSeconds.value);
125
+ }
126
+ });
127
+
128
+ onUnmounted(() => {
129
+ if (cooldownInterval) clearInterval(cooldownInterval);
130
+ });
131
+ </script>
132
+
133
+ <template>
134
+ <UPageCard class="w-md" variant="naked">
135
+ <!-- Verifying state: Token verification in progress -->
136
+ <template v-if="verifying">
137
+ <div class="flex flex-col items-center gap-6 py-4">
138
+ <UIcon name="i-lucide-loader-circle" class="size-16 animate-spin text-primary" />
139
+ <div class="text-center">
140
+ <h2 class="text-xl font-semibold">E-Mail wird verifiziert...</h2>
141
+ <p class="mt-2 text-sm text-muted">Bitte warte einen Moment.</p>
142
+ </div>
143
+ </div>
144
+ </template>
145
+
146
+ <!-- Success state: Email verified -->
147
+ <template v-else-if="verified">
148
+ <div class="flex flex-col items-center gap-6 py-4">
149
+ <UIcon name="i-lucide-check-circle" class="size-16 text-success" />
150
+ <div class="text-center">
151
+ <h2 class="text-xl font-semibold">E-Mail bestätigt</h2>
152
+ <p class="mt-2 text-sm text-muted">Deine E-Mail-Adresse wurde erfolgreich verifiziert. Du kannst dich jetzt anmelden.</p>
153
+ </div>
154
+ <UButton block to="/auth/login">Jetzt anmelden</UButton>
155
+ </div>
156
+ </template>
157
+
158
+ <!-- Error state: Verification failed -->
159
+ <template v-else-if="verifyError">
160
+ <div class="flex flex-col items-center gap-6 py-4">
161
+ <UIcon name="i-lucide-x-circle" class="size-16 text-error" />
162
+ <div class="text-center">
163
+ <h2 class="text-xl font-semibold">Verifizierung fehlgeschlagen</h2>
164
+ <p class="mt-2 text-sm text-muted">{{ verifyError }}</p>
165
+ </div>
166
+
167
+ <div class="flex w-full flex-col gap-3">
168
+ <UButton v-if="email" block :color="resendCooldown > 0 ? 'neutral' : 'primary'" :disabled="resendCooldown > 0" :loading="resending" :variant="resendCooldown > 0 ? 'outline' : 'solid'" @click="resendVerificationEmail">
169
+ {{ resendCooldown > 0 ? `Neue E-Mail senden (${resendCooldown}s)` : 'Neue E-Mail senden' }}
170
+ </UButton>
171
+ <UButton block variant="outline" color="neutral" to="/auth/login">Zurück zur Anmeldung</UButton>
172
+ </div>
173
+ </div>
174
+ </template>
175
+
176
+ <!-- Pending state: Waiting for verification (from register page) -->
177
+ <template v-else>
178
+ <div class="flex flex-col items-center gap-6 py-4">
179
+ <UIcon name="i-lucide-mail-check" class="size-16 text-primary" />
180
+ <div class="text-center">
181
+ <h2 class="text-xl font-semibold">E-Mail bestätigen</h2>
182
+ <p class="mt-2 text-sm text-muted">
183
+ Wir haben eine Bestätigungs-E-Mail an
184
+ <strong v-if="email">{{ email }}</strong>
185
+ <span v-else>deine E-Mail-Adresse</span>
186
+ gesendet.
187
+ </p>
188
+ <p class="mt-2 text-sm text-muted">Bitte klicke auf den Link in der E-Mail, um dein Konto zu aktivieren.</p>
189
+ </div>
190
+
191
+ <UAlert color="neutral" icon="i-lucide-info" title="Keine E-Mail erhalten?" variant="subtle">
192
+ <template #description>
193
+ <ul class="mt-1 list-inside list-disc text-sm">
194
+ <li>Prüfe deinen Spam-Ordner</li>
195
+ <li>Stelle sicher, dass die E-Mail-Adresse korrekt ist</li>
196
+ </ul>
197
+ </template>
198
+ </UAlert>
199
+
200
+ <div class="flex w-full flex-col gap-3">
201
+ <UButton v-if="email" block :color="resendCooldown > 0 ? 'neutral' : 'primary'" :disabled="resendCooldown > 0" :loading="resending" :variant="resendCooldown > 0 ? 'subtle' : 'outline'" @click="resendVerificationEmail">
202
+ {{ resendCooldown > 0 ? `Bestätigungs-E-Mail erneut senden (${resendCooldown}s)` : 'Bestätigungs-E-Mail erneut senden' }}
203
+ </UButton>
204
+ <UButton block variant="outline" color="neutral" to="/auth/login">Zurück zur Anmeldung</UButton>
205
+ </div>
206
+ </div>
207
+ </template>
208
+ </UPageCard>
209
+ </template>
@@ -15,6 +15,7 @@ export default defineNuxtConfig({
15
15
  // ============================================================================
16
16
  // Bug Reporting (Linear Integration via @lenne.tech/bug.lt)
17
17
  // ============================================================================
18
+ // @ts-expect-error bug.lt module config - module temporarily disabled
18
19
  bug: {
19
20
  enabled: process.env.APP_ENV !== 'production',
20
21
  linearApiKey: process.env.LINEAR_API_KEY,
@@ -60,6 +61,16 @@ export default defineNuxtConfig({
60
61
  provider: 'ipx',
61
62
  },
62
63
 
64
+ // ============================================================================
65
+ // Icon Configuration
66
+ // ============================================================================
67
+ icon: {
68
+ // Ensure dynamically rendered icons (e.g., inside v-for) are included in the bundle
69
+ clientBundle: {
70
+ icons: ['lucide:trash', 'lucide:key', 'lucide:copy'],
71
+ },
72
+ },
73
+
63
74
  // ============================================================================
64
75
  // Auto-imports
65
76
  // ============================================================================
@@ -10,17 +10,19 @@
10
10
  "@better-auth/passkey": "1.4.10",
11
11
  "@hey-api/client-fetch": "0.13.1",
12
12
  "@lenne.tech/bug.lt": "latest",
13
- "@lenne.tech/nuxt-extensions": "1.1.0",
13
+ "@lenne.tech/nuxt-extensions": "1.2.7",
14
14
  "@nuxt/image": "2.0.0",
15
15
  "@nuxt/ui": "4.3.0",
16
16
  "@pinia/nuxt": "0.11.3",
17
17
  "@vueuse/nuxt": "14.1.0",
18
18
  "better-auth": "1.4.10",
19
+ "qrcode": "1.5.4",
19
20
  "tus-js-client": "4.3.1",
20
21
  "valibot": "1.2.0"
21
22
  },
22
23
  "devDependencies": {
23
24
  "@hey-api/openapi-ts": "0.90.3",
25
+ "@iconify-json/lucide": "1.2.88",
24
26
  "@nuxt/devtools": "3.1.1",
25
27
  "@nuxt/test-utils": "3.23.0",
26
28
  "@nuxtjs/color-mode": "4.0.0",
@@ -30,6 +32,7 @@
30
32
  "@tailwindcss/typography": "0.5.19",
31
33
  "@tailwindcss/vite": "4.1.18",
32
34
  "@types/node": "25.0.6",
35
+ "@types/qrcode": "1.5.6",
33
36
  "@vitejs/plugin-vue": "^6.0.3",
34
37
  "@vue/test-utils": "^2.4.6",
35
38
  "dayjs-nuxt": "2.1.11",
@@ -1328,6 +1331,16 @@
1328
1331
  "typescript": ">=5.5.3"
1329
1332
  }
1330
1333
  },
1334
+ "node_modules/@iconify-json/lucide": {
1335
+ "version": "1.2.88",
1336
+ "resolved": "https://registry.npmjs.org/@iconify-json/lucide/-/lucide-1.2.88.tgz",
1337
+ "integrity": "sha512-QBJq+VSj3yHXoMgf+1I4guUhXA+tpxzAt46LJdTSFN6UKy254GstTh+P/a6GD4Bvyi1fCAfi5hS/yCmu0w3mNw==",
1338
+ "dev": true,
1339
+ "license": "ISC",
1340
+ "dependencies": {
1341
+ "@iconify/types": "*"
1342
+ }
1343
+ },
1331
1344
  "node_modules/@iconify/collections": {
1332
1345
  "version": "1.0.638",
1333
1346
  "license": "MIT",
@@ -2337,12 +2350,12 @@
2337
2350
  }
2338
2351
  },
2339
2352
  "node_modules/@lenne.tech/nuxt-extensions": {
2340
- "version": "1.1.0",
2341
- "resolved": "https://registry.npmjs.org/@lenne.tech/nuxt-extensions/-/nuxt-extensions-1.1.0.tgz",
2342
- "integrity": "sha512-P2PHOejJCql8Cksqym+ZceDv6lqzQvcFavfrjdZyDcNBSy8Ee9xE7AMPRrVmnVcZAqF422A+fjpGVQLt+w42pw==",
2353
+ "version": "1.2.7",
2354
+ "resolved": "https://registry.npmjs.org/@lenne.tech/nuxt-extensions/-/nuxt-extensions-1.2.7.tgz",
2355
+ "integrity": "sha512-NgReTI9b/7GDiBFBkMyxbNlcyIbGK4MOibEar8E0HLs+GScpVXON0vBvZ9f4JDAhrI9MvGXDjH17hRqq8NwXvw==",
2343
2356
  "license": "MIT",
2344
2357
  "dependencies": {
2345
- "@nuxt/kit": "4.2.2"
2358
+ "@nuxt/kit": "4.3.0"
2346
2359
  },
2347
2360
  "peerDependencies": {
2348
2361
  "@better-auth/passkey": ">=1.0.0",
@@ -2367,12 +2380,12 @@
2367
2380
  }
2368
2381
  },
2369
2382
  "node_modules/@lenne.tech/nuxt-extensions/node_modules/@nuxt/kit": {
2370
- "version": "4.2.2",
2371
- "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.2.2.tgz",
2372
- "integrity": "sha512-ZAgYBrPz/yhVgDznBNdQj2vhmOp31haJbO0I0iah/P9atw+OHH7NJLUZ3PK+LOz/0fblKTN1XJVSi8YQ1TQ0KA==",
2383
+ "version": "4.3.0",
2384
+ "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.3.0.tgz",
2385
+ "integrity": "sha512-cD/0UU9RQmlnTbmyJTDyzN8f6CzpziDLv3tFQCnwl0Aoxt3KmFu4k/XA4Sogxqj7jJ/3cdX1kL+Lnsh34sxcQQ==",
2373
2386
  "license": "MIT",
2374
2387
  "dependencies": {
2375
- "c12": "^3.3.2",
2388
+ "c12": "^3.3.3",
2376
2389
  "consola": "^3.4.2",
2377
2390
  "defu": "^6.1.4",
2378
2391
  "destr": "^2.0.5",
@@ -2389,8 +2402,8 @@
2389
2402
  "scule": "^1.3.0",
2390
2403
  "semver": "^7.7.3",
2391
2404
  "tinyglobby": "^0.2.15",
2392
- "ufo": "^1.6.1",
2393
- "unctx": "^2.4.1",
2405
+ "ufo": "^1.6.3",
2406
+ "unctx": "^2.5.0",
2394
2407
  "untyped": "^2.0.0"
2395
2408
  },
2396
2409
  "engines": {
@@ -7185,6 +7198,16 @@
7185
7198
  "version": "7.0.3",
7186
7199
  "license": "MIT"
7187
7200
  },
7201
+ "node_modules/@types/qrcode": {
7202
+ "version": "1.5.6",
7203
+ "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz",
7204
+ "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==",
7205
+ "dev": true,
7206
+ "license": "MIT",
7207
+ "dependencies": {
7208
+ "@types/node": "*"
7209
+ }
7210
+ },
7188
7211
  "node_modules/@types/resolve": {
7189
7212
  "version": "1.20.2",
7190
7213
  "license": "MIT"
@@ -8556,6 +8579,15 @@
8556
8579
  "node": ">=8"
8557
8580
  }
8558
8581
  },
8582
+ "node_modules/camelcase": {
8583
+ "version": "5.3.1",
8584
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
8585
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
8586
+ "license": "MIT",
8587
+ "engines": {
8588
+ "node": ">=6"
8589
+ }
8590
+ },
8559
8591
  "node_modules/camelize": {
8560
8592
  "version": "1.0.1",
8561
8593
  "dev": true,
@@ -9341,6 +9373,15 @@
9341
9373
  }
9342
9374
  }
9343
9375
  },
9376
+ "node_modules/decamelize": {
9377
+ "version": "1.2.0",
9378
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
9379
+ "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
9380
+ "license": "MIT",
9381
+ "engines": {
9382
+ "node": ">=0.10.0"
9383
+ }
9384
+ },
9344
9385
  "node_modules/decimal.js": {
9345
9386
  "version": "10.6.0",
9346
9387
  "dev": true,
@@ -9445,6 +9486,12 @@
9445
9486
  "node": ">=0.3.1"
9446
9487
  }
9447
9488
  },
9489
+ "node_modules/dijkstrajs": {
9490
+ "version": "1.0.3",
9491
+ "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
9492
+ "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
9493
+ "license": "MIT"
9494
+ },
9448
9495
  "node_modules/dom-serializer": {
9449
9496
  "version": "2.0.0",
9450
9497
  "license": "MIT",
@@ -9981,6 +10028,19 @@
9981
10028
  "node": ">=8"
9982
10029
  }
9983
10030
  },
10031
+ "node_modules/find-up": {
10032
+ "version": "4.1.0",
10033
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
10034
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
10035
+ "license": "MIT",
10036
+ "dependencies": {
10037
+ "locate-path": "^5.0.0",
10038
+ "path-exists": "^4.0.0"
10039
+ },
10040
+ "engines": {
10041
+ "node": ">=8"
10042
+ }
10043
+ },
9984
10044
  "node_modules/fontaine": {
9985
10045
  "version": "0.7.0",
9986
10046
  "license": "MIT",
@@ -11580,6 +11640,18 @@
11580
11640
  "url": "https://github.com/sponsors/antfu"
11581
11641
  }
11582
11642
  },
11643
+ "node_modules/locate-path": {
11644
+ "version": "5.0.0",
11645
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
11646
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
11647
+ "license": "MIT",
11648
+ "dependencies": {
11649
+ "p-locate": "^4.1.0"
11650
+ },
11651
+ "engines": {
11652
+ "node": ">=8"
11653
+ }
11654
+ },
11583
11655
  "node_modules/lodash": {
11584
11656
  "version": "4.17.23",
11585
11657
  "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
@@ -13573,6 +13645,42 @@
13573
13645
  }
13574
13646
  }
13575
13647
  },
13648
+ "node_modules/p-limit": {
13649
+ "version": "2.3.0",
13650
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
13651
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
13652
+ "license": "MIT",
13653
+ "dependencies": {
13654
+ "p-try": "^2.0.0"
13655
+ },
13656
+ "engines": {
13657
+ "node": ">=6"
13658
+ },
13659
+ "funding": {
13660
+ "url": "https://github.com/sponsors/sindresorhus"
13661
+ }
13662
+ },
13663
+ "node_modules/p-locate": {
13664
+ "version": "4.1.0",
13665
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
13666
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
13667
+ "license": "MIT",
13668
+ "dependencies": {
13669
+ "p-limit": "^2.2.0"
13670
+ },
13671
+ "engines": {
13672
+ "node": ">=8"
13673
+ }
13674
+ },
13675
+ "node_modules/p-try": {
13676
+ "version": "2.2.0",
13677
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
13678
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
13679
+ "license": "MIT",
13680
+ "engines": {
13681
+ "node": ">=6"
13682
+ }
13683
+ },
13576
13684
  "node_modules/package-json-from-dist": {
13577
13685
  "version": "1.0.1",
13578
13686
  "license": "BlueOak-1.0.0"
@@ -13656,6 +13764,15 @@
13656
13764
  "version": "1.0.1",
13657
13765
  "license": "MIT"
13658
13766
  },
13767
+ "node_modules/path-exists": {
13768
+ "version": "4.0.0",
13769
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
13770
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
13771
+ "license": "MIT",
13772
+ "engines": {
13773
+ "node": ">=8"
13774
+ }
13775
+ },
13659
13776
  "node_modules/path-key": {
13660
13777
  "version": "3.1.1",
13661
13778
  "license": "MIT",
@@ -13784,6 +13901,15 @@
13784
13901
  "node": ">=18"
13785
13902
  }
13786
13903
  },
13904
+ "node_modules/pngjs": {
13905
+ "version": "5.0.0",
13906
+ "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
13907
+ "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
13908
+ "license": "MIT",
13909
+ "engines": {
13910
+ "node": ">=10.13.0"
13911
+ }
13912
+ },
13787
13913
  "node_modules/postcss": {
13788
13914
  "version": "8.5.6",
13789
13915
  "funding": [
@@ -14526,6 +14652,90 @@
14526
14652
  "node": ">=16.0.0"
14527
14653
  }
14528
14654
  },
14655
+ "node_modules/qrcode": {
14656
+ "version": "1.5.4",
14657
+ "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
14658
+ "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
14659
+ "license": "MIT",
14660
+ "peer": true,
14661
+ "dependencies": {
14662
+ "dijkstrajs": "^1.0.1",
14663
+ "pngjs": "^5.0.0",
14664
+ "yargs": "^15.3.1"
14665
+ },
14666
+ "bin": {
14667
+ "qrcode": "bin/qrcode"
14668
+ },
14669
+ "engines": {
14670
+ "node": ">=10.13.0"
14671
+ }
14672
+ },
14673
+ "node_modules/qrcode/node_modules/cliui": {
14674
+ "version": "6.0.0",
14675
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
14676
+ "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
14677
+ "license": "ISC",
14678
+ "dependencies": {
14679
+ "string-width": "^4.2.0",
14680
+ "strip-ansi": "^6.0.0",
14681
+ "wrap-ansi": "^6.2.0"
14682
+ }
14683
+ },
14684
+ "node_modules/qrcode/node_modules/wrap-ansi": {
14685
+ "version": "6.2.0",
14686
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
14687
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
14688
+ "license": "MIT",
14689
+ "dependencies": {
14690
+ "ansi-styles": "^4.0.0",
14691
+ "string-width": "^4.1.0",
14692
+ "strip-ansi": "^6.0.0"
14693
+ },
14694
+ "engines": {
14695
+ "node": ">=8"
14696
+ }
14697
+ },
14698
+ "node_modules/qrcode/node_modules/y18n": {
14699
+ "version": "4.0.3",
14700
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
14701
+ "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
14702
+ "license": "ISC"
14703
+ },
14704
+ "node_modules/qrcode/node_modules/yargs": {
14705
+ "version": "15.4.1",
14706
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
14707
+ "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
14708
+ "license": "MIT",
14709
+ "dependencies": {
14710
+ "cliui": "^6.0.0",
14711
+ "decamelize": "^1.2.0",
14712
+ "find-up": "^4.1.0",
14713
+ "get-caller-file": "^2.0.1",
14714
+ "require-directory": "^2.1.1",
14715
+ "require-main-filename": "^2.0.0",
14716
+ "set-blocking": "^2.0.0",
14717
+ "string-width": "^4.2.0",
14718
+ "which-module": "^2.0.0",
14719
+ "y18n": "^4.0.0",
14720
+ "yargs-parser": "^18.1.2"
14721
+ },
14722
+ "engines": {
14723
+ "node": ">=8"
14724
+ }
14725
+ },
14726
+ "node_modules/qrcode/node_modules/yargs-parser": {
14727
+ "version": "18.1.3",
14728
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
14729
+ "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
14730
+ "license": "ISC",
14731
+ "dependencies": {
14732
+ "camelcase": "^5.0.0",
14733
+ "decamelize": "^1.2.0"
14734
+ },
14735
+ "engines": {
14736
+ "node": ">=6"
14737
+ }
14738
+ },
14529
14739
  "node_modules/quansync": {
14530
14740
  "version": "0.2.11",
14531
14741
  "funding": [
@@ -14722,6 +14932,12 @@
14722
14932
  "node": ">=0.10.0"
14723
14933
  }
14724
14934
  },
14935
+ "node_modules/require-main-filename": {
14936
+ "version": "2.0.0",
14937
+ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
14938
+ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
14939
+ "license": "ISC"
14940
+ },
14725
14941
  "node_modules/requires-port": {
14726
14942
  "version": "1.0.0",
14727
14943
  "license": "MIT"
@@ -15118,6 +15334,12 @@
15118
15334
  "url": "https://opencollective.com/express"
15119
15335
  }
15120
15336
  },
15337
+ "node_modules/set-blocking": {
15338
+ "version": "2.0.0",
15339
+ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
15340
+ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
15341
+ "license": "ISC"
15342
+ },
15121
15343
  "node_modules/set-cookie-parser": {
15122
15344
  "version": "2.7.2",
15123
15345
  "license": "MIT"
@@ -17617,6 +17839,12 @@
17617
17839
  "node": "^18.17.0 || >=20.5.0"
17618
17840
  }
17619
17841
  },
17842
+ "node_modules/which-module": {
17843
+ "version": "2.0.1",
17844
+ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
17845
+ "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
17846
+ "license": "ISC"
17847
+ },
17620
17848
  "node_modules/why-is-node-running": {
17621
17849
  "version": "2.3.0",
17622
17850
  "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
@@ -46,17 +46,19 @@
46
46
  "@better-auth/passkey": "1.4.10",
47
47
  "@hey-api/client-fetch": "0.13.1",
48
48
  "@lenne.tech/bug.lt": "latest",
49
- "@lenne.tech/nuxt-extensions": "1.1.0",
49
+ "@lenne.tech/nuxt-extensions": "1.2.7",
50
50
  "@nuxt/image": "2.0.0",
51
51
  "@nuxt/ui": "4.3.0",
52
52
  "@pinia/nuxt": "0.11.3",
53
53
  "@vueuse/nuxt": "14.1.0",
54
54
  "better-auth": "1.4.10",
55
+ "qrcode": "1.5.4",
55
56
  "tus-js-client": "4.3.1",
56
57
  "valibot": "1.2.0"
57
58
  },
58
59
  "devDependencies": {
59
60
  "@hey-api/openapi-ts": "0.90.3",
61
+ "@iconify-json/lucide": "1.2.88",
60
62
  "@nuxt/devtools": "3.1.1",
61
63
  "@nuxt/test-utils": "3.23.0",
62
64
  "@nuxtjs/color-mode": "4.0.0",
@@ -66,6 +68,7 @@
66
68
  "@tailwindcss/typography": "0.5.19",
67
69
  "@tailwindcss/vite": "4.1.18",
68
70
  "@types/node": "25.0.6",
71
+ "@types/qrcode": "1.5.6",
69
72
  "@vitejs/plugin-vue": "^6.0.3",
70
73
  "@vue/test-utils": "^2.4.6",
71
74
  "dayjs-nuxt": "2.1.11",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nuxt-base",
3
- "version": "2.1.3",
3
+ "version": "2.2.0",
4
4
  "description": "Starter to generate a configured environment with VueJS, Nuxt, Tailwind, Eslint, Unit Tests, Playwright etc.",
5
5
  "license": "MIT",
6
6
  "author": "lenne.Tech GmbH",