@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.
Files changed (78) hide show
  1. package/dist/constants/error-messages.d.ts +1 -0
  2. package/dist/constants/error-messages.d.ts.map +1 -1
  3. package/dist/constants/error-messages.js +1 -0
  4. package/dist/constants/error-messages.js.map +1 -1
  5. package/dist/controllers/authentication.controller.d.ts +1 -1
  6. package/dist/controllers/chatter-message.controller.d.ts +0 -2
  7. package/dist/controllers/chatter-message.controller.d.ts.map +1 -1
  8. package/dist/controllers/chatter-message.controller.js +0 -16
  9. package/dist/controllers/chatter-message.controller.js.map +1 -1
  10. package/dist/guards/access-token.guard.d.ts +5 -1
  11. package/dist/guards/access-token.guard.d.ts.map +1 -1
  12. package/dist/guards/access-token.guard.js +31 -3
  13. package/dist/guards/access-token.guard.js.map +1 -1
  14. package/dist/guards/authentication.guard.d.ts +1 -0
  15. package/dist/guards/authentication.guard.d.ts.map +1 -1
  16. package/dist/guards/authentication.guard.js +16 -1
  17. package/dist/guards/authentication.guard.js.map +1 -1
  18. package/dist/helpers/bootstrap.helper.d.ts.map +1 -1
  19. package/dist/helpers/bootstrap.helper.js +3 -1
  20. package/dist/helpers/bootstrap.helper.js.map +1 -1
  21. package/dist/helpers/solid-core-error-codes-provider.service.d.ts.map +1 -1
  22. package/dist/helpers/solid-core-error-codes-provider.service.js +28 -0
  23. package/dist/helpers/solid-core-error-codes-provider.service.js.map +1 -1
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +1 -0
  27. package/dist/index.js.map +1 -1
  28. package/dist/interfaces/active-user-data.interface.d.ts +1 -0
  29. package/dist/interfaces/active-user-data.interface.d.ts.map +1 -1
  30. package/dist/interfaces/active-user-data.interface.js.map +1 -1
  31. package/dist/seeders/module-metadata-seeder.service.d.ts.map +1 -1
  32. package/dist/seeders/module-metadata-seeder.service.js +18 -1
  33. package/dist/seeders/module-metadata-seeder.service.js.map +1 -1
  34. package/dist/seeders/seed-data/solid-core-metadata.json +0 -1
  35. package/dist/services/active-session-storage.service.d.ts +10 -0
  36. package/dist/services/active-session-storage.service.d.ts.map +1 -0
  37. package/dist/services/active-session-storage.service.js +41 -0
  38. package/dist/services/active-session-storage.service.js.map +1 -0
  39. package/dist/services/authentication.service.d.ts +6 -3
  40. package/dist/services/authentication.service.d.ts.map +1 -1
  41. package/dist/services/authentication.service.js +29 -5
  42. package/dist/services/authentication.service.js.map +1 -1
  43. package/dist/services/chatter-message.service.d.ts +2 -0
  44. package/dist/services/chatter-message.service.d.ts.map +1 -1
  45. package/dist/services/chatter-message.service.js +25 -2
  46. package/dist/services/chatter-message.service.js.map +1 -1
  47. package/dist/services/crud.service.d.ts.map +1 -1
  48. package/dist/services/crud.service.js +1 -0
  49. package/dist/services/crud.service.js.map +1 -1
  50. package/dist/services/refresh-token-ids-storage.service.d.ts +6 -1
  51. package/dist/services/refresh-token-ids-storage.service.d.ts.map +1 -1
  52. package/dist/services/refresh-token-ids-storage.service.js +0 -2
  53. package/dist/services/refresh-token-ids-storage.service.js.map +1 -1
  54. package/dist/services/settings/default-settings-provider.service.d.ts +18 -0
  55. package/dist/services/settings/default-settings-provider.service.d.ts.map +1 -1
  56. package/dist/services/settings/default-settings-provider.service.js +10 -0
  57. package/dist/services/settings/default-settings-provider.service.js.map +1 -1
  58. package/dist/solid-core.module.d.ts.map +1 -1
  59. package/dist/solid-core.module.js +2 -0
  60. package/dist/solid-core.module.js.map +1 -1
  61. package/package.json +1 -1
  62. package/src/constants/error-messages.ts +1 -0
  63. package/src/controllers/chatter-message.controller.ts +7 -7
  64. package/src/guards/access-token.guard.ts +43 -3
  65. package/src/guards/authentication.guard.ts +22 -1
  66. package/src/helpers/bootstrap.helper.ts +9 -3
  67. package/src/helpers/solid-core-error-codes-provider.service.ts +29 -1
  68. package/src/index.ts +1 -0
  69. package/src/interfaces/active-user-data.interface.ts +6 -0
  70. package/src/seeders/module-metadata-seeder.service.ts +23 -2
  71. package/src/seeders/seed-data/solid-core-metadata.json +0 -1
  72. package/src/services/active-session-storage.service.ts +24 -0
  73. package/src/services/authentication.service.ts +30 -3
  74. package/src/services/chatter-message.service.ts +32 -2
  75. package/src/services/crud.service.ts +1 -0
  76. package/src/services/refresh-token-ids-storage.service.ts +9 -8
  77. package/src/services/settings/default-settings-provider.service.ts +10 -1
  78. 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
- { username: user.username, email: user.email, roles: userRoleNames },
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 = postDto.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: lowerFirst(postDto.coModelName) },
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
- // TODO: save a refresh token object with this shape {"currentRefreshToken": "", "previousRefreshToken": ""}
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 { currentRefreshToken?: string, previousRefreshToken?: string } | undefined;
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<any> {
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",
@@ -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,