@solidxai/core 0.1.13-beta.9 → 0.1.14-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9244 -0
- package/README.md +19 -0
- package/dist/commands/run-tests.command.d.ts +2 -0
- package/dist/commands/run-tests.command.d.ts.map +1 -1
- package/dist/commands/run-tests.command.js +20 -3
- package/dist/commands/run-tests.command.js.map +1 -1
- package/dist/config/cache.options.d.ts.map +1 -1
- package/dist/config/cache.options.js +2 -2
- package/dist/config/cache.options.js.map +1 -1
- package/dist/constants/error-messages.d.ts +7 -0
- package/dist/constants/error-messages.d.ts.map +1 -1
- package/dist/constants/error-messages.js +7 -0
- package/dist/constants/error-messages.js.map +1 -1
- package/dist/constants/media-file-types.d.ts +17 -0
- package/dist/constants/media-file-types.d.ts.map +1 -1
- package/dist/constants/media-file-types.js +129 -22
- package/dist/constants/media-file-types.js.map +1 -1
- package/dist/constants/success-messages.d.ts +3 -0
- package/dist/constants/success-messages.d.ts.map +1 -1
- package/dist/constants/success-messages.js +3 -0
- package/dist/constants/success-messages.js.map +1 -1
- package/dist/controllers/authentication.controller.d.ts +1 -1
- package/dist/controllers/authentication.controller.d.ts.map +1 -1
- package/dist/controllers/authentication.controller.js +4 -3
- package/dist/controllers/authentication.controller.js.map +1 -1
- package/dist/controllers/field-metadata.controller.d.ts +6 -2
- package/dist/controllers/field-metadata.controller.d.ts.map +1 -1
- package/dist/controllers/mpin-authentication.controller.d.ts +44 -0
- package/dist/controllers/mpin-authentication.controller.d.ts.map +1 -0
- package/dist/controllers/mpin-authentication.controller.js +102 -0
- package/dist/controllers/mpin-authentication.controller.js.map +1 -0
- package/dist/controllers/view-metadata.controller.d.ts +4 -0
- package/dist/controllers/view-metadata.controller.d.ts.map +1 -1
- package/dist/dtos/change-mpin.dto.d.ts +6 -0
- package/dist/dtos/change-mpin.dto.d.ts.map +1 -0
- package/dist/dtos/change-mpin.dto.js +39 -0
- package/dist/dtos/change-mpin.dto.js.map +1 -0
- package/dist/dtos/create-field-metadata.dto.d.ts +3 -6
- package/dist/dtos/create-field-metadata.dto.d.ts.map +1 -1
- package/dist/dtos/create-field-metadata.dto.js +15 -10
- package/dist/dtos/create-field-metadata.dto.js.map +1 -1
- package/dist/dtos/create-model-sequence.dto.d.ts +1 -1
- package/dist/dtos/create-model-sequence.dto.d.ts.map +1 -1
- package/dist/dtos/create-model-sequence.dto.js +1 -1
- package/dist/dtos/create-model-sequence.dto.js.map +1 -1
- package/dist/dtos/mpin-login.dto.d.ts +5 -0
- package/dist/dtos/mpin-login.dto.d.ts.map +1 -0
- package/dist/dtos/mpin-login.dto.js +33 -0
- package/dist/dtos/mpin-login.dto.js.map +1 -0
- package/dist/dtos/setup-mpin.dto.d.ts +7 -0
- package/dist/dtos/setup-mpin.dto.d.ts.map +1 -0
- package/dist/dtos/setup-mpin.dto.js +47 -0
- package/dist/dtos/setup-mpin.dto.js.map +1 -0
- package/dist/entities/dashboard-user-layout.entity.js +2 -2
- package/dist/entities/dashboard-user-layout.entity.js.map +1 -1
- package/dist/entities/field-metadata.entity.d.ts +1 -0
- package/dist/entities/field-metadata.entity.d.ts.map +1 -1
- package/dist/entities/field-metadata.entity.js +5 -1
- package/dist/entities/field-metadata.entity.js.map +1 -1
- package/dist/entities/user-device-credential.entity.d.ts +18 -0
- package/dist/entities/user-device-credential.entity.d.ts.map +1 -0
- package/dist/entities/user-device-credential.entity.js +91 -0
- package/dist/entities/user-device-credential.entity.js.map +1 -0
- package/dist/entities/user.entity.js +2 -2
- package/dist/entities/user.entity.js.map +1 -1
- package/dist/entities/view-metadata.entity.js.map +1 -1
- package/dist/entities/workflow-execution-artifact.entity.d.ts.map +1 -1
- package/dist/entities/workflow-execution-artifact.entity.js +4 -3
- package/dist/entities/workflow-execution-artifact.entity.js.map +1 -1
- package/dist/entities/workflow-execution-log.entity.d.ts.map +1 -1
- package/dist/entities/workflow-execution-log.entity.js +4 -3
- package/dist/entities/workflow-execution-log.entity.js.map +1 -1
- package/dist/entities/workflow-execution.entity.d.ts.map +1 -1
- package/dist/entities/workflow-execution.entity.js +3 -2
- package/dist/entities/workflow-execution.entity.js.map +1 -1
- package/dist/entities/workflow-secret.entity.d.ts.map +1 -1
- package/dist/entities/workflow-secret.entity.js +3 -2
- package/dist/entities/workflow-secret.entity.js.map +1 -1
- package/dist/entities/workflow-step-execution.entity.d.ts.map +1 -1
- package/dist/entities/workflow-step-execution.entity.js +3 -2
- package/dist/entities/workflow-step-execution.entity.js.map +1 -1
- package/dist/entities/workflow-trigger-execution.entity.d.ts.map +1 -1
- package/dist/entities/workflow-trigger-execution.entity.js +6 -5
- package/dist/entities/workflow-trigger-execution.entity.js.map +1 -1
- package/dist/filters/http-exception.filter.d.ts.map +1 -1
- package/dist/filters/http-exception.filter.js +5 -1
- package/dist/filters/http-exception.filter.js.map +1 -1
- package/dist/helpers/bootstrap.helper.d.ts +5 -2
- package/dist/helpers/bootstrap.helper.d.ts.map +1 -1
- package/dist/helpers/bootstrap.helper.js +7 -3
- package/dist/helpers/bootstrap.helper.js.map +1 -1
- package/dist/helpers/field-crud-managers/LongTextFieldCrudManager.d.ts.map +1 -1
- package/dist/helpers/field-crud-managers/LongTextFieldCrudManager.js +3 -2
- package/dist/helpers/field-crud-managers/LongTextFieldCrudManager.js.map +1 -1
- package/dist/helpers/field-crud-managers/MediaFieldCrudManager.d.ts +8 -0
- package/dist/helpers/field-crud-managers/MediaFieldCrudManager.d.ts.map +1 -1
- package/dist/helpers/field-crud-managers/MediaFieldCrudManager.js +61 -76
- package/dist/helpers/field-crud-managers/MediaFieldCrudManager.js.map +1 -1
- package/dist/helpers/field-crud-managers/ShortTextFieldCrudManager.d.ts.map +1 -1
- package/dist/helpers/field-crud-managers/ShortTextFieldCrudManager.js +3 -2
- package/dist/helpers/field-crud-managers/ShortTextFieldCrudManager.js.map +1 -1
- package/dist/helpers/field-crud-managers/textFieldCrudManagerUtils.d.ts +3 -0
- package/dist/helpers/field-crud-managers/textFieldCrudManagerUtils.d.ts.map +1 -0
- package/dist/helpers/field-crud-managers/textFieldCrudManagerUtils.js +17 -0
- package/dist/helpers/field-crud-managers/textFieldCrudManagerUtils.js.map +1 -0
- package/dist/helpers/file-download.helper.d.ts.map +1 -1
- package/dist/helpers/file-download.helper.js +6 -1
- package/dist/helpers/file-download.helper.js.map +1 -1
- package/dist/helpers/security.helper.d.ts +5 -1
- package/dist/helpers/security.helper.d.ts.map +1 -1
- package/dist/helpers/security.helper.js +15 -4
- package/dist/helpers/security.helper.js.map +1 -1
- package/dist/helpers/solid-core-error-codes-provider.service.d.ts.map +1 -1
- package/dist/helpers/solid-core-error-codes-provider.service.js +45 -0
- package/dist/helpers/solid-core-error-codes-provider.service.js.map +1 -1
- package/dist/helpers/solid-microservice-adapter.service.d.ts.map +1 -1
- package/dist/helpers/solid-microservice-adapter.service.js +1 -1
- package/dist/helpers/solid-microservice-adapter.service.js.map +1 -1
- package/dist/helpers/typeorm-db-helper.d.ts.map +1 -1
- package/dist/helpers/typeorm-db-helper.js +9 -0
- package/dist/helpers/typeorm-db-helper.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/interfaces/active-user-data.interface.d.ts +1 -0
- package/dist/interfaces/active-user-data.interface.d.ts.map +1 -1
- package/dist/interfaces/active-user-data.interface.js.map +1 -1
- package/dist/repository/user-device-credential.repository.d.ts +12 -0
- package/dist/repository/user-device-credential.repository.d.ts.map +1 -0
- package/dist/repository/user-device-credential.repository.js +34 -0
- package/dist/repository/user-device-credential.repository.js.map +1 -0
- package/dist/seeders/module-metadata-seeder.service.d.ts.map +1 -1
- package/dist/seeders/module-metadata-seeder.service.js +7 -3
- package/dist/seeders/module-metadata-seeder.service.js.map +1 -1
- package/dist/seeders/seed-data/solid-core-metadata.json +168 -2
- package/dist/services/api-key.service.d.ts.map +1 -1
- package/dist/services/api-key.service.js +1 -1
- package/dist/services/api-key.service.js.map +1 -1
- package/dist/services/authentication.service.d.ts +5 -4
- package/dist/services/authentication.service.d.ts.map +1 -1
- package/dist/services/authentication.service.js +50 -19
- package/dist/services/authentication.service.js.map +1 -1
- package/dist/services/chatter-message.service.d.ts +5 -1
- package/dist/services/chatter-message.service.d.ts.map +1 -1
- package/dist/services/chatter-message.service.js +48 -13
- package/dist/services/chatter-message.service.js.map +1 -1
- package/dist/services/crud-helper.service.d.ts +1 -2
- package/dist/services/crud-helper.service.d.ts.map +1 -1
- package/dist/services/crud-helper.service.js +2 -16
- package/dist/services/crud-helper.service.js.map +1 -1
- package/dist/services/crud.service.d.ts.map +1 -1
- package/dist/services/crud.service.js +4 -2
- package/dist/services/crud.service.js.map +1 -1
- package/dist/services/draft-publish-helper.service.d.ts +1 -0
- package/dist/services/draft-publish-helper.service.d.ts.map +1 -1
- package/dist/services/draft-publish-helper.service.js +38 -4
- package/dist/services/draft-publish-helper.service.js.map +1 -1
- package/dist/services/field-metadata.service.d.ts +6 -2
- package/dist/services/field-metadata.service.d.ts.map +1 -1
- package/dist/services/field-metadata.service.js +5 -1
- package/dist/services/field-metadata.service.js.map +1 -1
- package/dist/services/media-storage.utils.d.ts +1 -0
- package/dist/services/media-storage.utils.d.ts.map +1 -1
- package/dist/services/media-storage.utils.js +12 -1
- package/dist/services/media-storage.utils.js.map +1 -1
- package/dist/services/media.service.d.ts.map +1 -1
- package/dist/services/media.service.js +1 -0
- package/dist/services/media.service.js.map +1 -1
- package/dist/services/module-metadata.service.d.ts.map +1 -1
- package/dist/services/module-metadata.service.js +9 -1
- package/dist/services/module-metadata.service.js.map +1 -1
- package/dist/services/mpin.service.d.ts +67 -0
- package/dist/services/mpin.service.d.ts.map +1 -0
- package/dist/services/mpin.service.js +282 -0
- package/dist/services/mpin.service.js.map +1 -0
- package/dist/services/refresh-token-ids-storage.service.d.ts +25 -6
- package/dist/services/refresh-token-ids-storage.service.d.ts.map +1 -1
- package/dist/services/refresh-token-ids-storage.service.js +80 -42
- package/dist/services/refresh-token-ids-storage.service.js.map +1 -1
- package/dist/services/setting.service.d.ts.map +1 -1
- package/dist/services/setting.service.js +6 -1
- package/dist/services/setting.service.js.map +1 -1
- package/dist/services/settings/default-settings-provider.service.d.ts +202 -6
- package/dist/services/settings/default-settings-provider.service.d.ts.map +1 -1
- package/dist/services/settings/default-settings-provider.service.js +114 -3
- package/dist/services/settings/default-settings-provider.service.js.map +1 -1
- package/dist/services/view-metadata.service.d.ts +4 -0
- package/dist/services/view-metadata.service.d.ts.map +1 -1
- package/dist/services/view-metadata.service.js +2 -0
- package/dist/services/view-metadata.service.js.map +1 -1
- package/dist/solid-core.module.d.ts +1 -3
- package/dist/solid-core.module.d.ts.map +1 -1
- package/dist/solid-core.module.js +14 -39
- package/dist/solid-core.module.js.map +1 -1
- package/dist/testing/reporter/console-reporter.d.ts +3 -0
- package/dist/testing/reporter/console-reporter.d.ts.map +1 -1
- package/dist/testing/reporter/console-reporter.js +2 -0
- package/dist/testing/reporter/console-reporter.js.map +1 -1
- package/dist/testing/reporter/progress-reporter.d.ts +64 -0
- package/dist/testing/reporter/progress-reporter.d.ts.map +1 -0
- package/dist/testing/reporter/progress-reporter.js +224 -0
- package/dist/testing/reporter/progress-reporter.js.map +1 -0
- package/dist/testing/reporter/reporter.types.d.ts +3 -0
- package/dist/testing/reporter/reporter.types.d.ts.map +1 -1
- package/dist/testing/reporter/reporter.types.js.map +1 -1
- package/dist/testing/runner/run-from-metadata.d.ts.map +1 -1
- package/dist/testing/runner/run-from-metadata.js +1 -0
- package/dist/testing/runner/run-from-metadata.js.map +1 -1
- package/dist/theme/theme-registry.d.ts +14 -0
- package/dist/theme/theme-registry.d.ts.map +1 -0
- package/dist/theme/theme-registry.js +147 -0
- package/dist/theme/theme-registry.js.map +1 -0
- package/package.json +2 -2
- package/src/commands/run-tests.command.ts +17 -3
- package/src/config/cache.options.ts +9 -1
- package/src/constants/error-messages.ts +14 -0
- package/src/constants/media-file-types.ts +182 -19
- package/src/constants/success-messages.ts +5 -0
- package/src/controllers/authentication.controller.ts +5 -2
- package/src/controllers/mpin-authentication.controller.ts +67 -0
- package/src/dtos/change-mpin.dto.ts +18 -0
- package/src/dtos/create-field-metadata.dto.ts +17 -9
- package/src/dtos/create-model-sequence.dto.ts +1 -1
- package/src/dtos/mpin-login.dto.ts +24 -0
- package/src/dtos/setup-mpin.dto.ts +51 -0
- package/src/entities/dashboard-user-layout.entity.ts +2 -2
- package/src/entities/field-metadata.entity.ts +3 -0
- package/src/entities/user-device-credential.entity.ts +130 -0
- package/src/entities/user.entity.ts +2 -2
- package/src/entities/view-metadata.entity.ts +2 -2
- package/src/entities/workflow-execution-artifact.entity.ts +4 -3
- package/src/entities/workflow-execution-log.entity.ts +4 -3
- package/src/entities/workflow-execution.entity.ts +3 -2
- package/src/entities/workflow-secret.entity.ts +3 -2
- package/src/entities/workflow-step-execution.entity.ts +3 -2
- package/src/entities/workflow-trigger-execution.entity.ts +6 -5
- package/src/filters/http-exception.filter.ts +11 -1
- package/src/helpers/bootstrap.helper.ts +43 -6
- package/src/helpers/field-crud-managers/LongTextFieldCrudManager.ts +3 -2
- package/src/helpers/field-crud-managers/MediaFieldCrudManager.ts +83 -96
- package/src/helpers/field-crud-managers/ShortTextFieldCrudManager.ts +3 -2
- package/src/helpers/field-crud-managers/textFieldCrudManagerUtils.ts +14 -0
- package/src/helpers/file-download.helper.ts +15 -1
- package/src/helpers/security.helper.ts +35 -8
- package/src/helpers/solid-core-error-codes-provider.service.ts +53 -0
- package/src/helpers/solid-microservice-adapter.service.ts +6 -1
- package/src/helpers/typeorm-db-helper.ts +12 -1
- package/src/index.ts +6 -0
- package/src/interfaces/active-user-data.interface.ts +9 -0
- package/src/repository/user-device-credential.repository.ts +17 -0
- package/src/seeders/module-metadata-seeder.service.ts +11 -3
- package/src/seeders/seed-data/solid-core-metadata.json +168 -2
- package/src/services/api-key.service.ts +13 -1
- package/src/services/authentication.service.ts +127 -36
- package/src/services/chatter-message.service.ts +78 -15
- package/src/services/crud-helper.service.ts +19 -16
- package/src/services/crud.service.ts +5 -2
- package/src/services/draft-publish-helper.service.ts +56 -4
- package/src/services/field-metadata.service.ts +6 -2
- package/src/services/media-storage.utils.ts +26 -1
- package/src/services/media.service.ts +1 -0
- package/src/services/module-metadata.service.ts +10 -1
- package/src/services/mpin.service.ts +447 -0
- package/src/services/refresh-token-ids-storage.service.ts +229 -90
- package/src/services/setting.service.ts +8 -1
- package/src/services/settings/default-settings-provider.service.ts +119 -3
- package/src/services/view-metadata.service.ts +4 -0
- package/src/solid-core.module.ts +26 -12
- package/src/testing/README.md +2 -1
- package/src/testing/reporter/console-reporter.ts +4 -0
- package/src/testing/reporter/progress-reporter.ts +251 -0
- package/src/testing/reporter/reporter.types.ts +3 -0
- package/src/testing/runner/run-from-metadata.ts +1 -0
- package/src/theme/theme-registry.ts +181 -0
- package/CLAUDE.md +0 -26
|
@@ -1,10 +1,101 @@
|
|
|
1
1
|
export type MediaCategory = 'image' | 'audio' | 'video' | 'file' | 'pdf';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
export const MEDIA_CATEGORIES = ['image', 'audio', 'video', 'file', 'pdf'] as const;
|
|
4
|
+
|
|
5
|
+
export type MediaFileTypeDefinition = {
|
|
6
|
+
mediaType: MediaCategory;
|
|
7
|
+
mimeType: string;
|
|
8
|
+
extension: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// Single source of truth for every supported media extension, its canonical MIME type,
|
|
12
|
+
// and the coarse media category used across validation and UI metadata.
|
|
13
|
+
export const MEDIA_FILE_TYPES: MediaFileTypeDefinition[] = [
|
|
14
|
+
{ mediaType: 'image', mimeType: 'image/png', extension: 'png' },
|
|
15
|
+
{ mediaType: 'image', mimeType: 'image/jpeg', extension: 'jpg' },
|
|
16
|
+
{ mediaType: 'image', mimeType: 'image/jpg', extension: 'jpg' },
|
|
17
|
+
{ mediaType: 'image', mimeType: 'image/jpeg', extension: 'jpeg' },
|
|
18
|
+
{ mediaType: 'image', mimeType: 'image/webp', extension: 'webp' },
|
|
19
|
+
{ mediaType: 'image', mimeType: 'image/gif', extension: 'gif' },
|
|
20
|
+
{ mediaType: 'image', mimeType: 'image/bmp', extension: 'bmp' },
|
|
21
|
+
{ mediaType: 'image', mimeType: 'image/tiff', extension: 'tiff' },
|
|
22
|
+
{ mediaType: 'image', mimeType: 'image/heic', extension: 'heic' },
|
|
23
|
+
{ mediaType: 'image', mimeType: 'image/heif', extension: 'heif' },
|
|
24
|
+
|
|
25
|
+
{ mediaType: 'audio', mimeType: 'audio/mpeg', extension: 'mp3' },
|
|
26
|
+
{ mediaType: 'audio', mimeType: 'audio/mp3', extension: 'mp3' },
|
|
27
|
+
{ mediaType: 'audio', mimeType: 'audio/wav', extension: 'wav' },
|
|
28
|
+
{ mediaType: 'audio', mimeType: 'audio/x-wav', extension: 'wav' },
|
|
29
|
+
{ mediaType: 'audio', mimeType: 'audio/ogg', extension: 'ogg' },
|
|
30
|
+
{ mediaType: 'audio', mimeType: 'audio/aac', extension: 'aac' },
|
|
31
|
+
{ mediaType: 'audio', mimeType: 'audio/mp4', extension: 'm4a' },
|
|
32
|
+
{ mediaType: 'audio', mimeType: 'audio/x-m4a', extension: 'm4a' },
|
|
33
|
+
{ mediaType: 'audio', mimeType: 'audio/flac', extension: 'flac' },
|
|
34
|
+
{ mediaType: 'audio', mimeType: 'audio/webm', extension: 'webm' },
|
|
35
|
+
|
|
36
|
+
{ mediaType: 'video', mimeType: 'video/mp4', extension: 'mp4' },
|
|
37
|
+
{ mediaType: 'video', mimeType: 'video/quicktime', extension: 'mov' },
|
|
38
|
+
{ mediaType: 'video', mimeType: 'video/x-msvideo', extension: 'avi' },
|
|
39
|
+
{ mediaType: 'video', mimeType: 'video/x-matroska', extension: 'mkv' },
|
|
40
|
+
{ mediaType: 'video', mimeType: 'video/mpeg', extension: 'mpeg' },
|
|
41
|
+
{ mediaType: 'video', mimeType: 'video/mpeg', extension: 'mpg' },
|
|
42
|
+
{ mediaType: 'video', mimeType: 'video/3gpp', extension: '3gp' },
|
|
43
|
+
{ mediaType: 'video', mimeType: 'video/3gpp2', extension: '3g2' },
|
|
44
|
+
{ mediaType: 'video', mimeType: 'video/webm', extension: 'webm' },
|
|
45
|
+
{ mediaType: 'video', mimeType: 'video/ogg', extension: 'ogg' },
|
|
46
|
+
|
|
47
|
+
{ mediaType: 'file', mimeType: 'text/plain', extension: 'txt' },
|
|
48
|
+
{ mediaType: 'file', mimeType: 'text/markdown', extension: 'md' },
|
|
49
|
+
{ mediaType: 'file', mimeType: 'text/csv', extension: 'csv' },
|
|
50
|
+
{ mediaType: 'file', mimeType: 'application/json', extension: 'json' },
|
|
51
|
+
{ mediaType: 'file', mimeType: 'application/msword', extension: 'doc' },
|
|
52
|
+
{ mediaType: 'file', mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', extension: 'docx' },
|
|
53
|
+
{ mediaType: 'file', mimeType: 'application/vnd.ms-excel', extension: 'xls' },
|
|
54
|
+
{ mediaType: 'file', mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', extension: 'xlsx' },
|
|
55
|
+
{ mediaType: 'file', mimeType: 'application/vnd.ms-powerpoint', extension: 'ppt' },
|
|
56
|
+
{ mediaType: 'file', mimeType: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', extension: 'pptx' },
|
|
57
|
+
{ mediaType: 'file', mimeType: 'application/zip', extension: 'zip' },
|
|
58
|
+
{ mediaType: 'file', mimeType: 'application/x-zip-compressed', extension: 'zip' },
|
|
59
|
+
{ mediaType: 'file', mimeType: 'application/x-rar-compressed', extension: 'rar' },
|
|
60
|
+
{ mediaType: 'file', mimeType: 'application/x-7z-compressed', extension: '7z' },
|
|
61
|
+
|
|
62
|
+
{ mediaType: 'pdf', mimeType: 'application/pdf', extension: 'pdf' },
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const createMediaTypeBuckets = (): Record<MediaCategory, string[]> => ({
|
|
66
|
+
image: [],
|
|
67
|
+
audio: [],
|
|
68
|
+
video: [],
|
|
69
|
+
file: [],
|
|
70
|
+
pdf: [],
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
export const MEDIA_TYPE_TO_EXTENSIONS: Record<MediaCategory, string[]> = MEDIA_FILE_TYPES.reduce((acc, fileType) => {
|
|
74
|
+
if (!acc[fileType.mediaType].includes(fileType.extension)) {
|
|
75
|
+
acc[fileType.mediaType].push(fileType.extension);
|
|
76
|
+
}
|
|
77
|
+
return acc;
|
|
78
|
+
}, createMediaTypeBuckets());
|
|
79
|
+
|
|
80
|
+
export const IMAGE_EXTENSIONS = MEDIA_TYPE_TO_EXTENSIONS.image;
|
|
81
|
+
export const AUDIO_EXTENSIONS = MEDIA_TYPE_TO_EXTENSIONS.audio;
|
|
82
|
+
export const VIDEO_EXTENSIONS = MEDIA_TYPE_TO_EXTENSIONS.video;
|
|
83
|
+
export const DOCUMENT_EXTENSIONS = MEDIA_TYPE_TO_EXTENSIONS.file;
|
|
84
|
+
export const PDF_EXTENSION = 'pdf';
|
|
85
|
+
|
|
86
|
+
export const EXTENSION_TO_MIME_TYPE: Record<string, string> = MEDIA_FILE_TYPES.reduce((acc, fileType) => {
|
|
87
|
+
if (!(fileType.extension in acc)) {
|
|
88
|
+
acc[fileType.extension] = fileType.mimeType;
|
|
89
|
+
}
|
|
90
|
+
return acc;
|
|
91
|
+
}, {} as Record<string, string>);
|
|
92
|
+
|
|
93
|
+
export const MIME_TO_MEDIA_TYPE: Record<string, MediaCategory> = MEDIA_FILE_TYPES.reduce((acc, fileType) => {
|
|
94
|
+
if (!(fileType.mimeType in acc)) {
|
|
95
|
+
acc[fileType.mimeType] = fileType.mediaType;
|
|
96
|
+
}
|
|
97
|
+
return acc;
|
|
98
|
+
}, {} as Record<string, MediaCategory>);
|
|
8
99
|
|
|
9
100
|
/**
|
|
10
101
|
* Extensions that must never be accepted as media uploads, regardless of declared mimetype or
|
|
@@ -15,6 +106,7 @@ export const DANGEROUS_EXTENSIONS = new Set([
|
|
|
15
106
|
'html', 'htm', 'xhtml', 'svg', 'xml',
|
|
16
107
|
'js', 'mjs', 'php', 'phtml', 'jsp', 'asp', 'aspx',
|
|
17
108
|
'exe', 'sh', 'bat', 'cmd', 'ps1', 'jar', 'dll',
|
|
109
|
+
'msi', 'msix', 'msp', 'com', 'scr', 'vbs', 'vbe', 'wsf', 'wsh', 'hta', 'cpl', 'pif', 'reg',
|
|
18
110
|
]);
|
|
19
111
|
|
|
20
112
|
|
|
@@ -27,17 +119,92 @@ export const DANGEROUS_MIME_TYPES = new Set([
|
|
|
27
119
|
'image/svg+xml', 'text/html', 'application/xhtml+xml',
|
|
28
120
|
]);
|
|
29
121
|
|
|
30
|
-
|
|
122
|
+
/**
|
|
123
|
+
* Percent-decodes a filename, repeating a bounded number of times so a double-encoded payload
|
|
124
|
+
* (`%252e` -> `%2e` -> `.`) survives a single pass through a proxy/framework that already
|
|
125
|
+
* decoded once. Malformed escape sequences are left as-is rather than thrown, since this runs
|
|
126
|
+
* on attacker-controlled input and a literal `%` in a filename (`100%done.pdf`) is legal.
|
|
127
|
+
*/
|
|
128
|
+
function decodePercentEncodingSafely(name: string): string {
|
|
129
|
+
let decoded = name;
|
|
130
|
+
for (let i = 0; i < 3; i++) {
|
|
131
|
+
let next: string;
|
|
132
|
+
try {
|
|
133
|
+
next = decodeURIComponent(decoded);
|
|
134
|
+
} catch {
|
|
135
|
+
return decoded;
|
|
136
|
+
}
|
|
137
|
+
if (next === decoded) {
|
|
138
|
+
return decoded;
|
|
139
|
+
}
|
|
140
|
+
decoded = next;
|
|
141
|
+
}
|
|
142
|
+
return decoded;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Reduces a client-supplied name to the name that will actually be resolved on disk, so
|
|
147
|
+
* extension checks can't be dodged by decorating it. Each step defends a known bypass:
|
|
148
|
+
*
|
|
149
|
+
* - `evil.exe%2e` -> percent-encoding a trailing dot (or anything else) hides the real
|
|
150
|
+
* extension from naive string matching; decode before inspecting it.
|
|
151
|
+
* - `../../evil.exe` -> only the final path segment is the file name.
|
|
152
|
+
* - `evil.exe\0.png` -> a NUL truncates the name at the syscall/filesystem layer, so
|
|
153
|
+
* anything after it is invisible to the OS but visible to naive
|
|
154
|
+
* validation.
|
|
155
|
+
* - `evil.exe::$DATA` -> NTFS alternate data stream suffix; the file on disk is evil.exe.
|
|
156
|
+
* - `evil.exe.` / `.exe ` -> Windows silently strips trailing dots and spaces when resolving
|
|
157
|
+
* a path, so these land on disk as evil.exe.
|
|
158
|
+
*/
|
|
159
|
+
export function normalizeUploadedFileName(fileName?: string | null): string {
|
|
31
160
|
if (!fileName) {
|
|
32
|
-
return
|
|
161
|
+
return '';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let name = decodePercentEncodingSafely(String(fileName));
|
|
165
|
+
name = name.split(/[/\\]/).pop() ?? '';
|
|
166
|
+
|
|
167
|
+
const nulIndex = name.indexOf('\0');
|
|
168
|
+
if (nulIndex >= 0) {
|
|
169
|
+
name = name.slice(0, nulIndex);
|
|
33
170
|
}
|
|
34
171
|
|
|
35
|
-
const
|
|
36
|
-
if (
|
|
172
|
+
const streamIndex = name.indexOf(':');
|
|
173
|
+
if (streamIndex >= 0) {
|
|
174
|
+
name = name.slice(0, streamIndex);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return name.replace(/[\s.]+$/, '');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The effective extension - what the OS/browser will treat the file as. Used for media
|
|
182
|
+
* category resolution and inline-safety decisions. Always normalizes first, so
|
|
183
|
+
* `evil.exe.` resolves to `exe` rather than to nothing.
|
|
184
|
+
*/
|
|
185
|
+
export function getLowercaseFileExtension(fileName?: string | null): string | undefined {
|
|
186
|
+
const name = normalizeUploadedFileName(fileName);
|
|
187
|
+
|
|
188
|
+
const lastDotIndex = name.lastIndexOf('.');
|
|
189
|
+
if (lastDotIndex < 0 || lastDotIndex === name.length - 1) {
|
|
37
190
|
return undefined;
|
|
38
191
|
}
|
|
39
192
|
|
|
40
|
-
return
|
|
193
|
+
return name.slice(lastDotIndex + 1).toLowerCase();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Every dot-delimited segment after the stem, lowercased. The danger check uses this rather
|
|
198
|
+
* than only the effective extension so a dangerous type buried in a double extension
|
|
199
|
+
* (`evil.exe.png`, `shell.php.jpg`) is still caught - several servers and browsers resolve
|
|
200
|
+
* such names by an earlier segment than the last one.
|
|
201
|
+
*
|
|
202
|
+
* The stem is skipped so an innocuous file that merely starts with a keyword (`exe.png`)
|
|
203
|
+
* isn't rejected.
|
|
204
|
+
*/
|
|
205
|
+
export function getLowercaseFileExtensionSegments(fileName?: string | null): string[] {
|
|
206
|
+
const segments = normalizeUploadedFileName(fileName).toLowerCase().split('.');
|
|
207
|
+
return segments.slice(1).filter(segment => segment.length > 0);
|
|
41
208
|
}
|
|
42
209
|
|
|
43
210
|
/** Minimal shape of an uploaded file, so this module stays free of framework imports. */
|
|
@@ -55,12 +222,12 @@ export function getUploadedFileName(file?: UploadedFileLike | null): string {
|
|
|
55
222
|
/**
|
|
56
223
|
* Single predicate for "must never be stored". Shared by every upload entry point
|
|
57
224
|
* (MediaFieldCrudManager, chatter attachments) so the rules can't drift between them.
|
|
58
|
-
*
|
|
59
|
-
* application/octet-stream on an .html file) can't bypass it.
|
|
225
|
+
* Extensions are checked before mimetype so a spoofed Content-Type (e.g.
|
|
226
|
+
* application/octet-stream, or image/png, on an .html file) can't bypass it.
|
|
60
227
|
*/
|
|
61
228
|
export function isDangerousMediaFile(file?: UploadedFileLike | null): boolean {
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
229
|
+
const segments = getLowercaseFileExtensionSegments(getUploadedFileName(file));
|
|
230
|
+
if (segments.some(segment => DANGEROUS_EXTENSIONS.has(segment))) {
|
|
64
231
|
return true;
|
|
65
232
|
}
|
|
66
233
|
|
|
@@ -75,11 +242,7 @@ export function isDangerousMediaFile(file?: UploadedFileLike | null): boolean {
|
|
|
75
242
|
* default to 'video' since that's the more common upload case.
|
|
76
243
|
*/
|
|
77
244
|
export const EXT_TO_MEDIA_TYPE: Record<string, MediaCategory> = {
|
|
78
|
-
...Object.fromEntries(
|
|
79
|
-
...Object.fromEntries(AUDIO_EXTENSIONS.map((ext) => [ext, 'audio' as const])),
|
|
80
|
-
...Object.fromEntries(VIDEO_EXTENSIONS.map((ext) => [ext, 'video' as const])),
|
|
81
|
-
...Object.fromEntries(DOCUMENT_EXTENSIONS.map((ext) => [ext, 'file' as const])),
|
|
82
|
-
[PDF_EXTENSION]: 'pdf',
|
|
245
|
+
...Object.fromEntries(MEDIA_FILE_TYPES.map((fileType) => [fileType.extension, fileType.mediaType])),
|
|
83
246
|
};
|
|
84
247
|
|
|
85
248
|
/**
|
|
@@ -6,6 +6,11 @@ export const SUCCESS_MESSAGES = {
|
|
|
6
6
|
LOGOUT_SUCCESS: 'Logged out successfully',
|
|
7
7
|
USER_REGISTERED: 'User registered successfully.',
|
|
8
8
|
|
|
9
|
+
// MPIN messages
|
|
10
|
+
MPIN_SETUP_SUCCESS: 'MPIN set up successfully.',
|
|
11
|
+
MPIN_CHANGED_SUCCESS: 'MPIN changed successfully.',
|
|
12
|
+
MPIN_DEVICE_REVOKED: 'MPIN sign-in removed for this device.',
|
|
13
|
+
|
|
9
14
|
// CRUD service messages
|
|
10
15
|
RECORD_RECOVERED: 'Record successfully recovered',
|
|
11
16
|
SELECTED_RECORDS_RECOVERED: 'Selected records successfully recovered',
|
|
@@ -107,8 +107,11 @@ export class AuthenticationController {
|
|
|
107
107
|
@Post('logout')
|
|
108
108
|
@Public()
|
|
109
109
|
@HttpCode(HttpStatus.OK)
|
|
110
|
-
async logout(
|
|
111
|
-
|
|
110
|
+
async logout(
|
|
111
|
+
@Body('refreshToken') refreshToken: string,
|
|
112
|
+
@Body('allDevices') allDevices?: boolean,
|
|
113
|
+
) {
|
|
114
|
+
return this.authService.logout(refreshToken, allDevices === true);
|
|
112
115
|
}
|
|
113
116
|
|
|
114
117
|
@ApiBearerAuth("jwt")
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, ParseIntPipe, Post } from '@nestjs/common';
|
|
2
|
+
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
|
3
|
+
import { ActiveUser } from '../decorators/active-user.decorator';
|
|
4
|
+
import { Public } from '../decorators/public.decorator';
|
|
5
|
+
import { ChangeMpinDto } from '../dtos/change-mpin.dto';
|
|
6
|
+
import { MpinLoginDto } from '../dtos/mpin-login.dto';
|
|
7
|
+
import { SetupMpinDto } from '../dtos/setup-mpin.dto';
|
|
8
|
+
import { ActiveUserData } from '../interfaces/active-user-data.interface';
|
|
9
|
+
import { MpinService } from '../services/mpin.service';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* There is deliberately no public "is MPIN set up for this identifier" route.
|
|
13
|
+
* That would reintroduce exactly the user-enumeration oracle the opaque
|
|
14
|
+
* credential handle removes. The client decides whether to show the MPIN
|
|
15
|
+
* screen from the presence of its own stored handle.
|
|
16
|
+
*/
|
|
17
|
+
@Controller('iam/mpin')
|
|
18
|
+
@ApiTags('Solid Core')
|
|
19
|
+
export class MpinAuthenticationController {
|
|
20
|
+
constructor(private readonly mpinService: MpinService) { }
|
|
21
|
+
|
|
22
|
+
@ApiBearerAuth('jwt')
|
|
23
|
+
@Post('setup')
|
|
24
|
+
@HttpCode(HttpStatus.OK)
|
|
25
|
+
setup(
|
|
26
|
+
@Body() setupMpinDto: SetupMpinDto,
|
|
27
|
+
@ActiveUser() activeUser: ActiveUserData,
|
|
28
|
+
) {
|
|
29
|
+
return this.mpinService.setupMpin(activeUser, setupMpinDto);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@ApiBearerAuth('jwt')
|
|
33
|
+
@Post('change')
|
|
34
|
+
@HttpCode(HttpStatus.OK)
|
|
35
|
+
change(
|
|
36
|
+
@Body() changeMpinDto: ChangeMpinDto,
|
|
37
|
+
@ActiveUser() activeUser: ActiveUserData,
|
|
38
|
+
) {
|
|
39
|
+
return this.mpinService.changeMpin(activeUser, changeMpinDto);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Public by necessity - the caller has no session yet. The credential
|
|
43
|
+
// handle is unguessable, so this carries no enumeration surface.
|
|
44
|
+
@Public()
|
|
45
|
+
@Post('login')
|
|
46
|
+
@HttpCode(HttpStatus.OK)
|
|
47
|
+
login(@Body() mpinLoginDto: MpinLoginDto) {
|
|
48
|
+
return this.mpinService.loginWithMpin(mpinLoginDto);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@ApiBearerAuth('jwt')
|
|
52
|
+
@Get('devices')
|
|
53
|
+
listDevices(@ActiveUser() activeUser: ActiveUserData) {
|
|
54
|
+
return this.mpinService.listDevices(activeUser);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// By row id, never by credential handle, so the management UI never has to
|
|
58
|
+
// hold the secret.
|
|
59
|
+
@ApiBearerAuth('jwt')
|
|
60
|
+
@Delete('devices/:id')
|
|
61
|
+
revokeDevice(
|
|
62
|
+
@Param('id', ParseIntPipe) id: number,
|
|
63
|
+
@ActiveUser() activeUser: ActiveUserData,
|
|
64
|
+
) {
|
|
65
|
+
return this.mpinService.revokeDevice(activeUser, id);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { IsNotEmpty, IsString, MaxLength } from 'class-validator';
|
|
2
|
+
|
|
3
|
+
export class ChangeMpinDto {
|
|
4
|
+
@IsNotEmpty()
|
|
5
|
+
@IsString()
|
|
6
|
+
@MaxLength(128)
|
|
7
|
+
credentialRef: string;
|
|
8
|
+
|
|
9
|
+
@IsNotEmpty()
|
|
10
|
+
@IsString()
|
|
11
|
+
@MaxLength(32)
|
|
12
|
+
currentMpin: string;
|
|
13
|
+
|
|
14
|
+
@IsNotEmpty()
|
|
15
|
+
@IsString()
|
|
16
|
+
@MaxLength(32)
|
|
17
|
+
newMpin: string;
|
|
18
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ApiProperty } from '@nestjs/swagger';
|
|
2
2
|
import { Transform, Type } from 'class-transformer';
|
|
3
|
+
import { DANGEROUS_EXTENSIONS, MEDIA_CATEGORIES, MediaCategory } from '../constants/media-file-types';
|
|
3
4
|
|
|
4
5
|
import {
|
|
5
6
|
IsArray,
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
IsEnum,
|
|
8
9
|
IsIn,
|
|
9
10
|
IsInt,
|
|
11
|
+
IsNotIn,
|
|
10
12
|
IsOptional,
|
|
11
13
|
IsString,
|
|
12
14
|
Matches,
|
|
@@ -155,13 +157,7 @@ export enum DecryptWhenType {
|
|
|
155
157
|
afterTransit = 'after-transit',
|
|
156
158
|
}
|
|
157
159
|
|
|
158
|
-
export
|
|
159
|
-
image = 'image',
|
|
160
|
-
audio = 'audio',
|
|
161
|
-
video = 'video',
|
|
162
|
-
// document = 'document',
|
|
163
|
-
file = 'file',
|
|
164
|
-
}
|
|
160
|
+
export type MediaType = MediaCategory;
|
|
165
161
|
|
|
166
162
|
export enum RelationType {
|
|
167
163
|
manyToOne = 'many-to-one',
|
|
@@ -308,7 +304,7 @@ export class CreateFieldMetadataDto {
|
|
|
308
304
|
private: boolean;
|
|
309
305
|
|
|
310
306
|
@ApiProperty({ description: 'Allowed media types for the field. Only for type=mediaSingle,mediaMultiple', })
|
|
311
|
-
@
|
|
307
|
+
@IsIn([...MEDIA_CATEGORIES], { each: true })
|
|
312
308
|
@IsOptional()
|
|
313
309
|
mediaTypes: MediaType[];
|
|
314
310
|
|
|
@@ -317,6 +313,18 @@ export class CreateFieldMetadataDto {
|
|
|
317
313
|
@IsOptional()
|
|
318
314
|
mediaMaxSizeKb: number;
|
|
319
315
|
|
|
316
|
+
@ApiProperty({ description: 'Explicit allowlist of file extensions (e.g. ["pdf","xlsx","png"]) permitted for this field, checked in addition to any mediaTypes category restriction — both must pass if both are set. For a strict allowlist, leave mediaTypes empty and use this alone. Only for type=mediaSingle,mediaMultiple', })
|
|
317
|
+
@Transform(({ value }) =>
|
|
318
|
+
Array.isArray(value)
|
|
319
|
+
? value.map((ext: any) => String(ext).toLowerCase().trim().replace(/^\./, ''))
|
|
320
|
+
: value
|
|
321
|
+
)
|
|
322
|
+
@IsArray()
|
|
323
|
+
@IsString({ each: true })
|
|
324
|
+
@IsNotIn([...DANGEROUS_EXTENSIONS], { each: true })
|
|
325
|
+
@IsOptional()
|
|
326
|
+
mediaAllowedExtensions?: string[];
|
|
327
|
+
|
|
320
328
|
@ApiProperty({ description: 'Configured Media storage provider. Only for type=mediaSingle,mediaMultiple', })
|
|
321
329
|
@IsInt()
|
|
322
330
|
@IsOptional()
|
|
@@ -464,4 +472,4 @@ export class CreateFieldMetadataDto {
|
|
|
464
472
|
@ApiProperty({ description: 'Is Primary Key Field', })
|
|
465
473
|
@IsBoolean()
|
|
466
474
|
isPrimaryKey?: boolean;
|
|
467
|
-
}
|
|
475
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { IsNotEmpty, IsString, MaxLength } from 'class-validator';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Deliberately carries no identifier - no mobile, email or username.
|
|
5
|
+
*
|
|
6
|
+
* `credentialRef` is a server-issued 256-bit handle that already points at a
|
|
7
|
+
* single credential row, and therefore at a single user. That is what removes
|
|
8
|
+
* the user-enumeration surface a public login route would otherwise have: an
|
|
9
|
+
* attacker cannot probe "does this phone number have MPIN?" because there is
|
|
10
|
+
* nowhere to put a phone number.
|
|
11
|
+
*/
|
|
12
|
+
export class MpinLoginDto {
|
|
13
|
+
// Bounded so a pathological payload never reaches the regex test or the
|
|
14
|
+
// hash comparison. The handle is always 64 hex characters.
|
|
15
|
+
@IsNotEmpty()
|
|
16
|
+
@IsString()
|
|
17
|
+
@MaxLength(128)
|
|
18
|
+
credentialRef: string;
|
|
19
|
+
|
|
20
|
+
@IsNotEmpty()
|
|
21
|
+
@IsString()
|
|
22
|
+
@MaxLength(32)
|
|
23
|
+
mpin: string;
|
|
24
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { IsNotEmpty, IsOptional, IsString, IsUUID, Matches, MaxLength } from 'class-validator';
|
|
2
|
+
|
|
3
|
+
export class SetupMpinDto {
|
|
4
|
+
/**
|
|
5
|
+
* Stable identifier for the physical device, generated once by the client.
|
|
6
|
+
* Validated as a UUID before it ever reaches a cache key or a bucket name,
|
|
7
|
+
* so it cannot smuggle delimiters into the keyspace.
|
|
8
|
+
*/
|
|
9
|
+
@IsUUID()
|
|
10
|
+
deviceId: string;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A string, not a number, even though it looks like digits.
|
|
14
|
+
*
|
|
15
|
+
* A numeric MPIN cannot carry a leading zero: JSON forbids leading zeros in
|
|
16
|
+
* number literals, so a client serialising `012345` numerically emits
|
|
17
|
+
* `12345` and the server stores a five-digit MPIN the user never chose -
|
|
18
|
+
* which still satisfies the default `^\d{4,6}$` and so raises no error.
|
|
19
|
+
* `mpinRegex` is also tenant-configurable and may permit non-digits.
|
|
20
|
+
*
|
|
21
|
+
* Note this cannot be enforced here: the global ValidationPipe runs with
|
|
22
|
+
* `enableImplicitConversion`, so a numeric payload is coerced to a string
|
|
23
|
+
* before @IsString() sees it. Clients must send a quoted JSON string; that
|
|
24
|
+
* requirement is documented in the mobile integration guide.
|
|
25
|
+
*
|
|
26
|
+
* Length and shape are checked server-side against `mpinRegex`, since that
|
|
27
|
+
* setting is the single authority. This only bounds the input.
|
|
28
|
+
*/
|
|
29
|
+
@IsNotEmpty()
|
|
30
|
+
@IsString()
|
|
31
|
+
@MaxLength(32)
|
|
32
|
+
mpin: string;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Rendered in a device-management list, so it is bounded here rather than
|
|
36
|
+
* trusting every consuming UI to escape it.
|
|
37
|
+
*/
|
|
38
|
+
@IsOptional()
|
|
39
|
+
@IsString()
|
|
40
|
+
@MaxLength(64)
|
|
41
|
+
@Matches(/^[\w\s.'()-]*$/, {
|
|
42
|
+
message: 'deviceName may contain only letters, numbers, spaces and . \' ( ) - _',
|
|
43
|
+
})
|
|
44
|
+
deviceName?: string;
|
|
45
|
+
|
|
46
|
+
@IsOptional()
|
|
47
|
+
@IsString()
|
|
48
|
+
@MaxLength(32)
|
|
49
|
+
@Matches(/^[\w.-]*$/, { message: 'platform may contain only letters, numbers and . - _' })
|
|
50
|
+
platform?: string;
|
|
51
|
+
}
|
|
@@ -6,12 +6,12 @@ import { ModuleMetadata } from 'src/entities/module-metadata.entity'
|
|
|
6
6
|
@Entity('ss_dashboard_user_layout')
|
|
7
7
|
export class DashboardUserLayout extends CommonEntity {
|
|
8
8
|
@Index()
|
|
9
|
-
@ManyToOne(() => User, {
|
|
9
|
+
@ManyToOne(() => User, { nullable: false })
|
|
10
10
|
@JoinColumn()
|
|
11
11
|
user: User;
|
|
12
12
|
|
|
13
13
|
@Index()
|
|
14
|
-
@ManyToOne(() => ModuleMetadata, {
|
|
14
|
+
@ManyToOne(() => ModuleMetadata, { nullable: false })
|
|
15
15
|
@JoinColumn()
|
|
16
16
|
module: ModuleMetadata;
|
|
17
17
|
|
|
@@ -77,6 +77,9 @@ export class FieldMetadata extends CommonEntity {
|
|
|
77
77
|
@Column({ name: 'media_max_size_kb', nullable: true })
|
|
78
78
|
mediaMaxSizeKb: number;
|
|
79
79
|
|
|
80
|
+
@Column({ name: 'media_allowed_extensions', nullable: true, type: "simple-json" })
|
|
81
|
+
mediaAllowedExtensions: string[];
|
|
82
|
+
|
|
80
83
|
@Index()
|
|
81
84
|
@ManyToOne(() => MediaStorageProviderMetadata, { onDelete: 'SET NULL' })
|
|
82
85
|
@JoinColumn({ name: 'media_storage_provider_id' })
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Exclude, Expose } from "class-transformer";
|
|
2
|
+
import { CommonEntity } from "src/entities/common.entity";
|
|
3
|
+
import { Column, Entity, Index, ManyToOne } from "typeorm";
|
|
4
|
+
import { User } from "./user.entity";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A per-device MPIN credential.
|
|
8
|
+
*
|
|
9
|
+
* Login presents `credentialRef` (a server-issued opaque handle) plus the MPIN.
|
|
10
|
+
* The handle - not a mobile number or email - is what identifies the account,
|
|
11
|
+
* which is why the login route needs no identifier and offers no user
|
|
12
|
+
* enumeration surface.
|
|
13
|
+
*/
|
|
14
|
+
@Entity("ss_user_device_credential")
|
|
15
|
+
// Non-unique: one physical device may legitimately hold credentials for
|
|
16
|
+
// several accounts, and each account setup mints its own row.
|
|
17
|
+
@Index(["user", "deviceId"])
|
|
18
|
+
@Exclude()
|
|
19
|
+
export class UserDeviceCredential extends CommonEntity {
|
|
20
|
+
|
|
21
|
+
// SHA-256 of the opaque handle, and the login lookup key. Never exposed:
|
|
22
|
+
// the raw handle is returned to the client once at setup and never stored.
|
|
23
|
+
// SHA-256 rather than bcrypt because the handle is 256 bits of randomness -
|
|
24
|
+
// there is nothing to brute-force, so a slow hash would only add latency.
|
|
25
|
+
// Same treatment as UserApiKey.hashedKey.
|
|
26
|
+
@Index({ unique: true })
|
|
27
|
+
@Column()
|
|
28
|
+
hashedCredentialRef: string;
|
|
29
|
+
|
|
30
|
+
// bcrypt hash of the MPIN, via HashingService. Never exposed - same
|
|
31
|
+
// treatment as User.password. Slow hashing is warranted here: a 4-6 digit
|
|
32
|
+
// secret is exactly the low-entropy case bcrypt exists for.
|
|
33
|
+
@Column()
|
|
34
|
+
hashedMpin: string;
|
|
35
|
+
|
|
36
|
+
// Mirrors User.passwordScheme / passwordSchemeVersion so
|
|
37
|
+
// HashingService.needsRehash can upgrade the hash on a successful login.
|
|
38
|
+
@Column()
|
|
39
|
+
mpinScheme: string;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The hashing *policy* version this row was hashed under - not a version of
|
|
43
|
+
* MPIN itself. It comes from `HashingService.currentVersion()`, which
|
|
44
|
+
* `BcryptService` currently returns as **2**, so freshly created rows read
|
|
45
|
+
* 2, exactly as `ss_user.password_scheme_version` does.
|
|
46
|
+
*
|
|
47
|
+
* It matters because `BcryptService.normalize` applies the configured
|
|
48
|
+
* pepper only when `version >= 2`. Storing the version per row means an
|
|
49
|
+
* older hash still verifies after the policy changes: `compare()` uses the
|
|
50
|
+
* version the row was written with, and `needsRehash()` upgrades it on the
|
|
51
|
+
* next successful login.
|
|
52
|
+
*
|
|
53
|
+
* The `default: 1` is inherited from the `User` precedent and is never
|
|
54
|
+
* reached, since every write path sets this explicitly. Note it would be
|
|
55
|
+
* *wrong* if it ever were: a row hashed at 2 but recorded as 1 would be
|
|
56
|
+
* compared without the pepper and could never authenticate.
|
|
57
|
+
*/
|
|
58
|
+
@Column({ default: 1 })
|
|
59
|
+
mpinSchemeVersion: number;
|
|
60
|
+
|
|
61
|
+
// Client-supplied, stable for the physical device. Used for grouping and
|
|
62
|
+
// the device-management UI only - never a lookup key. Deliberately NOT
|
|
63
|
+
// unique: several accounts on one device share a deviceId.
|
|
64
|
+
@Expose()
|
|
65
|
+
@Column()
|
|
66
|
+
deviceId: string;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Human-readable natural key - `<username>-<deviceId>` - unique per row.
|
|
70
|
+
*
|
|
71
|
+
* Neither column alone can serve as the model's user key: `deviceId` is
|
|
72
|
+
* shared by every account on a device, and `deviceName` is nullable.
|
|
73
|
+
* Combining them is unique in both directions - one user across many
|
|
74
|
+
* devices, and many users on one device.
|
|
75
|
+
*
|
|
76
|
+
* Populated by MpinService rather than declared as a `computed` field in
|
|
77
|
+
* the metadata manifest. Computed fields are evaluated by
|
|
78
|
+
* ComputedFieldCrudManager on the generic CRUD path (`crud.service.ts`),
|
|
79
|
+
* which this service bypasses by writing through the repository directly,
|
|
80
|
+
* so a computed declaration would silently leave this null. It is also
|
|
81
|
+
* beyond ConcatComputedFieldProvider, which resolves `dto[field]` flatly
|
|
82
|
+
* within one model and cannot reach `username` across the user relation.
|
|
83
|
+
*/
|
|
84
|
+
@Expose()
|
|
85
|
+
@Index({ unique: true })
|
|
86
|
+
@Column()
|
|
87
|
+
credentialKey: string;
|
|
88
|
+
|
|
89
|
+
@Expose()
|
|
90
|
+
@Column({ nullable: true })
|
|
91
|
+
deviceName: string;
|
|
92
|
+
|
|
93
|
+
@Expose()
|
|
94
|
+
@Column({ nullable: true })
|
|
95
|
+
platform: string;
|
|
96
|
+
|
|
97
|
+
// Revocation is a soft-delete: the row survives so that a correct MPIN can
|
|
98
|
+
// be answered with "this device was removed" rather than a baffling
|
|
99
|
+
// "incorrect MPIN". Only active rows count towards mpinMaxDevicesPerUser.
|
|
100
|
+
@Expose()
|
|
101
|
+
@Column({ default: true })
|
|
102
|
+
isActive: boolean;
|
|
103
|
+
|
|
104
|
+
// Cumulative, and reset only on a successful login - never when a lockout
|
|
105
|
+
// expires. That is what makes the second threshold detectable without a
|
|
106
|
+
// separate lockout counter.
|
|
107
|
+
@Expose()
|
|
108
|
+
@Column({ default: 0 })
|
|
109
|
+
failedAttempts: number;
|
|
110
|
+
|
|
111
|
+
@Expose()
|
|
112
|
+
@Column({ nullable: true })
|
|
113
|
+
lockedUntil: Date;
|
|
114
|
+
|
|
115
|
+
@Expose()
|
|
116
|
+
@Column({ nullable: true })
|
|
117
|
+
lastUsedAt: Date;
|
|
118
|
+
|
|
119
|
+
// Deliberately no inverse property on User. Nothing needs
|
|
120
|
+
// `user.deviceCredentials`, and leaving the most widely referenced entity
|
|
121
|
+
// in the system untouched keeps this feature purely additive.
|
|
122
|
+
//
|
|
123
|
+
// `nullable: false` because TypeORM makes relations nullable by default,
|
|
124
|
+
// which would let a credential exist with no owner. Such a row could never
|
|
125
|
+
// authenticate - loginWithMpin rejects it - so it would simply be dead
|
|
126
|
+
// data, but the database should refuse it outright. This also keeps the
|
|
127
|
+
// column honest against the metadata manifest, which declares it required.
|
|
128
|
+
@ManyToOne(() => User, { nullable: false })
|
|
129
|
+
user: User;
|
|
130
|
+
}
|