@solidxai/core 0.1.12-beta.2 → 0.1.12-beta.3
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/.claude/settings.local.json +16 -0
- package/CLAUDE.md +26 -0
- package/dist/constants/media-file-types.d.ts +5 -0
- package/dist/constants/media-file-types.d.ts.map +1 -0
- package/dist/constants/media-file-types.js +27 -0
- package/dist/constants/media-file-types.js.map +1 -0
- package/dist/controllers/media.controller.d.ts +14 -11
- package/dist/controllers/media.controller.d.ts.map +1 -1
- package/dist/controllers/media.controller.js +72 -33
- package/dist/controllers/media.controller.js.map +1 -1
- package/dist/dtos/create-media.dto.d.ts +1 -0
- package/dist/dtos/create-media.dto.d.ts.map +1 -1
- package/dist/dtos/create-media.dto.js +7 -1
- package/dist/dtos/create-media.dto.js.map +1 -1
- package/dist/dtos/update-media.dto.d.ts +1 -0
- package/dist/dtos/update-media.dto.d.ts.map +1 -1
- package/dist/dtos/update-media.dto.js +7 -1
- package/dist/dtos/update-media.dto.js.map +1 -1
- package/dist/entities/media.entity.d.ts +1 -0
- package/dist/entities/media.entity.d.ts.map +1 -1
- package/dist/entities/media.entity.js +9 -1
- package/dist/entities/media.entity.js.map +1 -1
- package/dist/enums/auth-type.enum.d.ts +2 -1
- package/dist/enums/auth-type.enum.d.ts.map +1 -1
- package/dist/enums/auth-type.enum.js +1 -0
- package/dist/enums/auth-type.enum.js.map +1 -1
- package/dist/guards/authentication.guard.d.ts +3 -1
- package/dist/guards/authentication.guard.d.ts.map +1 -1
- package/dist/guards/authentication.guard.js +5 -1
- package/dist/guards/authentication.guard.js.map +1 -1
- package/dist/guards/media-signed-url.guard.d.ts +9 -0
- package/dist/guards/media-signed-url.guard.d.ts.map +1 -0
- package/dist/guards/media-signed-url.guard.js +49 -0
- package/dist/guards/media-signed-url.guard.js.map +1 -0
- package/dist/helpers/field-crud-managers/MediaFieldCrudManager.d.ts.map +1 -1
- package/dist/helpers/field-crud-managers/MediaFieldCrudManager.js +10 -13
- package/dist/helpers/field-crud-managers/MediaFieldCrudManager.js.map +1 -1
- package/dist/repository/media.repository.d.ts.map +1 -1
- package/dist/repository/media.repository.js +2 -0
- package/dist/repository/media.repository.js.map +1 -1
- package/dist/seeders/seed-data/solid-core-metadata.json +88 -3
- package/dist/services/media-download-url.service.d.ts +21 -0
- package/dist/services/media-download-url.service.d.ts.map +1 -0
- package/dist/services/media-download-url.service.js +67 -0
- package/dist/services/media-download-url.service.js.map +1 -0
- package/dist/services/media-storage.utils.d.ts +27 -0
- package/dist/services/media-storage.utils.d.ts.map +1 -0
- package/dist/services/media-storage.utils.js +82 -0
- package/dist/services/media-storage.utils.js.map +1 -0
- package/dist/services/media.service.d.ts +20 -4
- package/dist/services/media.service.d.ts.map +1 -1
- package/dist/services/media.service.js +157 -55
- package/dist/services/media.service.js.map +1 -1
- package/dist/services/mediaStorageProviders/file-s3-storage-provider.d.ts +4 -4
- package/dist/services/mediaStorageProviders/file-s3-storage-provider.d.ts.map +1 -1
- package/dist/services/mediaStorageProviders/file-s3-storage-provider.js +37 -63
- package/dist/services/mediaStorageProviders/file-s3-storage-provider.js.map +1 -1
- package/dist/services/mediaStorageProviders/file-storage-provider.d.ts +5 -5
- package/dist/services/mediaStorageProviders/file-storage-provider.d.ts.map +1 -1
- package/dist/services/mediaStorageProviders/file-storage-provider.js +37 -38
- package/dist/services/mediaStorageProviders/file-storage-provider.js.map +1 -1
- package/dist/solid-core.module.d.ts.map +1 -1
- package/dist/solid-core.module.js +11 -1
- package/dist/solid-core.module.js.map +1 -1
- package/dist/subscribers/audit.subscriber.js +1 -1
- package/dist/subscribers/audit.subscriber.js.map +1 -1
- package/package.json +1 -1
- package/src/constants/media-file-types.ts +44 -0
- package/src/controllers/media.controller.ts +44 -24
- package/src/dtos/create-media.dto.ts +8 -3
- package/src/dtos/update-media.dto.ts +8 -3
- package/src/entities/media.entity.ts +4 -1
- package/src/enums/auth-type.enum.ts +1 -0
- package/src/guards/authentication.guard.ts +3 -0
- package/src/guards/media-signed-url.guard.ts +41 -0
- package/src/helpers/field-crud-managers/MediaFieldCrudManager.ts +12 -24
- package/src/repository/media.repository.ts +2 -0
- package/src/seeders/seed-data/solid-core-metadata.json +88 -3
- package/src/services/1.js +6 -0
- package/src/services/media-download-url.service.ts +67 -0
- package/src/services/media-storage.utils.ts +79 -0
- package/src/services/media.service.ts +189 -59
- package/src/services/mediaStorageProviders/file-s3-storage-provider.ts +52 -76
- package/src/services/mediaStorageProviders/file-storage-provider.ts +44 -48
- package/src/solid-core.module.ts +21 -2
- package/src/subscribers/audit.subscriber.ts +1 -1
- package/dist-tests/api/authenticate.spec.js +0 -119
- package/dist-tests/api/authenticate.spec.js.map +0 -1
- package/dist-tests/api/crud-service.findOne.cityMaster.spec.js +0 -97
- package/dist-tests/api/crud-service.findOne.cityMaster.spec.js.map +0 -1
- package/dist-tests/api/ping.spec.js +0 -21
- package/dist-tests/api/ping.spec.js.map +0 -1
- package/dist-tests/helpers/auth.js +0 -41
- package/dist-tests/helpers/auth.js.map +0 -1
- package/dist-tests/helpers/env.js +0 -11
- package/dist-tests/helpers/env.js.map +0 -1
- package/docs/agent-builder/notebook-lm-prompt-for-agent-builder.md +0 -136
- package/docs/agent-builder/team-ready-note-agent-builder.md +0 -305
- package/docs/agent-hub-grooming.md +0 -301
- package/docs/dashboards/AGENTIC_DASHBOARD_IMPLEMENTATION_PLAN.md +0 -438
- package/docs/dashboards/dashboard-curl-smoke-tests.txt +0 -146
- package/docs/dashboards/delete-legacy-dashboard-metadata.sql +0 -172
- package/docs/datasource-introspection-ddl-analysis.md +0 -326
- package/docs/datasource-introspection-implementation-plan.md +0 -306
- package/docs/grouping-enhancements.md +0 -89
- package/docs/java-spring/README.md +0 -3
- package/docs/java-spring/solid-core-module-deep-dive-report.md +0 -1317
- package/docs/module-package-import-handoff.md +0 -691
- package/docs/seed-changes.md +0 -65
- package/docs/test-data-workflow.md +0 -200
- package/docs/type-declaration-import-issue.md +0 -24
- package/docs/workflow/business-automation-example-notes.md +0 -309
- package/docs/workflow/data-engineering-pipeline-example-notes.md +0 -330
- package/docs/workflow/foreach-example-notes.md +0 -187
- package/docs/workflow/hello-world-example-notes.md +0 -271
- package/docs/workflow/kestra-concepts-plugins-blueprints.md +0 -315
- package/docs/workflow/loop-until-example-notes.md +0 -198
- package/docs/workflow/microservices-and-apis-example-notes.md +0 -264
- package/docs/workflow/samples.md +0 -25
- package/docs/workflow/what-is-kestra.md +0 -79
- package/docs/workflow/workflow-core-module-handoff-summary.md +0 -191
- package/docs/workflow/workflow-module-brd.md +0 -185
- package/docs/workflow/workflow-module-domain-model.md +0 -252
- package/docs/workflow/workflow-module-metadata-dsl.md +0 -302
- package/docs/workflow/workflow-module-technical-spec.md +0 -293
- package/docs/workflow/workflow-node-type-contracts.md +0 -635
- package/docs/workflow/workflow-node-ui-metadata-contract.md +0 -1262
- package/docs/workflow/workflow-solidx-model-field-spec.md +0 -284
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { IsInt,IsOptional, IsNotEmpty, IsString } from 'class-validator';
|
|
2
|
-
import { ApiProperty } from '@nestjs/swagger';
|
|
1
|
+
import { IsBoolean, IsInt,IsOptional, IsNotEmpty, IsString } from 'class-validator';
|
|
2
|
+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
3
3
|
export class UpdateMediaDto {
|
|
4
4
|
@IsOptional()
|
|
5
5
|
@IsInt()
|
|
@@ -11,6 +11,11 @@ export class UpdateMediaDto {
|
|
|
11
11
|
@ApiProperty()
|
|
12
12
|
entityId: number;
|
|
13
13
|
|
|
14
|
+
@IsOptional()
|
|
15
|
+
@IsBoolean()
|
|
16
|
+
@ApiPropertyOptional({ default: true })
|
|
17
|
+
isPublic?: boolean;
|
|
18
|
+
|
|
14
19
|
@IsOptional()
|
|
15
20
|
@IsString()
|
|
16
21
|
@ApiProperty()
|
|
@@ -60,4 +65,4 @@ fieldMetadataId: number;
|
|
|
60
65
|
@IsOptional()
|
|
61
66
|
@ApiProperty()
|
|
62
67
|
fieldMetadataUserKey: string;
|
|
63
|
-
}
|
|
68
|
+
}
|
|
@@ -9,6 +9,9 @@ export class Media extends CommonEntity {
|
|
|
9
9
|
@Column({ type: "integer" })
|
|
10
10
|
entityId: number;
|
|
11
11
|
|
|
12
|
+
@Column({ name: "is_public", default: true })
|
|
13
|
+
isPublic: boolean = true;
|
|
14
|
+
|
|
12
15
|
@Column({ type: "varchar", nullable: true })
|
|
13
16
|
relativeUri: string;
|
|
14
17
|
|
|
@@ -36,4 +39,4 @@ export class Media extends CommonEntity {
|
|
|
36
39
|
@ManyToOne(() => FieldMetadata, { nullable: false })
|
|
37
40
|
@JoinColumn()
|
|
38
41
|
fieldMetadata: FieldMetadata;
|
|
39
|
-
}
|
|
42
|
+
}
|
|
@@ -9,6 +9,7 @@ import { AUTH_TYPE_KEY } from '../decorators/auth.decorator';
|
|
|
9
9
|
import { AuthType } from '../enums/auth-type.enum';
|
|
10
10
|
import { AccessTokenGuard } from './access-token.guard';
|
|
11
11
|
import { ApiKeyGuard } from './api-key.guard';
|
|
12
|
+
import { MediaSignedUrlGuard } from './media-signed-url.guard';
|
|
12
13
|
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
|
|
13
14
|
import { PermissionMetadataService } from '../services/permission-metadata.service';
|
|
14
15
|
import { ClsService } from 'nestjs-cls';
|
|
@@ -22,12 +23,14 @@ export class AuthenticationGuard implements CanActivate {
|
|
|
22
23
|
[AuthType.Bearer]: this.accessTokenGuard,
|
|
23
24
|
[AuthType.ApiKey]: this.apiKeyGuard,
|
|
24
25
|
[AuthType.None]: { canActivate: () => true },
|
|
26
|
+
[AuthType.MediaSignedUrl]: this.mediaSignedUrlGuard,
|
|
25
27
|
};
|
|
26
28
|
|
|
27
29
|
constructor(
|
|
28
30
|
private readonly reflector: Reflector,
|
|
29
31
|
private readonly accessTokenGuard: AccessTokenGuard,
|
|
30
32
|
private readonly apiKeyGuard: ApiKeyGuard,
|
|
33
|
+
private readonly mediaSignedUrlGuard: MediaSignedUrlGuard,
|
|
31
34
|
private readonly permissionService: PermissionMetadataService,
|
|
32
35
|
private readonly cls: ClsService,
|
|
33
36
|
) { }
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
|
|
2
|
+
import { Request } from 'express';
|
|
3
|
+
import { MediaDownloadUrlService } from 'src/services/media-download-url.service';
|
|
4
|
+
|
|
5
|
+
@Injectable()
|
|
6
|
+
export class MediaSignedUrlGuard implements CanActivate {
|
|
7
|
+
constructor(private readonly mediaDownloadUrlService: MediaDownloadUrlService) { }
|
|
8
|
+
|
|
9
|
+
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
10
|
+
const request = context.switchToHttp().getRequest<Request>();
|
|
11
|
+
const token = this.extractTokenFromQuery(request);
|
|
12
|
+
if (!token) {
|
|
13
|
+
throw new UnauthorizedException();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let payload: { mediaId: number };
|
|
17
|
+
try {
|
|
18
|
+
payload = await this.mediaDownloadUrlService.verifyDownloadToken(token);
|
|
19
|
+
} catch {
|
|
20
|
+
throw new UnauthorizedException();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const routeMediaId = Number(request.params?.id);
|
|
24
|
+
if (!Number.isFinite(routeMediaId) || payload.mediaId !== routeMediaId) {
|
|
25
|
+
throw new UnauthorizedException();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// No REQUEST_USER_KEY is ever set for token auth (only Bearer/ApiKey guards set it), so
|
|
29
|
+
// the sibling global APP_GUARD PermissionsGuard would otherwise reject this request for
|
|
30
|
+
// lack of a per-user permission grant. Flag it the same way AuthenticationGuard flags
|
|
31
|
+
// Public-role-exempt requests - token possession IS the full authorization decision here.
|
|
32
|
+
request['isListedInPublicRole'] = true;
|
|
33
|
+
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private extractTokenFromQuery(request: Request): string | undefined {
|
|
38
|
+
const token = request.query?.token;
|
|
39
|
+
return typeof token === 'string' ? token : undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FieldCrudManager, ValidationError } from "src/interfaces";
|
|
2
|
+
import { DANGEROUS_EXTENSIONS, EXT_TO_MEDIA_TYPE, MediaCategory } from "src/constants/media-file-types";
|
|
2
3
|
|
|
3
4
|
export enum SolidMediaType {
|
|
4
5
|
mediaSingle = 'mediaSingle',
|
|
@@ -14,7 +15,7 @@ export interface MediaFieldOptions {
|
|
|
14
15
|
isUpdate: boolean | undefined | null;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
type MediaType =
|
|
18
|
+
type MediaType = MediaCategory;
|
|
18
19
|
|
|
19
20
|
const MIME_TO_MEDIA_TYPE: Record<string, MediaType> = {
|
|
20
21
|
// Images
|
|
@@ -25,7 +26,6 @@ const MIME_TO_MEDIA_TYPE: Record<string, MediaType> = {
|
|
|
25
26
|
'image/gif': 'image',
|
|
26
27
|
'image/bmp': 'image',
|
|
27
28
|
'image/tiff': 'image',
|
|
28
|
-
'image/svg+xml': 'image',
|
|
29
29
|
'image/heic': 'image',
|
|
30
30
|
'image/heif': 'image',
|
|
31
31
|
|
|
@@ -72,35 +72,23 @@ const MIME_TO_MEDIA_TYPE: Record<string, MediaType> = {
|
|
|
72
72
|
'application/x-zip-compressed': 'file',
|
|
73
73
|
'application/x-rar-compressed': 'file',
|
|
74
74
|
'application/x-7z-compressed': 'file',
|
|
75
|
-
|
|
76
|
-
// Common binary fallback category
|
|
77
|
-
'application/octet-stream': 'file',
|
|
78
75
|
};
|
|
79
76
|
|
|
80
|
-
const EXT_TO_MEDIA_TYPE: Record<string, MediaType> = {
|
|
81
|
-
// Images
|
|
82
|
-
png: 'image', jpg: 'image', jpeg: 'image', webp: 'image', gif: 'image', bmp: 'image', tiff: 'image', svg: 'image', heic: 'image', heif: 'image',
|
|
83
|
-
|
|
84
|
-
// Audio
|
|
85
|
-
mp3: 'audio', wav: 'audio', ogg: 'audio', aac: 'audio', m4a: 'audio', flac: 'audio',
|
|
86
|
-
|
|
87
|
-
// Video
|
|
88
|
-
mp4: 'video', mov: 'video', avi: 'video', mkv: 'video', mpeg: 'video', mpg: 'video', '3gp': 'video', '3g2': 'video',
|
|
89
|
-
|
|
90
|
-
// Files
|
|
91
|
-
pdf: 'file', txt: 'file', md: 'file', csv: 'file', json: 'file',
|
|
92
|
-
doc: 'file', docx: 'file', xls: 'file', xlsx: 'file', ppt: 'file', pptx: 'file',
|
|
93
|
-
zip: 'file', rar: 'file', '7z': 'file',
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
|
|
97
77
|
export class MediaFieldCrudManager implements FieldCrudManager {
|
|
98
78
|
|
|
99
79
|
constructor(private readonly options: MediaFieldOptions) {
|
|
100
80
|
}
|
|
101
81
|
|
|
102
82
|
private resolveMediaType(mimetype?: string, filename?: string): MediaType | null {
|
|
83
|
+
const ext = (filename || '').split('.').pop()?.toLowerCase();
|
|
84
|
+
if (ext && DANGEROUS_EXTENSIONS.has(ext)) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
103
88
|
const mt = (mimetype || '').toLowerCase().trim();
|
|
89
|
+
if (mt === 'image/svg+xml' || mt === 'text/html' || mt === 'application/xhtml+xml') {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
104
92
|
if (mt && MIME_TO_MEDIA_TYPE[mt]) {
|
|
105
93
|
return MIME_TO_MEDIA_TYPE[mt];
|
|
106
94
|
}
|
|
@@ -110,8 +98,8 @@ export class MediaFieldCrudManager implements FieldCrudManager {
|
|
|
110
98
|
if (mt.startsWith('audio/')) return 'audio';
|
|
111
99
|
if (mt.startsWith('video/')) return 'video';
|
|
112
100
|
|
|
113
|
-
// Fallback to extension if provided
|
|
114
|
-
|
|
101
|
+
// Fallback to extension if provided (also covers generic/unrecognized
|
|
102
|
+
// mimetypes such as application/octet-stream).
|
|
115
103
|
if (ext && EXT_TO_MEDIA_TYPE[ext]) {
|
|
116
104
|
return EXT_TO_MEDIA_TYPE[ext];
|
|
117
105
|
}
|
|
@@ -7,6 +7,7 @@ import { MediaStorageProviderMetadataRepository } from './media-storage-provider
|
|
|
7
7
|
import { ModelMetadataRepository } from './model-metadata.repository';
|
|
8
8
|
import { SecurityRuleRepository } from './security-rule.repository';
|
|
9
9
|
import { SolidBaseRepository } from './solid-base.repository';
|
|
10
|
+
import { resolveStoredMediaIsPublic } from 'src/services/media-storage.utils';
|
|
10
11
|
|
|
11
12
|
@Injectable()
|
|
12
13
|
export class MediaRepository extends SolidBaseRepository<Media> {
|
|
@@ -44,6 +45,7 @@ export class MediaRepository extends SolidBaseRepository<Media> {
|
|
|
44
45
|
id: createDto['mediaStorageProviderMetadataId']
|
|
45
46
|
},
|
|
46
47
|
});
|
|
48
|
+
createDto['isPublic'] = resolveStoredMediaIsPublic(createDto['isPublic'], createDto['mediaStorageProviderMetadata']);
|
|
47
49
|
const media = this.create(createDto);
|
|
48
50
|
return this.save(media);
|
|
49
51
|
}
|
|
@@ -1704,6 +1704,18 @@
|
|
|
1704
1704
|
"columnName": "original_file_name",
|
|
1705
1705
|
"isSystem": true
|
|
1706
1706
|
},
|
|
1707
|
+
{
|
|
1708
|
+
"name": "isPublic",
|
|
1709
|
+
"displayName": "Is Public",
|
|
1710
|
+
"type": "boolean",
|
|
1711
|
+
"required": false,
|
|
1712
|
+
"unique": false,
|
|
1713
|
+
"index": false,
|
|
1714
|
+
"private": false,
|
|
1715
|
+
"encrypt": false,
|
|
1716
|
+
"columnName": "is_public",
|
|
1717
|
+
"isSystem": true
|
|
1718
|
+
},
|
|
1707
1719
|
{
|
|
1708
1720
|
"name": "modelMetadata",
|
|
1709
1721
|
"displayName": "Model Metadata",
|
|
@@ -5306,6 +5318,9 @@
|
|
|
5306
5318
|
"FieldMetadataController.getSelectionDynamicValues",
|
|
5307
5319
|
"FieldMetadataController.getSelectionDynamicValue",
|
|
5308
5320
|
"FieldMetadataController.findFieldDefaultMetaData",
|
|
5321
|
+
"MediaController.findMany",
|
|
5322
|
+
"MediaController.findOne",
|
|
5323
|
+
"MediaController.download",
|
|
5309
5324
|
"SavedFiltersController.delete",
|
|
5310
5325
|
"SavedFiltersController.deleteMany",
|
|
5311
5326
|
"SavedFiltersController.findOne",
|
|
@@ -8607,9 +8622,9 @@
|
|
|
8607
8622
|
"attrs": {
|
|
8608
8623
|
"name": "roles",
|
|
8609
8624
|
"widget": "checkbox",
|
|
8610
|
-
"inlineCreateAutoSave": "
|
|
8625
|
+
"inlineCreateAutoSave": "false",
|
|
8611
8626
|
"renderModeCheckboxPreprocessor": "MenuItemMetadataRolesFormFieldPreprocessor",
|
|
8612
|
-
"inlineCreate": "
|
|
8627
|
+
"inlineCreate": "false",
|
|
8613
8628
|
"inlineCreateLayout": {
|
|
8614
8629
|
"type": "form",
|
|
8615
8630
|
"attrs": {
|
|
@@ -8758,6 +8773,13 @@
|
|
|
8758
8773
|
"sortable": false
|
|
8759
8774
|
}
|
|
8760
8775
|
},
|
|
8776
|
+
{
|
|
8777
|
+
"type": "field",
|
|
8778
|
+
"attrs": {
|
|
8779
|
+
"name": "isPublic",
|
|
8780
|
+
"sortable": true
|
|
8781
|
+
}
|
|
8782
|
+
},
|
|
8761
8783
|
{
|
|
8762
8784
|
"type": "field",
|
|
8763
8785
|
"attrs": {
|
|
@@ -8853,6 +8875,14 @@
|
|
|
8853
8875
|
"isSearchable": true
|
|
8854
8876
|
}
|
|
8855
8877
|
},
|
|
8878
|
+
{
|
|
8879
|
+
"type": "field",
|
|
8880
|
+
"attrs": {
|
|
8881
|
+
"name": "isPublic",
|
|
8882
|
+
"label": "isPublic",
|
|
8883
|
+
"isSearchable": true
|
|
8884
|
+
}
|
|
8885
|
+
},
|
|
8856
8886
|
{
|
|
8857
8887
|
"type": "field",
|
|
8858
8888
|
"attrs": {
|
|
@@ -8921,6 +8951,12 @@
|
|
|
8921
8951
|
"name": "mediaStorageProviderMetadata"
|
|
8922
8952
|
}
|
|
8923
8953
|
},
|
|
8954
|
+
{
|
|
8955
|
+
"type": "field",
|
|
8956
|
+
"attrs": {
|
|
8957
|
+
"name": "isPublic"
|
|
8958
|
+
}
|
|
8959
|
+
},
|
|
8924
8960
|
{
|
|
8925
8961
|
"type": "field",
|
|
8926
8962
|
"attrs": {
|
|
@@ -14525,7 +14561,15 @@
|
|
|
14525
14561
|
"mediaStorageProviders": [
|
|
14526
14562
|
{
|
|
14527
14563
|
"name": "default-filesystem",
|
|
14528
|
-
"type": "filesystem"
|
|
14564
|
+
"type": "filesystem",
|
|
14565
|
+
"isPublic": true,
|
|
14566
|
+
"localPath": "media-files-storage"
|
|
14567
|
+
},
|
|
14568
|
+
{
|
|
14569
|
+
"name": "private-filesystem",
|
|
14570
|
+
"type": "filesystem",
|
|
14571
|
+
"isPublic": false,
|
|
14572
|
+
"localPath": "media-private-files-storage"
|
|
14529
14573
|
},
|
|
14530
14574
|
{
|
|
14531
14575
|
"name": "default-aws-s3",
|
|
@@ -14561,6 +14605,47 @@
|
|
|
14561
14605
|
}
|
|
14562
14606
|
}
|
|
14563
14607
|
}
|
|
14608
|
+
},
|
|
14609
|
+
{
|
|
14610
|
+
"name": "model:Media-role:Internal User-own",
|
|
14611
|
+
"description": "Show Media records owned by the current user",
|
|
14612
|
+
"roleUserKey": "Internal User",
|
|
14613
|
+
"modelMetadataUserKey": "media",
|
|
14614
|
+
"securityRuleConfig": {
|
|
14615
|
+
"filters": {
|
|
14616
|
+
"createdBy": {
|
|
14617
|
+
"$eq": "$activeUserId"
|
|
14618
|
+
}
|
|
14619
|
+
}
|
|
14620
|
+
}
|
|
14621
|
+
},
|
|
14622
|
+
{
|
|
14623
|
+
"name": "model:Media-role:Internal User-public",
|
|
14624
|
+
"description": "Show public Media records",
|
|
14625
|
+
"roleUserKey": "Internal User",
|
|
14626
|
+
"modelMetadataUserKey": "media",
|
|
14627
|
+
"securityRuleConfig": {
|
|
14628
|
+
"filters": {
|
|
14629
|
+
"mediaStorageProviderMetadata": {
|
|
14630
|
+
"isPublic": {
|
|
14631
|
+
"$eq": true
|
|
14632
|
+
}
|
|
14633
|
+
}
|
|
14634
|
+
}
|
|
14635
|
+
}
|
|
14636
|
+
},
|
|
14637
|
+
{
|
|
14638
|
+
"name": "model:Media-role:Admin",
|
|
14639
|
+
"description": "Show All Media records",
|
|
14640
|
+
"roleUserKey": "Admin",
|
|
14641
|
+
"modelMetadataUserKey": "media",
|
|
14642
|
+
"securityRuleConfig": {
|
|
14643
|
+
"filters": {
|
|
14644
|
+
"id": {
|
|
14645
|
+
"$ne": "0"
|
|
14646
|
+
}
|
|
14647
|
+
}
|
|
14648
|
+
}
|
|
14564
14649
|
}
|
|
14565
14650
|
],
|
|
14566
14651
|
"modelSequences": [],
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
1. Do i need to create a storeStreams method for aws service too?
|
|
2
|
+
- Handle later
|
|
3
|
+
2. queues handling -> if queues is enabled by default, i.e triggerExport(exportTransactionEntity.id).
|
|
4
|
+
- startExport should either return the data or return the transaction id
|
|
5
|
+
3. How to handle scenarios wherein, nested related exist.(do i need to only get the userkey)
|
|
6
|
+
- show the userKey
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Injectable, Logger } from '@nestjs/common';
|
|
2
|
+
import { ConfigService } from '@nestjs/config';
|
|
3
|
+
import { JwtService } from '@nestjs/jwt';
|
|
4
|
+
import { MediaStorageProviderType } from 'src/dtos/create-media-storage-provider-metadata.dto';
|
|
5
|
+
import { MediaStorageProviderMetadata } from 'src/entities/media-storage-provider-metadata.entity';
|
|
6
|
+
import { S3FileService } from 'src/services/file';
|
|
7
|
+
import { getEffectiveS3Region } from 'src/services/media-storage.utils';
|
|
8
|
+
import { SettingService } from 'src/services/setting.service';
|
|
9
|
+
import type { SolidCoreSetting } from 'src/services/settings/default-settings-provider.service';
|
|
10
|
+
|
|
11
|
+
export const MEDIA_DOWNLOAD_TOKEN_AUDIENCE = 'media-download';
|
|
12
|
+
const DEFAULT_SIGNED_URL_EXPIRY_MINUTES = 60;
|
|
13
|
+
|
|
14
|
+
export interface MediaDownloadTokenPayload {
|
|
15
|
+
mediaId: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@Injectable()
|
|
19
|
+
export class MediaDownloadUrlService {
|
|
20
|
+
private readonly logger = new Logger(MediaDownloadUrlService.name);
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
private readonly jwtService: JwtService,
|
|
24
|
+
private readonly s3FileService: S3FileService,
|
|
25
|
+
private readonly settingService: SettingService,
|
|
26
|
+
private readonly configService: ConfigService,
|
|
27
|
+
) { }
|
|
28
|
+
|
|
29
|
+
async resolveDownloadUrl(mediaId: number, relativeUri: string, storageProvider?: MediaStorageProviderMetadata): Promise<string> {
|
|
30
|
+
const expiryMinutes = storageProvider?.signedUrlExpiry ?? DEFAULT_SIGNED_URL_EXPIRY_MINUTES;
|
|
31
|
+
|
|
32
|
+
if ((storageProvider?.type as MediaStorageProviderType) === MediaStorageProviderType.AwsS3) {
|
|
33
|
+
if (storageProvider?.bucketName) {
|
|
34
|
+
return this.s3FileService.getUrl(
|
|
35
|
+
`${storageProvider.bucketName}:${relativeUri}`,
|
|
36
|
+
{ region: getEffectiveS3Region(this.configService, storageProvider.region), expiresIn: expiryMinutes * 60 },
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
this.logger.warn(`Media id ${mediaId}: AwsS3 provider missing bucketName; falling back to app-hosted token URL`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Filesystem (or misconfigured S3) has no native presigned-URL mechanism, so mint our
|
|
43
|
+
// own short-lived token and embed it in the download URL - the token alone authorizes
|
|
44
|
+
// the request (see MediaSignedUrlGuard), so it works in a plain <img src> with no
|
|
45
|
+
// Authorization header needed.
|
|
46
|
+
const token = await this.mintDownloadToken(mediaId, expiryMinutes);
|
|
47
|
+
const downloadPath = `/api/media/${mediaId}/download?token=${token}`;
|
|
48
|
+
const baseUrl = this.settingService.getConfigValue<SolidCoreSetting>('baseUrl');
|
|
49
|
+
return `${String(baseUrl).replace(/\/+$/, '')}${downloadPath}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async verifyDownloadToken(token: string): Promise<MediaDownloadTokenPayload> {
|
|
53
|
+
return this.jwtService.verifyAsync<MediaDownloadTokenPayload>(token, {
|
|
54
|
+
secret: this.settingService.getConfigValue<SolidCoreSetting>('secret'),
|
|
55
|
+
audience: MEDIA_DOWNLOAD_TOKEN_AUDIENCE,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private mintDownloadToken(mediaId: number, expiryMinutes: number): Promise<string> {
|
|
60
|
+
const payload: MediaDownloadTokenPayload = { mediaId };
|
|
61
|
+
return this.jwtService.signAsync(payload, {
|
|
62
|
+
secret: this.settingService.getConfigValue<SolidCoreSetting>('secret'),
|
|
63
|
+
audience: MEDIA_DOWNLOAD_TOKEN_AUDIENCE,
|
|
64
|
+
expiresIn: expiryMinutes * 60,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { ConfigService } from '@nestjs/config';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { FieldMetadata } from 'src/entities/field-metadata.entity';
|
|
4
|
+
import { MediaStorageProviderMetadata } from 'src/entities/media-storage-provider-metadata.entity';
|
|
5
|
+
import { SettingService } from 'src/services/setting.service';
|
|
6
|
+
import { DEFAULT_MEDIA_FILE_STORAGE_DIR } from 'src/services/settings/default-settings-provider.service';
|
|
7
|
+
import type { SolidCoreSetting } from 'src/services/settings/default-settings-provider.service';
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_PRIVATE_MEDIA_STORAGE_DIR = 'media-private-files-storage';
|
|
10
|
+
|
|
11
|
+
export interface MediaRecordCreateInput {
|
|
12
|
+
entityId: number;
|
|
13
|
+
modelMetadataId: number;
|
|
14
|
+
mediaStorageProviderMetadataId: number;
|
|
15
|
+
fieldMetadataId: number;
|
|
16
|
+
relativeUri: string;
|
|
17
|
+
mimeType?: string;
|
|
18
|
+
fileSize?: number;
|
|
19
|
+
originalFileName?: string;
|
|
20
|
+
isPublic?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Builds the stored file name format shared by media storage implementations.
|
|
24
|
+
export function buildStoredMediaFileName(file: Pick<Express.Multer.File, 'filename' | 'originalname'>): string {
|
|
25
|
+
return `${file.filename}-${file.originalname}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Resolves the on-disk path for a media file based on settings and provider visibility.
|
|
29
|
+
export function buildDiskMediaPath(fileName: string,settingService: Pick<SettingService, 'getConfigValue'>,storageProvider?: Pick<MediaStorageProviderMetadata, 'localPath' | 'isPublic'>,): string {
|
|
30
|
+
const publicBase = settingService.getConfigValue<SolidCoreSetting>('fileStorageDir') || DEFAULT_MEDIA_FILE_STORAGE_DIR;
|
|
31
|
+
const privateBase = DEFAULT_PRIVATE_MEDIA_STORAGE_DIR;
|
|
32
|
+
const providerBase = storageProvider?.localPath || (storageProvider?.isPublic === false ? privateBase : publicBase);
|
|
33
|
+
|
|
34
|
+
if ( path.isAbsolute(fileName) || fileName.startsWith(`${publicBase}/`) || fileName.startsWith(`${privateBase}/`) || (!!storageProvider?.localPath && fileName.startsWith(`${storageProvider.localPath}/`))) {
|
|
35
|
+
return fileName;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return `${providerBase}/${fileName}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Returns the S3 region from the provider when present, otherwise falls back to app config.
|
|
42
|
+
export function getEffectiveS3Region(configService: Pick<ConfigService, 'get'>,providerRegion?: string,): string | undefined {
|
|
43
|
+
return providerRegion || configService.get<string>('S3_AWS_REGION_NAME');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Resolves the effective public/private flag for a media storage provider.
|
|
47
|
+
export function resolveMediaIsPublic(storageProvider?: Pick<MediaStorageProviderMetadata, 'isPublic'>,): boolean | undefined {
|
|
48
|
+
if (!storageProvider) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return storageProvider.isPublic !== false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Preserves an explicit media visibility value or derives it from the storage provider.
|
|
56
|
+
export function resolveStoredMediaIsPublic(currentValue: boolean | undefined,storageProvider?:Pick<MediaStorageProviderMetadata, 'isPublic'>,): boolean | undefined {
|
|
57
|
+
if (typeof currentValue === 'boolean') {
|
|
58
|
+
return currentValue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return resolveMediaIsPublic(storageProvider);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Builds the shared media record payload used before saving media metadata to the database.
|
|
65
|
+
export function buildMediaRecordCreateInput(
|
|
66
|
+
entityId: number,
|
|
67
|
+
mediaFieldMetadata: Pick<FieldMetadata, 'id' | 'model'> & { model: { id: number } },
|
|
68
|
+
storageProvider: Pick<MediaStorageProviderMetadata, 'id'>,
|
|
69
|
+
overrides: Pick<MediaRecordCreateInput, 'relativeUri'>
|
|
70
|
+
& Partial<Omit<MediaRecordCreateInput, 'entityId' | 'modelMetadataId' | 'mediaStorageProviderMetadataId' | 'fieldMetadataId' | 'relativeUri'>>,
|
|
71
|
+
): MediaRecordCreateInput {
|
|
72
|
+
return {
|
|
73
|
+
entityId,
|
|
74
|
+
modelMetadataId: mediaFieldMetadata.model.id,
|
|
75
|
+
mediaStorageProviderMetadataId: storageProvider.id,
|
|
76
|
+
fieldMetadataId: mediaFieldMetadata.id,
|
|
77
|
+
...overrides,
|
|
78
|
+
};
|
|
79
|
+
}
|