@lenne.tech/nest-server 11.7.2 → 11.8.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 (50) hide show
  1. package/dist/core/common/interfaces/server-options.interface.d.ts +22 -0
  2. package/dist/core/modules/auth/guards/roles.guard.d.ts +12 -2
  3. package/dist/core/modules/auth/guards/roles.guard.js +192 -5
  4. package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
  5. package/dist/core/modules/file/core-file.controller.d.ts +1 -0
  6. package/dist/core/modules/file/core-file.controller.js +22 -0
  7. package/dist/core/modules/file/core-file.controller.js.map +1 -1
  8. package/dist/core/modules/tus/core-tus.controller.d.ts +9 -0
  9. package/dist/core/modules/tus/core-tus.controller.js +85 -0
  10. package/dist/core/modules/tus/core-tus.controller.js.map +1 -0
  11. package/dist/core/modules/tus/core-tus.service.d.ts +30 -0
  12. package/dist/core/modules/tus/core-tus.service.js +284 -0
  13. package/dist/core/modules/tus/core-tus.service.js.map +1 -0
  14. package/dist/core/modules/tus/index.d.ts +4 -0
  15. package/dist/core/modules/tus/index.js +21 -0
  16. package/dist/core/modules/tus/index.js.map +1 -0
  17. package/dist/core/modules/tus/interfaces/tus-config.interface.d.ts +10 -0
  18. package/dist/core/modules/tus/interfaces/tus-config.interface.js +59 -0
  19. package/dist/core/modules/tus/interfaces/tus-config.interface.js.map +1 -0
  20. package/dist/core/modules/tus/tus.module.d.ts +21 -0
  21. package/dist/core/modules/tus/tus.module.js +99 -0
  22. package/dist/core/modules/tus/tus.module.js.map +1 -0
  23. package/dist/core/modules/user/core-user.service.d.ts +1 -0
  24. package/dist/core/modules/user/core-user.service.js +12 -0
  25. package/dist/core/modules/user/core-user.service.js.map +1 -1
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.js +1 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/server/modules/file/file.controller.d.ts +5 -7
  30. package/dist/server/modules/file/file.controller.js +3 -31
  31. package/dist/server/modules/file/file.controller.js.map +1 -1
  32. package/dist/server/server.module.js +3 -1
  33. package/dist/server/server.module.js.map +1 -1
  34. package/dist/tsconfig.build.tsbuildinfo +1 -1
  35. package/package.json +4 -1
  36. package/src/core/common/interfaces/server-options.interface.ts +154 -0
  37. package/src/core/modules/auth/guards/roles.guard.ts +298 -5
  38. package/src/core/modules/file/README.md +165 -0
  39. package/src/core/modules/file/core-file.controller.ts +27 -1
  40. package/src/core/modules/tus/INTEGRATION-CHECKLIST.md +176 -0
  41. package/src/core/modules/tus/README.md +439 -0
  42. package/src/core/modules/tus/core-tus.controller.ts +88 -0
  43. package/src/core/modules/tus/core-tus.service.ts +424 -0
  44. package/src/core/modules/tus/index.ts +5 -0
  45. package/src/core/modules/tus/interfaces/tus-config.interface.ts +107 -0
  46. package/src/core/modules/tus/tus.module.ts +187 -0
  47. package/src/core/modules/user/core-user.service.ts +27 -0
  48. package/src/index.ts +6 -0
  49. package/src/server/modules/file/file.controller.ts +14 -34
  50. package/src/server/server.module.ts +5 -1
