create-nuxt-base 1.2.0 → 2.0.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 +33 -0
- package/nuxt-base-template/app/components/Modal/ModalBackupCodes.vue +2 -1
- package/nuxt-base-template/app/components/Upload/TusFileUpload.vue +7 -7
- package/nuxt-base-template/app/interfaces/user.interface.ts +5 -12
- package/nuxt-base-template/app/layouts/default.vue +1 -1
- package/nuxt-base-template/app/middleware/admin.global.ts +2 -2
- package/nuxt-base-template/app/middleware/auth.global.ts +2 -2
- package/nuxt-base-template/app/middleware/guest.global.ts +2 -2
- package/nuxt-base-template/app/pages/app/index.vue +1 -1
- package/nuxt-base-template/app/pages/app/settings/security.vue +2 -2
- package/nuxt-base-template/app/pages/auth/2fa.vue +2 -3
- package/nuxt-base-template/app/pages/auth/forgot-password.vue +2 -1
- package/nuxt-base-template/app/pages/auth/login.vue +2 -2
- package/nuxt-base-template/app/pages/auth/register.vue +1 -1
- package/nuxt-base-template/app/pages/auth/reset-password.vue +2 -1
- package/nuxt-base-template/docs/pages/docs.vue +1 -1
- package/nuxt-base-template/nuxt.config.ts +38 -1
- package/nuxt-base-template/package-lock.json +136 -2905
- package/nuxt-base-template/package.json +1 -0
- package/package.json +1 -1
- package/nuxt-base-template/app/components/Transition/TransitionFade.vue +0 -27
- package/nuxt-base-template/app/components/Transition/TransitionFadeScale.vue +0 -27
- package/nuxt-base-template/app/components/Transition/TransitionSlide.vue +0 -12
- package/nuxt-base-template/app/components/Transition/TransitionSlideBottom.vue +0 -12
- package/nuxt-base-template/app/components/Transition/TransitionSlideRevert.vue +0 -12
- package/nuxt-base-template/app/composables/use-better-auth.ts +0 -597
- package/nuxt-base-template/app/composables/use-file.ts +0 -71
- package/nuxt-base-template/app/composables/use-share.ts +0 -38
- package/nuxt-base-template/app/composables/use-tus-upload.ts +0 -278
- package/nuxt-base-template/app/composables/use-tw.ts +0 -1
- package/nuxt-base-template/app/interfaces/upload.interface.ts +0 -58
- package/nuxt-base-template/app/lib/auth-client.ts +0 -229
- package/nuxt-base-template/app/lib/auth-state.ts +0 -206
- package/nuxt-base-template/app/plugins/auth-interceptor.client.ts +0 -151
- package/nuxt-base-template/app/utils/crypto.ts +0 -44
|
@@ -1,597 +0,0 @@
|
|
|
1
|
-
import { authClient } from '~/lib/auth-client';
|
|
2
|
-
import { arrayBufferToBase64Url, base64UrlToUint8Array } from '~/utils/crypto';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* User type for Better Auth session
|
|
6
|
-
*/
|
|
7
|
-
interface BetterAuthUser {
|
|
8
|
-
email: string;
|
|
9
|
-
emailVerified?: boolean;
|
|
10
|
-
id: string;
|
|
11
|
-
name?: string;
|
|
12
|
-
role?: string;
|
|
13
|
-
twoFactorEnabled?: boolean;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Stored auth state (persisted in cookie for SSR compatibility)
|
|
18
|
-
*/
|
|
19
|
-
interface StoredAuthState {
|
|
20
|
-
authMode: 'cookie' | 'jwt';
|
|
21
|
-
user: BetterAuthUser | null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Result of passkey authentication
|
|
26
|
-
*/
|
|
27
|
-
interface PasskeyAuthResult {
|
|
28
|
-
error?: string;
|
|
29
|
-
session?: { token: string };
|
|
30
|
-
success: boolean;
|
|
31
|
-
user?: BetterAuthUser;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Better Auth composable with client-side state management
|
|
36
|
-
*
|
|
37
|
-
* This composable manages auth state using:
|
|
38
|
-
* 1. Primary: Session cookies (more secure, HttpOnly)
|
|
39
|
-
* 2. Fallback: JWT tokens (when cookies are not available/working)
|
|
40
|
-
*
|
|
41
|
-
* The auth mode is automatically detected:
|
|
42
|
-
* - If session cookie works → use cookies
|
|
43
|
-
* - If cookies fail (401) → switch to JWT mode
|
|
44
|
-
*/
|
|
45
|
-
export function useBetterAuth() {
|
|
46
|
-
// Use useCookie for SSR-compatible persistent state
|
|
47
|
-
// Note: No default value to prevent overwriting existing cookies during hydration
|
|
48
|
-
const authState = useCookie<StoredAuthState | null>('auth-state', {
|
|
49
|
-
maxAge: 60 * 60 * 24 * 7, // 7 days
|
|
50
|
-
sameSite: 'lax',
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
// On client, sync from browser cookie to ensure we have the latest value
|
|
54
|
-
// This prevents hydration mismatch where useCookie may return stale/null value
|
|
55
|
-
if (import.meta.client) {
|
|
56
|
-
try {
|
|
57
|
-
const cookieStr = document.cookie.split('; ').find((row) => row.startsWith('auth-state='));
|
|
58
|
-
if (cookieStr) {
|
|
59
|
-
const parts = cookieStr.split('=');
|
|
60
|
-
const value = parts.length > 1 ? decodeURIComponent(parts.slice(1).join('=')) : '';
|
|
61
|
-
if (value) {
|
|
62
|
-
const parsed = JSON.parse(value);
|
|
63
|
-
// Only update if the browser cookie has a user but useCookie doesn't
|
|
64
|
-
if (parsed?.user && !authState.value?.user) {
|
|
65
|
-
authState.value = parsed;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
} catch {
|
|
70
|
-
// Ignore parse errors
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Initialize with default only on server if cookie doesn't exist
|
|
75
|
-
if (import.meta.server && (authState.value === null || authState.value === undefined)) {
|
|
76
|
-
authState.value = { user: null, authMode: 'cookie' };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// JWT token storage (used when cookies are not available)
|
|
80
|
-
const jwtToken = useCookie<string | null>('jwt-token', {
|
|
81
|
-
maxAge: 60 * 60 * 24 * 7, // 7 days
|
|
82
|
-
sameSite: 'lax',
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// Loading state
|
|
86
|
-
const isLoading = ref<boolean>(false);
|
|
87
|
-
|
|
88
|
-
// Auth mode: 'cookie' (default) or 'jwt' (fallback)
|
|
89
|
-
const authMode = computed(() => authState.value?.authMode || 'cookie');
|
|
90
|
-
const isJwtMode = computed(() => authMode.value === 'jwt');
|
|
91
|
-
|
|
92
|
-
// Computed properties based on stored state
|
|
93
|
-
const user = computed<BetterAuthUser | null>(() => authState.value?.user ?? null);
|
|
94
|
-
const isAuthenticated = computed<boolean>(() => !!user.value);
|
|
95
|
-
const isAdmin = computed<boolean>(() => user.value?.role === 'admin');
|
|
96
|
-
const is2FAEnabled = computed<boolean>(() => user.value?.twoFactorEnabled ?? false);
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Get the API base URL
|
|
100
|
-
*/
|
|
101
|
-
function getApiBase(): string {
|
|
102
|
-
const isDev = import.meta.dev;
|
|
103
|
-
const runtimeConfig = useRuntimeConfig();
|
|
104
|
-
return isDev ? '/api/iam' : `${runtimeConfig.public.apiUrl || 'http://localhost:3000'}/iam`;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Set user data after successful login/signup
|
|
109
|
-
* Also manually writes to browser cookie for SSR compatibility
|
|
110
|
-
*/
|
|
111
|
-
function setUser(userData: BetterAuthUser | null, mode: 'cookie' | 'jwt' = 'cookie'): void {
|
|
112
|
-
const newState = { user: userData, authMode: mode };
|
|
113
|
-
authState.value = newState;
|
|
114
|
-
|
|
115
|
-
// Manually write to browser cookie for immediate SSR compatibility
|
|
116
|
-
if (import.meta.client) {
|
|
117
|
-
const maxAge = 60 * 60 * 24 * 7; // 7 days
|
|
118
|
-
document.cookie = `auth-state=${encodeURIComponent(JSON.stringify(newState))}; path=/; max-age=${maxAge}; samesite=lax`;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Clear user data on logout
|
|
124
|
-
* Also manually clears browser cookies for SSR compatibility
|
|
125
|
-
*/
|
|
126
|
-
function clearUser(): void {
|
|
127
|
-
const clearedState = { user: null, authMode: 'cookie' as const };
|
|
128
|
-
authState.value = clearedState;
|
|
129
|
-
jwtToken.value = null;
|
|
130
|
-
|
|
131
|
-
// Manually clear browser cookies for immediate SSR compatibility
|
|
132
|
-
if (import.meta.client) {
|
|
133
|
-
const maxAge = 60 * 60 * 24 * 7; // 7 days
|
|
134
|
-
document.cookie = `auth-state=${encodeURIComponent(JSON.stringify(clearedState))}; path=/; max-age=${maxAge}; samesite=lax`;
|
|
135
|
-
document.cookie = `jwt-token=; path=/; max-age=0`;
|
|
136
|
-
|
|
137
|
-
// Clear Better Auth session cookies (set by the API)
|
|
138
|
-
// These cookies may have different names depending on the configuration
|
|
139
|
-
const sessionCookieNames = ['better-auth.session_token', 'better-auth.session', '__Secure-better-auth.session_token', 'session_token', 'session'];
|
|
140
|
-
|
|
141
|
-
for (const name of sessionCookieNames) {
|
|
142
|
-
// Clear with different path variations
|
|
143
|
-
document.cookie = `${name}=; path=/; max-age=0`;
|
|
144
|
-
document.cookie = `${name}=; path=/api; max-age=0`;
|
|
145
|
-
document.cookie = `${name}=; path=/api/iam; max-age=0`;
|
|
146
|
-
document.cookie = `${name}=; path=/iam; max-age=0`;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Switch to JWT mode and fetch a token
|
|
153
|
-
*/
|
|
154
|
-
async function switchToJwtMode(): Promise<boolean> {
|
|
155
|
-
try {
|
|
156
|
-
const apiBase = getApiBase();
|
|
157
|
-
const response = await fetch(`${apiBase}/token`, {
|
|
158
|
-
method: 'GET',
|
|
159
|
-
credentials: 'include',
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
if (response.ok) {
|
|
163
|
-
const data = await response.json();
|
|
164
|
-
if (data.token) {
|
|
165
|
-
jwtToken.value = data.token;
|
|
166
|
-
if (authState.value) {
|
|
167
|
-
authState.value = { ...authState.value, authMode: 'jwt' };
|
|
168
|
-
}
|
|
169
|
-
console.debug('[Auth] Switched to JWT mode');
|
|
170
|
-
return true;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
return false;
|
|
174
|
-
} catch {
|
|
175
|
-
return false;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Refresh JWT token before it expires
|
|
181
|
-
*/
|
|
182
|
-
async function refreshJwtToken(): Promise<boolean> {
|
|
183
|
-
if (!isJwtMode.value || !jwtToken.value) return false;
|
|
184
|
-
return switchToJwtMode();
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Authenticated fetch wrapper
|
|
189
|
-
* Uses cookies by default, falls back to JWT if cookies fail
|
|
190
|
-
*/
|
|
191
|
-
async function fetchWithAuth(url: string, options: RequestInit = {}): Promise<Response> {
|
|
192
|
-
const headers = new Headers(options.headers);
|
|
193
|
-
|
|
194
|
-
// In JWT mode, add Authorization header
|
|
195
|
-
if (isJwtMode.value && jwtToken.value) {
|
|
196
|
-
headers.set('Authorization', `Bearer ${jwtToken.value}`);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const response = await fetch(url, {
|
|
200
|
-
...options,
|
|
201
|
-
headers,
|
|
202
|
-
// Only include credentials in cookie mode
|
|
203
|
-
credentials: isJwtMode.value ? 'omit' : 'include',
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
// If we get 401 in cookie mode, try switching to JWT
|
|
207
|
-
if (response.status === 401 && !isJwtMode.value && isAuthenticated.value) {
|
|
208
|
-
console.debug('[Auth] Cookie auth failed, attempting JWT fallback...');
|
|
209
|
-
const switched = await switchToJwtMode();
|
|
210
|
-
|
|
211
|
-
if (switched) {
|
|
212
|
-
// Retry the request with JWT
|
|
213
|
-
headers.set('Authorization', `Bearer ${jwtToken.value}`);
|
|
214
|
-
return fetch(url, {
|
|
215
|
-
...options,
|
|
216
|
-
headers,
|
|
217
|
-
credentials: 'omit',
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return response;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Validate session with backend (called on app init)
|
|
227
|
-
* If session is invalid, clear the stored state
|
|
228
|
-
*/
|
|
229
|
-
async function validateSession(): Promise<boolean> {
|
|
230
|
-
try {
|
|
231
|
-
// Try to get session from Better Auth
|
|
232
|
-
const session = authClient.useSession();
|
|
233
|
-
|
|
234
|
-
// Wait for session to load
|
|
235
|
-
if (session.value.isPending) {
|
|
236
|
-
await new Promise((resolve) => {
|
|
237
|
-
const unwatch = watch(
|
|
238
|
-
() => session.value.isPending,
|
|
239
|
-
(isPending) => {
|
|
240
|
-
if (!isPending) {
|
|
241
|
-
unwatch();
|
|
242
|
-
resolve(true);
|
|
243
|
-
}
|
|
244
|
-
},
|
|
245
|
-
{ immediate: true },
|
|
246
|
-
);
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// If session has user data, update our state
|
|
251
|
-
if (session.value.data?.user) {
|
|
252
|
-
setUser(session.value.data.user as BetterAuthUser, 'cookie');
|
|
253
|
-
// Pre-fetch JWT for fallback
|
|
254
|
-
switchToJwtMode().catch(() => {});
|
|
255
|
-
return true;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Session not found from Better Auth API
|
|
259
|
-
// Trust the stored auth-state if user exists (e.g., after 2FA verification)
|
|
260
|
-
// The auth-state cookie is set by our application after successful login/2FA
|
|
261
|
-
if (authState.value?.user) {
|
|
262
|
-
// Pre-fetch JWT for fallback
|
|
263
|
-
switchToJwtMode().catch(() => {});
|
|
264
|
-
return true;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
return false;
|
|
268
|
-
} catch (error) {
|
|
269
|
-
console.debug('Session validation failed:', error);
|
|
270
|
-
return !!authState.value?.user;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Sign in with email and password
|
|
276
|
-
*/
|
|
277
|
-
const signIn = {
|
|
278
|
-
...authClient.signIn,
|
|
279
|
-
email: async (params: { email: string; password: string; rememberMe?: boolean }, options?: any) => {
|
|
280
|
-
isLoading.value = true;
|
|
281
|
-
try {
|
|
282
|
-
const result = await authClient.signIn.email(params, options);
|
|
283
|
-
|
|
284
|
-
// Extract token from response (JWT mode: cookies: false)
|
|
285
|
-
const resultAny = result as any;
|
|
286
|
-
const token = resultAny?.token || resultAny?.data?.token;
|
|
287
|
-
const userData = resultAny?.user || resultAny?.data?.user;
|
|
288
|
-
|
|
289
|
-
if (token) {
|
|
290
|
-
// JWT mode: Token is in the response
|
|
291
|
-
jwtToken.value = token;
|
|
292
|
-
if (userData) {
|
|
293
|
-
setUser(userData as BetterAuthUser, 'jwt');
|
|
294
|
-
}
|
|
295
|
-
console.debug('[Auth] JWT token received from login response');
|
|
296
|
-
} else if (userData) {
|
|
297
|
-
// Cookie mode: No token in response, use cookies
|
|
298
|
-
setUser(userData as BetterAuthUser, 'cookie');
|
|
299
|
-
// Try to get JWT token for fallback
|
|
300
|
-
switchToJwtMode().catch(() => {});
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
return result;
|
|
304
|
-
} finally {
|
|
305
|
-
isLoading.value = false;
|
|
306
|
-
}
|
|
307
|
-
},
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Sign up with email and password
|
|
312
|
-
*/
|
|
313
|
-
const signUp = {
|
|
314
|
-
...authClient.signUp,
|
|
315
|
-
email: async (params: { email: string; name: string; password: string }, options?: any) => {
|
|
316
|
-
isLoading.value = true;
|
|
317
|
-
try {
|
|
318
|
-
const result = await authClient.signUp.email(params, options);
|
|
319
|
-
|
|
320
|
-
// Extract token from response (JWT mode: cookies: false)
|
|
321
|
-
const resultAny = result as any;
|
|
322
|
-
const token = resultAny?.token || resultAny?.data?.token;
|
|
323
|
-
const userData = resultAny?.user || resultAny?.data?.user;
|
|
324
|
-
|
|
325
|
-
if (token) {
|
|
326
|
-
// JWT mode: Token is in the response
|
|
327
|
-
jwtToken.value = token;
|
|
328
|
-
if (userData) {
|
|
329
|
-
setUser(userData as BetterAuthUser, 'jwt');
|
|
330
|
-
}
|
|
331
|
-
console.debug('[Auth] JWT token received from signup response');
|
|
332
|
-
} else if (userData) {
|
|
333
|
-
// Cookie mode: No token in response, use cookies
|
|
334
|
-
setUser(userData as BetterAuthUser, 'cookie');
|
|
335
|
-
switchToJwtMode().catch(() => {});
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
return result;
|
|
339
|
-
} finally {
|
|
340
|
-
isLoading.value = false;
|
|
341
|
-
}
|
|
342
|
-
},
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* Sign out
|
|
347
|
-
*/
|
|
348
|
-
const signOut = async (options?: any) => {
|
|
349
|
-
isLoading.value = true;
|
|
350
|
-
try {
|
|
351
|
-
const result = await authClient.signOut(options);
|
|
352
|
-
// Clear user data on logout
|
|
353
|
-
clearUser();
|
|
354
|
-
return result;
|
|
355
|
-
} finally {
|
|
356
|
-
isLoading.value = false;
|
|
357
|
-
}
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Authenticate with a passkey (WebAuthn)
|
|
362
|
-
*
|
|
363
|
-
* This function handles the complete WebAuthn authentication flow:
|
|
364
|
-
* 1. Fetches authentication options from the server
|
|
365
|
-
* 2. Prompts the user to select a passkey via the browser's WebAuthn API
|
|
366
|
-
* 3. Sends the signed credential to the server for verification
|
|
367
|
-
* 4. Stores user data on successful authentication
|
|
368
|
-
*
|
|
369
|
-
* @returns Result with success status, user data, or error message
|
|
370
|
-
*/
|
|
371
|
-
async function authenticateWithPasskey(): Promise<PasskeyAuthResult> {
|
|
372
|
-
isLoading.value = true;
|
|
373
|
-
|
|
374
|
-
try {
|
|
375
|
-
const apiBase = getApiBase();
|
|
376
|
-
|
|
377
|
-
// Step 1: Get authentication options from server
|
|
378
|
-
const optionsResponse = await fetchWithAuth(`${apiBase}/passkey/generate-authenticate-options`, {
|
|
379
|
-
method: 'GET',
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
if (!optionsResponse.ok) {
|
|
383
|
-
return { success: false, error: 'Konnte Passkey-Optionen nicht laden' };
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
const options = await optionsResponse.json();
|
|
387
|
-
|
|
388
|
-
// Step 2: Convert challenge from base64url to ArrayBuffer
|
|
389
|
-
const challengeBuffer = base64UrlToUint8Array(options.challenge).buffer as ArrayBuffer;
|
|
390
|
-
|
|
391
|
-
// Step 3: Get credential from browser's WebAuthn API
|
|
392
|
-
const credential = (await navigator.credentials.get({
|
|
393
|
-
publicKey: {
|
|
394
|
-
challenge: challengeBuffer,
|
|
395
|
-
rpId: options.rpId,
|
|
396
|
-
allowCredentials: options.allowCredentials || [],
|
|
397
|
-
userVerification: options.userVerification,
|
|
398
|
-
timeout: options.timeout,
|
|
399
|
-
},
|
|
400
|
-
})) as PublicKeyCredential | null;
|
|
401
|
-
|
|
402
|
-
if (!credential) {
|
|
403
|
-
return { success: false, error: 'Kein Passkey ausgewählt' };
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// Step 4: Convert credential response to base64url format
|
|
407
|
-
const response = credential.response as AuthenticatorAssertionResponse;
|
|
408
|
-
const credentialBody = {
|
|
409
|
-
id: credential.id,
|
|
410
|
-
rawId: arrayBufferToBase64Url(credential.rawId),
|
|
411
|
-
type: credential.type,
|
|
412
|
-
response: {
|
|
413
|
-
authenticatorData: arrayBufferToBase64Url(response.authenticatorData),
|
|
414
|
-
clientDataJSON: arrayBufferToBase64Url(response.clientDataJSON),
|
|
415
|
-
signature: arrayBufferToBase64Url(response.signature),
|
|
416
|
-
userHandle: response.userHandle ? arrayBufferToBase64Url(response.userHandle) : null,
|
|
417
|
-
},
|
|
418
|
-
clientExtensionResults: credential.getClientExtensionResults?.() || {},
|
|
419
|
-
};
|
|
420
|
-
|
|
421
|
-
// Step 5: Verify with server
|
|
422
|
-
// Note: The server expects { response: credentialData } format (matching @simplewebauthn/browser output)
|
|
423
|
-
// Include challengeId for JWT mode (database challenge storage)
|
|
424
|
-
const authResponse = await fetchWithAuth(`${apiBase}/passkey/verify-authentication`, {
|
|
425
|
-
method: 'POST',
|
|
426
|
-
headers: { 'Content-Type': 'application/json' },
|
|
427
|
-
body: JSON.stringify({ challengeId: options.challengeId, response: credentialBody }),
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
const result = await authResponse.json();
|
|
431
|
-
|
|
432
|
-
if (!authResponse.ok) {
|
|
433
|
-
return { success: false, error: result.message || 'Passkey-Anmeldung fehlgeschlagen' };
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// Store user data after successful passkey login
|
|
437
|
-
if (result.user) {
|
|
438
|
-
setUser(result.user as BetterAuthUser, 'cookie');
|
|
439
|
-
switchToJwtMode().catch(() => {});
|
|
440
|
-
} else if (result.session?.token) {
|
|
441
|
-
// Passkey auth returns session without user in JWT mode
|
|
442
|
-
// Store the session token as JWT and fetch user via validateSession
|
|
443
|
-
jwtToken.value = result.session.token;
|
|
444
|
-
if (authState.value) {
|
|
445
|
-
authState.value = { ...authState.value, authMode: 'jwt' };
|
|
446
|
-
}
|
|
447
|
-
console.debug('[Auth] Passkey: Stored session token as JWT');
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
return { success: true, user: result.user as BetterAuthUser, session: result.session };
|
|
451
|
-
} catch (err: unknown) {
|
|
452
|
-
// Handle WebAuthn-specific errors
|
|
453
|
-
if (err instanceof Error && err.name === 'NotAllowedError') {
|
|
454
|
-
return { success: false, error: 'Passkey-Authentifizierung wurde abgebrochen' };
|
|
455
|
-
}
|
|
456
|
-
return { success: false, error: err instanceof Error ? err.message : 'Passkey-Anmeldung fehlgeschlagen' };
|
|
457
|
-
} finally {
|
|
458
|
-
isLoading.value = false;
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
/**
|
|
463
|
-
* Register a new passkey for the current user
|
|
464
|
-
*
|
|
465
|
-
* This function handles the complete WebAuthn registration flow:
|
|
466
|
-
* 1. Fetches registration options from the server
|
|
467
|
-
* 2. Prompts the user to create a passkey via the browser's WebAuthn API
|
|
468
|
-
* 3. Sends the credential to the server for storage
|
|
469
|
-
*
|
|
470
|
-
* @param name - Optional name for the passkey
|
|
471
|
-
* @returns Result with success status or error message
|
|
472
|
-
*/
|
|
473
|
-
async function registerPasskey(name?: string): Promise<{ success: boolean; error?: string; passkey?: any }> {
|
|
474
|
-
isLoading.value = true;
|
|
475
|
-
|
|
476
|
-
try {
|
|
477
|
-
const apiBase = getApiBase();
|
|
478
|
-
|
|
479
|
-
// Step 1: Get registration options from server
|
|
480
|
-
const optionsResponse = await fetchWithAuth(`${apiBase}/passkey/generate-register-options`, {
|
|
481
|
-
method: 'GET',
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
if (!optionsResponse.ok) {
|
|
485
|
-
const error = await optionsResponse.json().catch(() => ({}));
|
|
486
|
-
return { success: false, error: error.message || 'Konnte Registrierungsoptionen nicht laden' };
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
const options = await optionsResponse.json();
|
|
490
|
-
|
|
491
|
-
// Step 2: Convert challenge from base64url to ArrayBuffer
|
|
492
|
-
const challengeBuffer = base64UrlToUint8Array(options.challenge).buffer as ArrayBuffer;
|
|
493
|
-
|
|
494
|
-
// Step 3: Convert user.id from base64url to ArrayBuffer
|
|
495
|
-
const userIdBuffer = base64UrlToUint8Array(options.user.id).buffer as ArrayBuffer;
|
|
496
|
-
|
|
497
|
-
// Step 4: Create credential via browser's WebAuthn API
|
|
498
|
-
const credential = (await navigator.credentials.create({
|
|
499
|
-
publicKey: {
|
|
500
|
-
challenge: challengeBuffer,
|
|
501
|
-
rp: options.rp,
|
|
502
|
-
user: {
|
|
503
|
-
...options.user,
|
|
504
|
-
id: userIdBuffer,
|
|
505
|
-
},
|
|
506
|
-
pubKeyCredParams: options.pubKeyCredParams,
|
|
507
|
-
timeout: options.timeout,
|
|
508
|
-
attestation: options.attestation,
|
|
509
|
-
authenticatorSelection: options.authenticatorSelection,
|
|
510
|
-
excludeCredentials: (options.excludeCredentials || []).map((cred: any) => ({
|
|
511
|
-
...cred,
|
|
512
|
-
id: base64UrlToUint8Array(cred.id).buffer as ArrayBuffer,
|
|
513
|
-
})),
|
|
514
|
-
},
|
|
515
|
-
})) as PublicKeyCredential | null;
|
|
516
|
-
|
|
517
|
-
if (!credential) {
|
|
518
|
-
return { success: false, error: 'Passkey-Erstellung abgebrochen' };
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
// Step 5: Convert credential response to base64url format
|
|
522
|
-
const attestationResponse = credential.response as AuthenticatorAttestationResponse;
|
|
523
|
-
const credentialBody = {
|
|
524
|
-
name,
|
|
525
|
-
// Include challengeId for JWT mode (database challenge storage)
|
|
526
|
-
challengeId: options.challengeId,
|
|
527
|
-
response: {
|
|
528
|
-
id: credential.id,
|
|
529
|
-
rawId: arrayBufferToBase64Url(credential.rawId),
|
|
530
|
-
type: credential.type,
|
|
531
|
-
response: {
|
|
532
|
-
attestationObject: arrayBufferToBase64Url(attestationResponse.attestationObject),
|
|
533
|
-
clientDataJSON: arrayBufferToBase64Url(attestationResponse.clientDataJSON),
|
|
534
|
-
transports: attestationResponse.getTransports?.() || [],
|
|
535
|
-
},
|
|
536
|
-
clientExtensionResults: credential.getClientExtensionResults?.() || {},
|
|
537
|
-
},
|
|
538
|
-
};
|
|
539
|
-
|
|
540
|
-
// Step 6: Send to server for verification and storage
|
|
541
|
-
const registerResponse = await fetchWithAuth(`${apiBase}/passkey/verify-registration`, {
|
|
542
|
-
method: 'POST',
|
|
543
|
-
headers: { 'Content-Type': 'application/json' },
|
|
544
|
-
body: JSON.stringify(credentialBody),
|
|
545
|
-
});
|
|
546
|
-
|
|
547
|
-
const result = await registerResponse.json();
|
|
548
|
-
|
|
549
|
-
if (!registerResponse.ok) {
|
|
550
|
-
return { success: false, error: result.message || 'Passkey-Registrierung fehlgeschlagen' };
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
return { success: true, passkey: result };
|
|
554
|
-
} catch (err: unknown) {
|
|
555
|
-
if (err instanceof Error && err.name === 'NotAllowedError') {
|
|
556
|
-
return { success: false, error: 'Passkey-Erstellung wurde abgebrochen' };
|
|
557
|
-
}
|
|
558
|
-
return { success: false, error: err instanceof Error ? err.message : 'Passkey-Registrierung fehlgeschlagen' };
|
|
559
|
-
} finally {
|
|
560
|
-
isLoading.value = false;
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
return {
|
|
565
|
-
// Auth state
|
|
566
|
-
authMode,
|
|
567
|
-
isAuthenticated,
|
|
568
|
-
isJwtMode,
|
|
569
|
-
isLoading: computed(() => isLoading.value),
|
|
570
|
-
user,
|
|
571
|
-
|
|
572
|
-
// User properties
|
|
573
|
-
is2FAEnabled,
|
|
574
|
-
isAdmin,
|
|
575
|
-
|
|
576
|
-
// Auth actions
|
|
577
|
-
authenticateWithPasskey,
|
|
578
|
-
changePassword: authClient.changePassword,
|
|
579
|
-
clearUser,
|
|
580
|
-
registerPasskey,
|
|
581
|
-
setUser,
|
|
582
|
-
signIn,
|
|
583
|
-
signOut,
|
|
584
|
-
signUp,
|
|
585
|
-
validateSession,
|
|
586
|
-
|
|
587
|
-
// JWT management
|
|
588
|
-
fetchWithAuth,
|
|
589
|
-
jwtToken,
|
|
590
|
-
refreshJwtToken,
|
|
591
|
-
switchToJwtMode,
|
|
592
|
-
|
|
593
|
-
// Better Auth client passthrough
|
|
594
|
-
passkey: authClient.passkey,
|
|
595
|
-
twoFactor: authClient.twoFactor,
|
|
596
|
-
};
|
|
597
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
interface FileInfo {
|
|
2
|
-
[key: string]: unknown;
|
|
3
|
-
filename: string;
|
|
4
|
-
id: string;
|
|
5
|
-
mimetype: string;
|
|
6
|
-
size: number;
|
|
7
|
-
url?: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function useFile() {
|
|
11
|
-
const config = useRuntimeConfig();
|
|
12
|
-
|
|
13
|
-
function isValidMongoID(id: string): boolean {
|
|
14
|
-
return /^[a-f\d]{24}$/i.test(id);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
async function getFileInfo(id: string | undefined): Promise<FileInfo | null | string> {
|
|
18
|
-
if (!id) {
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (!isValidMongoID(id)) {
|
|
23
|
-
return id;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
try {
|
|
27
|
-
const response = await $fetch<FileInfo>(config.public.host + '/files/info/' + id, {
|
|
28
|
-
credentials: 'include',
|
|
29
|
-
method: 'GET',
|
|
30
|
-
});
|
|
31
|
-
return response;
|
|
32
|
-
} catch (error) {
|
|
33
|
-
console.error('Error fetching file info:', error);
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function getFileUrl(id: string): string {
|
|
39
|
-
return `${config.public.host}/files/${id}`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function getDownloadUrl(id: string, filename?: string): string {
|
|
43
|
-
const base = `${config.public.host}/files/download/${id}`;
|
|
44
|
-
return filename ? `${base}?filename=${encodeURIComponent(filename)}` : base;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function formatFileSize(bytes: number): string {
|
|
48
|
-
if (bytes === 0) return '0 B';
|
|
49
|
-
const k = 1024;
|
|
50
|
-
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
51
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
52
|
-
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function formatDuration(seconds: number): string {
|
|
56
|
-
if (seconds < 60) return `${seconds}s`;
|
|
57
|
-
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
58
|
-
const hours = Math.floor(seconds / 3600);
|
|
59
|
-
const minutes = Math.floor((seconds % 3600) / 60);
|
|
60
|
-
return `${hours}h ${minutes}m`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return {
|
|
64
|
-
formatDuration,
|
|
65
|
-
formatFileSize,
|
|
66
|
-
getDownloadUrl,
|
|
67
|
-
getFileInfo,
|
|
68
|
-
getFileUrl,
|
|
69
|
-
isValidMongoID,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
export function useShare() {
|
|
2
|
-
const route = useRoute();
|
|
3
|
-
|
|
4
|
-
async function share(title?: string, text?: string, url?: string) {
|
|
5
|
-
if (!import.meta.client) {
|
|
6
|
-
return;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
if (window?.navigator?.share) {
|
|
10
|
-
try {
|
|
11
|
-
await window.navigator.share({
|
|
12
|
-
text: text ?? window.location.origin,
|
|
13
|
-
title: title,
|
|
14
|
-
url: url ?? route.fullPath,
|
|
15
|
-
});
|
|
16
|
-
} catch (error) {
|
|
17
|
-
console.error('Error sharing:', error);
|
|
18
|
-
}
|
|
19
|
-
} else {
|
|
20
|
-
// Fallback: Copy to clipboard
|
|
21
|
-
try {
|
|
22
|
-
await navigator.clipboard.writeText(url ?? window.location.origin);
|
|
23
|
-
const toast = useToast();
|
|
24
|
-
toast.add({
|
|
25
|
-
color: 'success',
|
|
26
|
-
description: 'Der Link wurde in die Zwischenablage kopiert.',
|
|
27
|
-
title: 'Link kopiert',
|
|
28
|
-
});
|
|
29
|
-
} catch (error) {
|
|
30
|
-
console.error('Error copying to clipboard:', error);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return {
|
|
36
|
-
share,
|
|
37
|
-
};
|
|
38
|
-
}
|