@solidxai/core 0.1.8-beta.9 → 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/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/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 +128 -0
- 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/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/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 +128 -0
- 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,37 @@
|
|
|
1
|
+
import { Exclude, Expose } from "class-transformer";
|
|
2
|
+
import { CommonEntity } from "src/entities/common.entity";
|
|
3
|
+
import { Column, Entity, Index, ManyToOne } from "typeorm";
|
|
4
|
+
import { User } from "./user.entity";
|
|
5
|
+
|
|
6
|
+
@Entity("ss_user_api_key")
|
|
7
|
+
@Exclude()
|
|
8
|
+
export class UserApiKey extends CommonEntity {
|
|
9
|
+
|
|
10
|
+
@Expose()
|
|
11
|
+
@Column()
|
|
12
|
+
name: string;
|
|
13
|
+
|
|
14
|
+
// SHA-256 hash of the raw key — never exposed, same treatment as User.password
|
|
15
|
+
@Index({ unique: true })
|
|
16
|
+
@Column()
|
|
17
|
+
hashedKey: string;
|
|
18
|
+
|
|
19
|
+
@Expose()
|
|
20
|
+
@Column()
|
|
21
|
+
maskedKey: string;
|
|
22
|
+
|
|
23
|
+
@Expose()
|
|
24
|
+
@Column({ default: true })
|
|
25
|
+
isActive: boolean;
|
|
26
|
+
|
|
27
|
+
@Expose()
|
|
28
|
+
@Column({ nullable: true })
|
|
29
|
+
expiresAt: Date;
|
|
30
|
+
|
|
31
|
+
@Expose()
|
|
32
|
+
@Column({ nullable: true })
|
|
33
|
+
lastUsedAt: Date;
|
|
34
|
+
|
|
35
|
+
@ManyToOne(() => User, user => user.apiKeys)
|
|
36
|
+
user: User;
|
|
37
|
+
}
|
|
@@ -2,6 +2,7 @@ import { CommonEntity } from "src/entities/common.entity"
|
|
|
2
2
|
import { Entity, Column, Index, JoinTable, ManyToMany, OneToMany, TableInheritance } from "typeorm";
|
|
3
3
|
import { RoleMetadata } from 'src/entities/role-metadata.entity';
|
|
4
4
|
import { UserViewMetadata } from 'src/entities/user-view-metadata.entity'
|
|
5
|
+
import { UserApiKey } from 'src/entities/user-api-key.entity'
|
|
5
6
|
import { Exclude, Expose } from "class-transformer";
|
|
6
7
|
|
|
7
8
|
@Entity("ss_user")
|
|
@@ -151,4 +152,11 @@ export class User extends CommonEntity {
|
|
|
151
152
|
@Expose()
|
|
152
153
|
_media: any;
|
|
153
154
|
|
|
155
|
+
@Column({ default: false })
|
|
156
|
+
@Expose()
|
|
157
|
+
isAllowedToGenerateApiKeys: boolean = false;
|
|
158
|
+
|
|
159
|
+
@OneToMany(() => UserApiKey, key => key.user)
|
|
160
|
+
apiKeys: UserApiKey[];
|
|
161
|
+
|
|
154
162
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
|
|
2
|
+
import { Request } from 'express';
|
|
3
|
+
import { REQUEST_USER_KEY } from 'src/constants';
|
|
4
|
+
import { ApiKeyService } from 'src/services/api-key.service';
|
|
5
|
+
import { ClsService } from 'nestjs-cls';
|
|
6
|
+
|
|
7
|
+
@Injectable()
|
|
8
|
+
export class ApiKeyGuard implements CanActivate {
|
|
9
|
+
constructor(
|
|
10
|
+
private readonly apiKeyService: ApiKeyService,
|
|
11
|
+
private readonly cls: ClsService,
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
15
|
+
const request = context.switchToHttp().getRequest<Request>();
|
|
16
|
+
const rawKey = this.extractKeyFromHeader(request);
|
|
17
|
+
|
|
18
|
+
if (!rawKey) {
|
|
19
|
+
throw new UnauthorizedException();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const activeUser = await this.apiKeyService.validate(rawKey);
|
|
23
|
+
request[REQUEST_USER_KEY] = activeUser;
|
|
24
|
+
this.cls.set(REQUEST_USER_KEY, activeUser);
|
|
25
|
+
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private extractKeyFromHeader(request: Request): string | undefined {
|
|
30
|
+
return request.headers['solidx-api-key'] as string | undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -8,23 +8,26 @@ import { Reflector } from '@nestjs/core';
|
|
|
8
8
|
import { AUTH_TYPE_KEY } from '../decorators/auth.decorator';
|
|
9
9
|
import { AuthType } from '../enums/auth-type.enum';
|
|
10
10
|
import { AccessTokenGuard } from './access-token.guard';
|
|
11
|
+
import { ApiKeyGuard } from './api-key.guard';
|
|
11
12
|
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
|
|
12
13
|
import { PermissionMetadataService } from '../services/permission-metadata.service';
|
|
13
14
|
import { ClsService } from 'nestjs-cls';
|
|
14
15
|
|
|
15
16
|
@Injectable()
|
|
16
17
|
export class AuthenticationGuard implements CanActivate {
|
|
17
|
-
private static readonly
|
|
18
|
+
private static readonly defaultAuthTypes = [AuthType.Bearer, AuthType.ApiKey];
|
|
18
19
|
private readonly authTypeGuardMap: Record<
|
|
19
20
|
AuthType,
|
|
20
21
|
CanActivate | CanActivate[]> = {
|
|
21
22
|
[AuthType.Bearer]: this.accessTokenGuard,
|
|
23
|
+
[AuthType.ApiKey]: this.apiKeyGuard,
|
|
22
24
|
[AuthType.None]: { canActivate: () => true },
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
constructor(
|
|
26
28
|
private readonly reflector: Reflector,
|
|
27
29
|
private readonly accessTokenGuard: AccessTokenGuard,
|
|
30
|
+
private readonly apiKeyGuard: ApiKeyGuard,
|
|
28
31
|
private readonly permissionService: PermissionMetadataService,
|
|
29
32
|
private readonly cls: ClsService,
|
|
30
33
|
) { }
|
|
@@ -49,7 +52,7 @@ export class AuthenticationGuard implements CanActivate {
|
|
|
49
52
|
return true;
|
|
50
53
|
}
|
|
51
54
|
|
|
52
|
-
// TODO: Check if this permission viz. contextPermission is listed in the Public role.
|
|
55
|
+
// TODO: Check if this permission viz. contextPermission is listed in the Public role.
|
|
53
56
|
const contextPermission = `${context.getClass().name}.${context.getHandler().name}`;
|
|
54
57
|
|
|
55
58
|
const permissionExistsInRole = await this.permissionService.permissionExistsInRole('Public', contextPermission)
|
|
@@ -61,7 +64,7 @@ export class AuthenticationGuard implements CanActivate {
|
|
|
61
64
|
const authTypes = this.reflector.getAllAndOverride<AuthType[]>(
|
|
62
65
|
AUTH_TYPE_KEY,
|
|
63
66
|
[context.getHandler(), context.getClass()],
|
|
64
|
-
) ??
|
|
67
|
+
) ?? AuthenticationGuard.defaultAuthTypes;
|
|
65
68
|
const guards = authTypes.map((type) => this.authTypeGuardMap[type]).flat();
|
|
66
69
|
let error = new UnauthorizedException();
|
|
67
70
|
|
package/src/index.ts
CHANGED
|
@@ -126,6 +126,7 @@ export * from './entities/permission-metadata.entity'
|
|
|
126
126
|
export * from './entities/role-metadata.entity'
|
|
127
127
|
export * from './entities/sms-template.entity'
|
|
128
128
|
export * from './entities/user.entity'
|
|
129
|
+
export * from './entities/user-api-key.entity'
|
|
129
130
|
export * from './entities/view-metadata.entity'
|
|
130
131
|
export * from './entities/setting.entity'
|
|
131
132
|
export * from './entities/saved-filters.entity'
|
|
@@ -314,6 +315,7 @@ export * from './services/user.service'
|
|
|
314
315
|
export * from './services/view-metadata.service'
|
|
315
316
|
export * from './services/whatsapp/Msg91WhatsappService' //rename
|
|
316
317
|
export * from './services/setting.service'
|
|
318
|
+
export * from './services/encryption.service'
|
|
317
319
|
export * from './services/info.service'
|
|
318
320
|
export * from './controllers/info.controller'
|
|
319
321
|
export * from './services/settings/default-settings-provider.service'
|
package/src/interfaces.ts
CHANGED
|
@@ -70,6 +70,7 @@ export interface SettingDefinition<T = any> {
|
|
|
70
70
|
key: string;
|
|
71
71
|
value: T;
|
|
72
72
|
level: SettingLevel;
|
|
73
|
+
encrypted?: boolean;
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
// solid-core/settings/settings-provider.interface.ts
|
|
@@ -395,3 +396,18 @@ export interface AwsS3Config {
|
|
|
395
396
|
|
|
396
397
|
// Prevents inference so callers must provide explicit type arguments; reusable for other APIs.
|
|
397
398
|
export type NoInfer<T> = [T][T extends any ? 0 : never];
|
|
399
|
+
|
|
400
|
+
export type AuditEventType = 'insert' | 'update' | 'delete';
|
|
401
|
+
|
|
402
|
+
export interface AuditQueuePayload {
|
|
403
|
+
eventType: AuditEventType;
|
|
404
|
+
modelName: string;
|
|
405
|
+
entityId: string | number | null;
|
|
406
|
+
occurredAt: string;
|
|
407
|
+
after?: any;
|
|
408
|
+
before?: any;
|
|
409
|
+
updatedColumnNames?: string[];
|
|
410
|
+
userId?: number | null;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
|
|
@@ -4,7 +4,7 @@ import { DatabasePublisher } from 'src/services/queues/database-publisher.servic
|
|
|
4
4
|
import { MqMessageQueueService } from '../../services/mq-message-queue.service';
|
|
5
5
|
import { MqMessageService } from '../../services/mq-message.service';
|
|
6
6
|
import { QueuesModuleOptions } from "../../interfaces";
|
|
7
|
-
import { AuditQueuePayload } from '
|
|
7
|
+
import { AuditQueuePayload } from '../../interfaces';
|
|
8
8
|
import chatterQueueOptionsDatabase from './chatter-queue-options-database';
|
|
9
9
|
|
|
10
10
|
@Injectable()
|
|
@@ -6,7 +6,7 @@ import { MqMessageService } from '../../services/mq-message.service';
|
|
|
6
6
|
import { MqMessageQueueService } from '../../services/mq-message-queue.service';
|
|
7
7
|
import { QueuesModuleOptions } from "../../interfaces";
|
|
8
8
|
import { PollerService } from 'src/services/poller.service';
|
|
9
|
-
import { AuditQueuePayload } from '
|
|
9
|
+
import { AuditQueuePayload } from '../../interfaces';
|
|
10
10
|
import { ChatterMessageService } from 'src/services/chatter-message.service';
|
|
11
11
|
import chatterQueueOptionsDatabase from './chatter-queue-options-database';
|
|
12
12
|
|
|
@@ -4,21 +4,7 @@ import { RabbitMqPublisher } from 'src/services/queues/rabbitmq-publisher.servic
|
|
|
4
4
|
import chatterQueueOptions from './chatter-queue-options';
|
|
5
5
|
import { MqMessageQueueService } from '../../services/mq-message-queue.service';
|
|
6
6
|
import { MqMessageService } from '../../services/mq-message.service';
|
|
7
|
-
import { QueuesModuleOptions } from "../../interfaces";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export type AuditEventType = 'insert' | 'update' | 'delete';
|
|
11
|
-
|
|
12
|
-
export interface AuditQueuePayload {
|
|
13
|
-
eventType: AuditEventType;
|
|
14
|
-
modelName: string; // TypeORM entity class name (e.g. 'Order')
|
|
15
|
-
entityId: string | number | null;
|
|
16
|
-
occurredAt: string; // ISO timestamp, captured at event time
|
|
17
|
-
after?: any; // entity state after operation (insert/update)
|
|
18
|
-
before?: any; // entity state before operation (update/delete)
|
|
19
|
-
updatedColumnNames?: string[]; // propertyNames of changed columns (update only)
|
|
20
|
-
userId?: number | null; // active user captured at event time
|
|
21
|
-
}
|
|
7
|
+
import { AuditQueuePayload, QueuesModuleOptions } from "../../interfaces";
|
|
22
8
|
|
|
23
9
|
@Injectable()
|
|
24
10
|
export class ChatterQueuePublisherRabbitmq extends RabbitMqPublisher<AuditQueuePayload> {
|
|
@@ -6,7 +6,7 @@ import { MqMessageService } from '../../services/mq-message.service';
|
|
|
6
6
|
import { MqMessageQueueService } from '../../services/mq-message-queue.service';
|
|
7
7
|
import { QueuesModuleOptions } from "../../interfaces";
|
|
8
8
|
import chatterQueueOptions from './chatter-queue-options';
|
|
9
|
-
import { AuditQueuePayload } from '
|
|
9
|
+
import { AuditQueuePayload } from '../../interfaces';
|
|
10
10
|
import { ChatterMessageService } from 'src/services/chatter-message.service';
|
|
11
11
|
|
|
12
12
|
@Injectable()
|
|
@@ -6,7 +6,7 @@ import chatterQueueConfig from './chatter-queue-options-redis';
|
|
|
6
6
|
import { MqMessageService } from '../../services/mq-message.service';
|
|
7
7
|
import { MqMessageQueueService } from '../../services/mq-message-queue.service';
|
|
8
8
|
import { QueuesModuleOptions } from "../../interfaces";
|
|
9
|
-
import { AuditQueuePayload } from '
|
|
9
|
+
import { AuditQueuePayload } from '../../interfaces';
|
|
10
10
|
import { ChatterMessageService } from '../../services/chatter-message.service';
|
|
11
11
|
|
|
12
12
|
@Injectable()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { UserApiKey } from 'src/entities/user-api-key.entity';
|
|
3
|
+
import { RequestContextService } from 'src/services/request-context.service';
|
|
4
|
+
import { DataSource } from 'typeorm';
|
|
5
|
+
import { SecurityRuleRepository } from './security-rule.repository';
|
|
6
|
+
import { SolidBaseRepository } from './solid-base.repository';
|
|
7
|
+
|
|
8
|
+
@Injectable()
|
|
9
|
+
export class UserApiKeyRepository extends SolidBaseRepository<UserApiKey> {
|
|
10
|
+
constructor(
|
|
11
|
+
readonly dataSource: DataSource,
|
|
12
|
+
readonly requestContextService: RequestContextService,
|
|
13
|
+
readonly securityRuleRepository: SecurityRuleRepository,
|
|
14
|
+
) {
|
|
15
|
+
super(UserApiKey, dataSource, requestContextService, securityRuleRepository);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -2085,6 +2085,35 @@
|
|
|
2085
2085
|
"relationModelModuleName": "solid-core",
|
|
2086
2086
|
"isSystem": true
|
|
2087
2087
|
},
|
|
2088
|
+
{
|
|
2089
|
+
"name": "isAllowedToGenerateApiKeys",
|
|
2090
|
+
"displayName": "Is Allowed To Generate API Keys",
|
|
2091
|
+
"type": "boolean",
|
|
2092
|
+
"required": true,
|
|
2093
|
+
"unique": false,
|
|
2094
|
+
"index": false,
|
|
2095
|
+
"private": false,
|
|
2096
|
+
"encrypt": false,
|
|
2097
|
+
"defaultValue": "false",
|
|
2098
|
+
"isSystem": true,
|
|
2099
|
+
"enableAuditTracking": true
|
|
2100
|
+
},
|
|
2101
|
+
{
|
|
2102
|
+
"name": "apiKeys",
|
|
2103
|
+
"displayName": "API Keys",
|
|
2104
|
+
"type": "relation",
|
|
2105
|
+
"required": false,
|
|
2106
|
+
"unique": false,
|
|
2107
|
+
"index": false,
|
|
2108
|
+
"private": false,
|
|
2109
|
+
"encrypt": false,
|
|
2110
|
+
"relationType": "one-to-many",
|
|
2111
|
+
"relationCoModelSingularName": "userApiKey",
|
|
2112
|
+
"relationCoModelFieldName": "user",
|
|
2113
|
+
"relationCreateInverse": true,
|
|
2114
|
+
"relationModelModuleName": "solid-core",
|
|
2115
|
+
"isSystem": true
|
|
2116
|
+
},
|
|
2088
2117
|
{
|
|
2089
2118
|
"name": "passwordScheme",
|
|
2090
2119
|
"displayName": "Password Scheme",
|
|
@@ -2129,6 +2158,105 @@
|
|
|
2129
2158
|
}
|
|
2130
2159
|
]
|
|
2131
2160
|
},
|
|
2161
|
+
{
|
|
2162
|
+
"singularName": "userApiKey",
|
|
2163
|
+
"tableName": "ss_user_api_key",
|
|
2164
|
+
"pluralName": "userApiKeys",
|
|
2165
|
+
"displayName": "User API Key",
|
|
2166
|
+
"description": "API keys for programmatic server-to-server access",
|
|
2167
|
+
"dataSource": "default",
|
|
2168
|
+
"dataSourceType": "postgres",
|
|
2169
|
+
"userKeyFieldUserKey": "name",
|
|
2170
|
+
"isSystem": true,
|
|
2171
|
+
"fields": [
|
|
2172
|
+
{
|
|
2173
|
+
"name": "name",
|
|
2174
|
+
"displayName": "Name",
|
|
2175
|
+
"type": "shortText",
|
|
2176
|
+
"length": 512,
|
|
2177
|
+
"required": true,
|
|
2178
|
+
"unique": false,
|
|
2179
|
+
"index": false,
|
|
2180
|
+
"private": false,
|
|
2181
|
+
"encrypt": false,
|
|
2182
|
+
"isSystem": true
|
|
2183
|
+
},
|
|
2184
|
+
{
|
|
2185
|
+
"name": "hashedKey",
|
|
2186
|
+
"displayName": "Hashed Key",
|
|
2187
|
+
"type": "shortText",
|
|
2188
|
+
"length": 512,
|
|
2189
|
+
"required": true,
|
|
2190
|
+
"unique": true,
|
|
2191
|
+
"index": true,
|
|
2192
|
+
"private": true,
|
|
2193
|
+
"encrypt": false,
|
|
2194
|
+
"isSystem": true
|
|
2195
|
+
},
|
|
2196
|
+
{
|
|
2197
|
+
"name": "maskedKey",
|
|
2198
|
+
"displayName": "Masked Key",
|
|
2199
|
+
"type": "shortText",
|
|
2200
|
+
"length": 512,
|
|
2201
|
+
"required": true,
|
|
2202
|
+
"unique": false,
|
|
2203
|
+
"index": false,
|
|
2204
|
+
"private": false,
|
|
2205
|
+
"encrypt": false,
|
|
2206
|
+
"isSystem": true
|
|
2207
|
+
},
|
|
2208
|
+
{
|
|
2209
|
+
"name": "isActive",
|
|
2210
|
+
"displayName": "Is Active",
|
|
2211
|
+
"type": "boolean",
|
|
2212
|
+
"required": true,
|
|
2213
|
+
"unique": false,
|
|
2214
|
+
"index": false,
|
|
2215
|
+
"private": false,
|
|
2216
|
+
"encrypt": false,
|
|
2217
|
+
"defaultValue": "true",
|
|
2218
|
+
"isSystem": true
|
|
2219
|
+
},
|
|
2220
|
+
{
|
|
2221
|
+
"name": "expiresAt",
|
|
2222
|
+
"displayName": "Expires At",
|
|
2223
|
+
"type": "datetime",
|
|
2224
|
+
"required": false,
|
|
2225
|
+
"unique": false,
|
|
2226
|
+
"index": false,
|
|
2227
|
+
"private": false,
|
|
2228
|
+
"encrypt": false,
|
|
2229
|
+
"isSystem": true
|
|
2230
|
+
},
|
|
2231
|
+
{
|
|
2232
|
+
"name": "lastUsedAt",
|
|
2233
|
+
"displayName": "Last Used At",
|
|
2234
|
+
"type": "datetime",
|
|
2235
|
+
"required": false,
|
|
2236
|
+
"unique": false,
|
|
2237
|
+
"index": false,
|
|
2238
|
+
"private": false,
|
|
2239
|
+
"encrypt": false,
|
|
2240
|
+
"isSystem": true
|
|
2241
|
+
},
|
|
2242
|
+
{
|
|
2243
|
+
"name": "user",
|
|
2244
|
+
"displayName": "User",
|
|
2245
|
+
"type": "relation",
|
|
2246
|
+
"required": true,
|
|
2247
|
+
"unique": false,
|
|
2248
|
+
"index": false,
|
|
2249
|
+
"private": false,
|
|
2250
|
+
"encrypt": false,
|
|
2251
|
+
"relationType": "many-to-one",
|
|
2252
|
+
"relationCoModelSingularName": "user",
|
|
2253
|
+
"relationCoModelFieldName": "apiKeys",
|
|
2254
|
+
"relationCreateInverse": true,
|
|
2255
|
+
"relationModelModuleName": "solid-core",
|
|
2256
|
+
"isSystem": true
|
|
2257
|
+
}
|
|
2258
|
+
]
|
|
2259
|
+
},
|
|
2132
2260
|
{
|
|
2133
2261
|
"singularName": "userActivityHistory",
|
|
2134
2262
|
"tableName": "ss_user_activity_history",
|
|
@@ -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
|
+
}
|