@loomcore/api 0.2.57 → 0.2.58

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 (50) hide show
  1. package/dist/__tests__/common-test.utils.d.ts +2 -2
  2. package/dist/__tests__/common-test.utils.js +12 -11
  3. package/dist/__tests__/mongo-db.test-database.js +5 -1
  4. package/dist/controllers/auth.controller.d.ts +1 -0
  5. package/dist/controllers/auth.controller.js +39 -5
  6. package/dist/controllers/authorizations.controller.js +58 -6
  7. package/dist/controllers/organizations.controller.d.ts +4 -1
  8. package/dist/controllers/organizations.controller.js +39 -12
  9. package/dist/controllers/types.d.ts +2 -2
  10. package/dist/decorators/allow-anonymous.decorator.d.ts +3 -0
  11. package/dist/decorators/allow-anonymous.decorator.js +13 -0
  12. package/dist/decorators/auth-requirement.interface.d.ts +7 -0
  13. package/dist/decorators/auth-requirement.interface.js +1 -0
  14. package/dist/decorators/authorize.decorator.d.ts +8 -12
  15. package/dist/decorators/authorize.decorator.js +18 -29
  16. package/dist/decorators/index.d.ts +3 -1
  17. package/dist/decorators/index.js +2 -1
  18. package/dist/decorators/resolve-allow-anonymous.util.d.ts +1 -0
  19. package/dist/decorators/resolve-allow-anonymous.util.js +6 -0
  20. package/dist/decorators/resolve-auth-decorator-target.util.d.ts +1 -0
  21. package/dist/decorators/resolve-auth-decorator-target.util.js +9 -0
  22. package/dist/decorators/resolve-auth-requirement.util.d.ts +2 -0
  23. package/dist/decorators/resolve-auth-requirement.util.js +6 -0
  24. package/dist/errors/unauthorized.error.d.ts +1 -1
  25. package/dist/errors/unauthorized.error.js +2 -4
  26. package/dist/middleware/authorize/authorize.middleware.d.ts +2 -2
  27. package/dist/middleware/authorize/authorize.middleware.js +17 -12
  28. package/dist/services/generic-api-service/generic-api-service.interface.d.ts +2 -2
  29. package/dist/services/generic-api-service/generic-api.service.d.ts +6 -6
  30. package/dist/services/generic-api-service/generic-api.service.js +22 -33
  31. package/dist/services/multi-tenant-api.service.d.ts +1 -1
  32. package/dist/services/multi-tenant-api.service.js +8 -3
  33. package/dist/services/organization.service.js +5 -3
  34. package/dist/services/tenant-query-decorator.d.ts +1 -1
  35. package/dist/services/tenant-query-decorator.js +10 -7
  36. package/dist/services/utils/audit-for-create.util.js +1 -2
  37. package/dist/services/utils/audit-for-update.util.js +1 -2
  38. package/dist/utils/auth/authorize-method.util.d.ts +1 -1
  39. package/dist/utils/auth/authorize-method.util.js +14 -3
  40. package/dist/utils/auth/impersonate-user.util.d.ts +6 -0
  41. package/dist/utils/auth/impersonate-user.util.js +35 -0
  42. package/dist/utils/auth/index.d.ts +2 -0
  43. package/dist/utils/auth/index.js +2 -0
  44. package/dist/utils/auth/is-admin.util.d.ts +2 -1
  45. package/dist/utils/auth/is-admin.util.js +9 -1
  46. package/dist/utils/auth/is-system-user.util.d.ts +2 -0
  47. package/dist/utils/auth/is-system-user.util.js +4 -0
  48. package/dist/utils/jwt.utils.d.ts +2 -1
  49. package/dist/utils/jwt.utils.js +3 -2
  50. package/package.json +2 -2
@@ -46,8 +46,8 @@ export declare class ProductsController extends ApiController<IProduct> {
46
46
  export declare class MultiTenantProductService extends MultiTenantApiService<IProduct> {
47
47
  private db;
48
48
  constructor(database: IDatabase);
49
- prepareQuery(userContext: IUserContext, queryObject: IQueryOptions, operations: Operation[]): {
50
- queryObject: IQueryOptions;
49
+ prepareQuery(userContext: IUserContext, queryOptions: IQueryOptions, operations: Operation[]): {
50
+ queryOptions: IQueryOptions;
51
51
  operations: Operation[];
52
52
  };
53
53
  postProcessEntity(userContext: IUserContext, single: any): any;
@@ -58,19 +58,20 @@ async function createMetaOrg() {
58
58
  throw new Error('OrganizationService not initialized. Call initialize() first.');
59
59
  }
60
60
  try {
61
- const existingMetaOrg = await organizationService.getMetaOrg(EmptyUserContext);
61
+ const systemUserContext = getSystemUserContext();
62
+ const existingMetaOrg = await organizationService.getMetaOrg(systemUserContext);
62
63
  if (!existingMetaOrg) {
63
- const metaOrgInsertResult = await organizationService.create(EmptyUserContext, getTestMetaOrg());
64
+ const metaOrgInsertResult = await organizationService.create(systemUserContext, getTestMetaOrg());
64
65
  if (metaOrgInsertResult) {
65
66
  setTestMetaOrgId(metaOrgInsertResult._id);
66
- await organizationDomainService.create(EmptyUserContext, getTestMetaOrgDomain(metaOrgInsertResult._id));
67
+ await organizationDomainService.create(systemUserContext, getTestMetaOrgDomain(metaOrgInsertResult._id));
67
68
  }
68
69
  }
69
70
  else {
70
71
  setTestMetaOrgId(existingMetaOrg._id);
71
- const existingDomain = await organizationDomainService.findOne(EmptyUserContext, { filters: { domain: { eq: TEST_META_ORG_DOMAIN } } });
72
+ const existingDomain = await organizationDomainService.findOne(systemUserContext, { filters: { domain: { eq: TEST_META_ORG_DOMAIN } } });
72
73
  if (!existingDomain) {
73
- await organizationDomainService.create(EmptyUserContext, getTestMetaOrgDomain(existingMetaOrg._id));
74
+ await organizationDomainService.create(systemUserContext, getTestMetaOrgDomain(existingMetaOrg._id));
74
75
  }
75
76
  }
76
77
  }
@@ -120,13 +121,13 @@ async function createTestUsers() {
120
121
  throw new Error('Failed to create test organization');
121
122
  }
122
123
  setTestOrgId(createdTestOrg._id);
123
- await organizationDomainService.create(EmptyUserContext, getTestOrgDomain(createdTestOrg._id));
124
+ await organizationDomainService.create(getSystemUserContext(), getTestOrgDomain(createdTestOrg._id));
124
125
  }
125
126
  else {
126
127
  setTestOrgId(existingTestOrg._id);
127
128
  const existingDomain = await organizationDomainService.findOne(EmptyUserContext, { filters: { domain: { eq: TEST_ORG_DOMAIN } } });
128
129
  if (!existingDomain) {
129
- await organizationDomainService.create(EmptyUserContext, getTestOrgDomain(existingTestOrg._id));
130
+ await organizationDomainService.create(getSystemUserContext(), getTestOrgDomain(existingTestOrg._id));
130
131
  }
131
132
  }
132
133
  const createdTestOrgUser = await userService.create(getTestOrgUserContext(), getTestOrgUser());
@@ -271,9 +272,9 @@ export function setupTestConfig(isMultiTenant = true, dbType) {
271
272
  },
272
273
  });
273
274
  }
