create-nuxt-base 1.1.2 → 1.2.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/.github/workflows/publish.yml +1 -1
- package/AUTH.md +16 -14
- package/CHANGELOG.md +13 -11
- package/README.md +5 -5
- package/nuxt-base-template/README.md +11 -11
- package/nuxt-base-template/app/composables/use-better-auth.ts +242 -60
- package/nuxt-base-template/app/lib/auth-client.ts +8 -11
- package/nuxt-base-template/app/lib/auth-state.ts +206 -0
- package/nuxt-base-template/app/middleware/admin.global.ts +27 -7
- package/nuxt-base-template/app/middleware/auth.global.ts +23 -6
- package/nuxt-base-template/app/middleware/guest.global.ts +22 -7
- package/nuxt-base-template/app/pages/app/index.vue +3 -17
- package/nuxt-base-template/app/pages/auth/2fa.vue +38 -6
- package/nuxt-base-template/app/pages/auth/login.vue +13 -20
- package/nuxt-base-template/app/pages/auth/register.vue +5 -8
- package/nuxt-base-template/app/plugins/auth-interceptor.client.ts +23 -15
- package/nuxt-base-template/server/api/iam/[...path].ts +12 -4
- package/package.json +2 -2
|
@@ -17,6 +17,7 @@ interface BetterAuthUser {
|
|
|
17
17
|
* Stored auth state (persisted in cookie for SSR compatibility)
|
|
18
18
|
*/
|
|
19
19
|
interface StoredAuthState {
|
|
20
|
+
authMode: 'cookie' | 'jwt';
|
|
20
21
|
user: BetterAuthUser | null;
|
|
21
22
|
}
|
|
22
23
|
|
|
@@ -25,6 +26,7 @@ interface StoredAuthState {
|
|
|
25
26
|
*/
|
|
26
27
|
interface PasskeyAuthResult {
|
|
27
28
|
error?: string;
|
|
29
|
+
session?: { token: string };
|
|
28
30
|
success: boolean;
|
|
29
31
|
user?: BetterAuthUser;
|
|
30
32
|
}
|
|
@@ -33,22 +35,49 @@ interface PasskeyAuthResult {
|
|
|
33
35
|
* Better Auth composable with client-side state management
|
|
34
36
|
*
|
|
35
37
|
* This composable manages auth state using:
|
|
36
|
-
* 1.
|
|
37
|
-
* 2.
|
|
38
|
+
* 1. Primary: Session cookies (more secure, HttpOnly)
|
|
39
|
+
* 2. Fallback: JWT tokens (when cookies are not available/working)
|
|
38
40
|
*
|
|
39
|
-
* The
|
|
41
|
+
* The auth mode is automatically detected:
|
|
42
|
+
* - If session cookie works → use cookies
|
|
43
|
+
* - If cookies fail (401) → switch to JWT mode
|
|
40
44
|
*/
|
|
41
45
|
export function useBetterAuth() {
|
|
42
46
|
// Use useCookie for SSR-compatible persistent state
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
// Note: No default value to prevent overwriting existing cookies during hydration
|
|
48
|
+
const authState = useCookie<StoredAuthState | null>('auth-state', {
|
|
45
49
|
maxAge: 60 * 60 * 24 * 7, // 7 days
|
|
46
50
|
sameSite: 'lax',
|
|
47
51
|
});
|
|
48
52
|
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
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', {
|
|
52
81
|
maxAge: 60 * 60 * 24 * 7, // 7 days
|
|
53
82
|
sameSite: 'lax',
|
|
54
83
|
});
|
|
@@ -56,24 +85,141 @@ export function useBetterAuth() {
|
|
|
56
85
|
// Loading state
|
|
57
86
|
const isLoading = ref<boolean>(false);
|
|
58
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
|
+
|
|
59
92
|
// Computed properties based on stored state
|
|
60
93
|
const user = computed<BetterAuthUser | null>(() => authState.value?.user ?? null);
|
|
61
94
|
const isAuthenticated = computed<boolean>(() => !!user.value);
|
|
62
95
|
const isAdmin = computed<boolean>(() => user.value?.role === 'admin');
|
|
63
96
|
const is2FAEnabled = computed<boolean>(() => user.value?.twoFactorEnabled ?? false);
|
|
64
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
|
+
|
|
65
107
|
/**
|
|
66
108
|
* Set user data after successful login/signup
|
|
109
|
+
* Also manually writes to browser cookie for SSR compatibility
|
|
67
110
|
*/
|
|
68
|
-
function setUser(userData: BetterAuthUser | null): void {
|
|
69
|
-
|
|
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
|
+
}
|
|
70
120
|
}
|
|
71
121
|
|
|
72
122
|
/**
|
|
73
123
|
* Clear user data on logout
|
|
124
|
+
* Also manually clears browser cookies for SSR compatibility
|
|
74
125
|
*/
|
|
75
126
|
function clearUser(): void {
|
|
76
|
-
|
|
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;
|
|
77
223
|
}
|
|
78
224
|
|
|
79
225
|
/**
|
|
@@ -103,23 +249,21 @@ export function useBetterAuth() {
|
|
|
103
249
|
|
|
104
250
|
// If session has user data, update our state
|
|
105
251
|
if (session.value.data?.user) {
|
|
106
|
-
setUser(session.value.data.user as BetterAuthUser);
|
|
252
|
+
setUser(session.value.data.user as BetterAuthUser, 'cookie');
|
|
253
|
+
// Pre-fetch JWT for fallback
|
|
254
|
+
switchToJwtMode().catch(() => {});
|
|
107
255
|
return true;
|
|
108
256
|
}
|
|
109
257
|
|
|
110
|
-
// Session not found
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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(() => {});
|
|
116
264
|
return true;
|
|
117
265
|
}
|
|
118
266
|
|
|
119
|
-
// No valid session found - clear state
|
|
120
|
-
if (authState.value?.user) {
|
|
121
|
-
clearUser();
|
|
122
|
-
}
|
|
123
267
|
return false;
|
|
124
268
|
} catch (error) {
|
|
125
269
|
console.debug('Session validation failed:', error);
|
|
@@ -137,11 +281,23 @@ export function useBetterAuth() {
|
|
|
137
281
|
try {
|
|
138
282
|
const result = await authClient.signIn.email(params, options);
|
|
139
283
|
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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(() => {});
|
|
145
301
|
}
|
|
146
302
|
|
|
147
303
|
return result;
|
|
@@ -161,11 +317,22 @@ export function useBetterAuth() {
|
|
|
161
317
|
try {
|
|
162
318
|
const result = await authClient.signUp.email(params, options);
|
|
163
319
|
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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(() => {});
|
|
169
336
|
}
|
|
170
337
|
|
|
171
338
|
return result;
|
|
@@ -205,16 +372,11 @@ export function useBetterAuth() {
|
|
|
205
372
|
isLoading.value = true;
|
|
206
373
|
|
|
207
374
|
try {
|
|
208
|
-
|
|
209
|
-
// In production, use the direct API URL
|
|
210
|
-
const isDev = import.meta.dev;
|
|
211
|
-
const runtimeConfig = useRuntimeConfig();
|
|
212
|
-
const apiBase = isDev ? '/api/iam' : `${runtimeConfig.public.apiUrl || 'http://localhost:3000'}/iam`;
|
|
375
|
+
const apiBase = getApiBase();
|
|
213
376
|
|
|
214
377
|
// Step 1: Get authentication options from server
|
|
215
|
-
const optionsResponse = await
|
|
378
|
+
const optionsResponse = await fetchWithAuth(`${apiBase}/passkey/generate-authenticate-options`, {
|
|
216
379
|
method: 'GET',
|
|
217
|
-
credentials: 'include',
|
|
218
380
|
});
|
|
219
381
|
|
|
220
382
|
if (!optionsResponse.ok) {
|
|
@@ -258,11 +420,11 @@ export function useBetterAuth() {
|
|
|
258
420
|
|
|
259
421
|
// Step 5: Verify with server
|
|
260
422
|
// Note: The server expects { response: credentialData } format (matching @simplewebauthn/browser output)
|
|
261
|
-
|
|
423
|
+
// Include challengeId for JWT mode (database challenge storage)
|
|
424
|
+
const authResponse = await fetchWithAuth(`${apiBase}/passkey/verify-authentication`, {
|
|
262
425
|
method: 'POST',
|
|
263
|
-
credentials: 'include',
|
|
264
426
|
headers: { 'Content-Type': 'application/json' },
|
|
265
|
-
body: JSON.stringify({ response: credentialBody }),
|
|
427
|
+
body: JSON.stringify({ challengeId: options.challengeId, response: credentialBody }),
|
|
266
428
|
});
|
|
267
429
|
|
|
268
430
|
const result = await authResponse.json();
|
|
@@ -273,10 +435,19 @@ export function useBetterAuth() {
|
|
|
273
435
|
|
|
274
436
|
// Store user data after successful passkey login
|
|
275
437
|
if (result.user) {
|
|
276
|
-
setUser(result.user as BetterAuthUser);
|
|
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');
|
|
277
448
|
}
|
|
278
449
|
|
|
279
|
-
return { success: true, user: result.user as BetterAuthUser };
|
|
450
|
+
return { success: true, user: result.user as BetterAuthUser, session: result.session };
|
|
280
451
|
} catch (err: unknown) {
|
|
281
452
|
// Handle WebAuthn-specific errors
|
|
282
453
|
if (err instanceof Error && err.name === 'NotAllowedError') {
|
|
@@ -303,16 +474,11 @@ export function useBetterAuth() {
|
|
|
303
474
|
isLoading.value = true;
|
|
304
475
|
|
|
305
476
|
try {
|
|
306
|
-
|
|
307
|
-
// In production, use the direct API URL
|
|
308
|
-
const isDev = import.meta.dev;
|
|
309
|
-
const runtimeConfig = useRuntimeConfig();
|
|
310
|
-
const apiBase = isDev ? '/api/iam' : `${runtimeConfig.public.apiUrl || 'http://localhost:3000'}/iam`;
|
|
477
|
+
const apiBase = getApiBase();
|
|
311
478
|
|
|
312
479
|
// Step 1: Get registration options from server
|
|
313
|
-
const optionsResponse = await
|
|
480
|
+
const optionsResponse = await fetchWithAuth(`${apiBase}/passkey/generate-register-options`, {
|
|
314
481
|
method: 'GET',
|
|
315
|
-
credentials: 'include',
|
|
316
482
|
});
|
|
317
483
|
|
|
318
484
|
if (!optionsResponse.ok) {
|
|
@@ -356,6 +522,8 @@ export function useBetterAuth() {
|
|
|
356
522
|
const attestationResponse = credential.response as AuthenticatorAttestationResponse;
|
|
357
523
|
const credentialBody = {
|
|
358
524
|
name,
|
|
525
|
+
// Include challengeId for JWT mode (database challenge storage)
|
|
526
|
+
challengeId: options.challengeId,
|
|
359
527
|
response: {
|
|
360
528
|
id: credential.id,
|
|
361
529
|
rawId: arrayBufferToBase64Url(credential.rawId),
|
|
@@ -370,9 +538,8 @@ export function useBetterAuth() {
|
|
|
370
538
|
};
|
|
371
539
|
|
|
372
540
|
// Step 6: Send to server for verification and storage
|
|
373
|
-
const registerResponse = await
|
|
541
|
+
const registerResponse = await fetchWithAuth(`${apiBase}/passkey/verify-registration`, {
|
|
374
542
|
method: 'POST',
|
|
375
|
-
credentials: 'include',
|
|
376
543
|
headers: { 'Content-Type': 'application/json' },
|
|
377
544
|
body: JSON.stringify(credentialBody),
|
|
378
545
|
});
|
|
@@ -395,21 +562,36 @@ export function useBetterAuth() {
|
|
|
395
562
|
}
|
|
396
563
|
|
|
397
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
|
|
398
577
|
authenticateWithPasskey,
|
|
399
578
|
changePassword: authClient.changePassword,
|
|
400
579
|
clearUser,
|
|
401
|
-
is2FAEnabled,
|
|
402
|
-
isAdmin,
|
|
403
|
-
isAuthenticated,
|
|
404
|
-
isLoading: computed(() => isLoading.value),
|
|
405
|
-
passkey: authClient.passkey,
|
|
406
580
|
registerPasskey,
|
|
407
581
|
setUser,
|
|
408
582
|
signIn,
|
|
409
583
|
signOut,
|
|
410
584
|
signUp,
|
|
411
|
-
twoFactor: authClient.twoFactor,
|
|
412
|
-
user,
|
|
413
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,
|
|
414
596
|
};
|
|
415
597
|
}
|
|
@@ -2,6 +2,7 @@ import { passkeyClient } from '@better-auth/passkey/client';
|
|
|
2
2
|
import { adminClient, twoFactorClient } from 'better-auth/client/plugins';
|
|
3
3
|
import { createAuthClient } from 'better-auth/vue';
|
|
4
4
|
|
|
5
|
+
import { authFetch } from '~/lib/auth-state';
|
|
5
6
|
import { sha256 } from '~/utils/crypto';
|
|
6
7
|
|
|
7
8
|
// =============================================================================
|
|
@@ -85,18 +86,13 @@ export function createBetterAuthClient(config: AuthClientConfig = {}) {
|
|
|
85
86
|
// - Frontend runs on localhost:3002, API on localhost:3000
|
|
86
87
|
// - WebAuthn validates the origin, which must be consistent
|
|
87
88
|
// - The Nuxt server proxy ensures requests come from the frontend origin
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
// Note: In Nuxt, use import.meta.dev (not import.meta.env?.DEV which is Vite-specific)
|
|
90
|
+
// At lenne.tech, 'development' is a stage on a web server, 'local' is the local dev environment
|
|
91
|
+
const isDev = import.meta.dev || process.env.NODE_ENV === 'local';
|
|
92
|
+
const defaultBaseURL = isDev ? '' : import.meta.env?.VITE_API_URL || process.env.API_URL || 'http://localhost:3000';
|
|
90
93
|
const defaultBasePath = isDev ? '/api/iam' : '/iam';
|
|
91
94
|
|
|
92
|
-
const {
|
|
93
|
-
baseURL = defaultBaseURL,
|
|
94
|
-
basePath = defaultBasePath,
|
|
95
|
-
twoFactorRedirectPath = '/auth/2fa',
|
|
96
|
-
enableAdmin = true,
|
|
97
|
-
enableTwoFactor = true,
|
|
98
|
-
enablePasskey = true,
|
|
99
|
-
} = config;
|
|
95
|
+
const { baseURL = defaultBaseURL, basePath = defaultBasePath, twoFactorRedirectPath = '/auth/2fa', enableAdmin = true, enableTwoFactor = true, enablePasskey = true } = config;
|
|
100
96
|
|
|
101
97
|
// Build plugins array based on configuration
|
|
102
98
|
const plugins: any[] = [];
|
|
@@ -120,11 +116,12 @@ export function createBetterAuthClient(config: AuthClientConfig = {}) {
|
|
|
120
116
|
}
|
|
121
117
|
|
|
122
118
|
// Create base client with configuration
|
|
119
|
+
// Uses authFetch for automatic Cookie/JWT dual-mode authentication
|
|
123
120
|
const baseClient = createAuthClient({
|
|
124
121
|
basePath,
|
|
125
122
|
baseURL,
|
|
126
123
|
fetchOptions: {
|
|
127
|
-
|
|
124
|
+
customFetchImpl: authFetch,
|
|
128
125
|
},
|
|
129
126
|
plugins,
|
|
130
127
|
});
|