create-nuxt-base 0.3.17 → 1.0.3
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/.github/workflows/publish.yml +4 -2
- package/.oxfmtrc.jsonc +7 -0
- package/CHANGELOG.md +22 -8
- package/README.md +130 -3
- package/nuxt-base-template/.dockerignore +44 -0
- package/nuxt-base-template/.env.example +0 -2
- package/nuxt-base-template/.nuxtrc +1 -0
- package/nuxt-base-template/.oxfmtrc.jsonc +8 -0
- package/nuxt-base-template/Dockerfile.dev +23 -0
- package/nuxt-base-template/README.md +76 -29
- package/nuxt-base-template/app/components/Modal/ModalBackupCodes.vue +117 -0
- package/nuxt-base-template/app/components/Upload/TusFileUpload.vue +302 -0
- package/nuxt-base-template/app/composables/use-better-auth.ts +25 -0
- package/nuxt-base-template/app/composables/use-file.ts +39 -4
- package/nuxt-base-template/app/composables/use-share.ts +1 -1
- package/nuxt-base-template/app/composables/use-tus-upload.ts +278 -0
- package/nuxt-base-template/app/interfaces/upload.interface.ts +58 -0
- package/nuxt-base-template/app/interfaces/user.interface.ts +12 -0
- package/nuxt-base-template/app/lib/auth-client.ts +135 -0
- package/nuxt-base-template/app/middleware/admin.global.ts +23 -0
- package/nuxt-base-template/app/middleware/auth.global.ts +18 -0
- package/nuxt-base-template/app/middleware/guest.global.ts +18 -0
- package/nuxt-base-template/app/pages/app/settings/security.vue +409 -0
- package/nuxt-base-template/app/pages/auth/2fa.vue +120 -0
- package/nuxt-base-template/app/pages/auth/forgot-password.vue +72 -21
- package/nuxt-base-template/app/pages/auth/login.vue +75 -11
- package/nuxt-base-template/app/pages/auth/register.vue +184 -0
- package/nuxt-base-template/app/pages/auth/reset-password.vue +153 -0
- package/nuxt-base-template/app/utils/crypto.ts +13 -0
- package/nuxt-base-template/docker-entrypoint.sh +21 -0
- package/nuxt-base-template/nuxt.config.ts +4 -1
- package/nuxt-base-template/oxlint.json +14 -0
- package/nuxt-base-template/package-lock.json +11582 -10675
- package/nuxt-base-template/package.json +35 -32
- package/nuxt-base-template/tests/iam.spec.ts +247 -0
- package/package.json +14 -11
- package/.eslintignore +0 -14
- package/.eslintrc +0 -3
- package/.prettierignore +0 -5
- package/.prettierrc +0 -6
- package/nuxt-base-template/CLAUDE.md +0 -361
- package/nuxt-base-template/app/pages/auth/reset-password/[token].vue +0 -110
- package/nuxt-base-template/app/public/favicon.ico +0 -0
- package/nuxt-base-template/eslint.config.mjs +0 -4
|
@@ -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: '
|
|
33
|
+
label: 'E-Mail',
|
|
23
34
|
name: 'email',
|
|
24
|
-
placeholder: '
|
|
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('
|
|
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
|
-
|
|
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
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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>
|
|
@@ -7,6 +7,13 @@ 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
|
+
|
|
10
17
|
// ============================================================================
|
|
11
18
|
// Page Meta
|
|
12
19
|
// ============================================================================
|
|
@@ -17,35 +24,79 @@ definePageMeta({
|
|
|
17
24
|
// ============================================================================
|
|
18
25
|
// Variables
|
|
19
26
|
// ============================================================================
|
|
27
|
+
const loading = ref<boolean>(false);
|
|
28
|
+
const passkeyLoading = ref<boolean>(false);
|
|
29
|
+
|
|
20
30
|
const fields: AuthFormField[] = [
|
|
21
31
|
{
|
|
22
|
-
label: '
|
|
32
|
+
label: 'E-Mail',
|
|
23
33
|
name: 'email',
|
|
24
|
-
placeholder: '
|
|
34
|
+
placeholder: 'E-Mail eingeben',
|
|
25
35
|
required: true,
|
|
26
36
|
type: 'email',
|
|
27
37
|
},
|
|
28
38
|
{
|
|
29
|
-
label: '
|
|
39
|
+
label: 'Passwort',
|
|
30
40
|
name: 'password',
|
|
31
|
-
placeholder: '
|
|
41
|
+
placeholder: 'Passwort eingeben',
|
|
32
42
|
required: true,
|
|
33
43
|
type: 'password',
|
|
34
44
|
},
|
|
35
45
|
];
|
|
36
46
|
|
|
37
47
|
const schema = v.object({
|
|
38
|
-
email: v.pipe(v.string('
|
|
39
|
-
password: v.pipe(v.string('
|
|
48
|
+
email: v.pipe(v.string('E-Mail ist erforderlich'), v.email('Bitte eine gültige E-Mail eingeben')),
|
|
49
|
+
password: v.pipe(v.string('Passwort ist erforderlich'), v.minLength(5, 'Mindestens 5 Zeichen erforderlich')),
|
|
40
50
|
});
|
|
41
51
|
|
|
42
52
|
type Schema = InferOutput<typeof schema>;
|
|
43
53
|
|
|
54
|
+
async function onPasskeyLogin(): Promise<void> {
|
|
55
|
+
passkeyLoading.value = true;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const { error } = await authClient.signIn.passkey();
|
|
59
|
+
|
|
60
|
+
if (error) {
|
|
61
|
+
toast.add({
|
|
62
|
+
color: 'error',
|
|
63
|
+
description: error.message || 'Passkey-Anmeldung fehlgeschlagen',
|
|
64
|
+
title: 'Fehler',
|
|
65
|
+
});
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
await navigateTo('/app');
|
|
70
|
+
} finally {
|
|
71
|
+
passkeyLoading.value = false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
44
75
|
// ============================================================================
|
|
45
76
|
// Functions
|
|
46
77
|
// ============================================================================
|
|
47
78
|
async function onSubmit(payload: FormSubmitEvent<Schema>): Promise<void> {
|
|
48
|
-
|
|
79
|
+
loading.value = true;
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const { error } = await authClient.signIn.email({
|
|
83
|
+
email: payload.data.email,
|
|
84
|
+
password: payload.data.password,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (error) {
|
|
88
|
+
toast.add({
|
|
89
|
+
color: 'error',
|
|
90
|
+
description: error.message || 'Anmeldung fehlgeschlagen',
|
|
91
|
+
title: 'Fehler',
|
|
92
|
+
});
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
await navigateTo('/app');
|
|
97
|
+
} finally {
|
|
98
|
+
loading.value = false;
|
|
99
|
+
}
|
|
49
100
|
}
|
|
50
101
|
</script>
|
|
51
102
|
|
|
@@ -53,18 +104,31 @@ async function onSubmit(payload: FormSubmitEvent<Schema>): Promise<void> {
|
|
|
53
104
|
<UPageCard class="w-md" variant="naked">
|
|
54
105
|
<UAuthForm
|
|
55
106
|
:schema="schema"
|
|
56
|
-
title="
|
|
107
|
+
title="Anmelden"
|
|
57
108
|
icon="i-lucide-user"
|
|
58
109
|
:fields="fields"
|
|
59
|
-
|
|
110
|
+
:loading="loading"
|
|
60
111
|
:submit="{
|
|
61
|
-
label: '
|
|
112
|
+
label: 'Anmelden',
|
|
62
113
|
block: true,
|
|
63
114
|
}"
|
|
64
115
|
@submit="onSubmit"
|
|
65
116
|
>
|
|
66
117
|
<template #password-hint>
|
|
67
|
-
<ULink to="/auth/forgot-password" class="text-primary font-medium" tabindex="-1">
|
|
118
|
+
<ULink to="/auth/forgot-password" class="text-primary font-medium" tabindex="-1">Passwort vergessen?</ULink>
|
|
119
|
+
</template>
|
|
120
|
+
|
|
121
|
+
<template #footer>
|
|
122
|
+
<div class="flex flex-col gap-4">
|
|
123
|
+
<USeparator label="oder" />
|
|
124
|
+
|
|
125
|
+
<UButton block color="neutral" variant="outline" icon="i-lucide-key" :loading="passkeyLoading" @click="onPasskeyLogin"> Mit Passkey anmelden </UButton>
|
|
126
|
+
|
|
127
|
+
<p class="text-center text-sm text-muted">
|
|
128
|
+
Noch kein Konto?
|
|
129
|
+
<ULink to="/auth/register" class="text-primary font-medium">Registrieren</ULink>
|
|
130
|
+
</p>
|
|
131
|
+
</div>
|
|
68
132
|
</template>
|
|
69
133
|
</UAuthForm>
|
|
70
134
|
</UPageCard>
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// ============================================================================
|
|
3
|
+
// Imports
|
|
4
|
+
// ============================================================================
|
|
5
|
+
import type { AuthFormField, 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 showPasskeyPrompt = ref<boolean>(false);
|
|
29
|
+
const passkeyLoading = ref<boolean>(false);
|
|
30
|
+
|
|
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
|
+
];
|
|
61
|
+
|
|
62
|
+
const schema = v.pipe(
|
|
63
|
+
v.object({
|
|
64
|
+
confirmPassword: v.pipe(v.string('Passwortbestätigung ist erforderlich'), v.minLength(8, 'Mindestens 8 Zeichen erforderlich')),
|
|
65
|
+
email: v.pipe(v.string('E-Mail ist erforderlich'), v.email('Bitte eine gültige E-Mail eingeben')),
|
|
66
|
+
name: v.pipe(v.string('Name ist erforderlich'), v.minLength(2, 'Mindestens 2 Zeichen erforderlich')),
|
|
67
|
+
password: v.pipe(v.string('Passwort ist erforderlich'), v.minLength(8, 'Mindestens 8 Zeichen erforderlich')),
|
|
68
|
+
}),
|
|
69
|
+
v.forward(
|
|
70
|
+
v.partialCheck([['password'], ['confirmPassword']], (input) => input.password === input.confirmPassword, 'Passwörter stimmen nicht überein'),
|
|
71
|
+
['confirmPassword'],
|
|
72
|
+
),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
type Schema = InferOutput<typeof schema>;
|
|
76
|
+
|
|
77
|
+
async function addPasskey(): Promise<void> {
|
|
78
|
+
passkeyLoading.value = true;
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const { error } = await authClient.passkey.addPasskey({
|
|
82
|
+
name: 'Mein Gerät',
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (error) {
|
|
86
|
+
toast.add({
|
|
87
|
+
color: 'error',
|
|
88
|
+
description: error.message || 'Passkey konnte nicht hinzugefügt werden',
|
|
89
|
+
title: 'Fehler',
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
toast.add({
|
|
95
|
+
color: 'success',
|
|
96
|
+
description: 'Passkey wurde erfolgreich hinzugefügt',
|
|
97
|
+
title: 'Erfolg',
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
await navigateTo('/app');
|
|
101
|
+
} finally {
|
|
102
|
+
passkeyLoading.value = false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ============================================================================
|
|
107
|
+
// Functions
|
|
108
|
+
// ============================================================================
|
|
109
|
+
async function onSubmit(payload: FormSubmitEvent<Schema>): Promise<void> {
|
|
110
|
+
loading.value = true;
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const { error } = await authClient.signUp.email({
|
|
114
|
+
email: payload.data.email,
|
|
115
|
+
name: payload.data.name,
|
|
116
|
+
password: payload.data.password,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (error) {
|
|
120
|
+
toast.add({
|
|
121
|
+
color: 'error',
|
|
122
|
+
description: error.message || 'Registrierung fehlgeschlagen',
|
|
123
|
+
title: 'Fehler',
|
|
124
|
+
});
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
toast.add({
|
|
129
|
+
color: 'success',
|
|
130
|
+
description: 'Dein Konto wurde erfolgreich erstellt',
|
|
131
|
+
title: 'Willkommen!',
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
showPasskeyPrompt.value = true;
|
|
135
|
+
} finally {
|
|
136
|
+
loading.value = false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function skipPasskey(): Promise<void> {
|
|
141
|
+
await navigateTo('/app');
|
|
142
|
+
}
|
|
143
|
+
</script>
|
|
144
|
+
|
|
145
|
+
<template>
|
|
146
|
+
<UPageCard class="w-md" variant="naked">
|
|
147
|
+
<template v-if="!showPasskeyPrompt">
|
|
148
|
+
<UAuthForm
|
|
149
|
+
:schema="schema"
|
|
150
|
+
title="Registrieren"
|
|
151
|
+
icon="i-lucide-user-plus"
|
|
152
|
+
:fields="fields"
|
|
153
|
+
:loading="loading"
|
|
154
|
+
:submit="{
|
|
155
|
+
label: 'Konto erstellen',
|
|
156
|
+
block: true,
|
|
157
|
+
}"
|
|
158
|
+
@submit="onSubmit"
|
|
159
|
+
>
|
|
160
|
+
<template #footer>
|
|
161
|
+
<p class="text-center text-sm text-muted">
|
|
162
|
+
Bereits ein Konto?
|
|
163
|
+
<ULink to="/auth/login" class="text-primary font-medium">Anmelden</ULink>
|
|
164
|
+
</p>
|
|
165
|
+
</template>
|
|
166
|
+
</UAuthForm>
|
|
167
|
+
</template>
|
|
168
|
+
|
|
169
|
+
<template v-else>
|
|
170
|
+
<div class="flex flex-col items-center gap-6">
|
|
171
|
+
<UIcon name="i-lucide-key" class="size-16 text-primary" />
|
|
172
|
+
<div class="text-center">
|
|
173
|
+
<h2 class="text-xl font-semibold">Passkey hinzufügen?</h2>
|
|
174
|
+
<p class="mt-2 text-sm text-muted">Mit einem Passkey kannst du dich schnell und sicher ohne Passwort anmelden.</p>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<div class="flex w-full flex-col gap-3">
|
|
178
|
+
<UButton block :loading="passkeyLoading" @click="addPasskey"> Passkey hinzufügen </UButton>
|
|
179
|
+
<UButton block variant="outline" color="neutral" @click="skipPasskey"> Später einrichten </UButton>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
</template>
|
|
183
|
+
</UPageCard>
|
|
184
|
+
</template>
|