@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.
- package/CHANGELOG.md +7465 -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 +3 -0
- package/dist/constants/media-file-types.d.ts.map +1 -1
- package/dist/constants/media-file-types.js +47 -8
- 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 +2 -2
- 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/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/MediaFieldCrudManager.d.ts +6 -0
- package/dist/helpers/field-crud-managers/MediaFieldCrudManager.d.ts.map +1 -1
- package/dist/helpers/field-crud-managers/MediaFieldCrudManager.js +54 -27
- package/dist/helpers/field-crud-managers/MediaFieldCrudManager.js.map +1 -1
- 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 +2 -1
- 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 +2 -2
- package/dist/services/field-metadata.service.d.ts.map +1 -1
- package/dist/services/field-metadata.service.js +4 -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/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 +87 -9
- 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/MediaFieldCrudManager.ts +73 -34
- 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 +3 -1
- package/src/services/draft-publish-helper.service.ts +56 -4
- package/src/services/field-metadata.service.ts +5 -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/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
|
@@ -131,7 +131,19 @@ export class ApiKeyService {
|
|
|
131
131
|
|
|
132
132
|
const user = apiKeyRecord.user;
|
|
133
133
|
|
|
134
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
1867
|
-
|
|
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
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
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
|
-
|
|
1905
|
-
|
|
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
|
-
|
|
2297
|
-
|
|
2298
|
-
//
|
|
2299
|
-
//
|
|
2300
|
-
//
|
|
2301
|
-
//
|
|
2302
|
-
//
|
|
2303
|
-
//
|
|
2304
|
-
//
|
|
2305
|
-
|
|
2306
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
417
|
-
|
|
475
|
+
let mediaFields: any[] = [];
|
|
418
476
|
if (files && files.length > 0) {
|
|
419
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
894
|
-
|
|
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
|
-
|
|
896
|
+
// const filteredQB = this.buildFilterQuery(qb, filteredDto as BasicFilterDto, entityAlias, internationalisation, draftPublishWorkflow, moduleRef, FilterCombinator.AND, false, false);
|
|
897
897
|
|
|
898
|
-
|
|
898
|
+
// const groupByFields = this.normalize(filteredDto.groupBy);
|
|
899
899
|
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
900
|
+
// if (!groupByFields || groupByFields.length === 0) {
|
|
901
|
+
// throw new Error(ERROR_MESSAGES.INVALID_GROUP_BY_COUNT);
|
|
902
|
+
// }
|
|
903
903
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
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
|
-
|
|
909
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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,
|
|
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:
|
|
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",
|