@@ -0,0 +1,424 @@
1
+ import { Injectable, Logger, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
2
+ import { FileStore } from '@tus/file-store';
3
+ import { Server, Upload } from '@tus/server';
4
+ import * as fs from 'fs';
5
+ import { Connection, mongo } from 'mongoose';
6
+ import * as path from 'path';
7
+
8
+ import { GridFSHelper } from '../../common/helpers/gridfs.helper';
9
+ import { ITusConfig } from '../../common/interfaces/server-options.interface';
10
+ import {
11
+ DEFAULT_TUS_ALLOWED_HEADERS,
12
+ DEFAULT_TUS_CONFIG,
13
+ normalizeTusConfig,
14
+ parseExpirationTime,
15
+ } from './interfaces/tus-config.interface';
16
+
17
+ /**
18
+ * Core TUS Service
19
+ *
20
+ * Provides integration with @tus/server for resumable file uploads.
21
+ * After upload completion, files are migrated to GridFS and a File entity is created.
22
+ *
23
+ * This service follows the Module Inheritance Pattern and can be extended in projects.
24
+ */
25
+ @Injectable()
26
+ export class CoreTusService implements OnModuleDestroy, OnModuleInit {
27
+ private readonly logger = new Logger(CoreTusService.name);
28
+ private tusServer: null | Server = null;
29
+ private config: ITusConfig;
30
+ private files: mongo.GridFSBucket;
31
+ private cleanupInterval: NodeJS.Timeout | null = null;
32
+
33
+ constructor(private readonly connection: Connection) {
34
+ // Initialize with defaults - will be configured in onModuleInit or via configure()
35
+ this.config = { ...DEFAULT_TUS_CONFIG };
36
+ }
37
+
38
+ /**
39
+ * Configure the TUS service
40
+ * Called by TusModule.forRoot() with the resolved configuration
41
+ */
42
+ configure(config: boolean | ITusConfig | undefined): void {
43
+ const normalizedConfig = normalizeTusConfig(config);
44
+ if (normalizedConfig) {
45
+ this.config = normalizedConfig;
46
+ }
47
+ }
48
+
49
+ async onModuleInit(): Promise<void> {
50
+ if (!this.config.enabled) {
51
+ this.logger.debug('TUS uploads disabled');
52
+ return;
53
+ }
54
+
55
+ // Initialize GridFS bucket
56
+ this.files = new mongo.GridFSBucket(this.connection.db, { bucketName: 'fs' });
57
+
58
+ // Ensure upload directory exists
59
+ const uploadDir = this.config.uploadDir || DEFAULT_TUS_CONFIG.uploadDir;
60
+ await this.ensureUploadDir(uploadDir);
61
+
62
+ // Create TUS server instance
63
+ this.tusServer = this.createTusServer(uploadDir);
64
+
65
+ // Setup expiration cleanup if enabled
66
+ this.setupExpirationCleanup();
67
+
68
+ this.logger.log(`TUS server initialized at ${this.config.path}`);
69
+ this.logEnabledFeatures();
70
+ }
71
+
72
+ async onModuleDestroy(): Promise<void> {
73
+ if (this.cleanupInterval) {
74
+ clearInterval(this.cleanupInterval);
75
+ this.cleanupInterval = null;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Get the TUS server instance
81
+ */
82
+ getServer(): null | Server {
83
+ return this.tusServer;
84
+ }
85
+
86
+ /**
87
+ * Get the current configuration
88
+ */
89
+ getConfig(): ITusConfig {
90
+ return this.config;
91
+ }
92
+
93
+ /**
94
+ * Check if TUS is enabled
95
+ */
96
+ isEnabled(): boolean {
97
+ return this.config.enabled !== false && this.tusServer !== null;
98
+ }
99
+
100
+ /**
101
+ * Get the configured path
102
+ */
103
+ getPath(): string {
104
+ return this.config.path || DEFAULT_TUS_CONFIG.path;
105
+ }
106
+
107
+ /**
108
+ * Handle upload completion - migrate to GridFS
109
+ *
110
+ * This method can be overridden in extending services to customize
111
+ * what happens after an upload completes.
112
+ */
113
+ protected async onUploadComplete(upload: Upload): Promise<void> {
114
+ const uploadDir = this.config.uploadDir || DEFAULT_TUS_CONFIG.uploadDir;
115
+ const filePath = path.join(uploadDir, upload.id);
116
+
117
+ try {
118
+ // Extract metadata
119
+ const metadata = this.parseMetadata(upload.metadata);
120
+ const filename = metadata.filename || upload.id;
121
+ const contentType = metadata.filetype || 'application/octet-stream';
122
+
123
+ // Check if file exists
124
+ const fileExists = await fs.promises
125
+ .access(filePath)
126
+ .then(() => true)
127
+ .catch(() => false);
128
+ if (!fileExists) {
129
+ this.logger.warn(`Upload file not found at ${filePath}, skipping GridFS migration`);
130
+ return;
131
+ }
132
+
133
+ // Read the completed file and upload to GridFS
134
+ const readStream = fs.createReadStream(filePath);
135
+ const fileInfo = await GridFSHelper.writeFileFromStream(this.files, readStream, {
136
+ contentType,
137
+ filename,
138
+ metadata: {
139
+ originalMetadata: metadata,
140
+ tusUploadId: upload.id,
141
+ uploadedAt: new Date(),
142
+ },
143
+ });
144
+
145
+ this.logger.debug(`Upload ${upload.id} migrated to GridFS as ${fileInfo._id} (filename: ${filename})`);
146
+
147
+ // Clean up the temporary file
148
+ await this.deleteTemporaryFile(upload.id);
149
+ } catch (error) {
150
+ this.logger.error(`Failed to migrate upload ${upload.id} to GridFS: ${error.message}`);
151
+ throw error;
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Handle upload termination (deletion)
157
+ */
158
+ protected async onUploadTerminate(upload: Upload): Promise<void> {
159
+ this.logger.debug(`Upload ${upload.id} terminated`);
160
+ await this.deleteTemporaryFile(upload.id);
161
+ }
162
+
163
+ /**
164
+ * Validate file type against allowedTypes configuration
165
+ *
166
+ * This method can be overridden in extending services to customize
167
+ * file type validation logic.
168
+ *
169
+ * @param filetype - The MIME type from upload metadata
170
+ * @returns true if allowed, false if rejected
171
+ */
172
+ protected validateFileType(filetype: string | undefined): boolean {
173
+ const allowedTypes = this.config.allowedTypes;
174
+
175
+ // If no restrictions configured, allow all types
176
+ if (!allowedTypes || allowedTypes.length === 0) {
177
+ return true;
178
+ }
179
+
180
+ // If no filetype provided in metadata, reject when restrictions exist
181
+ if (!filetype) {
182
+ return false;
183
+ }
184
+
185
+ // Check exact match
186
+ if (allowedTypes.includes(filetype)) {
187
+ return true;
188
+ }
189
+
190
+ // Check wildcard patterns (e.g., 'image/*')
191
+ for (const allowed of allowedTypes) {
192
+ if (allowed.endsWith('/*')) {
193
+ const prefix = allowed.slice(0, -1); // 'image/*' -> 'image/'
194
+ if (filetype.startsWith(prefix)) {
195
+ return true;
196
+ }
197
+ }
198
+ }
199
+
200
+ return false;
201
+ }
202
+
203
+ /**
204
+ * Create the TUS server instance with configured extensions
205
+ */
206
+ private createTusServer(uploadDir: string): Server {
207
+ const datastore = new FileStore({ directory: uploadDir });
208
+
209
+ const server = new Server({
210
+ allowedHeaders: this.config.allowedHeaders || DEFAULT_TUS_ALLOWED_HEADERS,
211
+ datastore,
212
+ maxSize: this.config.maxSize,
213
+ onUploadCreate: async (_req, upload) => {
214
+ // Validate file type if allowedTypes is configured
215
+ if (this.config.allowedTypes && this.config.allowedTypes.length > 0) {
216
+ const metadata = this.parseMetadata(upload.metadata);
217
+ const filetype = metadata.filetype;
218
+
219
+ if (!this.validateFileType(filetype)) {
220
+ const allowedList = this.config.allowedTypes.join(', ');
221
+ this.logger.warn(
222
+ `Upload rejected: file type '${filetype || 'unknown'}' not allowed. Allowed types: ${allowedList}`,
223
+ );
224
+
225
+ // Throw error to reject the upload
226
+ // @tus/server v2 expects throwing an error with status_code
227
+ const error = new Error(
228
+ `File type '${filetype || 'unknown'}' is not allowed. Allowed types: ${allowedList}`,
229
+ );
230
+ (error as any).status_code = 415; // Unsupported Media Type
231
+ throw error;
232
+ }
233
+ }
234
+
235
+ // Return empty object to proceed with upload
236
+ return {};
237
+ },
238
+ onUploadFinish: async (_req, upload) => {
239
+ try {
240
+ await this.onUploadComplete(upload);
241
+ return {};
242
+ } catch (error) {
243
+ this.logger.error(`Upload finish error: ${error.message}`);
244
+ return {};
245
+ }
246
+ },
247
+ path: this.config.path || DEFAULT_TUS_CONFIG.path,
248
+ respectForwardedHeaders: true,
249
+ });
250
+
251
+ return server;
252
+ }
253
+
254
+ /**
255
+ * Parse TUS metadata into object
256
+ * @tus/server v2 already parses metadata into an object
257
+ */
258
+ private parseMetadata(metadata: Record<string, string> | string | undefined): Record<string, string> {
259
+ if (!metadata) {
260
+ return {};
261
+ }
262
+
263
+ // @tus/server v2 returns metadata as an object
264
+ if (typeof metadata === 'object') {
265
+ return metadata;
266
+ }
267
+
268
+ // Fallback for string format (legacy or raw)
269
+ const result: Record<string, string> = {};
270
+ const pairs = metadata.split(',');
271
+
272
+ for (const pair of pairs) {
273
+ const [key, value] = pair.trim().split(' ');
274
+ if (key) {
275
+ // Decode base64 value if present
276
+ result[key] = value ? Buffer.from(value, 'base64').toString('utf-8') : '';
277
+ }
278
+ }
279
+
280
+ return result;
281
+ }
282
+
283
+ /**
284
+ * Ensure the upload directory exists
285
+ */
286
+ private async ensureUploadDir(uploadDir: string): Promise<void> {
287
+ try {
288
+ await fs.promises.mkdir(uploadDir, { recursive: true });
289
+ } catch (error) {
290
+ if (error.code !== 'EEXIST') {
291
+ throw error;
292
+ }
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Delete a temporary upload file
298
+ */
299
+ private async deleteTemporaryFile(uploadId: string): Promise<void> {
300
+ const uploadDir = this.config.uploadDir || DEFAULT_TUS_CONFIG.uploadDir;
301
+ const filePath = path.join(uploadDir, uploadId);
302
+ const infoPath = `${filePath}.json`;
303
+
304
+ try {
305
+ await fs.promises.unlink(filePath);
306
+ } catch {
307
+ // Ignore if file doesn't exist
308
+ }
309
+
310
+ try {
311
+ await fs.promises.unlink(infoPath);
312
+ } catch {
313
+ // Ignore if info file doesn't exist
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Setup periodic cleanup of expired uploads
319
+ */
320
+ private setupExpirationCleanup(): void {
321
+ const expirationConfig = this.config.expiration;
322
+ // Handle boolean | ITusExpirationConfig type
323
+ if (!expirationConfig) {
324
+ return;
325
+ }
326
+
327
+ let expiresIn = '24h';
328
+
329
+ // If explicitly set to false, skip
330
+ if (typeof expirationConfig === 'boolean') {
331
+ if (!expirationConfig) {
332
+ return;
333
+ }
334
+ // If true, use defaults (expiresIn already set to '24h')
335
+ } else {
336
+ // ITusExpirationConfig object
337
+ if (expirationConfig.enabled === false) {
338
+ return;
339
+ }
340
+ expiresIn = expirationConfig.expiresIn || '24h';
341
+ }
342
+
343
+ const expirationMs = parseExpirationTime(expiresIn);
344
+
345
+ // Run cleanup every hour
346
+ this.cleanupInterval = setInterval(
347
+ async () => {
348
+ await this.cleanupExpiredUploads(expirationMs);
349
+ },
350
+ 60 * 60 * 1000,
351
+ );
352
+
353
+ this.logger.debug(`Expiration cleanup scheduled (expire after ${expiresIn})`);
354
+ }
355
+
356
+ /**
357
+ * Clean up expired incomplete uploads
358
+ */
359
+ private async cleanupExpiredUploads(maxAgeMs: number): Promise<void> {
360
+ const uploadDir = this.config.uploadDir || DEFAULT_TUS_CONFIG.uploadDir;
361
+ const now = Date.now();
362
+
363
+ try {
364
+ const files = await fs.promises.readdir(uploadDir);
365
+ let cleanedCount = 0;
366
+
367
+ for (const file of files) {
368
+ if (file.endsWith('.json')) {
369
+ continue; // Skip info files
370
+ }
371
+
372
+ const filePath = path.join(uploadDir, file);
373
+ const stats = await fs.promises.stat(filePath);
374
+ const age = now - stats.mtimeMs;
375
+
376
+ if (age > maxAgeMs) {
377
+ await this.deleteTemporaryFile(file);
378
+ cleanedCount++;
379
+ }
380
+ }
381
+
382
+ if (cleanedCount > 0) {
383
+ this.logger.debug(`Cleaned up ${cleanedCount} expired uploads`);
384
+ }
385
+ } catch (error) {
386
+ this.logger.warn(`Failed to cleanup expired uploads: ${error.message}`);
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Log which features are enabled
392
+ */
393
+ private logEnabledFeatures(): void {
394
+ const features: string[] = [];
395
+
396
+ if (this.config.creation !== false) {
397
+ features.push('creation');
398
+ }
399
+ if (this.config.creationWithUpload !== false) {
400
+ features.push('creation-with-upload');
401
+ }
402
+ if (this.config.termination !== false) {
403
+ features.push('termination');
404
+ }
405
+ if (this.config.expiration !== false) {
406
+ features.push('expiration');
407
+ }
408
+ if (this.config.checksum !== false) {
409
+ features.push('checksum');
410
+ }
411
+ if (this.config.concatenation !== false) {
412
+ features.push('concatenation');
413
+ }
414
+
415
+ if (features.length > 0) {
416
+ this.logger.log(`TUS extensions: ${features.join(', ')}`);
417
+ }
418
+
419
+ // Log file type restrictions if configured
420
+ if (this.config.allowedTypes && this.config.allowedTypes.length > 0) {
421
+ this.logger.log(`TUS allowed types: ${this.config.allowedTypes.join(', ')}`);
422
+ }
423
+ }
424
+ }
@@ -0,0 +1,5 @@
1
+ // TUS Module exports
2
+ export * from './core-tus.controller';
3
+ export * from './core-tus.service';
4
+ export * from './interfaces/tus-config.interface';
5
+ export * from './tus.module';
@@ -0,0 +1,107 @@
1
+ /**
2
+ * TUS Configuration Helper Functions
3
+ *
4
+ * The interfaces are defined in server-options.interface.ts to avoid circular imports.
5
+ * This file contains helper functions and defaults.
6
+ */
7
+ import { ITusConfig, ITusExpirationConfig } from '../../../common/interfaces/server-options.interface';
8
+
9
+ // Re-export for convenience
10
+ export {
11
+ ITusConfig,
12
+ ITusCreationConfig,
13
+ ITusExpirationConfig,
14
+ } from '../../../common/interfaces/server-options.interface';
15
+
16
+ /**
17
+ * Additional allowed headers for TUS requests (beyond @tus/server defaults).
18
+ *
19
+ * Note: @tus/server already includes all TUS protocol headers by default:
20
+ * Authorization, Content-Type, Location, Tus-Extension, Tus-Max-Size,
21
+ * Tus-Resumable, Tus-Version, Upload-Concat, Upload-Defer-Length,
22
+ * Upload-Length, Upload-Metadata, Upload-Offset, X-HTTP-Method-Override,
23
+ * X-Requested-With, X-Forwarded-Host, X-Forwarded-Proto, Forwarded
24
+ *
25
+ * This array is only for PROJECT-SPECIFIC additional headers.
26
+ */
27
+ export const DEFAULT_TUS_ALLOWED_HEADERS: string[] = [];
28
+
29
+ /**
30
+ * Default TUS configuration
31
+ */
32
+ export const DEFAULT_TUS_CONFIG: Required<
33
+ Omit<ITusConfig, 'allowedTypes' | 'creation' | 'expiration'> & {
34
+ allowedTypes: undefined;
35
+ creation: boolean;
36
+ expiration: ITusExpirationConfig;
37
+ }
38
+ > = {
39
+ allowedHeaders: DEFAULT_TUS_ALLOWED_HEADERS,
40
+ allowedTypes: undefined,
41
+ checksum: true,
42
+ concatenation: true,
43
+ creation: true,
44
+ creationWithUpload: true,
45
+ enabled: true,
46
+ expiration: { enabled: true, expiresIn: '24h' },
47
+ maxSize: 50 * 1024 * 1024 * 1024, // 50 GB
48
+ path: '/tus',
49
+ termination: true,
50
+ uploadDir: 'uploads/tus',
51
+ };
52
+
53
+ /**
54
+ * Normalizes tus config from various input formats
55
+ * - `undefined` → enabled with defaults (new behavior: enabled by default)
56
+ * - `true` → enabled with defaults
57
+ * - `false` → disabled
58
+ * - `{ ... }` → enabled with custom config
59
+ */
60
+ export function normalizeTusConfig(config: boolean | ITusConfig | undefined): ITusConfig | null {
61
+ // Enabled by default if not specified
62
+ if (config === undefined) {
63
+ return { ...DEFAULT_TUS_CONFIG };
64
+ }
65
+ if (config === true) {
66
+ return { ...DEFAULT_TUS_CONFIG };
67
+ }
68
+ if (config === false) {
69
+ return null;
70
+ }
71
+ // Check explicit enabled: false
72
+ if (config.enabled === false) {
73
+ return null;
74
+ }
75
+ // Merge with defaults
76
+ return {
77
+ ...DEFAULT_TUS_CONFIG,
78
+ ...config,
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Parse expiration time string to milliseconds
84
+ * Supports: '24h', '1d', '12h', '30m', etc.
85
+ */
86
+ export function parseExpirationTime(expiresIn: string): number {
87
+ const match = expiresIn.match(/^(\d+)([hdms])$/);
88
+ if (!match) {
89
+ return 24 * 60 * 60 * 1000; // Default: 24 hours
90
+ }
91
+
92
+ const value = parseInt(match[1], 10);
93
+ const unit = match[2];
94
+
95
+ switch (unit) {
96
+ case 'd':
97
+ return value * 24 * 60 * 60 * 1000;
98
+ case 'h':
99
+ return value * 60 * 60 * 1000;
100
+ case 'm':
101
+ return value * 60 * 1000;
102
+ case 's':
103
+ return value * 1000;
104
+ default:
105
+ return 24 * 60 * 60 * 1000;
106
+ }
107
+ }