create-nuxt-base 1.0.3 → 1.1.1
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/AUTH.md +289 -0
- package/CHANGELOG.md +24 -0
- package/README.md +41 -33
- package/nuxt-base-template/app/composables/use-better-auth.ts +393 -8
- package/nuxt-base-template/app/lib/auth-client.ts +175 -78
- package/nuxt-base-template/app/pages/app/index.vue +101 -0
- package/nuxt-base-template/app/pages/app/settings/security.vue +30 -13
- package/nuxt-base-template/app/pages/auth/2fa.vue +23 -8
- package/nuxt-base-template/app/pages/auth/login.vue +71 -7
- package/nuxt-base-template/app/pages/auth/register.vue +49 -77
- package/nuxt-base-template/app/utils/crypto.ts +31 -0
- package/nuxt-base-template/server/api/iam/[...path].ts +65 -0
- package/package.json +1 -1
|
@@ -34,102 +34,199 @@ export interface AuthResponse {
|
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
baseURL
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Configuration options for the auth client factory
|
|
39
|
+
* All options have sensible defaults for nest-server compatibility
|
|
40
|
+
*/
|
|
41
|
+
export interface AuthClientConfig {
|
|
42
|
+
/** API base URL (default: from env or http://localhost:3000) */
|
|
43
|
+
baseURL?: string;
|
|
44
|
+
/** Auth API base path (default: '/iam' - must match nest-server betterAuth.basePath) */
|
|
45
|
+
basePath?: string;
|
|
46
|
+
/** 2FA redirect path (default: '/auth/2fa') */
|
|
47
|
+
twoFactorRedirectPath?: string;
|
|
48
|
+
/** Enable admin plugin (default: true) */
|
|
49
|
+
enableAdmin?: boolean;
|
|
50
|
+
/** Enable 2FA plugin (default: true) */
|
|
51
|
+
enableTwoFactor?: boolean;
|
|
52
|
+
/** Enable passkey plugin (default: true) */
|
|
53
|
+
enablePasskey?: boolean;
|
|
54
|
+
}
|
|
54
55
|
|
|
55
56
|
// =============================================================================
|
|
56
|
-
// Auth Client
|
|
57
|
+
// Auth Client Factory
|
|
57
58
|
// =============================================================================
|
|
58
59
|
|
|
59
60
|
/**
|
|
60
|
-
*
|
|
61
|
+
* Creates a configured Better-Auth client with password hashing
|
|
62
|
+
*
|
|
63
|
+
* This factory function allows creating auth clients with custom configuration,
|
|
64
|
+
* making it reusable across different projects.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```typescript
|
|
68
|
+
* // Default configuration (works with nest-server defaults)
|
|
69
|
+
* const authClient = createBetterAuthClient();
|
|
70
|
+
*
|
|
71
|
+
* // Custom configuration
|
|
72
|
+
* const authClient = createBetterAuthClient({
|
|
73
|
+
* baseURL: 'https://api.example.com',
|
|
74
|
+
* basePath: '/auth',
|
|
75
|
+
* twoFactorRedirectPath: '/login/2fa',
|
|
76
|
+
* });
|
|
77
|
+
* ```
|
|
61
78
|
*
|
|
62
79
|
* SECURITY: Passwords are hashed with SHA256 client-side to prevent
|
|
63
80
|
* plain text password transmission over the network.
|
|
64
|
-
*
|
|
65
|
-
* The server's normalizePasswordForIam() detects SHA256 hashes (64 hex chars)
|
|
66
|
-
* and processes them correctly.
|
|
67
81
|
*/
|
|
68
|
-
export
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
82
|
+
export function createBetterAuthClient(config: AuthClientConfig = {}) {
|
|
83
|
+
// In development, use empty baseURL and /api/iam path to leverage Nuxt server proxy
|
|
84
|
+
// This is REQUIRED for WebAuthn/Passkey to work correctly because:
|
|
85
|
+
// - Frontend runs on localhost:3002, API on localhost:3000
|
|
86
|
+
// - WebAuthn validates the origin, which must be consistent
|
|
87
|
+
// - The Nuxt server proxy ensures requests come from the frontend origin
|
|
88
|
+
const isDev = import.meta.env?.DEV || process.env.NODE_ENV === 'development';
|
|
89
|
+
const defaultBaseURL = isDev ? '' : (import.meta.env?.VITE_API_URL || process.env.API_URL || 'http://localhost:3000');
|
|
90
|
+
const defaultBasePath = isDev ? '/api/iam' : '/iam';
|
|
91
|
+
|
|
92
|
+
const {
|
|
93
|
+
baseURL = defaultBaseURL,
|
|
94
|
+
basePath = defaultBasePath,
|
|
95
|
+
twoFactorRedirectPath = '/auth/2fa',
|
|
96
|
+
enableAdmin = true,
|
|
97
|
+
enableTwoFactor = true,
|
|
98
|
+
enablePasskey = true,
|
|
99
|
+
} = config;
|
|
100
|
+
|
|
101
|
+
// Build plugins array based on configuration
|
|
102
|
+
const plugins: any[] = [];
|
|
103
|
+
|
|
104
|
+
if (enableAdmin) {
|
|
105
|
+
plugins.push(adminClient());
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (enableTwoFactor) {
|
|
109
|
+
plugins.push(
|
|
110
|
+
twoFactorClient({
|
|
111
|
+
onTwoFactorRedirect() {
|
|
112
|
+
navigateTo(twoFactorRedirectPath);
|
|
113
|
+
},
|
|
114
|
+
}),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (enablePasskey) {
|
|
119
|
+
plugins.push(passkeyClient());
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Create base client with configuration
|
|
123
|
+
const baseClient = createAuthClient({
|
|
124
|
+
basePath,
|
|
125
|
+
baseURL,
|
|
126
|
+
fetchOptions: {
|
|
127
|
+
credentials: 'include', // Required for cross-origin cookie handling
|
|
97
128
|
},
|
|
98
|
-
|
|
129
|
+
plugins,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Return extended client with password hashing
|
|
133
|
+
return {
|
|
134
|
+
// Spread all base client properties and methods
|
|
135
|
+
...baseClient,
|
|
99
136
|
|
|
100
|
-
|
|
101
|
-
|
|
137
|
+
// Explicitly pass through methods not captured by spread operator
|
|
138
|
+
useSession: baseClient.useSession,
|
|
139
|
+
passkey: (baseClient as any).passkey,
|
|
140
|
+
admin: (baseClient as any).admin,
|
|
141
|
+
$Infer: baseClient.$Infer,
|
|
142
|
+
$fetch: baseClient.$fetch,
|
|
143
|
+
$store: baseClient.$store,
|
|
102
144
|
|
|
103
|
-
// Override signUp to hash password
|
|
104
|
-
signUp: {
|
|
105
|
-
...baseClient.signUp,
|
|
106
145
|
/**
|
|
107
|
-
*
|
|
146
|
+
* Change password for an authenticated user (both passwords are hashed)
|
|
108
147
|
*/
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
return baseClient.
|
|
148
|
+
changePassword: async (params: { currentPassword: string; newPassword: string }, options?: any) => {
|
|
149
|
+
const [hashedCurrent, hashedNew] = await Promise.all([sha256(params.currentPassword), sha256(params.newPassword)]);
|
|
150
|
+
return baseClient.changePassword?.({ currentPassword: hashedCurrent, newPassword: hashedNew }, options);
|
|
112
151
|
},
|
|
113
|
-
},
|
|
114
152
|
|
|
115
|
-
// Override twoFactor to hash passwords
|
|
116
|
-
twoFactor: {
|
|
117
|
-
...baseClient.twoFactor,
|
|
118
153
|
/**
|
|
119
|
-
*
|
|
154
|
+
* Reset password with token (new password is hashed before sending)
|
|
120
155
|
*/
|
|
121
|
-
|
|
122
|
-
const hashedPassword = await sha256(params.
|
|
123
|
-
return baseClient.
|
|
156
|
+
resetPassword: async (params: { newPassword: string; token: string }, options?: any) => {
|
|
157
|
+
const hashedPassword = await sha256(params.newPassword);
|
|
158
|
+
return baseClient.resetPassword?.({ newPassword: hashedPassword, token: params.token }, options);
|
|
124
159
|
},
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
160
|
+
|
|
161
|
+
// Override signIn to hash password (keep passkey method from plugin)
|
|
162
|
+
signIn: {
|
|
163
|
+
...baseClient.signIn,
|
|
164
|
+
/**
|
|
165
|
+
* Sign in with email and password (password is hashed before sending)
|
|
166
|
+
*/
|
|
167
|
+
email: async (params: { email: string; password: string; rememberMe?: boolean }, options?: any) => {
|
|
168
|
+
const hashedPassword = await sha256(params.password);
|
|
169
|
+
return baseClient.signIn.email({ ...params, password: hashedPassword }, options);
|
|
170
|
+
},
|
|
171
|
+
/**
|
|
172
|
+
* Sign in with passkey (pass through to base client - provided by passkeyClient plugin)
|
|
173
|
+
* @see https://www.better-auth.com/docs/plugins/passkey
|
|
174
|
+
*/
|
|
175
|
+
passkey: (baseClient.signIn as any).passkey,
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
// Explicitly pass through signOut (not captured by spread operator)
|
|
179
|
+
signOut: baseClient.signOut,
|
|
180
|
+
|
|
181
|
+
// Override signUp to hash password
|
|
182
|
+
signUp: {
|
|
183
|
+
...baseClient.signUp,
|
|
184
|
+
/**
|
|
185
|
+
* Sign up with email and password (password is hashed before sending)
|
|
186
|
+
*/
|
|
187
|
+
email: async (params: { email: string; name: string; password: string }, options?: any) => {
|
|
188
|
+
const hashedPassword = await sha256(params.password);
|
|
189
|
+
return baseClient.signUp.email({ ...params, password: hashedPassword }, options);
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
// Override twoFactor to hash passwords (provided by twoFactorClient plugin)
|
|
194
|
+
twoFactor: {
|
|
195
|
+
...(baseClient as any).twoFactor,
|
|
196
|
+
/**
|
|
197
|
+
* Disable 2FA (password is hashed before sending)
|
|
198
|
+
*/
|
|
199
|
+
disable: async (params: { password: string }, options?: any) => {
|
|
200
|
+
const hashedPassword = await sha256(params.password);
|
|
201
|
+
return (baseClient as any).twoFactor.disable({ password: hashedPassword }, options);
|
|
202
|
+
},
|
|
203
|
+
/**
|
|
204
|
+
* Enable 2FA (password is hashed before sending)
|
|
205
|
+
*/
|
|
206
|
+
enable: async (params: { password: string }, options?: any) => {
|
|
207
|
+
const hashedPassword = await sha256(params.password);
|
|
208
|
+
return (baseClient as any).twoFactor.enable({ password: hashedPassword }, options);
|
|
209
|
+
},
|
|
210
|
+
/**
|
|
211
|
+
* Verify TOTP code (pass through to base client)
|
|
212
|
+
*/
|
|
213
|
+
verifyTotp: (baseClient as any).twoFactor.verifyTotp,
|
|
214
|
+
/**
|
|
215
|
+
* Verify backup code (pass through to base client)
|
|
216
|
+
*/
|
|
217
|
+
verifyBackupCode: (baseClient as any).twoFactor.verifyBackupCode,
|
|
131
218
|
},
|
|
132
|
-
}
|
|
133
|
-
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// =============================================================================
|
|
223
|
+
// Default Auth Client Instance
|
|
224
|
+
// =============================================================================
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Default auth client instance with standard nest-server configuration
|
|
228
|
+
* Use createBetterAuthClient() for custom configuration
|
|
229
|
+
*/
|
|
230
|
+
export const authClient = createBetterAuthClient();
|
|
134
231
|
|
|
135
|
-
export type AuthClient = typeof
|
|
232
|
+
export type AuthClient = ReturnType<typeof createBetterAuthClient>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// ============================================================================
|
|
3
|
+
// Composables
|
|
4
|
+
// ============================================================================
|
|
5
|
+
const { user, signOut } = useBetterAuth();
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Variables
|
|
9
|
+
// ============================================================================
|
|
10
|
+
const pages = [
|
|
11
|
+
{
|
|
12
|
+
title: 'Sicherheit',
|
|
13
|
+
description: 'Verwalte 2FA, Passkeys und Kontosicherheit',
|
|
14
|
+
icon: 'i-lucide-shield-check',
|
|
15
|
+
to: '/app/settings/security',
|
|
16
|
+
},
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Functions
|
|
21
|
+
// ============================================================================
|
|
22
|
+
async function handleSignOut(): Promise<void> {
|
|
23
|
+
await signOut();
|
|
24
|
+
await navigateTo('/auth/login');
|
|
25
|
+
}
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<template>
|
|
29
|
+
<div class="mx-auto max-w-4xl px-4 py-8">
|
|
30
|
+
<!-- Welcome Header -->
|
|
31
|
+
<div class="mb-8">
|
|
32
|
+
<h1 class="text-3xl font-bold">
|
|
33
|
+
Willkommen{{ user?.name ? `, ${user.name}` : '' }}!
|
|
34
|
+
</h1>
|
|
35
|
+
<p class="mt-2 text-muted">
|
|
36
|
+
{{ user?.email }}
|
|
37
|
+
</p>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<!-- Quick Actions -->
|
|
41
|
+
<div class="mb-8">
|
|
42
|
+
<h2 class="mb-4 text-xl font-semibold">Schnellzugriff</h2>
|
|
43
|
+
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
44
|
+
<UCard
|
|
45
|
+
v-for="page in pages"
|
|
46
|
+
:key="page.to"
|
|
47
|
+
class="cursor-pointer transition-shadow hover:shadow-lg"
|
|
48
|
+
@click="navigateTo(page.to)"
|
|
49
|
+
>
|
|
50
|
+
<div class="flex items-start gap-4">
|
|
51
|
+
<div class="rounded-lg bg-primary/10 p-3">
|
|
52
|
+
<UIcon :name="page.icon" class="size-6 text-primary" />
|
|
53
|
+
</div>
|
|
54
|
+
<div>
|
|
55
|
+
<h3 class="font-semibold">{{ page.title }}</h3>
|
|
56
|
+
<p class="mt-1 text-sm text-muted">{{ page.description }}</p>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</UCard>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<!-- User Info Card -->
|
|
64
|
+
<UCard>
|
|
65
|
+
<template #header>
|
|
66
|
+
<div class="flex items-center gap-3">
|
|
67
|
+
<UIcon name="i-lucide-user" class="size-6 text-primary" />
|
|
68
|
+
<h2 class="font-semibold">Dein Konto</h2>
|
|
69
|
+
</div>
|
|
70
|
+
</template>
|
|
71
|
+
|
|
72
|
+
<div class="space-y-3">
|
|
73
|
+
<div class="flex justify-between">
|
|
74
|
+
<span class="text-muted">Name</span>
|
|
75
|
+
<span class="font-medium">{{ user?.name || '-' }}</span>
|
|
76
|
+
</div>
|
|
77
|
+
<div class="flex justify-between">
|
|
78
|
+
<span class="text-muted">E-Mail</span>
|
|
79
|
+
<span class="font-medium">{{ user?.email || '-' }}</span>
|
|
80
|
+
</div>
|
|
81
|
+
<div class="flex justify-between">
|
|
82
|
+
<span class="text-muted">E-Mail verifiziert</span>
|
|
83
|
+
<UBadge :color="user?.emailVerified ? 'success' : 'warning'">
|
|
84
|
+
{{ user?.emailVerified ? 'Ja' : 'Nein' }}
|
|
85
|
+
</UBadge>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<template #footer>
|
|
90
|
+
<UButton
|
|
91
|
+
color="error"
|
|
92
|
+
variant="outline"
|
|
93
|
+
icon="i-lucide-log-out"
|
|
94
|
+
@click="handleSignOut"
|
|
95
|
+
>
|
|
96
|
+
Abmelden
|
|
97
|
+
</UButton>
|
|
98
|
+
</template>
|
|
99
|
+
</UCard>
|
|
100
|
+
</div>
|
|
101
|
+
</template>
|
|
@@ -24,7 +24,7 @@ interface Passkey {
|
|
|
24
24
|
// ============================================================================
|
|
25
25
|
const toast = useToast();
|
|
26
26
|
const overlay = useOverlay();
|
|
27
|
-
const { is2FAEnabled, user } = useBetterAuth();
|
|
27
|
+
const { is2FAEnabled, registerPasskey, setUser, user } = useBetterAuth();
|
|
28
28
|
|
|
29
29
|
// ============================================================================
|
|
30
30
|
// Variables
|
|
@@ -39,6 +39,11 @@ const passkeyLoading = ref<boolean>(false);
|
|
|
39
39
|
const newPasskeyName = ref<string>('');
|
|
40
40
|
const showAddPasskey = ref<boolean>(false);
|
|
41
41
|
|
|
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
|
+
|
|
42
47
|
const passwordSchema = v.object({
|
|
43
48
|
password: v.pipe(v.string('Passwort ist erforderlich'), v.minLength(1, 'Passwort ist erforderlich')),
|
|
44
49
|
});
|
|
@@ -70,14 +75,13 @@ async function addPasskey(): Promise<void> {
|
|
|
70
75
|
passkeyLoading.value = true;
|
|
71
76
|
|
|
72
77
|
try {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
});
|
|
78
|
+
// Use custom registerPasskey method with direct API calls
|
|
79
|
+
const result = await registerPasskey(newPasskeyName.value);
|
|
76
80
|
|
|
77
|
-
if (
|
|
81
|
+
if (!result.success) {
|
|
78
82
|
toast.add({
|
|
79
83
|
color: 'error',
|
|
80
|
-
description: error
|
|
84
|
+
description: result.error || 'Passkey konnte nicht hinzugefügt werden',
|
|
81
85
|
title: 'Fehler',
|
|
82
86
|
});
|
|
83
87
|
return;
|
|
@@ -143,6 +147,11 @@ async function disable2FA(payload: FormSubmitEvent<PasswordSchema>): Promise<voi
|
|
|
143
147
|
return;
|
|
144
148
|
}
|
|
145
149
|
|
|
150
|
+
// Update user state to reflect 2FA disabled
|
|
151
|
+
if (user.value) {
|
|
152
|
+
setUser({ ...user.value, twoFactorEnabled: false });
|
|
153
|
+
}
|
|
154
|
+
|
|
146
155
|
toast.add({
|
|
147
156
|
color: 'success',
|
|
148
157
|
description: '2FA wurde deaktiviert',
|
|
@@ -150,6 +159,7 @@ async function disable2FA(payload: FormSubmitEvent<PasswordSchema>): Promise<voi
|
|
|
150
159
|
});
|
|
151
160
|
|
|
152
161
|
show2FADisable.value = false;
|
|
162
|
+
disable2FAForm.password = '';
|
|
153
163
|
} finally {
|
|
154
164
|
loading.value = false;
|
|
155
165
|
}
|
|
@@ -175,6 +185,7 @@ async function enable2FA(payload: FormSubmitEvent<PasswordSchema>): Promise<void
|
|
|
175
185
|
totpUri.value = data?.totpURI ?? '';
|
|
176
186
|
backupCodes.value = data?.backupCodes ?? [];
|
|
177
187
|
showTotpSetup.value = true;
|
|
188
|
+
enable2FAForm.password = '';
|
|
178
189
|
} finally {
|
|
179
190
|
loading.value = false;
|
|
180
191
|
}
|
|
@@ -224,6 +235,11 @@ async function verifyTotp(payload: FormSubmitEvent<TotpSchema>): Promise<void> {
|
|
|
224
235
|
return;
|
|
225
236
|
}
|
|
226
237
|
|
|
238
|
+
// Update user state to reflect 2FA enabled
|
|
239
|
+
if (user.value) {
|
|
240
|
+
setUser({ ...user.value, twoFactorEnabled: true });
|
|
241
|
+
}
|
|
242
|
+
|
|
227
243
|
toast.add({
|
|
228
244
|
color: 'success',
|
|
229
245
|
description: '2FA wurde erfolgreich aktiviert',
|
|
@@ -231,6 +247,7 @@ async function verifyTotp(payload: FormSubmitEvent<TotpSchema>): Promise<void> {
|
|
|
231
247
|
});
|
|
232
248
|
|
|
233
249
|
showTotpSetup.value = false;
|
|
250
|
+
totpForm.code = '';
|
|
234
251
|
await openBackupCodesModal(backupCodes.value);
|
|
235
252
|
} finally {
|
|
236
253
|
loading.value = false;
|
|
@@ -271,9 +288,9 @@ async function verifyTotp(payload: FormSubmitEvent<TotpSchema>): Promise<void> {
|
|
|
271
288
|
</div>
|
|
272
289
|
|
|
273
290
|
<template v-if="!is2FAEnabled && !showTotpSetup">
|
|
274
|
-
<UForm :schema="passwordSchema" class="space-y-4" @submit="enable2FA">
|
|
291
|
+
<UForm :schema="passwordSchema" :state="enable2FAForm" class="space-y-4" @submit="enable2FA">
|
|
275
292
|
<UFormField label="Passwort bestätigen" name="password">
|
|
276
|
-
<UInput
|
|
293
|
+
<UInput v-model="enable2FAForm.password" type="password" placeholder="Dein Passwort" />
|
|
277
294
|
</UFormField>
|
|
278
295
|
<UButton type="submit" :loading="loading"> 2FA aktivieren </UButton>
|
|
279
296
|
</UForm>
|
|
@@ -281,15 +298,15 @@ async function verifyTotp(payload: FormSubmitEvent<TotpSchema>): Promise<void> {
|
|
|
281
298
|
|
|
282
299
|
<template v-if="showTotpSetup">
|
|
283
300
|
<div class="space-y-4">
|
|
284
|
-
<
|
|
301
|
+
<p class="text-sm text-muted">Scanne den QR-Code mit deiner Authenticator-App (z.B. Google Authenticator, Authy) und gib den Code ein.</p>
|
|
285
302
|
|
|
286
303
|
<div class="flex justify-center">
|
|
287
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" />
|
|
288
305
|
</div>
|
|
289
306
|
|
|
290
|
-
<UForm :schema="totpSchema" class="space-y-4" @submit="verifyTotp">
|
|
307
|
+
<UForm :schema="totpSchema" :state="totpForm" class="space-y-4" @submit="verifyTotp">
|
|
291
308
|
<UFormField label="Verifizierungscode" name="code">
|
|
292
|
-
<UInput
|
|
309
|
+
<UInput v-model="totpForm.code" placeholder="000000" class="text-center font-mono" />
|
|
293
310
|
</UFormField>
|
|
294
311
|
<div class="flex gap-2">
|
|
295
312
|
<UButton type="submit" :loading="loading"> Verifizieren </UButton>
|
|
@@ -307,10 +324,10 @@ async function verifyTotp(payload: FormSubmitEvent<TotpSchema>): Promise<void> {
|
|
|
307
324
|
</template>
|
|
308
325
|
|
|
309
326
|
<template v-if="show2FADisable">
|
|
310
|
-
<UForm :schema="passwordSchema" class="space-y-4" @submit="disable2FA">
|
|
327
|
+
<UForm :schema="passwordSchema" :state="disable2FAForm" class="space-y-4" @submit="disable2FA">
|
|
311
328
|
<UAlert color="warning" icon="i-lucide-alert-triangle"> 2FA zu deaktivieren verringert die Sicherheit deines Kontos. </UAlert>
|
|
312
329
|
<UFormField label="Passwort bestätigen" name="password">
|
|
313
|
-
<UInput
|
|
330
|
+
<UInput v-model="disable2FAForm.password" type="password" placeholder="Dein Passwort" />
|
|
314
331
|
</UFormField>
|
|
315
332
|
<div class="flex gap-2">
|
|
316
333
|
<UButton type="submit" color="error" :loading="loading"> 2FA deaktivieren </UButton>
|
|
@@ -13,6 +13,7 @@ import { authClient } from '~/lib/auth-client';
|
|
|
13
13
|
// Composables
|
|
14
14
|
// ============================================================================
|
|
15
15
|
const toast = useToast();
|
|
16
|
+
const { setUser, validateSession } = useBetterAuth();
|
|
16
17
|
|
|
17
18
|
// ============================================================================
|
|
18
19
|
// Page Meta
|
|
@@ -28,6 +29,9 @@ const loading = ref<boolean>(false);
|
|
|
28
29
|
const useBackupCode = ref<boolean>(false);
|
|
29
30
|
const trustDevice = ref<boolean>(false);
|
|
30
31
|
|
|
32
|
+
// Form state for UForm
|
|
33
|
+
const formState = reactive({ code: '' });
|
|
34
|
+
|
|
31
35
|
const schema = v.object({
|
|
32
36
|
code: v.pipe(v.string('Code ist erforderlich'), v.minLength(6, 'Code muss mindestens 6 Zeichen haben')),
|
|
33
37
|
});
|
|
@@ -41,35 +45,46 @@ async function onSubmit(payload: FormSubmitEvent<Schema>): Promise<void> {
|
|
|
41
45
|
loading.value = true;
|
|
42
46
|
|
|
43
47
|
try {
|
|
48
|
+
let result: any;
|
|
49
|
+
|
|
44
50
|
if (useBackupCode.value) {
|
|
45
|
-
|
|
51
|
+
result = await authClient.twoFactor.verifyBackupCode({
|
|
46
52
|
code: payload.data.code,
|
|
47
53
|
});
|
|
48
54
|
|
|
49
|
-
if (error) {
|
|
55
|
+
if (result.error) {
|
|
50
56
|
toast.add({
|
|
51
57
|
color: 'error',
|
|
52
|
-
description: error.message || 'Backup-Code ungültig',
|
|
58
|
+
description: result.error.message || 'Backup-Code ungültig',
|
|
53
59
|
title: 'Fehler',
|
|
54
60
|
});
|
|
55
61
|
return;
|
|
56
62
|
}
|
|
57
63
|
} else {
|
|
58
|
-
|
|
64
|
+
result = await authClient.twoFactor.verifyTotp({
|
|
59
65
|
code: payload.data.code,
|
|
60
66
|
trustDevice: trustDevice.value,
|
|
61
67
|
});
|
|
62
68
|
|
|
63
|
-
if (error) {
|
|
69
|
+
if (result.error) {
|
|
64
70
|
toast.add({
|
|
65
71
|
color: 'error',
|
|
66
|
-
description: error.message || 'Code ungültig',
|
|
72
|
+
description: result.error.message || 'Code ungültig',
|
|
67
73
|
title: 'Fehler',
|
|
68
74
|
});
|
|
69
75
|
return;
|
|
70
76
|
}
|
|
71
77
|
}
|
|
72
78
|
|
|
79
|
+
// Update auth state with user data from response
|
|
80
|
+
const userData = result?.data?.user || result?.user;
|
|
81
|
+
if (userData) {
|
|
82
|
+
setUser(userData);
|
|
83
|
+
} else {
|
|
84
|
+
// Fallback: validate session to get user data
|
|
85
|
+
await validateSession();
|
|
86
|
+
}
|
|
87
|
+
|
|
73
88
|
await navigateTo('/app');
|
|
74
89
|
} finally {
|
|
75
90
|
loading.value = false;
|
|
@@ -92,10 +107,10 @@ function toggleBackupCode(): void {
|
|
|
92
107
|
</p>
|
|
93
108
|
</div>
|
|
94
109
|
|
|
95
|
-
<UForm :schema="schema" class="flex flex-col gap-4" @submit="onSubmit">
|
|
110
|
+
<UForm :schema="schema" :state="formState" class="flex flex-col gap-4" @submit="onSubmit">
|
|
96
111
|
<UFormField :label="useBackupCode ? 'Backup-Code' : 'Authentifizierungscode'" name="code">
|
|
97
112
|
<UInput
|
|
98
|
-
|
|
113
|
+
v-model="formState.code"
|
|
99
114
|
:placeholder="useBackupCode ? 'Backup-Code eingeben' : '000000'"
|
|
100
115
|
size="lg"
|
|
101
116
|
class="text-center font-mono text-lg tracking-widest"
|