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 +14 -0
- package/nuxt-base-template/app/layouts/default.vue +5 -1
- package/nuxt-base-template/app/pages/app/settings/security.vue +26 -6
- package/nuxt-base-template/app/pages/auth/forgot-password.vue +2 -2
- package/nuxt-base-template/app/pages/auth/login.vue +39 -8
- package/nuxt-base-template/app/pages/auth/register.vue +87 -13
- package/nuxt-base-template/app/pages/auth/reset-password.vue +2 -2
- package/nuxt-base-template/app/pages/auth/verify-email.vue +209 -0
- package/nuxt-base-template/nuxt.config.ts +11 -0
- package/nuxt-base-template/package-lock.json +239 -11
- package/nuxt-base-template/package.json +4 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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="
|
|
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"
|
|
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="
|
|
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
|
|
11
|
-
const authClient =
|
|
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,
|
|
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 (
|
|
118
|
-
const errorMessage =
|
|
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 =
|
|
130
|
-
const requires2FA = resultData && (
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
11
|
-
const authClient =
|
|
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.
|
|
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.
|
|
2341
|
-
"resolved": "https://registry.npmjs.org/@lenne.tech/nuxt-extensions/-/nuxt-extensions-1.
|
|
2342
|
-
"integrity": "sha512-
|
|
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.
|
|
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.
|
|
2371
|
-
"resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.
|
|
2372
|
-
"integrity": "sha512-
|
|
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.
|
|
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.
|
|
2393
|
-
"unctx": "^2.
|
|
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.
|
|
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