@solidxai/core 0.1.13-beta.9 → 0.1.13

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 (255) hide show
  1. package/CHANGELOG.md +7465 -0
  2. package/README.md +19 -0
  3. package/dist/commands/run-tests.command.d.ts +2 -0
  4. package/dist/commands/run-tests.command.d.ts.map +1 -1
  5. package/dist/commands/run-tests.command.js +20 -3
  6. package/dist/commands/run-tests.command.js.map +1 -1
  7. package/dist/config/cache.options.d.ts.map +1 -1
  8. package/dist/config/cache.options.js +2 -2
  9. package/dist/config/cache.options.js.map +1 -1
  10. package/dist/constants/error-messages.d.ts +7 -0
  11. package/dist/constants/error-messages.d.ts.map +1 -1
  12. package/dist/constants/error-messages.js +7 -0
  13. package/dist/constants/error-messages.js.map +1 -1
  14. package/dist/constants/media-file-types.d.ts +3 -0
  15. package/dist/constants/media-file-types.d.ts.map +1 -1
  16. package/dist/constants/media-file-types.js +47 -8
  17. package/dist/constants/media-file-types.js.map +1 -1
  18. package/dist/constants/success-messages.d.ts +3 -0
  19. package/dist/constants/success-messages.d.ts.map +1 -1
  20. package/dist/constants/success-messages.js +3 -0
  21. package/dist/constants/success-messages.js.map +1 -1
  22. package/dist/controllers/authentication.controller.d.ts +1 -1
  23. package/dist/controllers/authentication.controller.d.ts.map +1 -1
  24. package/dist/controllers/authentication.controller.js +4 -3
  25. package/dist/controllers/authentication.controller.js.map +1 -1
  26. package/dist/controllers/field-metadata.controller.d.ts +2 -2
  27. package/dist/controllers/mpin-authentication.controller.d.ts +44 -0
  28. package/dist/controllers/mpin-authentication.controller.d.ts.map +1 -0
  29. package/dist/controllers/mpin-authentication.controller.js +102 -0
  30. package/dist/controllers/mpin-authentication.controller.js.map +1 -0
  31. package/dist/dtos/change-mpin.dto.d.ts +6 -0
  32. package/dist/dtos/change-mpin.dto.d.ts.map +1 -0
  33. package/dist/dtos/change-mpin.dto.js +39 -0
  34. package/dist/dtos/change-mpin.dto.js.map +1 -0
  35. package/dist/dtos/create-field-metadata.dto.d.ts +3 -6
  36. package/dist/dtos/create-field-metadata.dto.d.ts.map +1 -1
  37. package/dist/dtos/create-field-metadata.dto.js +15 -10
  38. package/dist/dtos/create-field-metadata.dto.js.map +1 -1
  39. package/dist/dtos/create-model-sequence.dto.d.ts +1 -1
  40. package/dist/dtos/create-model-sequence.dto.d.ts.map +1 -1
  41. package/dist/dtos/create-model-sequence.dto.js +1 -1
  42. package/dist/dtos/create-model-sequence.dto.js.map +1 -1
  43. package/dist/dtos/mpin-login.dto.d.ts +5 -0
  44. package/dist/dtos/mpin-login.dto.d.ts.map +1 -0
  45. package/dist/dtos/mpin-login.dto.js +33 -0
  46. package/dist/dtos/mpin-login.dto.js.map +1 -0
  47. package/dist/dtos/setup-mpin.dto.d.ts +7 -0
  48. package/dist/dtos/setup-mpin.dto.d.ts.map +1 -0
  49. package/dist/dtos/setup-mpin.dto.js +47 -0
  50. package/dist/dtos/setup-mpin.dto.js.map +1 -0
  51. package/dist/entities/dashboard-user-layout.entity.js +2 -2
  52. package/dist/entities/dashboard-user-layout.entity.js.map +1 -1
  53. package/dist/entities/field-metadata.entity.d.ts +1 -0
  54. package/dist/entities/field-metadata.entity.d.ts.map +1 -1
  55. package/dist/entities/field-metadata.entity.js +5 -1
  56. package/dist/entities/field-metadata.entity.js.map +1 -1
  57. package/dist/entities/user-device-credential.entity.d.ts +18 -0
  58. package/dist/entities/user-device-credential.entity.d.ts.map +1 -0
  59. package/dist/entities/user-device-credential.entity.js +91 -0
  60. package/dist/entities/user-device-credential.entity.js.map +1 -0
  61. package/dist/entities/user.entity.js +2 -2
  62. package/dist/entities/user.entity.js.map +1 -1
  63. package/dist/entities/view-metadata.entity.js.map +1 -1
  64. package/dist/entities/workflow-execution-artifact.entity.d.ts.map +1 -1
  65. package/dist/entities/workflow-execution-artifact.entity.js +4 -3
  66. package/dist/entities/workflow-execution-artifact.entity.js.map +1 -1
  67. package/dist/entities/workflow-execution-log.entity.d.ts.map +1 -1
  68. package/dist/entities/workflow-execution-log.entity.js +4 -3
  69. package/dist/entities/workflow-execution-log.entity.js.map +1 -1
  70. package/dist/entities/workflow-execution.entity.d.ts.map +1 -1
  71. package/dist/entities/workflow-execution.entity.js +3 -2
  72. package/dist/entities/workflow-execution.entity.js.map +1 -1
  73. package/dist/entities/workflow-secret.entity.d.ts.map +1 -1
  74. package/dist/entities/workflow-secret.entity.js +3 -2
  75. package/dist/entities/workflow-secret.entity.js.map +1 -1
  76. package/dist/entities/workflow-step-execution.entity.d.ts.map +1 -1
  77. package/dist/entities/workflow-step-execution.entity.js +3 -2
  78. package/dist/entities/workflow-step-execution.entity.js.map +1 -1
  79. package/dist/entities/workflow-trigger-execution.entity.d.ts.map +1 -1
  80. package/dist/entities/workflow-trigger-execution.entity.js +6 -5
  81. package/dist/entities/workflow-trigger-execution.entity.js.map +1 -1
  82. package/dist/filters/http-exception.filter.d.ts.map +1 -1
  83. package/dist/filters/http-exception.filter.js +5 -1
  84. package/dist/filters/http-exception.filter.js.map +1 -1
  85. package/dist/helpers/bootstrap.helper.d.ts +5 -2
  86. package/dist/helpers/bootstrap.helper.d.ts.map +1 -1
  87. package/dist/helpers/bootstrap.helper.js +7 -3
  88. package/dist/helpers/bootstrap.helper.js.map +1 -1
  89. package/dist/helpers/field-crud-managers/MediaFieldCrudManager.d.ts +6 -0
  90. package/dist/helpers/field-crud-managers/MediaFieldCrudManager.d.ts.map +1 -1
  91. package/dist/helpers/field-crud-managers/MediaFieldCrudManager.js +54 -27
  92. package/dist/helpers/field-crud-managers/MediaFieldCrudManager.js.map +1 -1
  93. package/dist/helpers/file-download.helper.d.ts.map +1 -1
  94. package/dist/helpers/file-download.helper.js +6 -1
  95. package/dist/helpers/file-download.helper.js.map +1 -1
  96. package/dist/helpers/security.helper.d.ts +5 -1
  97. package/dist/helpers/security.helper.d.ts.map +1 -1
  98. package/dist/helpers/security.helper.js +15 -4
  99. package/dist/helpers/security.helper.js.map +1 -1
  100. package/dist/helpers/solid-core-error-codes-provider.service.d.ts.map +1 -1
  101. package/dist/helpers/solid-core-error-codes-provider.service.js +45 -0
  102. package/dist/helpers/solid-core-error-codes-provider.service.js.map +1 -1
  103. package/dist/helpers/solid-microservice-adapter.service.d.ts.map +1 -1
  104. package/dist/helpers/solid-microservice-adapter.service.js +1 -1
  105. package/dist/helpers/solid-microservice-adapter.service.js.map +1 -1
  106. package/dist/helpers/typeorm-db-helper.d.ts.map +1 -1
  107. package/dist/helpers/typeorm-db-helper.js +9 -0
  108. package/dist/helpers/typeorm-db-helper.js.map +1 -1
  109. package/dist/index.d.ts +6 -0
  110. package/dist/index.d.ts.map +1 -1
  111. package/dist/index.js +6 -0
  112. package/dist/index.js.map +1 -1
  113. package/dist/interfaces/active-user-data.interface.d.ts +1 -0
  114. package/dist/interfaces/active-user-data.interface.d.ts.map +1 -1
  115. package/dist/interfaces/active-user-data.interface.js.map +1 -1
  116. package/dist/repository/user-device-credential.repository.d.ts +12 -0
  117. package/dist/repository/user-device-credential.repository.d.ts.map +1 -0
  118. package/dist/repository/user-device-credential.repository.js +34 -0
  119. package/dist/repository/user-device-credential.repository.js.map +1 -0
  120. package/dist/seeders/module-metadata-seeder.service.d.ts.map +1 -1
  121. package/dist/seeders/module-metadata-seeder.service.js +7 -3
  122. package/dist/seeders/module-metadata-seeder.service.js.map +1 -1
  123. package/dist/seeders/seed-data/solid-core-metadata.json +168 -2
  124. package/dist/services/api-key.service.d.ts.map +1 -1
  125. package/dist/services/api-key.service.js +1 -1
  126. package/dist/services/api-key.service.js.map +1 -1
  127. package/dist/services/authentication.service.d.ts +5 -4
  128. package/dist/services/authentication.service.d.ts.map +1 -1
  129. package/dist/services/authentication.service.js +50 -19
  130. package/dist/services/authentication.service.js.map +1 -1
  131. package/dist/services/chatter-message.service.d.ts +5 -1
  132. package/dist/services/chatter-message.service.d.ts.map +1 -1
  133. package/dist/services/chatter-message.service.js +48 -13
  134. package/dist/services/chatter-message.service.js.map +1 -1
  135. package/dist/services/crud-helper.service.d.ts +1 -2
  136. package/dist/services/crud-helper.service.d.ts.map +1 -1
  137. package/dist/services/crud-helper.service.js +2 -16
  138. package/dist/services/crud-helper.service.js.map +1 -1
  139. package/dist/services/crud.service.d.ts.map +1 -1
  140. package/dist/services/crud.service.js +2 -1
  141. package/dist/services/crud.service.js.map +1 -1
  142. package/dist/services/draft-publish-helper.service.d.ts +1 -0
  143. package/dist/services/draft-publish-helper.service.d.ts.map +1 -1
  144. package/dist/services/draft-publish-helper.service.js +38 -4
  145. package/dist/services/draft-publish-helper.service.js.map +1 -1
  146. package/dist/services/field-metadata.service.d.ts +2 -2
  147. package/dist/services/field-metadata.service.d.ts.map +1 -1
  148. package/dist/services/field-metadata.service.js +4 -1
  149. package/dist/services/field-metadata.service.js.map +1 -1
  150. package/dist/services/media-storage.utils.d.ts +1 -0
  151. package/dist/services/media-storage.utils.d.ts.map +1 -1
  152. package/dist/services/media-storage.utils.js +12 -1
  153. package/dist/services/media-storage.utils.js.map +1 -1
  154. package/dist/services/media.service.d.ts.map +1 -1
  155. package/dist/services/media.service.js +1 -0
  156. package/dist/services/media.service.js.map +1 -1
  157. package/dist/services/module-metadata.service.d.ts.map +1 -1
  158. package/dist/services/module-metadata.service.js +9 -1
  159. package/dist/services/module-metadata.service.js.map +1 -1
  160. package/dist/services/mpin.service.d.ts +67 -0
  161. package/dist/services/mpin.service.d.ts.map +1 -0
  162. package/dist/services/mpin.service.js +282 -0
  163. package/dist/services/mpin.service.js.map +1 -0
  164. package/dist/services/refresh-token-ids-storage.service.d.ts +25 -6
  165. package/dist/services/refresh-token-ids-storage.service.d.ts.map +1 -1
  166. package/dist/services/refresh-token-ids-storage.service.js +80 -42
  167. package/dist/services/refresh-token-ids-storage.service.js.map +1 -1
  168. package/dist/services/setting.service.d.ts.map +1 -1
  169. package/dist/services/setting.service.js +6 -1
  170. package/dist/services/setting.service.js.map +1 -1
  171. package/dist/services/settings/default-settings-provider.service.d.ts +202 -6
  172. package/dist/services/settings/default-settings-provider.service.d.ts.map +1 -1
  173. package/dist/services/settings/default-settings-provider.service.js +114 -3
  174. package/dist/services/settings/default-settings-provider.service.js.map +1 -1
  175. package/dist/solid-core.module.d.ts +1 -3
  176. package/dist/solid-core.module.d.ts.map +1 -1
  177. package/dist/solid-core.module.js +14 -39
  178. package/dist/solid-core.module.js.map +1 -1
  179. package/dist/testing/reporter/console-reporter.d.ts +3 -0
  180. package/dist/testing/reporter/console-reporter.d.ts.map +1 -1
  181. package/dist/testing/reporter/console-reporter.js +2 -0
  182. package/dist/testing/reporter/console-reporter.js.map +1 -1
  183. package/dist/testing/reporter/progress-reporter.d.ts +64 -0
  184. package/dist/testing/reporter/progress-reporter.d.ts.map +1 -0
  185. package/dist/testing/reporter/progress-reporter.js +224 -0
  186. package/dist/testing/reporter/progress-reporter.js.map +1 -0
  187. package/dist/testing/reporter/reporter.types.d.ts +3 -0
  188. package/dist/testing/reporter/reporter.types.d.ts.map +1 -1
  189. package/dist/testing/reporter/reporter.types.js.map +1 -1
  190. package/dist/testing/runner/run-from-metadata.d.ts.map +1 -1
  191. package/dist/testing/runner/run-from-metadata.js +1 -0
  192. package/dist/testing/runner/run-from-metadata.js.map +1 -1
  193. package/dist/theme/theme-registry.d.ts +14 -0
  194. package/dist/theme/theme-registry.d.ts.map +1 -0
  195. package/dist/theme/theme-registry.js +147 -0
  196. package/dist/theme/theme-registry.js.map +1 -0
  197. package/package.json +2 -2
  198. package/src/commands/run-tests.command.ts +17 -3
  199. package/src/config/cache.options.ts +9 -1
  200. package/src/constants/error-messages.ts +14 -0
  201. package/src/constants/media-file-types.ts +87 -9
  202. package/src/constants/success-messages.ts +5 -0
  203. package/src/controllers/authentication.controller.ts +5 -2
  204. package/src/controllers/mpin-authentication.controller.ts +67 -0
  205. package/src/dtos/change-mpin.dto.ts +18 -0
  206. package/src/dtos/create-field-metadata.dto.ts +17 -9
  207. package/src/dtos/create-model-sequence.dto.ts +1 -1
  208. package/src/dtos/mpin-login.dto.ts +24 -0
  209. package/src/dtos/setup-mpin.dto.ts +51 -0
  210. package/src/entities/dashboard-user-layout.entity.ts +2 -2
  211. package/src/entities/field-metadata.entity.ts +3 -0
  212. package/src/entities/user-device-credential.entity.ts +130 -0
  213. package/src/entities/user.entity.ts +2 -2
  214. package/src/entities/view-metadata.entity.ts +2 -2
  215. package/src/entities/workflow-execution-artifact.entity.ts +4 -3
  216. package/src/entities/workflow-execution-log.entity.ts +4 -3
  217. package/src/entities/workflow-execution.entity.ts +3 -2
  218. package/src/entities/workflow-secret.entity.ts +3 -2
  219. package/src/entities/workflow-step-execution.entity.ts +3 -2
  220. package/src/entities/workflow-trigger-execution.entity.ts +6 -5
  221. package/src/filters/http-exception.filter.ts +11 -1
  222. package/src/helpers/bootstrap.helper.ts +43 -6
  223. package/src/helpers/field-crud-managers/MediaFieldCrudManager.ts +73 -34
  224. package/src/helpers/file-download.helper.ts +15 -1
  225. package/src/helpers/security.helper.ts +35 -8
  226. package/src/helpers/solid-core-error-codes-provider.service.ts +53 -0
  227. package/src/helpers/solid-microservice-adapter.service.ts +6 -1
  228. package/src/helpers/typeorm-db-helper.ts +12 -1
  229. package/src/index.ts +6 -0
  230. package/src/interfaces/active-user-data.interface.ts +9 -0
  231. package/src/repository/user-device-credential.repository.ts +17 -0
  232. package/src/seeders/module-metadata-seeder.service.ts +11 -3
  233. package/src/seeders/seed-data/solid-core-metadata.json +168 -2
  234. package/src/services/api-key.service.ts +13 -1
  235. package/src/services/authentication.service.ts +127 -36
  236. package/src/services/chatter-message.service.ts +78 -15
  237. package/src/services/crud-helper.service.ts +19 -16
  238. package/src/services/crud.service.ts +3 -1
  239. package/src/services/draft-publish-helper.service.ts +56 -4
  240. package/src/services/field-metadata.service.ts +5 -2
  241. package/src/services/media-storage.utils.ts +26 -1
  242. package/src/services/media.service.ts +1 -0
  243. package/src/services/module-metadata.service.ts +10 -1
  244. package/src/services/mpin.service.ts +447 -0
  245. package/src/services/refresh-token-ids-storage.service.ts +229 -90
  246. package/src/services/setting.service.ts +8 -1
  247. package/src/services/settings/default-settings-provider.service.ts +119 -3
  248. package/src/solid-core.module.ts +26 -12
  249. package/src/testing/README.md +2 -1
  250. package/src/testing/reporter/console-reporter.ts +4 -0
  251. package/src/testing/reporter/progress-reporter.ts +251 -0
  252. package/src/testing/reporter/reporter.types.ts +3 -0
  253. package/src/testing/runner/run-from-metadata.ts +1 -0
  254. package/src/theme/theme-registry.ts +181 -0
  255. package/CLAUDE.md +0 -26
