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,278 +0,0 @@
1
- import * as tus from 'tus-js-client';
2
-
3
- import type { UploadItem, UploadOptions, UploadProgress, UseTusUploadReturn } from '~/interfaces/upload.interface';
4
-
5
- export function useTusUpload(defaultOptions: UploadOptions = {}): UseTusUploadReturn {
6
- const config = useRuntimeConfig();
7
-
8
- // State
9
- const uploadItems = ref<Map<string, UploadItem>>(new Map());
10
- const tusUploads = ref<Map<string, tus.Upload>>(new Map());
11
-
12
- // Default config
13
- const defaultConfig: UploadOptions = {
14
- autoStart: true,
15
- chunkSize: 5 * 1024 * 1024, // 5MB chunks
16
- endpoint: `${config.public.host}/files/upload`,
17
- parallelUploads: 3,
18
- retryDelays: [0, 1000, 3000, 5000, 10000],
19
- ...defaultOptions,
20
- };
21
-
22
- // Computed
23
- const uploads = computed(() => Array.from(uploadItems.value.values()));
24
- const isUploading = computed(() => uploads.value.some((u) => u.status === 'uploading'));
25
- const totalProgress = computed<UploadProgress>(() => {
26
- const items = uploads.value;
27
- if (items.length === 0) {
28
- return { bytesTotal: 0, bytesUploaded: 0, percentage: 0, remainingTime: 0, speed: 0 };
29
- }
30
-
31
- const bytesUploaded = items.reduce((acc, i) => acc + i.progress.bytesUploaded, 0);
32
- const bytesTotal = items.reduce((acc, i) => acc + i.progress.bytesTotal, 0);
33
- const speed = items.reduce((acc, i) => acc + i.progress.speed, 0);
34
-
35
- return {
36
- bytesTotal,
37
- bytesUploaded,
38
- percentage: bytesTotal > 0 ? Math.round((bytesUploaded / bytesTotal) * 100) : 0,
39
- remainingTime: speed > 0 ? Math.ceil((bytesTotal - bytesUploaded) / speed) : 0,
40
- speed,
41
- };
42
- });
43
-
44
- // Helper: Generate unique ID
45
- function generateId(): string {
46
- return `upload_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
47
- }
48
-
49
- // Helper: Calculate speed with smoothing
50
- function createSpeedTracker() {
51
- let lastBytes = 0;
52
- let lastTime = Date.now();
53
- let smoothedSpeed = 0;
54
-
55
- return (bytesUploaded: number): number => {
56
- const now = Date.now();
57
- const timeDiff = (now - lastTime) / 1000;
58
- const bytesDiff = bytesUploaded - lastBytes;
59
-
60
- if (timeDiff > 0) {
61
- const currentSpeed = bytesDiff / timeDiff;
62
- // Exponential moving average for smoother display
63
- smoothedSpeed = smoothedSpeed === 0 ? currentSpeed : smoothedSpeed * 0.7 + currentSpeed * 0.3;
64
- }
65
-
66
- lastBytes = bytesUploaded;
67
- lastTime = now;
68
-
69
- return Math.round(smoothedSpeed);
70
- };
71
- }
72
-
73
- // Update item in map (triggers reactivity)
74
- function updateItem(id: string, updates: Partial<UploadItem>): void {
75
- const item = uploadItems.value.get(id);
76
- if (item) {
77
- const newMap = new Map(uploadItems.value);
78
- newMap.set(id, { ...item, ...updates });
79
- uploadItems.value = newMap;
80
- }
81
- }
82
-
83
- // Create TUS upload instance
84
- function createTusUpload(item: UploadItem, options: UploadOptions): tus.Upload {
85
- const speedTracker = createSpeedTracker();
86
-
87
- return new tus.Upload(item.file, {
88
- chunkSize: options.chunkSize || defaultConfig.chunkSize,
89
- endpoint: options.endpoint || defaultConfig.endpoint,
90
- headers: options.headers,
91
- metadata: {
92
- filename: item.file.name,
93
- filetype: item.file.type,
94
- ...options.metadata,
95
- ...item.metadata,
96
- },
97
- onBeforeRequest: (req) => {
98
- const xhr = req.getUnderlyingObject() as XMLHttpRequest;
99
- xhr.withCredentials = true;
100
- },
101
- onError: (error) => {
102
- updateItem(item.id, {
103
- error: error.message,
104
- status: 'error',
105
- });
106
- options.onError?.(uploadItems.value.get(item.id)!, error);
107
- },
108
-
109
- onProgress: (bytesUploaded, bytesTotal) => {
110
- const speed = speedTracker(bytesUploaded);
111
- const percentage = Math.round((bytesUploaded / bytesTotal) * 100);
112
- const remainingTime = speed > 0 ? Math.ceil((bytesTotal - bytesUploaded) / speed) : 0;
113
-
114
- updateItem(item.id, {
115
- progress: { bytesTotal, bytesUploaded, percentage, remainingTime, speed },
116
- });
117
-
118
- options.onProgress?.(uploadItems.value.get(item.id)!);
119
- },
120
-
121
- onShouldRetry: (err) => {
122
- const status = (err as { originalResponse?: { getStatus?: () => number } }).originalResponse?.getStatus?.();
123
- // Don't retry on 4xx errors (except 429 Too Many Requests)
124
- if (status && status >= 400 && status < 500 && status !== 429) {
125
- return false;
126
- }
127
- return true;
128
- },
129
-
130
- onSuccess: () => {
131
- const tusUpload = tusUploads.value.get(item.id);
132
- const currentItem = uploadItems.value.get(item.id);
133
- updateItem(item.id, {
134
- completedAt: new Date(),
135
- progress: { ...currentItem!.progress, percentage: 100 },
136
- status: 'completed',
137
- url: tusUpload?.url ?? undefined,
138
- });
139
- options.onSuccess?.(uploadItems.value.get(item.id)!);
140
- },
141
-
142
- retryDelays: options.retryDelays || defaultConfig.retryDelays,
143
- });
144
- }
145
-
146
- // Actions
147
- function addFiles(files: File | File[]): string[] {
148
- const fileArray = Array.isArray(files) ? files : [files];
149
- const ids: string[] = [];
150
-
151
- for (const file of fileArray) {
152
- const id = generateId();
153
- const item: UploadItem = {
154
- file,
155
- id,
156
- metadata: defaultConfig.metadata,
157
- progress: { bytesTotal: file.size, bytesUploaded: 0, percentage: 0, remainingTime: 0, speed: 0 },
158
- status: 'idle',
159
- };
160
-
161
- const newMap = new Map(uploadItems.value);
162
- newMap.set(id, item);
163
- uploadItems.value = newMap;
164
-
165
- const tusUpload = createTusUpload(item, defaultConfig);
166
- tusUploads.value.set(id, tusUpload);
167
-
168
- ids.push(id);
169
- }
170
-
171
- if (defaultConfig.autoStart) {
172
- startAll();
173
- }
174
-
175
- return ids;
176
- }
177
-
178
- function startUpload(id: string): void {
179
- const item = uploadItems.value.get(id);
180
- const tusUpload = tusUploads.value.get(id);
181
-
182
- if (item && tusUpload && item.status !== 'uploading') {
183
- updateItem(id, { startedAt: new Date(), status: 'uploading' });
184
-
185
- // Check for previous uploads to resume
186
- tusUpload.findPreviousUploads().then((previousUploads) => {
187
- const previousUpload = previousUploads[0];
188
- if (previousUpload) {
189
- tusUpload.resumeFromPreviousUpload(previousUpload);
190
- }
191
- tusUpload.start();
192
- });
193
- }
194
- }
195
-
196
- function startAll(): void {
197
- const pending = uploads.value.filter((u) => u.status === 'idle' || u.status === 'paused');
198
- const currentlyUploading = uploads.value.filter((u) => u.status === 'uploading').length;
199
- const limit = (defaultConfig.parallelUploads || 3) - currentlyUploading;
200
-
201
- pending.slice(0, Math.max(0, limit)).forEach((item) => startUpload(item.id));
202
- }
203
-
204
- function pauseUpload(id: string): void {
205
- const tusUpload = tusUploads.value.get(id);
206
- if (tusUpload) {
207
- tusUpload.abort();
208
- updateItem(id, { status: 'paused' });
209
- }
210
- }
211
-
212
- function pauseAll(): void {
213
- uploads.value.filter((u) => u.status === 'uploading').forEach((item) => pauseUpload(item.id));
214
- }
215
-
216
- function resumeUpload(id: string): void {
217
- startUpload(id);
218
- }
219
-
220
- function resumeAll(): void {
221
- uploads.value.filter((u) => u.status === 'paused').forEach((item) => resumeUpload(item.id));
222
- }
223
-
224
- function cancelUpload(id: string): void {
225
- const tusUpload = tusUploads.value.get(id);
226
- if (tusUpload) {
227
- tusUpload.abort();
228
- }
229
- tusUploads.value.delete(id);
230
-
231
- const newMap = new Map(uploadItems.value);
232
- newMap.delete(id);
233
- uploadItems.value = newMap;
234
- }
235
-
236
- function cancelAll(): void {
237
- uploads.value.forEach((item) => cancelUpload(item.id));
238
- }
239
-
240
- function removeUpload(id: string): void {
241
- cancelUpload(id);
242
- }
243
-
244
- function clearCompleted(): void {
245
- uploads.value.filter((u) => u.status === 'completed').forEach((item) => removeUpload(item.id));
246
- }
247
-
248
- function retryUpload(id: string): void {
249
- const item = uploadItems.value.get(id);
250
- if (item && item.status === 'error') {
251
- updateItem(id, { error: undefined, status: 'idle' });
252
- startUpload(id);
253
- }
254
- }
255
-
256
- function getUpload(id: string): undefined | UploadItem {
257
- return uploadItems.value.get(id);
258
- }
259
-
260
- return {
261
- addFiles,
262
- cancelAll,
263
- cancelUpload,
264
- clearCompleted,
265
- getUpload,
266
- isUploading,
267
- pauseAll,
268
- pauseUpload,
269
- removeUpload,
270
- resumeAll,
271
- resumeUpload,
272
- retryUpload,
273
- startAll,
274
- startUpload,
275
- totalProgress,
276
- uploads,
277
- };
278
- }
@@ -1 +0,0 @@
1
- export const tw = <T extends string | TemplateStringsArray>(tailwindClasses: T) => tailwindClasses;
@@ -1,58 +0,0 @@
1
- import type { ComputedRef } from 'vue';
2
-
3
- export interface UploadItem {
4
- completedAt?: Date;
5
- error?: string;
6
- file: File;
7
- id: string;
8
- metadata?: Record<string, string>;
9
- progress: UploadProgress;
10
- startedAt?: Date;
11
- status: UploadStatus;
12
- url?: string;
13
- }
14
-
15
- export interface UploadOptions {
16
- autoStart?: boolean;
17
- chunkSize?: number;
18
- endpoint?: string;
19
- headers?: Record<string, string>;
20
- metadata?: Record<string, string>;
21
- onError?: (item: UploadItem, error: Error) => void;
22
- onProgress?: (item: UploadItem) => void;
23
- onSuccess?: (item: UploadItem) => void;
24
- parallelUploads?: number;
25
- retryDelays?: number[];
26
- }
27
-
28
- export interface UploadProgress {
29
- bytesTotal: number;
30
- bytesUploaded: number;
31
- percentage: number;
32
- remainingTime: number; // seconds
33
- speed: number; // bytes/second
34
- }
35
-
36
- export type UploadStatus = 'completed' | 'error' | 'idle' | 'paused' | 'uploading';
37
-
38
- export interface UseTusUploadReturn {
39
- // Actions
40
- addFiles: (files: File | File[]) => string[];
41
- cancelAll: () => void;
42
- cancelUpload: (id: string) => void;
43
- clearCompleted: () => void;
44
- getUpload: (id: string) => undefined | UploadItem;
45
-
46
- // State
47
- isUploading: ComputedRef<boolean>;
48
- pauseAll: () => void;
49
- pauseUpload: (id: string) => void;
50
- removeUpload: (id: string) => void;
51
- resumeAll: () => void;
52
- resumeUpload: (id: string) => void;
53
- retryUpload: (id: string) => void;
54
- startAll: () => void;
55
- startUpload: (id: string) => void;
56
- totalProgress: ComputedRef<UploadProgress>;
57
- uploads: ComputedRef<UploadItem[]>;
58
- }
@@ -1,229 +0,0 @@
1
- import { passkeyClient } from '@better-auth/passkey/client';
2
- import { adminClient, twoFactorClient } from 'better-auth/client/plugins';
3
- import { createAuthClient } from 'better-auth/vue';
4
-
5
- import { authFetch } from '~/lib/auth-state';
6
- import { sha256 } from '~/utils/crypto';
7
-
8
- // =============================================================================
9
- // Type Definitions
10
- // =============================================================================
11
-
12
- /**
13
- * Normalized response type for Better-Auth operations
14
- * The Vue client returns complex union types - this provides a consistent interface
15
- */
16
- export interface AuthResponse {
17
- data?: null | {
18
- redirect?: boolean;
19
- token?: null | string;
20
- url?: string;
21
- user?: {
22
- createdAt?: Date;
23
- email?: string;
24
- emailVerified?: boolean;
25
- id?: string;
26
- image?: string;
27
- name?: string;
28
- updatedAt?: Date;
29
- };
30
- };
31
- error?: null | {
32
- code?: string;
33
- message?: string;
34
- status?: number;
35
- };
36
- }
37
-
38
- /**
39
- * Configuration options for the auth client factory
40
- * All options have sensible defaults for nest-server compatibility
41
- */
42
- export interface AuthClientConfig {
43
- /** API base URL (default: from env or http://localhost:3000) */
44
- baseURL?: string;
45
- /** Auth API base path (default: '/iam' - must match nest-server betterAuth.basePath) */
46
- basePath?: string;
47
- /** 2FA redirect path (default: '/auth/2fa') */
48
- twoFactorRedirectPath?: string;
49
- /** Enable admin plugin (default: true) */
50
- enableAdmin?: boolean;
51
- /** Enable 2FA plugin (default: true) */
52
- enableTwoFactor?: boolean;
53
- /** Enable passkey plugin (default: true) */
54
- enablePasskey?: boolean;
55
- }
56
-
57
- // =============================================================================
58
- // Auth Client Factory
59
- // =============================================================================
60
-
61
- /**
62
- * Creates a configured Better-Auth client with password hashing
63
- *
64
- * This factory function allows creating auth clients with custom configuration,
65
- * making it reusable across different projects.
66
- *
67
- * @example
68
- * ```typescript
69
- * // Default configuration (works with nest-server defaults)
70
- * const authClient = createBetterAuthClient();
71
- *
72
- * // Custom configuration
73
- * const authClient = createBetterAuthClient({
74
- * baseURL: 'https://api.example.com',
75
- * basePath: '/auth',
76
- * twoFactorRedirectPath: '/login/2fa',
77
- * });
78
- * ```
79
- *
80
- * SECURITY: Passwords are hashed with SHA256 client-side to prevent
81
- * plain text password transmission over the network.
82
- */
83
- export function createBetterAuthClient(config: AuthClientConfig = {}) {
84
- // In development, use empty baseURL and /api/iam path to leverage Nuxt server proxy
85
- // This is REQUIRED for WebAuthn/Passkey to work correctly because:
86
- // - Frontend runs on localhost:3002, API on localhost:3000
87
- // - WebAuthn validates the origin, which must be consistent
88
- // - The Nuxt server proxy ensures requests come from the frontend origin
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';
93
- const defaultBasePath = isDev ? '/api/iam' : '/iam';
94
-
95
- const { baseURL = defaultBaseURL, basePath = defaultBasePath, twoFactorRedirectPath = '/auth/2fa', enableAdmin = true, enableTwoFactor = true, enablePasskey = true } = config;
96
-
97
- // Build plugins array based on configuration
98
- const plugins: any[] = [];
99
-
100
- if (enableAdmin) {
101
- plugins.push(adminClient());
102
- }
103
-
104
- if (enableTwoFactor) {
105
- plugins.push(
106
- twoFactorClient({
107
- onTwoFactorRedirect() {
108
- navigateTo(twoFactorRedirectPath);
109
- },
110
- }),
111
- );
112
- }
113
-
114
- if (enablePasskey) {
115
- plugins.push(passkeyClient());
116
- }
117
-
118
- // Create base client with configuration
119
- // Uses authFetch for automatic Cookie/JWT dual-mode authentication
120
- const baseClient = createAuthClient({
121
- basePath,
122
- baseURL,
123
- fetchOptions: {
124
- customFetchImpl: authFetch,
125
- },
126
- plugins,
127
- });
128
-
129
- // Return extended client with password hashing
130
- return {
131
- // Spread all base client properties and methods
132
- ...baseClient,
133
-
134
- // Explicitly pass through methods not captured by spread operator
135
- useSession: baseClient.useSession,
136
- passkey: (baseClient as any).passkey,
137
- admin: (baseClient as any).admin,
138
- $Infer: baseClient.$Infer,
139
- $fetch: baseClient.$fetch,
140
- $store: baseClient.$store,
141
-
142
- /**
143
- * Change password for an authenticated user (both passwords are hashed)
144
- */
145
- changePassword: async (params: { currentPassword: string; newPassword: string }, options?: any) => {
146
- const [hashedCurrent, hashedNew] = await Promise.all([sha256(params.currentPassword), sha256(params.newPassword)]);
147
- return baseClient.changePassword?.({ currentPassword: hashedCurrent, newPassword: hashedNew }, options);
148
- },
149
-
150
- /**
151
- * Reset password with token (new password is hashed before sending)
152
- */
153
- resetPassword: async (params: { newPassword: string; token: string }, options?: any) => {
154
- const hashedPassword = await sha256(params.newPassword);
155
- return baseClient.resetPassword?.({ newPassword: hashedPassword, token: params.token }, options);
156
- },
157
-
158
- // Override signIn to hash password (keep passkey method from plugin)
159
- signIn: {
160
- ...baseClient.signIn,
161
- /**
162
- * Sign in with email and password (password is hashed before sending)
163
- */
164
- email: async (params: { email: string; password: string; rememberMe?: boolean }, options?: any) => {
165
- const hashedPassword = await sha256(params.password);
166
- return baseClient.signIn.email({ ...params, password: hashedPassword }, options);
167
- },
168
- /**
169
- * Sign in with passkey (pass through to base client - provided by passkeyClient plugin)
170
- * @see https://www.better-auth.com/docs/plugins/passkey
171
- */
172
- passkey: (baseClient.signIn as any).passkey,
173
- },
174
-
175
- // Explicitly pass through signOut (not captured by spread operator)
176
- signOut: baseClient.signOut,
177
-
178
- // Override signUp to hash password
179
- signUp: {
180
- ...baseClient.signUp,
181
- /**
182
- * Sign up with email and password (password is hashed before sending)
183
- */
184
- email: async (params: { email: string; name: string; password: string }, options?: any) => {
185
- const hashedPassword = await sha256(params.password);
186
- return baseClient.signUp.email({ ...params, password: hashedPassword }, options);
187
- },
188
- },
189
-
190
- // Override twoFactor to hash passwords (provided by twoFactorClient plugin)
191
- twoFactor: {
192
- ...(baseClient as any).twoFactor,
193
- /**
194
- * Disable 2FA (password is hashed before sending)
195
- */
196
- disable: async (params: { password: string }, options?: any) => {
197
- const hashedPassword = await sha256(params.password);
198
- return (baseClient as any).twoFactor.disable({ password: hashedPassword }, options);
199
- },
200
- /**
201
- * Enable 2FA (password is hashed before sending)
202
- */
203
- enable: async (params: { password: string }, options?: any) => {
204
- const hashedPassword = await sha256(params.password);
205
- return (baseClient as any).twoFactor.enable({ password: hashedPassword }, options);
206
- },
207
- /**
208
- * Verify TOTP code (pass through to base client)
209
- */
210
- verifyTotp: (baseClient as any).twoFactor.verifyTotp,
211
- /**
212
- * Verify backup code (pass through to base client)
213
- */
214
- verifyBackupCode: (baseClient as any).twoFactor.verifyBackupCode,
215
- },
216
- };
217
- }
218
-
219
- // =============================================================================
220
- // Default Auth Client Instance
221
- // =============================================================================
222
-
223
- /**
224
- * Default auth client instance with standard nest-server configuration
225
- * Use createBetterAuthClient() for custom configuration
226
- */
227
- export const authClient = createBetterAuthClient();
228
-
229
- export type AuthClient = ReturnType<typeof createBetterAuthClient>;