create-nuxt-base 2.1.1 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,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.1.2](https://github.com/lenneTech/nuxt-base-starter/compare/v2.1.1...v2.1.2) (2026-01-24)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **forms:** restore UForm/UAuthForm with Valibot schema validation ([aa5e93c](https://github.com/lenneTech/nuxt-base-starter/commit/aa5e93cdd19193cc4dd1153fc4f629407b0273f0))
|
|
11
|
+
|
|
5
12
|
### [2.1.1](https://github.com/lenneTech/nuxt-base-starter/compare/v2.1.0...v2.1.1) (2026-01-24)
|
|
6
13
|
|
|
7
14
|
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
// ============================================================================
|
|
3
3
|
// Imports
|
|
4
4
|
// ============================================================================
|
|
5
|
+
import type { FormSubmitEvent } from '@nuxt/ui';
|
|
6
|
+
import type { InferOutput } from 'valibot';
|
|
7
|
+
|
|
8
|
+
import * as v from 'valibot';
|
|
9
|
+
|
|
5
10
|
import ModalBackupCodes from '~/components/Modal/ModalBackupCodes.vue';
|
|
6
11
|
|
|
7
12
|
// ============================================================================
|
|
@@ -34,10 +39,21 @@ const passkeyLoading = ref<boolean>(false);
|
|
|
34
39
|
const newPasskeyName = ref<string>('');
|
|
35
40
|
const showAddPasskey = ref<boolean>(false);
|
|
36
41
|
|
|
37
|
-
// Form states
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
const
|
|
42
|
+
// Form states for UForm (required for proper data binding)
|
|
43
|
+
const enable2FAForm = reactive({ password: '' });
|
|
44
|
+
const disable2FAForm = reactive({ password: '' });
|
|
45
|
+
const totpForm = reactive({ code: '' });
|
|
46
|
+
|
|
47
|
+
const passwordSchema = v.object({
|
|
48
|
+
password: v.pipe(v.string('Passwort ist erforderlich'), v.minLength(1, 'Passwort ist erforderlich')),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const totpSchema = v.object({
|
|
52
|
+
code: v.pipe(v.string('Code ist erforderlich'), v.length(6, 'Code muss 6 Ziffern haben')),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
type PasswordSchema = InferOutput<typeof passwordSchema>;
|
|
56
|
+
type TotpSchema = InferOutput<typeof totpSchema>;
|
|
41
57
|
|
|
42
58
|
// ============================================================================
|
|
43
59
|
// Lifecycle Hooks
|
|
@@ -114,21 +130,12 @@ async function deletePasskey(id: string): Promise<void> {
|
|
|
114
130
|
}
|
|
115
131
|
}
|
|
116
132
|
|
|
117
|
-
async function disable2FA(): Promise<void> {
|
|
118
|
-
if (!disable2FAPassword.value) {
|
|
119
|
-
toast.add({
|
|
120
|
-
color: 'error',
|
|
121
|
-
description: 'Passwort ist erforderlich',
|
|
122
|
-
title: 'Validierungsfehler',
|
|
123
|
-
});
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
|
|
133
|
+
async function disable2FA(payload: FormSubmitEvent<PasswordSchema>): Promise<void> {
|
|
127
134
|
loading.value = true;
|
|
128
135
|
|
|
129
136
|
try {
|
|
130
137
|
const { error } = await authClient.twoFactor.disable({
|
|
131
|
-
password:
|
|
138
|
+
password: payload.data.password,
|
|
132
139
|
});
|
|
133
140
|
|
|
134
141
|
if (error) {
|
|
@@ -152,27 +159,18 @@ async function disable2FA(): Promise<void> {
|
|
|
152
159
|
});
|
|
153
160
|
|
|
154
161
|
show2FADisable.value = false;
|
|
155
|
-
|
|
162
|
+
disable2FAForm.password = '';
|
|
156
163
|
} finally {
|
|
157
164
|
loading.value = false;
|
|
158
165
|
}
|
|
159
166
|
}
|
|
160
167
|
|
|
161
|
-
async function enable2FA(): Promise<void> {
|
|
162
|
-
if (!enable2FAPassword.value) {
|
|
163
|
-
toast.add({
|
|
164
|
-
color: 'error',
|
|
165
|
-
description: 'Passwort ist erforderlich',
|
|
166
|
-
title: 'Validierungsfehler',
|
|
167
|
-
});
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
|
|
168
|
+
async function enable2FA(payload: FormSubmitEvent<PasswordSchema>): Promise<void> {
|
|
171
169
|
loading.value = true;
|
|
172
170
|
|
|
173
171
|
try {
|
|
174
172
|
const { data, error } = await authClient.twoFactor.enable({
|
|
175
|
-
password:
|
|
173
|
+
password: payload.data.password,
|
|
176
174
|
});
|
|
177
175
|
|
|
178
176
|
if (error) {
|
|
@@ -187,7 +185,7 @@ async function enable2FA(): Promise<void> {
|
|
|
187
185
|
totpUri.value = data?.totpURI ?? '';
|
|
188
186
|
backupCodes.value = data?.backupCodes ?? [];
|
|
189
187
|
showTotpSetup.value = true;
|
|
190
|
-
|
|
188
|
+
enable2FAForm.password = '';
|
|
191
189
|
} finally {
|
|
192
190
|
loading.value = false;
|
|
193
191
|
}
|
|
@@ -220,21 +218,12 @@ async function openBackupCodesModal(codes: string[] = []): Promise<void> {
|
|
|
220
218
|
await modal.open();
|
|
221
219
|
}
|
|
222
220
|
|
|
223
|
-
async function verifyTotp(): Promise<void> {
|
|
224
|
-
if (!totpCode.value || totpCode.value.length !== 6) {
|
|
225
|
-
toast.add({
|
|
226
|
-
color: 'error',
|
|
227
|
-
description: 'Code muss 6 Ziffern haben',
|
|
228
|
-
title: 'Validierungsfehler',
|
|
229
|
-
});
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
|
|
221
|
+
async function verifyTotp(payload: FormSubmitEvent<TotpSchema>): Promise<void> {
|
|
233
222
|
loading.value = true;
|
|
234
223
|
|
|
235
224
|
try {
|
|
236
225
|
const { error } = await authClient.twoFactor.verifyTotp({
|
|
237
|
-
code:
|
|
226
|
+
code: payload.data.code,
|
|
238
227
|
});
|
|
239
228
|
|
|
240
229
|
if (error) {
|
|
@@ -258,7 +247,7 @@ async function verifyTotp(): Promise<void> {
|
|
|
258
247
|
});
|
|
259
248
|
|
|
260
249
|
showTotpSetup.value = false;
|
|
261
|
-
|
|
250
|
+
totpForm.code = '';
|
|
262
251
|
await openBackupCodesModal(backupCodes.value);
|
|
263
252
|
} finally {
|
|
264
253
|
loading.value = false;
|
|
@@ -299,12 +288,12 @@ async function verifyTotp(): Promise<void> {
|
|
|
299
288
|
</div>
|
|
300
289
|
|
|
301
290
|
<template v-if="!is2FAEnabled && !showTotpSetup">
|
|
302
|
-
<
|
|
303
|
-
<UFormField label="Passwort bestätigen" name="password"
|
|
304
|
-
<UInput v-model="
|
|
291
|
+
<UForm :schema="passwordSchema" :state="enable2FAForm" class="space-y-4" @submit="enable2FA">
|
|
292
|
+
<UFormField label="Passwort bestätigen" name="password">
|
|
293
|
+
<UInput v-model="enable2FAForm.password" type="password" placeholder="Dein Passwort" />
|
|
305
294
|
</UFormField>
|
|
306
295
|
<UButton type="submit" :loading="loading"> 2FA aktivieren </UButton>
|
|
307
|
-
</
|
|
296
|
+
</UForm>
|
|
308
297
|
</template>
|
|
309
298
|
|
|
310
299
|
<template v-if="showTotpSetup">
|
|
@@ -315,15 +304,15 @@ async function verifyTotp(): Promise<void> {
|
|
|
315
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" />
|
|
316
305
|
</div>
|
|
317
306
|
|
|
318
|
-
<
|
|
319
|
-
<UFormField label="Verifizierungscode" name="code"
|
|
320
|
-
<UInput v-model="
|
|
307
|
+
<UForm :schema="totpSchema" :state="totpForm" class="space-y-4" @submit="verifyTotp">
|
|
308
|
+
<UFormField label="Verifizierungscode" name="code">
|
|
309
|
+
<UInput v-model="totpForm.code" placeholder="000000" class="text-center font-mono" />
|
|
321
310
|
</UFormField>
|
|
322
311
|
<div class="flex gap-2">
|
|
323
312
|
<UButton type="submit" :loading="loading"> Verifizieren </UButton>
|
|
324
313
|
<UButton variant="outline" color="neutral" @click="showTotpSetup = false"> Abbrechen </UButton>
|
|
325
314
|
</div>
|
|
326
|
-
</
|
|
315
|
+
</UForm>
|
|
327
316
|
</div>
|
|
328
317
|
</template>
|
|
329
318
|
|
|
@@ -335,16 +324,16 @@ async function verifyTotp(): Promise<void> {
|
|
|
335
324
|
</template>
|
|
336
325
|
|
|
337
326
|
<template v-if="show2FADisable">
|
|
338
|
-
<
|
|
327
|
+
<UForm :schema="passwordSchema" :state="disable2FAForm" class="space-y-4" @submit="disable2FA">
|
|
339
328
|
<UAlert color="warning" icon="i-lucide-alert-triangle"> 2FA zu deaktivieren verringert die Sicherheit deines Kontos. </UAlert>
|
|
340
|
-
<UFormField label="Passwort bestätigen" name="password"
|
|
341
|
-
<UInput v-model="
|
|
329
|
+
<UFormField label="Passwort bestätigen" name="password">
|
|
330
|
+
<UInput v-model="disable2FAForm.password" type="password" placeholder="Dein Passwort" />
|
|
342
331
|
</UFormField>
|
|
343
332
|
<div class="flex gap-2">
|
|
344
333
|
<UButton type="submit" color="error" :loading="loading"> 2FA deaktivieren </UButton>
|
|
345
334
|
<UButton variant="outline" color="neutral" @click="show2FADisable = false"> Abbrechen </UButton>
|
|
346
335
|
</div>
|
|
347
|
-
</
|
|
336
|
+
</UForm>
|
|
348
337
|
</template>
|
|
349
338
|
</div>
|
|
350
339
|
</UCard>
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
// ============================================================================
|
|
3
3
|
// Imports
|
|
4
4
|
// ============================================================================
|
|
5
|
+
import type { AuthFormField, FormSubmitEvent } from '@nuxt/ui';
|
|
6
|
+
import type { InferOutput } from 'valibot';
|
|
7
|
+
|
|
5
8
|
import * as v from 'valibot';
|
|
6
9
|
|
|
7
10
|
// ============================================================================
|
|
@@ -9,6 +12,7 @@ import * as v from 'valibot';
|
|
|
9
12
|
// ============================================================================
|
|
10
13
|
const toast = useToast();
|
|
11
14
|
const { signUp, signIn, registerPasskey } = useLtAuth();
|
|
15
|
+
const { translateError } = useLtErrorTranslation();
|
|
12
16
|
|
|
13
17
|
// ============================================================================
|
|
14
18
|
// Page Meta
|
|
@@ -24,13 +28,36 @@ const loading = ref<boolean>(false);
|
|
|
24
28
|
const showPasskeyPrompt = ref<boolean>(false);
|
|
25
29
|
const passkeyLoading = ref<boolean>(false);
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
const fields: AuthFormField[] = [
|
|
32
|
+
{
|
|
33
|
+
label: 'Name',
|
|
34
|
+
name: 'name',
|
|
35
|
+
placeholder: 'Name eingeben',
|
|
36
|
+
required: true,
|
|
37
|
+
type: 'text',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
label: 'E-Mail',
|
|
41
|
+
name: 'email',
|
|
42
|
+
placeholder: 'E-Mail eingeben',
|
|
43
|
+
required: true,
|
|
44
|
+
type: 'email',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
label: 'Passwort',
|
|
48
|
+
name: 'password',
|
|
49
|
+
placeholder: 'Passwort eingeben',
|
|
50
|
+
required: true,
|
|
51
|
+
type: 'password',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
label: 'Passwort bestätigen',
|
|
55
|
+
name: 'confirmPassword',
|
|
56
|
+
placeholder: 'Passwort wiederholen',
|
|
57
|
+
required: true,
|
|
58
|
+
type: 'password',
|
|
59
|
+
},
|
|
60
|
+
];
|
|
34
61
|
|
|
35
62
|
const schema = v.pipe(
|
|
36
63
|
v.object({
|
|
@@ -45,47 +72,38 @@ const schema = v.pipe(
|
|
|
45
72
|
),
|
|
46
73
|
);
|
|
47
74
|
|
|
75
|
+
type Schema = InferOutput<typeof schema>;
|
|
76
|
+
|
|
48
77
|
// ============================================================================
|
|
49
78
|
// Functions
|
|
50
79
|
// ============================================================================
|
|
51
|
-
async function onSubmit(): Promise<void> {
|
|
52
|
-
// Validate
|
|
53
|
-
const result = v.safeParse(schema, formState);
|
|
54
|
-
if (!result.success) {
|
|
55
|
-
const firstError = result.issues[0];
|
|
56
|
-
toast.add({
|
|
57
|
-
color: 'error',
|
|
58
|
-
description: firstError.message,
|
|
59
|
-
title: 'Validierungsfehler',
|
|
60
|
-
});
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
80
|
+
async function onSubmit(payload: FormSubmitEvent<Schema>): Promise<void> {
|
|
64
81
|
loading.value = true;
|
|
65
82
|
|
|
66
83
|
try {
|
|
67
84
|
// Step 1: Sign up
|
|
68
85
|
const signUpResult = await signUp.email({
|
|
69
|
-
email:
|
|
70
|
-
name:
|
|
71
|
-
password:
|
|
86
|
+
email: payload.data.email,
|
|
87
|
+
name: payload.data.name,
|
|
88
|
+
password: payload.data.password,
|
|
72
89
|
});
|
|
73
90
|
|
|
74
91
|
const signUpError = 'error' in signUpResult ? signUpResult.error : null;
|
|
75
92
|
|
|
76
93
|
if (signUpError) {
|
|
94
|
+
const errorMessage = signUpError.message || 'Registrierung fehlgeschlagen';
|
|
77
95
|
toast.add({
|
|
78
96
|
color: 'error',
|
|
79
|
-
description:
|
|
80
|
-
title: '
|
|
97
|
+
description: translateError(errorMessage),
|
|
98
|
+
title: 'Registrierung fehlgeschlagen',
|
|
81
99
|
});
|
|
82
100
|
return;
|
|
83
101
|
}
|
|
84
102
|
|
|
85
103
|
// Step 2: Sign in to create session (required for passkey registration)
|
|
86
104
|
const signInResult = await signIn.email({
|
|
87
|
-
email:
|
|
88
|
-
password:
|
|
105
|
+
email: payload.data.email,
|
|
106
|
+
password: payload.data.password,
|
|
89
107
|
});
|
|
90
108
|
|
|
91
109
|
const signInError = 'error' in signInResult ? signInResult.error : null;
|
|
@@ -152,65 +170,25 @@ async function skipPasskey(): Promise<void> {
|
|
|
152
170
|
<template>
|
|
153
171
|
<UPageCard class="w-md" variant="naked">
|
|
154
172
|
<template v-if="!showPasskeyPrompt">
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
v-model="formState.email"
|
|
175
|
-
name="email"
|
|
176
|
-
type="email"
|
|
177
|
-
placeholder="E-Mail eingeben"
|
|
178
|
-
autocomplete="email"
|
|
179
|
-
class="w-full"
|
|
180
|
-
/>
|
|
181
|
-
</UFormField>
|
|
182
|
-
|
|
183
|
-
<UFormField label="Passwort" name="password" required class="w-full">
|
|
184
|
-
<UInput
|
|
185
|
-
v-model="formState.password"
|
|
186
|
-
name="password"
|
|
187
|
-
type="password"
|
|
188
|
-
placeholder="Passwort eingeben"
|
|
189
|
-
autocomplete="new-password"
|
|
190
|
-
class="w-full"
|
|
191
|
-
/>
|
|
192
|
-
</UFormField>
|
|
193
|
-
|
|
194
|
-
<UFormField label="Passwort bestätigen" name="confirmPassword" required class="w-full">
|
|
195
|
-
<UInput
|
|
196
|
-
v-model="formState.confirmPassword"
|
|
197
|
-
name="confirmPassword"
|
|
198
|
-
type="password"
|
|
199
|
-
placeholder="Passwort wiederholen"
|
|
200
|
-
autocomplete="new-password"
|
|
201
|
-
class="w-full"
|
|
202
|
-
/>
|
|
203
|
-
</UFormField>
|
|
204
|
-
|
|
205
|
-
<UButton type="submit" block :loading="loading">
|
|
206
|
-
Konto erstellen
|
|
207
|
-
</UButton>
|
|
208
|
-
</form>
|
|
209
|
-
|
|
210
|
-
<p class="mt-4 text-center text-sm text-muted">
|
|
211
|
-
Bereits ein Konto?
|
|
212
|
-
<ULink to="/auth/login" class="text-primary font-medium">Anmelden</ULink>
|
|
213
|
-
</p>
|
|
173
|
+
<UAuthForm
|
|
174
|
+
:schema="schema"
|
|
175
|
+
title="Registrieren"
|
|
176
|
+
icon="i-lucide-user-plus"
|
|
177
|
+
:fields="fields"
|
|
178
|
+
:loading="loading"
|
|
179
|
+
:submit="{
|
|
180
|
+
label: 'Konto erstellen',
|
|
181
|
+
block: true,
|
|
182
|
+
}"
|
|
183
|
+
@submit="onSubmit"
|
|
184
|
+
>
|
|
185
|
+
<template #footer>
|
|
186
|
+
<p class="text-center text-sm text-muted">
|
|
187
|
+
Bereits ein Konto?
|
|
188
|
+
<ULink to="/auth/login" class="text-primary font-medium">Anmelden</ULink>
|
|
189
|
+
</p>
|
|
190
|
+
</template>
|
|
191
|
+
</UAuthForm>
|
|
214
192
|
</template>
|
|
215
193
|
|
|
216
194
|
<template v-else>
|
package/package.json
CHANGED