@solidxai/core 0.1.12-beta.2 → 0.1.12-beta.4
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/commands/seed.command.d.ts.map +1 -1
- package/dist/commands/seed.command.js +15 -2
- package/dist/commands/seed.command.js.map +1 -1
- 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/create-role-metadata.dto.d.ts +2 -0
- package/dist/dtos/create-role-metadata.dto.d.ts.map +1 -1
- package/dist/dtos/create-role-metadata.dto.js +3 -1
- package/dist/dtos/create-role-metadata.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/interfaces.d.ts +19 -1
- package/dist/interfaces.d.ts.map +1 -1
- package/dist/interfaces.js +2 -0
- package/dist/interfaces.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/module-metadata-seeder.service.d.ts +7 -1
- package/dist/seeders/module-metadata-seeder.service.d.ts.map +1 -1
- package/dist/seeders/module-metadata-seeder.service.js +87 -3
- package/dist/seeders/module-metadata-seeder.service.js.map +1 -1
- package/dist/seeders/permission-metadata-seeder.service.d.ts +1 -0
- package/dist/seeders/permission-metadata-seeder.service.d.ts.map +1 -1
- package/dist/seeders/permission-metadata-seeder.service.js +6 -1
- package/dist/seeders/permission-metadata-seeder.service.js.map +1 -1
- package/dist/seeders/seed-data/solid-core-metadata.json +114 -13
- package/dist/services/authentication.service.d.ts +1 -0
- package/dist/services/authentication.service.d.ts.map +1 -1
- package/dist/services/authentication.service.js +19 -1
- package/dist/services/authentication.service.js.map +1 -1
- 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/services/role-metadata.service.d.ts +1 -0
- package/dist/services/role-metadata.service.d.ts.map +1 -1
- package/dist/services/role-metadata.service.js +8 -5
- package/dist/services/role-metadata.service.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/commands/seed.command.ts +17 -3
- 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/create-role-metadata.dto.ts +2 -0
- 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/interfaces.ts +20 -0
- package/src/repository/media.repository.ts +2 -0
- package/src/seeders/module-metadata-seeder.service.ts +109 -3
- package/src/seeders/permission-metadata-seeder.service.ts +6 -1
- package/src/seeders/seed-data/solid-core-metadata.json +114 -13
- package/src/services/1.js +6 -0
- package/src/services/authentication.service.ts +24 -1
- 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/services/role-metadata.service.ts +17 -5
- 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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit.subscriber.js","sourceRoot":"","sources":["../../src/subscribers/audit.subscriber.ts"],"names":[],"mappings":"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","sourcesContent":["import { Injectable, Logger, Scope } from '@nestjs/common';\nimport { ModelMetadataHelperService } from 'src/helpers/model-metadata-helper.service';\nimport { lowerFirst } from 'src/helpers/string.helper';\nimport { SolidRegistry } from 'src/helpers/solid-registry';\nimport { DataSource, EntityMetadata, EntitySubscriberInterface, InsertEvent, RemoveEvent, UpdateEvent } from 'typeorm';\nimport { AuditQueuePayload } from 'src/interfaces';\nimport { RequestContextService } from 'src/services/request-context.service';\nimport { PublisherFactory } from 'src/services/queues/publisher-factory.service';\nconst AUDIT_BEFORE_SNAPSHOT = '__auditBeforeSnapshot';\n\n@Injectable({ scope: Scope.TRANSIENT })\nexport class AuditSubscriber implements EntitySubscriberInterface {\n private readonly logger = new Logger(AuditSubscriber.name);\n private dataSource: DataSource;\n constructor(\n private readonly publisherFactory: PublisherFactory<AuditQueuePayload>,\n private readonly solidRegistry: SolidRegistry,\n private readonly requestContextService: RequestContextService,\n private readonly modelMetadataHelperService: ModelMetadataHelperService,\n ) { }\n\n bindToDataSource(dataSource: DataSource) {\n this.dataSource = dataSource;\n this.dataSource.subscribers.push(this);\n }\n\n // Per-transaction buffer (auto-GC when queryRunner is gone)\n private perTxn = new WeakMap<any, AuditQueuePayload[]>();\n\n private enqueue(event: { queryRunner: any }, payload: AuditQueuePayload) {\n const qr = event.queryRunner;\n const arr = this.perTxn.get(qr) ?? [];\n arr.push(payload);\n this.perTxn.set(qr, arr);\n }\n\n private shouldTrackAudit(metadata: EntityMetadata): boolean {\n return this.solidRegistry.isAuditableModel(lowerFirst(metadata.name));\n }\n\n private activeUserId(): number | null {\n return this.requestContextService.getActiveUser()?.sub ?? null;\n }\n\n async afterInsert(event: InsertEvent<any>) {\n if (!this.shouldTrackAudit(event.metadata)) return;\n this.enqueue(event, {\n eventType: 'insert',\n modelName: event.metadata.name,\n entityId: event.entity?.id ?? null,\n occurredAt: new Date().toISOString(),\n after: event.entity ?? null,\n userId: this.activeUserId(),\n });\n }\n\n async afterUpdate(event: UpdateEvent<any>) {\n if (!this.shouldTrackAudit(event.metadata)) return;\n const entityId = event.entity?.id ?? event.databaseEntity?.id ?? (event as any).entityId ?? null;\n let before = event.databaseEntity ?? null;\n let after = event.entity ?? null;\n const updatedColumnNames = (event.updatedColumns ?? []).map(c => c.propertyName);\n\n const auditResult = await this.prepareManyToManyAuditUpdateSnapshot(event, entityId, updatedColumnNames);\n\n if (auditResult) {\n before = auditResult.before;\n after = auditResult.after;\n }\n\n this.enqueue(event, {\n eventType: 'update',\n modelName: event.metadata.name,\n entityId: entityId,\n occurredAt: new Date().toISOString(),\n after: after,\n // databaseEntity is only populated when the entity was fetched first (save() path).\n // QueryBuilder update() leaves this undefined; postAuditMessageOnUpdate guards for it.\n before: before,\n updatedColumnNames: updatedColumnNames,\n userId: this.activeUserId(),\n });\n }\n\n /**\n * Resolves before/after snapshots for audit-enabled many-to-many relations\n * and updates changed relation field names for audit tracking.\n */\n private async prepareManyToManyAuditUpdateSnapshot(event: UpdateEvent<any>, entityId: number | null, updatedColumnNames: string[]): Promise<{ before: any; after: any } | null> {\n\n const auditRelationFields = (await this.modelMetadataHelperService.loadFieldHierarchy(lowerFirst(event.metadata.name))).filter(field =>\n field.enableAuditTracking &&\n field.type === 'relation' &&\n field.relationType !== 'one-to-many'\n );\n\n if (!entityId || auditRelationFields.length === 0) {\n return null;\n }\n\n const relations: Record<string, boolean> = {};\n\n auditRelationFields.forEach(field => {\n relations[field.name] = true;\n });\n\n const relationBefore = event.entity?.[AUDIT_BEFORE_SNAPSHOT] ?? null;\n\n const relationAfter = await this.dataSource.getRepository(event.metadata.target as any).findOne({\n where: { id: entityId } as any,\n relations: relations as any,\n });\n\n if (relationBefore && relationAfter) {\n auditRelationFields.forEach(field => {\n const oldIds = Array.isArray(relationBefore[field.name])\n ? relationBefore[field.name].map(item => item.id).sort()\n : [];\n\n const newIds = Array.isArray(relationAfter[field.name])\n ? relationAfter[field.name].map(item => item.id).sort()\n : [];\n\n if ((oldIds.length !== newIds.length || JSON.stringify(oldIds) !== JSON.stringify(newIds))\n && !updatedColumnNames.includes(field.name)) {\n updatedColumnNames.push(field.name);\n }\n });\n }\n return {\n before: relationBefore ?? event.databaseEntity ?? null,\n after: relationAfter ?? event.entity ?? null,\n };\n }\n async afterRemove(event: RemoveEvent<any>) {\n if (!this.shouldTrackAudit(event.metadata)) return;\n this.enqueue(event, {\n eventType: 'delete',\n modelName: event.metadata.name,\n entityId: event.databaseEntity?.id ?? null,\n occurredAt: new Date().toISOString(),\n before: event.databaseEntity,\n userId: this.activeUserId(),\n });\n }\n\n // --------- transaction lifecycle ----------\n async afterTransactionCommit(event: { queryRunner: any }) {\n const batch = this.perTxn.get(event.queryRunner) ?? [];\n this.perTxn.delete(event.queryRunner);\n\n if (batch.length === 0) return;\n\n await this.publishAuditBatch(batch);\n }\n\n afterTransactionRollback(event: { queryRunner: any }) {\n // Drop buffered payloads; the write never happened.\n this.perTxn.delete(event.queryRunner);\n }\n\n private async publishAuditBatch(batch: AuditQueuePayload[]) {\n try {\n // allSettled: publish in parallel; a single failure does not block the rest.\n const results = await Promise.allSettled(\n batch.map(payload => this.publisherFactory.publish({ payload }, 'ChatterQueuePublisher'))\n );\n\n results.forEach((result, i) => {\n if (result.status === 'rejected') {\n this.logger.error(\n `Failed to publish audit event for ${batch[i].modelName}#${batch[i].entityId}`,\n result.reason,\n );\n }\n });\n } catch (error) {\n this.logger.error('Failed to publish audit batch', error);\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"audit.subscriber.js","sourceRoot":"","sources":["../../src/subscribers/audit.subscriber.ts"],"names":[],"mappings":"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","sourcesContent":["import { Injectable, Logger, Scope } from '@nestjs/common';\nimport { ModelMetadataHelperService } from 'src/helpers/model-metadata-helper.service';\nimport { lowerFirst } from 'src/helpers/string.helper';\nimport { SolidRegistry } from 'src/helpers/solid-registry';\nimport { DataSource, EntityMetadata, EntitySubscriberInterface, InsertEvent, RemoveEvent, UpdateEvent } from 'typeorm';\nimport { AuditQueuePayload } from 'src/interfaces';\nimport { RequestContextService } from 'src/services/request-context.service';\nimport { PublisherFactory } from 'src/services/queues/publisher-factory.service';\nconst AUDIT_BEFORE_SNAPSHOT = '__auditBeforeSnapshot';\n\n@Injectable({ scope: Scope.TRANSIENT })\nexport class AuditSubscriber implements EntitySubscriberInterface {\n private readonly logger = new Logger(AuditSubscriber.name);\n private dataSource: DataSource;\n constructor(\n private readonly publisherFactory: PublisherFactory<AuditQueuePayload>,\n private readonly solidRegistry: SolidRegistry,\n private readonly requestContextService: RequestContextService,\n private readonly modelMetadataHelperService: ModelMetadataHelperService,\n ) { }\n\n bindToDataSource(dataSource: DataSource) {\n this.dataSource = dataSource;\n this.dataSource.subscribers.push(this);\n }\n\n // Per-transaction buffer (auto-GC when queryRunner is gone)\n private perTxn = new WeakMap<any, AuditQueuePayload[]>();\n\n private enqueue(event: { queryRunner: any }, payload: AuditQueuePayload) {\n const qr = event.queryRunner;\n const arr = this.perTxn.get(qr) ?? [];\n arr.push(payload);\n this.perTxn.set(qr, arr);\n }\n\n private shouldTrackAudit(metadata: EntityMetadata): boolean {\n return this.solidRegistry.isAuditableModel(lowerFirst(metadata.name));\n }\n\n private activeUserId(): number | null {\n return this.requestContextService.getActiveUser()?.sub ?? null;\n }\n\n async afterInsert(event: InsertEvent<any>) {\n if (!this.shouldTrackAudit(event.metadata)) return;\n this.enqueue(event, {\n eventType: 'insert',\n modelName: event.metadata.name,\n entityId: event.entity?.id ?? null,\n occurredAt: new Date().toISOString(),\n after: event.entity ?? null,\n userId: this.activeUserId(),\n });\n }\n\n async afterUpdate(event: UpdateEvent<any>) {\n if (!this.shouldTrackAudit(event.metadata)) return;\n const entityId = event.entity?.id ?? event.databaseEntity?.id ?? (event as any).entityId ?? null;\n let before = event.databaseEntity ?? null;\n let after = event.entity ?? null;\n const updatedColumnNames = (event.updatedColumns ?? []).map(c => c.propertyName);\n\n const auditResult = await this.prepareManyToManyAuditUpdateSnapshot(event, entityId, updatedColumnNames);\n\n if (auditResult) {\n before = auditResult.before;\n after = auditResult.after;\n }\n\n this.enqueue(event, {\n eventType: 'update',\n modelName: event.metadata.name,\n entityId: entityId,\n occurredAt: new Date().toISOString(),\n after: after,\n // databaseEntity is only populated when the entity was fetched first (save() path).\n // QueryBuilder update() leaves this undefined; postAuditMessageOnUpdate guards for it.\n before: before,\n updatedColumnNames: updatedColumnNames,\n userId: this.activeUserId(),\n });\n }\n\n /**\n * Resolves before/after snapshots for audit-enabled many-to-many relations\n * and updates changed relation field names for audit tracking.\n */\n private async prepareManyToManyAuditUpdateSnapshot(event: UpdateEvent<any>, entityId: number | null, updatedColumnNames: string[]): Promise<{ before: any; after: any } | null> {\n\n const auditRelationFields = (await this.modelMetadataHelperService.loadFieldHierarchy(lowerFirst(event.metadata.name))).filter(field =>\n field.enableAuditTracking &&\n field.type === 'relation' &&\n field.relationType !== 'one-to-many'\n );\n\n if (!entityId || auditRelationFields.length === 0) {\n return null;\n }\n\n const relations: Record<string, boolean> = {};\n\n auditRelationFields.forEach(field => {\n relations[field.name] = true;\n });\n\n const relationBefore = event.entity?.[AUDIT_BEFORE_SNAPSHOT] ?? null;\n\n const relationAfter = await event.queryRunner.manager.getRepository(event.metadata.target as any).findOne({\n where: { id: entityId } as any,\n relations: relations as any,\n });\n\n if (relationBefore && relationAfter) {\n auditRelationFields.forEach(field => {\n const oldIds = Array.isArray(relationBefore[field.name])\n ? relationBefore[field.name].map(item => item.id).sort()\n : [];\n\n const newIds = Array.isArray(relationAfter[field.name])\n ? relationAfter[field.name].map(item => item.id).sort()\n : [];\n\n if ((oldIds.length !== newIds.length || JSON.stringify(oldIds) !== JSON.stringify(newIds))\n && !updatedColumnNames.includes(field.name)) {\n updatedColumnNames.push(field.name);\n }\n });\n }\n return {\n before: relationBefore ?? event.databaseEntity ?? null,\n after: relationAfter ?? event.entity ?? null,\n };\n }\n async afterRemove(event: RemoveEvent<any>) {\n if (!this.shouldTrackAudit(event.metadata)) return;\n this.enqueue(event, {\n eventType: 'delete',\n modelName: event.metadata.name,\n entityId: event.databaseEntity?.id ?? null,\n occurredAt: new Date().toISOString(),\n before: event.databaseEntity,\n userId: this.activeUserId(),\n });\n }\n\n // --------- transaction lifecycle ----------\n async afterTransactionCommit(event: { queryRunner: any }) {\n const batch = this.perTxn.get(event.queryRunner) ?? [];\n this.perTxn.delete(event.queryRunner);\n\n if (batch.length === 0) return;\n\n await this.publishAuditBatch(batch);\n }\n\n afterTransactionRollback(event: { queryRunner: any }) {\n // Drop buffered payloads; the write never happened.\n this.perTxn.delete(event.queryRunner);\n }\n\n private async publishAuditBatch(batch: AuditQueuePayload[]) {\n try {\n // allSettled: publish in parallel; a single failure does not block the rest.\n const results = await Promise.allSettled(\n batch.map(payload => this.publisherFactory.publish({ payload }, 'ChatterQueuePublisher'))\n );\n\n results.forEach((result, i) => {\n if (result.status === 'rejected') {\n this.logger.error(\n `Failed to publish audit event for ${batch[i].modelName}#${batch[i].entityId}`,\n result.reason,\n );\n }\n });\n } catch (error) {\n this.logger.error('Failed to publish audit batch', error);\n }\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidxai/core",
|
|
3
|
-
"version": "0.1.12-beta.
|
|
3
|
+
"version": "0.1.12-beta.4",
|
|
4
4
|
"description": "This module is a NestJS module containing all the required core providers required by a Solid application",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Logger } from '@nestjs/common';
|
|
2
2
|
import { Command, CommandRunner, Option } from 'nest-commander';
|
|
3
3
|
import { SolidRegistry } from 'src/helpers/solid-registry';
|
|
4
|
+
import { ModuleMetadataSeederOptions } from 'src/interfaces';
|
|
4
5
|
|
|
5
6
|
interface SeedCommandOptions {
|
|
6
7
|
seeder?: string;
|
|
@@ -17,20 +18,33 @@ export class SeedCommand extends CommandRunner {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
async run(passedParam: string[], options?: SeedCommandOptions): Promise<void> {
|
|
20
|
-
let parsedConf:
|
|
21
|
+
let parsedConf: ModuleMetadataSeederOptions | null = null;
|
|
21
22
|
if (options?.modulesToSeed) {
|
|
22
23
|
const modulesToSeed = options.modulesToSeed
|
|
23
24
|
.split(',')
|
|
24
25
|
.map((m) => m.trim())
|
|
25
26
|
.filter(Boolean);
|
|
26
|
-
parsedConf = {
|
|
27
|
+
parsedConf = {
|
|
28
|
+
modulesToSeed,
|
|
29
|
+
pruneMetadata: true,
|
|
30
|
+
seedGlobalMetadata: true,
|
|
31
|
+
};
|
|
27
32
|
this.logger.log(`Modules to seed: ${modulesToSeed.join(', ')}`);
|
|
28
33
|
} else {
|
|
29
34
|
this.logger.log('No --modules-to-seed flag provided. Running with default seeder behavior.');
|
|
35
|
+
parsedConf = {
|
|
36
|
+
modulesToSeed: null,
|
|
37
|
+
pruneMetadata: true,
|
|
38
|
+
seedGlobalMetadata: true,
|
|
39
|
+
};
|
|
30
40
|
}
|
|
31
41
|
|
|
32
42
|
if (options?.prune) {
|
|
33
|
-
parsedConf = parsedConf ?? {
|
|
43
|
+
parsedConf = parsedConf ?? {
|
|
44
|
+
modulesToSeed: null,
|
|
45
|
+
pruneMetadata: true,
|
|
46
|
+
seedGlobalMetadata: true,
|
|
47
|
+
};
|
|
34
48
|
parsedConf.pruneMetadata = true;
|
|
35
49
|
}
|
|
36
50
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type MediaCategory = 'image' | 'audio' | 'video' | 'file' | 'pdf';
|
|
2
|
+
|
|
3
|
+
const IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'webp', 'gif', 'bmp', 'tiff', 'heic', 'heif'];
|
|
4
|
+
const AUDIO_EXTENSIONS = ['mp3', 'wav', 'ogg', 'aac', 'm4a', 'flac', 'webm'];
|
|
5
|
+
const VIDEO_EXTENSIONS = ['mp4', 'mov', 'avi', 'mkv', 'mpeg', 'mpg', '3gp', '3g2', 'webm', 'ogg'];
|
|
6
|
+
const DOCUMENT_EXTENSIONS = ['txt', 'md', 'csv', 'json', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'zip', 'rar', '7z'];
|
|
7
|
+
const PDF_EXTENSION = 'pdf';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Extensions that must never be accepted as media uploads, regardless of declared mimetype or
|
|
11
|
+
* admin-configured mediaTypes allowlist. Checked before any other resolution so a spoofed
|
|
12
|
+
* Content-Type (e.g. application/octet-stream on an .html file) can't bypass this.
|
|
13
|
+
*/
|
|
14
|
+
export const DANGEROUS_EXTENSIONS = new Set([
|
|
15
|
+
'html', 'htm', 'xhtml', 'svg', 'xml',
|
|
16
|
+
'js', 'mjs', 'php', 'phtml', 'jsp', 'asp', 'aspx',
|
|
17
|
+
'exe', 'sh', 'bat', 'cmd', 'ps1', 'jar', 'dll',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Single source of truth for extension -> coarse media category. Shared by upload-time
|
|
22
|
+
* validation (MediaFieldCrudManager) and serve-time header hardening (ServeStaticModule), so
|
|
23
|
+
* both agree on which extensions belong to which category and neither drifts out of sync.
|
|
24
|
+
* webm/ogg are shared between AUDIO_EXTENSIONS and VIDEO_EXTENSIONS - last one wins here, they
|
|
25
|
+
* default to 'video' since that's the more common upload case.
|
|
26
|
+
*/
|
|
27
|
+
export const EXT_TO_MEDIA_TYPE: Record<string, MediaCategory> = {
|
|
28
|
+
...Object.fromEntries(IMAGE_EXTENSIONS.map((ext) => [ext, 'image' as const])),
|
|
29
|
+
...Object.fromEntries(AUDIO_EXTENSIONS.map((ext) => [ext, 'audio' as const])),
|
|
30
|
+
...Object.fromEntries(VIDEO_EXTENSIONS.map((ext) => [ext, 'video' as const])),
|
|
31
|
+
...Object.fromEntries(DOCUMENT_EXTENSIONS.map((ext) => [ext, 'file' as const])),
|
|
32
|
+
[PDF_EXTENSION]: 'pdf',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Extensions safe to serve inline with their natural Content-Type; everything else served from
|
|
37
|
+
* media-files-storage is forced to application/octet-stream + Content-Disposition: attachment.
|
|
38
|
+
*/
|
|
39
|
+
export const INLINE_SAFE_EXTENSIONS = new Set<string>([
|
|
40
|
+
...IMAGE_EXTENSIONS,
|
|
41
|
+
...AUDIO_EXTENSIONS,
|
|
42
|
+
...VIDEO_EXTENSIONS,
|
|
43
|
+
PDF_EXTENSION,
|
|
44
|
+
]);
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query, UploadedFiles, UseInterceptors } from '@nestjs/common';
|
|
1
|
+
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query, Res, UploadedFiles, UseInterceptors } from '@nestjs/common';
|
|
2
2
|
import { AnyFilesInterceptor } from "@nestjs/platform-express";
|
|
3
3
|
import { ApiBearerAuth, ApiQuery, ApiTags } from '@nestjs/swagger';
|
|
4
|
-
import {
|
|
4
|
+
import { Auth } from 'src/decorators/auth.decorator';
|
|
5
|
+
import { AuthType } from 'src/enums/auth-type.enum';
|
|
5
6
|
import { CreateMediaDto } from 'src/dtos/create-media.dto';
|
|
7
|
+
import { SolidRequestContextDecorator } from 'src/decorators/solid-request-context.decorator';
|
|
8
|
+
import { SolidRequestContextDto } from 'src/dtos/solid-request-context.dto';
|
|
6
9
|
import { UpdateMediaDto } from 'src/dtos/update-media.dto';
|
|
7
10
|
import { MediaService } from 'src/services/media.service';
|
|
11
|
+
import { Response } from 'express';
|
|
8
12
|
|
|
9
13
|
enum ShowSoftDeleted {
|
|
10
14
|
INCLUSIVE = "inclusive",
|
|
@@ -21,30 +25,30 @@ export class MediaController {
|
|
|
21
25
|
@ApiBearerAuth("jwt")
|
|
22
26
|
@Post()
|
|
23
27
|
@UseInterceptors(AnyFilesInterceptor())
|
|
24
|
-
create(@Body() createDto: CreateMediaDto, @UploadedFiles() files: Array<Express.Multer.File
|
|
25
|
-
return this.service.create(createDto, files);
|
|
28
|
+
create(@Body() createDto: CreateMediaDto, @UploadedFiles() files: Array<Express.Multer.File>, @SolidRequestContextDecorator() solidRequestContext: SolidRequestContextDto) {
|
|
29
|
+
return this.service.create(createDto, files, solidRequestContext);
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
@ApiBearerAuth("jwt")
|
|
29
33
|
@Post('/bulk')
|
|
30
34
|
@UseInterceptors(AnyFilesInterceptor())
|
|
31
|
-
insertMany(@Body() createDtos: CreateMediaDto[], @UploadedFiles() filesArray: Express.Multer.File[][] = []) {
|
|
32
|
-
return this.service.insertMany(createDtos, filesArray);
|
|
35
|
+
insertMany(@Body() createDtos: CreateMediaDto[], @UploadedFiles() filesArray: Express.Multer.File[][] = [], @SolidRequestContextDecorator() solidRequestContext: SolidRequestContextDto) {
|
|
36
|
+
return this.service.insertMany(createDtos, filesArray, solidRequestContext);
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
|
|
36
40
|
@ApiBearerAuth("jwt")
|
|
37
41
|
@Put(':id')
|
|
38
42
|
@UseInterceptors(AnyFilesInterceptor())
|
|
39
|
-
update(@Param('id') id: number, @Body() updateDto: UpdateMediaDto, @UploadedFiles() files: Array<Express.Multer.File
|
|
40
|
-
return this.service.update(id, updateDto, files);
|
|
43
|
+
update(@Param('id') id: number, @Body() updateDto: UpdateMediaDto, @UploadedFiles() files: Array<Express.Multer.File>, @SolidRequestContextDecorator() solidRequestContext: SolidRequestContextDto) {
|
|
44
|
+
return this.service.update(id, updateDto, files, false, solidRequestContext);
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
@ApiBearerAuth("jwt")
|
|
44
48
|
@Patch(':id')
|
|
45
49
|
@UseInterceptors(AnyFilesInterceptor())
|
|
46
|
-
partialUpdate(@Param('id') id: number, @Body() updateDto: UpdateMediaDto, @UploadedFiles() files: Array<Express.Multer.File
|
|
47
|
-
return this.service.update(id, updateDto, files, true);
|
|
50
|
+
partialUpdate(@Param('id') id: number, @Body() updateDto: UpdateMediaDto, @UploadedFiles() files: Array<Express.Multer.File>, @SolidRequestContextDecorator() solidRequestContext: SolidRequestContextDto) {
|
|
51
|
+
return this.service.update(id, updateDto, files, true, solidRequestContext);
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
// @Public()
|
|
@@ -52,22 +56,22 @@ export class MediaController {
|
|
|
52
56
|
@ApiBearerAuth("jwt")
|
|
53
57
|
@Post('/upload')
|
|
54
58
|
@UseInterceptors(AnyFilesInterceptor())
|
|
55
|
-
upload(@UploadedFiles() files: Array<Express.Multer.File>, @Body() createDto: CreateMediaDto, ) {
|
|
59
|
+
upload(@UploadedFiles() files: Array<Express.Multer.File>, @Body() createDto: CreateMediaDto, @SolidRequestContextDecorator() solidRequestContext: SolidRequestContextDto) {
|
|
56
60
|
// this.logger.log(`Creating a new model: ${JSON.stringify(createDto)}`);
|
|
57
|
-
return this.service.upload(createDto, files);
|
|
61
|
+
return this.service.upload(createDto, files, solidRequestContext);
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
|
|
61
65
|
@ApiBearerAuth("jwt")
|
|
62
66
|
@Post('/bulk-recover')
|
|
63
|
-
async recoverMany(@Body() ids: number[]) {
|
|
64
|
-
return this.service.recoverMany(ids);
|
|
67
|
+
async recoverMany(@Body() ids: number[], @SolidRequestContextDecorator() solidRequestContext: SolidRequestContextDto) {
|
|
68
|
+
return this.service.recoverMany(ids, solidRequestContext);
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
@ApiBearerAuth("jwt")
|
|
68
72
|
@Get('/recover/:id')
|
|
69
|
-
async recover(@Param('id') id: number) {
|
|
70
|
-
return this.service.recover(id);
|
|
73
|
+
async recover(@Param('id') id: number, @SolidRequestContextDecorator() solidRequestContext: SolidRequestContextDto) {
|
|
74
|
+
return this.service.recover(id, solidRequestContext);
|
|
71
75
|
}
|
|
72
76
|
|
|
73
77
|
@ApiBearerAuth("jwt")
|
|
@@ -81,26 +85,42 @@ export class MediaController {
|
|
|
81
85
|
@ApiQuery({ name: 'populateMedia', required: false, type: Array })
|
|
82
86
|
@ApiQuery({ name: 'filters', required: false, type: Array })
|
|
83
87
|
@Get()
|
|
84
|
-
async findMany(@Query() query: any) {
|
|
85
|
-
return this.service.find(query);
|
|
88
|
+
async findMany(@Query() query: any, @SolidRequestContextDecorator() solidRequestContext: SolidRequestContextDto) {
|
|
89
|
+
return this.service.find(query, solidRequestContext);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@Auth(AuthType.MediaSignedUrl)
|
|
93
|
+
@ApiQuery({ name: 'token', required: true, type: String, description: "Short-lived signed token obtained from a private Media record's URL - not entered manually." })
|
|
94
|
+
@ApiQuery({ name: 'disposition', required: false, enum: ['inline', 'attachment'], description: "'inline' (default) renders/previews in the browser, matching public media. Pass 'attachment' to force a browser download (e.g. from a download button)." })
|
|
95
|
+
@Get(':id/download')
|
|
96
|
+
async download(@Param('id') id: string, @Query('disposition') disposition: string, @Res() res: Response, @SolidRequestContextDecorator() solidRequestContext: SolidRequestContextDto) {
|
|
97
|
+
const media = await this.service.findOne(+id, {}, solidRequestContext);
|
|
98
|
+
const { stream, fileName, mimeType } = await this.service.fileDownloadStream(media);
|
|
99
|
+
|
|
100
|
+
const contentDisposition = disposition === 'attachment' ? 'attachment' : 'inline';
|
|
101
|
+
res.setHeader('Content-Disposition', `${contentDisposition}; filename="${fileName}"`);
|
|
102
|
+
res.setHeader('Content-Type', mimeType);
|
|
103
|
+
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
|
|
104
|
+
res.setHeader('Access-Control-Expose-Headers', 'Content-Disposition, Content-Type');
|
|
105
|
+
stream.pipe(res);
|
|
86
106
|
}
|
|
87
107
|
|
|
88
108
|
@ApiBearerAuth("jwt")
|
|
89
109
|
@Get(':id')
|
|
90
|
-
async findOne(@Param('id') id: string, @Query() query: any) {
|
|
91
|
-
return this.service.findOne(+id, query);
|
|
110
|
+
async findOne(@Param('id') id: string, @Query() query: any, @SolidRequestContextDecorator() solidRequestContext: SolidRequestContextDto) {
|
|
111
|
+
return this.service.findOne(+id, query, solidRequestContext);
|
|
92
112
|
}
|
|
93
113
|
|
|
94
114
|
@ApiBearerAuth("jwt")
|
|
95
115
|
@Delete('/bulk')
|
|
96
|
-
async deleteMany(@Body() ids: number[]) {
|
|
97
|
-
return this.service.deleteMany(ids);
|
|
116
|
+
async deleteMany(@Body() ids: number[], @SolidRequestContextDecorator() solidRequestContext: SolidRequestContextDto) {
|
|
117
|
+
return this.service.deleteMany(ids, solidRequestContext);
|
|
98
118
|
}
|
|
99
119
|
|
|
100
120
|
@ApiBearerAuth("jwt")
|
|
101
121
|
@Delete(':id')
|
|
102
|
-
async delete(@Param('id') id: number) {
|
|
103
|
-
return this.service.delete(id);
|
|
122
|
+
async delete(@Param('id') id: number, @SolidRequestContextDecorator() solidRequestContext: SolidRequestContextDto) {
|
|
123
|
+
return this.service.delete(id, solidRequestContext);
|
|
104
124
|
}
|
|
105
125
|
|
|
106
126
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ApiProperty } from '@nestjs/swagger';
|
|
2
|
-
import { IsInt } from 'class-validator';
|
|
1
|
+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
2
|
+
import { IsBoolean, IsInt } from 'class-validator';
|
|
3
3
|
import { IsNotEmpty, IsString, IsOptional } from 'class-validator';
|
|
4
4
|
export class CreateMediaDto {
|
|
5
5
|
@IsNotEmpty()
|
|
@@ -7,6 +7,11 @@ export class CreateMediaDto {
|
|
|
7
7
|
@ApiProperty()
|
|
8
8
|
entityId: number;
|
|
9
9
|
|
|
10
|
+
@IsOptional()
|
|
11
|
+
@IsBoolean()
|
|
12
|
+
@ApiPropertyOptional({ default: true })
|
|
13
|
+
isPublic?: boolean;
|
|
14
|
+
|
|
10
15
|
@IsOptional()
|
|
11
16
|
@IsString()
|
|
12
17
|
@ApiProperty()
|
|
@@ -56,4 +61,4 @@ fieldMetadataId: number;
|
|
|
56
61
|
@IsOptional()
|
|
57
62
|
@ApiProperty()
|
|
58
63
|
fieldMetadataUserKey: string;
|
|
59
|
-
}
|
|
64
|
+
}
|
|
@@ -8,6 +8,8 @@ import { ApiProperty } from '@nestjs/swagger';
|
|
|
8
8
|
|
|
9
9
|
export const ADMIN_ROLE_NAME = 'Admin';
|
|
10
10
|
export const INTERNAL_ROLE_NAME = 'Internal User';
|
|
11
|
+
export const ALLOWED_TO_IMPORT_ROLE_NAME = 'Allowed To Import';
|
|
12
|
+
export const ALLOWED_TO_EXPORT_ROLE_NAME = 'Allowed To Export';
|
|
11
13
|
export const PUBLIC_ROLE_NAME = 'Public';
|
|
12
14
|
|
|
13
15
|
export class CreateRoleMetadataDto {
|
|
@@ -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
|
}
|
package/src/interfaces.ts
CHANGED
|
@@ -228,6 +228,8 @@ export interface ISolidDatabaseModule {
|
|
|
228
228
|
|
|
229
229
|
export enum EventType {
|
|
230
230
|
USER_REGISTERED = 'user.registered',
|
|
231
|
+
MODULE_METADATA_SEEDER_STARTED = 'module-metadata-seeder.started',
|
|
232
|
+
MODULE_METADATA_SEEDER_FINISHED = 'module-metadata-seeder.finished',
|
|
231
233
|
}
|
|
232
234
|
|
|
233
235
|
export class EventDetails<T> {
|
|
@@ -237,6 +239,24 @@ export class EventDetails<T> {
|
|
|
237
239
|
) { }
|
|
238
240
|
}
|
|
239
241
|
|
|
242
|
+
export interface ModuleMetadataSeederOptions {
|
|
243
|
+
modulesToSeed: string[] | null;
|
|
244
|
+
pruneMetadata: boolean;
|
|
245
|
+
seedGlobalMetadata: boolean;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export interface ModuleMetadataSeederEventPayload {
|
|
249
|
+
seedRunId: string;
|
|
250
|
+
options: ModuleMetadataSeederOptions;
|
|
251
|
+
startedAt: string;
|
|
252
|
+
finishedAt?: string;
|
|
253
|
+
durationMs?: number;
|
|
254
|
+
success?: boolean;
|
|
255
|
+
seededModuleNames?: string[];
|
|
256
|
+
currentStep?: string;
|
|
257
|
+
errorMessage?: string;
|
|
258
|
+
}
|
|
259
|
+
|
|
240
260
|
export interface IExtensionUserCreationProvider<T extends User = User, TDto extends CreateUserDto = CreateUserDto> {
|
|
241
261
|
readonly repo: Repository<T>;
|
|
242
262
|
buildExtensionEntity(dto: TDto): Promise<T>;
|
|
@@ -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
|
}
|