@solidxai/core 0.1.13-beta.6 → 0.1.13-beta.8

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.
Files changed (211) hide show
  1. package/CHANGELOG.md +1076 -0
  2. package/dist/constants/draft-publish-fields.d.ts +2 -0
  3. package/dist/constants/draft-publish-fields.d.ts.map +1 -0
  4. package/dist/constants/draft-publish-fields.js +5 -0
  5. package/dist/constants/draft-publish-fields.js.map +1 -0
  6. package/dist/constants/media-file-types.d.ts +9 -0
  7. package/dist/constants/media-file-types.d.ts.map +1 -1
  8. package/dist/constants/media-file-types.js +27 -1
  9. package/dist/constants/media-file-types.js.map +1 -1
  10. package/dist/controllers/action-metadata.controller.d.ts +1 -1
  11. package/dist/controllers/action-metadata.controller.js +1 -1
  12. package/dist/controllers/agent-event.controller.d.ts +1 -1
  13. package/dist/controllers/agent-event.controller.js +1 -1
  14. package/dist/controllers/agent-session.controller.d.ts +1 -1
  15. package/dist/controllers/agent-session.controller.js +1 -1
  16. package/dist/controllers/dashboard-user-layout.controller.d.ts +1 -1
  17. package/dist/controllers/dashboard-user-layout.controller.js +1 -1
  18. package/dist/controllers/email-template.controller.d.ts +1 -1
  19. package/dist/controllers/email-template.controller.js +1 -1
  20. package/dist/controllers/export-template.controller.d.ts +1 -1
  21. package/dist/controllers/export-template.controller.d.ts.map +1 -1
  22. package/dist/controllers/export-template.controller.js +6 -4
  23. package/dist/controllers/export-template.controller.js.map +1 -1
  24. package/dist/controllers/export-transaction.controller.d.ts +1 -1
  25. package/dist/controllers/export-transaction.controller.js +1 -1
  26. package/dist/controllers/import-transaction-error-log.controller.d.ts +1 -1
  27. package/dist/controllers/import-transaction-error-log.controller.js +1 -1
  28. package/dist/controllers/import-transaction.controller.d.ts +1 -1
  29. package/dist/controllers/import-transaction.controller.d.ts.map +1 -1
  30. package/dist/controllers/import-transaction.controller.js +7 -7
  31. package/dist/controllers/import-transaction.controller.js.map +1 -1
  32. package/dist/controllers/list-of-values.controller.d.ts +1 -1
  33. package/dist/controllers/list-of-values.controller.js +1 -1
  34. package/dist/controllers/locale.controller.d.ts +1 -1
  35. package/dist/controllers/locale.controller.js +1 -1
  36. package/dist/controllers/mcp-audit-log.controller.d.ts +1 -1
  37. package/dist/controllers/mcp-audit-log.controller.js +1 -1
  38. package/dist/controllers/media.controller.d.ts +2 -2
  39. package/dist/controllers/media.controller.d.ts.map +1 -1
  40. package/dist/controllers/media.controller.js +13 -8
  41. package/dist/controllers/media.controller.js.map +1 -1
  42. package/dist/controllers/menu-item-metadata.controller.d.ts +1 -1
  43. package/dist/controllers/menu-item-metadata.controller.js +1 -1
  44. package/dist/controllers/model-sequence.controller.d.ts +1 -1
  45. package/dist/controllers/model-sequence.controller.js +1 -1
  46. package/dist/controllers/module-package.controller.d.ts.map +1 -1
  47. package/dist/controllers/module-package.controller.js +2 -3
  48. package/dist/controllers/module-package.controller.js.map +1 -1
  49. package/dist/controllers/mq-message-queue.controller.d.ts +1 -1
  50. package/dist/controllers/mq-message-queue.controller.js +1 -1
  51. package/dist/controllers/mq-message.controller.d.ts +1 -1
  52. package/dist/controllers/mq-message.controller.js +1 -1
  53. package/dist/controllers/permission-metadata.controller.d.ts +1 -1
  54. package/dist/controllers/permission-metadata.controller.js +1 -1
  55. package/dist/controllers/role-metadata.controller.d.ts +1 -1
  56. package/dist/controllers/role-metadata.controller.js +1 -1
  57. package/dist/controllers/saved-filters.controller.d.ts +1 -1
  58. package/dist/controllers/saved-filters.controller.js +1 -1
  59. package/dist/controllers/scheduled-job.controller.d.ts +1 -1
  60. package/dist/controllers/scheduled-job.controller.js +1 -1
  61. package/dist/controllers/security-rule.controller.d.ts +1 -1
  62. package/dist/controllers/security-rule.controller.js +1 -1
  63. package/dist/controllers/sms-template.controller.d.ts +1 -1
  64. package/dist/controllers/sms-template.controller.js +1 -1
  65. package/dist/controllers/user-activity-history.controller.d.ts +1 -1
  66. package/dist/controllers/user-activity-history.controller.js +1 -1
  67. package/dist/controllers/user-view-metadata.controller.d.ts +1 -1
  68. package/dist/controllers/user-view-metadata.controller.js +1 -1
  69. package/dist/controllers/user.controller.d.ts +2 -2
  70. package/dist/controllers/user.controller.js +2 -2
  71. package/dist/controllers/view-metadata.controller.d.ts +2 -2
  72. package/dist/controllers/view-metadata.controller.js +1 -1
  73. package/dist/controllers/workflow-definition.controller.d.ts +1 -1
  74. package/dist/controllers/workflow-definition.controller.js +1 -1
  75. package/dist/controllers/workflow-execution-artifact.controller.d.ts +1 -1
  76. package/dist/controllers/workflow-execution-artifact.controller.js +1 -1
  77. package/dist/controllers/workflow-execution-log.controller.d.ts +1 -1
  78. package/dist/controllers/workflow-execution-log.controller.js +1 -1
  79. package/dist/controllers/workflow-execution.controller.d.ts +1 -1
  80. package/dist/controllers/workflow-execution.controller.js +1 -1
  81. package/dist/controllers/workflow-secret.controller.d.ts +1 -1
  82. package/dist/controllers/workflow-secret.controller.js +1 -1
  83. package/dist/controllers/workflow-step-execution.controller.d.ts +1 -1
  84. package/dist/controllers/workflow-step-execution.controller.js +1 -1
  85. package/dist/controllers/workflow-trigger-execution.controller.d.ts +1 -1
  86. package/dist/controllers/workflow-trigger-execution.controller.js +1 -1
  87. package/dist/dtos/basic-filters.dto.d.ts +0 -1
  88. package/dist/dtos/basic-filters.dto.d.ts.map +1 -1
  89. package/dist/dtos/basic-filters.dto.js +1 -6
  90. package/dist/dtos/basic-filters.dto.js.map +1 -1
  91. package/dist/entities/common.entity.d.ts +4 -0
  92. package/dist/entities/common.entity.d.ts.map +1 -1
  93. package/dist/entities/common.entity.js +25 -1
  94. package/dist/entities/common.entity.js.map +1 -1
  95. package/dist/entities/user.entity.js +2 -2
  96. package/dist/entities/user.entity.js.map +1 -1
  97. package/dist/factories/mail.factory.d.ts.map +1 -1
  98. package/dist/factories/mail.factory.js +4 -0
  99. package/dist/factories/mail.factory.js.map +1 -1
  100. package/dist/helpers/field-crud-managers/MediaFieldCrudManager.d.ts +1 -0
  101. package/dist/helpers/field-crud-managers/MediaFieldCrudManager.d.ts.map +1 -1
  102. package/dist/helpers/field-crud-managers/MediaFieldCrudManager.js +18 -11
  103. package/dist/helpers/field-crud-managers/MediaFieldCrudManager.js.map +1 -1
  104. package/dist/helpers/file-download.helper.d.ts +9 -0
  105. package/dist/helpers/file-download.helper.d.ts.map +1 -0
  106. package/dist/helpers/file-download.helper.js +14 -0
  107. package/dist/helpers/file-download.helper.js.map +1 -0
  108. package/dist/helpers/model-metadata-helper.service.d.ts.map +1 -1
  109. package/dist/helpers/model-metadata-helper.service.js +30 -1
  110. package/dist/helpers/model-metadata-helper.service.js.map +1 -1
  111. package/dist/helpers/solid-registry.js +1 -1
  112. package/dist/helpers/solid-registry.js.map +1 -1
  113. package/dist/index.d.ts +2 -0
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.js +2 -0
  116. package/dist/index.js.map +1 -1
  117. package/dist/seeders/module-test-data.service.d.ts +3 -1
  118. package/dist/seeders/module-test-data.service.d.ts.map +1 -1
  119. package/dist/seeders/module-test-data.service.js +8 -2
  120. package/dist/seeders/module-test-data.service.js.map +1 -1
  121. package/dist/seeders/seed-data/solid-core-metadata.json +30 -2
  122. package/dist/services/authentication.service.d.ts +4 -1
  123. package/dist/services/authentication.service.d.ts.map +1 -1
  124. package/dist/services/authentication.service.js +52 -4
  125. package/dist/services/authentication.service.js.map +1 -1
  126. package/dist/services/chatter-message.service.d.ts +1 -0
  127. package/dist/services/chatter-message.service.d.ts.map +1 -1
  128. package/dist/services/chatter-message.service.js +8 -0
  129. package/dist/services/chatter-message.service.js.map +1 -1
  130. package/dist/services/crud-helper.service.d.ts +5 -2
  131. package/dist/services/crud-helper.service.d.ts.map +1 -1
  132. package/dist/services/crud-helper.service.js +30 -32
  133. package/dist/services/crud-helper.service.js.map +1 -1
  134. package/dist/services/crud.service.d.ts +11 -1
  135. package/dist/services/crud.service.d.ts.map +1 -1
  136. package/dist/services/crud.service.js +79 -66
  137. package/dist/services/crud.service.js.map +1 -1
  138. package/dist/services/datasource-introspection.service.d.ts.map +1 -1
  139. package/dist/services/datasource-introspection.service.js.map +1 -1
  140. package/dist/services/draft-publish-helper.service.d.ts +52 -0
  141. package/dist/services/draft-publish-helper.service.d.ts.map +1 -0
  142. package/dist/services/draft-publish-helper.service.js +368 -0
  143. package/dist/services/draft-publish-helper.service.js.map +1 -0
  144. package/dist/services/internationalisation-helper.service.d.ts +19 -0
  145. package/dist/services/internationalisation-helper.service.d.ts.map +1 -0
  146. package/dist/services/internationalisation-helper.service.js +107 -0
  147. package/dist/services/internationalisation-helper.service.js.map +1 -0
  148. package/dist/services/locale.service.d.ts +1 -1
  149. package/dist/services/media.service.d.ts +3 -2
  150. package/dist/services/media.service.d.ts.map +1 -1
  151. package/dist/services/media.service.js +44 -29
  152. package/dist/services/media.service.js.map +1 -1
  153. package/dist/services/metadata-validation.service.d.ts +7 -0
  154. package/dist/services/metadata-validation.service.d.ts.map +1 -0
  155. package/dist/services/metadata-validation.service.js +42 -0
  156. package/dist/services/metadata-validation.service.js.map +1 -0
  157. package/dist/services/model-metadata.service.d.ts.map +1 -1
  158. package/dist/services/model-metadata.service.js +2 -1
  159. package/dist/services/model-metadata.service.js.map +1 -1
  160. package/dist/services/object.utils.d.ts +2 -0
  161. package/dist/services/object.utils.d.ts.map +1 -0
  162. package/dist/services/object.utils.js +11 -0
  163. package/dist/services/object.utils.js.map +1 -0
  164. package/dist/services/settings/default-settings-provider.service.d.ts +6 -6
  165. package/dist/services/settings/default-settings-provider.service.js +3 -3
  166. package/dist/services/settings/default-settings-provider.service.js.map +1 -1
  167. package/dist/services/user.service.d.ts +2 -2
  168. package/dist/services/user.service.d.ts.map +1 -1
  169. package/dist/services/user.service.js +9 -3
  170. package/dist/services/user.service.js.map +1 -1
  171. package/dist/services/view-metadata.service.d.ts +1 -2
  172. package/dist/services/view-metadata.service.d.ts.map +1 -1
  173. package/dist/services/view-metadata.service.js +3 -55
  174. package/dist/services/view-metadata.service.js.map +1 -1
  175. package/dist/services/workflow-definition.service.d.ts +1 -1
  176. package/dist/solid-core.module.d.ts.map +1 -1
  177. package/dist/solid-core.module.js +10 -1
  178. package/dist/solid-core.module.js.map +1 -1
  179. package/package.json +1 -1
  180. package/src/constants/draft-publish-fields.ts +5 -0
  181. package/src/constants/media-file-types.ts +50 -0
  182. package/src/controllers/export-template.controller.ts +5 -3
  183. package/src/controllers/import-transaction.controller.ts +6 -6
  184. package/src/controllers/media.controller.ts +17 -6
  185. package/src/controllers/module-package.controller.ts +2 -3
  186. package/src/dtos/basic-filters.dto.ts +0 -4
  187. package/src/entities/common.entity.ts +20 -0
  188. package/src/entities/user.entity.ts +2 -2
  189. package/src/factories/mail.factory.ts +7 -1
  190. package/src/helpers/field-crud-managers/MediaFieldCrudManager.ts +26 -12
  191. package/src/helpers/file-download.helper.ts +31 -0
  192. package/src/helpers/model-metadata-helper.service.ts +32 -1
  193. package/src/helpers/solid-registry.ts +1 -1
  194. package/src/index.ts +2 -0
  195. package/src/seeders/module-test-data.service.ts +6 -0
  196. package/src/seeders/seed-data/solid-core-metadata.json +30 -2
  197. package/src/services/authentication.service.ts +76 -5
  198. package/src/services/chatter-message.service.ts +16 -0
  199. package/src/services/crud-helper.service.ts +16 -34
  200. package/src/services/crud.service.ts +98 -103
  201. package/src/services/datasource-introspection.service.ts +5 -0
  202. package/src/services/draft-publish-helper.service.ts +529 -0
  203. package/src/services/internationalisation-helper.service.ts +148 -0
  204. package/src/services/media.service.ts +67 -36
  205. package/src/services/metadata-validation.service.ts +39 -0
  206. package/src/services/model-metadata.service.ts +4 -1
  207. package/src/services/object.utils.ts +10 -0
  208. package/src/services/settings/default-settings-provider.service.ts +3 -3
  209. package/src/services/user.service.ts +18 -3
  210. package/src/services/view-metadata.service.ts +5 -145
  211. package/src/solid-core.module.ts +16 -3
