@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.
- 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
|
@@ -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,
|
|
50
|
-
|
|
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
|
|
61
|
+
const systemUserContext = getSystemUserContext();
|
|
62
|
+
const existingMetaOrg = await organizationService.getMetaOrg(systemUserContext);
|
|
62
63
|
if (!existingMetaOrg) {
|
|
63
|
-
const metaOrgInsertResult = await organizationService.create(
|
|
64
|
+
const metaOrgInsertResult = await organizationService.create(systemUserContext, getTestMetaOrg());
|
|
64
65
|
if (metaOrgInsertResult) {
|
|
65
66
|
setTestMetaOrgId(metaOrgInsertResult._id);
|
|
66
|
-
await organizationDomainService.create(
|
|
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(
|
|
72
|
+
const existingDomain = await organizationDomainService.findOne(systemUserContext, { filters: { domain: { eq: TEST_META_ORG_DOMAIN } } });
|
|
72
73
|
if (!existingDomain) {
|
|
73
|
-
await organizationDomainService.create(
|
|
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(
|
|
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(
|
|
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,
|
|
275
|
+
const prepareQueryCustom = (userContext, queryOptions, operations) => {
|
|
275
276
|
return {
|
|
276
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
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
|
-
|
|
29
|
-
|
|
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(
|
|
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(
|
|
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,
|
|
4
|
-
|
|
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,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 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export
|
|
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
|
-
|
|
3
|
-
export function
|
|
4
|
-
const
|
|
5
|
-
|
|
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
|
|
13
|
-
return
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
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';
|
package/dist/decorators/index.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export { Authorize
|
|
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,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,10 +1,8 @@
|
|
|
1
1
|
import { CustomError } from '@loomcore/common/errors';
|
|
2
2
|
export class UnauthorizedError extends CustomError {
|
|
3
3
|
statusCode = 403;
|
|
4
|
-
constructor(
|
|
5
|
-
super(
|
|
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 {
|
|
3
|
-
export declare function buildAuthGuard(requirement:
|
|
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 (
|
|
11
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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,
|
|
12
|
-
|
|
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,
|
|
19
|
-
|
|
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,
|
|
38
|
+
update(userContext: IUserContext, queryOptions: IQueryOptions, entity: Partial<T>): Promise<T[]>;
|
|
39
39
|
deleteById(userContext: IUserContext, id: AppIdType): Promise<DeleteResult>;
|
|
40
|
-
deleteMany(userContext: IUserContext,
|
|
41
|
-
find(userContext: IUserContext,
|
|
42
|
-
findOne(userContext: IUserContext,
|
|
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
|
-
|
|
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,
|
|
40
|
-
return {
|
|
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, {}, [])
|
|
80
|
+
({ queryOptions, operations } = prepareQueryCustom(userContext, {}, []));
|
|
84
81
|
}
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
101
|
+
let queryOptions = {};
|
|
107
102
|
if (prepareQueryCustom) {
|
|
108
|
-
|
|
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
|
-
|
|
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 {
|
|
159
|
-
const rawUpdatedEntities = await this.database.batchUpdate(preparedEntities, operations,
|
|
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,
|
|
165
|
-
const existingEntity = await this.database.getById(operations,
|
|
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,
|
|
192
|
-
const {
|
|
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,
|
|
206
|
-
const {
|
|
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,
|
|
211
|
-
const {
|
|
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,
|
|
216
|
-
const {
|
|
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
|
-
|
|
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
|
-
|
|
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",
|