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.
Files changed (35) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/nuxt-base-template/app/components/Modal/ModalBackupCodes.vue +2 -1
  3. package/nuxt-base-template/app/components/Upload/TusFileUpload.vue +7 -7
  4. package/nuxt-base-template/app/interfaces/user.interface.ts +5 -12
  5. package/nuxt-base-template/app/layouts/default.vue +1 -1
  6. package/nuxt-base-template/app/middleware/admin.global.ts +2 -2
  7. package/nuxt-base-template/app/middleware/auth.global.ts +2 -2
  8. package/nuxt-base-template/app/middleware/guest.global.ts +2 -2
  9. package/nuxt-base-template/app/pages/app/index.vue +1 -1
  10. package/nuxt-base-template/app/pages/app/settings/security.vue +2 -2
  11. package/nuxt-base-template/app/pages/auth/2fa.vue +2 -3
  12. package/nuxt-base-template/app/pages/auth/forgot-password.vue +2 -1
  13. package/nuxt-base-template/app/pages/auth/login.vue +2 -2
  14. package/nuxt-base-template/app/pages/auth/register.vue +1 -1
  15. package/nuxt-base-template/app/pages/auth/reset-password.vue +2 -1
  16. package/nuxt-base-template/docs/pages/docs.vue +1 -1
  17. package/nuxt-base-template/nuxt.config.ts +38 -1
  18. package/nuxt-base-template/package-lock.json +136 -2905
  19. package/nuxt-base-template/package.json +1 -0
  20. package/package.json +1 -1
  21. package/nuxt-base-template/app/components/Transition/TransitionFade.vue +0 -27
  22. package/nuxt-base-template/app/components/Transition/TransitionFadeScale.vue +0 -27
  23. package/nuxt-base-template/app/components/Transition/TransitionSlide.vue +0 -12
  24. package/nuxt-base-template/app/components/Transition/TransitionSlideBottom.vue +0 -12
  25. package/nuxt-base-template/app/components/Transition/TransitionSlideRevert.vue +0 -12
  26. package/nuxt-base-template/app/composables/use-better-auth.ts +0 -597
  27. package/nuxt-base-template/app/composables/use-file.ts +0 -71
  28. package/nuxt-base-template/app/composables/use-share.ts +0 -38
  29. package/nuxt-base-template/app/composables/use-tus-upload.ts +0 -278
  30. package/nuxt-base-template/app/composables/use-tw.ts +0 -1
  31. package/nuxt-base-template/app/interfaces/upload.interface.ts +0 -58
  32. package/nuxt-base-template/app/lib/auth-client.ts +0 -229
  33. package/nuxt-base-template/app/lib/auth-state.ts +0 -206
  34. package/nuxt-base-template/app/plugins/auth-interceptor.client.ts +0 -151
  35. package/nuxt-base-template/app/utils/crypto.ts +0 -44
