create-nuxt-base 0.3.17 → 1.0.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.
Files changed (41) hide show
  1. package/.github/workflows/publish.yml +4 -2
  2. package/.oxfmtrc.jsonc +7 -0
  3. package/CHANGELOG.md +20 -8
  4. package/nuxt-base-template/.dockerignore +44 -0
  5. package/nuxt-base-template/.nuxtrc +1 -0
  6. package/nuxt-base-template/.oxfmtrc.jsonc +8 -0
  7. package/nuxt-base-template/Dockerfile.dev +23 -0
  8. package/nuxt-base-template/app/components/Modal/ModalBackupCodes.vue +117 -0
  9. package/nuxt-base-template/app/components/Upload/TusFileUpload.vue +302 -0
  10. package/nuxt-base-template/app/composables/use-better-auth.ts +25 -0
  11. package/nuxt-base-template/app/composables/use-file.ts +39 -4
  12. package/nuxt-base-template/app/composables/use-share.ts +1 -1
  13. package/nuxt-base-template/app/composables/use-tus-upload.ts +278 -0
  14. package/nuxt-base-template/app/interfaces/upload.interface.ts +58 -0
  15. package/nuxt-base-template/app/interfaces/user.interface.ts +12 -0
  16. package/nuxt-base-template/app/lib/auth-client.ts +135 -0
  17. package/nuxt-base-template/app/middleware/admin.global.ts +23 -0
  18. package/nuxt-base-template/app/middleware/auth.global.ts +18 -0
  19. package/nuxt-base-template/app/middleware/guest.global.ts +18 -0
  20. package/nuxt-base-template/app/pages/app/settings/security.vue +409 -0
  21. package/nuxt-base-template/app/pages/auth/2fa.vue +120 -0
  22. package/nuxt-base-template/app/pages/auth/forgot-password.vue +72 -21
  23. package/nuxt-base-template/app/pages/auth/login.vue +75 -11
  24. package/nuxt-base-template/app/pages/auth/register.vue +184 -0
  25. package/nuxt-base-template/app/pages/auth/reset-password.vue +153 -0
  26. package/nuxt-base-template/app/utils/crypto.ts +13 -0
  27. package/nuxt-base-template/docker-entrypoint.sh +21 -0
  28. package/nuxt-base-template/nuxt.config.ts +4 -1
  29. package/nuxt-base-template/oxlint.json +14 -0
  30. package/nuxt-base-template/package-lock.json +11582 -10675
  31. package/nuxt-base-template/package.json +35 -32
  32. package/nuxt-base-template/tests/iam.spec.ts +247 -0
  33. package/package.json +14 -11
  34. package/.eslintignore +0 -14
  35. package/.eslintrc +0 -3
  36. package/.prettierignore +0 -5
  37. package/.prettierrc +0 -6
  38. package/nuxt-base-template/CLAUDE.md +0 -361
  39. package/nuxt-base-template/app/pages/auth/reset-password/[token].vue +0 -110
  40. package/nuxt-base-template/app/public/favicon.ico +0 -0
  41. package/nuxt-base-template/eslint.config.mjs +0 -4
