create-nuxt-base 1.1.1 → 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 +15 -11
- package/README.md +5 -5
- package/nuxt-base-template/README.md +11 -11
- package/nuxt-base-template/app/composables/use-better-auth.ts +243 -56
- package/nuxt-base-template/app/layouts/default.vue +17 -0
- 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 +76 -25
- package/nuxt-base-template/app/plugins/auth-interceptor.client.ts +151 -0
- package/nuxt-base-template/server/api/iam/[...path].ts +12 -4
- package/package.json +2 -2
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Interceptor Plugin
|
|
3
|
+
*
|
|
4
|
+
* This plugin intercepts all API responses and handles session expiration.
|
|
5
|
+
* When a 401 (Unauthorized) response is received, it automatically:
|
|
6
|
+
* 1. Clears the user session state
|
|
7
|
+
* 2. Redirects to the login page
|
|
8
|
+
*
|
|
9
|
+
* Note: This is a client-only plugin (.client.ts) since auth state
|
|
10
|
+
* management only makes sense in the browser context.
|
|
11
|
+
*/
|
|
12
|
+
export default defineNuxtPlugin(() => {
|
|
13
|
+
const { clearUser, isAuthenticated } = useBetterAuth();
|
|
14
|
+
const route = useRoute();
|
|
15
|
+
|
|
16
|
+
// Track if we're already handling a 401 to prevent multiple redirects
|
|
17
|
+
let isHandling401 = false;
|
|
18
|
+
|
|
19
|
+
// Paths that should not trigger auto-logout on 401
|
|
20
|
+
// (public auth endpoints where 401 is expected)
|
|
21
|
+
const publicAuthPaths = ['/auth/login', '/auth/register', '/auth/forgot-password', '/auth/reset-password', '/auth/2fa'];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check if current route is a public auth route
|
|
25
|
+
*/
|
|
26
|
+
function isPublicAuthRoute(): boolean {
|
|
27
|
+
return publicAuthPaths.some((path) => route.path.startsWith(path));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if URL is an auth-related endpoint that shouldn't trigger logout
|
|
32
|
+
* (e.g., login, register, password reset, passkey endpoints)
|
|
33
|
+
* These endpoints use the authFetch wrapper which handles JWT fallback
|
|
34
|
+
*/
|
|
35
|
+
function isAuthEndpoint(url: string): boolean {
|
|
36
|
+
const authEndpoints = [
|
|
37
|
+
'/sign-in',
|
|
38
|
+
'/sign-up',
|
|
39
|
+
'/sign-out',
|
|
40
|
+
'/forgot-password',
|
|
41
|
+
'/reset-password',
|
|
42
|
+
'/verify-email',
|
|
43
|
+
'/session',
|
|
44
|
+
'/token',
|
|
45
|
+
// Passkey endpoints - handled by authFetch with JWT fallback
|
|
46
|
+
'/passkey/',
|
|
47
|
+
'/list-user-passkeys',
|
|
48
|
+
'/generate-register-options',
|
|
49
|
+
'/verify-registration',
|
|
50
|
+
'/generate-authenticate-options',
|
|
51
|
+
'/verify-authentication',
|
|
52
|
+
// Two-factor endpoints
|
|
53
|
+
'/two-factor/',
|
|
54
|
+
];
|
|
55
|
+
return authEndpoints.some((endpoint) => url.includes(endpoint));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Handle 401 Unauthorized responses
|
|
60
|
+
* Clears user state and redirects to login page
|
|
61
|
+
*/
|
|
62
|
+
async function handleUnauthorized(requestUrl?: string): Promise<void> {
|
|
63
|
+
// Prevent multiple simultaneous 401 handling
|
|
64
|
+
if (isHandling401) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Don't handle 401 for auth endpoints (expected behavior)
|
|
69
|
+
if (requestUrl && isAuthEndpoint(requestUrl)) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Don't handle 401 on public auth pages
|
|
74
|
+
if (isPublicAuthRoute()) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
isHandling401 = true;
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
// Only handle if user was authenticated (prevents redirect loops)
|
|
82
|
+
if (isAuthenticated.value) {
|
|
83
|
+
console.debug('[Auth Interceptor] Session expired, logging out...');
|
|
84
|
+
|
|
85
|
+
// Clear user state
|
|
86
|
+
clearUser();
|
|
87
|
+
|
|
88
|
+
// Redirect to login page with return URL
|
|
89
|
+
await navigateTo(
|
|
90
|
+
{
|
|
91
|
+
path: '/auth/login',
|
|
92
|
+
query: {
|
|
93
|
+
redirect: route.fullPath !== '/auth/login' ? route.fullPath : undefined,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
replace: true,
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
} finally {
|
|
102
|
+
// Reset flag after a short delay to allow navigation to complete
|
|
103
|
+
setTimeout(() => {
|
|
104
|
+
isHandling401 = false;
|
|
105
|
+
}, 1000);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Override the default $fetch to add response error handling
|
|
110
|
+
const originalFetch = globalThis.$fetch;
|
|
111
|
+
|
|
112
|
+
// Use a wrapper to intercept responses
|
|
113
|
+
globalThis.$fetch = ((url: string, options?: any) => {
|
|
114
|
+
return originalFetch(url, {
|
|
115
|
+
...options,
|
|
116
|
+
onResponseError: (context: any) => {
|
|
117
|
+
// Call original onResponseError if provided
|
|
118
|
+
if (options?.onResponseError) {
|
|
119
|
+
options.onResponseError(context);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Handle 401 errors
|
|
123
|
+
if (context.response?.status === 401) {
|
|
124
|
+
handleUnauthorized(url);
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
}) as typeof globalThis.$fetch;
|
|
129
|
+
|
|
130
|
+
// Also intercept native fetch for manual API calls
|
|
131
|
+
const originalNativeFetch = globalThis.fetch;
|
|
132
|
+
|
|
133
|
+
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
134
|
+
const response = await originalNativeFetch(input, init);
|
|
135
|
+
|
|
136
|
+
// Handle 401 errors from native fetch
|
|
137
|
+
if (response.status === 401) {
|
|
138
|
+
const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
|
|
139
|
+
handleUnauthorized(url);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return response;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Provide a manual method to trigger logout on 401
|
|
146
|
+
return {
|
|
147
|
+
provide: {
|
|
148
|
+
handleUnauthorized,
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
});
|
|
@@ -28,8 +28,9 @@ export default defineEventHandler(async (event) => {
|
|
|
28
28
|
body = await readBody(event);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
// Forward cookies from the incoming request
|
|
31
|
+
// Forward cookies and authorization from the incoming request
|
|
32
32
|
const cookieHeader = getHeader(event, 'cookie');
|
|
33
|
+
const authHeader = getHeader(event, 'authorization');
|
|
33
34
|
// Use the actual origin from the request, fallback to localhost
|
|
34
35
|
const originHeader = getHeader(event, 'origin') || getHeader(event, 'referer')?.replace(/\/[^/]*$/, '') || 'http://localhost:3001';
|
|
35
36
|
|
|
@@ -39,8 +40,9 @@ export default defineEventHandler(async (event) => {
|
|
|
39
40
|
body: body ? JSON.stringify(body) : undefined,
|
|
40
41
|
headers: {
|
|
41
42
|
'Content-Type': 'application/json',
|
|
42
|
-
|
|
43
|
+
Origin: originHeader,
|
|
43
44
|
...(cookieHeader ? { Cookie: cookieHeader } : {}),
|
|
45
|
+
...(authHeader ? { Authorization: authHeader } : {}),
|
|
44
46
|
},
|
|
45
47
|
credentials: 'include',
|
|
46
48
|
// Don't throw on error status codes
|
|
@@ -50,10 +52,16 @@ export default defineEventHandler(async (event) => {
|
|
|
50
52
|
// Forward Set-Cookie headers from the API response
|
|
51
53
|
const setCookieHeaders = response.headers.getSetCookie?.() || [];
|
|
52
54
|
for (const cookie of setCookieHeaders) {
|
|
53
|
-
// Rewrite cookie to work on localhost
|
|
55
|
+
// Rewrite cookie to work on localhost:
|
|
56
|
+
// 1. Remove domain (cookie will be set for current origin)
|
|
57
|
+
// 2. Remove secure flag (not needed for localhost)
|
|
58
|
+
// 3. Rewrite path from /iam to /api/iam (or set to / for broader access)
|
|
54
59
|
const rewrittenCookie = cookie
|
|
55
60
|
.replace(/domain=[^;]+;?\s*/gi, '')
|
|
56
|
-
.replace(/secure;?\s*/gi, '')
|
|
61
|
+
.replace(/secure;?\s*/gi, '')
|
|
62
|
+
// Ensure path is set to / so cookies work for all routes
|
|
63
|
+
.replace(/path=\/iam[^;]*;?\s*/gi, 'path=/; ')
|
|
64
|
+
.replace(/path=[^;]+;?\s*/gi, 'path=/; ');
|
|
57
65
|
appendResponseHeader(event, 'Set-Cookie', rewrittenCookie);
|
|
58
66
|
}
|
|
59
67
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-nuxt-base",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Starter to generate a configured environment with VueJS, Nuxt, Tailwind, Eslint, Unit Tests, Playwright etc.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"author": "lenne.Tech GmbH",
|
|
6
7
|
"repository": {
|
|
7
8
|
"url": "https://github.com/lenneTech/nuxt-base-starter"
|
|
8
9
|
},
|
|
9
|
-
"author": "lenne.Tech GmbH",
|
|
10
10
|
"bin": {
|
|
11
11
|
"create-nuxt-base": "./index.js"
|
|
12
12
|
},
|