@solidxai/core 0.1.8-beta.1 → 0.1.8-beta.11

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 (202) hide show
  1. package/README.md +197 -0
  2. package/dist/controllers/authentication.controller.d.ts +32 -2
  3. package/dist/controllers/authentication.controller.d.ts.map +1 -1
  4. package/dist/controllers/authentication.controller.js +80 -3
  5. package/dist/controllers/authentication.controller.js.map +1 -1
  6. package/dist/dtos/create-api-key.dto.d.ts +5 -0
  7. package/dist/dtos/create-api-key.dto.d.ts.map +1 -0
  8. package/dist/dtos/create-api-key.dto.js +34 -0
  9. package/dist/dtos/create-api-key.dto.js.map +1 -0
  10. package/dist/dtos/post-chatter-message.dto.d.ts +1 -0
  11. package/dist/dtos/post-chatter-message.dto.d.ts.map +1 -1
  12. package/dist/dtos/post-chatter-message.dto.js +6 -1
  13. package/dist/dtos/post-chatter-message.dto.js.map +1 -1
  14. package/dist/dtos/register-private.dto.d.ts +3 -5
  15. package/dist/dtos/register-private.dto.d.ts.map +1 -1
  16. package/dist/dtos/register-private.dto.js +6 -18
  17. package/dist/dtos/register-private.dto.js.map +1 -1
  18. package/dist/dtos/sso-exchange.dto.d.ts +4 -0
  19. package/dist/dtos/sso-exchange.dto.d.ts.map +1 -0
  20. package/dist/dtos/sso-exchange.dto.js +26 -0
  21. package/dist/dtos/sso-exchange.dto.js.map +1 -0
  22. package/dist/dtos/update-api-key.dto.d.ts +4 -0
  23. package/dist/dtos/update-api-key.dto.d.ts.map +1 -0
  24. package/dist/dtos/update-api-key.dto.js +28 -0
  25. package/dist/dtos/update-api-key.dto.js.map +1 -0
  26. package/dist/entities/agent-event.entity.d.ts +3 -12
  27. package/dist/entities/agent-event.entity.d.ts.map +1 -1
  28. package/dist/entities/agent-event.entity.js +21 -46
  29. package/dist/entities/agent-event.entity.js.map +1 -1
  30. package/dist/entities/agent-session.entity.d.ts +2 -11
  31. package/dist/entities/agent-session.entity.d.ts.map +1 -1
  32. package/dist/entities/agent-session.entity.js +15 -40
  33. package/dist/entities/agent-session.entity.js.map +1 -1
  34. package/dist/entities/field-metadata.entity.js +1 -1
  35. package/dist/entities/field-metadata.entity.js.map +1 -1
  36. package/dist/entities/legacy-common.entity.d.ts +9 -9
  37. package/dist/entities/legacy-common.entity.d.ts.map +1 -1
  38. package/dist/entities/legacy-common.entity.js +7 -7
  39. package/dist/entities/legacy-common.entity.js.map +1 -1
  40. package/dist/entities/setting.entity.d.ts +1 -0
  41. package/dist/entities/setting.entity.d.ts.map +1 -1
  42. package/dist/entities/setting.entity.js +5 -1
  43. package/dist/entities/setting.entity.js.map +1 -1
  44. package/dist/entities/sms-template.entity.d.ts.map +1 -1
  45. package/dist/entities/sms-template.entity.js +2 -1
  46. package/dist/entities/sms-template.entity.js.map +1 -1
  47. package/dist/entities/user-api-key.entity.d.ts +12 -0
  48. package/dist/entities/user-api-key.entity.d.ts.map +1 -0
  49. package/dist/entities/user-api-key.entity.js +62 -0
  50. package/dist/entities/user-api-key.entity.js.map +1 -0
  51. package/dist/entities/user.entity.d.ts +3 -0
  52. package/dist/entities/user.entity.d.ts.map +1 -1
  53. package/dist/entities/user.entity.js +12 -1
  54. package/dist/entities/user.entity.js.map +1 -1
  55. package/dist/enums/auth-type.enum.d.ts +2 -1
  56. package/dist/enums/auth-type.enum.d.ts.map +1 -1
  57. package/dist/enums/auth-type.enum.js +2 -1
  58. package/dist/enums/auth-type.enum.js.map +1 -1
  59. package/dist/guards/api-key.guard.d.ts +11 -0
  60. package/dist/guards/api-key.guard.d.ts.map +1 -0
  61. package/dist/guards/api-key.guard.js +43 -0
  62. package/dist/guards/api-key.guard.js.map +1 -0
  63. package/dist/guards/authentication.guard.d.ts +4 -2
  64. package/dist/guards/authentication.guard.d.ts.map +1 -1
  65. package/dist/guards/authentication.guard.js +7 -3
  66. package/dist/guards/authentication.guard.js.map +1 -1
  67. package/dist/helpers/bootstrap.helper.d.ts.map +1 -1
  68. package/dist/helpers/bootstrap.helper.js +12 -1
  69. package/dist/helpers/bootstrap.helper.js.map +1 -1
  70. package/dist/helpers/field-crud-managers/SelectionDynamicFieldCrudManager.d.ts.map +1 -1
  71. package/dist/helpers/field-crud-managers/SelectionDynamicFieldCrudManager.js +15 -6
  72. package/dist/helpers/field-crud-managers/SelectionDynamicFieldCrudManager.js.map +1 -1
  73. package/dist/helpers/typeorm-db-helper.d.ts.map +1 -1
  74. package/dist/helpers/typeorm-db-helper.js +9 -0
  75. package/dist/helpers/typeorm-db-helper.js.map +1 -1
  76. package/dist/index.d.ts +2 -0
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +2 -0
  79. package/dist/index.js.map +1 -1
  80. package/dist/interfaces.d.ts +12 -0
  81. package/dist/interfaces.d.ts.map +1 -1
  82. package/dist/interfaces.js.map +1 -1
  83. package/dist/jobs/database/chatter-queue-publisher-database.service.d.ts +1 -1
  84. package/dist/jobs/database/chatter-queue-publisher-database.service.d.ts.map +1 -1
  85. package/dist/jobs/database/chatter-queue-publisher-database.service.js.map +1 -1
  86. package/dist/jobs/database/chatter-queue-subscriber-database.service.d.ts +1 -1
  87. package/dist/jobs/database/chatter-queue-subscriber-database.service.d.ts.map +1 -1
  88. package/dist/jobs/database/chatter-queue-subscriber-database.service.js.map +1 -1
  89. package/dist/jobs/rabbitmq/chatter-queue-publisher.service.d.ts +1 -12
  90. package/dist/jobs/rabbitmq/chatter-queue-publisher.service.d.ts.map +1 -1
  91. package/dist/jobs/rabbitmq/chatter-queue-publisher.service.js.map +1 -1
  92. package/dist/jobs/rabbitmq/chatter-queue-subscriber.service.d.ts +1 -1
  93. package/dist/jobs/rabbitmq/chatter-queue-subscriber.service.d.ts.map +1 -1
  94. package/dist/jobs/rabbitmq/chatter-queue-subscriber.service.js.map +1 -1
  95. package/dist/jobs/redis/chatter-queue-subscriber-redis.service.d.ts +1 -1
  96. package/dist/jobs/redis/chatter-queue-subscriber-redis.service.d.ts.map +1 -1
  97. package/dist/jobs/redis/chatter-queue-subscriber-redis.service.js.map +1 -1
  98. package/dist/repository/user-api-key.repository.d.ts +12 -0
  99. package/dist/repository/user-api-key.repository.d.ts.map +1 -0
  100. package/dist/repository/user-api-key.repository.js +34 -0
  101. package/dist/repository/user-api-key.repository.js.map +1 -0
  102. package/dist/seeders/module-test-data.service.d.ts +5 -0
  103. package/dist/seeders/module-test-data.service.d.ts.map +1 -1
  104. package/dist/seeders/module-test-data.service.js +131 -4
  105. package/dist/seeders/module-test-data.service.js.map +1 -1
  106. package/dist/seeders/seed-data/solid-core-metadata.json +287 -197
  107. package/dist/services/api-key.service.d.ts +20 -0
  108. package/dist/services/api-key.service.d.ts.map +1 -0
  109. package/dist/services/api-key.service.js +98 -0
  110. package/dist/services/api-key.service.js.map +1 -0
  111. package/dist/services/authentication.service.d.ts +19 -1
  112. package/dist/services/authentication.service.d.ts.map +1 -1
  113. package/dist/services/authentication.service.js +31 -5
  114. package/dist/services/authentication.service.js.map +1 -1
  115. package/dist/services/chatter-message.service.d.ts.map +1 -1
  116. package/dist/services/chatter-message.service.js +6 -0
  117. package/dist/services/chatter-message.service.js.map +1 -1
  118. package/dist/services/encryption.service.d.ts +8 -0
  119. package/dist/services/encryption.service.d.ts.map +1 -0
  120. package/dist/services/encryption.service.js +75 -0
  121. package/dist/services/encryption.service.js.map +1 -0
  122. package/dist/services/export-transaction.service.d.ts.map +1 -1
  123. package/dist/services/export-transaction.service.js +0 -23
  124. package/dist/services/export-transaction.service.js.map +1 -1
  125. package/dist/services/field-metadata.service.d.ts +1 -3
  126. package/dist/services/field-metadata.service.d.ts.map +1 -1
  127. package/dist/services/field-metadata.service.js +6 -13
  128. package/dist/services/field-metadata.service.js.map +1 -1
  129. package/dist/services/file/disk-file.service.d.ts +1 -0
  130. package/dist/services/file/disk-file.service.d.ts.map +1 -1
  131. package/dist/services/file/disk-file.service.js +11 -3
  132. package/dist/services/file/disk-file.service.js.map +1 -1
  133. package/dist/services/media.service.d.ts +0 -1
  134. package/dist/services/media.service.d.ts.map +1 -1
  135. package/dist/services/media.service.js +10 -11
  136. package/dist/services/media.service.js.map +1 -1
  137. package/dist/services/setting.service.d.ts +1 -0
  138. package/dist/services/setting.service.d.ts.map +1 -1
  139. package/dist/services/setting.service.js +35 -7
  140. package/dist/services/setting.service.js.map +1 -1
  141. package/dist/services/settings/default-settings-provider.service.d.ts +12 -0
  142. package/dist/services/settings/default-settings-provider.service.d.ts.map +1 -1
  143. package/dist/services/settings/default-settings-provider.service.js +7 -3
  144. package/dist/services/settings/default-settings-provider.service.js.map +1 -1
  145. package/dist/services/sso-code-storage.service.d.ts +15 -0
  146. package/dist/services/sso-code-storage.service.d.ts.map +1 -0
  147. package/dist/services/sso-code-storage.service.js +47 -0
  148. package/dist/services/sso-code-storage.service.js.map +1 -0
  149. package/dist/services/user.service.d.ts.map +1 -1
  150. package/dist/services/user.service.js +3 -2
  151. package/dist/services/user.service.js.map +1 -1
  152. package/dist/solid-core.module.d.ts.map +1 -1
  153. package/dist/solid-core.module.js +10 -0
  154. package/dist/solid-core.module.js.map +1 -1
  155. package/dist/subscribers/audit.subscriber.d.ts +1 -1
  156. package/dist/subscribers/audit.subscriber.d.ts.map +1 -1
  157. package/dist/subscribers/audit.subscriber.js.map +1 -1
  158. package/package.json +1 -1
  159. package/src/controllers/authentication.controller.ts +59 -3
  160. package/src/dtos/create-api-key.dto.ts +14 -0
  161. package/src/dtos/post-chatter-message.dto.ts +4 -0
  162. package/src/dtos/register-private.dto.ts +5 -14
  163. package/src/dtos/sso-exchange.dto.ts +7 -0
  164. package/src/dtos/update-api-key.dto.ts +9 -0
  165. package/src/entities/agent-event.entity.ts +21 -55
  166. package/src/entities/agent-session.entity.ts +15 -47
  167. package/src/entities/field-metadata.entity.ts +1 -1
  168. package/src/entities/legacy-common.entity.ts +15 -15
  169. package/src/entities/setting.entity.ts +3 -0
  170. package/src/entities/sms-template.entity.ts +3 -2
  171. package/src/entities/user-api-key.entity.ts +37 -0
  172. package/src/entities/user.entity.ts +8 -0
  173. package/src/enums/auth-type.enum.ts +1 -0
  174. package/src/guards/api-key.guard.ts +32 -0
  175. package/src/guards/authentication.guard.ts +6 -3
  176. package/src/helpers/bootstrap.helper.ts +16 -1
  177. package/src/helpers/field-crud-managers/SelectionDynamicFieldCrudManager.ts +17 -6
  178. package/src/helpers/typeorm-db-helper.ts +11 -0
  179. package/src/index.ts +2 -0
  180. package/src/interfaces.ts +16 -0
  181. package/src/jobs/database/chatter-queue-publisher-database.service.ts +1 -1
  182. package/src/jobs/database/chatter-queue-subscriber-database.service.ts +1 -1
  183. package/src/jobs/rabbitmq/chatter-queue-publisher.service.ts +1 -15
  184. package/src/jobs/rabbitmq/chatter-queue-subscriber.service.ts +1 -1
  185. package/src/jobs/redis/chatter-queue-subscriber-redis.service.ts +1 -1
  186. package/src/repository/user-api-key.repository.ts +17 -0
  187. package/src/seeders/module-test-data.service.ts +165 -6
  188. package/src/seeders/seed-data/solid-core-metadata.json +287 -197
  189. package/src/services/api-key.service.ts +111 -0
  190. package/src/services/authentication.service.ts +35 -3
  191. package/src/services/chatter-message.service.ts +7 -0
  192. package/src/services/encryption.service.ts +43 -0
  193. package/src/services/export-transaction.service.ts +0 -26
  194. package/src/services/field-metadata.service.ts +5 -12
  195. package/src/services/file/disk-file.service.ts +15 -7
  196. package/src/services/media.service.ts +12 -51
  197. package/src/services/setting.service.ts +38 -9
  198. package/src/services/settings/default-settings-provider.service.ts +7 -3
  199. package/src/services/sso-code-storage.service.ts +36 -0
  200. package/src/services/user.service.ts +3 -2
  201. package/src/solid-core.module.ts +10 -0
  202. package/src/subscribers/audit.subscriber.ts +1 -1