274
- const prepareQueryCustom = (userContext, queryObject, operations) => {
275
+ const prepareQueryCustom = (userContext, queryOptions, operations) => {
275
276
  return {
276
- queryObject: queryObject,
277
+ queryOptions: queryOptions,
277
278
  operations: [
278
279
  ...operations,
279
280
  new LeftJoin('categories', 'category_id', '_id', 'category'),
@@ -342,12 +343,12 @@ export class MultiTenantProductService extends MultiTenantApiService {
342
343
  super(database, 'products', 'product', ProductSpec);
343
344
  this.db = database;
344
345
  }
345
- prepareQuery(userContext, queryObject, operations) {
346
+ prepareQuery(userContext, queryOptions, operations) {
346
347
  const newOperations = [
347
348
  ...operations,
348
349
  new LeftJoin('categories', 'categoryId', '_id', 'category'),
349
350
  ];
350
- return super.prepareQuery(userContext, queryObject, newOperations);
351
+ return super.prepareQuery(userContext, queryOptions, newOperations);
351
352
  }
352
353
  postProcessEntity(userContext, single) {
353
354
  if (single && single.category) {
@@ -1,7 +1,7 @@
1
1
  import { MongoMemoryServer } from 'mongodb-memory-server';
2
2
  import { MongoClient, ObjectId } from 'mongodb';
3
3
  import testUtils from './common-test.utils.js';
4
- import { initSystemUserContext } from '../config/base-api-config.js';
4
+ import { config, initSystemUserContext } from '../config/base-api-config.js';
5
5
  import { MongoDBDatabase } from '../databases/index.js';
6
6
  function isPortBindError(error) {
7
7
  const code = error?.code;
@@ -64,6 +64,10 @@ export class TestMongoDatabase {
64
64
  this.database = testDatabase;
65
65
  testUtils.initialize(testDatabase);
66
66
  await this.createIndexes(this.mongoDb);
67
+ const { initializeSystemUserContext, isSystemUserContextInitialized } = await import('@loomcore/common/models');
68
+ if (!isSystemUserContextInitialized()) {
69
+ initializeSystemUserContext(config.email?.systemEmailAddress || 'system@test.com', undefined);
70
+ }
67
71
  await testUtils.createMetaOrg();
68
72
  }
69
73
  await initSystemUserContext(this.database);
@@ -21,6 +21,7 @@ export declare class AuthController {
21
21
  login(req: Request, res: Response): Promise<void>;
22
22
  requestTokenUsingRefreshToken(req: Request, res: Response): Promise<void>;
23
23
  getUserContext(req: Request, res: Response): Promise<void>;
24
+ impersonate(req: Request, res: Response): Promise<void>;
24
25
  afterAuth(_req: Request, _res: Response, _loginResponse: ILoginResponse): Promise<void>;
25
26
  changePassword(req: Request, res: Response): Promise<void>;
26
27
  forgotPassword(req: Request, res: Response): Promise<void>;
@@ -37,19 +37,22 @@ import { entityUtils } from '@loomcore/common/utils';
37
37
  import { config } from '../config/base-api-config.js';
38
38
  import { BadRequestError, UnauthenticatedError } from '../errors/index.js';
39
39
  import { OrganizationService, UserService } from '../services/index.js';
40
- import { attemptLogin, authorizeMethod, createLoginResponseSpec, getAndSetDeviceIdCookie, getDeviceIdFromCookie, requestTokenUsingRefreshToken, resetPassword, sendResetPasswordEmail, } from '../utils/auth/index.js';
40
+ import { attemptLogin, authorizeMethod, createLoginResponseSpec, getAndSetDeviceIdCookie, getDeviceIdFromCookie, impersonateUser, requestTokenUsingRefreshToken, resetPassword, sendResetPasswordEmail, } from '../utils/auth/index.js';
41
41
  import { apiUtils } from '../utils/index.js';
42
42
  import { Authorize } from '../decorators/authorize.decorator.js';
43
43
  let AuthController = (() => {
44
44
  let _instanceExtraInitializers = [];
45
45
  let _getUserContext_decorators;
46
+ let _impersonate_decorators;
46
47
  let _changePassword_decorators;
47
48
  return class AuthController {
48
49
  static {
49
50
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
50
51
  _getUserContext_decorators = [Authorize()];
51
- _changePassword_decorators = [Authorize()];
52
+ _impersonate_decorators = [Authorize('admin')];
53
+ _changePassword_decorators = [Authorize({ denyOnImpersonation: true })];
52
54
  __esDecorate(this, null, _getUserContext_decorators, { kind: "method", name: "getUserContext", static: false, private: false, access: { has: obj => "getUserContext" in obj, get: obj => obj.getUserContext }, metadata: _metadata }, null, _instanceExtraInitializers);
55
+ __esDecorate(this, null, _impersonate_decorators, { kind: "method", name: "impersonate", static: false, private: false, access: { has: obj => "impersonate" in obj, get: obj => obj.impersonate }, metadata: _metadata }, null, _instanceExtraInitializers);
53
56
  __esDecorate(this, null, _changePassword_decorators, { kind: "method", name: "changePassword", static: false, private: false, access: { has: obj => "changePassword" in obj, get: obj => obj.changePassword }, metadata: _metadata }, null, _instanceExtraInitializers);
54
57
  if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
55
58
  }
@@ -80,6 +83,7 @@ let AuthController = (() => {
80
83
  app.post(`/api/auth/login`, this.login.bind(this));
81
84
  app.get(`/api/auth/refresh`, this.requestTokenUsingRefreshToken.bind(this));
82
85
  app.get(`/api/auth/get-user-context`, authorize('getUserContext'), this.getUserContext.bind(this));
86
+ app.post(`/api/auth/impersonate`, authorize('impersonate'), this.impersonate.bind(this));
83
87
  app.patch(`/api/auth/change-password`, authorize('changePassword'), this.changePassword.bind(this));
84
88
  app.post(`/api/auth/forgot-password`, this.forgotPassword.bind(this));
85
89
  app.post(`/api/auth/reset-password`, this.resetPassword.bind(this));
@@ -99,6 +103,9 @@ let AuthController = (() => {
99
103
  throw new BadRequestError('Missing required fields: referer is required.');
100
104
  }
101
105
  const domain = referer.split('/')[2];
106
+ if (!domain) {
107
+ throw new BadRequestError('Missing required fields: domain is required.');
108
+ }
102
109
  organization = await this.organizationService.findByDomain(EmptyUserContext, domain);
103
110
  if (!organization) {
104
111
  throw new BadRequestError(`No organization found for domain: ${domain}`);
@@ -126,6 +133,26 @@ let AuthController = (() => {
126
133
  const userContext = req.userContext;
127
134
  apiUtils.apiResponse(res, 200, { data: userContext }, this.userContextSpec);
128
135
  }
136
+ async impersonate(req, res) {
137
+ const userContext = req.userContext;
138
+ if (!userContext) {
139
+ throw new UnauthenticatedError();
140
+ }
141
+ const refreshToken = req.body.refreshToken;
142
+ if (!refreshToken || typeof refreshToken !== 'string') {
143
+ throw new BadRequestError('Missing required fields: refreshToken is required.');
144
+ }
145
+ const userId = req.body?.userId;
146
+ if (!userId) {
147
+ throw new BadRequestError('Missing required fields: userId is required.');
148
+ }
149
+ const deviceId = getDeviceIdFromCookie(req);
150
+ if (!deviceId || typeof deviceId !== 'string') {
151
+ throw new UnauthenticatedError();
152
+ }
153
+ const tokens = await impersonateUser(this.database, userContext, userId, refreshToken, this.userService);
154
+ apiUtils.apiResponse(res, 200, { data: tokens }, TokenResponseSpec);
155
+ }
129
156
  async afterAuth(_req, _res, _loginResponse) { }
130
157
  async changePassword(req, res) {
131
158
  const userContext = req.userContext;
@@ -151,10 +178,13 @@ let AuthController = (() => {
151
178
  if (!referer) {
152
179
  throw new BadRequestError('Missing required fields: referer is required.');
153
180
  }
154
- referer = referer.replace(/\/$/, '');
155
181
  let organization = null;
156
182
  if (config.app.isMultiTenant) {
157
- organization = await this.organizationService.findByDomain(EmptyUserContext, referer.split('/')[2]);
183
+ const domain = referer.split('/')[2];
184
+ if (!domain) {
185
+ throw new BadRequestError('Missing required fields: domain is required.');
186
+ }
187
+ organization = await this.organizationService.findByDomain(EmptyUserContext, domain);
158
188
  if (!organization) {
159
189
  throw new BadRequestError('Missing required fields: organization is required.');
160
190
  }
@@ -179,7 +209,11 @@ let AuthController = (() => {
179
209
  if (!referer) {
180
210
  throw new BadRequestError('Missing required fields: referer is required.');
181
211
  }
182
- organization = await this.organizationService.findByDomain(EmptyUserContext, referer.split('/')[2]);
212
+ const domain = referer.split('/')[2];
213
+ if (!domain) {
214
+ throw new BadRequestError('Missing required fields: domain is required.');
215
+ }
216
+ organization = await this.organizationService.findByDomain(EmptyUserContext, domain);
183
217
  if (!organization) {
184
218
  throw new BadRequestError('Missing required fields: organization is required.');
185
219
  }
@@ -1,9 +1,61 @@
1
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
2
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
3
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
4
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
5
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
6
+ var _, done = false;
7
+ for (var i = decorators.length - 1; i >= 0; i--) {
8
+ var context = {};
9
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
10
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
11
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
12
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
13
+ if (kind === "accessor") {
14
+ if (result === void 0) continue;
15
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
16
+ if (_ = accept(result.get)) descriptor.get = _;
17
+ if (_ = accept(result.set)) descriptor.set = _;
18
+ if (_ = accept(result.init)) initializers.unshift(_);
19
+ }
20
+ else if (_ = accept(result)) {
21
+ if (kind === "field") initializers.unshift(_);
22
+ else descriptor[key] = _;
23
+ }
24
+ }
25
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
26
+ done = true;
27
+ };
28
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
29
+ var useValue = arguments.length > 2;
30
+ for (var i = 0; i < initializers.length; i++) {
31
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
32
+ }
33
+ return useValue ? value : void 0;
34
+ };
1
35
  import { ApiController } from './api.controller.js';
2
36
  import { AuthorizationModelSpec } from '@loomcore/common/models';
3
37
  import { AuthorizationService } from '../services/authorization.service.js';
4
- export class AuthorizationsController extends ApiController {
5
- constructor(app, database) {
6
- const authorizationService = new AuthorizationService(database);
7
- super('authorizations', app, authorizationService, 'authorization', AuthorizationModelSpec);
8
- }
9
- }
38
+ import { Authorize } from '../decorators/authorize.decorator.js';
39
+ let AuthorizationsController = (() => {
40
+ let _classDecorators = [Authorize('admin')];
41
+ let _classDescriptor;
42
+ let _classExtraInitializers = [];
43
+ let _classThis;
44
+ let _classSuper = ApiController;
45
+ var AuthorizationsController = class extends _classSuper {
46
+ static { _classThis = this; }
47
+ static {
48
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
49
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
50
+ AuthorizationsController = _classThis = _classDescriptor.value;
51
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
52
+ __runInitializers(_classThis, _classExtraInitializers);
53
+ }
54
+ constructor(app, database) {
55
+ const authorizationService = new AuthorizationService(database);
56
+ super('authorizations', app, authorizationService, 'authorization', AuthorizationModelSpec);
57
+ }
58
+ };
59
+ return AuthorizationsController = _classThis;
60
+ })();
61
+ export { AuthorizationsController };
@@ -1,4 +1,4 @@
1
- import type { IOrganization } from '@loomcore/common/models';
1
+ import { type IOrganization } from '@loomcore/common/models';
2
2
  import type { Application, NextFunction, Request, Response } from 'express';
3
3
  import type { IDatabase } from '../databases/models/index.js';
4
4
  import { OrganizationService } from '../services/index.js';
@@ -13,4 +13,7 @@ export declare class OrganizationsController extends ApiController<IOrganization
13
13
  getByCode(req: Request<{
14
14
  code: string;
15
15
  }>, res: Response, next: NextFunction): Promise<void>;
16
+ getById(req: Request<{
17
+ id: string;
18
+ }>, res: Response, next: NextFunction): Promise<void>;
16
19
  }
@@ -1,3 +1,10 @@
1
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
2
+ var useValue = arguments.length > 2;
3
+ for (var i = 0; i < initializers.length; i++) {
4
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
5
+ }
6
+ return useValue ? value : void 0;
7
+ };
1
8
  var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
2
9
  function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
3
10
  var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
@@ -25,34 +32,32 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
25
32
  if (target) Object.defineProperty(target, contextIn.name, descriptor);
26
33
  done = true;
27
34
  };
28
- var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
29
- var useValue = arguments.length > 2;
30
- for (var i = 0; i < initializers.length; i++) {
31
- value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
32
- }
33
- return useValue ? value : void 0;
34
- };
35
- import { BadRequestError } from '../errors/index.js';
35
+ import { getSystemUserContext } from '@loomcore/common/models';
36
+ import { BadRequestError, UnauthenticatedError } from '../errors/index.js';
36
37
  import { OrganizationService } from '../services/index.js';
37
38
  import { apiUtils } from '../utils/index.js';
38
39
  import { ApiController } from './api.controller.js';
39
40
  import { Authorize } from '../decorators/authorize.decorator.js';
40
41
  let OrganizationsController = (() => {
41
- let _classDecorators = [Authorize('admin')];
42
+ let _classDecorators = [Authorize({ features: ['admin'], requireMetaOrg: true })];
42
43
  let _classDescriptor;
43
44
  let _classExtraInitializers = [];
44
45
  let _classThis;
45
46
  let _classSuper = ApiController;
47
+ let _instanceExtraInitializers = [];
48
+ let _getById_decorators;
46
49
  var OrganizationsController = class extends _classSuper {
47
50
  static { _classThis = this; }
48
51
  static {
49
52
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
53
+ _getById_decorators = [Authorize('admin')];
54
+ __esDecorate(this, null, _getById_decorators, { kind: "method", name: "getById", static: false, private: false, access: { has: obj => "getById" in obj, get: obj => obj.getById }, metadata: _metadata }, null, _instanceExtraInitializers);
50
55
  __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
51
56
  OrganizationsController = _classThis = _classDescriptor.value;
52
57
  if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
53
58
  __runInitializers(_classThis, _classExtraInitializers);
54
59
  }
55
- orgService;
60
+ orgService = __runInitializers(this, _instanceExtraInitializers);
56
61
  constructor(app, database) {
57
62
  const orgService = new OrganizationService(database);
58
63
  super('organizations', app, orgService);
@@ -65,9 +70,13 @@ let OrganizationsController = (() => {
65
70
  }
66
71
  async getByName(req, res, next) {
67
72
  const { name } = req.params;
73
+ const userContext = req.userContext;
74
+ if (!userContext) {
75
+ throw new UnauthenticatedError();
76
+ }
68
77
  try {
69
78
  res.set('Content-Type', 'application/json');
70
- const entity = await this.orgService.findOne(req.userContext, {
79
+ const entity = await this.orgService.findOne(userContext, {
71
80
  filters: { name: { contains: name } },
72
81
  });
73
82
  if (!entity)
@@ -81,9 +90,13 @@ let OrganizationsController = (() => {
81
90
  }
82
91
  async getByCode(req, res, next) {
83
92
  const { code } = req.params;
93
+ const userContext = req.userContext;
94
+ if (!userContext) {
95
+ throw new UnauthenticatedError();
96
+ }
84
97
  try {
85
98
  res.set('Content-Type', 'application/json');
86
- const entity = await this.orgService.findOne(req.userContext, {
99
+ const entity = await this.orgService.findOne(userContext, {
87
100
  filters: { code: { eq: code } },
88
101
  });
89
102
  if (!entity)
@@ -95,6 +108,20 @@ let OrganizationsController = (() => {
95
108
  return;
96
109
  }
97
110
  }
111
+ async getById(req, res, next) {
112
+ const userContext = req.userContext;
113
+ if (!userContext) {
114
+ throw new UnauthenticatedError();
115
+ }
116
+ if (String(req.params.id) === String(userContext.user._orgId)) {
117
+ return super.getById(req, res, next);
118
+ }
119
+ const systemUserContext = getSystemUserContext();
120
+ if (String(userContext.user._orgId) === String(systemUserContext.user._orgId)) {
121
+ return super.getById(req, res, next);
122
+ }
123
+ throw new BadRequestError('User is not authorized to access this organization');
124
+ }
98
125
  };
99
126
  return OrganizationsController = _classThis;
100
127
  })();
@@ -1,7 +1,7 @@
1
1
  import { IEntity, IQueryOptions, IUserContext } from '@loomcore/common/models';
2
2
  import { Operation } from '../databases/index.js';
3
- export type PrepareQueryCustomFunction = (userContext: IUserContext | undefined, queryObject: IQueryOptions, operations: Operation[]) => {
4
- queryObject: IQueryOptions;
3
+ export type PrepareQueryCustomFunction = (userContext: IUserContext | undefined, queryOptions: IQueryOptions, operations: Operation[]) => {
4
+ queryOptions: IQueryOptions;
5
5
  operations: Operation[];
6
6
  };
7
7
  export type PostProcessEntityCustomFunction<TIn extends IEntity, TOut extends IEntity> = (userContext: IUserContext, entity: TIn) => TOut;
@@ -0,0 +1,3 @@
1
+ export declare function getValue(entity: any): boolean | undefined;
2
+ export declare function hasAllowAnonymous(entity: any): boolean;
3
+ export declare function AllowAnonymous(): (target: Function, _context: ClassDecoratorContext | ClassMethodDecoratorContext) => void;
@@ -0,0 +1,13 @@
1
+ const ALLOW_ANONYMOUS_METADATA_KEY = Symbol('auth-decorator:allow-anonymous');
2
+ export function getValue(entity) {
3
+ const value = Reflect.getMetadata(ALLOW_ANONYMOUS_METADATA_KEY, entity);
4
+ return value;
5
+ }
6
+ export function hasAllowAnonymous(entity) {
7
+ return Reflect.hasOwnMetadata(ALLOW_ANONYMOUS_METADATA_KEY, entity);
8
+ }
9
+ export function AllowAnonymous() {
10
+ return function (target, _context) {
11
+ Reflect.defineMetadata(ALLOW_ANONYMOUS_METADATA_KEY, true, target);
12
+ };
13
+ }
@@ -0,0 +1,7 @@
1
+ export type MatchMode = 'all' | 'any';
2
+ export interface IAuthRequirement {
3
+ features?: string[];
4
+ matchMode?: MatchMode;
5
+ denyOnImpersonation?: boolean;
6
+ requireMetaOrg?: boolean;
7
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,13 +1,9 @@
1
1
  import 'reflect-metadata';
2
- export declare const AUTH_METADATA_KEY: unique symbol;
3
- export type MatchMode = 'all' | 'any';
4
- export interface AuthRequirement {
5
- features: string[];
6
- mode: MatchMode;
7
- allowAnonymous?: boolean;
8
- }
9
- export declare function Authorize(features?: string | string[], options?: {
10
- all?: boolean;
11
- }): (value: Function, _context: ClassDecoratorContext | ClassMethodDecoratorContext) => void;
12
- export declare function AllowAnonymous(): (value: Function, _context: ClassDecoratorContext | ClassMethodDecoratorContext) => void;
13
- export declare function resolveAuthRequirement(controllerConstructor: Function, prototype: any, propertyKey: string): AuthRequirement | undefined;
2
+ import { IAuthRequirement, MatchMode } from './index.js';
3
+ export declare function getAuthRequirement(entity: any): IAuthRequirement | undefined;
4
+ export declare function hasAuthRequirement(entity: any): boolean;
5
+ type AuthorizeDecorator = (target: Function, _context: ClassDecoratorContext | ClassMethodDecoratorContext) => void;
6
+ export declare function Authorize(): AuthorizeDecorator;
7
+ export declare function Authorize(authRequirement: IAuthRequirement): AuthorizeDecorator;
8
+ export declare function Authorize(features: string | string[], matchMode?: MatchMode): AuthorizeDecorator;
9
+ export {};
@@ -1,33 +1,22 @@
1
1
  import 'reflect-metadata';
2
- export const AUTH_METADATA_KEY = Symbol('authorize:features');
3
- export function Authorize(features = [], options = {}) {
4
- const requirement = {
5
- features: Array.isArray(features) ? features : [features],
6
- mode: options.all ? 'all' : 'any',
7
- };
8
- return function (value, _context) {
9
- Reflect.defineMetadata(AUTH_METADATA_KEY, requirement, value);
10
- };
2
+ const AUTHORIZE_METADATA_KEY = Symbol('auth-decorator:authorize');
3
+ export function getAuthRequirement(entity) {
4
+ const value = Reflect.getMetadata(AUTHORIZE_METADATA_KEY, entity);
5
+ return value;
11
6
  }
12
- export function AllowAnonymous() {
13
- return function (value, _context) {
14
- const metadataValue = {
15
- features: [],
16
- mode: 'any',
17
- allowAnonymous: true,
18
- };
19
- Reflect.defineMetadata(AUTH_METADATA_KEY, metadataValue, value);
20
- };
7
+ export function hasAuthRequirement(entity) {
8
+ return Reflect.hasOwnMetadata(AUTHORIZE_METADATA_KEY, entity);
21
9
  }
22
- export function resolveAuthRequirement(controllerConstructor, prototype, propertyKey) {
23
- const method = prototype[propertyKey];
24
- if (typeof method === 'function') {
25
- const methodLevel = Reflect.getMetadata(AUTH_METADATA_KEY, method);
26
- if (methodLevel)
27
- return methodLevel;
28
- }
29
- const classLevel = Reflect.getMetadata(AUTH_METADATA_KEY, controllerConstructor);
30
- if (classLevel)
31
- return classLevel;
32
- return undefined;
10
+ export function Authorize(authRequirementOrFeatures, matchMode) {
11
+ return function (target, _context) {
12
+ let authRequirement;
13
+ if (typeof authRequirementOrFeatures === 'string' || Array.isArray(authRequirementOrFeatures)) {
14
+ const featureList = Array.isArray(authRequirementOrFeatures) ? authRequirementOrFeatures : [authRequirementOrFeatures];
15
+ authRequirement = { features: featureList, matchMode: matchMode ?? 'any' };
16
+ }
17
+ else {
18
+ authRequirement = authRequirementOrFeatures;
19
+ }
20
+ Reflect.defineMetadata(AUTHORIZE_METADATA_KEY, authRequirement, target);
21
+ };
33
22
  }
@@ -1 +1,3 @@
1
- export { Authorize, AllowAnonymous } from './authorize.decorator.js';
1
+ export { Authorize } from './authorize.decorator.js';
2
+ export { AllowAnonymous } from './allow-anonymous.decorator.js';
3
+ export type { IAuthRequirement, MatchMode } from './auth-requirement.interface.js';
@@ -1 +1,2 @@
1
- export { Authorize, AllowAnonymous } from './authorize.decorator.js';
1
+ export { Authorize } from './authorize.decorator.js';
2
+ export { AllowAnonymous } from './allow-anonymous.decorator.js';
@@ -0,0 +1 @@
1
+ export declare function resolveAllowAnonymous(controllerConstructor: Function, prototype: any, propertyKey: string): boolean | undefined;
@@ -0,0 +1,6 @@
1
+ import { getValue } from './allow-anonymous.decorator.js';
2
+ import { resolveAuthDecoratorTarget } from './resolve-auth-decorator-target.util.js';
3
+ export function resolveAllowAnonymous(controllerConstructor, prototype, propertyKey) {
4
+ const target = resolveAuthDecoratorTarget(controllerConstructor, prototype, propertyKey);
5
+ return getValue(target);
6
+ }
@@ -0,0 +1 @@
1
+ export declare function resolveAuthDecoratorTarget(controllerConstructor: Function, prototype: any, propertyKey: string): Function;
@@ -0,0 +1,9 @@
1
+ import { hasAllowAnonymous } from './allow-anonymous.decorator.js';
2
+ import { hasAuthRequirement } from './authorize.decorator.js';
3
+ export function resolveAuthDecoratorTarget(controllerConstructor, prototype, propertyKey) {
4
+ const method = prototype?.[propertyKey];
5
+ if (typeof method === 'function' && (hasAuthRequirement(method) || hasAllowAnonymous(method))) {
6
+ return method;
7
+ }
8
+ return controllerConstructor;
9
+ }
@@ -0,0 +1,2 @@
1
+ import { IAuthRequirement } from './auth-requirement.interface.js';
2
+ export declare function resolveAuthRequirement(controllerConstructor: Function, prototype: any, propertyKey: string): IAuthRequirement | undefined;
@@ -0,0 +1,6 @@
1
+ import { getAuthRequirement } from './authorize.decorator.js';
2
+ import { resolveAuthDecoratorTarget } from './resolve-auth-decorator-target.util.js';
3
+ export function resolveAuthRequirement(controllerConstructor, prototype, propertyKey) {
4
+ const target = resolveAuthDecoratorTarget(controllerConstructor, prototype, propertyKey);
5
+ return getAuthRequirement(target);
6
+ }
@@ -1,7 +1,7 @@
1
1
  import { CustomError } from '@loomcore/common/errors';
2
2
  export declare class UnauthorizedError extends CustomError {
3
3
  statusCode: number;
4
- constructor(missing?: string[]);
4
+ constructor(message?: string);
5
5
  serializeErrors(): {
6
6
  message: string;
7
7
  }[];
@@ -1,10 +1,8 @@
1
1
  import { CustomError } from '@loomcore/common/errors';
2
2
  export class UnauthorizedError extends CustomError {
3
3
  statusCode = 403;
4
- constructor(missing) {
5
- super(missing?.length
6
- ? `Missing required feature(s): ${missing.join(', ')}`
7
- : 'Unauthorized');
4
+ constructor(message) {
5
+ super(message ?? 'Unauthorized');
8
6
  Object.setPrototypeOf(this, UnauthorizedError.prototype);
9
7
  }
10
8
  serializeErrors() {
@@ -1,3 +1,3 @@
1
1
  import type { RequestHandler } from 'express';
2
- import { AuthRequirement } from '../../decorators/authorize.decorator.js';
3
- export declare function buildAuthGuard(requirement: AuthRequirement | undefined): RequestHandler;
2
+ import { IAuthRequirement } from '../../decorators/auth-requirement.interface.js';
3
+ export declare function buildAuthGuard(requirement: IAuthRequirement | undefined): RequestHandler;
@@ -1,23 +1,28 @@
1
1
  import { UnauthorizedError } from '../../errors/index.js';
2
2
  import { authenticateRequest } from './authenticate-request.js';
3
+ import { getSystemUserContext } from '@loomcore/common/models';
3
4
  export function buildAuthGuard(requirement) {
4
5
  return (req, _res, next) => {
5
- if (requirement?.allowAnonymous) {
6
- return next();
7
- }
8
6
  const userContext = authenticateRequest(req);
9
7
  req.userContext = userContext;
10
- if (!requirement || requirement.features.length === 0) {
11
- return next();
8
+ if (requirement?.denyOnImpersonation && userContext.impersonatorId !== undefined) {
9
+ throw new UnauthorizedError('Unauthorized: Endpoint requires non-impersonated user.');
10
+ }
11
+ const systemUserContext = getSystemUserContext();
12
+ if (requirement?.requireMetaOrg && userContext.user._orgId !== systemUserContext.user._orgId) {
13
+ throw new UnauthorizedError('Unauthorized: Endpoint requires meta organization user.');
12
14
  }
13
- const userFeatures = new Set(userContext.features);
14
- const hasAccess = requirement.mode === 'all'
15
- ? requirement.features.every((f) => userFeatures.has(f))
16
- : requirement.features.some((f) => userFeatures.has(f));
17
- if (!hasAccess) {
18
- const missing = requirement.features.filter((f) => !userFeatures.has(f));
19
- throw new UnauthorizedError(missing);
15
+ if (requirement?.features?.length) {
16
+ const userFeatures = new Set(userContext.features);
17
+ const hasFeatures = requirement.matchMode === 'all'
18
+ ? requirement.features?.every((f) => userFeatures.has(f))
19
+ : requirement.features?.some((f) => userFeatures.has(f));
20
+ if (!hasFeatures) {
21
+ const missing = requirement.features?.filter((f) => !userFeatures.has(f));
22
+ throw new UnauthorizedError(`Unauthorized: Missing required feature(s): ${missing?.join(', ')}`);
23
+ }
20
24
  }
21
25
  next();
22
26
  };
23
27
  }
28
+ ;
@@ -8,8 +8,8 @@ export interface IGenericApiService<T extends IEntity> {
8
8
  getModelSpec(): IModelSpec;
9
9
  validate(doc: any, isPartial?: boolean): ValueError[] | null;
10
10
  validateMany(docs: any[], isPartial?: boolean): ValueError[] | null;
11
- prepareQuery(userContext: IUserContext | undefined, queryObject: IQueryOptions, operations: Operation[]): {
12
- queryObject: IQueryOptions;
11
+ prepareQuery(userContext: IUserContext | undefined, queryOptions: IQueryOptions, operations: Operation[]): {
12
+ queryOptions: IQueryOptions;
13
13
  operations: Operation[];
14
14
  };
15
15
  preProcessEntity(userContext: IUserContext, entity: Partial<T>, isCreate: boolean, allowId: boolean): Promise<Partial<T>>;
@@ -15,8 +15,8 @@ export declare class GenericApiService<T extends IEntity> implements IGenericApi
15
15
  getModelSpec(): IModelSpec;
16
16
  getAll(userContext: IUserContext): Promise<T[]>;
17
17
  getAll<TCustom extends IEntity>(userContext: IUserContext, prepareQueryCustom: PrepareQueryCustomFunction, postProcessEntityCustom: PostProcessEntityCustomFunction<T, TCustom>): Promise<TCustom[]>;
18
- prepareQuery(userContext: IUserContext | undefined, queryObject: IQueryOptions, operations: Operation[]): {
19
- queryObject: IQueryOptions;
18
+ prepareQuery(userContext: IUserContext | undefined, queryOptions: IQueryOptions, operations: Operation[]): {
19
+ queryOptions: IQueryOptions;
20
20
  operations: Operation[];
21
21
  };
22
22
  validate(doc: any, isPartial?: boolean): ValueError[] | null;
@@ -35,9 +35,9 @@ export declare class GenericApiService<T extends IEntity> implements IGenericApi
35
35
  fullUpdateById(userContext: IUserContext, id: AppIdType, entity: T): Promise<T>;
36
36
  partialUpdateById(userContext: IUserContext, id: AppIdType, entity: Partial<T>): Promise<T>;
37
37
  partialUpdateByIdWithoutPreAndPostProcessing(userContext: IUserContext, id: AppIdType, entity: Partial<T>): Promise<T>;
38
- update(userContext: IUserContext, queryObject: IQueryOptions, entity: Partial<T>): Promise<T[]>;
38
+ update(userContext: IUserContext, queryOptions: IQueryOptions, entity: Partial<T>): Promise<T[]>;
39
39
  deleteById(userContext: IUserContext, id: AppIdType): Promise<DeleteResult>;
40
- deleteMany(userContext: IUserContext, queryObject: IQueryOptions): Promise<DeleteResult>;
41
- find(userContext: IUserContext, queryObject: IQueryOptions): Promise<T[]>;
42
- findOne(userContext: IUserContext, queryObject: IQueryOptions): Promise<T | null>;
40
+ deleteMany(userContext: IUserContext, queryOptions: IQueryOptions): Promise<DeleteResult>;
41
+ find(userContext: IUserContext, queryOptions: IQueryOptions): Promise<T[]>;
42
+ findOne(userContext: IUserContext, queryOptions: IQueryOptions): Promise<T | null>;
43
43
  }
@@ -24,9 +24,7 @@ export class GenericApiService {
24
24
  if (prepareQueryCustom) {
25
25
  operations = prepareQueryCustom(userContext, {}, []).operations;
26
26
  }
27
- else {
28
- operations = this.prepareQuery(userContext, {}, []).operations;
29
- }
27
+ operations = this.prepareQuery(userContext, {}, operations).operations;
30
28
  const entities = await this.database.getAll(operations, this.pluralResourceName);
31
29
  return entities.map((entity) => {
32
30
  const dbProcessed = this.postProcessEntity(userContext, entity);
@@ -36,8 +34,8 @@ export class GenericApiService {
36
34
  return dbProcessed;
37
35
  });
38
36
  }
39
- prepareQuery(userContext, queryObject, operations) {
40
- return { queryObject, operations };
37
+ prepareQuery(userContext, queryOptions, operations) {
38
+ return { queryOptions, operations };
41
39
  }
42
40
  validate(doc, isPartial = false) {
43
41
  return entityUtils.validate(this.modelSpec, doc, isPartial);
@@ -77,15 +75,12 @@ export class GenericApiService {
77
75
  return this.database.postProcessEntity(entity, this.modelSpec.fullSchema);
78
76
  }
79
77
  async get(userContext, queryOptions = { ...DefaultQueryOptions }, prepareQueryCustom, postProcessEntityCustom) {
80
- const preparedOptions = this.prepareQueryOptions(userContext, queryOptions);
81
78
  let operations = [];
82
79
  if (prepareQueryCustom) {
83
- operations = prepareQueryCustom(userContext, {}, []).operations;
80
+ ({ queryOptions, operations } = prepareQueryCustom(userContext, {}, []));
84
81
  }
85
- else {
86
- operations = this.prepareQuery(userContext, {}, []).operations;
87
- }
88
- const pagedResult = await this.database.get(operations, preparedOptions, this.modelSpec, this.pluralResourceName);
82
+ ({ queryOptions, operations } = this.prepareQuery(userContext, queryOptions, operations));
83
+ const pagedResult = await this.database.get(operations, queryOptions, this.modelSpec, this.pluralResourceName);
89
84
  const transformedEntities = (pagedResult.entities || []).map((entity) => {
90
85
  const transformedEntity = this.postProcessEntity(userContext, entity);
91
86
  if (postProcessEntityCustom) {
@@ -103,18 +98,12 @@ export class GenericApiService {
103
98
  }
104
99
  async getById(userContext, id, prepareQueryCustom, postProcessEntityCustom) {
105
100
  let operations = [];
106
- let queryObject = {};
101
+ let queryOptions = {};
107
102
  if (prepareQueryCustom) {
108
- const result = prepareQueryCustom(userContext, {}, []);
109
- operations = result.operations;
110
- queryObject = result.queryObject;
111
- }
112
- else {
113
- const result = this.prepareQuery(userContext, {}, []);
114
- operations = result.operations;
115
- queryObject = result.queryObject;
103
+ ({ operations, queryOptions } = prepareQueryCustom(userContext, {}, []));
116
104
  }
117
- const entity = await this.database.getById(operations, queryObject, id, this.pluralResourceName);
105
+ ({ operations, queryOptions } = this.prepareQuery(userContext, queryOptions, operations));
106
+ const entity = await this.database.getById(operations, queryOptions, id, this.pluralResourceName);
118
107
  if (!entity) {
119
108
  throw new IdNotFoundError();
120
109
  }
@@ -155,14 +144,14 @@ export class GenericApiService {
155
144
  return [];
156
145
  }
157
146
  const preparedEntities = await Promise.all(entities.map((entity) => this.preProcessEntity(userContext, entity, false, true)));
158
- const { queryObject, operations } = this.prepareQuery(userContext, {}, []);
159
- const rawUpdatedEntities = await this.database.batchUpdate(preparedEntities, operations, queryObject, this.pluralResourceName);
147
+ const { queryOptions, operations } = this.prepareQuery(userContext, {}, []);
148
+ const rawUpdatedEntities = await this.database.batchUpdate(preparedEntities, operations, queryOptions, this.pluralResourceName);
160
149
  const updatedEntities = rawUpdatedEntities.map((entity) => this.postProcessEntity(userContext, entity));
161
150
  return updatedEntities;
162
151
  }
163
152
  async fullUpdateById(userContext, id, entity) {
164
- const { operations, queryObject } = this.prepareQuery(userContext, {}, []);
165
- const existingEntity = await this.database.getById(operations, queryObject, id, this.pluralResourceName);
153
+ const { operations, queryOptions } = this.prepareQuery(userContext, {}, []);
154
+ const existingEntity = await this.database.getById(operations, queryOptions, id, this.pluralResourceName);
166
155
  if (!existingEntity) {
167
156
  throw new IdNotFoundError();
168
157
  }
@@ -188,8 +177,8 @@ export class GenericApiService {
188
177
  const rawUpdatedEntity = await this.database.partialUpdateById([], id, preparedEntity, this.pluralResourceName);
189
178
  return this.database.postProcessEntity(rawUpdatedEntity, this.modelSpec.fullSchema);
190
179
  }
191
- async update(userContext, queryObject, entity) {
192
- const { queryObject: preparedQuery, operations } = this.prepareQuery(userContext, queryObject, []);
180
+ async update(userContext, queryOptions, entity) {
181
+ const { queryOptions: preparedQuery, operations } = this.prepareQuery(userContext, queryOptions, []);
193
182
  const preparedEntity = await this.preProcessEntity(userContext, entity, false, true);
194
183
  const rawUpdatedEntities = await this.database.update(preparedQuery, preparedEntity, operations, this.pluralResourceName);
195
184
  const updatedEntities = rawUpdatedEntities.map((entity) => this.postProcessEntity(userContext, entity));
@@ -202,18 +191,18 @@ export class GenericApiService {
202
191
  }
203
192
  return deleteResult;
204
193
  }
205
- async deleteMany(userContext, queryObject) {
206
- const { queryObject: preparedQuery, operations } = this.prepareQuery(userContext, queryObject, []);
194
+ async deleteMany(userContext, queryOptions) {
195
+ const { queryOptions: preparedQuery, operations } = this.prepareQuery(userContext, queryOptions, []);
207
196
  const deleteResult = await this.database.deleteMany(preparedQuery, this.pluralResourceName);
208
197
  return deleteResult;
209
198
  }
210
- async find(userContext, queryObject) {
211
- const { queryObject: preparedQuery, operations } = this.prepareQuery(userContext, queryObject, []);
199
+ async find(userContext, queryOptions) {
200
+ const { queryOptions: preparedQuery, operations } = this.prepareQuery(userContext, queryOptions, []);
212
201
  const rawEntities = await this.database.find(preparedQuery, this.pluralResourceName);
213
202
  return rawEntities.map((entity) => this.postProcessEntity(userContext, entity));
214
203
  }
215
- async findOne(userContext, queryObject) {
216
- const { queryObject: preparedQuery, operations } = this.prepareQuery(userContext, queryObject, []);
204
+ async findOne(userContext, queryOptions) {
205
+ const { queryOptions: preparedQuery, operations } = this.prepareQuery(userContext, queryOptions, []);
217
206
  const rawEntity = await this.database.findOne(preparedQuery, this.pluralResourceName);
218
207
  return rawEntity ? this.postProcessEntity(userContext, rawEntity) : null;
219
208
  }
@@ -6,7 +6,7 @@ export declare class MultiTenantApiService<T extends IEntity> extends GenericApi
6
6
  private tenantDecorator?;
7
7
  constructor(database: IDatabase, pluralResourceName: string, singularResourceName: string, modelSpec: IModelSpec);
8
8
  prepareQuery(userContext: IUserContext, queryOptions: IQueryOptions, operations: Operation[]): {
9
- queryObject: IQueryOptions;
9
+ queryOptions: IQueryOptions;
10
10
  operations: Operation[];
11
11
  };
12
12
  preProcessEntity(userContext: IUserContext, entity: Partial<T>, isCreate: boolean, allowId?: boolean): Promise<Partial<T>>;
@@ -2,6 +2,8 @@ import { getSystemUserId } from '@loomcore/common/validation';
2
2
  import { config } from '../config/base-api-config.js';
3
3
  import { BadRequestError } from '../errors/bad-request.error.js';
4
4
  import { ServerError } from '../errors/index.js';
5
+ import { isMetaOrgAdmin } from '../utils/auth/is-admin.util.js';
6
+ import { isSystemUser } from '../utils/auth/is-system-user.util.js';
5
7
  import { GenericApiService } from './generic-api-service/generic-api.service.js';
6
8
  import { TenantQueryDecorator } from './tenant-query-decorator.js';
7
9
  export class MultiTenantApiService extends GenericApiService {
@@ -23,8 +25,8 @@ export class MultiTenantApiService extends GenericApiService {
23
25
  if (!this.tenantDecorator) {
24
26
  throw new ServerError('A valid tenantDecorator was not provided to MultiTenantApiService.prepareQuery');
25
27
  }
26
- const queryObject = this.tenantDecorator.applyTenantToQuery(userContext, queryOptions, this.pluralResourceName);
27
- return { queryObject, operations };
28
+ queryOptions = this.tenantDecorator.applyTenantToQuery(userContext, queryOptions, this.pluralResourceName);
29
+ return { queryOptions, operations };
28
30
  }
29
31
  async preProcessEntity(userContext, entity, isCreate, allowId = false) {
30
32
  if (!config?.app?.isMultiTenant) {
@@ -34,9 +36,12 @@ export class MultiTenantApiService extends GenericApiService {
34
36
  throw new BadRequestError('A valid userContext was not provided to MultiTenantApiService.prepareEntity');
35
37
  }
36
38
  const preparedEntity = await super.preProcessEntity(userContext, entity, isCreate, allowId);
37
- if (isCreate && userContext.user._id !== getSystemUserId()) {
39
+ if (isCreate && !isSystemUser(userContext) && !isMetaOrgAdmin(userContext)) {
38
40
  preparedEntity._orgId = userContext.user._orgId;
39
41
  }
42
+ if (isCreate && isMetaOrgAdmin(userContext)) {
43
+ preparedEntity._orgId = preparedEntity._orgId ?? userContext.user._orgId;
44
+ }
40
45
  return preparedEntity;
41
46
  }
42
47
  }
@@ -1,7 +1,7 @@
1
1
  import { OrganizationSpec, } from '@loomcore/common/models';
2
2
  import { BadRequestError } from '../errors/index.js';
3
- import { GenericApiService } from './generic-api-service/generic-api.service.js';
4
3
  import { OrganizationDomainService } from './organization-domain.service.js';
4
+ import { GenericApiService } from './generic-api-service/generic-api.service.js';
5
5
  export class OrganizationService extends GenericApiService {
6
6
  organizationDomainService;
7
7
  constructor(database) {
@@ -36,12 +36,14 @@ export class OrganizationService extends GenericApiService {
36
36
  return orgId;
37
37
  }
38
38
  async getMetaOrg(userContext) {
39
- const org = await this.findOne(userContext, {
39
+ return this.findOne(userContext, {
40
40
  filters: { isMetaOrg: { eq: true } },
41
41
  });
42
- return org;
43
42
  }
44
43
  async findByDomain(userContext, domain) {
44
+ if (!domain) {
45
+ throw new BadRequestError('Missing required fields: domain is required.');
46
+ }
45
47
  const organizationDomain = await this.organizationDomainService.findOne(userContext, {
46
48
  filters: { domain: { eq: domain } },
47
49
  });
@@ -7,7 +7,7 @@ export declare const DEFAULT_TENANT_OPTIONS: ITenantQueryOptions;
7
7
  export declare class TenantQueryDecorator {
8
8
  private options;
9
9
  constructor(options?: Partial<ITenantQueryOptions>);
10
- applyTenantToQuery(userContext: IUserContext, queryObject: IQueryOptions, collectionName: string): IQueryOptions;
10
+ applyTenantToQuery(userContext: IUserContext, queryOptions: IQueryOptions, collectionName: string): IQueryOptions;
11
11
  applyTenantToQueryOptions(userContext: IUserContext, queryOptions: IQueryOptions, collectionName: string): IQueryOptions;
12
12
  applyTenantToEntity<T extends IEntity>(userContext: IUserContext, entity: T, collectionName: string): T;
13
13
  getOrgIdField(): string;
@@ -1,4 +1,6 @@
1
1
  import { ServerError } from '../errors/index.js';
2
+ import { isSystemUser } from '../utils/auth/is-system-user.util.js';
3
+ import { isMetaOrgAdmin } from '../utils/auth/is-admin.util.js';
2
4
  export const DEFAULT_TENANT_OPTIONS = {
3
5
  orgIdField: '_orgId',
4
6
  excludedCollections: [],
@@ -8,16 +10,17 @@ export class TenantQueryDecorator {
8
10
  constructor(options = {}) {
9
11
  this.options = { ...DEFAULT_TENANT_OPTIONS, ...options };
10
12
  }
11
- applyTenantToQuery(userContext, queryObject, collectionName) {
12
- let result = queryObject;
13
+ applyTenantToQuery(userContext, queryOptions, collectionName) {
13
14
  const shouldApplyTenantFilter = !this.options.excludedCollections?.includes(collectionName) &&
14
- userContext?.user._orgId;
15
+ userContext.user._orgId &&
16
+ !isSystemUser(userContext) &&
17
+ !isMetaOrgAdmin(userContext);
15
18
  if (shouldApplyTenantFilter) {
16
19
  const orgIdField = this.options.orgIdField || '_orgId';
17
- result = {
18
- ...queryObject,
20
+ queryOptions = {
21
+ ...queryOptions,
19
22
  filters: {
20
- ...queryObject.filters,
23
+ ...queryOptions.filters,
21
24
  [orgIdField]: { eq: userContext.user._orgId },
22
25
  },
23
26
  };
@@ -27,7 +30,7 @@ export class TenantQueryDecorator {
27
30
  throw new ServerError('No _orgId found in userContext');
28
31
  }
29
32
  }
30
- return result;
33
+ return queryOptions;
31
34
  }
32
35
  applyTenantToQueryOptions(userContext, queryOptions, collectionName) {
33
36
  const result = { ...queryOptions };
@@ -1,8 +1,7 @@
1
- import { getSystemUserId } from '@loomcore/common/validation';
2
1
  import moment from 'moment';
3
2
  export function auditForCreate(userContext, doc) {
4
3
  const now = moment().utc().toDate();
5
- const userId = userContext.user?._id ?? getSystemUserId();
4
+ const userId = userContext.impersonatorId ?? userContext.user?._id;
6
5
  doc._created = now;
7
6
  doc._createdBy = userId;
8
7
  }
@@ -1,7 +1,6 @@
1
- import { getSystemUserId } from '@loomcore/common/validation';
2
1
  import moment from 'moment';
3
2
  export function auditForUpdate(userContext, doc) {
4
- const userId = userContext.user?._id ?? getSystemUserId();
3
+ const userId = userContext.impersonatorId ?? userContext.user?._id;
5
4
  doc._updated = moment().utc().toDate();
6
5
  doc._updatedBy = userId;
7
6
  }
@@ -1,2 +1,2 @@
1
- import { RequestHandler } from 'express';
1
+ import type { RequestHandler } from 'express';
2
2
  export declare function authorizeMethod(instance: object, methodName: string): RequestHandler;
@@ -1,8 +1,19 @@
1
- import { resolveAuthRequirement } from '../../decorators/authorize.decorator.js';
1
+ import { hasAllowAnonymous } from '../../decorators/allow-anonymous.decorator.js';
2
+ import { hasAuthRequirement } from '../../decorators/authorize.decorator.js';
3
+ import { resolveAllowAnonymous } from '../../decorators/resolve-allow-anonymous.util.js';
4
+ import { resolveAuthRequirement } from '../../decorators/resolve-auth-requirement.util.js';
2
5
  import { buildAuthGuard } from '../../middleware/authorize/authorize.middleware.js';
3
6
  export function authorizeMethod(instance, methodName) {
4
7
  const constructor = instance.constructor;
5
8
  const prototype = Object.getPrototypeOf(instance);
6
- const requirement = resolveAuthRequirement(constructor, prototype, methodName);
7
- return buildAuthGuard(requirement);
9
+ const method = prototype?.[methodName];
10
+ const methodHasBoth = typeof method === 'function' && hasAllowAnonymous(method) && hasAuthRequirement(method);
11
+ const classHasBoth = hasAllowAnonymous(constructor) && hasAuthRequirement(constructor);
12
+ if (methodHasBoth || classHasBoth) {
13
+ throw new Error('Allow anonymous and authorize decorators cannot be used together.');
14
+ }
15
+ if (resolveAllowAnonymous(constructor, prototype, methodName)) {
16
+ return (_req, _res, next) => next();
17
+ }
18
+ return buildAuthGuard(resolveAuthRequirement(constructor, prototype, methodName));
8
19
  }
@@ -0,0 +1,6 @@
1
+ import { type ITokenResponse, type IUserContext } from '@loomcore/common/models';
2
+ import type { AppIdType } from '@loomcore/common/types';
3
+ import type { IDatabase } from '../../databases/models/index.js';
4
+ import { AuthorizationService } from '../../services/authorization.service.js';
5
+ import { UserService } from '../../services/user.service.js';
6
+ export declare function impersonateUser(database: IDatabase, impersonatorUserContext: IUserContext, targetUserId: AppIdType, refreshToken: string, userService?: UserService, authorizationsService?: AuthorizationService): Promise<ITokenResponse>;
@@ -0,0 +1,35 @@
1
+ import { BadRequestError } from '../../errors/index.js';
2
+ import { AuthorizationService } from '../../services/authorization.service.js';
3
+ import { UserService } from '../../services/user.service.js';
4
+ import { generateJwt, IMPERSONATION_JWT_EXPIRATION_IN_SECONDS } from '../jwt.utils.js';
5
+ import { getExpiresOnFromSeconds } from './get-expires-on-from-seconds.util.js';
6
+ import { isAdmin } from './is-admin.util.js';
7
+ export async function impersonateUser(database, impersonatorUserContext, targetUserId, refreshToken, userService = new UserService(database), authorizationsService = new AuthorizationService(database)) {
8
+ if (impersonatorUserContext.impersonatorId) {
9
+ throw new BadRequestError('Already impersonating.');
10
+ }
11
+ if (impersonatorUserContext.user._id === targetUserId) {
12
+ throw new BadRequestError('Cannot impersonate yourself.');
13
+ }
14
+ const targetUser = await userService.findOne(impersonatorUserContext, {
15
+ filters: { _id: { eq: targetUserId } },
16
+ });
17
+ if (!targetUser) {
18
+ throw new BadRequestError('User not found');
19
+ }
20
+ const targetFeatures = await authorizationsService.getUserContextFeatures(targetUser);
21
+ const impersonatedUserContext = {
22
+ user: targetUser,
23
+ features: targetFeatures,
24
+ impersonatorId: impersonatorUserContext.user._id,
25
+ };
26
+ if (isAdmin(impersonatedUserContext)) {
27
+ throw new BadRequestError('Cannot impersonate admin user.');
28
+ }
29
+ const accessToken = generateJwt(impersonatedUserContext, IMPERSONATION_JWT_EXPIRATION_IN_SECONDS);
30
+ return {
31
+ accessToken,
32
+ refreshToken,
33
+ expiresOn: getExpiresOnFromSeconds(IMPERSONATION_JWT_EXPIRATION_IN_SECONDS),
34
+ };
35
+ }
@@ -11,7 +11,9 @@ export * from './get-device-id-from-cookie.util.js';
11
11
  export * from './get-expires-on-from-days.util.js';
12
12
  export * from './get-expires-on-from-minutes.util.js';
13
13
  export * from './get-expires-on-from-seconds.util.js';
14
+ export * from './impersonate-user.util.js';
14
15
  export * from './is-admin.util.js';
16
+ export * from './is-system-user.util.js';
15
17
  export * from './log-user-in.util.js';
16
18
  export * from './request-token-using-refresh-token.util.js';
17
19
  export * from './reset-password.util.js';
@@ -11,7 +11,9 @@ export * from './get-device-id-from-cookie.util.js';
11
11
  export * from './get-expires-on-from-days.util.js';
12
12
  export * from './get-expires-on-from-minutes.util.js';
13
13
  export * from './get-expires-on-from-seconds.util.js';
14
+ export * from './impersonate-user.util.js';
14
15
  export * from './is-admin.util.js';
16
+ export * from './is-system-user.util.js';
15
17
  export * from './log-user-in.util.js';
16
18
  export * from './request-token-using-refresh-token.util.js';
17
19
  export * from './reset-password.util.js';
@@ -1,2 +1,3 @@
1
- import type { IUserContext } from '@loomcore/common/models';
1
+ import { type IUserContext } from '@loomcore/common/models';
2
2
  export declare function isAdmin(userContext: IUserContext): boolean;
3
+ export declare function isMetaOrgAdmin(userContext: IUserContext): boolean;
@@ -1,3 +1,11 @@
1
+ import { getSystemUserContext } from '@loomcore/common/models';
1
2
  export function isAdmin(userContext) {
2
- return userContext.features.some((feature) => feature === 'admin' || feature === 'system');
3
+ return userContext.features.some((feature) => feature === 'admin');
4
+ }
5
+ export function isMetaOrgAdmin(userContext) {
6
+ if (!isAdmin(userContext)) {
7
+ return false;
8
+ }
9
+ const systemUserContext = getSystemUserContext();
10
+ return userContext.user._orgId === systemUserContext.user._orgId;
3
11
  }
@@ -0,0 +1,2 @@
1
+ import type { IUserContext } from '@loomcore/common/models';
2
+ export declare function isSystemUser(userContext: IUserContext): boolean;
@@ -0,0 +1,4 @@
1
+ import { getSystemUserId } from '@loomcore/common/validation';
2
+ export function isSystemUser(userContext) {
3
+ return userContext.user?._id === getSystemUserId();
4
+ }
@@ -1,2 +1,3 @@
1
1
  import type { IUserContext } from '@loomcore/common/models';
2
- export declare function generateJwt(userContext: IUserContext): string;
2
+ export declare const IMPERSONATION_JWT_EXPIRATION_IN_SECONDS = 3600;
3
+ export declare function generateJwt(userContext: IUserContext, expiresInSeconds?: number): string;
@@ -1,8 +1,9 @@
1
1
  import jwt from 'jsonwebtoken';
2
2
  import { getAuthConfig } from './auth/get-auth-config.util.js';
3
- export function generateJwt(userContext) {
3
+ export const IMPERSONATION_JWT_EXPIRATION_IN_SECONDS = 3600;
4
+ export function generateJwt(userContext, expiresInSeconds) {
4
5
  const authConfig = getAuthConfig();
5
- const jwtExpiryConfig = authConfig.jwtExpirationInSeconds;
6
+ const jwtExpiryConfig = expiresInSeconds ?? authConfig.jwtExpirationInSeconds;
6
7
  const jwtExpirationInSeconds = typeof jwtExpiryConfig === 'string'
7
8
  ? Number.parseInt(jwtExpiryConfig, 10)
8
9
  : jwtExpiryConfig;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loomcore/api",
3
- "version": "0.2.57",
3
+ "version": "0.2.58",
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": {
@@ -60,7 +60,7 @@
60
60
  "reflect-metadata": "^0.2.2"
61
61
  },
62
62
  "peerDependencies": {
63
- "@loomcore/common": "^0.0.91",
63
+ "@loomcore/common": "^0.0.92",
64
64
  "@sinclair/typebox": "0.34.33",
65
65
  "cookie-parser": "^1.4.6",
66
66
  "cors": "^2.8.5",