@@ -1,206 +0,0 @@
1
- /**
2
- * Shared authentication state for Cookie/JWT dual-mode authentication
3
- *
4
- * This module provides a reactive state that is shared between:
5
- * - auth-client.ts (uses it for customFetch)
6
- * - use-better-auth.ts (manages the state)
7
- *
8
- * Auth Mode Strategy:
9
- * 1. Primary: Session cookies (more secure, HttpOnly)
10
- * 2. Fallback: JWT tokens (when cookies are not available/working)
11
- *
12
- * The state is persisted in cookies for SSR compatibility.
13
- */
14
-
15
- export type AuthMode = 'cookie' | 'jwt';
16
-
17
- /**
18
- * Get the current auth mode from cookie
19
- */
20
- export function getAuthMode(): AuthMode {
21
- if (import.meta.server) return 'cookie';
22
-
23
- try {
24
- const cookie = document.cookie.split('; ').find((row) => row.startsWith('auth-state='));
25
- if (cookie) {
26
- const parts = cookie.split('=');
27
- const value = parts.length > 1 ? decodeURIComponent(parts.slice(1).join('=')) : '';
28
- const state = JSON.parse(value);
29
- return state?.authMode || 'cookie';
30
- }
31
- } catch {
32
- // Ignore parse errors
33
- }
34
- return 'cookie';
35
- }
36
-
37
- /**
38
- * Get the JWT token from cookie
39
- */
40
- export function getJwtToken(): string | null {
41
- if (import.meta.server) return null;
42
-
43
- try {
44
- const cookie = document.cookie.split('; ').find((row) => row.startsWith('jwt-token='));
45
- if (cookie) {
46
- const parts = cookie.split('=');
47
- const value = parts.length > 1 ? decodeURIComponent(parts.slice(1).join('=')) : '';
48
- // Handle JSON-encoded string (useCookie stores as JSON)
49
- if (value.startsWith('"') && value.endsWith('"')) {
50
- return JSON.parse(value);
51
- }
52
- return value || null;
53
- }
54
- } catch {
55
- // Ignore parse errors
56
- }
57
- return null;
58
- }
59
-
60
- /**
61
- * Set JWT token in cookie
62
- */
63
- export function setJwtToken(token: string | null): void {
64
- if (import.meta.server) return;
65
-
66
- const maxAge = 60 * 60 * 24 * 7; // 7 days
67
- if (token) {
68
- document.cookie = `jwt-token=${encodeURIComponent(JSON.stringify(token))}; path=/; max-age=${maxAge}; samesite=lax`;
69
- } else {
70
- document.cookie = `jwt-token=; path=/; max-age=0`;
71
- }
72
- }
73
-
74
- /**
75
- * Update auth mode in the auth-state cookie
76
- */
77
- export function setAuthMode(mode: AuthMode): void {
78
- if (import.meta.server) return;
79
-
80
- try {
81
- const cookie = document.cookie.split('; ').find((row) => row.startsWith('auth-state='));
82
-
83
- let state = { user: null, authMode: mode };
84
- if (cookie) {
85
- const parts = cookie.split('=');
86
- const value = parts.length > 1 ? decodeURIComponent(parts.slice(1).join('=')) : '';
87
- state = { ...JSON.parse(value), authMode: mode };
88
- }
89
-
90
- const maxAge = 60 * 60 * 24 * 7; // 7 days
91
- document.cookie = `auth-state=${encodeURIComponent(JSON.stringify(state))}; path=/; max-age=${maxAge}; samesite=lax`;
92
- } catch {
93
- // Ignore errors
94
- }
95
- }
96
-
97
- /**
98
- * Get the API base URL
99
- */
100
- export function getApiBase(): string {
101
- const isDev = import.meta.dev;
102
- if (isDev) {
103
- return '/api/iam';
104
- }
105
- // In production, try to get from runtime config or fall back to default
106
- if (typeof window !== 'undefined' && (window as any).__NUXT__?.config?.public?.apiUrl) {
107
- return `${(window as any).__NUXT__.config.public.apiUrl}/iam`;
108
- }
109
- return 'http://localhost:3000/iam';
110
- }
111
-
112
- /**
113
- * Attempt to switch to JWT mode by fetching a token
114
- */
115
- export async function attemptJwtSwitch(): Promise<boolean> {
116
- try {
117
- const apiBase = getApiBase();
118
- const response = await fetch(`${apiBase}/token`, {
119
- method: 'GET',
120
- credentials: 'include',
121
- });
122
-
123
- if (response.ok) {
124
- const data = await response.json();
125
- if (data.token) {
126
- setJwtToken(data.token);
127
- setAuthMode('jwt');
128
- console.debug('[Auth] Switched to JWT mode');
129
- return true;
130
- }
131
- }
132
- return false;
133
- } catch {
134
- return false;
135
- }
136
- }
137
-
138
- /**
139
- * Check if user is authenticated (has auth-state with user)
140
- */
141
- export function isAuthenticated(): boolean {
142
- if (import.meta.server) return false;
143
-
144
- try {
145
- const cookie = document.cookie.split('; ').find((row) => row.startsWith('auth-state='));
146
- if (cookie) {
147
- const parts = cookie.split('=');
148
- const value = parts.length > 1 ? decodeURIComponent(parts.slice(1).join('=')) : '';
149
- const state = JSON.parse(value);
150
- return !!state?.user;
151
- }
152
- } catch {
153
- // Ignore parse errors
154
- }
155
- return false;
156
- }
157
-
158
- /**
159
- * Custom fetch function that handles Cookie/JWT dual-mode authentication
160
- *
161
- * This function:
162
- * 1. In cookie mode: Uses credentials: 'include'
163
- * 2. In JWT mode: Adds Authorization header
164
- * 3. On 401 in cookie mode: Attempts to switch to JWT and retries
165
- */
166
- export async function authFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
167
- const authMode = getAuthMode();
168
- const jwtToken = getJwtToken();
169
-
170
- const headers = new Headers(init?.headers);
171
-
172
- // In JWT mode, add Authorization header
173
- if (authMode === 'jwt' && jwtToken) {
174
- headers.set('Authorization', `Bearer ${jwtToken}`);
175
- }
176
-
177
- // Always include credentials for cookie-based session auth
178
- // In JWT mode, cookies are sent but ignored by the server (Authorization header is used instead)
179
- // This is more robust than conditionally omitting cookies
180
- const response = await fetch(input, {
181
- ...init,
182
- headers,
183
- credentials: 'include',
184
- });
185
-
186
- // If we get 401 in cookie mode and user is authenticated, try JWT fallback
187
- if (response.status === 401 && authMode === 'cookie' && isAuthenticated()) {
188
- console.debug('[Auth] Cookie auth failed, attempting JWT fallback...');
189
- const switched = await attemptJwtSwitch();
190
-
191
- if (switched) {
192
- // Retry the request with JWT
193
- const newToken = getJwtToken();
194
- if (newToken) {
195
- headers.set('Authorization', `Bearer ${newToken}`);
196
- return fetch(input, {
197
- ...init,
198
- headers,
199
- credentials: 'include',
200
- });
201
- }
202
- }
203
- }
204
-
205
- return response;
206
- }
@@ -1,151 +0,0 @@
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
- });
@@ -1,44 +0,0 @@
1
- /**
2
- * Hashes a string using SHA256
3
- * Uses the Web Crypto API which is available in all modern browsers
4
- *
5
- * @param message - The string to hash
6
- * @returns The SHA256 hash as a lowercase hex string (64 characters)
7
- */
8
- export async function sha256(message: string): Promise<string> {
9
- const msgBuffer = new TextEncoder().encode(message);
10
- const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
11
- const hashArray = Array.from(new Uint8Array(hashBuffer));
12
- return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
13
- }
14
-
15
- // ============================================================================
16
- // WebAuthn/Passkey Utilities
17
- // ============================================================================
18
-
19
- /**
20
- * Converts an ArrayBuffer to a base64url-encoded string
21
- * Used for WebAuthn credential responses
22
- *
23
- * @param buffer - The ArrayBuffer to convert
24
- * @returns The base64url-encoded string (no padding)
25
- */
26
- export function arrayBufferToBase64Url(buffer: ArrayBuffer): string {
27
- const bytes = new Uint8Array(buffer);
28
- let binary = '';
29
- bytes.forEach((b) => (binary += String.fromCharCode(b)));
30
- return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
31
- }
32
-
33
- /**
34
- * Converts a base64url-encoded string to a Uint8Array
35
- * Used for WebAuthn challenge decoding
36
- *
37
- * @param base64url - The base64url-encoded string
38
- * @returns The decoded Uint8Array
39
- */
40
- export function base64UrlToUint8Array(base64url: string): Uint8Array {
41
- const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
42
- const paddedBase64 = base64 + '='.repeat((4 - (base64.length % 4)) % 4);
43
- return Uint8Array.from(atob(paddedBase64), (c) => c.charCodeAt(0));
44
- }