@@ -36,12 +36,12 @@ export class User extends CommonEntity {
36
36
  @Expose()
37
37
  username: string;
38
38
 
39
- @Index()
39
+ @Index({ unique: true })
40
40
  @Column({ type: "varchar", nullable: true })
41
41
  @Expose()
42
42
  email: string;
43
43
 
44
- @Index()
44
+ @Index({ unique: true })
45
45
  @Column({ type: "varchar", nullable: true })
46
46
  @Expose()
47
47
  mobile: string;
@@ -30,8 +30,14 @@ export class MailFactory {
30
30
  }
31
31
 
32
32
  const mailServiceProvider = mailProviders.find(provider => provider.name === mailServiceName);
33
+ if (!mailServiceProvider?.instance) {
34
+ const registeredProviders = mailProviders.map((provider) => provider?.name).filter(Boolean).join(", ");
35
+ throw new Error(
36
+ `Mail provider "${mailServiceName}" is not registered. Registered providers: ${registeredProviders || "none"}`,
37
+ );
38
+ }
33
39
 
34
40
  return mailServiceProvider.instance as IMail;
35
41
  }
36
42
 
37
- }
43
+ }
@@ -1,5 +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
+ import { EXT_TO_MEDIA_TYPE, getLowercaseFileExtension, getUploadedFileName, isDangerousMediaFile, MediaCategory } from "src/constants/media-file-types";
3
3
 
