@loomcore/api 0.1.35 → 0.1.37

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 (28) hide show
  1. package/dist/__tests__/common-test.utils.d.ts +1 -1
  2. package/dist/__tests__/common-test.utils.js +5 -2
  3. package/dist/__tests__/test-objects.d.ts +3 -1
  4. package/dist/__tests__/test-objects.js +20 -30
  5. package/dist/config/base-api-config.js +3 -4
  6. package/dist/controllers/authorizations.controller.d.ts +1 -1
  7. package/dist/controllers/authorizations.controller.js +1 -1
  8. package/dist/databases/mongo-db/commands/mongo-batch-update.command.js +20 -7
  9. package/dist/databases/postgres/migrations/005-create-meta-org.migration.js +2 -2
  10. package/dist/databases/postgres/migrations/006-create-admin-user.migration.js +2 -3
  11. package/dist/databases/postgres/postgres.database.d.ts +2 -2
  12. package/dist/databases/postgres/postgres.database.js +10 -30
  13. package/dist/models/authorization.model.d.ts +16 -0
  14. package/dist/models/authorization.model.js +11 -0
  15. package/dist/services/auth.service.d.ts +1 -1
  16. package/dist/services/auth.service.js +25 -17
  17. package/dist/services/index.d.ts +1 -1
  18. package/dist/services/index.js +1 -1
  19. package/dist/services/multi-tenant-api.service.js +3 -3
  20. package/dist/services/organization.service.d.ts +1 -1
  21. package/dist/services/organization.service.js +2 -2
  22. package/dist/services/password-reset-token.service.d.ts +1 -1
  23. package/dist/services/tenant-query-decorator.js +8 -8
  24. package/dist/services/{user-service/user.service.d.ts → user.service.d.ts} +2 -3
  25. package/dist/services/{user-service/user.service.js → user.service.js} +5 -26
  26. package/dist/services/utils/getUserContextAuthorizations.util.d.ts +3 -0
  27. package/dist/services/utils/getUserContextAuthorizations.util.js +9 -0
  28. package/package.json +2 -2
@@ -1,5 +1,5 @@
1
1
  import { Application } from 'express';
2
- import { IUser, IUserContext, IQueryOptions } from '@loomcore/common/models';
2
+ import { IUserContext, IQueryOptions, IUser } from '@loomcore/common/models';
3
3
  import { ApiController } from '../controllers/api.controller.js';
4
4
  import { MultiTenantApiService } from '../services/multi-tenant-api.service.js';
5
5
  import { Operation } from '../databases/operations/operation.js';
@@ -9,11 +9,12 @@ import { OrganizationService } from '../services/organization.service.js';
9
9
  import { AuthService, GenericApiService } from '../services/index.js';
10
10
  import { ObjectId } from 'mongodb';
11
11
  import * as testObjectsModule from './test-objects.js';
12
- const { getTestMetaOrg, getTestOrg, getTestMetaOrgUser, getTestMetaOrgUserContext, getTestOrgUser, getTestOrgUserContext, setTestOrgId, setTestMetaOrgId } = testObjectsModule;
12
+ const { getTestMetaOrg, getTestOrg, getTestMetaOrgUser, getTestMetaOrgUserContext, getTestOrgUserContext, setTestOrgId, setTestMetaOrgId } = testObjectsModule;
13
13
  import { CategorySpec } from './models/category.model.js';
14
14
  import { ProductSpec } from './models/product.model.js';
15
15
  import { setBaseApiConfig } from '../config/index.js';
16
16
  import { entityUtils } from '@loomcore/common/utils';
17
+ import { getTestOrgUser } from './test-objects.js';
17
18
  let deviceIdCookie;
18
19
  let authService;
19
20
  let organizationService;
@@ -123,7 +124,7 @@ async function simulateloginWithTestUser() {
123
124
  return res;
124
125
  }
125
126
  };
126
- const loginResponse = await authService.attemptLogin(req, res, getTestMetaOrgUser().email, getTestMetaOrgUser().password);
127
+ const loginResponse = await authService.attemptLogin(req, res, getTestMetaOrgUser().email, testObjectsModule.TEST_META_ORG_USER_PASSWORD);
127
128
  if (!loginResponse?.tokens?.accessToken) {
128
129
  throw new Error('Failed to login with test user');
129
130
  }
@@ -131,11 +132,13 @@ async function simulateloginWithTestUser() {
131
132
  }
