@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.
- package/LICENSE +201 -201
- package/dist/__tests__/common-test.utils.d.ts +2 -2
- package/dist/__tests__/common-test.utils.js +12 -11
- package/dist/__tests__/mongo-db.test-database.js +5 -1
- package/dist/controllers/auth.controller.d.ts +1 -0
- package/dist/controllers/auth.controller.js +39 -5
- package/dist/controllers/authorizations.controller.js +58 -6
- package/dist/controllers/organizations.controller.d.ts +4 -1
- package/dist/controllers/organizations.controller.js +39 -12
- package/dist/controllers/types.d.ts +2 -2
- package/dist/decorators/allow-anonymous.decorator.d.ts +3 -0
- package/dist/decorators/allow-anonymous.decorator.js +13 -0
- package/dist/decorators/auth-requirement.interface.d.ts +7 -0
- package/dist/decorators/auth-requirement.interface.js +1 -0
- package/dist/decorators/authorize.decorator.d.ts +8 -12
- package/dist/decorators/authorize.decorator.js +18 -29
- package/dist/decorators/index.d.ts +3 -1
- package/dist/decorators/index.js +2 -1
- package/dist/decorators/resolve-allow-anonymous.util.d.ts +1 -0
- package/dist/decorators/resolve-allow-anonymous.util.js +6 -0
- package/dist/decorators/resolve-auth-decorator-target.util.d.ts +1 -0
- package/dist/decorators/resolve-auth-decorator-target.util.js +9 -0
- package/dist/decorators/resolve-auth-requirement.util.d.ts +2 -0
- package/dist/decorators/resolve-auth-requirement.util.js +6 -0
- package/dist/errors/unauthorized.error.d.ts +1 -1
- package/dist/errors/unauthorized.error.js +2 -4
- package/dist/middleware/authorize/authorize.middleware.d.ts +2 -2
- package/dist/middleware/authorize/authorize.middleware.js +17 -12
- package/dist/services/generic-api-service/generic-api-service.interface.d.ts +2 -2
- package/dist/services/generic-api-service/generic-api.service.d.ts +6 -6
- package/dist/services/generic-api-service/generic-api.service.js +22 -33
- package/dist/services/multi-tenant-api.service.d.ts +1 -1
- package/dist/services/multi-tenant-api.service.js +8 -3
- package/dist/services/organization.service.js +5 -3
- package/dist/services/tenant-query-decorator.d.ts +1 -1
- package/dist/services/tenant-query-decorator.js +10 -7
- package/dist/services/utils/audit-for-create.util.js +1 -2
- package/dist/services/utils/audit-for-update.util.js +1 -2
- package/dist/utils/auth/authorize-method.util.d.ts +1 -1
- package/dist/utils/auth/authorize-method.util.js +14 -3
- package/dist/utils/auth/impersonate-user.util.d.ts +6 -0
- package/dist/utils/auth/impersonate-user.util.js +35 -0
- package/dist/utils/auth/index.d.ts +2 -0
- package/dist/utils/auth/index.js +2 -0
- package/dist/utils/auth/is-admin.util.d.ts +2 -1
- package/dist/utils/auth/is-admin.util.js +9 -1
- package/dist/utils/auth/is-system-user.util.d.ts +2 -0
- package/dist/utils/auth/is-system-user.util.js +4 -0
- package/dist/utils/jwt.utils.d.ts +2 -1
- package/dist/utils/jwt.utils.js +3 -2
- 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
|
-
|
|
27
|
-
return {
|
|
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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
12
|
-
let result = queryObject;
|
|
13
|
+
applyTenantToQuery(userContext, queryOptions, collectionName) {
|
|
13
14
|
const shouldApplyTenantFilter = !this.options.excludedCollections?.includes(collectionName) &&
|
|
14
|
-
userContext
|
|
15
|
+
userContext.user._orgId &&
|
|
16
|
+
!isSystemUser(userContext) &&
|
|
17
|
+
!isMetaOrgAdmin(userContext);
|
|
15
18
|
if (shouldApplyTenantFilter) {
|
|
16
19
|
const orgIdField = this.options.orgIdField || '_orgId';
|
|
17
|
-
|
|
18
|
-
...
|
|
20
|
+
queryOptions = {
|
|
21
|
+
...queryOptions,
|
|
19
22
|
filters: {
|
|
20
|
-
...
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
7
|
-
|
|
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';
|
package/dist/utils/auth/index.js
CHANGED
|
@@ -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,3 +1,11 @@
|
|
|
1
|
+
import { getSystemUserContext } from '@loomcore/common/models';
|
|
1
2
|
export function isAdmin(userContext) {
|
|
2
|
-
return userContext.features.some((feature) => feature === 'admin'
|
|
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
|
}
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import type { IUserContext } from '@loomcore/common/models';
|
|
2
|
-
export declare
|
|
2
|
+
export declare const IMPERSONATION_JWT_EXPIRATION_IN_SECONDS = 3600;
|
|
3
|
+
export declare function generateJwt(userContext: IUserContext, expiresInSeconds?: number): string;
|
package/dist/utils/jwt.utils.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import jwt from 'jsonwebtoken';
|
|
2
2
|
import { getAuthConfig } from './auth/get-auth-config.util.js';
|
|
3
|
-
export
|
|
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.
|
|
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.
|
|
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",
|