4
4
  export enum SolidMediaType {
5
5
  mediaSingle = 'mediaSingle',
@@ -79,16 +79,15 @@ export class MediaFieldCrudManager implements FieldCrudManager {
79
79
  constructor(private readonly options: MediaFieldOptions) {
80
80
  }
81
81
 
82
- private resolveMediaType(mimetype?: string, filename?: string): MediaType | null {
83
- const ext = (filename || '').split('.').pop()?.toLowerCase();
84
- if (ext && DANGEROUS_EXTENSIONS.has(ext)) {
82
+ private resolveMediaType(file: Express.Multer.File): MediaType | null {
83
+ // Belt-and-braces: dangerous files are already rejected outright by
84
+ // validateDangerousFiles, but never let one resolve to a usable category.
85
+ if (isDangerousMediaFile(file)) {
85
86
  return null;
86
87
  }
87
88
 
88
- const mt = (mimetype || '').toLowerCase().trim();
89
- if (mt === 'image/svg+xml' || mt === 'text/html' || mt === 'application/xhtml+xml') {
90
- return null;
91
- }
89
+ const ext = getLowercaseFileExtension(getUploadedFileName(file));
90
+ const mt = (file.mimetype || '').toLowerCase().trim();
92
91
  if (mt && MIME_TO_MEDIA_TYPE[mt]) {
93
92
  return MIME_TO_MEDIA_TYPE[mt];
94
93
  }
@@ -113,16 +112,31 @@ export class MediaFieldCrudManager implements FieldCrudManager {
113
112
  }
114
113
 
115
114
  private applyValidations(fieldFiles: Array<Express.Multer.File>): ValidationError[] {
115
+ // Runs for every media field type and regardless of the admin-configured mediaTypes
116
+ // allowlist - a dangerous upload is never acceptable, so this must not sit behind
117
+ // the optional type checks below.
118
+ const errors = this.validateDangerousFiles(fieldFiles);
119
+
116
120
  switch (this.options.type) {
117
121
  case SolidMediaType.mediaSingle:
118
- return this.validateMediaSingle(fieldFiles);
122
+ return [...errors, ...this.validateMediaSingle(fieldFiles)];
119
123
  case SolidMediaType.mediaMultiple:
120
- return this.validateMediaMultiple(fieldFiles);
124
+ return [...errors, ...this.validateMediaMultiple(fieldFiles)];
121
125
  default:
122
- return [];
126
+ return errors;
123
127
  }
124
128
  }
125
129
 
130
+ private validateDangerousFiles(fieldFiles: Array<Express.Multer.File>): ValidationError[] {
131
+ return fieldFiles
132
+ .filter(fieldFile => isDangerousMediaFile(fieldFile))
133
+ .map(fieldFile => ({
134
+ field: this.options.fieldName,
135
+ error: `${this.options.fieldName} file type not allowed. ` +
136
+ `${getUploadedFileName(fieldFile) || 'File'} is a potentially dangerous file type.`
137
+ }));
138
+ }
139
+
126
140
  private validateMediaSingle(fieldFiles: Array<Express.Multer.File>): ValidationError[] {
127
141
  const errors: ValidationError[] = [];
128
142
  if (!this.options.isUpdate && this.options.required && fieldFiles.length === 0) {
@@ -157,7 +171,7 @@ export class MediaFieldCrudManager implements FieldCrudManager {
157
171
  for (let i = 0; i < fieldFiles.length; i++) {
158
172
  const fieldFile = fieldFiles[i];
159
173
 
160
- const resolvedType = this.resolveMediaType(fieldFile.mimetype, fieldFile.originalname ?? fieldFile.filename ?? '');
174
+ const resolvedType = this.resolveMediaType(fieldFile);
161
175
  if (!resolvedType || !allowedFileTypes.includes(resolvedType)) {
162
176
  errors.push({
163
177
  field: this.options.fieldName,
@@ -0,0 +1,31 @@
1
+ import type { Response } from "express";
2
+
3
+ export interface FileDownloadHeaderOptions {
4
+ fileName: string;
5
+ mimeType: string;
6
+ /** Defaults to 'attachment'; only pass 'inline' for types known safe to render. */
7
+ disposition?: "inline" | "attachment";
8
+ /** Opt in for assets loaded cross-origin by the UI (e.g. media served to :3000). */
9
+ crossOriginResourcePolicy?: boolean;
10
+ }
11
+
12
+ /**
13
+ * Single place that stamps the response headers for every streamed file download, so the
14
+ * security headers can't be applied to one endpoint and forgotten on the next.
15
+ */
16
+ export function setFileDownloadHeaders(res: Response, options: FileDownloadHeaderOptions): void {
17
+ const disposition = options.disposition ?? "attachment";
18
+
19
+ res.setHeader("Content-Disposition", `${disposition}; filename="${options.fileName}"`);
20
+ res.setHeader("Content-Type", options.mimeType);
21
+ res.setHeader("Access-Control-Expose-Headers", "Content-Disposition, Content-Type");
22
+
23
+ // Prevent the browser from ever content-sniffing a served file into something more
24
+ // dangerous than its declared Content-Type (e.g. rendering a mislabeled upload as HTML
25
+ // and executing embedded script).
26
+ res.setHeader("X-Content-Type-Options", "nosniff");
27
+
28
+ if (options.crossOriginResourcePolicy) {
29
+ res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
30
+ }
31
+ }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { forwardRef, Inject, Injectable, Logger } from "@nestjs/common";
4
4
  import { _ } from "lodash";
5
+ import { DRAFT_PUBLISH_VERSIONING_FIELD_NAMES } from "src/constants/draft-publish-fields";
5
6
  import { LEGACY_TABLE_FIELDS_PREFIX } from "src/entities/legacy-common.entity-with-existing-id.entity";
6
7
  import { LegacyTableType } from "src/enums/legacy-table-type.enum";
7
8
  import { ModelMetadataRepository } from "src/repository/model-metadata.repository";
@@ -87,6 +88,32 @@ export class ModelMetadataHelperService {
87
88
  isSystem: true,
88
89
  enableAuditTracking:true
89
90
  },
91
+ {
92
+ name: "isPublished",
93
+ displayName: "Is Published",
94
+ type: "boolean",
95
+ isSystem: true,
96
+ enableAuditTracking: true
97
+ },
98
+ {
99
+ name: "isLatest",
100
+ displayName: "Is Latest",
101
+ type: "boolean",
102
+ isSystem: true,
103
+ enableAuditTracking: true
104
+ },
105
+ {
106
+ name: "initialEntityVersionId",
107
+ displayName: "Initial Entity Version Id",
108
+ type: "int",
109
+ isSystem: true,
110
+ },
111
+ {
112
+ name: "publishedTracker",
113
+ displayName: "Published Tracker",
114
+ type: "shortText",
115
+ isSystem: true,
116
+ },
90
117
  {
91
118
  name: "localeName",
92
119
  displayName: "Locale",
@@ -117,7 +144,11 @@ export class ModelMetadataHelperService {
117
144
  }
118
145
 
119
146
  private getSystemFieldsMetadataMappingForLegacyTable(withId: boolean=true) {
120
- const systemFieldsMetadata = this.getSystemFieldsMetadataMapping();
147
+ // Legacy tables don't have the draft/publish versioning columns, so exclude
148
+ // them from the legacy system fields metadata mapping.
149
+ const legacyExcludedFieldNames: readonly string[] = DRAFT_PUBLISH_VERSIONING_FIELD_NAMES;
150
+ const systemFieldsMetadata = this.getSystemFieldsMetadataMapping()
151
+ .filter(field => !legacyExcludedFieldNames.includes(field.name));
121
152
  if (!withId) {
122
153
  // Remove the id field metadata
123
154
  const index = systemFieldsMetadata.findIndex(field => field.name === 'id');
@@ -366,7 +366,7 @@ export class SolidRegistry {
366
366
  }
367
367
 
368
368
  getCommonEntityKeys(): (keyof CommonEntity | 'createdBy' | 'updatedBy')[] {
369
- return ['id', 'createdAt', 'updatedAt', 'deletedAt', 'createdBy', 'updatedBy', 'deletedTracker', 'localeName', 'defaultEntityLocaleId', 'publishedAt'];
369
+ return ['id', 'createdAt', 'updatedAt', 'deletedAt', 'createdBy', 'updatedBy', 'deletedTracker', 'localeName', 'defaultEntityLocaleId', 'publishedAt', 'isPublished', 'isLatest', 'initialEntityVersionId', 'publishedTracker'];
370
370
  // return Reflect.getMetadataKeys(CommonEntity.prototype) as (keyof CommonEntity)[];
371
371
  }
372
372
 
package/src/index.ts CHANGED
@@ -225,6 +225,7 @@ export * from './helpers/environment.helper'
225
225
  export * from './helpers/cache.helper'
226
226
  export * from './helpers/cors.helper'
227
227
  export * from './helpers/security.helper'
228
+ export * from './helpers/file-download.helper'
228
229
  export * from './helpers/model-metadata-helper.service'
229
230
  export * from './helpers/image-encoding.helper'
230
231
  export * from './helpers/solid-microservice-adapter.service'
@@ -337,6 +338,7 @@ export * from './services/action-metadata.service'
337
338
  export * from './services/authentication.service'
338
339
  export * from './services/bcrypt.service'
339
340
  export * from './services/computed-fields/uuid-external-id-computed-field-provider.service'
341
+ export * from './services/draft-publish-helper.service'
340
342
  export * from './services/crud-helper.service'
341
343
  export * from './services/crud.service'
342
344
  export * from './services/email-template.service'
@@ -12,6 +12,7 @@ import { CreateModelMetadataDto } from 'src/dtos/create-model-metadata.dto';
12
12
  import { MediaStorageProviderType } from 'src/dtos/create-media-storage-provider-metadata.dto';
13
13
  import { getDynamicModuleNamesBasedOnMetadata } from 'src/helpers/module.helper';
14
14
  import { SolidRegistry } from 'src/helpers/solid-registry';
15
+ import { InternationalisationHelperService } from 'src/services/internationalisation-helper.service';
15
16
  import { MediaRepository } from 'src/repository/media.repository';
16
17
  import { PermissionMetadataRepository } from 'src/repository/permission-metadata.repository';
17
18
  import { AuthenticationService } from 'src/services/authentication.service';
@@ -30,6 +31,7 @@ export class ModuleTestDataService {
30
31
  private readonly moduleRef: ModuleRef,
31
32
  private readonly discoveryService: DiscoveryService,
32
33
  private readonly solidRegistry: SolidRegistry,
34
+ private readonly internationalisationHelperService: InternationalisationHelperService,
33
35
  ) {}
34
36
 
35
37
  async setupTestData(modulesToTest?: string[]): Promise<void> {
@@ -246,6 +248,10 @@ export class ModuleTestDataService {
246
248
  const entityRepo = this.resolveRepository(modelUserKey);
247
249
  const payload: Record<string, any> = { ...(entry.data ?? {}) };
248
250
 
251
+ if (modelDef.internationalisation && !payload.localeName) {
252
+ payload.localeName = this.internationalisationHelperService.resolveDefaultLocaleName(this.solidRegistry);
253
+ }
254
+
249
255
  for (const field of modelDef.fields ?? []) {
250
256
  if (field.type === 'relation' && field.relationType === 'many-to-one') {
251
257
  const userKeyProp = `${field.name}UserKey`;
@@ -1808,6 +1808,8 @@
1808
1808
  "index": true,
1809
1809
  "private": false,
1810
1810
  "encrypt": false,
1811
+ "regexPattern": "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$",
1812
+ "regexPatternNotMatchingErrorMsg": "Please enter a valid email address.",
1811
1813
  "isSystem": true,
1812
1814
  "enableAuditTracking": true
1813
1815
  },
@@ -7850,6 +7852,8 @@
7850
7852
  "FieldMetadataController.getSelectionDynamicValues",
7851
7853
  "FieldMetadataController.getSelectionDynamicValue",
7852
7854
  "FieldMetadataController.findFieldDefaultMetaData",
7855
+ "MediaController.create",
7856
+ "MediaController.upload",
7853
7857
  "MediaController.findMany",
7854
7858
  "MediaController.findOne",
7855
7859
  "MediaController.download",
@@ -8090,6 +8094,19 @@
8090
8094
  "moduleUserKey": "solid-core",
8091
8095
  "modelUserKey": "media"
8092
8096
  },
8097
+ {
8098
+ "displayName": "Media Upload Action",
8099
+ "name": "media-upload-action",
8100
+ "type": "custom",
8101
+ "domain": "",
8102
+ "context": "",
8103
+ "customComponent": "/admin/core/solid-core/media/upload",
8104
+ "customIsModal": false,
8105
+ "serverEndpoint": "",
8106
+ "viewUserKey": "media-list-view",
8107
+ "moduleUserKey": "solid-core",
8108
+ "modelUserKey": "media"
8109
+ },
8093
8110
  {
8094
8111
  "displayName": "Solid Media Storage Provider Model List Action",
8095
8112
  "name": "mediaStorageProviderMetadata-list-action",
@@ -11565,7 +11582,8 @@
11565
11582
  50
11566
11583
  ],
11567
11584
  "enableGlobalSearch": true,
11568
- "create": false,
11585
+ "create": true,
11586
+ "createAction": "media-upload-action",
11569
11587
  "edit": false,
11570
11588
  "delete": true,
11571
11589
  "allowedViews": [
@@ -11603,6 +11621,15 @@
11603
11621
  "sortable": false
11604
11622
  }
11605
11623
  },
11624
+ {
11625
+ "type": "field",
11626
+ "attrs": {
11627
+ "name": "relativeUri",
11628
+ "viewWidget": "MediaRelativeUriListWidget",
11629
+ "isSearchable": true,
11630
+ "sortable": false
11631
+ }
11632
+ },
11606
11633
  {
11607
11634
  "type": "field",
11608
11635
  "attrs": {
@@ -11759,7 +11786,8 @@
11759
11786
  {
11760
11787
  "type": "field",
11761
11788
  "attrs": {
11762
- "name": "relativeUri"
11789
+ "name": "relativeUri",
11790
+ "viewWidget": "MediaRelativeUriFormViewWidget"
11763
11791
  }
11764
11792
  },
11765
11793
  {
@@ -20,7 +20,7 @@ import { SUCCESS_MESSAGES } from "src/constants/success-messages";
20
20
  import { CreateUserDto } from "src/dtos/create-user.dto";
21
21
  import { MailFactory } from "src/factories/mail.factory";
22
22
  import { UserRepository } from "src/repository/user.repository";
23
- import { DataSource, DeepPartial, Repository } from "typeorm";
23
+ import { DataSource, DeepPartial, FindOptionsWhere, Repository } from "typeorm";
24
24
  import { v4 as uuidv4 } from "uuid";
25
25
  import {
26
26
  ForgotPasswordSendVerificationTokenOn,
@@ -49,6 +49,7 @@ import { SsoCodeStorageService } from "./sso-code-storage.service";
49
49
  import { RoleMetadataService } from "./role-metadata.service";
50
50
  import { SettingService } from "./setting.service";
51
51
  import { UserActivityHistoryService } from "./user-activity-history.service";
52
+ import { MetadataValidationService } from "./metadata-validation.service";
52
53
  import { UserService } from "./user.service";
53
54
  import { SmsFactory } from "src/factories/sms.factory";
54
55
  import { WhatsAppFactory } from "src/factories/whatsapp.factory";
@@ -88,6 +89,7 @@ export class AuthenticationService {
88
89
  private readonly roleMetadataService: RoleMetadataService,
89
90
  private readonly userActivityHistoryService: UserActivityHistoryService,
90
91
  private readonly ssoCodeStorage: SsoCodeStorageService,
92
+ private readonly metadataValidationService: MetadataValidationService,
91
93
 
92
94
  @InjectDataSource()
93
95
  private readonly dataSource: DataSource,
@@ -225,6 +227,9 @@ export class AuthenticationService {
225
227
  preferEntityApiKeyFlag: boolean = false,
226
228
  ): Promise<T> {
227
229
  try {
230
+ await this.assertUniqueSignupIdentifiers(signUpDto, repo);
231
+ await this.metadataValidationService.validateCreateDto("user", signUpDto);
232
+
228
233
  const onForcePasswordChange =
229
234
  this.settingService.getConfigValue<SolidCoreSetting>(
230
235
  "forceChangePasswordOnFirstLogin",
@@ -276,6 +281,59 @@ export class AuthenticationService {
276
281
  }
277
282
  }
278
283
 
284
+ private async assertUniqueSignupIdentifiers<T extends User>(
285
+ signUpDto: SignUpDto,
286
+ repo: Repository<T>,
287
+ ): Promise<void> {
288
+ const username = signUpDto.username?.trim();
289
+ const email = signUpDto.email?.trim();
290
+ const mobile = signUpDto.mobile?.trim();
291
+
292
+ const where: FindOptionsWhere<T>[] = [];
293
+
294
+ if (username) {
295
+ where.push({ username } as FindOptionsWhere<T>);
296
+ }
297
+
298
+ if (email) {
299
+ where.push({ email } as FindOptionsWhere<T>);
300
+ }
301
+
302
+ if (mobile) {
303
+ where.push({ mobile } as FindOptionsWhere<T>);
304
+ }
305
+
306
+ if (where.length === 0) {
307
+ return;
308
+ }
309
+
310
+ const existingUser = await repo.findOne({ where });
311
+
312
+ if (!existingUser) {
313
+ return;
314
+ }
315
+
316
+ if (email && existingUser.email === email) {
317
+ throw new ConflictException(
318
+ `A user with email address "${email}" already exists.`,
319
+ );
320
+ }
321
+
322
+ if (mobile && existingUser.mobile === mobile) {
323
+ throw new ConflictException(
324
+ `A user with mobile number "${mobile}" already exists.`,
325
+ );
326
+ }
327
+
328
+ if (username && existingUser.username === username) {
329
+ throw new ConflictException(
330
+ `A user with username "${username}" already exists.`,
331
+ );
332
+ }
333
+
334
+ throw new ConflictException(ERROR_MESSAGES.USER_ALREADY_EXISTS);
335
+ }
336
+
279
337
  private async handlePasswordlessSignupOtp<T extends User>(
280
338
  user: T,
281
339
  signUpDto: SignUpDto,
@@ -400,11 +458,23 @@ export class AuthenticationService {
400
458
  // if forcePasswordChange is true, then we trigger an email to the user to change the password, this needs to be done using a queue.
401
459
  // Create a new method like notifyUserOnForcePasswordChange, create a new email template we can call it on-force-password-change this template to include the random password
402
460
  if (user.forcePasswordChange && autoGeneratedPwd) {
403
- await this.notifyUserOnForcePasswordChange(user, autoGeneratedPwd);
461
+ try {
462
+ await this.notifyUserOnForcePasswordChange(user, autoGeneratedPwd);
463
+ } catch (error: any) {
464
+ this.logger.error(
465
+ `Failed to send force-password-change email for user ${user.id}: ${error?.message ?? error}`,
466
+ );
467
+ }
404
468
  }
405
469
 
406
470
  // Send welcome notifications (email/SMS) if enabled.
407
- await this.notifyUserOnSignup(user);
471
+ try {
472
+ await this.notifyUserOnSignup(user);
473
+ } catch (error: any) {
474
+ this.logger.error(
475
+ `Failed to send signup notifications for user ${user.id}: ${error?.message ?? error}`,
476
+ );
477
+ }
408
478
  }
409
479
 
410
480
  generatePassword(length: number = 8): string {
@@ -1578,7 +1648,7 @@ export class AuthenticationService {
1578
1648
  // Assuming all users do not have mobile as mandatory.
1579
1649
  if (
1580
1650
  forgotPasswordSendVerificationTokenOn ==
1581
- ForgotPasswordSendVerificationTokenOn.MOBILE &&
1651
+ ForgotPasswordSendVerificationTokenOn.MOBILE &&
1582
1652
  user.mobile
1583
1653
  ) {
1584
1654
  const smsService = this.smsFactory.getSmsService();
@@ -1702,7 +1772,7 @@ export class AuthenticationService {
1702
1772
  // Assuming all users do not have mobile as mandatory.
1703
1773
  if (
1704
1774
  forgotPasswordSendVerificationTokenOn ==
1705
- ForgotPasswordSendVerificationTokenOn.MOBILE &&
1775
+ ForgotPasswordSendVerificationTokenOn.MOBILE &&
1706
1776
  user.mobile
1707
1777
  ) {
1708
1778
  const smsService = this.smsFactory.getSmsService();
@@ -2342,6 +2412,7 @@ function parseUniqueConstraintError(detail: string): string {
2342
2412
  const fieldMap: Record<string, string> = {
2343
2413
  username: "username",
2344
2414
  email: "email address",
2415
+ mobile: "mobile number",
2345
2416
  full_name_user_key: "full name",
2346
2417
  };
2347
2418
  const friendlyField = fieldMap[field] || field;
@@ -6,6 +6,7 @@ import { Brackets, EntityManager, EntityMetadata, In } from 'typeorm';
6
6
 
7
7
  import { classify } from '@angular-devkit/core/src/utils/strings';
8
8
  import { CHATTER_MESSAGE_STATUS, CHATTER_MESSAGE_SUBTYPE, CHATTER_MESSAGE_TYPE, CHATTER_MESSAGE_USER_FIELDS } from 'src/constants/chatter-message.constants';
9
+ import { isDangerousMediaFile } from 'src/constants/media-file-types';
9
10
  import { ERROR_MESSAGES } from 'src/constants/error-messages';
10
11
  import { PostChatterMessageDto } from 'src/dtos/post-chatter-message.dto';
11
12
  import { UpdateChatterNoteMessageDto } from 'src/dtos/update-chatter-note-message.dto';
@@ -174,6 +175,17 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
174
175
  }, messageBody || '');
175
176
  }
176
177
 
178
+ /**
179
+ * Chatter attachments bypass MediaFieldCrudManager - they go straight to
180
+ * storageProvider.store() - so the shared predicate is applied here directly. The rules
181
+ * themselves live in media-file-types so upload paths can't drift apart.
182
+ */
183
+ private validateChatterMediaFiles(files: Express.Multer.File[] = []) {
184
+ if (Array.isArray(files) && files.some(file => isDangerousMediaFile(file))) {
185
+ throw new BadRequestException('Dangerous file types are not allowed in chatter attachments.');
186
+ }
187
+ }
188
+
177
189
  private async publishChatterMentionNotifications(message: ChatterMessage, model: any) {
178
190
  if (message.messageType !== CHATTER_MESSAGE_TYPE.CUSTOM || message.messageSubType !== CHATTER_MESSAGE_SUBTYPE.NOTE) {
179
191
  return;
@@ -305,6 +317,8 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
305
317
  throw new BadRequestException('Message body cannot be empty.');
306
318
  }
307
319
 
320
+ this.validateChatterMediaFiles(files);
321
+
308
322
  if (hasMessageBody) {
309
323
  message.messageBody = trimmedMessageBody;
310
324
  }
@@ -379,6 +393,8 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
379
393
  const coModelName = lowerFirst(postDto.coModelName);
380
394
  await this.assertRecordAccess(coModelName, postDto.coModelEntityId);
381
395
 
396
+ this.validateChatterMediaFiles(files);
397
+
382
398
  const chatterMessage = new ChatterMessage();
383
399
  chatterMessage.messageType = CHATTER_MESSAGE_TYPE.CUSTOM;
384
400
  chatterMessage.messageSubType = postDto.messageSubType || CHATTER_MESSAGE_SUBTYPE.CUSTOM;
@@ -3,9 +3,12 @@ import { BasicFilterDto } from "../dtos/basic-filters.dto";
3
3
  import { classify } from '../helpers/string.helper';
4
4
  import { ActiveUserData } from "src/interfaces/active-user-data.interface";
5
5
  import { SolidRegistry } from "src/helpers/solid-registry";
6
- import { BadRequestException, Logger } from "@nestjs/common";
6
+ import { BadRequestException, Injectable, Logger } from "@nestjs/common";
7
7
  import { ERROR_MESSAGES } from "src/constants/error-messages";
8
8
  import { buildCastToText } from "src/helpers/typeorm-db-helper";
9
+ import { DraftPublishHelperService } from "./draft-publish-helper.service";
10
+ import { InternationalisationHelperService } from "./internationalisation-helper.service";
11
+ import { normalizeObjectKeys } from "./object.utils";
9
12
 
10
13
  export enum FilterCombinator {
11
14
  AND = '$and',
@@ -36,10 +39,14 @@ export interface ResolvedFieldPath {
36
39
  leafIsRelation: boolean;
37
40
  }
38
41
 
42
+ @Injectable()
39
43
  export class CrudHelperService {
44
+ private readonly logger = new Logger(CrudHelperService.name);
45
+
40
46
  constructor(
47
+ private readonly draftPublishHelperService: DraftPublishHelperService,
48
+ private readonly internationalisationHelperService: InternationalisationHelperService,
41
49
  ) { }
42
- private readonly logger = new Logger(CrudHelperService.name);
43
50
 
44
51
  /**
45
52
  * Resolve a user-supplied dotted path (e.g. "customer.name") against real TypeORM metadata.
@@ -163,7 +170,7 @@ export class CrudHelperService {
163
170
  }
164
171
 
165
172
  applyFilters(qb: WhereExpressionBuilder, filters: any, alias: string = 'entity', selectQb: SelectQueryBuilder<any>) {
166
- const normalizedFilters = this.normalizeObjectKeys(filters);
173
+ const normalizedFilters = normalizeObjectKeys(filters);
167
174
  if (normalizedFilters.$and) {
168
175
  normalizedFilters.$and.forEach((andFilter: any) => {
169
176
  qb.andWhere(
@@ -182,7 +189,7 @@ export class CrudHelperService {
182
189
  // For individual conditions
183
190
  Object.keys(normalizedFilters).forEach(key => {
184
191
  const primaryFilterObj = normalizedFilters[key];
185
- const normalizedPrimaryFilterObj = this.normalizeObjectKeys(primaryFilterObj);
192
+ const normalizedPrimaryFilterObj = normalizeObjectKeys(primaryFilterObj);
186
193
 
187
194
  const [rawField, funcAlias] = key.split(':');
188
195
 
@@ -312,17 +319,6 @@ export class CrudHelperService {
312
319
  }
313
320
  }
314
321
 
315
- // Normalize the primary operator object by removing the surrounding brackets in the keys e.g [$eq] => $eq
316
- private normalizeObjectKeys(obj: any): any {
317
- return Object.keys(obj).reduce((acc, key) => {
318
- // Transform the key by removing surrounding brackets
319
- const newKey = key.replace(/^\[(.*)\]$/, '$1');
320
- // Assign the value to the transformed key in the accumulator object
321
- acc[newKey] = obj[key];
322
- return acc;
323
- }, {});
324
- }
325
-
326
322
  normalize(value: string | string[]): string[] {
327
323
  if (!value) return []; // if the value is nullish, then return an empty array
328
324
  return Array.isArray(value) ? value : [value]; // if the value is an array, return it as is, otherwise return it as an array
@@ -359,7 +355,7 @@ export class CrudHelperService {
359
355
  applySorting: boolean = true
360
356
  ): SelectQueryBuilder<any> { // TODO : Check how to pass a type to SelectQueryBuilder instead of any
361
357
  let { limit, offset, showSoftDeleted, filters } = basicFilterDto;
362
- const { fields, sort, populate = [], populateMedia = [], locale, status } = basicFilterDto;
358
+ const { fields, sort, populate = [], populateMedia = [], locale } = basicFilterDto;
363
359
 
364
360
  // Normalize the fields, sort, groupBy and populate options i.e (since they can be either a string or an array of strings, when coming from the request)
365
361
  const normalizedFields = this.normalize(fields);
@@ -390,28 +386,14 @@ export class CrudHelperService {
390
386
  }
391
387
  }
392
388
 
393
- let finalLocale = locale
394
389
  if (internationalisation) {
395
- // If locale is not provided in the filter dto, then assume it is the default locale to be used.
396
- if (!finalLocale) {
397
- //Get default locale from registry
398
- const solidRegistry = moduleRef.get(SolidRegistry, { strict: false });
399
- const defaultLocale = solidRegistry.getDefaultLocale();
400
- if(defaultLocale){
401
- finalLocale = defaultLocale.locale;
402
- }else{
403
- finalLocale = 'en';
404
- }
405
- }
390
+ // If locale is not provided in the filter dto, then assume it is the default locale to be used.
391
+ const finalLocale = locale || this.internationalisationHelperService.resolveDefaultLocaleName(moduleRef.get(SolidRegistry, { strict: false }));
406
392
  qb.andWhere(`${entityAlias}.localeName = :locale`, { locale: finalLocale });
407
393
  }
408
394
 
409
- if (draftPublishWorkflow && status) {
410
- if (basicFilterDto.status === 'published') {
411
- qb.andWhere(`${entityAlias}.publishedAt IS NOT NULL`);
412
- } else if (basicFilterDto.status === 'draft') {
413
- qb.andWhere(`${entityAlias}.publishedAt IS NULL`);
414
- }
395
+ if (draftPublishWorkflow) {
396
+ this.draftPublishHelperService.applyDraftPublishFilterDefaults(qb, filters, entityAlias);
415
397
  }
416
398
  // Depending upon the select option, apply the select clause
417
399
  if (normalizedFields && normalizedFields.length) {