132
133
  function getAuthToken() {
133
134
  const metaOrgUser = getTestMetaOrgUser();
135
+ const metaOrg = getTestMetaOrg();
134
136
  const payload = {
135
137
  user: {
136
138
  _id: metaOrgUser._id,
137
139
  email: metaOrgUser.email
138
140
  },
141
+ organization: metaOrg,
139
142
  _orgId: metaOrgUser._orgId
140
143
  };
141
144
  const token = JwtService.sign(payload, JWT_SECRET, { expiresIn: 3600 });
@@ -1,10 +1,12 @@
1
- import { IOrganization, IUser, IUserContext } from "@loomcore/common/models";
1
+ import { IOrganization, IUserContext, IUser } from "@loomcore/common/models";
2
2
  export declare let TEST_META_ORG_ID: string;
3
3
  export declare function setTestMetaOrgId(metaOrgId: string): void;
4
+ export declare const TEST_META_ORG_USER_PASSWORD = "test-meta-org-user-password";
4
5
  export declare function getTestMetaOrg(): IOrganization;
5
6
  export declare function getTestMetaOrgUser(): IUser;
6
7
  export declare function getTestMetaOrgUserContext(): IUserContext;
7
8
  export declare function setTestOrgId(orgId: string): void;
8
9
  export declare function getTestOrg(): IOrganization;
10
+ export declare const TEST_ORG_USER_PASSWORD = "test-org-user-password";
9
11
  export declare function getTestOrgUser(): IUser;
10
12
  export declare function getTestOrgUserContext(): IUserContext;
@@ -2,6 +2,7 @@ export let TEST_META_ORG_ID = '69261691f936c45f85da24d0';
2
2
  export function setTestMetaOrgId(metaOrgId) {
3
3
  TEST_META_ORG_ID = metaOrgId;
4
4
  }
5
+ export const TEST_META_ORG_USER_PASSWORD = 'test-meta-org-user-password';
5
6
  export function getTestMetaOrg() {
6
7
  return {
7
8
  _id: TEST_META_ORG_ID,
@@ -21,33 +22,27 @@ export function getTestMetaOrgUser() {
21
22
  _id: '69261672f48fb7bf76e54dfb',
22
23
  _orgId: getTestMetaOrg()._id,
23
24
  email: 'test@example.com',
24
- password: 'testpassword',
25
25
  firstName: 'Test',
26
26
  lastName: 'User',
27
27
  displayName: 'Test User',
28
- authorizations: [{
29
- _id: '6939c54e57a1c6576a40c590',
30
- _orgId: getTestMetaOrg()._id,
31
- role: 'metaorgUser',
32
- feature: 'metaorgUser',
33
- config: {},
34
- _created: new Date(),
35
- _createdBy: 'system',
36
- _updated: new Date(),
37
- _updatedBy: 'system',
38
- }],
28
+ password: TEST_META_ORG_USER_PASSWORD,
39
29
  _created: new Date(),
40
30
  _createdBy: 'system',
41
- _lastLoggedIn: new Date(),
42
31
  _updated: new Date(),
43
32
  _updatedBy: 'system',
44
33
  };
45
34
  }
46
- ;
47
35
  export function getTestMetaOrgUserContext() {
48
36
  return {
49
37
  user: getTestMetaOrgUser(),
50
- _orgId: getTestMetaOrg()._id,
38
+ organization: getTestMetaOrg(),
39
+ authorizations: [{
40
+ _id: '6939c54e57a1c6576a40c590',
41
+ _orgId: getTestMetaOrg()._id,
42
+ role: 'metaorgUser',
43
+ feature: 'metaorgUser',
44
+ config: {},
45
+ }],
51
46
  };
52
47
  }
53
48
  ;
@@ -69,38 +64,33 @@ export function getTestOrg() {
69
64
  };
70
65
  }
71
66
  ;
67
+ export const TEST_ORG_USER_PASSWORD = 'test-org-user-password';
72
68
  export function getTestOrgUser() {
73
69
  return {
74
70
  _id: '6926167d06c0073a778a1250',
75
71
  _orgId: getTestOrg()._id,
76
72
  email: 'test-org-user@example.com',
77
- password: 'testpassword',
78
73
  firstName: 'Test',
79
74
  lastName: 'User',
80
75
  displayName: 'Test User',
81
- authorizations: [{
82
- _id: '6939c54e57a1c6576a40c591',
83
- _orgId: getTestOrg()._id,
84
- role: 'testOrgUser',
85
- feature: 'testOrgUser',
86
- config: {},
87
- _created: new Date(),
88
- _createdBy: 'system',
89
- _updated: new Date(),
90
- _updatedBy: 'system',
91
- }],
76
+ password: TEST_ORG_USER_PASSWORD,
92
77
  _created: new Date(),
93
78
  _createdBy: 'system',
94
- _lastLoggedIn: new Date(),
95
79
  _updated: new Date(),
96
80
  _updatedBy: 'system',
97
81
  };
98
82
  }
99
- ;
100
83
  export function getTestOrgUserContext() {
101
84
  return {
102
85
  user: getTestOrgUser(),
103
- _orgId: getTestOrg()._id,
86
+ organization: getTestOrg(),
87
+ authorizations: [{
88
+ _id: '6939c54e57a1c6576a40c591',
89
+ _orgId: getTestOrg()._id,
90
+ role: 'testOrgUser',
91
+ feature: 'testOrgUser',
92
+ config: {},
93
+ }],
104
94
  };
105
95
  }
106
96
  ;
@@ -24,17 +24,16 @@ export async function initSystemUserContext(database) {
24
24
  }
25
25
  if (!isSystemUserContextSet) {
26
26
  const systemEmail = config.email?.systemEmailAddress || 'system@example.com';
27
- let metaOrgId = undefined;
27
+ let metaOrg = undefined;
28
28
  if (config.app.isMultiTenant) {
29
29
  const { OrganizationService } = await import('../services/organization.service.js');
30
30
  const organizationService = new OrganizationService(database);
31
- const metaOrg = await organizationService.getMetaOrg(EmptyUserContext);
31
+ metaOrg = await organizationService.getMetaOrg(EmptyUserContext);
32
32
  if (!metaOrg) {
33
33
  throw new Error('Meta organization not found. Please create an organization with isMetaOrg=true before starting the API.');
34
34
  }
35
- metaOrgId = metaOrg._id;
36
35
  }
37
- initializeSystemUserContext(systemEmail, metaOrgId);
36
+ initializeSystemUserContext(systemEmail, metaOrg);
38
37
  isSystemUserContextSet = true;
39
38
  }
40
39
  else if (config.env !== 'test') {
@@ -1,4 +1,4 @@
1
- import { IAuthorization } from "@loomcore/common/models";
1
+ import { IAuthorization } from "../models/authorization.model.js";
2
2
  import { ApiController } from "./api.controller.js";
3
3
  import { Application } from "express";
4
4
  import { IDatabase } from "../databases/models/index.js";
@@ -1,4 +1,4 @@
1
- import { AuthorizationModelSpec } from "@loomcore/common/models";
1
+ import { AuthorizationModelSpec } from "../models/authorization.model.js";
2
2
  import { ApiController } from "./api.controller.js";
3
3
  import { MultiTenantApiService } from "../services/multi-tenant-api.service.js";
4
4
  export class AuthorizationsController extends ApiController {
@@ -1,6 +1,8 @@
1
1
  import { ObjectId } from "mongodb";
2
2
  import { BadRequestError } from "../../../errors/index.js";
3
3
  import { convertOperationsToPipeline } from "../utils/index.js";
4
+ import { buildNoSqlMatch } from "../utils/build-no-sql-match.util.js";
5
+ import NoSqlPipeline from "../models/no-sql-pipeline.js";
4
6
  export async function batchUpdate(db, entities, operations, queryObject, pluralResourceName) {
5
7
  const collection = db.collection(pluralResourceName);
6
8
  if (!entities || entities.length === 0) {
@@ -8,15 +10,18 @@ export async function batchUpdate(db, entities, operations, queryObject, pluralR
8
10
  }
9
11
  const bulkOperations = [];
10
12
  const entityIds = [];
13
+ const queryObjectMatch = buildNoSqlMatch(queryObject);
14
+ const baseFilter = queryObjectMatch.$match || {};
11
15
  for (const entity of entities) {
12
16
  const { _id, ...updateData } = entity;
13
17
  if (!_id || !(_id instanceof ObjectId)) {
14
18
  throw new BadRequestError('Each entity in a batch update must have a valid _id that has been converted to an ObjectId.');
15
19
  }
16
20
  entityIds.push(_id);
21
+ const updateFilter = { ...baseFilter, _id };
17
22
  bulkOperations.push({
18
23
  updateOne: {
19
- filter: { _id },
24
+ filter: updateFilter,
20
25
  update: { $set: updateData },
21
26
  },
22
27
  });
@@ -24,18 +29,26 @@ export async function batchUpdate(db, entities, operations, queryObject, pluralR
24
29
  if (bulkOperations.length > 0) {
25
30
  await collection.bulkWrite(bulkOperations);
26
31
  }
27
- const baseQuery = { _id: { $in: entityIds } };
32
+ const retrievalQueryObject = {
33
+ ...queryObject,
34
+ filters: {
35
+ ...(queryObject.filters || {}),
36
+ _id: { in: entityIds.map(id => id.toString()) }
37
+ }
38
+ };
28
39
  const operationsDocuments = convertOperationsToPipeline(operations);
29
40
  let updatedEntities;
30
41
  if (operationsDocuments.length > 0) {
31
- const pipeline = [
32
- { $match: baseQuery },
33
- ...operationsDocuments
34
- ];
42
+ const pipeline = new NoSqlPipeline()
43
+ .addMatch(retrievalQueryObject)
44
+ .addOperations(operations)
45
+ .build();
35
46
  updatedEntities = await collection.aggregate(pipeline).toArray();
36
47
  }
37
48
  else {
38
- updatedEntities = await collection.find(baseQuery).toArray();
49
+ const retrievalMatch = buildNoSqlMatch(retrievalQueryObject);
50
+ const retrievalFilter = retrievalMatch.$match || {};
51
+ updatedEntities = await collection.find(retrievalFilter).toArray();
39
52
  }
40
53
  return updatedEntities;
41
54
  }
@@ -14,13 +14,13 @@ export class CreateMetaOrgMigration {
14
14
  const orgResult = await this.client.query(`
15
15
  INSERT INTO "organizations" ("_id", "name", "code", "status", "isMetaOrg", "_created", "_createdBy", "_updated", "_updatedBy")
16
16
  VALUES ('${_id}', '${config.app.metaOrgName}', '${config.app.metaOrgCode}', 1, true, NOW(), 'system', NOW(), 'system')
17
- RETURNING "_id";
17
+ RETURNING "_id", "name", "code", "status", "isMetaOrg", "_created", "_createdBy", "_updated", "_updatedBy";
18
18
  `);
19
19
  if (orgResult.rowCount === 0) {
20
20
  await this.client.query('ROLLBACK');
21
21
  return { success: false, error: new Error(`Error creating meta org: No row returned`) };
22
22
  }
23
- initializeSystemUserContext(config.email?.systemEmailAddress || 'system@example.com', orgResult.rows[0]._id);
23
+ initializeSystemUserContext(config.email?.systemEmailAddress || 'system@example.com', orgResult.rows[0]);
24
24
  const migrationResult = await this.client.query(`
25
25
  INSERT INTO "migrations" ("_id", "index", "hasRun", "reverted")
26
26
  VALUES ('${_id}', ${this.index}, TRUE, FALSE);
@@ -15,11 +15,10 @@ export class CreateAdminUserMigration {
15
15
  async execute() {
16
16
  const _id = randomUUID().toString();
17
17
  const systemUserContext = getSystemUserContext();
18
- let createdUser;
19
18
  try {
20
- createdUser = await this.authService.createUser(systemUserContext, {
19
+ await this.authService.createUser(systemUserContext, {
21
20
  _id: _id,
22
- _orgId: systemUserContext._orgId,
21
+ _orgId: systemUserContext.organization?._id,
23
22
  email: config.adminUser?.email,
24
23
  password: config.adminUser?.password,
25
24
  firstName: 'Admin',
@@ -1,4 +1,4 @@
1
- import { IQueryOptions, IModelSpec, IPagedResult, IEntity } from "@loomcore/common/models";
1
+ import { IQueryOptions, IModelSpec, IPagedResult, IEntity, IUserContextAuthorization } from "@loomcore/common/models";
2
2
  import { TSchema } from "@sinclair/typebox";
3
3
  import { DeleteResult, IDatabase } from "../models/index.js";
4
4
  import { Operation } from "../operations/operation.js";
@@ -28,5 +28,5 @@ export declare class PostgresDatabase implements IDatabase {
28
28
  deleteMany(queryObject: IQueryOptions, pluralResourceName: string): Promise<DeleteResult>;
29
29
  find<T extends IEntity>(queryObject: IQueryOptions, pluralResourceName: string): Promise<T[]>;
30
30
  findOne<T extends IEntity>(queryObject: IQueryOptions, pluralResourceName: string): Promise<T | null>;
31
- getUserAuthorizations(userIds: string[], orgId?: string): Promise<Map<string, any[]>>;
31
+ getUserAuthorizations(userId: string, orgId?: string): Promise<IUserContextAuthorization[]>;
32
32
  }
@@ -66,12 +66,8 @@ export class PostgresDatabase {
66
66
  async findOne(queryObject, pluralResourceName) {
67
67
  return findOneQuery(this.client, queryObject, pluralResourceName);
68
68
  }
69
- async getUserAuthorizations(userIds, orgId) {
70
- if (userIds.length === 0) {
71
- return new Map();
72
- }
69
+ async getUserAuthorizations(userId, orgId) {
73
70
  const now = new Date();
74
- const placeholders = userIds.map((_, i) => `$${i + 1}`).join(', ');
75
71
  let query = `
76
72
  SELECT DISTINCT
77
73
  ur."userId" as "userId",
@@ -79,50 +75,34 @@ export class PostgresDatabase {
79
75
  f."name" as "feature",
80
76
  a."config",
81
77
  a."_id",
82
- a."_orgId",
83
- a."_created",
84
- a."_createdBy",
85
- a."_updated",
86
- a."_updatedBy"
78
+ a."_orgId"
87
79
  FROM "user_roles" ur
88
80
  INNER JOIN "roles" r ON ur."roleId" = r."_id"
89
81
  INNER JOIN "authorizations" a ON r."_id" = a."roleId"
90
82
  INNER JOIN "features" f ON a."featureId" = f."_id"
91
- WHERE ur."userId" IN (${placeholders})
83
+ WHERE ur."userId" = $1
92
84
  AND ur."_deleted" IS NULL
93
85
  AND a."_deleted" IS NULL
94
- AND (a."startDate" IS NULL OR a."startDate" <= $${userIds.length + 1})
95
- AND (a."endDate" IS NULL OR a."endDate" >= $${userIds.length + 1})
86
+ AND (a."startDate" IS NULL OR a."startDate" <= $2)
87
+ AND (a."endDate" IS NULL OR a."endDate" >= $2)
96
88
  `;
97
- const values = [...userIds, now];
89
+ const values = [userId, now];
98
90
  if (orgId) {
99
- query += ` AND ur."_orgId" = $${userIds.length + 2} AND r."_orgId" = $${userIds.length + 2} AND a."_orgId" = $${userIds.length + 2} AND f."_orgId" = $${userIds.length + 2}`;
91
+ query += ` AND ur."_orgId" = $3 AND r."_orgId" = $3 AND a."_orgId" = $3 AND f."_orgId" = $3`;
100
92
  values.push(orgId);
101
93
  }
102
94
  const result = await this.client.query(query, values);
103
- const authorizationsMap = new Map();
95
+ const authorizations = [];
104
96
  for (const row of result.rows) {
105
97
  const userId = row.userId;
106
- if (!authorizationsMap.has(userId)) {
107
- authorizationsMap.set(userId, []);
108
- }
109
- authorizationsMap.get(userId).push({
98
+ authorizations.push({
110
99
  _id: row._id,
111
100
  _orgId: row._orgId,
112
101
  role: row.role,
113
102
  feature: row.feature,
114
103
  config: row.config || undefined,
115
- _created: row._created,
116
- _createdBy: row._createdBy,
117
- _updated: row._updated,
118
- _updatedBy: row._updatedBy,
119
104
  });
120
105
  }
121
- for (const userId of userIds) {
122
- if (!authorizationsMap.has(userId)) {
123
- authorizationsMap.set(userId, []);
124
- }
125
- }
126
- return authorizationsMap;
106
+ return authorizations;
127
107
  }
128
108
  }
@@ -0,0 +1,16 @@
1
+ import { IAuditable, IEntity } from "@loomcore/common/models";
2
+ export interface IAuthorization extends IEntity, IAuditable {
3
+ roleId: string;
4
+ featureId: string;
5
+ startDate?: Date;
6
+ endDate?: Date;
7
+ config?: any;
8
+ }
9
+ export declare const AuthorizationSchema: import("@sinclair/typebox").TObject<{
10
+ roleId: import("@sinclair/typebox").TString;
11
+ featureId: import("@sinclair/typebox").TString;
12
+ startDate: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TTransform<import("@sinclair/typebox").TString, Date>>;
13
+ endDate: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TTransform<import("@sinclair/typebox").TString, Date>>;
14
+ config: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TAny>;
15
+ }>;
16
+ export declare const AuthorizationModelSpec: import("@loomcore/common/models").IModelSpec<import("@sinclair/typebox").TSchema>;
@@ -0,0 +1,11 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { entityUtils } from "@loomcore/common/utils";
3
+ import { TypeboxIsoDate } from "@loomcore/common/validation";
4
+ export const AuthorizationSchema = Type.Object({
5
+ roleId: Type.String({ minLength: 1, title: 'Role ID' }),
6
+ featureId: Type.String({ minLength: 1, title: 'Feature ID' }),
7
+ startDate: Type.Optional(TypeboxIsoDate({ title: 'Start Date' })),
8
+ endDate: Type.Optional(TypeboxIsoDate({ title: 'End Date' })),
9
+ config: Type.Optional(Type.Any({ title: 'Config' }))
10
+ });
11
+ export const AuthorizationModelSpec = entityUtils.getModelSpec(AuthorizationSchema, { isAuditable: true });
@@ -1,5 +1,5 @@
1
1
  import { Request, Response } from 'express';
2
- import { IUserContext, IUser, ITokenResponse, ILoginResponse } from '@loomcore/common/models';
2
+ import { IUserContext, ITokenResponse, ILoginResponse, IUser } from '@loomcore/common/models';
3
3
  import { MultiTenantApiService } from './multi-tenant-api.service.js';
4
4
  import { UpdateResult } from '../databases/models/update-result.js';
5
5
  import { IRefreshToken } from '../models/refresh-token.model.js';
@@ -11,6 +11,7 @@ import { OrganizationService } from './organization.service.js';
11
11
  import { passwordUtils } from '../utils/index.js';
12
12
  import { config } from '../config/index.js';
13
13
  import { refreshTokenModelSpec } from '../models/refresh-token.model.js';
14
+ import { getUserContextAuthorizations } from './utils/getUserContextAuthorizations.util.js';
14
15
  export class AuthService extends MultiTenantApiService {
15
16
  refreshTokenService;
16
17
  passwordResetTokenService;
@@ -26,6 +27,7 @@ export class AuthService extends MultiTenantApiService {
26
27
  async attemptLogin(req, res, email, password) {
27
28
  const lowerCaseEmail = email.toLowerCase();
28
29
  const user = await this.getUserByEmail(lowerCaseEmail);
30
+ const organization = await this.organizationService.findOne(EmptyUserContext, { filters: { _id: { eq: user?._orgId } } });
29
31
  if (!user) {
30
32
  throw new BadRequestError('Invalid Credentials');
31
33
  }
@@ -33,9 +35,11 @@ export class AuthService extends MultiTenantApiService {
33
35
  if (!passwordsMatch) {
34
36
  throw new BadRequestError('Invalid Credentials');
35
37
  }
38
+ const authorizations = await getUserContextAuthorizations(this.database, user);
36
39
  const userContext = {
37
40
  user: user,
38
- _orgId: user._orgId
41
+ organization: organization ?? undefined,
42
+ authorizations: authorizations
39
43
  };
40
44
  const deviceId = this.getAndSetDeviceIdCookie(req, res);
41
45
  const loginResponse = await this.logUserIn(userContext, deviceId);
@@ -44,7 +48,7 @@ export class AuthService extends MultiTenantApiService {
44
48
  async logUserIn(userContext, deviceId) {
45
49
  const payload = userContext;
46
50
  const accessToken = this.generateJwt(payload);
47
- const refreshTokenObject = await this.createNewRefreshToken(userContext.user._id, deviceId, userContext._orgId);
51
+ const refreshTokenObject = await this.createNewRefreshToken(userContext.user._id, deviceId, userContext.organization?._id);
48
52
  const accessTokenExpiresOn = this.getExpiresOnFromSeconds(config.auth.jwtExpirationInSeconds);
49
53
  let loginResponse = null;
50
54
  if (refreshTokenObject) {
@@ -66,27 +70,28 @@ export class AuthService extends MultiTenantApiService {
66
70
  if (!rawUser) {
67
71
  return null;
68
72
  }
69
- const userContext = {
70
- _orgId: rawUser._orgId,
71
- user: rawUser
72
- };
73
- return this.postprocessEntity(userContext, rawUser);
73
+ const dbPostprocessed = this.database.postprocessEntity(rawUser, this.modelSpec.fullSchema);
74
+ return dbPostprocessed;
74
75
  }
75
76
  async createUser(userContext, user) {
77
+ if (userContext.user?._id === 'system') {
78
+ if (user._orgId) {
79
+ const org = await this.organizationService.findOne(userContext, { filters: { _id: { eq: user._orgId } } });
80
+ if (!org) {
81
+ throw new BadRequestError('The specified organization does not exist');
82
+ }
83
+ }
84
+ if (config.app.isMultiTenant && userContext.organization?._id !== user._orgId) {
85
+ throw new BadRequestError('User is not authorized to create a user in this organization');
86
+ }
87
+ }
76
88
  if (user.email) {
77
89
  const existingUser = await this.getUserByEmail(user.email);
78
90
  if (existingUser) {
79
91
  throw new BadRequestError('A user with this email address already exists');
80
92
  }
81
93
  }
82
- if (user._orgId && userContext._orgId && userContext._orgId !== user._orgId && userContext.user?._id !== 'system') {
83
- const org = await this.organizationService.findOne(userContext, { filters: { _id: { eq: user._orgId } } });
84
- if (!org) {
85
- throw new BadRequestError('The specified organization does not exist');
86
- }
87
- }
88
- const createdUser = await this.create(userContext, user);
89
- return createdUser;
94
+ return await this.create(userContext, user);
90
95
  }
91
96
  async requestTokenUsingRefreshToken(refreshToken, deviceId) {
92
97
  let tokens = null;
@@ -94,9 +99,12 @@ export class AuthService extends MultiTenantApiService {
94
99
  if (activeRefreshToken) {
95
100
  const systemUserContext = getSystemUserContext();
96
101
  const user = await this.getById(systemUserContext, activeRefreshToken.userId);
102
+ const organization = await this.organizationService.findOne(EmptyUserContext, { filters: { _id: { eq: user?._orgId } } });
103
+ const authorizations = await getUserContextAuthorizations(this.database, user);
97
104
  const userContext = {
98
- _orgId: user._orgId,
99
- user: user
105
+ user: user,
106
+ organization: organization ?? undefined,
107
+ authorizations: authorizations
100
108
  };
101
109
  tokens = await this.createNewTokens(userContext, activeRefreshToken);
102
110
  }
@@ -7,4 +7,4 @@ export * from './multi-tenant-api.service.js';
7
7
  export * from './organization.service.js';
8
8
  export * from './password-reset-token.service.js';
9
9
  export * from './tenant-query-decorator.js';
10
- export * from './user-service/user.service.js';
10
+ export * from './user.service.js';
@@ -7,4 +7,4 @@ export * from './multi-tenant-api.service.js';
7
7
  export * from './organization.service.js';
8
8
  export * from './password-reset-token.service.js';
9
9
  export * from './tenant-query-decorator.js';
10
- export * from './user-service/user.service.js';
10
+ export * from './user.service.js';
@@ -14,7 +14,7 @@ export class MultiTenantApiService extends GenericApiService {
14
14
  if (!config?.app?.isMultiTenant || userContext?.user?._id === 'system') {
15
15
  return super.prepareQuery(userContext, queryOptions, operations);
16
16
  }
17
- if (!userContext || !userContext._orgId) {
17
+ if (!userContext || !userContext.organization?._id) {
18
18
  throw new BadRequestError('A valid userContext was not provided to MultiTenantApiService.prepareQuery');
19
19
  }
20
20
  const queryObject = this.tenantDecorator.applyTenantToQuery(userContext, queryOptions, this.pluralResourceName);
@@ -24,12 +24,12 @@ export class MultiTenantApiService extends GenericApiService {
24
24
  if (!config?.app?.isMultiTenant) {
25
25
  return super.preprocessEntity(userContext, entity, isCreate, allowId);
26
26
  }
27
- if (!userContext || !userContext._orgId) {
27
+ if (!userContext || !userContext.organization?._id) {
28
28
  throw new BadRequestError('A valid userContext was not provided to MultiTenantApiService.prepareEntity');
29
29
  }
30
30
  const preparedEntity = await super.preprocessEntity(userContext, entity, isCreate, allowId);
31
31
  if (isCreate && userContext.user._id !== 'system') {
32
- preparedEntity._orgId = userContext._orgId;
32
+ preparedEntity._orgId = userContext.organization?._id;
33
33
  }
34
34
  return preparedEntity;
35
35
  }
@@ -1,6 +1,6 @@
1
- import { GenericApiService } from './generic-api-service/generic-api.service.js';
2
1
  import { IOrganization, IUserContext } from '@loomcore/common/models';
3
2
  import { IDatabase } from '../databases/models/database.interface.js';
3
+ import { GenericApiService } from './generic-api-service/generic-api.service.js';
4
4
  export declare class OrganizationService extends GenericApiService<IOrganization> {
5
5
  constructor(database: IDatabase);
6
6
  preprocessEntity(userContext: IUserContext, entity: Partial<IOrganization>, isCreate: boolean, allowId?: boolean): Promise<Partial<IOrganization>>;
@@ -1,6 +1,6 @@
1
- import { GenericApiService } from './generic-api-service/generic-api.service.js';
2
1
  import { OrganizationSpec } from '@loomcore/common/models';
3
2
  import { BadRequestError } from '../errors/index.js';
3
+ import { GenericApiService } from './generic-api-service/generic-api.service.js';
4
4
  export class OrganizationService extends GenericApiService {
5
5
  constructor(database) {
6
6
  super(database, 'organizations', 'organization', OrganizationSpec);
@@ -11,7 +11,7 @@ export class OrganizationService extends GenericApiService {
11
11
  if (metaOrg && entity.isMetaOrg) {
12
12
  throw new BadRequestError('Meta organization already exists');
13
13
  }
14
- if (metaOrg && userContext._orgId !== metaOrg._id) {
14
+ if (metaOrg && userContext.organization?._id !== metaOrg._id) {
15
15
  throw new BadRequestError('User is not authorized to create an organization');
16
16
  }
17
17
  }
@@ -1,6 +1,6 @@
1
1
  import { IPasswordResetToken } from '@loomcore/common/models';
2
- import { GenericApiService } from './generic-api-service/generic-api.service.js';
3
2
  import { IDatabase } from '../databases/models/index.js';
3
+ import { GenericApiService } from './generic-api-service/generic-api.service.js';
4
4
  export declare class PasswordResetTokenService extends GenericApiService<IPasswordResetToken> {
5
5
  constructor(database: IDatabase);
6
6
  createPasswordResetToken(email: string, expiresOn: number): Promise<IPasswordResetToken | null>;
@@ -11,12 +11,12 @@ export class TenantQueryDecorator {
11
11
  applyTenantToQuery(userContext, queryObject, collectionName) {
12
12
  let result = queryObject;
13
13
  const shouldApplyTenantFilter = !this.options.excludedCollections?.includes(collectionName) &&
14
- userContext?._orgId;
14
+ userContext?.organization?._id;
15
15
  if (shouldApplyTenantFilter) {
16
16
  const orgIdField = this.options.orgIdField || '_orgId';
17
- result = { ...queryObject, filters: { ...queryObject.filters, [orgIdField]: { eq: userContext._orgId } } };
17
+ result = { ...queryObject, filters: { ...queryObject.filters, [orgIdField]: { eq: userContext.organization?._id } } };
18
18
  }
19
- else if (!userContext?._orgId) {
19
+ else if (!userContext?.organization?._id) {
20
20
  if (!this.options.excludedCollections?.includes(collectionName)) {
21
21
  throw new ServerError('No _orgId found in userContext');
22
22
  }
@@ -27,29 +27,29 @@ export class TenantQueryDecorator {
27
27
  const result = { ...queryOptions };
28
28
  const shouldApplyTenantFilter = !this.options.excludedCollections?.includes(collectionName);
29
29
  if (shouldApplyTenantFilter) {
30
- if (!userContext._orgId) {
30
+ if (!userContext?.organization?._id) {
31
31
  throw new ServerError('userContext must have an _orgId property to apply tenant filtering');
32
32
  }
33
33
  if (!result.filters) {
34
34
  result.filters = {};
35
35
  }
36
36
  const orgIdField = this.getOrgIdField();
37
- result.filters[orgIdField] = { eq: userContext._orgId };
37
+ result.filters[orgIdField] = { eq: userContext.organization?._id };
38
38
  }
39
39
  return result;
40
40
  }
41
41
  applyTenantToEntity(userContext, entity, collectionName) {
42
42
  let result = entity;
43
43
  const shouldApplyTenantFilter = !this.options.excludedCollections?.includes(collectionName) &&
44
- userContext?._orgId;
44
+ userContext?.organization?._id;
45
45
  if (shouldApplyTenantFilter) {
46
46
  const orgIdField = this.options.orgIdField || '_orgId';
47
47
  result = {
48
48
  ...entity,
49
- [orgIdField]: userContext._orgId
49
+ [orgIdField]: userContext.organization?._id
50
50
  };
51
51
  }
52
- else if (!userContext?._orgId) {
52
+ else if (!userContext?.organization?._id) {
53
53
  if (!this.options.excludedCollections?.includes(collectionName)) {
54
54
  throw new ServerError('No _orgId found in userContext');
55
55
  }
@@ -1,6 +1,6 @@
1
1
  import { IUser, IUserContext, IPagedResult, IQueryOptions } from '@loomcore/common/models';
2
- import { MultiTenantApiService } from '../index.js';
3
- import { IDatabase } from '../../databases/models/index.js';
2
+ import { MultiTenantApiService } from './index.js';
3
+ import { IDatabase } from '../databases/models/index.js';
4
4
  export declare class UserService extends MultiTenantApiService<IUser> {
5
5
  constructor(database: IDatabase);
6
6
  fullUpdateById(userContext: IUserContext, id: string, entity: IUser): Promise<IUser>;
@@ -8,5 +8,4 @@ export declare class UserService extends MultiTenantApiService<IUser> {
8
8
  get(userContext: IUserContext, queryOptions: IQueryOptions): Promise<IPagedResult<IUser>>;
9
9
  getAll(userContext: IUserContext): Promise<IUser[]>;
10
10
  preprocessEntity(userContext: IUserContext, entity: Partial<IUser>, isCreate: boolean, allowId?: boolean): Promise<Partial<IUser>>;
11
- private addAuthorizationsToUsers;
12
11
  }
@@ -1,8 +1,7 @@
1
1
  import { Value } from '@sinclair/typebox/value';
2
2
  import { UserSpec, PublicUserSchema } from '@loomcore/common/models';
3
- import { MultiTenantApiService } from '../index.js';
4
- import { IdNotFoundError, ServerError } from '../../errors/index.js';
5
- import { PostgresDatabase } from '../../databases/postgres/postgres.database.js';
3
+ import { MultiTenantApiService } from './index.js';
4
+ import { IdNotFoundError, ServerError } from '../errors/index.js';
6
5
  export class UserService extends MultiTenantApiService {
7
6
  constructor(database) {
8
7
  super(database, 'users', 'user', UserSpec);
@@ -16,13 +15,12 @@ export class UserService extends MultiTenantApiService {
16
15
  if (!user) {
17
16
  throw new IdNotFoundError();
18
17
  }
19
- const usersWithAuth = await this.addAuthorizationsToUsers(userContext, [user]);
20
- return usersWithAuth[0];
18
+ return this.postprocessEntity(userContext, user);
21
19
  }
22
20
  async get(userContext, queryOptions) {
23
21
  const { operations, queryObject } = this.prepareQuery(userContext, queryOptions, []);
24
22
  const pagedResult = await this.database.get(operations, queryObject, this.modelSpec, this.pluralResourceName);
25
- const transformedEntities = await this.addAuthorizationsToUsers(userContext, pagedResult.entities || []);
23
+ const transformedEntities = (pagedResult.entities || []).map(entity => this.postprocessEntity(userContext, entity));
26
24
  return {
27
25
  ...pagedResult,
28
26
  entities: transformedEntities
@@ -31,7 +29,7 @@ export class UserService extends MultiTenantApiService {
31
29
  async getAll(userContext) {
32
30
  const { operations } = this.prepareQuery(userContext, {}, []);
33
31
  const users = await this.database.getAll(operations, this.pluralResourceName);
34
- return this.addAuthorizationsToUsers(userContext, users);
32
+ return users.map(user => this.postprocessEntity(userContext, user));
35
33
  }
36
34
  async preprocessEntity(userContext, entity, isCreate, allowId = false) {
37
35
  const preparedEntity = await super.preprocessEntity(userContext, entity, isCreate);
@@ -43,23 +41,4 @@ export class UserService extends MultiTenantApiService {
43
41
  }
44
42
  return preparedEntity;
45
43
  }
46
- async addAuthorizationsToUsers(userContext, users) {
47
- if (users.length === 0) {
48
- return users;
49
- }
50
- if (!(this.database instanceof PostgresDatabase)) {
51
- return users.map(user => this.postprocessEntity(userContext, user));
52
- }
53
- const userIds = users.map(user => user._id);
54
- const orgId = userContext._orgId;
55
- const authorizationsMap = await this.database.getUserAuthorizations(userIds, orgId);
56
- return users.map(user => {
57
- const authorizations = authorizationsMap.get(user._id) || [];
58
- const userWithAuth = {
59
- ...user,
60
- authorizations
61
- };
62
- return this.postprocessEntity(userContext, userWithAuth);
63
- });
64
- }
65
44
  }
@@ -0,0 +1,3 @@
1
+ import { IUser, IUserContextAuthorization } from "@loomcore/common/models";
2
+ import { IDatabase } from "../../databases/models/index.js";
3
+ export declare function getUserContextAuthorizations(database: IDatabase, user: IUser): Promise<IUserContextAuthorization[]>;
@@ -0,0 +1,9 @@
1
+ import { PostgresDatabase } from "../../databases/postgres/postgres.database.js";
2
+ export async function getUserContextAuthorizations(database, user) {
3
+ if (!(database instanceof PostgresDatabase)) {
4
+ return [];
5
+ }
6
+ const orgId = user._orgId;
7
+ const authorizations = await database.getUserAuthorizations(user._id, orgId);
8
+ return authorizations;
9
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loomcore/api",
3
- "version": "0.1.35",
3
+ "version": "0.1.37",
4
4
  "private": false,
5
5
  "description": "Loom Core Api - An opinionated Node.js api using Typescript, Express, and MongoDb or PostgreSQL",
6
6
  "scripts": {
@@ -53,7 +53,7 @@
53
53
  "qs": "^6.14.0"
54
54
  },
55
55
  "peerDependencies": {
56
- "@loomcore/common": "^0.0.29",
56
+ "@loomcore/common": "^0.0.32",
57
57
  "@sinclair/typebox": "0.34.33",
58
58
  "cookie-parser": "^1.4.6",
59
59
  "cors": "^2.8.5",