@@ -0,0 +1,409 @@
1
+ <script setup lang="ts">
2
+ // ============================================================================
3
+ // Imports
4
+ // ============================================================================
5
+ import type { FormSubmitEvent } from '@nuxt/ui';
6
+ import type { InferOutput } from 'valibot';
7
+
8
+ import * as v from 'valibot';
9
+
10
+ import ModalBackupCodes from '~/components/Modal/ModalBackupCodes.vue';
11
+ import { authClient } from '~/lib/auth-client';
12
+
13
+ // ============================================================================
14
+ // Interfaces
15
+ // ============================================================================
16
+ interface Passkey {
17
+ createdAt: Date;
18
+ id: string;
19
+ name?: string;
20
+ }
21
+
22
+ // ============================================================================
23
+ // Composables
24
+ // ============================================================================
25
+ const toast = useToast();
26
+ const overlay = useOverlay();
27
+ const { is2FAEnabled, user } = useBetterAuth();
28
+
29
+ // ============================================================================
30
+ // Variables
31
+ // ============================================================================
32
+ const loading = ref<boolean>(false);
33
+ const totpUri = ref<string>('');
34
+ const backupCodes = ref<string[]>([]);
35
+ const showTotpSetup = ref<boolean>(false);
36
+ const show2FADisable = ref<boolean>(false);
37
+ const passkeys = ref<Passkey[]>([]);
38
+ const passkeyLoading = ref<boolean>(false);
39
+ const newPasskeyName = ref<string>('');
40
+ const showAddPasskey = ref<boolean>(false);
41
+
42
+ const passwordSchema = v.object({
43
+ password: v.pipe(v.string('Passwort ist erforderlich'), v.minLength(1, 'Passwort ist erforderlich')),
44
+ });
45
+
46
+ const totpSchema = v.object({
47
+ code: v.pipe(v.string('Code ist erforderlich'), v.length(6, 'Code muss 6 Ziffern haben')),
48
+ });
49
+
50
+ type PasswordSchema = InferOutput<typeof passwordSchema>;
51
+ type TotpSchema = InferOutput<typeof totpSchema>;
52
+
53
+ // ============================================================================
54
+ // Lifecycle Hooks
55
+ // ============================================================================
56
+ onMounted(async () => {
57
+ await loadPasskeys();
58
+ });
59
+
60
+ async function addPasskey(): Promise<void> {
61
+ if (!newPasskeyName.value.trim()) {
62
+ toast.add({
63
+ color: 'error',
64
+ description: 'Bitte gib einen Namen für den Passkey ein',
65
+ title: 'Fehler',
66
+ });
67
+ return;
68
+ }
69
+
70
+ passkeyLoading.value = true;
71
+
72
+ try {
73
+ const { error } = await authClient.passkey.addPasskey({
74
+ name: newPasskeyName.value,
75
+ });
76
+
77
+ if (error) {
78
+ toast.add({
79
+ color: 'error',
80
+ description: error.message || 'Passkey konnte nicht hinzugefügt werden',
81
+ title: 'Fehler',
82
+ });
83
+ return;
84
+ }
85
+
86
+ toast.add({
87
+ color: 'success',
88
+ description: 'Passkey wurde erfolgreich hinzugefügt',
89
+ title: 'Erfolg',
90
+ });
91
+
92
+ newPasskeyName.value = '';
93
+ showAddPasskey.value = false;
94
+ await loadPasskeys();
95
+ } finally {
96
+ passkeyLoading.value = false;
97
+ }
98
+ }
99
+
100
+ async function deletePasskey(id: string): Promise<void> {
101
+ passkeyLoading.value = true;
102
+
103
+ try {
104
+ const { error } = await authClient.passkey.deletePasskey({
105
+ id,
106
+ });
107
+
108
+ if (error) {
109
+ toast.add({
110
+ color: 'error',
111
+ description: error.message || 'Passkey konnte nicht gelöscht werden',
112
+ title: 'Fehler',
113
+ });
114
+ return;
115
+ }
116
+
117
+ toast.add({
118
+ color: 'success',
119
+ description: 'Passkey wurde gelöscht',
120
+ title: 'Erfolg',
121
+ });
122
+
123
+ await loadPasskeys();
124
+ } finally {
125
+ passkeyLoading.value = false;
126
+ }
127
+ }
128
+
129
+ async function disable2FA(payload: FormSubmitEvent<PasswordSchema>): Promise<void> {
130
+ loading.value = true;
131
+
132
+ try {
133
+ const { error } = await authClient.twoFactor.disable({
134
+ password: payload.data.password,
135
+ });
136
+
137
+ if (error) {
138
+ toast.add({
139
+ color: 'error',
140
+ description: error.message || '2FA konnte nicht deaktiviert werden',
141
+ title: 'Fehler',
142
+ });
143
+ return;
144
+ }
145
+
146
+ toast.add({
147
+ color: 'success',
148
+ description: '2FA wurde deaktiviert',
149
+ title: 'Erfolg',
150
+ });
151
+
152
+ show2FADisable.value = false;
153
+ } finally {
154
+ loading.value = false;
155
+ }
156
+ }
157
+
158
+ async function enable2FA(payload: FormSubmitEvent<PasswordSchema>): Promise<void> {
159
+ loading.value = true;
160
+
161
+ try {
162
+ const { data, error } = await authClient.twoFactor.enable({
163
+ password: payload.data.password,
164
+ });
165
+
166
+ if (error) {
167
+ toast.add({
168
+ color: 'error',
169
+ description: error.message || '2FA konnte nicht aktiviert werden',
170
+ title: 'Fehler',
171
+ });
172
+ return;
173
+ }
174
+
175
+ totpUri.value = data?.totpURI ?? '';
176
+ backupCodes.value = data?.backupCodes ?? [];
177
+ showTotpSetup.value = true;
178
+ } finally {
179
+ loading.value = false;
180
+ }
181
+ }
182
+
183
+ // ============================================================================
184
+ // Functions
185
+ // ============================================================================
186
+ async function loadPasskeys(): Promise<void> {
187
+ try {
188
+ const { data, error } = await authClient.passkey.listUserPasskeys();
189
+
190
+ if (error) {
191
+ console.error('Failed to load passkeys:', error);
192
+ return;
193
+ }
194
+
195
+ passkeys.value = (data ?? []) as Passkey[];
196
+ } catch {
197
+ console.error('Failed to load passkeys');
198
+ }
199
+ }
200
+
201
+ async function openBackupCodesModal(codes: string[] = []): Promise<void> {
202
+ const modal = overlay.create(ModalBackupCodes, {
203
+ props: {
204
+ initialCodes: codes,
205
+ },
206
+ });
207
+ await modal.open();
208
+ }
209
+
210
+ async function verifyTotp(payload: FormSubmitEvent<TotpSchema>): Promise<void> {
211
+ loading.value = true;
212
+
213
+ try {
214
+ const { error } = await authClient.twoFactor.verifyTotp({
215
+ code: payload.data.code,
216
+ });
217
+
218
+ if (error) {
219
+ toast.add({
220
+ color: 'error',
221
+ description: error.message || 'Code ungültig',
222
+ title: 'Fehler',
223
+ });
224
+ return;
225
+ }
226
+
227
+ toast.add({
228
+ color: 'success',
229
+ description: '2FA wurde erfolgreich aktiviert',
230
+ title: 'Erfolg',
231
+ });
232
+
233
+ showTotpSetup.value = false;
234
+ await openBackupCodesModal(backupCodes.value);
235
+ } finally {
236
+ loading.value = false;
237
+ }
238
+ }
239
+ </script>
240
+
241
+ <template>
242
+ <div class="mx-auto max-w-2xl space-y-8">
243
+ <div>
244
+ <h1 class="text-2xl font-bold">Sicherheit</h1>
245
+ <p class="text-muted">Verwalte deine Sicherheitseinstellungen</p>
246
+ </div>
247
+
248
+ <!-- 2FA Section -->
249
+ <UCard>
250
+ <template #header>
251
+ <div class="flex items-center gap-3">
252
+ <UIcon name="i-lucide-shield-check" class="size-6 text-primary" />
253
+ <div>
254
+ <h2 class="font-semibold">Zwei-Faktor-Authentifizierung</h2>
255
+ <p class="text-sm text-muted">Zusätzliche Sicherheit für dein Konto</p>
256
+ </div>
257
+ </div>
258
+ </template>
259
+
260
+ <div class="space-y-4">
261
+ <div class="flex items-center justify-between">
262
+ <div>
263
+ <p class="font-medium">Status</p>
264
+ <p class="text-sm text-muted">
265
+ {{ is2FAEnabled ? '2FA ist aktiviert' : '2FA ist deaktiviert' }}
266
+ </p>
267
+ </div>
268
+ <UBadge :color="is2FAEnabled ? 'success' : 'neutral'">
269
+ {{ is2FAEnabled ? 'Aktiv' : 'Inaktiv' }}
270
+ </UBadge>
271
+ </div>
272
+
273
+ <template v-if="!is2FAEnabled && !showTotpSetup">
274
+ <UForm :schema="passwordSchema" class="space-y-4" @submit="enable2FA">
275
+ <UFormField label="Passwort bestätigen" name="password">
276
+ <UInput name="password" type="password" placeholder="Dein Passwort" />
277
+ </UFormField>
278
+ <UButton type="submit" :loading="loading"> 2FA aktivieren </UButton>
279
+ </UForm>
280
+ </template>
281
+
282
+ <template v-if="showTotpSetup">
283
+ <div class="space-y-4">
284
+ <UAlert color="info" icon="i-lucide-info"> Scanne den QR-Code mit deiner Authenticator-App (z.B. Google Authenticator, Authy) und gib den Code ein. </UAlert>
285
+
286
+ <div class="flex justify-center">
287
+ <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" />
288
+ </div>
289
+
290
+ <UForm :schema="totpSchema" class="space-y-4" @submit="verifyTotp">
291
+ <UFormField label="Verifizierungscode" name="code">
292
+ <UInput name="code" placeholder="000000" class="text-center font-mono" />
293
+ </UFormField>
294
+ <div class="flex gap-2">
295
+ <UButton type="submit" :loading="loading"> Verifizieren </UButton>
296
+ <UButton variant="outline" color="neutral" @click="showTotpSetup = false"> Abbrechen </UButton>
297
+ </div>
298
+ </UForm>
299
+ </div>
300
+ </template>
301
+
302
+ <template v-if="is2FAEnabled && !show2FADisable">
303
+ <div class="flex gap-2">
304
+ <UButton variant="outline" @click="show2FADisable = true"> 2FA deaktivieren </UButton>
305
+ <UButton variant="outline" color="neutral" @click="openBackupCodesModal()"> Backup-Codes anzeigen </UButton>
306
+ </div>
307
+ </template>
308
+
309
+ <template v-if="show2FADisable">
310
+ <UForm :schema="passwordSchema" class="space-y-4" @submit="disable2FA">
311
+ <UAlert color="warning" icon="i-lucide-alert-triangle"> 2FA zu deaktivieren verringert die Sicherheit deines Kontos. </UAlert>
312
+ <UFormField label="Passwort bestätigen" name="password">
313
+ <UInput name="password" type="password" placeholder="Dein Passwort" />
314
+ </UFormField>
315
+ <div class="flex gap-2">
316
+ <UButton type="submit" color="error" :loading="loading"> 2FA deaktivieren </UButton>
317
+ <UButton variant="outline" color="neutral" @click="show2FADisable = false"> Abbrechen </UButton>
318
+ </div>
319
+ </UForm>
320
+ </template>
321
+ </div>
322
+ </UCard>
323
+
324
+ <!-- Passkeys Section -->
325
+ <UCard>
326
+ <template #header>
327
+ <div class="flex items-center gap-3">
328
+ <UIcon name="i-lucide-fingerprint" class="size-6 text-primary" />
329
+ <div>
330
+ <h2 class="font-semibold">Passkeys</h2>
331
+ <p class="text-sm text-muted">Passwordlose Anmeldung mit biometrischen Daten</p>
332
+ </div>
333
+ </div>
334
+ </template>
335
+
336
+ <div class="space-y-4">
337
+ <template v-if="passkeys.length > 0">
338
+ <div class="divide-y">
339
+ <div v-for="passkey in passkeys" :key="passkey.id" class="flex items-center justify-between py-3">
340
+ <div class="flex items-center gap-3">
341
+ <UIcon name="i-lucide-key" class="size-5 text-muted" />
342
+ <div>
343
+ <p class="font-medium">{{ passkey.name || 'Unbenannter Passkey' }}</p>
344
+ <p class="text-xs text-muted">Erstellt am {{ new Date(passkey.createdAt).toLocaleDateString('de-DE') }}</p>
345
+ </div>
346
+ </div>
347
+ <UButton variant="ghost" color="error" icon="i-lucide-trash-2" size="sm" :loading="passkeyLoading" @click="deletePasskey(passkey.id)" />
348
+ </div>
349
+ </div>
350
+ </template>
351
+
352
+ <template v-else>
353
+ <p class="text-center text-muted py-4">Du hast noch keine Passkeys eingerichtet.</p>
354
+ </template>
355
+
356
+ <template v-if="!showAddPasskey">
357
+ <UButton variant="outline" icon="i-lucide-plus" @click="showAddPasskey = true"> Passkey hinzufügen </UButton>
358
+ </template>
359
+
360
+ <template v-else>
361
+ <div class="flex gap-2">
362
+ <UInput v-model="newPasskeyName" placeholder="Name für den Passkey" class="flex-1" />
363
+ <UButton :loading="passkeyLoading" @click="addPasskey"> Hinzufügen </UButton>
364
+ <UButton
365
+ variant="outline"
366
+ color="neutral"
367
+ @click="
368
+ showAddPasskey = false;
369
+ newPasskeyName = '';
370
+ "
371
+ >
372
+ Abbrechen
373
+ </UButton>
374
+ </div>
375
+ </template>
376
+ </div>
377
+ </UCard>
378
+
379
+ <!-- Account Info -->
380
+ <UCard>
381
+ <template #header>
382
+ <div class="flex items-center gap-3">
383
+ <UIcon name="i-lucide-user" class="size-6 text-primary" />
384
+ <div>
385
+ <h2 class="font-semibold">Kontoinformationen</h2>
386
+ <p class="text-sm text-muted">Deine Kontodaten</p>
387
+ </div>
388
+ </div>
389
+ </template>
390
+
391
+ <div class="space-y-3">
392
+ <div class="flex justify-between">
393
+ <span class="text-muted">E-Mail</span>
394
+ <span class="font-medium">{{ user?.email }}</span>
395
+ </div>
396
+ <div class="flex justify-between">
397
+ <span class="text-muted">Name</span>
398
+ <span class="font-medium">{{ user?.name || '-' }}</span>
399
+ </div>
400
+ <div class="flex justify-between">
401
+ <span class="text-muted">E-Mail verifiziert</span>
402
+ <UBadge :color="user?.emailVerified ? 'success' : 'warning'">
403
+ {{ user?.emailVerified ? 'Ja' : 'Nein' }}
404
+ </UBadge>
405
+ </div>
406
+ </div>
407
+ </UCard>
408
+ </div>
409
+ </template>
@@ -0,0 +1,120 @@
1
+ <script setup lang="ts">
2
+ // ============================================================================
3
+ // Imports
4
+ // ============================================================================
5
+ import type { FormSubmitEvent } from '@nuxt/ui';
6
+ import type { InferOutput } from 'valibot';
7
+
8
+ import * as v from 'valibot';
9
+
10
+ import { authClient } from '~/lib/auth-client';
11
+
12
+ // ============================================================================
13
+ // Composables
14
+ // ============================================================================
15
+ const toast = useToast();
16
+
17
+ // ============================================================================
18
+ // Page Meta
19
+ // ============================================================================
20
+ definePageMeta({
21
+ layout: 'slim',
22
+ });
23
+
24
+ // ============================================================================
25
+ // Variables
26
+ // ============================================================================
27
+ const loading = ref<boolean>(false);
28
+ const useBackupCode = ref<boolean>(false);
29
+ const trustDevice = ref<boolean>(false);
30
+
31
+ const schema = v.object({
32
+ code: v.pipe(v.string('Code ist erforderlich'), v.minLength(6, 'Code muss mindestens 6 Zeichen haben')),
33
+ });
34
+
35
+ type Schema = InferOutput<typeof schema>;
36
+
37
+ // ============================================================================
38
+ // Functions
39
+ // ============================================================================
40
+ async function onSubmit(payload: FormSubmitEvent<Schema>): Promise<void> {
41
+ loading.value = true;
42
+
43
+ try {
44
+ if (useBackupCode.value) {
45
+ const { error } = await authClient.twoFactor.verifyBackupCode({
46
+ code: payload.data.code,
47
+ });
48
+
49
+ if (error) {
50
+ toast.add({
51
+ color: 'error',
52
+ description: error.message || 'Backup-Code ungültig',
53
+ title: 'Fehler',
54
+ });
55
+ return;
56
+ }
57
+ } else {
58
+ const { error } = await authClient.twoFactor.verifyTotp({
59
+ code: payload.data.code,
60
+ trustDevice: trustDevice.value,
61
+ });
62
+
63
+ if (error) {
64
+ toast.add({
65
+ color: 'error',
66
+ description: error.message || 'Code ungültig',
67
+ title: 'Fehler',
68
+ });
69
+ return;
70
+ }
71
+ }
72
+
73
+ await navigateTo('/app');
74
+ } finally {
75
+ loading.value = false;
76
+ }
77
+ }
78
+
79
+ function toggleBackupCode(): void {
80
+ useBackupCode.value = !useBackupCode.value;
81
+ }
82
+ </script>
83
+
84
+ <template>
85
+ <UPageCard class="w-md" variant="naked">
86
+ <div class="flex flex-col gap-6">
87
+ <div class="flex flex-col items-center gap-2">
88
+ <UIcon name="i-lucide-shield-check" class="size-12 text-primary" />
89
+ <h1 class="text-xl font-semibold">Zwei-Faktor-Authentifizierung</h1>
90
+ <p class="text-center text-sm text-muted">
91
+ {{ useBackupCode ? 'Gib einen deiner Backup-Codes ein' : 'Gib den 6-stelligen Code aus deiner Authenticator-App ein' }}
92
+ </p>
93
+ </div>
94
+
95
+ <UForm :schema="schema" class="flex flex-col gap-4" @submit="onSubmit">
96
+ <UFormField :label="useBackupCode ? 'Backup-Code' : 'Authentifizierungscode'" name="code">
97
+ <UInput
98
+ name="code"
99
+ :placeholder="useBackupCode ? 'Backup-Code eingeben' : '000000'"
100
+ size="lg"
101
+ class="text-center font-mono text-lg tracking-widest"
102
+ autocomplete="one-time-code"
103
+ />
104
+ </UFormField>
105
+
106
+ <UCheckbox v-if="!useBackupCode" v-model="trustDevice" label="Diesem Gerät vertrauen" />
107
+
108
+ <UButton type="submit" block :loading="loading"> Verifizieren </UButton>
109
+ </UForm>
110
+
111
+ <div class="flex flex-col items-center gap-2">
112
+ <UButton variant="link" color="neutral" @click="toggleBackupCode">
113
+ {{ useBackupCode ? 'Authenticator-Code verwenden' : 'Backup-Code verwenden' }}
114
+ </UButton>
115
+
116
+ <ULink to="/auth/login" class="text-sm text-muted hover:text-primary"> Zurück zur Anmeldung </ULink>
117
+ </div>
118
+ </div>
119
+ </UPageCard>
120
+ </template>
@@ -7,6 +7,14 @@ import type { InferOutput } from 'valibot';
7
7
 