@@ -0,0 +1,111 @@
1
+ import {
2
+ ForbiddenException,
3
+ Injectable,
4
+ Logger,
5
+ NotFoundException,
6
+ UnauthorizedException,
7
+ } from '@nestjs/common';
8
+ import { createHash, randomBytes } from 'crypto';
9
+ import { CreateApiKeyDto } from 'src/dtos/create-api-key.dto';
10
+ import { UpdateApiKeyDto } from 'src/dtos/update-api-key.dto';
11
+ import { UserApiKey } from 'src/entities/user-api-key.entity';
12
+ import { User } from 'src/entities/user.entity';
13
+ import { ActiveUserData } from 'src/interfaces/active-user-data.interface';
14
+ import { UserApiKeyRepository } from 'src/repository/user-api-key.repository';
15
+ import { PermissionMetadataService } from 'src/services/permission-metadata.service';
16
+
17
+ @Injectable()
18
+ export class ApiKeyService {
19
+ private readonly logger = new Logger(ApiKeyService.name);
20
+
21
+ constructor(
22
+ private readonly apiKeyRepository: UserApiKeyRepository,
23
+ private readonly permissionMetadataService: PermissionMetadataService,
24
+ ) {}
25
+
26
+ async generate(userId: number, dto: CreateApiKeyDto): Promise<{ apiKey: string; record: UserApiKey }> {
27
+ const user = await this.apiKeyRepository.manager.findOne(User, {
28
+ where: { id: userId },
29
+ select: ['id', 'isAllowedToGenerateApiKeys'],
30
+ });
31
+
32
+ if (!user?.isAllowedToGenerateApiKeys) {
33
+ throw new ForbiddenException('You are not allowed to generate API keys');
34
+ }
35
+
36
+ const rawKey = 'sldx_' + randomBytes(32).toString('hex');
37
+ const hashedKey = this.hash(rawKey);
38
+ const maskedKey = 'sldx_****' + rawKey.slice(-4);
39
+
40
+ const record = this.apiKeyRepository.create({
41
+ name: dto.name,
42
+ hashedKey,
43
+ maskedKey,
44
+ isActive: true,
45
+ expiresAt: dto.expiresAt ? new Date(dto.expiresAt) : null,
46
+ user,
47
+ });
48
+
49
+ await this.apiKeyRepository.save(record);
50
+
51
+ // Strip hashedKey from the returned record — maskedKey is all the UI needs
52
+ delete (record as any).hashedKey;
53
+
54
+ return { apiKey: rawKey, record };
55
+ }
56
+
57
+ async validate(rawKey: string): Promise<ActiveUserData> {
58
+ const hashedKey = this.hash(rawKey);
59
+
60
+ // Bypass security rules for auth validation — must find the key regardless of caller context
61
+ const keyRecord = await this.apiKeyRepository.findOne({
62
+ where: { hashedKey, isActive: true },
63
+ relations: ['user', 'user.roles'],
64
+ });
65
+
66
+ if (!keyRecord) {
67
+ throw new UnauthorizedException();
68
+ }
69
+
70
+ if (keyRecord.expiresAt && keyRecord.expiresAt < new Date()) {
71
+ throw new UnauthorizedException('API key expired');
72
+ }
73
+
74
+ // Fire-and-forget — does not need security rule context
75
+ this.apiKeyRepository.update(keyRecord.id, { lastUsedAt: new Date() }).catch((err) => {
76
+ this.logger.warn(`Failed to update lastUsedAt for key ${keyRecord.id}: ${err.message}`);
77
+ });
78
+
79
+ const roles = (keyRecord.user.roles ?? []).map((r) => r.name);
80
+ const permissions = await this.permissionMetadataService.findAllUsingRoles(roles);
81
+
82
+ return {
83
+ sub: keyRecord.user.id,
84
+ username: keyRecord.user.username,
85
+ email: keyRecord.user.email,
86
+ roles,
87
+ permissions: permissions.map((p) => p.name),
88
+ };
89
+ }
90
+
91
+ async updateKey(id: number, userId: number, dto: UpdateApiKeyDto): Promise<void> {
92
+ const keyRecord = await this.apiKeyRepository.findOne({
93
+ where: { id, user: { id: userId } },
94
+ });
95
+
96
+ if (!keyRecord) {
97
+ throw new NotFoundException('API key not found');
98
+ }
99
+
100
+ await this.apiKeyRepository.manager
101
+ .createQueryBuilder()
102
+ .update(UserApiKey)
103
+ .set({ isActive: dto.isActive })
104
+ .where('id = :id', { id })
105
+ .execute();
106
+ }
107
+
108
+ private hash(rawKey: string): string {
109
+ return createHash('sha256').update(rawKey).digest('hex');
110
+ }
111
+ }
@@ -41,6 +41,7 @@ import { EventDetails, EventType } from "../interfaces";
41
41
  import { ActiveUserData } from '../interfaces/active-user-data.interface';
