@solidxai/core 0.1.8-beta.8 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +197 -0
- package/dist/controllers/authentication.controller.d.ts +32 -2
- package/dist/controllers/authentication.controller.d.ts.map +1 -1
- package/dist/controllers/authentication.controller.js +80 -3
- package/dist/controllers/authentication.controller.js.map +1 -1
- package/dist/dtos/create-api-key.dto.d.ts +5 -0
- package/dist/dtos/create-api-key.dto.d.ts.map +1 -0
- package/dist/dtos/create-api-key.dto.js +34 -0
- package/dist/dtos/create-api-key.dto.js.map +1 -0
- package/dist/dtos/register-private.dto.d.ts +3 -5
- package/dist/dtos/register-private.dto.d.ts.map +1 -1
- package/dist/dtos/register-private.dto.js +6 -18
- package/dist/dtos/register-private.dto.js.map +1 -1
- package/dist/dtos/sso-exchange.dto.d.ts +4 -0
- package/dist/dtos/sso-exchange.dto.d.ts.map +1 -0
- package/dist/dtos/sso-exchange.dto.js +26 -0
- package/dist/dtos/sso-exchange.dto.js.map +1 -0
- package/dist/dtos/update-api-key.dto.d.ts +4 -0
- package/dist/dtos/update-api-key.dto.d.ts.map +1 -0
- package/dist/dtos/update-api-key.dto.js +28 -0
- package/dist/dtos/update-api-key.dto.js.map +1 -0
- package/dist/entities/agent-event.entity.js +1 -1
- package/dist/entities/agent-event.entity.js.map +1 -1
- package/dist/entities/agent-session.entity.js +1 -1
- package/dist/entities/agent-session.entity.js.map +1 -1
- package/dist/entities/setting.entity.d.ts +1 -0
- package/dist/entities/setting.entity.d.ts.map +1 -1
- package/dist/entities/setting.entity.js +5 -1
- package/dist/entities/setting.entity.js.map +1 -1
- package/dist/entities/user-api-key.entity.d.ts +12 -0
- package/dist/entities/user-api-key.entity.d.ts.map +1 -0
- package/dist/entities/user-api-key.entity.js +62 -0
- package/dist/entities/user-api-key.entity.js.map +1 -0
- package/dist/entities/user.entity.d.ts +3 -0
- package/dist/entities/user.entity.d.ts.map +1 -1
- package/dist/entities/user.entity.js +12 -1
- package/dist/entities/user.entity.js.map +1 -1
- package/dist/enums/auth-type.enum.d.ts +2 -1
- package/dist/enums/auth-type.enum.d.ts.map +1 -1
- package/dist/enums/auth-type.enum.js +2 -1
- package/dist/enums/auth-type.enum.js.map +1 -1
- package/dist/guards/api-key.guard.d.ts +11 -0
- package/dist/guards/api-key.guard.d.ts.map +1 -0
- package/dist/guards/api-key.guard.js +43 -0
- package/dist/guards/api-key.guard.js.map +1 -0
- package/dist/guards/authentication.guard.d.ts +4 -2
- package/dist/guards/authentication.guard.d.ts.map +1 -1
- package/dist/guards/authentication.guard.js +7 -3
- package/dist/guards/authentication.guard.js.map +1 -1
- package/dist/helpers/bootstrap.helper.d.ts.map +1 -1
- package/dist/helpers/bootstrap.helper.js +11 -0
- package/dist/helpers/bootstrap.helper.js.map +1 -1
- package/dist/helpers/field-crud-managers/SelectionDynamicFieldCrudManager.d.ts.map +1 -1
- package/dist/helpers/field-crud-managers/SelectionDynamicFieldCrudManager.js +15 -6
- package/dist/helpers/field-crud-managers/SelectionDynamicFieldCrudManager.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/interfaces.d.ts +12 -0
- package/dist/interfaces.d.ts.map +1 -1
- package/dist/interfaces.js.map +1 -1
- package/dist/jobs/database/chatter-queue-publisher-database.service.d.ts +1 -1
- package/dist/jobs/database/chatter-queue-publisher-database.service.d.ts.map +1 -1
- package/dist/jobs/database/chatter-queue-publisher-database.service.js.map +1 -1
- package/dist/jobs/database/chatter-queue-subscriber-database.service.d.ts +1 -1
- package/dist/jobs/database/chatter-queue-subscriber-database.service.d.ts.map +1 -1
- package/dist/jobs/database/chatter-queue-subscriber-database.service.js.map +1 -1
- package/dist/jobs/rabbitmq/chatter-queue-publisher.service.d.ts +1 -12
- package/dist/jobs/rabbitmq/chatter-queue-publisher.service.d.ts.map +1 -1
- package/dist/jobs/rabbitmq/chatter-queue-publisher.service.js.map +1 -1
- package/dist/jobs/rabbitmq/chatter-queue-subscriber.service.d.ts +1 -1
- package/dist/jobs/rabbitmq/chatter-queue-subscriber.service.d.ts.map +1 -1
- package/dist/jobs/rabbitmq/chatter-queue-subscriber.service.js.map +1 -1
- package/dist/jobs/redis/chatter-queue-subscriber-redis.service.d.ts +1 -1
- package/dist/jobs/redis/chatter-queue-subscriber-redis.service.d.ts.map +1 -1
- package/dist/jobs/redis/chatter-queue-subscriber-redis.service.js.map +1 -1
- package/dist/repository/user-api-key.repository.d.ts +12 -0
- package/dist/repository/user-api-key.repository.d.ts.map +1 -0
- package/dist/repository/user-api-key.repository.js +34 -0
- package/dist/repository/user-api-key.repository.js.map +1 -0
- package/dist/seeders/seed-data/solid-core-metadata.json +145 -20
- package/dist/services/api-key.service.d.ts +20 -0
- package/dist/services/api-key.service.d.ts.map +1 -0
- package/dist/services/api-key.service.js +98 -0
- package/dist/services/api-key.service.js.map +1 -0
- package/dist/services/authentication.service.d.ts +19 -1
- package/dist/services/authentication.service.d.ts.map +1 -1
- package/dist/services/authentication.service.js +31 -5
- package/dist/services/authentication.service.js.map +1 -1
- package/dist/services/encryption.service.d.ts +8 -0
- package/dist/services/encryption.service.d.ts.map +1 -0
- package/dist/services/encryption.service.js +75 -0
- package/dist/services/encryption.service.js.map +1 -0
- package/dist/services/setting.service.d.ts +1 -0
- package/dist/services/setting.service.d.ts.map +1 -1
- package/dist/services/setting.service.js +35 -7
- package/dist/services/setting.service.js.map +1 -1
- package/dist/services/settings/default-settings-provider.service.d.ts +12 -0
- package/dist/services/settings/default-settings-provider.service.d.ts.map +1 -1
- package/dist/services/settings/default-settings-provider.service.js +4 -3
- package/dist/services/settings/default-settings-provider.service.js.map +1 -1
- package/dist/services/sso-code-storage.service.d.ts +15 -0
- package/dist/services/sso-code-storage.service.d.ts.map +1 -0
- package/dist/services/sso-code-storage.service.js +47 -0
- package/dist/services/sso-code-storage.service.js.map +1 -0
- package/dist/solid-core.module.d.ts.map +1 -1
- package/dist/solid-core.module.js +10 -0
- package/dist/solid-core.module.js.map +1 -1
- package/dist/subscribers/audit.subscriber.d.ts +1 -1
- package/dist/subscribers/audit.subscriber.d.ts.map +1 -1
- package/dist/subscribers/audit.subscriber.js.map +1 -1
- package/package.json +1 -1
- package/src/controllers/authentication.controller.ts +59 -3
- package/src/dtos/create-api-key.dto.ts +14 -0
- package/src/dtos/register-private.dto.ts +5 -14
- package/src/dtos/sso-exchange.dto.ts +7 -0
- package/src/dtos/update-api-key.dto.ts +9 -0
- package/src/entities/agent-event.entity.ts +1 -1
- package/src/entities/agent-session.entity.ts +1 -1
- package/src/entities/setting.entity.ts +3 -0
- package/src/entities/user-api-key.entity.ts +37 -0
- package/src/entities/user.entity.ts +8 -0
- package/src/enums/auth-type.enum.ts +1 -0
- package/src/guards/api-key.guard.ts +32 -0
- package/src/guards/authentication.guard.ts +6 -3
- package/src/helpers/bootstrap.helper.ts +15 -0
- package/src/helpers/field-crud-managers/SelectionDynamicFieldCrudManager.ts +17 -6
- package/src/index.ts +2 -0
- package/src/interfaces.ts +16 -0
- package/src/jobs/database/chatter-queue-publisher-database.service.ts +1 -1
- package/src/jobs/database/chatter-queue-subscriber-database.service.ts +1 -1
- package/src/jobs/rabbitmq/chatter-queue-publisher.service.ts +1 -15
- package/src/jobs/rabbitmq/chatter-queue-subscriber.service.ts +1 -1
- package/src/jobs/redis/chatter-queue-subscriber-redis.service.ts +1 -1
- package/src/repository/user-api-key.repository.ts +17 -0
- package/src/seeders/seed-data/solid-core-metadata.json +145 -20
- package/src/services/api-key.service.ts +111 -0
- package/src/services/authentication.service.ts +35 -3
- package/src/services/encryption.service.ts +43 -0
- package/src/services/setting.service.ts +38 -9
- package/src/services/settings/default-settings-provider.service.ts +4 -3
- package/src/services/sso-code-storage.service.ts +36 -0
- package/src/solid-core.module.ts +10 -0
- 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
|
|
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 {
|
|
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 {
|
|
@@ -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
|
+
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
178
|
+
rawValue = value.toString();
|
|
163
179
|
} else if (Array.isArray(value)) {
|
|
164
|
-
|
|
180
|
+
rawValue = value.join(',');
|
|
165
181
|
} else if (value === null || value === undefined) {
|
|
166
|
-
|
|
182
|
+
rawValue = null;
|
|
167
183
|
} else {
|
|
168
|
-
|
|
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
|
-
|
|
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;
|
|
@@ -35,12 +35,13 @@ const getSolidCoreSettings = (isProd: boolean) => ([
|
|
|
35
35
|
{
|
|
36
36
|
moduleName: "solid-core", key: "solidXGenAiCodeBuilderConfig", value: JSON.stringify({
|
|
37
37
|
models: {
|
|
38
|
-
default: {
|
|
39
|
-
fast: {
|
|
38
|
+
default: { providerId: "", model: "", behavior: { streaming: false, custom: "" } },
|
|
39
|
+
fast: { providerId: "", model: "", behavior: { streaming: false, custom: "" } },
|
|
40
40
|
},
|
|
41
41
|
providers: {},
|
|
42
|
-
}), level: SettingLevel.SystemAdminEditable
|
|
42
|
+
}), level: SettingLevel.SystemAdminEditable, encrypted: true
|
|
43
43
|
},
|
|
44
|
+
{ moduleName: "solid-core", key: "appEncryptionKey", value: process.env.APP_ENCRYPTION_KEY, level: SettingLevel.SystemEnv },
|
|
44
45
|
{ moduleName: "solid-core", key: "mcpEnabled", value: process.env.MCP_ENABLED || false, level: SettingLevel.SystemAdminReadonly },
|
|
45
46
|
{ moduleName: "solid-core", key: "mcpServerUrl", value: process.env.MCP_SERVER_URL, level: SettingLevel.SystemAdminReadonly },
|
|
46
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
|
+
}
|
package/src/solid-core.module.ts
CHANGED
|
@@ -78,6 +78,7 @@ import { MqMessageQueue } from './entities/mq-message-queue.entity';
|
|
|
78
78
|
import { MqMessage } from './entities/mq-message.entity';
|
|
79
79
|
import { SmsTemplate } from './entities/sms-template.entity';
|
|
80
80
|
import { AccessTokenGuard } from './guards/access-token.guard';
|
|
81
|
+
import { ApiKeyGuard } from './guards/api-key.guard';
|
|
81
82
|
import { AuthenticationGuard } from './guards/authentication.guard';
|
|
82
83
|
import { PermissionsGuard } from './guards/permissions.guard';
|
|
83
84
|
import { SolidRegistry } from './helpers/solid-registry';
|
|
@@ -126,6 +127,7 @@ import { TwilioSmsQueuePublisherRedis } from './jobs/redis/twilio-sms-publisher-
|
|
|
126
127
|
import { TwilioSmsQueueSubscriberRedis } from './jobs/redis/twilio-sms-subscriber-redis.service';
|
|
127
128
|
import { UserRegistrationListener } from './listeners/user-registration.listener';
|
|
128
129
|
import { GoogleOauthStrategy } from './passport-strategies/google-oauth.strategy';
|
|
130
|
+
import { ApiKeyService } from './services/api-key.service';
|
|
129
131
|
import { AuthenticationService } from './services/authentication.service';
|
|
130
132
|
import { BcryptService } from './services/bcrypt.service';
|
|
131
133
|
import { UuidExternalIdEntityComputedFieldProvider } from './services/computed-fields/entity/uuid-externalid-entity-computed-field-provider.service';
|
|
@@ -140,6 +142,7 @@ import { MqMessageQueueService } from './services/mq-message-queue.service';
|
|
|
140
142
|
import { MqMessageService } from './services/mq-message.service';
|
|
141
143
|
import { PdfService } from './services/pdf.service';
|
|
142
144
|
import { RefreshTokenIdsStorageService } from './services/refresh-token-ids-storage.service';
|
|
145
|
+
import { SsoCodeStorageService } from './services/sso-code-storage.service';
|
|
143
146
|
import { ListOfModelsSelectionProvider } from './services/selection-providers/list-of-models-selection-provider.service';
|
|
144
147
|
import { TinyUrlService } from './services/short-url/tiny-url.service';
|
|
145
148
|
import { SmsTemplateService } from './services/sms-template.service';
|
|
@@ -205,6 +208,7 @@ import { SecurityRule } from './entities/security-rule.entity';
|
|
|
205
208
|
import { Setting } from './entities/setting.entity';
|
|
206
209
|
import { UserActivityHistory } from './entities/user-activity-history.entity';
|
|
207
210
|
import { UserViewMetadata } from './entities/user-view-metadata.entity';
|
|
211
|
+
import { UserApiKey } from './entities/user-api-key.entity';
|
|
208
212
|
import { User } from './entities/user.entity';
|
|
209
213
|
import { HttpExceptionFilter } from './filters/http-exception.filter';
|
|
210
214
|
import { ModelMetadataHelperService } from './helpers/model-metadata-helper.service';
|
|
@@ -284,6 +288,7 @@ import { SettingRepository } from './repository/setting.repository';
|
|
|
284
288
|
import { SmsTemplateRepository } from './repository/sms-template.repository';
|
|
285
289
|
import { UserActivityHistoryRepository } from './repository/user-activity-history.repository';
|
|
286
290
|
import { UserViewMetadataRepository } from './repository/user-view-metadata.repository';
|
|
291
|
+
import { UserApiKeyRepository } from './repository/user-api-key.repository';
|
|
287
292
|
import { UserRepository } from './repository/user.repository';
|
|
288
293
|
import { ViewMetadataRepository } from './repository/view-metadata.repository';
|
|
289
294
|
import { PermissionMetadataSeederService } from './seeders/permission-metadata-seeder.service';
|
|
@@ -415,6 +420,7 @@ import { Entity } from 'typeorm';
|
|
|
415
420
|
Setting,
|
|
416
421
|
SmsTemplate,
|
|
417
422
|
User,
|
|
423
|
+
UserApiKey,
|
|
418
424
|
UserActivityHistory,
|
|
419
425
|
UserViewMetadata,
|
|
420
426
|
ViewMetadata,
|
|
@@ -627,9 +633,12 @@ import { Entity } from 'typeorm';
|
|
|
627
633
|
LocaleListSelectionProvider,
|
|
628
634
|
SoftDeleteAwareEventSubscriber,
|
|
629
635
|
AccessTokenGuard,
|
|
636
|
+
ApiKeyGuard,
|
|
637
|
+
ApiKeyService,
|
|
630
638
|
AuthenticationService,
|
|
631
639
|
GoogleAuthenticationController,
|
|
632
640
|
RefreshTokenIdsStorageService,
|
|
641
|
+
SsoCodeStorageService,
|
|
633
642
|
GoogleOauthStrategy,
|
|
634
643
|
UserRegistrationListener,
|
|
635
644
|
TestQueuePublisher,
|
|
@@ -679,6 +688,7 @@ import { Entity } from 'typeorm';
|
|
|
679
688
|
RoleMetadataService,
|
|
680
689
|
PermissionMetadataSeederService,
|
|
681
690
|
UserService,
|
|
691
|
+
UserApiKeyRepository,
|
|
682
692
|
UserRepository,
|
|
683
693
|
SettingService,
|
|
684
694
|
ConcatComputedFieldProvider,
|
|
@@ -2,7 +2,7 @@ import { Injectable, Logger, Scope } from '@nestjs/common';
|
|
|
2
2
|
import { lowerFirst } from 'src/helpers/string.helper';
|
|
3
3
|
import { SolidRegistry } from 'src/helpers/solid-registry';
|
|
4
4
|
import { DataSource, EntityMetadata, EntitySubscriberInterface, InsertEvent, RemoveEvent, UpdateEvent } from 'typeorm';
|
|
5
|
-
import { AuditQueuePayload } from 'src/
|
|
5
|
+
import { AuditQueuePayload } from 'src/interfaces';
|
|
6
6
|
import { RequestContextService } from 'src/services/request-context.service';
|
|
7
7
|
import { PublisherFactory } from 'src/services/queues/publisher-factory.service';
|
|
8
8
|
|