@loomcore/api 0.2.26 → 0.2.28
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/README.md +1 -1
- package/dist/__tests__/common-test.utils.js +7 -7
- package/dist/__tests__/test-objects.js +3 -27
- package/dist/controllers/api.controller.d.ts +1 -3
- package/dist/controllers/api.controller.js +28 -27
- package/dist/controllers/auth.controller.js +174 -126
- package/dist/controllers/authorizations.controller.d.ts +1 -1
- package/dist/controllers/authorizations.controller.js +4 -5
- package/dist/controllers/features.controller.d.ts +1 -1
- package/dist/controllers/features.controller.js +4 -5
- package/dist/controllers/organizations.controller.js +95 -44
- package/dist/controllers/query-api.controller.d.ts +1 -3
- package/dist/controllers/query-api.controller.js +11 -13
- package/dist/controllers/roles.controller.d.ts +3 -3
- package/dist/controllers/roles.controller.js +4 -5
- package/dist/controllers/user-roles.controller.d.ts +1 -1
- package/dist/controllers/user-roles.controller.js +4 -5
- package/dist/controllers/users.controller.d.ts +0 -2
- package/dist/controllers/users.controller.js +1 -3
- package/dist/databases/postgres/postgres.database.d.ts +2 -2
- package/dist/databases/postgres/postgres.database.js +2 -12
- package/dist/decorators/authorize.decorator.d.ts +13 -0
- package/dist/decorators/authorize.decorator.js +33 -0
- package/dist/errors/unauthorized.error.d.ts +1 -1
- package/dist/errors/unauthorized.error.js +5 -3
- package/dist/middleware/authorize/authenticate-request.d.ts +3 -0
- package/dist/middleware/authorize/authenticate-request.js +21 -0
- package/dist/middleware/authorize/authorize.middleware.d.ts +3 -0
- package/dist/middleware/authorize/authorize.middleware.js +23 -0
- package/dist/middleware/index.d.ts +1 -2
- package/dist/middleware/index.js +1 -2
- package/dist/models/index.d.ts +0 -1
- package/dist/models/index.js +0 -1
- package/dist/services/authorizations.service.d.ts +7 -0
- package/dist/services/authorizations.service.js +12 -0
- package/dist/services/features.service.d.ts +7 -0
- package/dist/services/features.service.js +12 -0
- package/dist/services/index.d.ts +4 -0
- package/dist/services/index.js +4 -0
- package/dist/services/refresh-token.service.d.ts +1 -1
- package/dist/services/refresh-token.service.js +1 -1
- package/dist/services/roles.service.d.ts +7 -0
- package/dist/services/roles.service.js +12 -0
- package/dist/services/user-roles.service.d.ts +7 -0
- package/dist/services/user-roles.service.js +12 -0
- package/dist/services/user.service.js +3 -3
- package/dist/services/utils/getUserContextAuthorizations.util.d.ts +2 -2
- package/dist/services/utils/getUserContextAuthorizations.util.js +3 -3
- package/dist/utils/auth/assert-owner-or-admin.util.d.ts +1 -1
- package/dist/utils/auth/assert-owner-or-admin.util.js +2 -2
- package/dist/utils/auth/assert-user-has-feature.util.d.ts +2 -0
- package/dist/utils/auth/assert-user-has-feature.util.js +10 -0
- package/dist/utils/auth/{assert-self-or-admin.util.d.ts → assert-user-or-admin.util.d.ts} +1 -1
- package/dist/utils/auth/{assert-self-or-admin.util.js → assert-user-or-admin.util.js} +1 -1
- package/dist/utils/auth/attempt-login.util.js +3 -3
- package/dist/utils/auth/auth-user-specs.util.d.ts +0 -1
- package/dist/utils/auth/auth-user-specs.util.js +1 -3
- package/dist/utils/auth/authorize-method.util.d.ts +2 -0
- package/dist/utils/auth/authorize-method.util.js +8 -0
- package/dist/utils/auth/create-new-refresh-token.util.d.ts +1 -1
- package/dist/utils/auth/create-new-tokens.util.d.ts +1 -2
- package/dist/utils/auth/get-active-refresh-token.util.d.ts +1 -2
- package/dist/utils/auth/index.d.ts +3 -1
- package/dist/utils/auth/index.js +3 -1
- package/dist/utils/auth/is-admin.util.js +1 -2
- package/package.json +4 -3
- package/dist/middleware/is-authorized.d.ts +0 -4
- package/dist/middleware/is-authorized.js +0 -57
- package/dist/models/authorization.model.d.ts +0 -16
- package/dist/models/authorization.model.js +0 -11
- package/dist/models/feature.model.d.ts +0 -7
- package/dist/models/feature.model.js +0 -6
- package/dist/models/method-auth.model.d.ts +0 -9
- package/dist/models/method-auth.model.js +0 -12
- package/dist/models/refresh-token.model.d.ts +0 -19
- package/dist/models/refresh-token.model.js +0 -12
- package/dist/models/role.model.d.ts +0 -7
- package/dist/models/role.model.js +0 -6
- package/dist/models/user-role.model.d.ts +0 -8
- package/dist/models/user-role.model.js +0 -7
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ This library provides a foundational structure for creating robust APIs, with a
|
|
|
8
8
|
|
|
9
9
|
- **Generic API Controller**: A base `ApiController` that automatically scaffolds a full suite of RESTful endpoints for any data model. This includes support for CRUD operations, batch updates, pagination, and counting.
|
|
10
10
|
- **Built-in Multi-tenancy**: Optional, configuration-driven multi-tenancy. When enabled, all database operations are automatically scoped to the current user's organization, ensuring strict data isolation.
|
|
11
|
-
- **Authentication & Authorization**: Integrated JWT-based authentication
|
|
11
|
+
- **Authentication & Authorization**: Integrated JWT-based authentication with `@Authorize` / `@AllowAnonymous` decorators to secure endpoints, plus support for OAuth 2.0 Code Flow including refresh tokens.
|
|
12
12
|
- **User & Organization Management**: Pre-built services and controllers for common user and organization management tasks.
|
|
13
13
|
- **Password Management**: Includes services for handling password reset requests and workflows.
|
|
14
14
|
- **Email Service Integration**: A ready-to-use service for sending transactional emails (e.g., for password resets or welcome messages).
|
|
@@ -5,7 +5,6 @@ import { Value } from "@sinclair/typebox/value";
|
|
|
5
5
|
import jwt from "jsonwebtoken";
|
|
6
6
|
import { ObjectId } from "mongodb";
|
|
7
7
|
import { ApiController } from "../controllers/api.controller.js";
|
|
8
|
-
import { authenticated } from "../middleware/index.js";
|
|
9
8
|
import { MongoDBDatabase } from "../databases/mongo-db/mongo-db.database.js";
|
|
10
9
|
import { LeftJoin } from "../databases/operations/left-join.operation.js";
|
|
11
10
|
import { PostgresDatabase } from "../databases/postgres/postgres.database.js";
|
|
@@ -220,7 +219,7 @@ export class CategoryService extends GenericApiService {
|
|
|
220
219
|
export class CategoryController extends ApiController {
|
|
221
220
|
constructor(app, database) {
|
|
222
221
|
const categoryService = new CategoryService(database);
|
|
223
|
-
super("categories", app, categoryService,
|
|
222
|
+
super("categories", app, categoryService, "category", CategorySpec);
|
|
224
223
|
}
|
|
225
224
|
}
|
|
226
225
|
export function setupTestConfig(isMultiTenant = true, dbType) {
|
|
@@ -285,7 +284,7 @@ export class ProductService extends GenericApiService {
|
|
|
285
284
|
export class ProductsController extends ApiController {
|
|
286
285
|
constructor(app, database) {
|
|
287
286
|
const productService = new ProductService(database);
|
|
288
|
-
super("products", app, productService,
|
|
287
|
+
super("products", app, productService, "product", ProductSpec);
|
|
289
288
|
}
|
|
290
289
|
async get(req, res, next) {
|
|
291
290
|
res.set("Content-Type", "application/json");
|
|
@@ -316,14 +315,15 @@ export class ProductsController extends ApiController {
|
|
|
316
315
|
if (!idParam) {
|
|
317
316
|
throw new Error("ID parameter is required");
|
|
318
317
|
}
|
|
318
|
+
let id;
|
|
319
319
|
try {
|
|
320
|
-
|
|
321
|
-
const entity = await this.service.getById(userContext, id, prepareQueryCustom, postProcessEntityCustom);
|
|
322
|
-
apiUtils.apiResponse(res, 200, { data: entity }, ProductWithCategorySpec, ProductWithCategoryPublicSpec);
|
|
320
|
+
id = Value.Convert(this.idSchema, idParam);
|
|
323
321
|
}
|
|
324
322
|
catch (error) {
|
|
325
323
|
throw new Error(`Invalid ID format: ${error.message || error}`);
|
|
326
324
|
}
|
|
325
|
+
const entity = await this.service.getById(userContext, id, prepareQueryCustom, postProcessEntityCustom);
|
|
326
|
+
apiUtils.apiResponse(res, 200, { data: entity }, ProductWithCategorySpec, ProductWithCategoryPublicSpec);
|
|
327
327
|
}
|
|
328
328
|
}
|
|
329
329
|
export class MultiTenantProductService extends MultiTenantApiService {
|
|
@@ -360,7 +360,7 @@ export class MultiTenantProductsController extends ApiController {
|
|
|
360
360
|
"internalNumber",
|
|
361
361
|
]);
|
|
362
362
|
const PublicAggregatedProductSpec = entityUtils.getModelSpec(PublicAggregatedProductSchema);
|
|
363
|
-
super("multi-tenant-products", app, productService,
|
|
363
|
+
super("multi-tenant-products", app, productService, "product", ProductSpec, PublicAggregatedProductSpec);
|
|
364
364
|
}
|
|
365
365
|
}
|
|
366
366
|
function configureJwtSecret() {
|
|
@@ -54,30 +54,14 @@ export function getTestMetaOrgUserContext() {
|
|
|
54
54
|
return {
|
|
55
55
|
user: getTestMetaOrgUser(),
|
|
56
56
|
organization: getTestMetaOrg(),
|
|
57
|
-
|
|
58
|
-
{
|
|
59
|
-
_id: "6939c54e57a1c6576a40c590",
|
|
60
|
-
_orgId: getTestMetaOrg()._id,
|
|
61
|
-
role: "metaorgUser",
|
|
62
|
-
feature: "metaorgUser",
|
|
63
|
-
config: {},
|
|
64
|
-
},
|
|
65
|
-
],
|
|
57
|
+
features: ["metaorgUser"],
|
|
66
58
|
};
|
|
67
59
|
}
|
|
68
60
|
export function getTestMetaOrgAdminUserContext() {
|
|
69
61
|
return {
|
|
70
62
|
user: getTestMetaOrgUser(),
|
|
71
63
|
organization: getTestMetaOrg(),
|
|
72
|
-
|
|
73
|
-
{
|
|
74
|
-
_id: "6939c54e57a1c6576a40c590",
|
|
75
|
-
_orgId: getTestMetaOrg()._id,
|
|
76
|
-
role: "admin",
|
|
77
|
-
feature: "admin",
|
|
78
|
-
config: {},
|
|
79
|
-
},
|
|
80
|
-
],
|
|
64
|
+
features: ["admin"],
|
|
81
65
|
};
|
|
82
66
|
}
|
|
83
67
|
let TEST_ORG_ID = "6926167d06c0073a778a124f";
|
|
@@ -122,14 +106,6 @@ export function getTestOrgUserContext() {
|
|
|
122
106
|
return {
|
|
123
107
|
user: getTestOrgUser(),
|
|
124
108
|
organization: getTestOrg(),
|
|
125
|
-
|
|
126
|
-
{
|
|
127
|
-
_id: "6939c54e57a1c6576a40c591",
|
|
128
|
-
_orgId: getTestOrg()._id,
|
|
129
|
-
role: "testOrgUser",
|
|
130
|
-
feature: "testOrgUser",
|
|
131
|
-
config: {},
|
|
132
|
-
},
|
|
133
|
-
],
|
|
109
|
+
features: ["testOrgUser"],
|
|
134
110
|
};
|
|
135
111
|
}
|
|
@@ -2,17 +2,15 @@ import { Application, NextFunction, Request, Response } from 'express';
|
|
|
2
2
|
import { IEntity, IModelSpec } from '@loomcore/common/models';
|
|
3
3
|
import type { TSchema } from '@sinclair/typebox';
|
|
4
4
|
import { IGenericApiService } from '../services/index.js';
|
|
5
|
-
import { type MethodAuth } from '../middleware/index.js';
|
|
6
5
|
export declare abstract class ApiController<T extends IEntity> {
|
|
7
6
|
protected app: Application;
|
|
8
7
|
protected service: IGenericApiService<T>;
|
|
9
8
|
protected slug: string;
|
|
10
9
|
protected apiResourceName: string;
|
|
11
|
-
protected routeAuth: MethodAuth;
|
|
12
10
|
protected modelSpec?: IModelSpec;
|
|
13
11
|
protected publicSpec?: IModelSpec;
|
|
14
12
|
protected idSchema: TSchema;
|
|
15
|
-
protected constructor(slug: string, app: Application, service: IGenericApiService<T>,
|
|
13
|
+
protected constructor(slug: string, app: Application, service: IGenericApiService<T>, resourceName?: string, modelSpec?: IModelSpec, publicSpec?: IModelSpec);
|
|
16
14
|
mapRoutes(app: Application): void;
|
|
17
15
|
protected validate(entity: any, isPartial?: boolean): void;
|
|
18
16
|
protected validateMany(entities: any[], isPartial?: boolean): void;
|
|
@@ -2,22 +2,19 @@ import { BadRequestError } from '../errors/index.js';
|
|
|
2
2
|
import { entityUtils } from '@loomcore/common/utils';
|
|
3
3
|
import { Value } from '@sinclair/typebox/value';
|
|
4
4
|
import { getIdSchema } from '@loomcore/common/validation';
|
|
5
|
-
import { apiUtils } from '../utils/index.js';
|
|
6
|
-
import { authenticated, isAuthorized, } from '../middleware/index.js';
|
|
5
|
+
import { apiUtils, authorizeMethod } from '../utils/index.js';
|
|
7
6
|
export class ApiController {
|
|
8
7
|
app;
|
|
9
8
|
service;
|
|
10
9
|
slug;
|
|
11
10
|
apiResourceName;
|
|
12
|
-
routeAuth;
|
|
13
11
|
modelSpec;
|
|
14
12
|
publicSpec;
|
|
15
13
|
idSchema;
|
|
16
|
-
constructor(slug, app, service,
|
|
14
|
+
constructor(slug, app, service, resourceName = '', modelSpec, publicSpec) {
|
|
17
15
|
this.slug = slug;
|
|
18
16
|
this.app = app;
|
|
19
17
|
this.service = service;
|
|
20
|
-
this.routeAuth = routeAuth;
|
|
21
18
|
this.apiResourceName = resourceName;
|
|
22
19
|
this.modelSpec = modelSpec;
|
|
23
20
|
this.publicSpec = publicSpec;
|
|
@@ -25,16 +22,16 @@ export class ApiController {
|
|
|
25
22
|
this.mapRoutes(app);
|
|
26
23
|
}
|
|
27
24
|
mapRoutes(app) {
|
|
28
|
-
const
|
|
29
|
-
app.get(`/api/${this.slug}`,
|
|
30
|
-
app.get(`/api/${this.slug}/all`,
|
|
31
|
-
app.get(`/api/${this.slug}/count`,
|
|
32
|
-
app.get(`/api/${this.slug}/:id`,
|
|
33
|
-
app.post(`/api/${this.slug}`,
|
|
34
|
-
app.patch(`/api/${this.slug}/batch`,
|
|
35
|
-
app.put(`/api/${this.slug}/:id`,
|
|
36
|
-
app.patch(`/api/${this.slug}/:id`,
|
|
37
|
-
app.delete(`/api/${this.slug}/:id`,
|
|
25
|
+
const authorize = (method) => authorizeMethod(this, method);
|
|
26
|
+
app.get(`/api/${this.slug}`, authorize('get'), this.get.bind(this));
|
|
27
|
+
app.get(`/api/${this.slug}/all`, authorize('getAll'), this.getAll.bind(this));
|
|
28
|
+
app.get(`/api/${this.slug}/count`, authorize('getCount'), this.getCount.bind(this));
|
|
29
|
+
app.get(`/api/${this.slug}/:id`, authorize('getById'), this.getById.bind(this));
|
|
30
|
+
app.post(`/api/${this.slug}`, authorize('create'), this.create.bind(this));
|
|
31
|
+
app.patch(`/api/${this.slug}/batch`, authorize('batchUpdate'), this.batchUpdate.bind(this));
|
|
32
|
+
app.put(`/api/${this.slug}/:id`, authorize('fullUpdateById'), this.fullUpdateById.bind(this));
|
|
33
|
+
app.patch(`/api/${this.slug}/:id`, authorize('partialUpdateById'), this.partialUpdateById.bind(this));
|
|
34
|
+
app.delete(`/api/${this.slug}/:id`, authorize('deleteById'), this.deleteById.bind(this));
|
|
38
35
|
}
|
|
39
36
|
validate(entity, isPartial = false) {
|
|
40
37
|
const validationErrors = this.service.validate(entity, isPartial);
|
|
@@ -61,14 +58,15 @@ export class ApiController {
|
|
|
61
58
|
if (!idParam) {
|
|
62
59
|
throw new BadRequestError('ID parameter is required');
|
|
63
60
|
}
|
|
61
|
+
let id;
|
|
64
62
|
try {
|
|
65
|
-
|
|
66
|
-
const entity = await this.service.getById(req.userContext, id);
|
|
67
|
-
apiUtils.apiResponse(res, 200, { data: entity }, this.modelSpec, this.publicSpec);
|
|
63
|
+
id = Value.Convert(this.idSchema, idParam);
|
|
68
64
|
}
|
|
69
65
|
catch (error) {
|
|
70
66
|
throw new BadRequestError(`Invalid ID format: ${error.message || error}`);
|
|
71
67
|
}
|
|
68
|
+
const entity = await this.service.getById(req.userContext, id);
|
|
69
|
+
apiUtils.apiResponse(res, 200, { data: entity }, this.modelSpec, this.publicSpec);
|
|
72
70
|
}
|
|
73
71
|
async getCount(req, res, next) {
|
|
74
72
|
res.set('Content-Type', 'application/json');
|
|
@@ -110,14 +108,15 @@ export class ApiController {
|
|
|
110
108
|
if (!idParam) {
|
|
111
109
|
throw new BadRequestError('ID parameter is required');
|
|
112
110
|
}
|
|
111
|
+
let id;
|
|
113
112
|
try {
|
|
114
|
-
|
|
115
|
-
const updateResult = await this.service.fullUpdateById(req.userContext, id, req.body);
|
|
116
|
-
apiUtils.apiResponse(res, 200, { data: updateResult }, this.modelSpec, this.publicSpec);
|
|
113
|
+
id = Value.Convert(this.idSchema, idParam);
|
|
117
114
|
}
|
|
118
115
|
catch (error) {
|
|
119
116
|
throw new BadRequestError(`Invalid ID format: ${error.message || error}`);
|
|
120
117
|
}
|
|
118
|
+
const updateResult = await this.service.fullUpdateById(req.userContext, id, req.body);
|
|
119
|
+
apiUtils.apiResponse(res, 200, { data: updateResult }, this.modelSpec, this.publicSpec);
|
|
121
120
|
}
|
|
122
121
|
async partialUpdateById(req, res, next) {
|
|
123
122
|
res.set('Content-Type', 'application/json');
|
|
@@ -126,14 +125,15 @@ export class ApiController {
|
|
|
126
125
|
if (!idParam) {
|
|
127
126
|
throw new BadRequestError('ID parameter is required');
|
|
128
127
|
}
|
|
128
|
+
let id;
|
|
129
129
|
try {
|
|
130
|
-
|
|
131
|
-
const updateResult = await this.service.partialUpdateById(req.userContext, id, req.body);
|
|
132
|
-
apiUtils.apiResponse(res, 200, { data: updateResult }, this.modelSpec, this.publicSpec);
|
|
130
|
+
id = Value.Convert(this.idSchema, idParam);
|
|
133
131
|
}
|
|
134
132
|
catch (error) {
|
|
135
133
|
throw new BadRequestError(`Invalid ID format: ${error.message || error}`);
|
|
136
134
|
}
|
|
135
|
+
const updateResult = await this.service.partialUpdateById(req.userContext, id, req.body);
|
|
136
|
+
apiUtils.apiResponse(res, 200, { data: updateResult }, this.modelSpec, this.publicSpec);
|
|
137
137
|
}
|
|
138
138
|
async deleteById(req, res, next) {
|
|
139
139
|
res.set('Content-Type', 'application/json');
|
|
@@ -141,13 +141,14 @@ export class ApiController {
|
|
|
141
141
|
if (!idParam) {
|
|
142
142
|
throw new BadRequestError('ID parameter is required');
|
|
143
143
|
}
|
|
144
|
+
let id;
|
|
144
145
|
try {
|
|
145
|
-
|
|
146
|
-
const deleteResult = await this.service.deleteById(req.userContext, id);
|
|
147
|
-
apiUtils.apiResponse(res, 200, { data: deleteResult }, this.modelSpec, this.publicSpec);
|
|
146
|
+
id = Value.Convert(this.idSchema, idParam);
|
|
148
147
|
}
|
|
149
148
|
catch (error) {
|
|
150
149
|
throw new BadRequestError(`Invalid ID format: ${error.message || error}`);
|
|
151
150
|
}
|
|
151
|
+
const deleteResult = await this.service.deleteById(req.userContext, id);
|
|
152
|
+
apiUtils.apiResponse(res, 200, { data: deleteResult }, this.modelSpec, this.publicSpec);
|
|
152
153
|
}
|
|
153
154
|
}
|
|
@@ -1,147 +1,195 @@
|
|
|
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
|
+
};
|
|
8
|
+
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
9
|
+
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
|
|
10
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
11
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
12
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
13
|
+
var _, done = false;
|
|
14
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
15
|
+
var context = {};
|
|
16
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
17
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
18
|
+
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
|
|
19
|
+
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
|
|
20
|
+
if (kind === "accessor") {
|
|
21
|
+
if (result === void 0) continue;
|
|
22
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
23
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
24
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
25
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
26
|
+
}
|
|
27
|
+
else if (_ = accept(result)) {
|
|
28
|
+
if (kind === "field") initializers.unshift(_);
|
|
29
|
+
else descriptor[key] = _;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
33
|
+
done = true;
|
|
34
|
+
};
|
|
1
35
|
import { EmptyUserContext, passwordValidator, PublicUserContextSpec, PublicUserSpec, TokenResponseSpec, UserSpec, } from "@loomcore/common/models";
|
|
2
36
|
import { entityUtils } from "@loomcore/common/utils";
|
|
3
37
|
import { config } from "../config/base-api-config.js";
|
|
4
38
|
import { BadRequestError, UnauthenticatedError } from "../errors/index.js";
|
|
5
|
-
import { isAuthorized } from "../middleware/index.js";
|
|
6
39
|
import { OrganizationService, UserService } from "../services/index.js";
|
|
7
|
-
import { attemptLogin, changePassword, createLoginResponseSpec, getAndSetDeviceIdCookie, getDeviceIdFromCookie, requestTokenUsingRefreshToken, resetPassword, sendResetPasswordEmail,
|
|
40
|
+
import { attemptLogin, authorizeMethod, changePassword, createLoginResponseSpec, getAndSetDeviceIdCookie, getDeviceIdFromCookie, requestTokenUsingRefreshToken, resetPassword, sendResetPasswordEmail, } from "../utils/auth/index.js";
|
|
8
41
|
import { apiUtils } from "../utils/index.js";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}) {
|
|
23
|
-
this.database = database;
|
|
24
|
-
this.userSpec = options.userSpec;
|
|
25
|
-
this.publicUserSpec = options.publicUserSpec;
|
|
26
|
-
this.userContextSpec = options.publicUserContextSpec;
|
|
27
|
-
setAuthUserContextSpec(this.userContextSpec);
|
|
28
|
-
this.loginResponseSpec = createLoginResponseSpec(this.userContextSpec);
|
|
29
|
-
this.userService = options.userService;
|
|
30
|
-
this.organizationService = new OrganizationService(database);
|
|
31
|
-
this.mapRoutes(app);
|
|
32
|
-
}
|
|
33
|
-
mapRoutes(app) {
|
|
34
|
-
app.post(`/api/auth/login`, this.login.bind(this), this.afterAuth.bind(this));
|
|
35
|
-
app.get(`/api/auth/refresh`, this.requestTokenUsingRefreshToken.bind(this));
|
|
36
|
-
app.get(`/api/auth/get-user-context`, isAuthorized({ read: true }), this.getUserContext.bind(this));
|
|
37
|
-
app.patch(`/api/auth/change-password`, isAuthorized({ update: true }), this.changePassword.bind(this));
|
|
38
|
-
app.post(`/api/auth/forgot-password`, this.forgotPassword.bind(this));
|
|
39
|
-
app.post(`/api/auth/reset-password`, this.resetPassword.bind(this));
|
|
40
|
-
}
|
|
41
|
-
async login(req, res) {
|
|
42
|
-
const { email, password } = req.body;
|
|
43
|
-
if (!email || typeof email !== "string") {
|
|
44
|
-
throw new BadRequestError("Missing required fields: email is required.");
|
|
42
|
+
import { Authorize } from "../decorators/authorize.decorator.js";
|
|
43
|
+
let AuthController = (() => {
|
|
44
|
+
let _instanceExtraInitializers = [];
|
|
45
|
+
let _getUserContext_decorators;
|
|
46
|
+
let _changePassword_decorators;
|
|
47
|
+
return class AuthController {
|
|
48
|
+
static {
|
|
49
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
|
|
50
|
+
_getUserContext_decorators = [Authorize()];
|
|
51
|
+
_changePassword_decorators = [Authorize()];
|
|
52
|
+
__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);
|
|
53
|
+
__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
|
+
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
45
55
|
}
|
|
46
|
-
|
|
47
|
-
|
|
56
|
+
database = __runInitializers(this, _instanceExtraInitializers);
|
|
57
|
+
userService;
|
|
58
|
+
organizationService;
|
|
59
|
+
userSpec;
|
|
60
|
+
publicUserSpec;
|
|
61
|
+
userContextSpec;
|
|
62
|
+
loginResponseSpec;
|
|
63
|
+
constructor(app, database, options = {
|
|
64
|
+
userService: new UserService(database),
|
|
65
|
+
userSpec: UserSpec,
|
|
66
|
+
publicUserSpec: PublicUserSpec,
|
|
67
|
+
publicUserContextSpec: PublicUserContextSpec,
|
|
68
|
+
}) {
|
|
69
|
+
this.database = database;
|
|
70
|
+
this.userSpec = options.userSpec;
|
|
71
|
+
this.publicUserSpec = options.publicUserSpec;
|
|
72
|
+
this.userContextSpec = options.publicUserContextSpec;
|
|
73
|
+
this.loginResponseSpec = createLoginResponseSpec(this.userContextSpec);
|
|
74
|
+
this.userService = options.userService;
|
|
75
|
+
this.organizationService = new OrganizationService(database);
|
|
76
|
+
this.mapRoutes(app);
|
|
48
77
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
78
|
+
mapRoutes(app) {
|
|
79
|
+
const authorize = (method) => authorizeMethod(this, method);
|
|
80
|
+
app.post(`/api/auth/login`, this.login.bind(this), this.afterAuth.bind(this));
|
|
81
|
+
app.get(`/api/auth/refresh`, this.requestTokenUsingRefreshToken.bind(this));
|
|
82
|
+
app.get(`/api/auth/get-user-context`, authorize('getUserContext'), this.getUserContext.bind(this));
|
|
83
|
+
app.patch(`/api/auth/change-password`, authorize('changePassword'), this.changePassword.bind(this));
|
|
84
|
+
app.post(`/api/auth/forgot-password`, this.forgotPassword.bind(this));
|
|
85
|
+
app.post(`/api/auth/reset-password`, this.resetPassword.bind(this));
|
|
86
|
+
}
|
|
87
|
+
async login(req, res) {
|
|
88
|
+
const { email, password } = req.body;
|
|
89
|
+
if (!email || typeof email !== "string") {
|
|
90
|
+
throw new BadRequestError("Missing required fields: email is required.");
|
|
54
91
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
throw new BadRequestError("Missing required fields: organization is required.");
|
|
92
|
+
if (!password || typeof password !== "string") {
|
|
93
|
+
throw new BadRequestError("Missing required fields: password is required.");
|
|
58
94
|
}
|
|
95
|
+
let organization = null;
|
|
96
|
+
if (config.app.isMultiTenant) {
|
|
97
|
+
const referer = req.get("referer") || req.headers.referer;
|
|
98
|
+
if (!referer) {
|
|
99
|
+
throw new BadRequestError("Missing required fields: referer is required.");
|
|
100
|
+
}
|
|
101
|
+
organization = await this.organizationService.findByDomain(EmptyUserContext, referer.split("/")[2]);
|
|
102
|
+
if (!organization) {
|
|
103
|
+
throw new BadRequestError("Missing required fields: organization is required.");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
res.set("Content-Type", "application/json");
|
|
107
|
+
const deviceId = getAndSetDeviceIdCookie(req, res);
|
|
108
|
+
const loginResponse = await attemptLogin(this.database, email, password, deviceId, organization, this.userService);
|
|
109
|
+
apiUtils.apiResponse(res, 200, { data: loginResponse }, this.loginResponseSpec);
|
|
59
110
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const tokens = await requestTokenUsingRefreshToken(this.database, userContext, refreshToken, deviceId);
|
|
76
|
-
if (!tokens) {
|
|
77
|
-
throw new UnauthenticatedError();
|
|
78
|
-
}
|
|
79
|
-
apiUtils.apiResponse(res, 200, { data: tokens }, TokenResponseSpec);
|
|
80
|
-
}
|
|
81
|
-
async getUserContext(req, res) {
|
|
82
|
-
const userContext = req.userContext;
|
|
83
|
-
apiUtils.apiResponse(res, 200, { data: userContext }, this.userContextSpec);
|
|
84
|
-
}
|
|
85
|
-
afterAuth(_req, _res, _loginResponse) {
|
|
86
|
-
console.log("in afterAuth");
|
|
87
|
-
}
|
|
88
|
-
async changePassword(req, res) {
|
|
89
|
-
const userContext = req.userContext;
|
|
90
|
-
if (!userContext) {
|
|
91
|
-
throw new BadRequestError("Missing required fields: userContext is required.");
|
|
111
|
+
async requestTokenUsingRefreshToken(req, res) {
|
|
112
|
+
const userContext = req.userContext;
|
|
113
|
+
if (!userContext) {
|
|
114
|
+
throw new BadRequestError("Missing required fields: userContext is required.");
|
|
115
|
+
}
|
|
116
|
+
const refreshToken = req.query.refreshToken;
|
|
117
|
+
if (!refreshToken || typeof refreshToken !== "string") {
|
|
118
|
+
throw new BadRequestError("Missing required fields: refreshToken is required.");
|
|
119
|
+
}
|
|
120
|
+
const deviceId = getDeviceIdFromCookie(req);
|
|
121
|
+
const tokens = await requestTokenUsingRefreshToken(this.database, userContext, refreshToken, deviceId);
|
|
122
|
+
if (!tokens) {
|
|
123
|
+
throw new UnauthenticatedError();
|
|
124
|
+
}
|
|
125
|
+
apiUtils.apiResponse(res, 200, { data: tokens }, TokenResponseSpec);
|
|
92
126
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const updateResult = await changePassword(this.database, userContext, password, this.userService);
|
|
97
|
-
apiUtils.apiResponse(res, 200, { data: updateResult });
|
|
98
|
-
}
|
|
99
|
-
async forgotPassword(req, res) {
|
|
100
|
-
const email = req.body?.email;
|
|
101
|
-
if (!email || typeof email !== "string") {
|
|
102
|
-
throw new BadRequestError("Missing required fields: email is required.");
|
|
127
|
+
async getUserContext(req, res) {
|
|
128
|
+
const userContext = req.userContext;
|
|
129
|
+
apiUtils.apiResponse(res, 200, { data: userContext }, this.userContextSpec);
|
|
103
130
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
throw new BadRequestError("Missing required fields: referer is required.");
|
|
131
|
+
afterAuth(_req, _res, _loginResponse) {
|
|
132
|
+
console.log("in afterAuth");
|
|
107
133
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (!organization) {
|
|
113
|
-
throw new BadRequestError("Missing required fields: organization is required.");
|
|
134
|
+
async changePassword(req, res) {
|
|
135
|
+
const userContext = req.userContext;
|
|
136
|
+
if (!userContext) {
|
|
137
|
+
throw new BadRequestError("Missing required fields: userContext is required.");
|
|
114
138
|
}
|
|
139
|
+
const password = req.body?.password;
|
|
140
|
+
const validationErrors = entityUtils.validate(this.userSpec, { password: password }, true, passwordValidator);
|
|
141
|
+
entityUtils.handleValidationResult(validationErrors, "AuthController.changePassword");
|
|
142
|
+
const updateResult = await changePassword(this.database, userContext, password, this.userService);
|
|
143
|
+
apiUtils.apiResponse(res, 200, { data: updateResult });
|
|
115
144
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
});
|
|
123
|
-
if (user) {
|
|
124
|
-
await sendResetPasswordEmail(this.database, email, referer, organization || undefined);
|
|
125
|
-
}
|
|
126
|
-
apiUtils.apiResponse(res, 200);
|
|
127
|
-
}
|
|
128
|
-
async resetPassword(req, res) {
|
|
129
|
-
const { email, token, password } = req.body;
|
|
130
|
-
if (!email || !token || !password) {
|
|
131
|
-
throw new BadRequestError("Missing required fields: email, token, and password are required.");
|
|
132
|
-
}
|
|
133
|
-
let organization = null;
|
|
134
|
-
if (config.app.isMultiTenant) {
|
|
135
|
-
const referer = req.get("referer") || req.headers.referer;
|
|
145
|
+
async forgotPassword(req, res) {
|
|
146
|
+
const email = req.body?.email;
|
|
147
|
+
if (!email || typeof email !== "string") {
|
|
148
|
+
throw new BadRequestError("Missing required fields: email is required.");
|
|
149
|
+
}
|
|
150
|
+
let referer = req.get("referer") || req.headers.referer;
|
|
136
151
|
if (!referer) {
|
|
137
152
|
throw new BadRequestError("Missing required fields: referer is required.");
|
|
138
153
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
154
|
+
referer = referer.replace(/\/$/, "");
|
|
155
|
+
let organization = null;
|
|
156
|
+
if (config.app.isMultiTenant) {
|
|
157
|
+
organization = await this.organizationService.findByDomain(EmptyUserContext, referer.split("/")[2]);
|
|
158
|
+
if (!organization) {
|
|
159
|
+
throw new BadRequestError("Missing required fields: organization is required.");
|
|
160
|
+
}
|
|
142
161
|
}
|
|
162
|
+
const userContext = {
|
|
163
|
+
...EmptyUserContext,
|
|
164
|
+
organization: organization || undefined,
|
|
165
|
+
};
|
|
166
|
+
const user = await this.userService.findOne(userContext, {
|
|
167
|
+
filters: { email: { eq: email.toLowerCase() } },
|
|
168
|
+
});
|
|
169
|
+
if (user) {
|
|
170
|
+
await sendResetPasswordEmail(this.database, email, referer, organization || undefined);
|
|
171
|
+
}
|
|
172
|
+
apiUtils.apiResponse(res, 200);
|
|
143
173
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
174
|
+
async resetPassword(req, res) {
|
|
175
|
+
const { email, token, password } = req.body;
|
|
176
|
+
if (!email || !token || !password) {
|
|
177
|
+
throw new BadRequestError("Missing required fields: email, token, and password are required.");
|
|
178
|
+
}
|
|
179
|
+
let organization = null;
|
|
180
|
+
if (config.app.isMultiTenant) {
|
|
181
|
+
const referer = req.get("referer") || req.headers.referer;
|
|
182
|
+
if (!referer) {
|
|
183
|
+
throw new BadRequestError("Missing required fields: referer is required.");
|
|
184
|
+
}
|
|
185
|
+
organization = await this.organizationService.findByDomain(EmptyUserContext, referer.split("/")[2]);
|
|
186
|
+
if (!organization) {
|
|
187
|
+
throw new BadRequestError("Missing required fields: organization is required.");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const response = await resetPassword(this.database, email, token, password, organization, this.userService, this.userSpec);
|
|
191
|
+
apiUtils.apiResponse(res, 200, { data: response });
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
})();
|
|
195
|
+
export { AuthController };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Application } from "express";
|
|
2
2
|
import type { IDatabase } from "../databases/models/index.js";
|
|
3
|
-
import { type IAuthorization } from "../models/authorization.model.js";
|
|
4
3
|
import { ApiController } from "./api.controller.js";
|
|
4
|
+
import { IAuthorization } from "@loomcore/common/models";
|
|
5
5
|
export declare class AuthorizationsController extends ApiController<IAuthorization> {
|
|
6
6
|
constructor(app: Application, database: IDatabase);
|
|
7
7
|
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { adminWrites } from "../middleware/index.js";
|
|
2
|
-
import { AuthorizationModelSpec, } from "../models/authorization.model.js";
|
|
3
|
-
import { MultiTenantApiService } from "../services/multi-tenant-api.service.js";
|
|
4
1
|
import { ApiController } from "./api.controller.js";
|
|
2
|
+
import { AuthorizationModelSpec } from "@loomcore/common/models";
|
|
3
|
+
import { AuthorizationsService } from "../services/authorizations.service.js";
|
|
5
4
|
export class AuthorizationsController extends ApiController {
|
|
6
5
|
constructor(app, database) {
|
|
7
|
-
const authorizationService = new
|
|
8
|
-
super("authorizations", app, authorizationService,
|
|
6
|
+
const authorizationService = new AuthorizationsService(database);
|
|
7
|
+
super("authorizations", app, authorizationService, "authorization", AuthorizationModelSpec);
|
|
9
8
|
}
|
|
10
9
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Application } from "express";
|
|
2
2
|
import type { IDatabase } from "../databases/models/index.js";
|
|
3
|
-
import { type IFeature } from "
|
|
3
|
+
import { type IFeature } from "@loomcore/common/models";
|
|
4
4
|
import { ApiController } from "./api.controller.js";
|
|
5
5
|
export declare class FeaturesController extends ApiController<IFeature> {
|
|
6
6
|
constructor(app: Application, database: IDatabase);
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { MultiTenantApiService } from "../services/multi-tenant-api.service.js";
|
|
1
|
+
import { FeatureModelSpec } from "@loomcore/common/models";
|
|
2
|
+
import { FeaturesService } from "../services/features.service.js";
|
|
4
3
|
import { ApiController } from "./api.controller.js";
|
|
5
4
|
export class FeaturesController extends ApiController {
|
|
6
5
|
constructor(app, database) {
|
|
7
|
-
const featureService = new
|
|
8
|
-
super("features", app, featureService,
|
|
6
|
+
const featureService = new FeaturesService(database);
|
|
7
|
+
super("features", app, featureService, "feature", FeatureModelSpec);
|
|
9
8
|
}
|
|
10
9
|
}
|