8
8
  import * as v from 'valibot';
9
9
 
10
+ import { authClient } from '~/lib/auth-client';
11
+
12
+ // ============================================================================
13
+ // Composables
14
+ // ============================================================================
15
+ const toast = useToast();
16
+ const config = useRuntimeConfig();
17
+
10
18
  // ============================================================================
11
19
  // Page Meta
12
20
  // ============================================================================
@@ -17,18 +25,21 @@ definePageMeta({
17
25
  // ============================================================================
18
26
  // Variables
19
27
  // ============================================================================
28
+ const loading = ref<boolean>(false);
29
+ const emailSent = ref<boolean>(false);
30
+
20
31
  const fields: AuthFormField[] = [
21
32
  {
22
- label: 'Email',
33
+ label: 'E-Mail',
23
34
  name: 'email',
24
- placeholder: 'Enter your email',
35
+ placeholder: 'E-Mail eingeben',
25
36
  required: true,
26
37
  type: 'email',
27
38
  },
28
39
  ];
29
40
 
30
41
  const schema = v.object({
31
- email: v.pipe(v.string('Email is required'), v.email('Has to be a valid email address')),
42
+ email: v.pipe(v.string('E-Mail ist erforderlich'), v.email('Bitte eine gültige E-Mail eingeben')),
32
43
  });
33
44
 
34
45
  type Schema = InferOutput<typeof schema>;
@@ -37,28 +48,68 @@ type Schema = InferOutput<typeof schema>;
37
48
  // Functions
38
49
  // ============================================================================
39
50
  async function onSubmit(payload: FormSubmitEvent<Schema>): Promise<void> {
40
- console.debug('Forgot password request for:', payload.data.email);
51
+ loading.value = true;
52
+
53
+ try {
54
+ const { error } = await authClient.requestPasswordReset({
55
+ email: payload.data.email,
56
+ redirectTo: `${config.public.siteUrl}/auth/reset-password`,
57
+ });
58
+
59
+ if (error) {
60
+ toast.add({
61
+ color: 'error',
62
+ description: error.message || 'Anfrage fehlgeschlagen',
63
+ title: 'Fehler',
64
+ });
65
+ return;
66
+ }
67
+
68
+ emailSent.value = true;
69
+ } finally {
70
+ loading.value = false;
71
+ }
41
72
  }
42
73
  </script>
43
74
 
44
75
  <template>
45
76
  <UPageCard class="w-md" variant="naked">
46
- <UAuthForm
47
- :schema="schema"
48
- title="Forgot password"
49
- icon="i-heroicons-lock-closed"
50
- :fields="fields"
51
- loadingAuto
52
- :submit="{
53
- label: 'Continue',
54
- block: true,
55
- }"
56
- @submit="onSubmit"
57
- >
58
- <template #footer>
59
- Back to
60
- <ULink to="/auth/login" class="text-primary font-medium" tabindex="-1">Sign In</ULink>
61
- </template>
62
- </UAuthForm>
77
+ <template v-if="!emailSent">
78
+ <UAuthForm
79
+ :schema="schema"
80
+ title="Passwort vergessen"
81
+ icon="i-lucide-lock"
82
+ :fields="fields"
83
+ :loading="loading"
84
+ :submit="{
85
+ label: 'Link anfordern',
86
+ block: true,
87
+ }"
88
+ @submit="onSubmit"
89
+ >
90
+ <template #description>
91
+ <p class="text-sm text-muted">Gib deine E-Mail-Adresse ein und wir senden dir einen Link zum Zurücksetzen deines Passworts.</p>
92
+ </template>
93
+
94
+ <template #footer>
95
+ <p class="text-center text-sm text-muted">
96
+ Zurück zur
97
+ <ULink to="/auth/login" class="text-primary font-medium">Anmeldung</ULink>
98
+ </p>
99
+ </template>
100
+ </UAuthForm>
101
+ </template>
102
+
103
+ <template v-else>
104
+ <div class="flex flex-col items-center gap-6">
105
+ <UIcon name="i-lucide-mail-check" class="size-16 text-success" />
106
+ <div class="text-center">
107
+ <h2 class="text-xl font-semibold">E-Mail gesendet</h2>
108
+ <p class="mt-2 text-sm text-muted">Wir haben dir eine E-Mail mit einem Link zum Zurücksetzen deines Passworts gesendet. Bitte überprüfe auch deinen Spam-Ordner.</p>
109
+ </div>
110
+
111
+ <UButton to="/auth/login" variant="outline" color="neutral"> Zurück zur Anmeldung </UButton>
112
+ </div>
113
+ </template>
63
114
  </UPageCard>
64
115
  </template>