@solidxai/core 0.1.11 → 0.1.12-beta.1
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/dist/constants/error-messages.d.ts +1 -0
- package/dist/constants/error-messages.d.ts.map +1 -1
- package/dist/constants/error-messages.js +1 -0
- package/dist/constants/error-messages.js.map +1 -1
- package/dist/controllers/authentication.controller.d.ts +1 -1
- package/dist/controllers/chatter-message.controller.d.ts +0 -2
- package/dist/controllers/chatter-message.controller.d.ts.map +1 -1
- package/dist/controllers/chatter-message.controller.js +0 -16
- package/dist/controllers/chatter-message.controller.js.map +1 -1
- package/dist/guards/access-token.guard.d.ts +5 -1
- package/dist/guards/access-token.guard.d.ts.map +1 -1
- package/dist/guards/access-token.guard.js +31 -3
- package/dist/guards/access-token.guard.js.map +1 -1
- package/dist/guards/authentication.guard.d.ts +1 -0
- package/dist/guards/authentication.guard.d.ts.map +1 -1
- package/dist/guards/authentication.guard.js +16 -1
- 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 +3 -1
- package/dist/helpers/bootstrap.helper.js.map +1 -1
- package/dist/helpers/solid-core-error-codes-provider.service.d.ts.map +1 -1
- package/dist/helpers/solid-core-error-codes-provider.service.js +28 -0
- package/dist/helpers/solid-core-error-codes-provider.service.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/interfaces/active-user-data.interface.d.ts +1 -0
- package/dist/interfaces/active-user-data.interface.d.ts.map +1 -1
- package/dist/interfaces/active-user-data.interface.js.map +1 -1
- package/dist/seeders/module-metadata-seeder.service.d.ts.map +1 -1
- package/dist/seeders/module-metadata-seeder.service.js +18 -1
- package/dist/seeders/module-metadata-seeder.service.js.map +1 -1
- package/dist/seeders/seed-data/solid-core-metadata.json +0 -1
- package/dist/services/active-session-storage.service.d.ts +10 -0
- package/dist/services/active-session-storage.service.d.ts.map +1 -0
- package/dist/services/active-session-storage.service.js +41 -0
- package/dist/services/active-session-storage.service.js.map +1 -0
- package/dist/services/authentication.service.d.ts +6 -3
- package/dist/services/authentication.service.d.ts.map +1 -1
- package/dist/services/authentication.service.js +29 -5
- package/dist/services/authentication.service.js.map +1 -1
- package/dist/services/chatter-message.service.d.ts +2 -0
- package/dist/services/chatter-message.service.d.ts.map +1 -1
- package/dist/services/chatter-message.service.js +25 -2
- package/dist/services/chatter-message.service.js.map +1 -1
- package/dist/services/crud.service.d.ts.map +1 -1
- package/dist/services/crud.service.js +1 -0
- package/dist/services/crud.service.js.map +1 -1
- package/dist/services/refresh-token-ids-storage.service.d.ts +6 -1
- package/dist/services/refresh-token-ids-storage.service.d.ts.map +1 -1
- package/dist/services/refresh-token-ids-storage.service.js +0 -2
- package/dist/services/refresh-token-ids-storage.service.js.map +1 -1
- package/dist/services/settings/default-settings-provider.service.d.ts +18 -0
- package/dist/services/settings/default-settings-provider.service.d.ts.map +1 -1
- package/dist/services/settings/default-settings-provider.service.js +10 -0
- package/dist/services/settings/default-settings-provider.service.js.map +1 -1
- package/dist/solid-core.module.d.ts.map +1 -1
- package/dist/solid-core.module.js +2 -0
- package/dist/solid-core.module.js.map +1 -1
- package/package.json +1 -1
- package/src/constants/error-messages.ts +1 -0
- package/src/controllers/chatter-message.controller.ts +7 -7
- package/src/guards/access-token.guard.ts +43 -3
- package/src/guards/authentication.guard.ts +22 -1
- package/src/helpers/bootstrap.helper.ts +9 -3
- package/src/helpers/solid-core-error-codes-provider.service.ts +29 -1
- package/src/index.ts +1 -0
- package/src/interfaces/active-user-data.interface.ts +6 -0
- package/src/seeders/module-metadata-seeder.service.ts +23 -2
- package/src/seeders/seed-data/solid-core-metadata.json +0 -1
- package/src/services/active-session-storage.service.ts +24 -0
- package/src/services/authentication.service.ts +30 -3
- package/src/services/chatter-message.service.ts +32 -2
- package/src/services/crud.service.ts +1 -0
- package/src/services/refresh-token-ids-storage.service.ts +9 -8
- package/src/services/settings/default-settings-provider.service.ts +10 -1
- package/src/solid-core.module.ts +2 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { CACHE_MANAGER } from "@nestjs/cache-manager";
|
|
2
|
+
import { Inject, Injectable } from "@nestjs/common";
|
|
3
|
+
import { Cache } from "cache-manager";
|
|
4
|
+
|
|
5
|
+
@Injectable()
|
|
6
|
+
export class ActiveSessionStorageService {
|
|
7
|
+
constructor(@Inject(CACHE_MANAGER) private readonly cacheManager: Cache) {}
|
|
8
|
+
|
|
9
|
+
async setActiveSession(userId: number, sessionId: string): Promise<void> {
|
|
10
|
+
await this.cacheManager.set(this.getKey(userId), sessionId);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async getActiveSession(userId: number): Promise<string | undefined> {
|
|
14
|
+
return this.cacheManager.get(this.getKey(userId));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async clearActiveSession(userId: number): Promise<void> {
|
|
18
|
+
await this.cacheManager.del(this.getKey(userId));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private getKey(userId: number): string {
|
|
22
|
+
return `active-session-${userId}`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -39,6 +39,7 @@ import { SignUpDto } from "../dtos/sign-up.dto";
|
|
|
39
39
|
import { User } from "../entities/user.entity";
|
|
40
40
|
import { EventDetails, EventType } from "../interfaces";
|
|
41
41
|
import { ActiveUserData } from "../interfaces/active-user-data.interface";
|
|
42
|
+
import { ActiveSessionStorageService } from "./active-session-storage.service";
|
|
42
43
|
import { HashingService } from "./hashing.service";
|
|
43
44
|
import {
|
|
44
45
|
InvalidatedRefreshTokenError,
|
|
@@ -74,6 +75,7 @@ export class AuthenticationService {
|
|
|
74
75
|
private readonly userRepository: UserRepository,
|
|
75
76
|
private readonly hashingService: HashingService,
|
|
76
77
|
private readonly jwtService: JwtService,
|
|
78
|
+
private readonly activeSessionStorage: ActiveSessionStorageService,
|
|
77
79
|
private readonly refreshTokenIdsStorage: RefreshTokenIdsStorageService,
|
|
78
80
|
private readonly httpService: HttpService,
|
|
79
81
|
// private readonly mailService: SMTPEMailService,
|
|
@@ -1711,8 +1713,16 @@ export class AuthenticationService {
|
|
|
1711
1713
|
}
|
|
1712
1714
|
|
|
1713
1715
|
async generateTokens(user: User) {
|
|
1716
|
+
const sessionId = this.shouldPreventConcurrentLogins()
|
|
1717
|
+
? randomUUID()
|
|
1718
|
+
: undefined;
|
|
1719
|
+
if (sessionId) {
|
|
1720
|
+
await this.activeSessionStorage.setActiveSession(user.id, sessionId);
|
|
1721
|
+
} else {
|
|
1722
|
+
await this.activeSessionStorage.clearActiveSession(user.id);
|
|
1723
|
+
}
|
|
1714
1724
|
const [accessToken, refreshToken] = await Promise.all([
|
|
1715
|
-
await this.generateAccessToken(user),
|
|
1725
|
+
await this.generateAccessToken(user, sessionId),
|
|
1716
1726
|
await this.generateRefreshToken(user),
|
|
1717
1727
|
]);
|
|
1718
1728
|
|
|
@@ -1722,16 +1732,24 @@ export class AuthenticationService {
|
|
|
1722
1732
|
};
|
|
1723
1733
|
}
|
|
1724
1734
|
|
|
1725
|
-
async generateAccessToken(user: User) {
|
|
1735
|
+
async generateAccessToken(user: User, sessionId?: string) {
|
|
1726
1736
|
// const userRoleNames = user.roles.map((role) => role.name).join(';')
|
|
1727
1737
|
const userRoleNames = user.roles.map((role) => role.name);
|
|
1738
|
+
const resolvedSessionId = this.shouldPreventConcurrentLogins()
|
|
1739
|
+
? sessionId ?? (await this.activeSessionStorage.getActiveSession(user.id))
|
|
1740
|
+
: undefined;
|
|
1728
1741
|
|
|
1729
1742
|
const accessTokenTtl =
|
|
1730
1743
|
this.settingService.getConfigValue<SolidCoreSetting>("accessTokenTtl");
|
|
1731
1744
|
const accessToken = await this.signToken<Partial<ActiveUserData>>(
|
|
1732
1745
|
user.id,
|
|
1733
1746
|
accessTokenTtl,
|
|
1734
|
-
{
|
|
1747
|
+
{
|
|
1748
|
+
username: user.username,
|
|
1749
|
+
email: user.email,
|
|
1750
|
+
roles: userRoleNames,
|
|
1751
|
+
...(resolvedSessionId ? { sessionId: resolvedSessionId } : {}),
|
|
1752
|
+
},
|
|
1735
1753
|
);
|
|
1736
1754
|
|
|
1737
1755
|
return accessToken;
|
|
@@ -2150,6 +2168,14 @@ export class AuthenticationService {
|
|
|
2150
2168
|
);
|
|
2151
2169
|
}
|
|
2152
2170
|
|
|
2171
|
+
private shouldPreventConcurrentLogins(): boolean {
|
|
2172
|
+
return (
|
|
2173
|
+
this.settingService.getConfigValue<SolidCoreSetting>(
|
|
2174
|
+
"preventConcurrentLogins",
|
|
2175
|
+
) === true
|
|
2176
|
+
);
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2153
2179
|
private checkAccountBlocked(user: User): void {
|
|
2154
2180
|
const maxFailedAttempts =
|
|
2155
2181
|
this.settingService.getConfigValue<SolidCoreSetting>(
|
|
@@ -2206,6 +2232,7 @@ export class AuthenticationService {
|
|
|
2206
2232
|
|
|
2207
2233
|
const userId = payload.sub;
|
|
2208
2234
|
await this.refreshTokenIdsStorage.invalidate(userId);
|
|
2235
|
+
await this.activeSessionStorage.clearActiveSession(userId);
|
|
2209
2236
|
const user = await this.userRepository.findOne({
|
|
2210
2237
|
where: {
|
|
2211
2238
|
id: userId,
|
|
@@ -23,6 +23,7 @@ import { ChatterMessage } from '../entities/chatter-message.entity';
|
|
|
23
23
|
import { getMediaStorageProvider } from './mediaStorageProviders';
|
|
24
24
|
import { RequestContextService } from './request-context.service';
|
|
25
25
|
import { Logger } from '@nestjs/common';
|
|
26
|
+
import { SolidIntrospectService } from './solid-introspect.service';
|
|
26
27
|
|
|
27
28
|
@Injectable()
|
|
28
29
|
export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
@@ -50,6 +51,26 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
50
51
|
super(entityManager, repo, 'chatterMessage', 'solid-core', moduleRef);
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
private getCoModelService(coModelName: string): CRUDService<any> {
|
|
55
|
+
const introspectService = this.moduleRef.get(SolidIntrospectService, { strict: false });
|
|
56
|
+
const modelSingularName = lowerFirst(coModelName);
|
|
57
|
+
const coModelService = introspectService?.getCRUDService(modelSingularName);
|
|
58
|
+
if (!coModelService) {
|
|
59
|
+
throw new BadRequestException(ERROR_MESSAGES.MODEL_SERVICE_NOT_FOUND(modelSingularName));
|
|
60
|
+
}
|
|
61
|
+
return coModelService;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private async assertRecordAccess(coModelName: string, coModelEntityId: number) {
|
|
65
|
+
const activeUser = this.requestContextService.getActiveUser();
|
|
66
|
+
if (!activeUser) {
|
|
67
|
+
throw new ForbiddenException(ERROR_MESSAGES.FORBIDDEN);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const coModelService = this.getCoModelService(coModelName);
|
|
71
|
+
await coModelService.findOne(coModelEntityId, {}, { activeUser });
|
|
72
|
+
}
|
|
73
|
+
|
|
53
74
|
private resolveMessageUserId(userId?: number | null): number | null {
|
|
54
75
|
if (userId) {
|
|
55
76
|
return userId;
|
|
@@ -96,6 +117,8 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
96
117
|
throw new NotFoundException(`Entity [solid-core.chatterMessage] with id ${id} not found`);
|
|
97
118
|
}
|
|
98
119
|
|
|
120
|
+
await this.assertRecordAccess(message.coModelName, message.coModelEntityId);
|
|
121
|
+
|
|
99
122
|
message.status = CHATTER_MESSAGE_STATUS.COMPLETED;
|
|
100
123
|
return this.repo.save(message);
|
|
101
124
|
}
|
|
@@ -111,6 +134,8 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
111
134
|
throw new NotFoundException(`Entity [solid-core.chatterMessage] with id ${id} not found`);
|
|
112
135
|
}
|
|
113
136
|
|
|
137
|
+
await this.assertRecordAccess(message.coModelName, message.coModelEntityId);
|
|
138
|
+
|
|
114
139
|
if (!this.isEditableCustomNoteMessage(message)) {
|
|
115
140
|
throw new BadRequestException('Only custom note messages can be edited.');
|
|
116
141
|
}
|
|
@@ -193,17 +218,20 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
193
218
|
}
|
|
194
219
|
|
|
195
220
|
async postMessage(postDto: PostChatterMessageDto, files: Express.Multer.File[] = []) {
|
|
221
|
+
const coModelName = lowerFirst(postDto.coModelName);
|
|
222
|
+
await this.assertRecordAccess(coModelName, postDto.coModelEntityId);
|
|
223
|
+
|
|
196
224
|
const chatterMessage = new ChatterMessage();
|
|
197
225
|
chatterMessage.messageType = CHATTER_MESSAGE_TYPE.CUSTOM;
|
|
198
226
|
chatterMessage.messageSubType = postDto.messageSubType || CHATTER_MESSAGE_SUBTYPE.CUSTOM;
|
|
199
227
|
chatterMessage.status = postDto.status ?? CHATTER_MESSAGE_STATUS.PENDING;
|
|
200
228
|
chatterMessage.messageBody = postDto.messageBody;
|
|
201
229
|
chatterMessage.coModelEntityId = postDto.coModelEntityId;
|
|
202
|
-
chatterMessage.coModelName =
|
|
230
|
+
chatterMessage.coModelName = coModelName;
|
|
203
231
|
chatterMessage.modelUserKey = postDto.modelUserKey ?? null;
|
|
204
232
|
|
|
205
233
|
const model = await this.modelMetadataRepo.findOne({
|
|
206
|
-
where: { singularName:
|
|
234
|
+
where: { singularName: coModelName },
|
|
207
235
|
relations: { userKeyField: true }
|
|
208
236
|
});
|
|
209
237
|
chatterMessage.modelDisplayName = model?.displayName ?? null;
|
|
@@ -683,6 +711,8 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
683
711
|
const { limit = 25, offset = 0, populate = [], populateMedia = [], filters } = query;
|
|
684
712
|
this.logHeapUsed('getChatterMessages-start');
|
|
685
713
|
|
|
714
|
+
await this.assertRecordAccess(lowerFirst(entityName), entityId);
|
|
715
|
+
|
|
686
716
|
const model = await this.modelMetadataRepo.findOne({
|
|
687
717
|
where: {
|
|
688
718
|
singularName: entityName
|
|
@@ -232,6 +232,7 @@ export class CRUDService<T extends CommonEntity> { // Add two generic value i.e
|
|
|
232
232
|
// 5. Save the entity
|
|
233
233
|
// For media, we need to use a storage provider and save the media, then save the associated uri against the entity or media table
|
|
234
234
|
const mergedEntity = this.repo.merge(entity, updateDto);
|
|
235
|
+
delete mergedEntity.updatedAt;
|
|
235
236
|
const savedEntity = await this.repo.save(mergedEntity) as T;
|
|
236
237
|
|
|
237
238
|
//FIXME: Skip the many-to-many, and instead fire differential updates and avoid loading the entire association graph for the ids
|
|
@@ -6,6 +6,11 @@ import { AuthenticationService } from './authentication.service';
|
|
|
6
6
|
// TODO: Ideally this should be in a separate file - putting this here for brevity
|
|
7
7
|
export class InvalidatedRefreshTokenError extends Error { }
|
|
8
8
|
|
|
9
|
+
type RefreshTokenState = {
|
|
10
|
+
currentRefreshToken: string;
|
|
11
|
+
previousRefreshToken: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
9
14
|
@Injectable()
|
|
10
15
|
// export class RefreshTokenIdsStorageService implements OnApplicationBootstrap, OnApplicationShutdown {
|
|
11
16
|
export class RefreshTokenIdsStorageService {
|
|
@@ -29,10 +34,7 @@ export class RefreshTokenIdsStorageService {
|
|
|
29
34
|
) { }
|
|
30
35
|
|
|
31
36
|
async insert(userId: number, refreshToken: string, previousRefreshToken?: string): Promise<void> {
|
|
32
|
-
|
|
33
|
-
// Save a refresh token object with the shape: { currentRefreshToken: string, previousRefreshToken: string }
|
|
34
|
-
const existing = (await this.cacheManager.get(this.getKey(userId))) as { currentRefreshToken?: string, previousRefreshToken?: string } | undefined;
|
|
35
|
-
const refreshTokenState = {
|
|
37
|
+
const refreshTokenState: RefreshTokenState = {
|
|
36
38
|
currentRefreshToken: refreshToken,
|
|
37
39
|
previousRefreshToken: previousRefreshToken ?? "",
|
|
38
40
|
};
|
|
@@ -58,8 +60,7 @@ export class RefreshTokenIdsStorageService {
|
|
|
58
60
|
|
|
59
61
|
// TODO: Assume you get this shape out of the cache {"currentRefreshToken": "", "previousRefreshToken": ""}
|
|
60
62
|
// Then you will compare against the currentRefreshToken.
|
|
61
|
-
const refreshTokenState = await this.cacheManager.get(this.getKey(user.id));
|
|
62
|
-
console.log("refreshTokenState", refreshTokenState);
|
|
63
|
+
const refreshTokenState = await this.cacheManager.get(this.getKey(user.id)) as RefreshTokenState | undefined;
|
|
63
64
|
|
|
64
65
|
// Use the authentication service to generate a new refresh token, set it in the currentRefreshToken in scenario 1 and return.
|
|
65
66
|
|
|
@@ -108,7 +109,7 @@ export class RefreshTokenIdsStorageService {
|
|
|
108
109
|
valid = true;
|
|
109
110
|
// Do not modify cache
|
|
110
111
|
// Generate new refresh token based on currentRefreshToken
|
|
111
|
-
const existingRefreshTokenState = (await this.cacheManager.get(this.getKey(user.id))) as
|
|
112
|
+
const existingRefreshTokenState = (await this.cacheManager.get(this.getKey(user.id))) as RefreshTokenState | undefined;
|
|
112
113
|
newRefreshToken = existingRefreshTokenState?.currentRefreshToken;
|
|
113
114
|
}
|
|
114
115
|
}
|
|
@@ -126,7 +127,7 @@ export class RefreshTokenIdsStorageService {
|
|
|
126
127
|
return `user-${userId}`;
|
|
127
128
|
}
|
|
128
129
|
|
|
129
|
-
getCurrentRefreshTokenState(userId: number): Promise<
|
|
130
|
+
getCurrentRefreshTokenState(userId: number): Promise<RefreshTokenState | undefined> {
|
|
130
131
|
return this.cacheManager.get(this.getKey(userId));
|
|
131
132
|
}
|
|
132
133
|
}
|
|
@@ -1201,7 +1201,16 @@ const getSolidCoreSettings = (isProd: boolean) =>
|
|
|
1201
1201
|
value: parseInt(process.env.IAM_JWT_REFRESH_TOKEN_TTL ?? "604800", 10),
|
|
1202
1202
|
level: SettingLevel.SystemEnv,
|
|
1203
1203
|
},
|
|
1204
|
-
|
|
1204
|
+
{
|
|
1205
|
+
moduleName: "solid-core",
|
|
1206
|
+
key: "preventConcurrentLogins",
|
|
1207
|
+
value: false,
|
|
1208
|
+
level: SettingLevel.SystemAdminEditable,
|
|
1209
|
+
label: "Prevent Concurrent Logins",
|
|
1210
|
+
group: "authentication-settings",
|
|
1211
|
+
sortOrder: 200,
|
|
1212
|
+
controlType: "boolean",
|
|
1213
|
+
},
|
|
1205
1214
|
// queues-settings-provider.service.ts
|
|
1206
1215
|
{
|
|
1207
1216
|
moduleName: "solid-core",
|
package/src/solid-core.module.ts
CHANGED
|
@@ -153,6 +153,7 @@ import { TwilioSmsQueueSubscriberRedis } from "./jobs/redis/twilio-sms-subscribe
|
|
|
153
153
|
import { UserRegistrationListener } from "./listeners/user-registration.listener";
|
|
154
154
|
import { GoogleOauthStrategy } from "./passport-strategies/google-oauth.strategy";
|
|
155
155
|
import { ApiKeyService } from "./services/api-key.service";
|
|
156
|
+
import { ActiveSessionStorageService } from "./services/active-session-storage.service";
|
|
156
157
|
import { AuthenticationService } from "./services/authentication.service";
|
|
157
158
|
import { BcryptService } from "./services/bcrypt.service";
|
|
158
159
|
import { UuidExternalIdEntityComputedFieldProvider } from "./services/computed-fields/entity/uuid-externalid-entity-computed-field-provider.service";
|
|
@@ -647,6 +648,7 @@ import { PostgresDatasourceIntrospectionProviderService } from "./services/datas
|
|
|
647
648
|
AccessTokenGuard,
|
|
648
649
|
ApiKeyGuard,
|
|
649
650
|
ApiKeyService,
|
|
651
|
+
ActiveSessionStorageService,
|
|
650
652
|
AuthenticationService,
|
|
651
653
|
GoogleAuthenticationController,
|
|
652
654
|
RefreshTokenIdsStorageService,
|