42
42
  import { HashingService } from './hashing.service';
43
43
  import { InvalidatedRefreshTokenError, RefreshTokenIdsStorageService } from './refresh-token-ids-storage.service';
44
+ import { SsoCodeStorageService } from './sso-code-storage.service';
44
45
  import { RoleMetadataService } from './role-metadata.service';
45
46
  import { SettingService } from './setting.service';
46
47
  import { UserActivityHistoryService } from './user-activity-history.service';
@@ -78,6 +79,7 @@ export class AuthenticationService {
78
79
  private readonly settingService: SettingService,
79
80
  private readonly roleMetadataService: RoleMetadataService,
80
81
  private readonly userActivityHistoryService: UserActivityHistoryService,
82
+ private readonly ssoCodeStorage: SsoCodeStorageService,
81
83
 
82
84
  @InjectDataSource()
83
85
  private readonly dataSource: DataSource,
@@ -152,6 +154,10 @@ export class AuthenticationService {
152
154
  const defaultRole = this.settingService.getConfigValue<SolidCoreSetting>('defaultRole');
153
155
 
154
156
  var { user, pwd, autoGeneratedPwd } = await this.populateForSignup(new User(), signUpDto, activateUserOnRegistration, onForcePasswordChange);
157
+ const privateDto = signUpDto as { isAllowedToGenerateApiKeys?: boolean };
158
+ if (privateDto.isAllowedToGenerateApiKeys !== undefined) {
159
+ user.isAllowedToGenerateApiKeys = privateDto.isAllowedToGenerateApiKeys;
160
+ }
155
161
  const savedUser = await this.userRepository.save(user);
156
162
  // Also assign a default role to the newly created user.
157
163
  const userRoles = signUpDto.roles ?? [];
@@ -879,11 +885,15 @@ export class AuthenticationService {
879
885
  }
880
886
  }
881
887
 
882
- private async buildLoginTokenResponse(user: User) {
883
- const { accessToken, refreshToken } = await this.generateTokens(user);
888
+ private buildUserPayload(user: User) {
884
889
  const { id, username, email, mobile, lastLoginProvider } = user;
885
890
  const roles = user.roles.map((role) => role.name);
886
- return { accessToken, refreshToken, user: { id, username, email, mobile, lastLoginProvider, roles } };
891
+ return { id, username, email, mobile, lastLoginProvider, roles };
892
+ }
893
+
894
+ private async buildLoginTokenResponse(user: User) {
895
+ const { accessToken, refreshToken } = await this.generateTokens(user);
896
+ return { accessToken, refreshToken, user: this.buildUserPayload(user) };
887
897
  }
888
898
 
889
899
  async changePassword(changePasswordDto: ChangePasswordDto, activeUser: ActiveUserData) {
@@ -1414,6 +1424,28 @@ export class AuthenticationService {
1414
1424
  return response;
1415
1425
  }
1416
1426
 
1427
+ async generateSsoCode(activeUser: ActiveUserData, rawAccessToken: string): Promise<{ ssoCode: string }> {
1428
+ const refreshTokenState = await this.refreshTokenIdsStorage.getCurrentRefreshTokenState(activeUser.sub);
1429
+ if (!refreshTokenState?.currentRefreshToken) {
1430
+ throw new UnauthorizedException('No active session found');
1431
+ }
1432
+ const ssoCode = await this.ssoCodeStorage.generateCode(
1433
+ activeUser.sub,
1434
+ rawAccessToken,
1435
+ refreshTokenState.currentRefreshToken,
1436
+ );
1437
+ return { ssoCode };
1438
+ }
1439
+
1440
+ async exchangeSsoCode(code: string) {
1441
+ const { userId, accessToken, refreshToken } = await this.ssoCodeStorage.consumeCode(code);
1442
+ const user = await this.userRepository.findOne({ where: { id: userId }, relations: { roles: true } });
1443
+ if (!user) {
1444
+ throw new UnauthorizedException('User not found');
1445
+ }
1446
+ return { accessToken, refreshToken, user: this.buildUserPayload(user) };
1447
+ }
1448
+
1417
1449
  }
1418
1450
 
1419
1451
  function parseUniqueConstraintError(detail: string): string {
@@ -53,6 +53,13 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
53
53
  chatterMessage.messageBody = postDto.messageBody;
54
54
  chatterMessage.coModelEntityId = postDto.coModelEntityId;
55
55
  chatterMessage.coModelName = postDto.coModelName;
56
+ chatterMessage.modelUserKey = postDto.modelUserKey ?? null;
57
+
58
+ const model = await this.modelMetadataRepo.findOne({
59
+ where: { singularName: lowerFirst(postDto.coModelName) },
60
+ relations: { userKeyField: true }
61
+ });
62
+ chatterMessage.modelDisplayName = model?.displayName ?? null;
56
63
 
57
64
  const activeUser = this.requestContextService.getActiveUser();
58
65
 
@@ -0,0 +1,43 @@
1
+ import * as crypto from 'crypto';
2
+
3
+ const ALGORITHM = 'aes-256-gcm';
4
+ const IV_LENGTH = 12;
5
+ const AUTH_TAG_LENGTH = 16;
6
+ const KEY_LENGTH = 32;
7
+ const SCRYPT_SALT = 'solid-encryption-salt';
8
+ const ENC_PREFIX = 'enc:';
9
+
10
+ export class EncryptionService {
11
+ private readonly key: Buffer;
12
+
13
+ constructor(secret: string) {
14
+ if (!secret) throw new Error('EncryptionService: secret must not be empty');
15
+ this.key = crypto.scryptSync(secret, SCRYPT_SALT, KEY_LENGTH) as Buffer;
16
+ }
17
+
18
+ encrypt(plaintext: string): string {
19
+ const iv = crypto.randomBytes(IV_LENGTH);
20
+ const cipher = crypto.createCipheriv(ALGORITHM, this.key, iv, { authTagLength: AUTH_TAG_LENGTH });
21
+ const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
22
+ const authTag = cipher.getAuthTag();
23
+ return `${ENC_PREFIX}${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted.toString('hex')}`;
24
+ }
25
+
26
+ decrypt(ciphertext: string): string {
27
+ if (!ciphertext.startsWith(ENC_PREFIX)) {
28
+ throw new Error('EncryptionService: value does not appear to be encrypted');
29
+ }
30
+ const payload = ciphertext.slice(ENC_PREFIX.length);
31
+ const [ivHex, authTagHex, encryptedHex] = payload.split(':');
32
+ const iv = Buffer.from(ivHex, 'hex');
33
+ const authTag = Buffer.from(authTagHex, 'hex');
34
+ const encryptedBuf = Buffer.from(encryptedHex, 'hex');
35
+ const decipher = crypto.createDecipheriv(ALGORITHM, this.key, iv, { authTagLength: AUTH_TAG_LENGTH });
36
+ decipher.setAuthTag(authTag);
37
+ return decipher.update(encryptedBuf).toString('utf8') + decipher.final('utf8');
38
+ }
39
+
40
+ isEncrypted(value: string): boolean {
41
+ return typeof value === 'string' && value.startsWith(ENC_PREFIX);
42
+ }
43
+ }
@@ -274,32 +274,6 @@ export class ExportTransactionService extends CRUDService<ExportTransaction> {
274
274
  }
275
275
  }
276
276
 
277
- // Include userKey from each related field
278
- for (const [relatedFieldName, userKeyFieldName] of relatedModelsUserKeyMap.entries()) {
279
- const relatedData = record[relatedFieldName];
280
- const displayKey = fieldNameToDisplayName.get(relatedFieldName) ?? relatedFieldName;
281
-
282
- if (Array.isArray(relatedData)) {
283
- // For many-to-many or one-to-many
284
- const values = relatedData
285
- .map(item => {
286
- let val = item?.[userKeyFieldName];
287
- const relatedFieldMeta = modelFields.find(f => f.name === relatedFieldName);
288
- if ((relatedFieldMeta?.type === 'datetime' || relatedFieldMeta?.type === 'date') && val) {
289
- val = new Date(val).toISOString();
290
- }
291
- return val;
292
- })
293
- .filter(Boolean);
294
- newRecord[displayKey] = values.join(', ');
295
- } else if (relatedData && typeof relatedData === 'object') {
296
- // For many-to-one or one-to-one
297
- newRecord[relatedFieldName] = relatedData?.[userKeyFieldName] ?? null;
298
- } else {
299
- newRecord[displayKey] = null;
300
- }
301
- }
302
-
303
277
  // Include userKey from each related field (with displayName)
304
278
  for (const [relatedFieldName, userKeyFieldName] of relatedModelsUserKeyMap.entries()) {
305
279
  const relatedData = record[relatedFieldName];
@@ -18,7 +18,6 @@ import { ERROR_MESSAGES } from 'src/constants/error-messages';
18
18
  import qs from 'qs';
19
19
  import { ResolveS3UrlDto } from 'src/dtos/resolve-s3-url.dto';
20
20
  import { MediaStorageProviderMetadataRepository } from 'src/repository/media-storage-provider-metadata.repository';
21
- import { ConfigService } from '@nestjs/config';
22
21
  import { S3FileService } from './file';
23
22
  import { MediaStorageProviderMetadata } from 'src/entities/media-storage-provider-metadata.entity';
24
23
 
@@ -27,7 +26,6 @@ import { MediaStorageProviderMetadata } from 'src/entities/media-storage-provide
27
26
  export class FieldMetadataService implements OnApplicationBootstrap {
28
27
  constructor(
29
28
  private readonly fieldMetadataRepo: FieldMetadataRepository,
30
- private readonly configService: ConfigService,
31
29
  private readonly fileService: S3FileService,
32
30
  private readonly mediaStorageProviderMetadataRepository: MediaStorageProviderMetadataRepository,
33
31
 
@@ -1291,7 +1289,6 @@ export class FieldMetadataService implements OnApplicationBootstrap {
1291
1289
  }
1292
1290
 
1293
1291
  async resolveS3Url(resolveS3UrlDto: ResolveS3UrlDto) {
1294
- let url = "";
1295
1292
  const normalizedKey = this.normalizeS3Key(resolveS3UrlDto.s3Key);
1296
1293
 
1297
1294
  let resolvedBucketName = resolveS3UrlDto.bucketName;
@@ -1308,14 +1305,11 @@ export class FieldMetadataService implements OnApplicationBootstrap {
1308
1305
  }
1309
1306
  this.logger.debug(`INSIDE::resolveS3Url:: resolvedBucketName: ${resolvedBucketName}`)
1310
1307
 
1311
- if (resolveS3UrlDto.isPrivate == "true") {
1312
- const expiryInSeconds = 60 * 60;
1313
- url = await this.fileService.getUrl(`${resolvedBucketName}:${normalizedKey}`, { expiresIn: expiryInSeconds });
1314
- } else {
1315
- url = `https://${resolvedBucketName}.s3.${this.configService.get(
1316
- 'S3_AWS_REGION_NAME',
1317
- )}.amazonaws.com/${normalizedKey}`;
1318
- }
1308
+ const expiryInSeconds = resolveS3UrlDto.isPrivate == "true" ? 60 * 60 : 0;
1309
+ const url = await this.fileService.getUrl(`${resolvedBucketName}:${normalizedKey}`, {
1310
+ expiresIn: expiryInSeconds,
1311
+ });
1312
+
1319
1313
  return { url: url }
1320
1314
  }
1321
1315
 
@@ -1332,4 +1326,3 @@ export class FieldMetadataService implements OnApplicationBootstrap {
1332
1326
  }
1333
1327
  }
1334
1328
 
1335
-
@@ -44,7 +44,7 @@ export class DiskFileService implements IFileService {
44
44
  async write(filePath: string, data: Buffer | string, options?: WriteOptions): Promise<string> {
45
45
  await this.ensureDirectoryExists(filePath);
46
46
  await fsPromises.writeFile(filePath, data);
47
- return `${this.baseUrl}/${filePath}`;
47
+ return this.buildUrl(filePath);
48
48
  }
49
49
 
50
50
  /**
@@ -57,7 +57,7 @@ export class DiskFileService implements IFileService {
57
57
  const writeStream = fs.createWriteStream(filePath);
58
58
  await pipeline(stream, writeStream);
59
59
  this.logger.debug(`File saved via stream: ${filePath}`);
60
- return `${this.baseUrl}/${filePath}`;
60
+ return this.buildUrl(filePath);
61
61
  }
62
62
 
63
63
  /**
@@ -97,13 +97,10 @@ export class DiskFileService implements IFileService {
97
97
  }
98
98
 
99
99
  /**
100
- * Get an accessible URL/path for the file
101
- * For disk storage, returns the file path as-is
100
+ * Get an accessible URL for the file
102
101
  */
103
102
  async getUrl(filePath: string, options?: UrlOptions): Promise<string> {
104
- // For disk storage, we simply return the path
105
- // The caller is responsible for constructing a full URL if needed
106
- return filePath;
103
+ return this.buildUrl(filePath);
107
104
  }
108
105
 
109
106
  /**
@@ -117,4 +114,15 @@ export class DiskFileService implements IFileService {
117
114
  await fsPromises.mkdir(dir, { recursive: true });
118
115
  }
119
116
  }
117
+
118
+ private buildUrl(filePath: string): string {
119
+ const normalizedBaseUrl = this.baseUrl.replace(/\/+$/, '');
120
+ const normalizedPath = filePath.replace(/^\/+/, '');
121
+
122
+ if (!normalizedBaseUrl) {
123
+ return `/${normalizedPath}`;
124
+ }
125
+
126
+ return `${normalizedBaseUrl}/${normalizedPath}`;
127
+ }
120
128
  }
@@ -53,51 +53,29 @@ export class MediaService extends CRUDService<Media> {
53
53
  if (data.records) {
54
54
 
55
55
  for (const media of data.records) {
56
- if (media.mediaStorageProviderMetadata?.type === MediaStorageProviderType.Filesystem) {
57
- media.relativeUri = `${this.settingService.getConfigValue<SolidCoreSetting>("baseUrl")}/${this.getFullFilePathForDisk(media.relativeUri)}`;
58
- } else if (media.mediaStorageProviderMetadata?.type === MediaStorageProviderType.AwsS3) {
59
- media.relativeUri = this.getAwsS3FullFilePath(
60
- media.relativeUri,
61
- media.mediaStorageProviderMetadata.bucketName,
62
- media.mediaStorageProviderMetadata.region
63
- );
56
+ const mediaStorageProvider = media.mediaStorageProviderMetadata;
57
+
58
+ if (mediaStorageProvider?.type === MediaStorageProviderType.Filesystem) {
59
+ media.relativeUri = await this.diskFileService.getUrl(this.getFullFilePathForDisk(media.relativeUri));
60
+ } else if (mediaStorageProvider?.type === MediaStorageProviderType.AwsS3) {
61
+ media.relativeUri = await this.s3FileService.getUrl(`${mediaStorageProvider.bucketName}:${media.relativeUri}`, { region: mediaStorageProvider.region });
64
62
  }
65
63
  }
66
- // data.records.forEach((media: Media) => {
67
- // if (media.mediaStorageProviderMetadata?.type === MediaStorageProviderType.Filesystem) {
68
- // media.relativeUri = `${process.env.BASE_URL}/${this.getFileSysytemFullFilePath(media.relativeUri)}`;
69
- // } else if (media.mediaStorageProviderMetadata?.type === MediaStorageProviderType.AwsS3) {
70
- // media.relativeUri = this.getAwsS3FullFilePath(
71
- // media.relativeUri,
72
- // media.mediaStorageProviderMetadata.bucketName,
73
- // media.mediaStorageProviderMetadata.region
74
- // );
75
- // }
76
- // });
77
64
  }
78
65
  if (data.groupRecords) {
79
66
 
80
67
  for (const group of data.groupRecords) {
81
68
  for (const media of group.groupData.records) {
82
- if (media.mediaStorageProviderMetadata?.type === MediaStorageProviderType.Filesystem) {
83
- media.relativeUri = `${this.settingService.getConfigValue<SolidCoreSetting>("baseUrl")}/${this.getFullFilePathForDisk(media.relativeUri)}`;
69
+ const mediaStorageProvider = media.mediaStorageProviderMetadata;
70
+
71
+ if (mediaStorageProvider?.type === MediaStorageProviderType.Filesystem) {
72
+ media.relativeUri = await this.diskFileService.getUrl(this.getFullFilePathForDisk(media.relativeUri));
84
73
  }
85
- else if (media.mediaStorageProviderMetadata?.type === MediaStorageProviderType.AwsS3) {
86
- media.relativeUri = this.getAwsS3FullFilePath(media.relativeUri, media.mediaStorageProviderMetadata.bucketName, media.mediaStorageProviderMetadata.region);
74
+ else if (mediaStorageProvider?.type === MediaStorageProviderType.AwsS3) {
75
+ media.relativeUri = await this.s3FileService.getUrl(`${mediaStorageProvider.bucketName}:${media.relativeUri}`, { region: mediaStorageProvider.region });
87
76
  }
88
77
  }
89
78
  }
90
-
91
- // data.groupRecords.forEach((group) => {
92
- // group.groupData.records.forEach((media) => {
93
- // if (media.mediaStorageProviderMetadata?.type === MediaStorageProviderType.Filesystem) {
94
- // media.relativeUri = `${process.env.BASE_URL}/${this.getFileSysytemFullFilePath(media.relativeUri)}`;
95
- // }
96
- // else if (media.mediaStorageProviderMetadata?.type === MediaStorageProviderType.AwsS3) {
97
- // media.relativeUri = this.getAwsS3FullFilePath(media.relativeUri, media.mediaStorageProviderMetadata.bucketName, media.mediaStorageProviderMetadata.region);
98
- // }
99
- // });
100
- // });
101
79
  }
102
80
  return data
103
81
  }
@@ -172,24 +150,12 @@ export class MediaService extends CRUDService<Media> {
172
150
  }
173
151
  }
174
152
  );
175
- // if (media.mediaStorageProviderMetadata.type === 'filesystem') {
176
- // const fileStorageProvider = new FileStorageProvider(this.configService, this.fileService, this);
177
-
178
- // await fileStorageProvider.delete(media, media.fieldMetadata);
179
-
180
- // } else if (media.mediaStorageProviderMetadata.type === 'aws-s3') {
181
- // const fileStorageProvider = new FileS3StorageProvider(this.configService, this.fileService, this);
182
- // await fileStorageProvider.delete(media, media.fieldMetadata);
183
-
184
- // } else {
185
- // }
186
153
  const storageProviderType = media.mediaStorageProviderMetadata.type as MediaStorageProviderType;
187
154
  const storageProvider = await getMediaStorageProvider(this.moduleRef, storageProviderType);
188
155
  await storageProvider.delete(modelEntity, media.fieldMetadata);
189
156
 
190
157
  return this.repo.remove(media);
191
158
  }
192
- //TODO: Move this to a app builder config
193
159
 
194
160
  private getFullFilePathForDisk(fileName: string): string {
195
161
  const base = this.settingService.getConfigValue<SolidCoreSetting>("fileStorageDir")
@@ -200,11 +166,6 @@ export class MediaService extends CRUDService<Media> {
200
166
  return `${base}/${fileName}`;
201
167
  }
202
168
 
203
- private getAwsS3FullFilePath(awsMediaurl: string, bucketName: string, regionName: string): string {
204
- // https://lunarismedia.s3.ap-south-1.amazonaws.com/LUNARIS_CP_REGISTRATION_CREATIVE.jpg
205
- return `https://${bucketName}.s3.${regionName}.amazonaws.com/${awsMediaurl}`;
206
- }
207
-
208
169
  private getFileName(file: Express.Multer.File): string {
209
170
  return `${file.filename}-${file.originalname}`;
210
171
  }
@@ -15,6 +15,7 @@ import { ISettingsProvider, NoInfer, SettingDefinition, SettingLevel } from '../
15
15
  import { ModuleMetadataRepository } from 'src/repository/module-metadata.repository';
16
16
  import type { SolidCoreSetting } from './settings/default-settings-provider.service';
17
17
  import { Logger } from '@nestjs/common';
18
+ import { EncryptionService } from './encryption.service';
18
19
 
19
20
 
20
21
  @Injectable()
@@ -23,6 +24,7 @@ export class SettingService {
23
24
 
24
25
  private settings: SettingDefinition[] = [];
25
26
  private settingsByKey = new Map<string, SettingDefinition>();
27
+ private readonly encryptionService: EncryptionService | null;
26
28
 
27
29
  private readonly arrayKeysToSkip = new Set([
28
30
  'authenticationPasswordRegex',
@@ -38,7 +40,11 @@ export class SettingService {
38
40
  readonly moduleMetadataRepo: ModuleMetadataRepository,
39
41
  private readonly requestContextService: RequestContextService,
40
42
  @InjectRepository(User) private readonly userRepository: Repository<User>,
41
- ) { }
43
+ ) {
44
+ const encKey = process.env.APP_ENCRYPTION_KEY;
45
+ this.encryptionService = encKey ? new EncryptionService(encKey) : null;
46
+ if (!encKey) this._logger.warn('APP_ENCRYPTION_KEY is not set — encrypted settings will not be decrypted');
47
+ }
42
48
 
43
49
  private async getSettingsFromDb(): Promise<Setting[]> {
44
50
  const settings = await this.repo.find({ relations: ['user'] });
@@ -112,7 +118,15 @@ export class SettingService {
112
118
  const settingFromDb = settingsFromDbByKey.get(setting.key);
113
119
  const valueFromDb = settingFromDb?.value;
114
120
  if (settingFromDb?.key && valueFromDb !== undefined && valueFromDb !== null) {
115
- const parsedValue = typeof valueFromDb === 'string' ? this.parseSettingValue(valueFromDb, settingFromDb.key) : valueFromDb;
121
+ let rawValue = valueFromDb;
122
+ if (settingFromDb.encrypted && this.encryptionService) {
123
+ try {
124
+ rawValue = this.encryptionService.decrypt(rawValue);
125
+ } catch {
126
+ this._logger.warn(`Failed to decrypt setting "${setting.key}" — using raw value`);
127
+ }
128
+ }
129
+ const parsedValue = typeof rawValue === 'string' ? this.parseSettingValue(rawValue, settingFromDb.key) : rawValue;
116
130
  return { ...setting, value: parsedValue };
117
131
  }
118
132
  return setting;
@@ -149,30 +163,38 @@ export class SettingService {
149
163
  const settingsToMutate: Setting[] = [];
150
164
  // const settingsToUpdate: Setting[] = [];
151
165
 
152
- for (const { key, value, level, moduleName } of saEditableAndAbove) {
166
+ for (const { key, value, level, moduleName, encrypted } of saEditableAndAbove) {
153
167
  const moduleMetadata = await this.moduleMetadataRepo.findOneBy({ name: moduleName });
154
168
  if (!existingSettingsFromDbByKey.has(key)) {
155
169
  const setting = new Setting();
156
170
  setting.key = key;
157
171
  setting.level = level;
172
+ setting.encrypted = !!encrypted;
158
173
  if (moduleMetadata)
159
174
  setting.moduleMetadata = moduleMetadata;
160
175
 
176
+ let rawValue: string | null;
161
177
  if (typeof value === 'boolean') {
162
- setting.value = value.toString();
178
+ rawValue = value.toString();
163
179
  } else if (Array.isArray(value)) {
164
- setting.value = value.join(',');
180
+ rawValue = value.join(',');
165
181
  } else if (value === null || value === undefined) {
166
- setting.value = null;
182
+ rawValue = null;
167
183
  } else {
168
- setting.value = String(value);
184
+ rawValue = String(value);
169
185
  }
170
186
 
187
+ if (encrypted && this.encryptionService && rawValue !== null) {
188
+ rawValue = this.encryptionService.encrypt(rawValue);
189
+ }
190
+ setting.value = rawValue;
191
+
171
192
  settingsToMutate.push(setting);
172
193
  }
173
194
  else {
174
195
  const setting = existingSettingsFromDbByKey.get(key);
175
196
  setting.level = level;
197
+ setting.encrypted = !!encrypted;
176
198
  if (moduleMetadata)
177
199
  setting.moduleMetadata = moduleMetadata;
178
200
  settingsToMutate.push(setting);
@@ -289,22 +311,29 @@ export class SettingService {
289
311
  }
290
312
 
291
313
  const key = settingDto.key;
292
- // const value = settingDto.value ?? '';
293
314
  const rawValue = settingDto.value;
294
- const value = rawValue === null || rawValue === undefined ? null : String(rawValue);
315
+ let value = rawValue === null || rawValue === undefined ? null : String(rawValue);
295
316
 
296
317
  const settingType = settingDto.type ?? 'system';
318
+ const definition = this.settingsByKey.get(key);
319
+ const shouldEncrypt = !!definition?.encrypted && this.encryptionService !== null && value !== null;
320
+
321
+ if (shouldEncrypt) {
322
+ value = this.encryptionService.encrypt(value);
323
+ }
297
324
 
298
325
  const existingSetting = existingSettings.find(s => s.key === key);
299
326
  if (existingSetting) {
300
327
  existingSetting.value = value;
301
328
  existingSetting.type = settingType;
329
+ existingSetting.encrypted = shouldEncrypt;
302
330
  settingsToUpdate.push(existingSetting);
303
331
  } else {
304
332
  const newSetting = new Setting();
305
333
  newSetting.key = key;
306
334
  newSetting.value = value;
307
335
  newSetting.type = settingType;
336
+ newSetting.encrypted = shouldEncrypt;
308
337
 
309
338
  if (settingType === 'user' && user) {
310
339
  newSetting.user = user;
@@ -34,10 +34,14 @@ const getSolidCoreSettings = (isProd: boolean) => ([
34
34
  { moduleName: "solid-core", key: "authScreenCenterBackgroundImage", value: null, level: SettingLevel.SystemAdminEditable },
35
35
  {
36
36
  moduleName: "solid-core", key: "solidXGenAiCodeBuilderConfig", value: JSON.stringify({
37
- fastModel: { provider: "", availableProviders: [] },
38
- defaultProvider: { provider: "", availableProviders: [] },
39
- }), level: SettingLevel.SystemAdminEditable
37
+ models: {
38
+ default: { providerId: "", model: "", behavior: { streaming: false, custom: "" } },
39
+ fast: { providerId: "", model: "", behavior: { streaming: false, custom: "" } },
40
+ },
41
+ providers: {},
42
+ }), level: SettingLevel.SystemAdminEditable, encrypted: true
40
43
  },
44
+ { moduleName: "solid-core", key: "appEncryptionKey", value: process.env.APP_ENCRYPTION_KEY, level: SettingLevel.SystemEnv },
41
45
  { moduleName: "solid-core", key: "mcpEnabled", value: process.env.MCP_ENABLED || false, level: SettingLevel.SystemAdminReadonly },
42
46
  { moduleName: "solid-core", key: "mcpServerUrl", value: process.env.MCP_SERVER_URL, level: SettingLevel.SystemAdminReadonly },
43
47
  { moduleName: "solid-core", key: "mcpApiKey", value: process.env.MCP_API_KEY, level: SettingLevel.SystemEnv },
@@ -0,0 +1,36 @@
1
+ import { CACHE_MANAGER } from '@nestjs/cache-manager';
2
+ import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
3
+ import { Cache } from 'cache-manager';
4
+ import { randomBytes } from 'crypto';
5
+
6
+ const SSO_CODE_TTL_MS = 60 * 1000; // 60 seconds
7
+
8
+ interface SsoCodeEntry {
9
+ userId: number;
10
+ accessToken: string;
11
+ refreshToken: string;
12
+ }
13
+
14
+ @Injectable()
15
+ export class SsoCodeStorageService {
16
+ constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
17
+
18
+ async generateCode(userId: number, accessToken: string, refreshToken: string): Promise<string> {
19
+ const code = randomBytes(32).toString('hex');
20
+ await this.cacheManager.set(this.getKey(code), { userId, accessToken, refreshToken }, SSO_CODE_TTL_MS);
21
+ return code;
22
+ }
23
+
24
+ async consumeCode(code: string): Promise<SsoCodeEntry> {
25
+ const entry = await this.cacheManager.get<SsoCodeEntry>(this.getKey(code));
26
+ if (!entry) {
27
+ throw new UnauthorizedException('Invalid or expired SSO code');
28
+ }
29
+ await this.cacheManager.del(this.getKey(code));
30
+ return entry;
31
+ }
32
+
33
+ private getKey(code: string): string {
34
+ return `sso-code-${code}`;
35
+ }
36
+ }
@@ -102,8 +102,9 @@ export class UserService extends CRUDService<User> {
102
102
  if (!user) {
103
103
  throw new Error(ERROR_MESSAGES.USER_NOT_FOUND);
104
104
  }
105
- const roles = updateDto.roles ? updateDto.roles : [];
106
- await this.addRolesToUser(user.username, roles);
105
+ if (updateDto.roles != null) {
106
+ await this.addRolesToUser(user.username, updateDto.roles);
107
+ }
107
108
  await this.update(id, updateDto, files, true);
108
109
  }
109
110