@hed-hog/core 0.0.361 → 0.0.364
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/dist/access-log/access-log.controller.d.ts +1 -1
- package/dist/access-log/access-log.service.d.ts +1 -1
- package/dist/access-log/access-log.service.d.ts.map +1 -1
- package/dist/access-log/access-log.service.js +43 -1
- package/dist/access-log/access-log.service.js.map +1 -1
- package/dist/auth/auth.controller.d.ts +1 -0
- package/dist/auth/auth.controller.d.ts.map +1 -1
- package/dist/auth/auth.controller.js +6 -2
- package/dist/auth/auth.controller.js.map +1 -1
- package/dist/auth/auth.service.d.ts +1 -0
- package/dist/auth/auth.service.d.ts.map +1 -1
- package/dist/auth/auth.service.js +15 -5
- package/dist/auth/auth.service.js.map +1 -1
- package/dist/auth/guards/auth.guard.d.ts +1 -0
- package/dist/auth/guards/auth.guard.d.ts.map +1 -1
- package/dist/auth/guards/auth.guard.js +11 -4
- package/dist/auth/guards/auth.guard.js.map +1 -1
- package/dist/file/file.module.d.ts.map +1 -1
- package/dist/file/file.module.js +3 -2
- package/dist/file/file.module.js.map +1 -1
- package/dist/file/file.service.d.ts +71 -1
- package/dist/file/file.service.d.ts.map +1 -1
- package/dist/file/file.service.js +180 -32
- package/dist/file/file.service.js.map +1 -1
- package/dist/file/image-optimization.service.d.ts +20 -0
- package/dist/file/image-optimization.service.d.ts.map +1 -0
- package/dist/file/image-optimization.service.js +126 -0
- package/dist/file/image-optimization.service.js.map +1 -0
- package/dist/file/provider/local.provider.d.ts +1 -1
- package/dist/file/provider/local.provider.d.ts.map +1 -1
- package/dist/file/provider/local.provider.js +2 -9
- package/dist/file/provider/local.provider.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/tools/core-users.mcp-tools.d.ts +1 -1
- package/dist/mcp/tools/core-users.mcp-tools.d.ts.map +1 -1
- package/dist/mcp/tools/core-users.mcp-tools.js +2 -2
- package/dist/mcp/tools/core-users.mcp-tools.js.map +1 -1
- package/dist/profile/profile.service.d.ts.map +1 -1
- package/dist/profile/profile.service.js +44 -23
- package/dist/profile/profile.service.js.map +1 -1
- package/dist/session/session.service.d.ts.map +1 -1
- package/dist/session/session.service.js +7 -2
- package/dist/session/session.service.js.map +1 -1
- package/dist/token/token.service.d.ts +1 -0
- package/dist/token/token.service.d.ts.map +1 -1
- package/dist/token/token.service.js +23 -6
- package/dist/token/token.service.js.map +1 -1
- package/dist/user/user.controller.d.ts +1 -1
- package/dist/user/user.controller.d.ts.map +1 -1
- package/dist/user/user.controller.js +4 -3
- package/dist/user/user.controller.js.map +1 -1
- package/dist/user/user.service.d.ts +1 -1
- package/dist/user/user.service.d.ts.map +1 -1
- package/dist/user/user.service.js +6 -6
- package/dist/user/user.service.js.map +1 -1
- package/hedhog/data/route.yaml +2 -0
- package/hedhog/data/setting_group.yaml +117 -0
- package/hedhog/data/setting_subgroup.yaml +8 -0
- package/hedhog/frontend/app/access-log/page.tsx.ejs +170 -11
- package/hedhog/frontend/app/account/components/remove-mfa-dialog.tsx.ejs +1 -1
- package/hedhog/frontend/app/account/components/verify-before-add-dialog.tsx.ejs +1 -1
- package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +112 -5
- package/hedhog/frontend/app/users/page.tsx.ejs +7 -7
- package/hedhog/frontend/app/users/permissions.tsx.ejs +137 -83
- package/hedhog/frontend/app/users/user-access-sheet.tsx.ejs +28 -26
- package/hedhog/frontend/app/users/user-sheet.tsx.ejs +207 -75
- package/hedhog/frontend/messages/en.json +9 -2
- package/hedhog/frontend/messages/pt.json +9 -2
- package/package.json +9 -4
- package/src/access-log/access-log.service.ts +19 -1
- package/src/auth/auth.controller.ts +5 -0
- package/src/auth/auth.service.ts +14 -4
- package/src/auth/guards/auth.guard.ts +10 -3
- package/src/file/file.module.ts +3 -2
- package/src/file/file.service.ts +361 -47
- package/src/file/image-optimization.service.ts +123 -0
- package/src/file/provider/local.provider.ts +2 -9
- package/src/index.ts +1 -0
- package/src/mcp/tools/core-users.mcp-tools.ts +267 -267
- package/src/profile/profile.service.ts +40 -22
- package/src/session/session.service.ts +10 -2
- package/src/token/token.service.ts +26 -6
- package/src/user/user.controller.ts +5 -2
- package/src/user/user.service.ts +11 -8
- package/dist/menu/menu.service.spec.d.ts +0 -1
- package/dist/menu/menu.service.spec.d.ts.map +0 -1
- package/dist/menu/menu.service.spec.js +0 -208
- package/dist/menu/menu.service.spec.js.map +0 -1
- package/dist/route/route.service.spec.d.ts +0 -1
- package/dist/route/route.service.spec.d.ts.map +0 -1
- package/dist/route/route.service.spec.js +0 -262
- package/dist/route/route.service.spec.js.map +0 -1
- package/dist/screen/screen.service.spec.d.ts +0 -1
- package/dist/screen/screen.service.spec.d.ts.map +0 -1
- package/dist/screen/screen.service.spec.js +0 -259
- package/dist/screen/screen.service.spec.js.map +0 -1
- package/dist/user/user.service.spec.d.ts +0 -1
- package/dist/user/user.service.spec.d.ts.map +0 -1
- package/dist/user/user.service.spec.js +0 -261
- package/dist/user/user.service.spec.js.map +0 -1
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
CanActivate,
|
|
6
6
|
ExecutionContext,
|
|
7
7
|
Injectable,
|
|
8
|
+
Logger,
|
|
8
9
|
UnauthorizedException,
|
|
9
10
|
} from '@nestjs/common';
|
|
10
11
|
import { Reflector } from '@nestjs/core';
|
|
@@ -14,6 +15,8 @@ import { TokenService } from '../../token/token.service';
|
|
|
14
15
|
|
|
15
16
|
@Injectable()
|
|
16
17
|
export class AuthGuard implements CanActivate {
|
|
18
|
+
private readonly logger = new Logger(AuthGuard.name);
|
|
19
|
+
|
|
17
20
|
constructor(
|
|
18
21
|
private token: TokenService,
|
|
19
22
|
private reflector: Reflector,
|
|
@@ -50,23 +53,26 @@ export class AuthGuard implements CanActivate {
|
|
|
50
53
|
if (isPublic) {
|
|
51
54
|
return true;
|
|
52
55
|
} else if (withNoRole) {
|
|
56
|
+
this.logger.debug(`[AuthGuard] No token: ${request.method} ${request.url}`);
|
|
53
57
|
// @NoRole() requires authentication but no specific role — token is mandatory
|
|
54
58
|
throw new UnauthorizedException(
|
|
55
59
|
getLocaleText('accessDenied', locale, 'Access denied.'),
|
|
56
60
|
);
|
|
57
61
|
} else if (withRole) {
|
|
62
|
+
this.logger.debug(`[AuthGuard] No token: ${request.method} ${request.url}`);
|
|
58
63
|
// @Role() decorator requires authentication
|
|
59
64
|
throw new UnauthorizedException(
|
|
60
65
|
getLocaleText('accessDenied', locale, 'Access denied.'),
|
|
61
66
|
);
|
|
62
67
|
} else {
|
|
68
|
+
this.logger.debug(`[AuthGuard] No token: ${request.method} ${request.url}`);
|
|
63
69
|
// No explicit decorator, default behavior (deny)
|
|
64
70
|
throw new UnauthorizedException(
|
|
65
71
|
getLocaleText('accessDenied', locale, 'Access denied.'),
|
|
66
72
|
);
|
|
67
73
|
}
|
|
68
74
|
}
|
|
69
|
-
|
|
75
|
+
|
|
70
76
|
try {
|
|
71
77
|
const payload = await this.token.verify(locale, token);
|
|
72
78
|
|
|
@@ -75,8 +81,9 @@ export class AuthGuard implements CanActivate {
|
|
|
75
81
|
if (isPublic) {
|
|
76
82
|
return true;
|
|
77
83
|
} else {
|
|
78
|
-
|
|
79
|
-
const message = (error as any)?.message ||
|
|
84
|
+
const errName = (error as any)?.name ?? 'UnknownError';
|
|
85
|
+
const message = (error as any)?.message || errName || 'Unauthorized';
|
|
86
|
+
this.logger.warn(`[AuthGuard] Token rejected ${request.method} ${request.url} error=${errName}: ${message}`);
|
|
80
87
|
throw new UnauthorizedException(message);
|
|
81
88
|
}
|
|
82
89
|
}
|
package/src/file/file.module.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { JwtModule } from '@nestjs/jwt';
|
|
|
5
5
|
import { SettingModule } from '../setting/setting.module';
|
|
6
6
|
import { FileController } from './file.controller';
|
|
7
7
|
import { FileService } from './file.service';
|
|
8
|
+
import { ImageOptimizationService } from './image-optimization.service';
|
|
8
9
|
|
|
9
10
|
@Module({
|
|
10
11
|
imports: [
|
|
@@ -15,8 +16,8 @@ import { FileService } from './file.service';
|
|
|
15
16
|
JwtModule,
|
|
16
17
|
),
|
|
17
18
|
],
|
|
18
|
-
providers: [FileService],
|
|
19
|
-
exports: [FileService],
|
|
19
|
+
providers: [FileService, ImageOptimizationService],
|
|
20
|
+
exports: [FileService, ImageOptimizationService],
|
|
20
21
|
controllers: [FileController],
|
|
21
22
|
})
|
|
22
23
|
export class FileModule { }
|
package/src/file/file.service.ts
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
GetBucketNotificationConfigurationCommand,
|
|
3
|
+
HeadBucketCommand,
|
|
4
|
+
HeadObjectCommand,
|
|
5
|
+
PutBucketNotificationConfigurationCommand,
|
|
6
|
+
S3Client,
|
|
7
|
+
} from '@aws-sdk/client-s3';
|
|
8
|
+
import {
|
|
9
|
+
AddPermissionCommand,
|
|
10
|
+
CreateFunctionCommand,
|
|
11
|
+
GetFunctionConfigurationCommand,
|
|
12
|
+
LambdaClient,
|
|
13
|
+
UpdateFunctionConfigurationCommand,
|
|
14
|
+
} from '@aws-sdk/client-lambda';
|
|
2
15
|
import {
|
|
3
16
|
AssumeRoleCommand,
|
|
4
17
|
GetSessionTokenCommand,
|
|
@@ -8,25 +21,26 @@ import { getLocaleText } from '@hed-hog/api-locale';
|
|
|
8
21
|
import { PaginationDTO, PaginationService } from '@hed-hog/api-pagination';
|
|
9
22
|
import { PrismaService } from '@hed-hog/api-prisma';
|
|
10
23
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
24
|
+
BadRequestException,
|
|
25
|
+
forwardRef,
|
|
26
|
+
Inject,
|
|
27
|
+
Injectable,
|
|
28
|
+
Logger,
|
|
29
|
+
NotFoundException,
|
|
30
|
+
OnModuleInit
|
|
18
31
|
} from '@nestjs/common';
|
|
19
32
|
import { JwtService } from '@nestjs/jwt';
|
|
20
33
|
import { createWriteStream, promises as fs } from 'fs';
|
|
21
34
|
import { basename, dirname } from 'path';
|
|
22
35
|
import { pipeline } from 'stream/promises';
|
|
23
36
|
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
37
|
+
buildStorageConfigFromIntegration,
|
|
38
|
+
resolveFileProviderSlug,
|
|
39
|
+
resolveStorageEnumProvider,
|
|
27
40
|
} from '../integration-profile/integration-profile.utils';
|
|
28
41
|
import { SettingService } from '../setting/setting.service';
|
|
29
42
|
import { DeleteDTO } from './dto/delete.dto';
|
|
43
|
+
import { ImageOptimizationService } from './image-optimization.service';
|
|
30
44
|
import { AbstractProvider } from './provider/abstract.provider';
|
|
31
45
|
import { ProviderFactory } from './provider/provider.factory';
|
|
32
46
|
|
|
@@ -71,6 +85,8 @@ export class FileService implements OnModuleInit {
|
|
|
71
85
|
private readonly settingService: SettingService,
|
|
72
86
|
@Inject(forwardRef(() => JwtService))
|
|
73
87
|
private readonly jwtService: JwtService,
|
|
88
|
+
@Inject(forwardRef(() => ImageOptimizationService))
|
|
89
|
+
private readonly imageOptimizationService: ImageOptimizationService,
|
|
74
90
|
) { }
|
|
75
91
|
|
|
76
92
|
async onModuleInit() {
|
|
@@ -397,21 +413,30 @@ export class FileService implements OnModuleInit {
|
|
|
397
413
|
fileBuffer.originalname,
|
|
398
414
|
);
|
|
399
415
|
|
|
416
|
+
const optimized = await this.imageOptimizationService.optimizeBuffer(
|
|
417
|
+
fileBuffer.buffer,
|
|
418
|
+
fileBuffer.mimetype,
|
|
419
|
+
normalizedFilename,
|
|
420
|
+
);
|
|
421
|
+
|
|
400
422
|
const normalizedFile = {
|
|
401
423
|
...fileBuffer,
|
|
402
|
-
|
|
424
|
+
buffer: optimized.buffer,
|
|
425
|
+
mimetype: optimized.mimetype,
|
|
426
|
+
originalname: optimized.originalname,
|
|
427
|
+
size: optimized.buffer.length,
|
|
403
428
|
};
|
|
404
429
|
|
|
405
430
|
const url = await provider.upload(destination, normalizedFile);
|
|
406
431
|
|
|
407
432
|
const file = await this.prismaService.file.create({
|
|
408
433
|
data: {
|
|
409
|
-
filename:
|
|
434
|
+
filename: optimized.originalname,
|
|
410
435
|
path: url,
|
|
411
436
|
provider_id: this.providerId,
|
|
412
437
|
location: destination,
|
|
413
|
-
mimetype_id: await this.getMimeType(
|
|
414
|
-
size:
|
|
438
|
+
mimetype_id: await this.getMimeType(optimized.mimetype),
|
|
439
|
+
size: optimized.buffer.length,
|
|
415
440
|
},
|
|
416
441
|
});
|
|
417
442
|
|
|
@@ -456,45 +481,35 @@ export class FileService implements OnModuleInit {
|
|
|
456
481
|
throw new BadRequestException(`File too large: ${size} bytes`);
|
|
457
482
|
}
|
|
458
483
|
|
|
459
|
-
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
};
|
|
484
|
+
// Read buffer to allow image optimization regardless of provider support
|
|
485
|
+
const rawBuffer = await fs.readFile(filePath);
|
|
486
|
+
const optimized = await this.imageOptimizationService.optimizeBuffer(
|
|
487
|
+
rawBuffer,
|
|
488
|
+
mimetype,
|
|
489
|
+
originalname,
|
|
490
|
+
);
|
|
467
491
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
encoding: metadata?.encoding ?? '7bit',
|
|
481
|
-
mimetype,
|
|
482
|
-
size,
|
|
483
|
-
destination: '',
|
|
484
|
-
filename: originalname,
|
|
485
|
-
path: filePath,
|
|
486
|
-
buffer,
|
|
487
|
-
});
|
|
488
|
-
}
|
|
492
|
+
const provider = await this.getProvider();
|
|
493
|
+
const url = await provider.upload(destination, {
|
|
494
|
+
fieldname: 'file',
|
|
495
|
+
originalname: optimized.originalname,
|
|
496
|
+
encoding: metadata?.encoding ?? '7bit',
|
|
497
|
+
mimetype: optimized.mimetype,
|
|
498
|
+
size: optimized.buffer.length,
|
|
499
|
+
destination: '',
|
|
500
|
+
filename: optimized.originalname,
|
|
501
|
+
path: filePath,
|
|
502
|
+
buffer: optimized.buffer,
|
|
503
|
+
});
|
|
489
504
|
|
|
490
505
|
const file = await this.prismaService.file.create({
|
|
491
506
|
data: {
|
|
492
|
-
filename: originalname,
|
|
507
|
+
filename: optimized.originalname,
|
|
493
508
|
path: url,
|
|
494
509
|
provider_id: this.providerId,
|
|
495
510
|
location: destination,
|
|
496
|
-
mimetype_id: await this.getMimeType(mimetype),
|
|
497
|
-
size,
|
|
511
|
+
mimetype_id: await this.getMimeType(optimized.mimetype),
|
|
512
|
+
size: optimized.buffer.length,
|
|
498
513
|
},
|
|
499
514
|
});
|
|
500
515
|
|
|
@@ -754,6 +769,305 @@ export class FileService implements OnModuleInit {
|
|
|
754
769
|
};
|
|
755
770
|
}
|
|
756
771
|
|
|
772
|
+
async headS3Bucket(params: {
|
|
773
|
+
accessKeyId: string;
|
|
774
|
+
secretAccessKey: string;
|
|
775
|
+
sessionToken?: string;
|
|
776
|
+
region: string;
|
|
777
|
+
bucket: string;
|
|
778
|
+
}): Promise<void> {
|
|
779
|
+
const s3 = new S3Client({
|
|
780
|
+
region: params.region,
|
|
781
|
+
credentials: {
|
|
782
|
+
accessKeyId: params.accessKeyId,
|
|
783
|
+
secretAccessKey: params.secretAccessKey,
|
|
784
|
+
...(params.sessionToken ? { sessionToken: params.sessionToken } : {}),
|
|
785
|
+
},
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
await s3.send(new HeadBucketCommand({ Bucket: params.bucket }));
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
async getLambdaFunctionConfiguration(params: {
|
|
792
|
+
functionName: string;
|
|
793
|
+
region: string;
|
|
794
|
+
accessKeyId: string;
|
|
795
|
+
secretAccessKey: string;
|
|
796
|
+
sessionToken?: string;
|
|
797
|
+
}) {
|
|
798
|
+
const lambda = new LambdaClient({
|
|
799
|
+
region: params.region,
|
|
800
|
+
credentials: {
|
|
801
|
+
accessKeyId: params.accessKeyId,
|
|
802
|
+
secretAccessKey: params.secretAccessKey,
|
|
803
|
+
...(params.sessionToken ? { sessionToken: params.sessionToken } : {}),
|
|
804
|
+
},
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
return lambda.send(
|
|
808
|
+
new GetFunctionConfigurationCommand({
|
|
809
|
+
FunctionName: params.functionName,
|
|
810
|
+
}),
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
async updateLambdaEnvironmentVariables(params: {
|
|
815
|
+
functionName: string;
|
|
816
|
+
region: string;
|
|
817
|
+
accessKeyId: string;
|
|
818
|
+
secretAccessKey: string;
|
|
819
|
+
sessionToken?: string;
|
|
820
|
+
environmentVariables: Record<string, string>;
|
|
821
|
+
}) {
|
|
822
|
+
const lambda = new LambdaClient({
|
|
823
|
+
region: params.region,
|
|
824
|
+
credentials: {
|
|
825
|
+
accessKeyId: params.accessKeyId,
|
|
826
|
+
secretAccessKey: params.secretAccessKey,
|
|
827
|
+
...(params.sessionToken ? { sessionToken: params.sessionToken } : {}),
|
|
828
|
+
},
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
const current = await lambda.send(
|
|
832
|
+
new GetFunctionConfigurationCommand({
|
|
833
|
+
FunctionName: params.functionName,
|
|
834
|
+
}),
|
|
835
|
+
);
|
|
836
|
+
|
|
837
|
+
const merged = {
|
|
838
|
+
...(current.Environment?.Variables ?? {}),
|
|
839
|
+
...params.environmentVariables,
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
await lambda.send(
|
|
843
|
+
new UpdateFunctionConfigurationCommand({
|
|
844
|
+
FunctionName: params.functionName,
|
|
845
|
+
Environment: {
|
|
846
|
+
Variables: merged,
|
|
847
|
+
},
|
|
848
|
+
}),
|
|
849
|
+
);
|
|
850
|
+
|
|
851
|
+
return {
|
|
852
|
+
functionName: current.FunctionName ?? params.functionName,
|
|
853
|
+
functionArn: current.FunctionArn ?? null,
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
async createLambdaFunctionIfMissing(params: {
|
|
858
|
+
functionName: string;
|
|
859
|
+
region: string;
|
|
860
|
+
accessKeyId: string;
|
|
861
|
+
secretAccessKey: string;
|
|
862
|
+
sessionToken?: string;
|
|
863
|
+
roleArn: string;
|
|
864
|
+
packageType: 'Image' | 'Zip';
|
|
865
|
+
imageUri?: string;
|
|
866
|
+
zipFile?: Uint8Array;
|
|
867
|
+
s3Bucket?: string;
|
|
868
|
+
s3Key?: string;
|
|
869
|
+
s3ObjectVersion?: string;
|
|
870
|
+
handler?: string;
|
|
871
|
+
runtime?: string;
|
|
872
|
+
timeout?: number;
|
|
873
|
+
memorySize?: number;
|
|
874
|
+
environmentVariables?: Record<string, string>;
|
|
875
|
+
}) {
|
|
876
|
+
const lambda = new LambdaClient({
|
|
877
|
+
region: params.region,
|
|
878
|
+
credentials: {
|
|
879
|
+
accessKeyId: params.accessKeyId,
|
|
880
|
+
secretAccessKey: params.secretAccessKey,
|
|
881
|
+
...(params.sessionToken ? { sessionToken: params.sessionToken } : {}),
|
|
882
|
+
},
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
try {
|
|
886
|
+
const existing = await lambda.send(
|
|
887
|
+
new GetFunctionConfigurationCommand({
|
|
888
|
+
FunctionName: params.functionName,
|
|
889
|
+
}),
|
|
890
|
+
);
|
|
891
|
+
|
|
892
|
+
return {
|
|
893
|
+
created: false,
|
|
894
|
+
functionName: existing.FunctionName ?? params.functionName,
|
|
895
|
+
functionArn: existing.FunctionArn ?? null,
|
|
896
|
+
};
|
|
897
|
+
} catch (error: any) {
|
|
898
|
+
if (String(error?.name ?? '') !== 'ResourceNotFoundException') {
|
|
899
|
+
throw error;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const baseInput = {
|
|
904
|
+
FunctionName: params.functionName,
|
|
905
|
+
Role: params.roleArn,
|
|
906
|
+
PackageType: params.packageType,
|
|
907
|
+
Timeout: params.timeout,
|
|
908
|
+
MemorySize: params.memorySize,
|
|
909
|
+
Environment: params.environmentVariables
|
|
910
|
+
? {
|
|
911
|
+
Variables: params.environmentVariables,
|
|
912
|
+
}
|
|
913
|
+
: undefined,
|
|
914
|
+
} as const;
|
|
915
|
+
|
|
916
|
+
const created =
|
|
917
|
+
params.packageType === 'Image'
|
|
918
|
+
? await lambda.send(
|
|
919
|
+
new CreateFunctionCommand({
|
|
920
|
+
...baseInput,
|
|
921
|
+
Code: {
|
|
922
|
+
ImageUri: params.imageUri,
|
|
923
|
+
},
|
|
924
|
+
}),
|
|
925
|
+
)
|
|
926
|
+
: await lambda.send(
|
|
927
|
+
new CreateFunctionCommand({
|
|
928
|
+
...baseInput,
|
|
929
|
+
Handler: params.handler,
|
|
930
|
+
Runtime: params.runtime as any,
|
|
931
|
+
Code: {
|
|
932
|
+
...(params.zipFile
|
|
933
|
+
? {
|
|
934
|
+
ZipFile: params.zipFile,
|
|
935
|
+
}
|
|
936
|
+
: {
|
|
937
|
+
S3Bucket: params.s3Bucket,
|
|
938
|
+
S3Key: params.s3Key,
|
|
939
|
+
...(params.s3ObjectVersion
|
|
940
|
+
? {
|
|
941
|
+
S3ObjectVersion: params.s3ObjectVersion,
|
|
942
|
+
}
|
|
943
|
+
: {}),
|
|
944
|
+
}),
|
|
945
|
+
},
|
|
946
|
+
}),
|
|
947
|
+
);
|
|
948
|
+
|
|
949
|
+
return {
|
|
950
|
+
created: true,
|
|
951
|
+
functionName: created.FunctionName ?? params.functionName,
|
|
952
|
+
functionArn: created.FunctionArn ?? null,
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
async allowS3InvokeLambda(params: {
|
|
957
|
+
functionName: string;
|
|
958
|
+
region: string;
|
|
959
|
+
accessKeyId: string;
|
|
960
|
+
secretAccessKey: string;
|
|
961
|
+
sessionToken?: string;
|
|
962
|
+
bucketName: string;
|
|
963
|
+
sourceAccount?: string;
|
|
964
|
+
statementId?: string;
|
|
965
|
+
}) {
|
|
966
|
+
const lambda = new LambdaClient({
|
|
967
|
+
region: params.region,
|
|
968
|
+
credentials: {
|
|
969
|
+
accessKeyId: params.accessKeyId,
|
|
970
|
+
secretAccessKey: params.secretAccessKey,
|
|
971
|
+
...(params.sessionToken ? { sessionToken: params.sessionToken } : {}),
|
|
972
|
+
},
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
const statementId =
|
|
976
|
+
params.statementId ?? `lms-bulk-upload-${params.bucketName.replace(/[^a-zA-Z0-9-_]/g, '-')}`;
|
|
977
|
+
|
|
978
|
+
try {
|
|
979
|
+
await lambda.send(
|
|
980
|
+
new AddPermissionCommand({
|
|
981
|
+
FunctionName: params.functionName,
|
|
982
|
+
StatementId: statementId.slice(0, 100),
|
|
983
|
+
Action: 'lambda:InvokeFunction',
|
|
984
|
+
Principal: 's3.amazonaws.com',
|
|
985
|
+
SourceArn: `arn:aws:s3:::${params.bucketName}`,
|
|
986
|
+
...(params.sourceAccount
|
|
987
|
+
? {
|
|
988
|
+
SourceAccount: params.sourceAccount,
|
|
989
|
+
}
|
|
990
|
+
: {}),
|
|
991
|
+
}),
|
|
992
|
+
);
|
|
993
|
+
} catch (error: any) {
|
|
994
|
+
if (String(error?.name ?? '') === 'ResourceConflictException') {
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
throw error;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
async configureS3BucketLambdaNotification(params: {
|
|
1003
|
+
bucketName: string;
|
|
1004
|
+
lambdaArn: string;
|
|
1005
|
+
region: string;
|
|
1006
|
+
accessKeyId: string;
|
|
1007
|
+
secretAccessKey: string;
|
|
1008
|
+
sessionToken?: string;
|
|
1009
|
+
configurationId?: string;
|
|
1010
|
+
prefix?: string;
|
|
1011
|
+
}) {
|
|
1012
|
+
const s3 = new S3Client({
|
|
1013
|
+
region: params.region,
|
|
1014
|
+
credentials: {
|
|
1015
|
+
accessKeyId: params.accessKeyId,
|
|
1016
|
+
secretAccessKey: params.secretAccessKey,
|
|
1017
|
+
...(params.sessionToken ? { sessionToken: params.sessionToken } : {}),
|
|
1018
|
+
},
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
const configurationId =
|
|
1022
|
+
params.configurationId ?? 'lms-bulk-upload-object-created';
|
|
1023
|
+
|
|
1024
|
+
const existing = await s3.send(
|
|
1025
|
+
new GetBucketNotificationConfigurationCommand({
|
|
1026
|
+
Bucket: params.bucketName,
|
|
1027
|
+
}),
|
|
1028
|
+
);
|
|
1029
|
+
|
|
1030
|
+
const lambdaConfigurations = Array.isArray(existing?.LambdaFunctionConfigurations)
|
|
1031
|
+
? existing.LambdaFunctionConfigurations.filter((cfg: any) => cfg?.Id !== configurationId)
|
|
1032
|
+
: [];
|
|
1033
|
+
|
|
1034
|
+
lambdaConfigurations.push({
|
|
1035
|
+
Id: configurationId,
|
|
1036
|
+
LambdaFunctionArn: params.lambdaArn,
|
|
1037
|
+
Events: ['s3:ObjectCreated:*'],
|
|
1038
|
+
...(params.prefix
|
|
1039
|
+
? {
|
|
1040
|
+
Filter: {
|
|
1041
|
+
Key: {
|
|
1042
|
+
FilterRules: [
|
|
1043
|
+
{
|
|
1044
|
+
Name: 'prefix',
|
|
1045
|
+
Value: params.prefix,
|
|
1046
|
+
},
|
|
1047
|
+
],
|
|
1048
|
+
},
|
|
1049
|
+
},
|
|
1050
|
+
}
|
|
1051
|
+
: {}),
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
await s3.send(
|
|
1055
|
+
new PutBucketNotificationConfigurationCommand({
|
|
1056
|
+
Bucket: params.bucketName,
|
|
1057
|
+
NotificationConfiguration: {
|
|
1058
|
+
TopicConfigurations: Array.isArray(existing?.TopicConfigurations)
|
|
1059
|
+
? existing.TopicConfigurations
|
|
1060
|
+
: [],
|
|
1061
|
+
QueueConfigurations: Array.isArray(existing?.QueueConfigurations)
|
|
1062
|
+
? existing.QueueConfigurations
|
|
1063
|
+
: [],
|
|
1064
|
+
EventBridgeConfiguration: existing?.EventBridgeConfiguration,
|
|
1065
|
+
LambdaFunctionConfigurations: lambdaConfigurations,
|
|
1066
|
+
},
|
|
1067
|
+
}),
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
757
1071
|
createDownloadToken(filepath: string, expires = 3600) {
|
|
758
1072
|
if (!filepath) {
|
|
759
1073
|
throw new BadRequestException(`No file path provided`);
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common';
|
|
2
|
+
import { basename, extname } from 'path';
|
|
3
|
+
import sharp from 'sharp';
|
|
4
|
+
import { SettingService } from '../setting/setting.service';
|
|
5
|
+
|
|
6
|
+
export interface OptimizedImage {
|
|
7
|
+
buffer: Buffer;
|
|
8
|
+
mimetype: string;
|
|
9
|
+
originalname: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@Injectable()
|
|
13
|
+
export class ImageOptimizationService {
|
|
14
|
+
private readonly logger = new Logger(ImageOptimizationService.name);
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
@Inject(forwardRef(() => SettingService))
|
|
18
|
+
private readonly settingService: SettingService,
|
|
19
|
+
) {}
|
|
20
|
+
|
|
21
|
+
private parseArraySetting(raw: string | undefined): string[] {
|
|
22
|
+
if (!raw) return [];
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(raw);
|
|
25
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
26
|
+
} catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private replaceExtension(filename: string, newExt: string): string {
|
|
32
|
+
const base = basename(filename, extname(filename));
|
|
33
|
+
return `${base}.${newExt}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Applies image compression and/or WebP conversion to the given buffer
|
|
38
|
+
* based on the current settings. Returns the original data unchanged if
|
|
39
|
+
* the settings are disabled or the MIME type is not in the configured lists.
|
|
40
|
+
*/
|
|
41
|
+
async optimizeBuffer(
|
|
42
|
+
buffer: Buffer,
|
|
43
|
+
mimetype: string,
|
|
44
|
+
originalname: string,
|
|
45
|
+
): Promise<OptimizedImage> {
|
|
46
|
+
if (!mimetype.startsWith('image/')) {
|
|
47
|
+
return { buffer, mimetype, originalname };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const settings = await this.settingService.getSettingValues([
|
|
51
|
+
'image-compression-enabled',
|
|
52
|
+
'image-compression-quality',
|
|
53
|
+
'image-compression-types',
|
|
54
|
+
'image-webp-conversion-enabled',
|
|
55
|
+
'image-webp-convert-types',
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
const compressionEnabled = settings['image-compression-enabled'] === 'true';
|
|
59
|
+
const webpEnabled = settings['image-webp-conversion-enabled'] === 'true';
|
|
60
|
+
|
|
61
|
+
if (!compressionEnabled && !webpEnabled) {
|
|
62
|
+
return { buffer, mimetype, originalname };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const quality = Math.min(
|
|
66
|
+
100,
|
|
67
|
+
Math.max(1, Number(settings['image-compression-quality']) || 80),
|
|
68
|
+
);
|
|
69
|
+
const compressionTypes = this.parseArraySetting(settings['image-compression-types']);
|
|
70
|
+
const webpConvertTypes = this.parseArraySetting(settings['image-webp-convert-types']);
|
|
71
|
+
|
|
72
|
+
// WebP conversion has priority over plain compression
|
|
73
|
+
if (webpEnabled && webpConvertTypes.includes(mimetype)) {
|
|
74
|
+
try {
|
|
75
|
+
const optimized = await sharp(buffer).webp({ quality }).toBuffer();
|
|
76
|
+
return {
|
|
77
|
+
buffer: optimized,
|
|
78
|
+
mimetype: 'image/webp',
|
|
79
|
+
originalname: this.replaceExtension(originalname, 'webp'),
|
|
80
|
+
};
|
|
81
|
+
} catch (err) {
|
|
82
|
+
this.logger.warn(
|
|
83
|
+
`WebP conversion failed for "${originalname}" (${mimetype}): ${err instanceof Error ? err.message : String(err)}. Falling back to original.`,
|
|
84
|
+
);
|
|
85
|
+
return { buffer, mimetype, originalname };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (compressionEnabled && compressionTypes.includes(mimetype)) {
|
|
90
|
+
try {
|
|
91
|
+
let instance = sharp(buffer);
|
|
92
|
+
if (mimetype === 'image/jpeg') {
|
|
93
|
+
instance = instance.jpeg({ quality });
|
|
94
|
+
} else if (mimetype === 'image/png') {
|
|
95
|
+
// sharp compressionLevel is 0-9 (9 = max); invert from quality scale
|
|
96
|
+
const compressionLevel = Math.round(((100 - quality) / 100) * 9);
|
|
97
|
+
instance = instance.png({ compressionLevel });
|
|
98
|
+
} else if (mimetype === 'image/webp') {
|
|
99
|
+
instance = instance.webp({ quality });
|
|
100
|
+
} else if (mimetype === 'image/bmp' || mimetype === 'image/tiff') {
|
|
101
|
+
// Convert to PNG as a compressed lossless alternative
|
|
102
|
+
instance = instance.png({ compressionLevel: Math.round(((100 - quality) / 100) * 9) });
|
|
103
|
+
return {
|
|
104
|
+
buffer: await instance.toBuffer(),
|
|
105
|
+
mimetype: 'image/png',
|
|
106
|
+
originalname: this.replaceExtension(originalname, 'png'),
|
|
107
|
+
};
|
|
108
|
+
} else {
|
|
109
|
+
return { buffer, mimetype, originalname };
|
|
110
|
+
}
|
|
111
|
+
const optimized = await instance.toBuffer();
|
|
112
|
+
return { buffer: optimized, mimetype, originalname };
|
|
113
|
+
} catch (err) {
|
|
114
|
+
this.logger.warn(
|
|
115
|
+
`Image compression failed for "${originalname}" (${mimetype}): ${err instanceof Error ? err.message : String(err)}. Falling back to original.`,
|
|
116
|
+
);
|
|
117
|
+
return { buffer, mimetype, originalname };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { buffer, mimetype, originalname };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -27,15 +27,8 @@ export class LocalProvider extends AbstractProvider {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
async createFolderRecursive(
|
|
31
|
-
|
|
32
|
-
let currentPath = '';
|
|
33
|
-
for (const folder of folders) {
|
|
34
|
-
currentPath = join(currentPath, folder);
|
|
35
|
-
if (!existsSync(currentPath)) {
|
|
36
|
-
await mkdir(currentPath, { recursive: true });
|
|
37
|
-
}
|
|
38
|
-
}
|
|
30
|
+
async createFolderRecursive(folderPath: string): Promise<void> {
|
|
31
|
+
await mkdir(folderPath, { recursive: true });
|
|
39
32
|
}
|
|
40
33
|
|
|
41
34
|
private resolveFilepath(filepath: string): string {
|
package/src/index.ts
CHANGED
|
@@ -39,6 +39,7 @@ export * from './ai/ai.service';
|
|
|
39
39
|
|
|
40
40
|
export * from './webhook-integration/event-webhook.service';
|
|
41
41
|
export * from './webhook-integration/integration-event-catalog.service';
|
|
42
|
+
export * from './webhook-integration/webhook-command.registry';
|
|
42
43
|
export * from './webhook-integration/webhook-integration.module';
|
|
43
44
|
export * from './webhook-integration/webhook-integration.service';
|
|
44
45
|
|