@hed-hog/core 0.0.360 → 0.0.362
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/ai/ai.service.d.ts +1 -1
- package/dist/auth/auth.controller.d.ts +27 -26
- 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 +35 -34
- 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/challenge/challenge.service.d.ts +12 -12
- package/dist/core/system.controller.d.ts +2 -2
- package/dist/core/system.service.d.ts +2 -2
- package/dist/dashboard/dashboard/dashboard.controller.d.ts +66 -66
- package/dist/dashboard/dashboard/dashboard.service.d.ts +66 -66
- package/dist/dashboard/dashboard-component/dashboard-component.controller.d.ts +26 -26
- package/dist/dashboard/dashboard-component/dashboard-component.service.d.ts +26 -26
- package/dist/dashboard/dashboard-component-role/dashboard-component-role.controller.d.ts +34 -34
- package/dist/dashboard/dashboard-component-role/dashboard-component-role.service.d.ts +34 -34
- package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +8 -8
- package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +8 -8
- package/dist/dashboard/dashboard-item/dashboard-item.controller.d.ts +5 -5
- package/dist/dashboard/dashboard-item/dashboard-item.service.d.ts +5 -5
- package/dist/dashboard/dashboard-role/dashboard-role.controller.d.ts +25 -25
- package/dist/dashboard/dashboard-role/dashboard-role.service.d.ts +25 -25
- package/dist/file/file.controller.d.ts +6 -6
- 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 +121 -25
- package/dist/file/file.service.d.ts.map +1 -1
- package/dist/file/file.service.js +217 -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/integration-profile/integration-profile.controller.d.ts +15 -15
- package/dist/integration-profile/integration-profile.service.d.ts +15 -15
- package/dist/locale/locale.controller.d.ts +4 -4
- package/dist/locale/locale.service.d.ts +4 -4
- package/dist/mail/mail.controller.d.ts +7 -7
- package/dist/mail/mail.service.d.ts +7 -7
- package/dist/mail-sent/mail-sent.controller.d.ts +12 -12
- package/dist/mail-sent/mail-sent.service.d.ts +12 -12
- package/dist/mcp/mcp-api-key.controller.d.ts +2 -2
- package/dist/mcp/mcp-api-key.service.d.ts +2 -2
- 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/mcp-chat/mcp-chat.controller.d.ts +8 -8
- package/dist/mcp-chat/mcp-chat.service.d.ts +8 -8
- package/dist/menu/menu.controller.d.ts +10 -10
- package/dist/menu/menu.service.d.ts +10 -10
- package/dist/notification/notification.controller.d.ts +36 -36
- package/dist/notification/notification.service.d.ts +36 -36
- package/dist/profile/profile.controller.d.ts +16 -16
- package/dist/profile/profile.service.d.ts +16 -16
- 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/role/role.controller.d.ts +6 -6
- package/dist/role/role.service.d.ts +6 -6
- package/dist/screen/screen.controller.d.ts +3 -3
- package/dist/screen/screen.service.d.ts +3 -3
- package/dist/session/session.controller.d.ts +2 -2
- package/dist/session/session.service.d.ts +6 -6
- 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/setting/setting.controller.d.ts +18 -18
- package/dist/setting/setting.service.d.ts +18 -18
- package/dist/setting/setting.service.d.ts.map +1 -1
- package/dist/setting/setting.service.js +0 -1
- package/dist/setting/setting.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 +30 -30
- 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 +68 -68
- 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/dist/webhook-integration/webhook-integration.controller.d.ts +2 -2
- package/dist/webhook-integration/webhook-integration.service.d.ts +2 -2
- package/hedhog/data/route.yaml +2 -0
- package/hedhog/data/setting_group.yaml +117 -26
- 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 +113 -38
- 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/hedhog/table/setting_runtime_state.yaml +9 -0
- package/package.json +12 -6
- 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 +448 -46
- 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/setting/setting.service.ts +0 -1
- 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
package/src/auth/auth.service.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
forwardRef,
|
|
7
7
|
Inject,
|
|
8
8
|
Injectable,
|
|
9
|
+
Logger,
|
|
9
10
|
NotFoundException,
|
|
10
11
|
UnauthorizedException,
|
|
11
12
|
} from '@nestjs/common';
|
|
@@ -21,6 +22,7 @@ import { LoginDTO } from './dto/login.dto';
|
|
|
21
22
|
|
|
22
23
|
@Injectable()
|
|
23
24
|
export class AuthService {
|
|
25
|
+
private readonly logger = new Logger(AuthService.name);
|
|
24
26
|
|
|
25
27
|
constructor(
|
|
26
28
|
@Inject(forwardRef(() => PrismaService))
|
|
@@ -46,7 +48,8 @@ export class AuthService {
|
|
|
46
48
|
const payload = { sub: userId, sessionId: session.id };
|
|
47
49
|
const accessToken = await this.token.createAccessToken(payload);
|
|
48
50
|
await this.prisma.user.update({ where: { id: userId }, data: { last_login_at: new Date() } });
|
|
49
|
-
|
|
51
|
+
this.logger.debug(`[AuthService] Session created userId=${userId} sessionId=${session.id}`);
|
|
52
|
+
|
|
50
53
|
return {
|
|
51
54
|
accessToken,
|
|
52
55
|
refreshToken,
|
|
@@ -474,9 +477,12 @@ export class AuthService {
|
|
|
474
477
|
async refreshAccessToken(locale: string, refreshToken: string, ipAddress: string, userAgent: string) {
|
|
475
478
|
|
|
476
479
|
if (!refreshToken) {
|
|
480
|
+
this.logger.warn(`[AuthService] refreshAccessToken called without token ip=${ipAddress}`);
|
|
477
481
|
throw new UnauthorizedException(getLocaleText('accessDenied', locale, 'Access denied.'));
|
|
478
482
|
}
|
|
479
483
|
|
|
484
|
+
this.logger.debug(`[AuthService] refreshAccessToken ip=${ipAddress} ua=${userAgent?.slice(0, 40)}`);
|
|
485
|
+
|
|
480
486
|
const { session, token } = await this.session.refresh(
|
|
481
487
|
locale,
|
|
482
488
|
refreshToken,
|
|
@@ -488,6 +494,8 @@ export class AuthService {
|
|
|
488
494
|
const payload = { sub: user.id, sessionId: session.id };
|
|
489
495
|
const accessToken = await this.token.createAccessToken(payload);
|
|
490
496
|
|
|
497
|
+
this.logger.debug(`[AuthService] refreshAccessToken success userId=${user.id} newSessionId=${session.id}`);
|
|
498
|
+
|
|
491
499
|
return { accessToken, refreshToken: token, session };
|
|
492
500
|
|
|
493
501
|
}
|
|
@@ -780,7 +788,8 @@ export class AuthService {
|
|
|
780
788
|
throw new BadRequestException(getLocaleText('webauthnNotConfigured', locale, 'WebAuthn not configured.'));
|
|
781
789
|
}
|
|
782
790
|
|
|
783
|
-
const
|
|
791
|
+
const primaryOrigin = (process.env.CORS_ALLOWED_ORIGINS ?? '').split(/[\n,;]+/).map(u => u.trim()).filter(Boolean)[0] ?? 'http://localhost:3200';
|
|
792
|
+
const rpID = new URL(primaryOrigin).hostname;
|
|
784
793
|
|
|
785
794
|
const allowCredentials = user.user_mfa
|
|
786
795
|
.filter(mfa => mfa.user_mfa_webauthn && mfa.user_mfa_webauthn.length > 0)
|
|
@@ -834,8 +843,9 @@ export class AuthService {
|
|
|
834
843
|
}
|
|
835
844
|
|
|
836
845
|
const authenticator = challengeRecord.user_mfa.user_mfa_webauthn[0];
|
|
837
|
-
const
|
|
838
|
-
const
|
|
846
|
+
const primaryOrigin = (process.env.CORS_ALLOWED_ORIGINS ?? '').split(/[\n,;]+/).map(u => u.trim()).filter(Boolean)[0] ?? 'http://localhost:3200';
|
|
847
|
+
const rpID = new URL(primaryOrigin).hostname;
|
|
848
|
+
const expectedOrigin = primaryOrigin;
|
|
839
849
|
|
|
840
850
|
const verification = await verifyAuthenticationResponse({
|
|
841
851
|
response: assertionResponse,
|
|
@@ -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,26 +1,46 @@
|
|
|
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';
|
|
15
|
+
import {
|
|
16
|
+
AssumeRoleCommand,
|
|
17
|
+
GetSessionTokenCommand,
|
|
18
|
+
STSClient,
|
|
19
|
+
} from '@aws-sdk/client-sts';
|
|
1
20
|
import { getLocaleText } from '@hed-hog/api-locale';
|
|
2
21
|
import { PaginationDTO, PaginationService } from '@hed-hog/api-pagination';
|
|
3
22
|
import { PrismaService } from '@hed-hog/api-prisma';
|
|
4
23
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
24
|
+
BadRequestException,
|
|
25
|
+
forwardRef,
|
|
26
|
+
Inject,
|
|
27
|
+
Injectable,
|
|
28
|
+
Logger,
|
|
29
|
+
NotFoundException,
|
|
30
|
+
OnModuleInit
|
|
12
31
|
} from '@nestjs/common';
|
|
13
32
|
import { JwtService } from '@nestjs/jwt';
|
|
14
33
|
import { createWriteStream, promises as fs } from 'fs';
|
|
15
34
|
import { basename, dirname } from 'path';
|
|
16
35
|
import { pipeline } from 'stream/promises';
|
|
17
36
|
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
37
|
+
buildStorageConfigFromIntegration,
|
|
38
|
+
resolveFileProviderSlug,
|
|
39
|
+
resolveStorageEnumProvider,
|
|
21
40
|
} from '../integration-profile/integration-profile.utils';
|
|
22
41
|
import { SettingService } from '../setting/setting.service';
|
|
23
42
|
import { DeleteDTO } from './dto/delete.dto';
|
|
43
|
+
import { ImageOptimizationService } from './image-optimization.service';
|
|
24
44
|
import { AbstractProvider } from './provider/abstract.provider';
|
|
25
45
|
import { ProviderFactory } from './provider/provider.factory';
|
|
26
46
|
|
|
@@ -65,6 +85,8 @@ export class FileService implements OnModuleInit {
|
|
|
65
85
|
private readonly settingService: SettingService,
|
|
66
86
|
@Inject(forwardRef(() => JwtService))
|
|
67
87
|
private readonly jwtService: JwtService,
|
|
88
|
+
@Inject(forwardRef(() => ImageOptimizationService))
|
|
89
|
+
private readonly imageOptimizationService: ImageOptimizationService,
|
|
68
90
|
) { }
|
|
69
91
|
|
|
70
92
|
async onModuleInit() {
|
|
@@ -391,21 +413,30 @@ export class FileService implements OnModuleInit {
|
|
|
391
413
|
fileBuffer.originalname,
|
|
392
414
|
);
|
|
393
415
|
|
|
416
|
+
const optimized = await this.imageOptimizationService.optimizeBuffer(
|
|
417
|
+
fileBuffer.buffer,
|
|
418
|
+
fileBuffer.mimetype,
|
|
419
|
+
normalizedFilename,
|
|
420
|
+
);
|
|
421
|
+
|
|
394
422
|
const normalizedFile = {
|
|
395
423
|
...fileBuffer,
|
|
396
|
-
|
|
424
|
+
buffer: optimized.buffer,
|
|
425
|
+
mimetype: optimized.mimetype,
|
|
426
|
+
originalname: optimized.originalname,
|
|
427
|
+
size: optimized.buffer.length,
|
|
397
428
|
};
|
|
398
429
|
|
|
399
430
|
const url = await provider.upload(destination, normalizedFile);
|
|
400
431
|
|
|
401
432
|
const file = await this.prismaService.file.create({
|
|
402
433
|
data: {
|
|
403
|
-
filename:
|
|
434
|
+
filename: optimized.originalname,
|
|
404
435
|
path: url,
|
|
405
436
|
provider_id: this.providerId,
|
|
406
437
|
location: destination,
|
|
407
|
-
mimetype_id: await this.getMimeType(
|
|
408
|
-
size:
|
|
438
|
+
mimetype_id: await this.getMimeType(optimized.mimetype),
|
|
439
|
+
size: optimized.buffer.length,
|
|
409
440
|
},
|
|
410
441
|
});
|
|
411
442
|
|
|
@@ -450,45 +481,35 @@ export class FileService implements OnModuleInit {
|
|
|
450
481
|
throw new BadRequestException(`File too large: ${size} bytes`);
|
|
451
482
|
}
|
|
452
483
|
|
|
453
|
-
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
};
|
|
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
|
+
);
|
|
461
491
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
encoding: metadata?.encoding ?? '7bit',
|
|
475
|
-
mimetype,
|
|
476
|
-
size,
|
|
477
|
-
destination: '',
|
|
478
|
-
filename: originalname,
|
|
479
|
-
path: filePath,
|
|
480
|
-
buffer,
|
|
481
|
-
});
|
|
482
|
-
}
|
|
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
|
+
});
|
|
483
504
|
|
|
484
505
|
const file = await this.prismaService.file.create({
|
|
485
506
|
data: {
|
|
486
|
-
filename: originalname,
|
|
507
|
+
filename: optimized.originalname,
|
|
487
508
|
path: url,
|
|
488
509
|
provider_id: this.providerId,
|
|
489
510
|
location: destination,
|
|
490
|
-
mimetype_id: await this.getMimeType(mimetype),
|
|
491
|
-
size,
|
|
511
|
+
mimetype_id: await this.getMimeType(optimized.mimetype),
|
|
512
|
+
size: optimized.buffer.length,
|
|
492
513
|
},
|
|
493
514
|
});
|
|
494
515
|
|
|
@@ -666,6 +687,387 @@ export class FileService implements OnModuleInit {
|
|
|
666
687
|
return url;
|
|
667
688
|
}
|
|
668
689
|
|
|
690
|
+
async getTemporaryCredentials(params: {
|
|
691
|
+
accessKeyId: string;
|
|
692
|
+
secretAccessKey: string;
|
|
693
|
+
sessionToken?: string;
|
|
694
|
+
region: string;
|
|
695
|
+
roleArn?: string;
|
|
696
|
+
externalId?: string;
|
|
697
|
+
durationSeconds: number;
|
|
698
|
+
sessionName: string;
|
|
699
|
+
}): Promise<{
|
|
700
|
+
AccessKeyId: string;
|
|
701
|
+
SecretAccessKey: string;
|
|
702
|
+
SessionToken: string;
|
|
703
|
+
Expiration?: Date;
|
|
704
|
+
}> {
|
|
705
|
+
const stsClient = new STSClient({
|
|
706
|
+
region: params.region,
|
|
707
|
+
credentials: {
|
|
708
|
+
accessKeyId: params.accessKeyId,
|
|
709
|
+
secretAccessKey: params.secretAccessKey,
|
|
710
|
+
...(params.sessionToken ? { sessionToken: params.sessionToken } : {}),
|
|
711
|
+
},
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
let creds: { AccessKeyId?: string; SecretAccessKey?: string; SessionToken?: string; Expiration?: Date } | undefined;
|
|
715
|
+
|
|
716
|
+
if (params.roleArn) {
|
|
717
|
+
const result = await stsClient.send(
|
|
718
|
+
new AssumeRoleCommand({
|
|
719
|
+
RoleArn: params.roleArn,
|
|
720
|
+
RoleSessionName: params.sessionName,
|
|
721
|
+
DurationSeconds: params.durationSeconds,
|
|
722
|
+
...(params.externalId ? { ExternalId: params.externalId } : {}),
|
|
723
|
+
}),
|
|
724
|
+
);
|
|
725
|
+
creds = result.Credentials;
|
|
726
|
+
} else {
|
|
727
|
+
const result = await stsClient.send(
|
|
728
|
+
new GetSessionTokenCommand({ DurationSeconds: params.durationSeconds }),
|
|
729
|
+
);
|
|
730
|
+
creds = result.Credentials;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) {
|
|
734
|
+
throw new BadRequestException('Could not generate temporary AWS credentials.');
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
return {
|
|
738
|
+
AccessKeyId: creds.AccessKeyId,
|
|
739
|
+
SecretAccessKey: creds.SecretAccessKey,
|
|
740
|
+
SessionToken: creds.SessionToken,
|
|
741
|
+
Expiration: creds.Expiration,
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
async headS3Object(params: {
|
|
746
|
+
accessKeyId: string;
|
|
747
|
+
secretAccessKey: string;
|
|
748
|
+
sessionToken?: string;
|
|
749
|
+
region: string;
|
|
750
|
+
bucket: string;
|
|
751
|
+
key: string;
|
|
752
|
+
}): Promise<{ ContentLength?: number; ETag?: string }> {
|
|
753
|
+
const s3 = new S3Client({
|
|
754
|
+
region: params.region,
|
|
755
|
+
credentials: {
|
|
756
|
+
accessKeyId: params.accessKeyId,
|
|
757
|
+
secretAccessKey: params.secretAccessKey,
|
|
758
|
+
...(params.sessionToken ? { sessionToken: params.sessionToken } : {}),
|
|
759
|
+
},
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
const result = await s3.send(
|
|
763
|
+
new HeadObjectCommand({ Bucket: params.bucket, Key: params.key }),
|
|
764
|
+
);
|
|
765
|
+
|
|
766
|
+
return {
|
|
767
|
+
ContentLength: result.ContentLength,
|
|
768
|
+
ETag: result.ETag,
|
|
769
|
+
};
|
|
770
|
+
}
|
|
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
|
+
|
|
669
1071
|
createDownloadToken(filepath: string, expires = 3600) {
|
|
670
1072
|
if (!filepath) {
|
|
671
1073
|
throw new BadRequestException(`No file path provided`);
|