create-nuxt-base 2.1.4 → 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 +7 -0
- package/nuxt-base-template/app/layouts/default.vue +5 -1
- package/nuxt-base-template/app/pages/app/settings/security.vue +2 -2
- 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 +22 -11
- package/nuxt-base-template/package.json +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
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
|
+
|
|
5
12
|
### [2.1.4](https://github.com/lenneTech/nuxt-base-starter/compare/v2.1.3...v2.1.4) (2026-01-26)
|
|
6
13
|
|
|
7
14
|
|
|
@@ -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();
|
|
@@ -345,7 +345,7 @@ async function verifyTotp(payload: FormSubmitEvent<TotpSchema>): Promise<void> {
|
|
|
345
345
|
|
|
346
346
|
<template v-if="show2FADisable">
|
|
347
347
|
<UForm :schema="passwordSchema" :state="disable2FAForm" class="space-y-4" @submit="disable2FA">
|
|
348
|
-
<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." />
|
|
349
349
|
<UFormField label="Passwort bestätigen" name="password">
|
|
350
350
|
<UInput v-model="disable2FAForm.password" type="password" placeholder="Dein Passwort" />
|
|
351
351
|
</UFormField>
|
|
@@ -381,7 +381,7 @@ async function verifyTotp(payload: FormSubmitEvent<TotpSchema>): Promise<void> {
|
|
|
381
381
|
<p class="text-xs text-muted">Erstellt am {{ new Date(passkey.createdAt).toLocaleDateString('de-DE') }}</p>
|
|
382
382
|
</div>
|
|
383
383
|
</div>
|
|
384
|
-
<UButton variant="
|
|
384
|
+
<UButton variant="outline" color="error" icon="i-lucide-trash" size="sm" :loading="passkeyLoading" @click="deletePasskey(passkey.id)">Löschen</UButton>
|
|
385
385
|
</div>
|
|
386
386
|
</div>
|
|
387
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,7 +10,7 @@
|
|
|
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",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@hey-api/openapi-ts": "0.90.3",
|
|
25
|
+
"@iconify-json/lucide": "1.2.88",
|
|
25
26
|
"@nuxt/devtools": "3.1.1",
|
|
26
27
|
"@nuxt/test-utils": "3.23.0",
|
|
27
28
|
"@nuxtjs/color-mode": "4.0.0",
|
|
@@ -1330,6 +1331,16 @@
|
|
|
1330
1331
|
"typescript": ">=5.5.3"
|
|
1331
1332
|
}
|
|
1332
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
|
+
},
|
|
1333
1344
|
"node_modules/@iconify/collections": {
|
|
1334
1345
|
"version": "1.0.638",
|
|
1335
1346
|
"license": "MIT",
|
|
@@ -2339,12 +2350,12 @@
|
|
|
2339
2350
|
}
|
|
2340
2351
|
},
|
|
2341
2352
|
"node_modules/@lenne.tech/nuxt-extensions": {
|
|
2342
|
-
"version": "1.
|
|
2343
|
-
"resolved": "https://registry.npmjs.org/@lenne.tech/nuxt-extensions/-/nuxt-extensions-1.
|
|
2344
|
-
"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==",
|
|
2345
2356
|
"license": "MIT",
|
|
2346
2357
|
"dependencies": {
|
|
2347
|
-
"@nuxt/kit": "4.
|
|
2358
|
+
"@nuxt/kit": "4.3.0"
|
|
2348
2359
|
},
|
|
2349
2360
|
"peerDependencies": {
|
|
2350
2361
|
"@better-auth/passkey": ">=1.0.0",
|
|
@@ -2369,12 +2380,12 @@
|
|
|
2369
2380
|
}
|
|
2370
2381
|
},
|
|
2371
2382
|
"node_modules/@lenne.tech/nuxt-extensions/node_modules/@nuxt/kit": {
|
|
2372
|
-
"version": "4.
|
|
2373
|
-
"resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.
|
|
2374
|
-
"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==",
|
|
2375
2386
|
"license": "MIT",
|
|
2376
2387
|
"dependencies": {
|
|
2377
|
-
"c12": "^3.3.
|
|
2388
|
+
"c12": "^3.3.3",
|
|
2378
2389
|
"consola": "^3.4.2",
|
|
2379
2390
|
"defu": "^6.1.4",
|
|
2380
2391
|
"destr": "^2.0.5",
|
|
@@ -2391,8 +2402,8 @@
|
|
|
2391
2402
|
"scule": "^1.3.0",
|
|
2392
2403
|
"semver": "^7.7.3",
|
|
2393
2404
|
"tinyglobby": "^0.2.15",
|
|
2394
|
-
"ufo": "^1.6.
|
|
2395
|
-
"unctx": "^2.
|
|
2405
|
+
"ufo": "^1.6.3",
|
|
2406
|
+
"unctx": "^2.5.0",
|
|
2396
2407
|
"untyped": "^2.0.0"
|
|
2397
2408
|
},
|
|
2398
2409
|
"engines": {
|
|
@@ -46,7 +46,7 @@
|
|
|
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",
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@hey-api/openapi-ts": "0.90.3",
|
|
61
|
+
"@iconify-json/lucide": "1.2.88",
|
|
61
62
|
"@nuxt/devtools": "3.1.1",
|
|
62
63
|
"@nuxt/test-utils": "3.23.0",
|
|
63
64
|
"@nuxtjs/color-mode": "4.0.0",
|
package/package.json
CHANGED