@@ -131,7 +131,19 @@ export class ApiKeyService {
131
131
 
132
132
  const user = apiKeyRecord.user;
133
133
 
134
- const tokens = await this.authenticationService.generateTokens(user);
134
+ // Keyed on the API key record so every call for this key reuses one
135
+ // refresh-token bucket. Without it each call mints a fresh key and
136
+ // therefore a new bucket with a refreshTokenTtl lifetime - a client
137
+ // polling this endpoint would accumulate them without bound.
138
+ //
139
+ // The consequence is that replicas sharing one API key share one
140
+ // bucket and invalidate each other, which is exactly the behaviour
141
+ // before per-device sessions existed, and is benign here: an API key
142
+ // is a permanent credential, so a failed refresh just re-mints.
143
+ const tokens = await this.authenticationService.generateTokens(
144
+ user,
145
+ `apikey-${apiKeyRecord.id}`,
146
+ );
135
147
 
136
148
 
137
149
  return {
@@ -1793,7 +1793,13 @@ export class AuthenticationService {
1793
1793
  }
1794
1794
  }
1795
1795
 
1796
- async generateTokens(user: User) {
1796
+ /**
1797
+ * @param deviceId optional stable per-device identifier. Supplied by callers
1798
+ * that own a device credential; otherwise a key is minted per login so that
1799
+ * clients with no device concept (the web UI) still get their own bucket
1800
+ * instead of sharing - and evicting - one.
1801
+ */
1802
+ async generateTokens(user: User, deviceId?: string) {
1797
1803
  const sessionId = this.shouldPreventConcurrentLogins()
1798
1804
  ? randomUUID()
1799
1805
  : undefined;
@@ -1802,9 +1808,17 @@ export class AuthenticationService {
1802
1808
  } else {
1803
1809
  await this.activeSessionStorage.clearActiveSession(user.id);
1804
1810
  }
1811
+
1812
+ // Mutually exclusive with sessionId by construction: per-device buckets
1813
+ // exist only when preventConcurrentLogins is off, sessionId only when it
1814
+ // is on.
1815
+ const deviceKey = this.refreshTokenIdsStorage.areConcurrentLoginsAllowed()
1816
+ ? deviceId ?? randomUUID()
1817
+ : undefined;
1818
+
1805
1819
  const [accessToken, refreshToken] = await Promise.all([
1806
- await this.generateAccessToken(user, sessionId),
1807
- await this.generateRefreshToken(user),
1820
+ await this.generateAccessToken(user, sessionId, deviceKey),
1821
+ await this.generateRefreshToken(user, undefined, deviceKey),
1808
1822
  ]);
1809
1823
 
1810
1824
  return {
@@ -1813,7 +1827,7 @@ export class AuthenticationService {
1813
1827
  };
1814
1828
  }
1815
1829
 
1816
- async generateAccessToken(user: User, sessionId?: string) {
1830
+ async generateAccessToken(user: User, sessionId?: string, deviceKey?: string) {
1817
1831
  // const userRoleNames = user.roles.map((role) => role.name).join(';')
1818
1832
  const userRoleNames = user.roles.map((role) => role.name);
1819
1833
  const resolvedSessionId = this.shouldPreventConcurrentLogins()
@@ -1830,18 +1844,36 @@ export class AuthenticationService {
1830
1844
  email: user.email,
1831
1845
  roles: userRoleNames,
1832
1846
  ...(resolvedSessionId ? { sessionId: resolvedSessionId } : {}),
1847
+ // Carried so that bearer-authenticated endpoints which need this
1848
+ // session's refresh token - generateSsoCode, me - can locate its
1849
+ // bucket. The refresh token is not available to them.
1850
+ ...(deviceKey ? { deviceKey } : {}),
1833
1851
  },
1834
1852
  );
1835
1853
 
1836
1854
  return accessToken;
1837
1855
  }
1838
1856
 
1839
- async generateRefreshToken(user: User, previousRefreshToken?: string) {
1857
+ async generateRefreshToken(
1858
+ user: User,
1859
+ previousRefreshToken?: string,
1860
+ deviceKey?: string,
1861
+ ) {
1840
1862
  const refreshTokenId = randomUUID();
1841
1863
  const refreshTokenTtl =
1842
1864
  this.settingService.getConfigValue<SolidCoreSetting>("refreshTokenTtl");
1865
+
1866
+ // Only per-device tokens carry an epoch - it is what makes bulk
1867
+ // invalidation possible without enumerating buckets. Single-slot tokens
1868
+ // are invalidated by deleting their one key.
1869
+ const epoch = deviceKey
1870
+ ? await this.refreshTokenIdsStorage.getEpoch(user.id)
1871
+ : undefined;
1872
+
1843
1873
  const refreshToken = await this.signToken(user.id, refreshTokenTtl, {
1844
1874
  refreshTokenId,
1875
+ ...(deviceKey ? { deviceKey } : {}),
1876
+ ...(epoch !== undefined ? { epoch } : {}),
1845
1877
  });
1846
1878
 
1847
1879
  // store the refresh token id in the redis storage.
@@ -1849,6 +1881,7 @@ export class AuthenticationService {
1849
1881
  user.id,
1850
1882
  refreshToken,
1851
1883
  previousRefreshToken,
1884
+ deviceKey,
1852
1885
  );
1853
1886
 
1854
1887
  return refreshToken;
@@ -1863,8 +1896,16 @@ export class AuthenticationService {
1863
1896
  const issuer =
1864
1897
  this.settingService.getConfigValue<SolidCoreSetting>("issuer");
1865
1898
 
1866
- const { sub } = await this.jwtService.verifyAsync<
1867
- Pick<ActiveUserData, "sub"> & { refreshTokenId: string }
1899
+ // deviceKey/epoch are absent on tokens issued before per-device sessions,
1900
+ // and on tokens issued while preventConcurrentLogins is on. That absence
1901
+ // is the signal the storage layer branches on - no request-body change
1902
+ // and no client change is needed to carry it.
1903
+ const { sub, deviceKey, epoch } = await this.jwtService.verifyAsync<
1904
+ Pick<ActiveUserData, "sub"> & {
1905
+ refreshTokenId: string;
1906
+ deviceKey?: string;
1907
+ epoch?: number;
1908
+ }
1868
1909
  >(refreshTokenDto.refreshToken, {
1869
1910
  secret,
1870
1911
  audience,
@@ -1892,17 +1933,24 @@ export class AuthenticationService {
1892
1933
  // throw new Error('Refresh token is invalid');
1893
1934
  // }
1894
1935
 
1895
- const currentRefreshToken =
1896
- await this.refreshTokenIdsStorage.validateAndRotate(
1897
- user,
1898
- refreshTokenDto.refreshToken,
1899
- );
1936
+ const rotated = await this.refreshTokenIdsStorage.validateAndRotate(
1937
+ user,
1938
+ refreshTokenDto.refreshToken,
1939
+ { deviceKey, epoch },
1940
+ );
1900
1941
 
1901
1942
  await this.userActivityHistoryService.logEvent("tokenRefreshed", user);
1902
1943
 
1903
1944
  return {
1904
- accessToken: await this.generateAccessToken(user),
1905
- refreshToken: currentRefreshToken,
1945
+ // The rotated key, not the incoming one: a session migrating out of the
1946
+ // pre-deploy single slot is assigned a bucket here, and the new access
1947
+ // token has to name the same one.
1948
+ accessToken: await this.generateAccessToken(
1949
+ user,
1950
+ undefined,
1951
+ rotated.deviceKey,
1952
+ ),
1953
+ refreshToken: rotated.refreshToken,
1906
1954
  };
1907
1955
  } catch (err: any) {
1908
1956
  if (err instanceof InvalidatedRefreshTokenError) {
@@ -2291,28 +2339,31 @@ export class AuthenticationService {
2291
2339
  // // Invalidate the refresh token
2292
2340
  // // await this.refreshTokenIdsStorage.invalidate(user.id);
2293
2341
  // }
2294
- async logout(refreshToken: string) {
2342
+ async logout(refreshToken: string, allDevices = false) {
2295
2343
  try {
2296
- // const activeUser = this.requestContextService.getActiveUser();
2297
- // const userId = activeUser?.sub;
2298
- // const user = await this.userRepository.findOne({
2299
- // where: {
2300
- // id: userId,
2301
- // }
2302
- // })
2303
- // // Invalidate refresh token if you store them
2304
- // await this.refreshTokenIdsStorage.invalidate(userId); // Your existing logic
2305
- // if (!refreshToken) {
2306
- // throw new UnauthorizedException('Refresh token is required');
2307
- // }
2308
- const payload = this.jwtService.decode(refreshToken) as any;
2309
-
2310
- if (!payload || !payload.sub) {
2311
- throw new UnauthorizedException(ERROR_MESSAGES.INVALID_REFRESH_TOKEN);
2344
+ const payload = await this.verifyRefreshTokenForLogout(refreshToken);
2345
+
2346
+ // Nothing verifiable to act on: forged, malformed, or signed with another
2347
+ // secret. Acting on an unauthenticated `sub` is exactly the hole this
2348
+ // closes - the route is @Public(), so a decoded-but-unverified token let
2349
+ // anyone log out an arbitrary user. Report success either way: logout
2350
+ // must be safe to call blindly from a client error path, and a 401/200
2351
+ // split here would leak whether a token string is genuine (RFC 7009 §2.2
2352
+ // takes the same position for revocation endpoints).
2353
+ if (!payload?.sub) {
2354
+ this.logger.warn("logout called with an unverifiable refresh token");
2355
+ return { message: SUCCESS_MESSAGES.LOGOUT_SUCCESS };
2312
2356
  }
2313
2357
 
2314
2358
  const userId = payload.sub;
2315
- await this.refreshTokenIdsStorage.invalidate(userId);
2359
+ if (allDevices) {
2360
+ await this.refreshTokenIdsStorage.invalidateAll(userId);
2361
+ } else {
2362
+ // Scoped to the bucket this token belongs to, so signing out on one
2363
+ // device leaves the others alive. A token with no deviceKey names the
2364
+ // single slot, which is the same key as before.
2365
+ await this.refreshTokenIdsStorage.invalidate(userId, payload.deviceKey);
2366
+ }
2316
2367
  await this.activeSessionStorage.clearActiveSession(userId);
2317
2368
  const user = await this.userRepository.findOne({
2318
2369
  where: {
@@ -2320,10 +2371,15 @@ export class AuthenticationService {
2320
2371
  },
2321
2372
  });
2322
2373
  // Log logout event
2323
- await this.userActivityHistoryService.logEvent("logout", user);
2374
+ if (user) {
2375
+ await this.userActivityHistoryService.logEvent("logout", user);
2376
+ }
2324
2377
 
2325
2378
  return { message: SUCCESS_MESSAGES.LOGOUT_SUCCESS };
2326
2379
  } catch (err: any) {
2380
+ // JWT problems no longer reach here - verifyRefreshTokenForLogout
2381
+ // swallows them - so this is left for genuine infrastructure failures
2382
+ // (cache or database unavailable), where a 500 is the correct answer.
2327
2383
  throw err instanceof UnauthorizedException ||
2328
2384
  err instanceof InternalServerErrorException
2329
2385
  ? err
@@ -2331,6 +2387,31 @@ export class AuthenticationService {
2331
2387
  }
2332
2388
  }
2333
2389
 
2390
+ private async verifyRefreshTokenForLogout(
2391
+ refreshToken: string,
2392
+ ): Promise<{ sub: number; deviceKey?: string } | null> {
2393
+ try {
2394
+ const payload = await this.jwtService.verifyAsync(refreshToken, {
2395
+ secret: this.settingService.getConfigValue<SolidCoreSetting>("secret"),
2396
+ audience:
2397
+ this.settingService.getConfigValue<SolidCoreSetting>("audience"),
2398
+ issuer: this.settingService.getConfigValue<SolidCoreSetting>("issuer"),
2399
+ // Signature, audience and issuer are still enforced; only `exp` is
2400
+ // tolerated. An expired session must still be able to log out - the
2401
+ // previous jwtService.decode() allowed that, and a plain verifyAsync
2402
+ // would throw TokenExpiredError, which the catch above would turn into
2403
+ // a 500 on an ordinary sign-out.
2404
+ ignoreExpiration: true,
2405
+ });
2406
+
2407
+ // Access and refresh tokens share secret, audience and issuer, so only
2408
+ // this claim distinguishes them. Reject an access token presented here.
2409
+ return payload?.refreshTokenId ? payload : null;
2410
+ } catch {
2411
+ return null; // bad signature or malformed
2412
+ }
2413
+ }
2414
+
2334
2415
  async activateUser(userId: number) {
2335
2416
  const user = await this.userService.findOne(userId, {});
2336
2417
  if (!user) {
@@ -2352,9 +2433,14 @@ export class AuthenticationService {
2352
2433
 
2353
2434
  // const tokens = await this.generateTokens(user);
2354
2435
 
2355
- // Get the refresh token for a user from refresh token storage.
2436
+ // Get the refresh token for a user from refresh token storage. The bucket
2437
+ // is named by the access token's own deviceKey claim; absent for
2438
+ // single-slot sessions, which resolves to the same key as before.
2356
2439
  const refreshTokenState =
2357
- await this.refreshTokenIdsStorage.getCurrentRefreshTokenState(user.id);
2440
+ await this.refreshTokenIdsStorage.getCurrentRefreshTokenState(
2441
+ user.id,
2442
+ activeUser.deviceKey,
2443
+ );
2358
2444
 
2359
2445
  const response = {
2360
2446
  user: {
@@ -2365,7 +2451,11 @@ export class AuthenticationService {
2365
2451
  id: user.id,
2366
2452
  roles: user.roles.map((role) => role.name),
2367
2453
  },
2368
- refreshToken: refreshTokenState.currentRefreshToken,
2454
+ // Null-guarded because the cache entry now carries a TTL: once it
2455
+ // expires - or after a logout followed by a call with a still-valid
2456
+ // access token - there is no state to read. Matches the existing
2457
+ // handling in generateSsoCode.
2458
+ refreshToken: refreshTokenState?.currentRefreshToken ?? null,
2369
2459
  // ...tokens
2370
2460
  };
2371
2461
  return response;
@@ -2378,6 +2468,7 @@ export class AuthenticationService {
2378
2468
  const refreshTokenState =
2379
2469
  await this.refreshTokenIdsStorage.getCurrentRefreshTokenState(
2380
2470
  activeUser.sub,
2471
+ activeUser.deviceKey,
2381
2472
  );
2382
2473
  if (!refreshTokenState?.currentRefreshToken) {
2383
2474
  throw new UnauthorizedException("No active session found");
@@ -8,6 +8,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
9
  import { isDangerousMediaFile } from 'src/constants/media-file-types';
10
10
  import { ERROR_MESSAGES } from 'src/constants/error-messages';
11
+ import { MediaFieldCrudManager, SolidMediaType } from 'src/helpers/field-crud-managers/MediaFieldCrudManager';
11
12
  import { PostChatterMessageDto } from 'src/dtos/post-chatter-message.dto';
12
13
  import { UpdateChatterNoteMessageDto } from 'src/dtos/update-chatter-note-message.dto';
13
14
  import { ModelMetadataHelperService } from 'src/helpers/model-metadata-helper.service';
@@ -37,6 +38,16 @@ interface ChatterMention {
37
38
  id?: string | number;
38
39
  }
39
40
 
41
+ /**
42
+ * Server-side-only overrides for `postMessage`. Not part of `PostChatterMessageDto`, so
43
+ * nothing here is settable over HTTP - only in-process callers such as migration or
44
+ * back-fill services can supply it.
45
+ */
46
+ export interface PostChatterMessageOptions {
47
+ /** Backdate the message instead of stamping it with the current time. */
48
+ createdAt?: Date;
49
+ }
50
+
40
51
  @Injectable()
41
52
  export class ChatterMessageService extends CRUDService<ChatterMessage> {
42
53
  private readonly _logger = new Logger(ChatterMessageService.name);
@@ -186,6 +197,34 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
186
197
  }
187
198
  }
188
199
 
200
+ /**
201
+ * Validates newly-uploaded attachments against each declared media field's real config
202
+ * (mediaTypes/mediaMaxSizeKb/mediaAllowedExtensions), grouped by fieldname - the same rules
203
+ * MediaFieldCrudManager already enforces everywhere else. Shared between postMessage and
204
+ * updateCustomNoteMessage so the two entry points can't drift apart; isUpdate is the only
205
+ * thing that differs between them.
206
+ */
207
+ private validateChatterAttachmentsForFields(files: Express.Multer.File[], mediaFields: any[], isUpdate: boolean): void {
208
+ for (const mediaField of mediaFields) {
209
+ const media = files.filter(multerFile => multerFile.fieldname === mediaField.name);
210
+ if (media.length === 0) continue;
211
+
212
+ const validator = new MediaFieldCrudManager({
213
+ type: mediaField.type as SolidMediaType,
214
+ required: mediaField.required,
215
+ fieldName: mediaField.name,
216
+ mediaMaxSizeKb: mediaField.mediaMaxSizeKb,
217
+ mediaTypes: mediaField.mediaTypes,
218
+ mediaAllowedExtensions: mediaField.mediaAllowedExtensions,
219
+ isUpdate,
220
+ });
221
+ const validationErrors = validator.validate({}, media);
222
+ if (validationErrors.length > 0) {
223
+ throw new BadRequestException(validationErrors.map(error => error.error).join(', '));
224
+ }
225
+ }
226
+ }
227
+
189
228
  private async publishChatterMentionNotifications(message: ChatterMessage, model: any) {
190
229
  if (message.messageType !== CHATTER_MESSAGE_TYPE.CUSTOM || message.messageSubType !== CHATTER_MESSAGE_SUBTYPE.NOTE) {
191
230
  return;
@@ -319,6 +358,24 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
319
358
 
320
359
  this.validateChatterMediaFiles(files);
321
360
 
361
+ let chatterModel: any = null;
362
+ let mediaFields: any[] = [];
363
+ if (removeAttachmentIds.length > 0 || hasNewFiles) {
364
+ chatterModel = await this.modelMetadataService.findOneBySingularName('chatterMessage', {
365
+ fields: {
366
+ model: true,
367
+ mediaStorageProvider: true,
368
+ },
369
+ module: true,
370
+ });
371
+
372
+ mediaFields = chatterModel.fields.filter(field => field.type === 'mediaSingle' || field.type === 'mediaMultiple');
373
+
374
+ if (hasNewFiles) {
375
+ this.validateChatterAttachmentsForFields(files, mediaFields, true);
376
+ }
377
+ }
378
+
322
379
  if (hasMessageBody) {
323
380
  message.messageBody = trimmedMessageBody;
324
381
  }
@@ -335,15 +392,6 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
335
392
  const savedMessage = await this.repo.save(message);
336
393
 
337
394
  if (removeAttachmentIds.length > 0 || hasNewFiles) {
338
- const model = await this.modelMetadataService.findOneBySingularName('chatterMessage', {
339
- fields: {
340
- model: true,
341
- mediaStorageProvider: true,
342
- },
343
- module: true,
344
- });
345
-
346
- const mediaFields = model.fields.filter(field => field.type === 'mediaSingle' || field.type === 'mediaMultiple');
347
395
  const attachmentFieldIds = mediaFields.map(field => field.id);
348
396
 
349
397
  if (removeAttachmentIds.length > 0 && attachmentFieldIds.length > 0) {
@@ -351,7 +399,7 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
351
399
  where: {
352
400
  id: In(removeAttachmentIds),
353
401
  entityId: savedMessage.id,
354
- modelMetadata: { id: model.id },
402
+ modelMetadata: { id: chatterModel.id },
355
403
  fieldMetadata: { id: In(attachmentFieldIds) },
356
404
  },
357
405
  relations: {
@@ -389,7 +437,14 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
389
437
  return this.trimMessageUser(savedMessage);
390
438
  }
391
439
 
392
- async postMessage(postDto: PostChatterMessageDto, files: Express.Multer.File[] = []) {
440
+ /**
441
+ * Post a chatter message.
442
+ *
443
+ * `options` is deliberately kept out of `PostChatterMessageDto` so it can only be supplied
444
+ * by server-side callers (migrations, back-fills). The controller binds the DTO alone, so
445
+ * an API client has no way to backdate a message.
446
+ */
447
+ async postMessage(postDto: PostChatterMessageDto, files: Express.Multer.File[] = [], options: PostChatterMessageOptions = {}) {
393
448
  const coModelName = lowerFirst(postDto.coModelName);
394
449
  await this.assertRecordAccess(coModelName, postDto.coModelEntityId);
395
450
 
@@ -404,6 +459,10 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
404
459
  chatterMessage.coModelEntityId = postDto.coModelEntityId;
405
460
  chatterMessage.coModelName = coModelName;
406
461
  chatterMessage.modelUserKey = postDto.modelUserKey ?? null;
462
+ // Left unset otherwise, so the @CreateDateColumn default still applies.
463
+ if (options.createdAt) {
464
+ chatterMessage.createdAt = options.createdAt;
465
+ }
407
466
 
408
467
  const model = await this.modelMetadataRepo.findOne({
409
468
  where: { singularName: coModelName },
@@ -413,10 +472,9 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
413
472
 
414
473
  this.stampMessageAuditFields(chatterMessage);
415
474
 
416
- const savedMessage = await this.repo.save(chatterMessage);
417
-
475
+ let mediaFields: any[] = [];
418
476
  if (files && files.length > 0) {
419
- const model = await this.modelMetadataService.findOneBySingularName('chatterMessage', {
477
+ const chatterModel = await this.modelMetadataService.findOneBySingularName('chatterMessage', {
420
478
  fields: {
421
479
  model: true,
422
480
  mediaStorageProvider: true,
@@ -424,8 +482,13 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
424
482
  module: true,
425
483
  });
426
484
 
427
- const mediaFields = model.fields.filter(field => field.type === 'mediaSingle' || field.type === 'mediaMultiple');
485
+ mediaFields = chatterModel.fields.filter(field => field.type === 'mediaSingle' || field.type === 'mediaMultiple');
486
+ this.validateChatterAttachmentsForFields(files, mediaFields, false);
487
+ }
488
+
489
+ const savedMessage = await this.repo.save(chatterMessage);
428
490
 
491
+ if (files && files.length > 0) {
429
492
  for (const mediaField of mediaFields) {
430
493
  const media = files.filter(multerFile => multerFile.fieldname === mediaField.name);
431
494
  if (media.length > 0) {
@@ -889,25 +889,25 @@ export class CrudHelperService {
889
889
  };
890
890
  }
891
891
 
892
- async countGroupedRecords(qb: SelectQueryBuilder<any>, basicFilterDto: BasicFilterDto, entityAlias: string) { //TODO : Check how to pass a type to SelectQueryBuilder instead of any
893
- const { limit, offset, ...rest } = basicFilterDto;
894
- const filteredDto = { ...rest, limit: undefined, offset: undefined };
892
+ // async countGroupedRecords(qb: SelectQueryBuilder<any>, basicFilterDto: BasicFilterDto, entityAlias: string, internationalisation?: boolean, draftPublishWorkflow?: boolean, moduleRef?: any) { //TODO : Check how to pass a type to SelectQueryBuilder instead of any
893
+ // const { limit, offset, ...rest } = basicFilterDto;
894
+ // const filteredDto = { ...rest, limit: undefined, offset: undefined };
895
895
 
896
- const filteredQB = this.buildFilterQuery(qb, filteredDto as BasicFilterDto, entityAlias, undefined, undefined, undefined, FilterCombinator.AND, false, false);
896
+ // const filteredQB = this.buildFilterQuery(qb, filteredDto as BasicFilterDto, entityAlias, internationalisation, draftPublishWorkflow, moduleRef, FilterCombinator.AND, false, false);
897
897
 
898
- const groupByFields = this.normalize(filteredDto.groupBy);
898
+ // const groupByFields = this.normalize(filteredDto.groupBy);
899
899
 
900
- if (!groupByFields || groupByFields.length === 0) {
901
- throw new Error(ERROR_MESSAGES.INVALID_GROUP_BY_COUNT);
902
- }
900
+ // if (!groupByFields || groupByFields.length === 0) {
901
+ // throw new Error(ERROR_MESSAGES.INVALID_GROUP_BY_COUNT);
902
+ // }
903
903
 
904
- this.applyGroupBySelections(filteredQB, groupByFields, entityAlias);
905
- this.applyAggregates(filteredQB, ['count'], entityAlias);
906
- filteredQB.limit(undefined).offset(undefined).take(undefined).skip(undefined);
904
+ // this.applyGroupBySelections(filteredQB, groupByFields, entityAlias);
905
+ // this.applyAggregates(filteredQB, ['count'], entityAlias);
906
+ // filteredQB.limit(undefined).offset(undefined).take(undefined).skip(undefined);
907
907
 
908
- const rawResults = await filteredQB.getRawMany();
909
- return rawResults.length;
910
- }
908
+ // const rawResults = await filteredQB.getRawMany();
909
+ // return rawResults.length;
910
+ // }
911
911
 
912
912
  hasReadPermissionOnModel = (activeUser: ActiveUserData, modelName: string) => {
913
913
  const permissionNames = [`${classify(modelName)}Controller.findOne`, `${classify(modelName)}Controller.findMany`];
@@ -980,7 +980,10 @@ export class CrudHelperService {
980
980
  basicFilterDto: BasicFilterDto,
981
981
  alias: string,
982
982
  createQbFn: () => Promise<SelectQueryBuilder<T>>,
983
- postProcessEntities?: (entities: T[]) => Promise<void>
983
+ postProcessEntities?: (entities: T[]) => Promise<void>,
984
+ internationalisation?: boolean,
985
+ draftPublishWorkflow?: boolean,
986
+ moduleRef?: any
984
987
  ): Promise<{ meta: { totalRecords: number }; groupMeta: any[]; groupRecords: any[] }> {
985
988
  const groupByFields = this.normalize(basicFilterDto.groupBy);
986
989
  if (!groupByFields.length) throw new BadRequestException(ERROR_MESSAGES.INVALID_GROUP_BY_COUNT);
@@ -1015,7 +1018,7 @@ export class CrudHelperService {
1015
1018
  aggregates: undefined,
1016
1019
  sort: basicFilterDto.groupFilter?.sort,
1017
1020
  };
1018
- groupQb = this.buildFilterQuery(groupQb, groupFilterDto, alias);
1021
+ groupQb = this.buildFilterQuery(groupQb, groupFilterDto, alias, internationalisation, draftPublishWorkflow, moduleRef);
1019
1022
  groupQb = this.buildGroupByRecordsQuery(groupQb, group, alias, groupAliasMap, aggregateAliasMap, groupExpressionMap);
1020
1023
  const [entities, count] = await groupQb.getManyAndCount();
1021
1024
  if (postProcessEntities) await postProcessEntities(entities);
@@ -446,6 +446,7 @@ private async prepareManyToManyAuditSnapshot(entity: T,id: number,modelSingularN
446
446
  ...commonOptions,
447
447
  mediaMaxSizeKb: fieldMetadata.mediaMaxSizeKb,
448
448
  mediaTypes: fieldMetadata.mediaTypes,
449
+ mediaAllowedExtensions: fieldMetadata.mediaAllowedExtensions,
449
450
  type: fieldMetadata.type as unknown as SolidMediaType
450
451
  };
451
452
  return new MediaFieldCrudManager(options);
@@ -583,7 +584,8 @@ private async prepareManyToManyAuditSnapshot(entity: T,id: number,modelSingularN
583
584
  async (entities) => {
584
585
  if (populateUserIdFields?.length) await this.handlePopulateUserIdFields(populateUserIdFields, entities);
585
586
  if (populateMedia?.length) await this.handlePopulateMedia(populateMedia, entities);
586
- }
587
+ },
588
+ internationalisation, draftPublishWorkflow, this.moduleRef
587
589
  );
588
590
  }
589
591
  else {
@@ -333,13 +333,65 @@ export class DraftPublishHelperService {
333
333
  throw new BadRequestException(`${modelName} with id ${id} is already unpublished`);
334
334
  }
335
335
 
336
- return repo.save({
337
- ...entity,
338
- isPublished: false,
339
- updatedBy: activeUser?.sub ?? entity.updatedBy,
336
+ const chainId = entity.initialEntityVersionId || entity.id;
337
+
338
+ return repo.manager.transaction(async (manager) => {
339
+ const transactionalRepo = manager.getRepository(repo.metadata.target);
340
+
341
+ // Release the "na" published tracker before anything else can claim it, mirroring
342
+ // publishRecord's archive-others-then-publish ordering in reverse. unpublishRecord
343
+ // only ever acts on the latest version (see loadLatestEntityForPublishAction), so
344
+ // this always becomes a Draft rather than an Archived version — clear publishedAt
345
+ // to match applyCreateDefaults' Draft convention instead of leaving a stale date.
346
+ const unpublishedEntity = await transactionalRepo.save({
347
+ ...entity,
348
+ isPublished: false,
349
+ publishedAt: null,
350
+ publishedTracker: this.createPublishedVersionTracker(entity.id),
351
+ updatedBy: activeUser?.sub ?? entity.updatedBy,
352
+ } as any) as unknown as T;
353
+
354
+ const chainVersions = await transactionalRepo.find({
355
+ where: [
356
+ { initialEntityVersionId: chainId },
357
+ { id: chainId },
358
+ ] as any,
359
+ }) as unknown as T[];
360
+
361
+ const versionToRestore = this.pickMostRecentlyPublishedVersion(
362
+ chainVersions.filter(version => version.id !== entity.id),
363
+ );
364
+
365
+ if (versionToRestore) {
366
+ await transactionalRepo.save({
367
+ ...versionToRestore,
368
+ isPublished: true,
369
+ publishedAt: new Date(),
370
+ publishedTracker: "na",
371
+ } as any);
372
+ }
373
+
374
+ return unpublishedEntity;
340
375
  });
341
376
  }
342
377
 
378
+ /**
379
+ * Among the other versions in a chain, find the one that was live most recently before
380
+ * being superseded (i.e. the "Archived" version unpublishRecord should restore). Ties on
381
+ * publishedAt fall back to the highest id, matching pickCurrentLocaleRecord's ordering.
382
+ */
383
+ private pickMostRecentlyPublishedVersion<T extends CommonEntity>(versions: T[]): T | null {
384
+ const previouslyPublished = versions.filter(version => Boolean(version.publishedAt));
385
+ if (previouslyPublished.length === 0) return null;
386
+
387
+ return previouslyPublished.sort((a, b) => {
388
+ const aPublishedAt = new Date(a.publishedAt as any).getTime();
389
+ const bPublishedAt = new Date(b.publishedAt as any).getTime();
390
+ if (aPublishedAt !== bPublishedAt) return bPublishedAt - aPublishedAt;
391
+ return Number(b.id) - Number(a.id);
392
+ })[0];
393
+ }
394
+
343
395
  /**
344
396
  * Load the latest version of a draft/publish-enabled record for a publish/unpublish
345
397
  * action, applying the two checks both actions share (workflow enabled, version is latest).
@@ -7,12 +7,13 @@ import { ComputedFieldMetadata, SolidRegistry } from 'src/helpers/solid-registry
7
7
  import { FieldMetadataRepository } from 'src/repository/field-metadata.repository';
8
8
  import { DataSource, Repository, SelectQueryBuilder } from 'typeorm';
9
9
  import { BasicFilterDto } from '../dtos/basic-filters.dto';
10
- import { CascadeType, ComputedFieldValueType, CreateFieldMetadataDto, DecryptWhenType, EncryptionType, MediaType, MSSQLType, PSQLType, RelationType, SelectionValueType, SolidFieldType } from '../dtos/create-field-metadata.dto';
10
+ import { CascadeType, ComputedFieldValueType, CreateFieldMetadataDto, DecryptWhenType, EncryptionType, MSSQLType, PSQLType, RelationType, SelectionValueType, SolidFieldType } from '../dtos/create-field-metadata.dto';
11
11
  import { SelectionDynamicQueryDto } from '../dtos/selection-dynamic-query.dto';
12
12
  import { UpdateFieldMetaDataDto } from '../dtos/update-field-metadata.dto';
13
13
  import { FieldMetadata } from '../entities/field-metadata.entity';
14
14
  import { ModelMetadata } from '../entities/model-metadata.entity';
15
15
  import { ISelectionProviderValues, MediaStorageProvider } from '../interfaces';
16
+ import { MEDIA_CATEGORIES } from '../constants/media-file-types';
16
17
  import { CrudHelperService } from './crud-helper.service';
17
18
  import { ERROR_MESSAGES } from 'src/constants/error-messages';
18
19
  import qs from 'qs';
@@ -414,7 +415,7 @@ export class FieldMetadataService implements OnApplicationBootstrap {
414
415
  encryptionTypes: enumToResponseArray(EncryptionType),
415
416
  ormType: ormFieldTypeForSolid,
416
417
  decryptWhenTypes: enumToResponseArray(DecryptWhenType),
417
- mediaTypes: enumToResponseArray(MediaType),
418
+ mediaTypes: MEDIA_CATEGORIES.map((value) => ({ label: value, value })),
418
419
  relationTypes: enumToResponseArray(RelationType),
419
420
  selectionDynamicProviders: sps,
420
421
  computedProviders: cps,
@@ -944,6 +945,7 @@ export class FieldMetadataService implements OnApplicationBootstrap {
944
945
  "isSystem",
945
946
  "mediaTypes",
946
947
  "mediaMaxSizeKb",
948
+ "mediaAllowedExtensions",
947
949
  "mediaStorageProviderId",
948
950
  "required",
949
951
  "unique",
@@ -965,6 +967,7 @@ export class FieldMetadataService implements OnApplicationBootstrap {
965
967
  "isSystem",
966
968
  "mediaTypes",
967
969
  "mediaMaxSizeKb",
970
+ "mediaAllowedExtensions",
968
971
  "mediaStorageProviderId",
969
972
  "required",
970
973
  "unique",