@loomcore/api 0.2.56 → 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 (51) hide show
  1. package/LICENSE +201 -201
  2. package/dist/__tests__/common-test.utils.d.ts +2 -2
  3. package/dist/__tests__/common-test.utils.js +12 -11
  4. package/dist/__tests__/mongo-db.test-database.js +5 -1
  5. package/dist/controllers/auth.controller.d.ts +1 -0
  6. package/dist/controllers/auth.controller.js +39 -5
  7. package/dist/controllers/authorizations.controller.js +58 -6
  8. package/dist/controllers/organizations.controller.d.ts +4 -1
  9. package/dist/controllers/organizations.controller.js +39 -12
  10. package/dist/controllers/types.d.ts +2 -2
  11. package/dist/decorators/allow-anonymous.decorator.d.ts +3 -0
  12. package/dist/decorators/allow-anonymous.decorator.js +13 -0
  13. package/dist/decorators/auth-requirement.interface.d.ts +7 -0
  14. package/dist/decorators/auth-requirement.interface.js +1 -0
  15. package/dist/decorators/authorize.decorator.d.ts +8 -12
  16. package/dist/decorators/authorize.decorator.js +18 -29
  17. package/dist/decorators/index.d.ts +3 -1
  18. package/dist/decorators/index.js +2 -1
  19. package/dist/decorators/resolve-allow-anonymous.util.d.ts +1 -0
  20. package/dist/decorators/resolve-allow-anonymous.util.js +6 -0
  21. package/dist/decorators/resolve-auth-decorator-target.util.d.ts +1 -0
  22. package/dist/decorators/resolve-auth-decorator-target.util.js +9 -0
  23. package/dist/decorators/resolve-auth-requirement.util.d.ts +2 -0
  24. package/dist/decorators/resolve-auth-requirement.util.js +6 -0
  25. package/dist/errors/unauthorized.error.d.ts +1 -1
  26. package/dist/errors/unauthorized.error.js +2 -4
  27. package/dist/middleware/authorize/authorize.middleware.d.ts +2 -2
  28. package/dist/middleware/authorize/authorize.middleware.js +17 -12
  29. package/dist/services/generic-api-service/generic-api-service.interface.d.ts +2 -2
  30. package/dist/services/generic-api-service/generic-api.service.d.ts +6 -6
  31. package/dist/services/generic-api-service/generic-api.service.js +22 -33
  32. package/dist/services/multi-tenant-api.service.d.ts +1 -1
  33. package/dist/services/multi-tenant-api.service.js +8 -3
  34. package/dist/services/organization.service.js +5 -3
  35. package/dist/services/tenant-query-decorator.d.ts +1 -1
  36. package/dist/services/tenant-query-decorator.js +10 -7
  37. package/dist/services/utils/audit-for-create.util.js +1 -2
  38. package/dist/services/utils/audit-for-update.util.js +1 -2
  39. package/dist/utils/auth/authorize-method.util.d.ts +1 -1
  40. package/dist/utils/auth/authorize-method.util.js +14 -3
  41. package/dist/utils/auth/impersonate-user.util.d.ts +6 -0
  42. package/dist/utils/auth/impersonate-user.util.js +35 -0
  43. package/dist/utils/auth/index.d.ts +2 -0
  44. package/dist/utils/auth/index.js +2 -0
  45. package/dist/utils/auth/is-admin.util.d.ts +2 -1
  46. package/dist/utils/auth/is-admin.util.js +9 -1
  47. package/dist/utils/auth/is-system-user.util.d.ts +2 -0
  48. package/dist/utils/auth/is-system-user.util.js +4 -0
  49. package/dist/utils/jwt.utils.d.ts +2 -1
  50. package/dist/utils/jwt.utils.js +3 -2
  51. package/package.json +2 -2
@@ -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.56",
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.90",
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",