@loomcore/api 0.2.8 → 0.2.10
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 +3 -1
- package/dist/__tests__/common-test.utils.js +42 -21
- package/dist/__tests__/test-express-app.d.ts +20 -19
- package/dist/__tests__/test-express-app.js +72 -68
- package/dist/__tests__/test-objects.d.ts +5 -0
- package/dist/__tests__/test-objects.js +16 -0
- package/dist/config/base-api-config.d.ts +2 -2
- package/dist/config/base-api-config.js +2 -2
- package/dist/controllers/auth.controller.d.ts +0 -1
- package/dist/controllers/auth.controller.js +39 -34
- package/dist/databases/postgres/migrations/__tests__/test-migration-helper.js +2 -1
- package/dist/databases/postgres/migrations/postgres-initial-schema.js +7 -5
- package/dist/middleware/index.d.ts +4 -4
- package/dist/middleware/index.js +4 -4
- package/dist/models/app-config.interface.d.ts +1 -1
- package/dist/models/base-api-config.interface.d.ts +5 -5
- package/dist/models/initial-database-config.interface.d.ts +4 -3
- package/dist/utils/auth/attempt-login.util.d.ts +2 -3
- package/dist/utils/auth/attempt-login.util.js +1 -11
- package/dist/utils/auth/reset-password.util.d.ts +2 -2
- package/dist/utils/auth/reset-password.util.js +1 -12
- package/dist/utils/auth/send-reset-password-email.util.d.ts +2 -2
- package/dist/utils/auth/send-reset-password-email.util.js +1 -8
- package/package.json +2 -2
|
@@ -5,6 +5,7 @@ import type { IDatabase } from "../databases/models/index.js";
|
|
|
5
5
|
import type { Operation } from "../databases/operations/operation.js";
|
|
6
6
|
import { GenericApiService } from "../services/index.js";
|
|
7
7
|
import { MultiTenantApiService } from "../services/multi-tenant-api.service.js";
|
|
8
|
+
import * as testObjectsModule from "./test-objects.js";
|
|
8
9
|
import type { DbType } from "../databases/db-type.type.js";
|
|
9
10
|
import { type ICategory } from "./models/category.model.js";
|
|
10
11
|
import { type IProduct } from "./models/product.model.js";
|
|
@@ -64,8 +65,9 @@ declare const testUtils: {
|
|
|
64
65
|
deleteMetaOrg: typeof deleteMetaOrg;
|
|
65
66
|
deleteTestUser: typeof deleteTestUser;
|
|
66
67
|
getAuthToken: typeof getAuthToken;
|
|
68
|
+
getTestMetaOrgRefererUrl: typeof testObjectsModule.getTestMetaOrgRefererUrl;
|
|
67
69
|
initialize: typeof initialize;
|
|
68
|
-
|
|
70
|
+
setupTestConfig: typeof setupTestConfig;
|
|
69
71
|
loginWithTestUser: typeof loginWithTestUser;
|
|
70
72
|
newUser1Email: string;
|
|
71
73
|
newUser1Password: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
-
import { EmptyUserContext, } from "@loomcore/common/models";
|
|
2
|
+
import { EmptyUserContext, getSystemUserContext, } from "@loomcore/common/models";
|
|
3
3
|
import { Type } from "@sinclair/typebox";
|
|
4
4
|
import { Value } from "@sinclair/typebox/value";
|
|
5
5
|
import jwt from "jsonwebtoken";
|
|
@@ -12,7 +12,7 @@ import { GenericApiService, UserService } from "../services/index.js";
|
|
|
12
12
|
import { MultiTenantApiService } from "../services/multi-tenant-api.service.js";
|
|
13
13
|
import { OrganizationService } from "../services/organization.service.js";
|
|
14
14
|
import * as testObjectsModule from "./test-objects.js";
|
|
15
|
-
const { getTestMetaOrg, getTestOrg, getTestMetaOrgUser, getTestMetaOrgUserContext, getTestOrgUserContext, setTestOrgId, setTestMetaOrgId, setTestMetaOrgUserId, setTestOrgUserId, } = testObjectsModule;
|
|
15
|
+
const { getTestMetaOrg, getTestMetaOrgRefererUrl, getTestOrg, getTestMetaOrgUser, getTestMetaOrgUserContext, getTestOrgUser, getTestOrgUserContext, setTestOrgId, setTestMetaOrgId, setTestMetaOrgUserId, setTestOrgUserId, } = testObjectsModule;
|
|
16
16
|
import { entityUtils } from "@loomcore/common/utils";
|
|
17
17
|
import { config, setBaseApiConfig } from "../config/index.js";
|
|
18
18
|
import { attemptLogin } from "../utils/auth/index.js";
|
|
@@ -21,14 +21,13 @@ import { CategorySpec } from "./models/category.model.js";
|
|
|
21
21
|
import { ProductSpec } from "./models/product.model.js";
|
|
22
22
|
import { ProductWithCategoryPublicSpec, ProductWithCategorySpec, } from "./models/product-with-category.model.js";
|
|
23
23
|
import { TestEmailClient } from "./test-email-client.js";
|
|
24
|
-
import { getTestOrgUser } from "./test-objects.js";
|
|
25
24
|
let deviceIdCookie;
|
|
26
25
|
let database;
|
|
27
26
|
let organizationService;
|
|
28
27
|
let userService;
|
|
29
28
|
const JWT_SECRET = "test-secret";
|
|
30
29
|
const newUser1Email = "one@test.com";
|
|
31
|
-
const newUser1Password = "
|
|
30
|
+
const newUser1Password = "testone1";
|
|
32
31
|
const constDeviceIdCookie = crypto.randomBytes(16).toString("hex");
|
|
33
32
|
function initialize(db) {
|
|
34
33
|
database = db;
|
|
@@ -135,16 +134,37 @@ async function deleteTestUser() {
|
|
|
135
134
|
if (!organizationService || !userService) {
|
|
136
135
|
return;
|
|
137
136
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
.
|
|
141
|
-
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
137
|
+
const systemUserContext = getSystemUserContext();
|
|
138
|
+
const metaOrgUser = await userService
|
|
139
|
+
.findOne(systemUserContext, {
|
|
140
|
+
filters: { email: { eq: getTestMetaOrgUser().email.toLowerCase() } },
|
|
141
|
+
})
|
|
142
|
+
.catch(() => null);
|
|
143
|
+
if (metaOrgUser) {
|
|
144
|
+
await userService
|
|
145
|
+
.deleteById(systemUserContext, metaOrgUser._id)
|
|
146
|
+
.catch(() => { });
|
|
147
|
+
}
|
|
148
|
+
const testOrgUser = await userService
|
|
149
|
+
.findOne(systemUserContext, {
|
|
150
|
+
filters: { email: { eq: getTestOrgUser().email.toLowerCase() } },
|
|
151
|
+
})
|
|
152
|
+
.catch(() => null);
|
|
153
|
+
if (testOrgUser) {
|
|
154
|
+
await userService
|
|
155
|
+
.deleteById(systemUserContext, testOrgUser._id)
|
|
156
|
+
.catch(() => { });
|
|
157
|
+
}
|
|
158
|
+
const testOrg = await organizationService
|
|
159
|
+
.findOne(EmptyUserContext, {
|
|
160
|
+
filters: { code: { eq: getTestOrg().code } },
|
|
161
|
+
})
|
|
162
|
+
.catch(() => null);
|
|
163
|
+
if (testOrg) {
|
|
164
|
+
await organizationService
|
|
165
|
+
.deleteById(getTestMetaOrgUserContext(), testOrg._id)
|
|
166
|
+
.catch(() => { });
|
|
167
|
+
}
|
|
148
168
|
}
|
|
149
169
|
async function simulateloginWithTestUser() {
|
|
150
170
|
const req = {
|
|
@@ -164,7 +184,7 @@ async function simulateloginWithTestUser() {
|
|
|
164
184
|
if (!database) {
|
|
165
185
|
throw new Error("Database not initialized. Call initialize() first.");
|
|
166
186
|
}
|
|
167
|
-
const loginResponse = await attemptLogin(database, getTestMetaOrgUser().email, testObjectsModule.TEST_META_ORG_USER_PASSWORD, deviceIdCookie, getTestMetaOrg()
|
|
187
|
+
const loginResponse = await attemptLogin(database, getTestMetaOrgUser().email, testObjectsModule.TEST_META_ORG_USER_PASSWORD, deviceIdCookie, getTestMetaOrg());
|
|
168
188
|
if (!loginResponse?.tokens?.accessToken) {
|
|
169
189
|
throw new Error("Failed to login with test user");
|
|
170
190
|
}
|
|
@@ -331,17 +351,17 @@ export class MultiTenantProductsController extends ApiController {
|
|
|
331
351
|
}
|
|
332
352
|
function configureJwtSecret() {
|
|
333
353
|
const originalJwtVerify = jwt.verify;
|
|
334
|
-
jwt.verify =
|
|
335
|
-
return originalJwtVerify(token, JWT_SECRET, options);
|
|
336
|
-
};
|
|
354
|
+
jwt.verify = (token, _secret, options) => originalJwtVerify(token, JWT_SECRET, options);
|
|
337
355
|
}
|
|
338
356
|
async function loginWithTestUser(agent) {
|
|
339
357
|
agent.set("Cookie", [`deviceId=${deviceIdCookie}`]);
|
|
340
358
|
const testUser = getTestMetaOrgUser();
|
|
341
|
-
const response = await agent
|
|
359
|
+
const response = await agent
|
|
360
|
+
.post("/api/auth/login")
|
|
361
|
+
.set("Referer", getTestMetaOrgRefererUrl())
|
|
362
|
+
.send({
|
|
342
363
|
email: testUser.email,
|
|
343
364
|
password: testUser.password,
|
|
344
|
-
organizationId: testUser._orgId,
|
|
345
365
|
});
|
|
346
366
|
if (!response.body?.data?.tokens?.accessToken) {
|
|
347
367
|
console.error("Login failed:", response.body);
|
|
@@ -368,8 +388,9 @@ const testUtils = {
|
|
|
368
388
|
deleteMetaOrg,
|
|
369
389
|
deleteTestUser,
|
|
370
390
|
getAuthToken,
|
|
391
|
+
getTestMetaOrgRefererUrl,
|
|
371
392
|
initialize,
|
|
372
|
-
|
|
393
|
+
setupTestConfig,
|
|
373
394
|
loginWithTestUser,
|
|
374
395
|
newUser1Email,
|
|
375
396
|
newUser1Password,
|
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
import { Application } from
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
1
|
+
import { type Application } from "express";
|
|
2
|
+
import type { IDatabase } from "../databases/models/database.interface.js";
|
|
3
|
+
import type { ITestDatabase } from "./test-database.interface.js";
|
|
4
|
+
type TestExpressAppInitResult = {
|
|
5
|
+
app: Application;
|
|
6
|
+
database: IDatabase;
|
|
7
|
+
testDatabase: ITestDatabase;
|
|
8
|
+
agent: any;
|
|
9
|
+
};
|
|
10
|
+
declare function init(useMongoDb?: boolean): Promise<TestExpressAppInitResult>;
|
|
11
|
+
declare function setupErrorHandling(): Promise<void>;
|
|
12
|
+
declare function clearCollections(): Promise<void>;
|
|
13
|
+
declare function cleanup(): Promise<void>;
|
|
14
|
+
export declare const TestExpressApp: {
|
|
15
|
+
init: typeof init;
|
|
16
|
+
setupErrorHandling: typeof setupErrorHandling;
|
|
17
|
+
clearCollections: typeof clearCollections;
|
|
18
|
+
cleanup: typeof cleanup;
|
|
19
|
+
};
|
|
20
|
+
export {};
|
|
@@ -1,76 +1,80 @@
|
|
|
1
|
-
import
|
|
2
|
-
import bodyParser from
|
|
3
|
-
import cookieParser from
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return this.initPromise;
|
|
23
|
-
}
|
|
24
|
-
this.initPromise = this._performInit(useMongoDb);
|
|
25
|
-
return this.initPromise;
|
|
1
|
+
import { initializeTypeBox } from "@loomcore/common/validation";
|
|
2
|
+
import bodyParser from "body-parser";
|
|
3
|
+
import cookieParser from "cookie-parser";
|
|
4
|
+
import express from "express";
|
|
5
|
+
import supertest from "supertest";
|
|
6
|
+
import { ensureUserContext } from "../middleware/ensure-user-context.js";
|
|
7
|
+
import { errorHandler } from "../middleware/error-handler.js";
|
|
8
|
+
import { setupTestConfig } from "./common-test.utils.js";
|
|
9
|
+
import { TestMongoDatabase } from "./mongo-db.test-database.js";
|
|
10
|
+
import { TestPostgresDatabase } from "./postgres.test-database.js";
|
|
11
|
+
let app;
|
|
12
|
+
let database;
|
|
13
|
+
let testDatabase;
|
|
14
|
+
let initPromise = null;
|
|
15
|
+
async function init(useMongoDb) {
|
|
16
|
+
if (useMongoDb === undefined) {
|
|
17
|
+
const testDb = process.env.TEST_DATABASE;
|
|
18
|
+
useMongoDb = testDb === "mongodb";
|
|
19
|
+
}
|
|
20
|
+
if (initPromise) {
|
|
21
|
+
return initPromise;
|
|
26
22
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
this.testDatabase = testPostgresDb;
|
|
39
|
-
this.database = await testPostgresDb.init();
|
|
40
|
-
}
|
|
23
|
+
initPromise = performInit(useMongoDb);
|
|
24
|
+
return initPromise;
|
|
25
|
+
}
|
|
26
|
+
async function performInit(useMongoDb) {
|
|
27
|
+
setupTestConfig(true, useMongoDb ? "mongodb" : "postgres");
|
|
28
|
+
initializeTypeBox();
|
|
29
|
+
if (!database) {
|
|
30
|
+
if (useMongoDb) {
|
|
31
|
+
const testMongoDb = new TestMongoDatabase();
|
|
32
|
+
testDatabase = testMongoDb;
|
|
33
|
+
database = await testMongoDb.init();
|
|
41
34
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
this.app.use(ensureUserContext);
|
|
47
|
-
this.app.use((req, res, next) => {
|
|
48
|
-
next();
|
|
49
|
-
});
|
|
35
|
+
else {
|
|
36
|
+
const testPostgresDb = new TestPostgresDatabase();
|
|
37
|
+
testDatabase = testPostgresDb;
|
|
38
|
+
database = await testPostgresDb.init();
|
|
50
39
|
}
|
|
51
|
-
const agent = supertest.agent(this.app);
|
|
52
|
-
return {
|
|
53
|
-
app: this.app,
|
|
54
|
-
database: this.database,
|
|
55
|
-
testDatabase: this.testDatabase,
|
|
56
|
-
agent
|
|
57
|
-
};
|
|
58
40
|
}
|
|
59
|
-
|
|
60
|
-
|
|
41
|
+
if (!app) {
|
|
42
|
+
app = express();
|
|
43
|
+
app.use(bodyParser.json());
|
|
44
|
+
app.use(cookieParser());
|
|
45
|
+
app.use(ensureUserContext);
|
|
46
|
+
app.use((req, res, next) => {
|
|
47
|
+
next();
|
|
48
|
+
});
|
|
61
49
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
50
|
+
const agent = supertest.agent(app);
|
|
51
|
+
return {
|
|
52
|
+
app,
|
|
53
|
+
database,
|
|
54
|
+
testDatabase,
|
|
55
|
+
agent,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async function setupErrorHandling() {
|
|
59
|
+
app.use(errorHandler);
|
|
60
|
+
}
|
|
61
|
+
async function clearCollections() {
|
|
62
|
+
if (testDatabase) {
|
|
63
|
+
await testDatabase.clearCollections();
|
|
66
64
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
this.initPromise = null;
|
|
72
|
-
this.app = undefined;
|
|
73
|
-
this.database = undefined;
|
|
74
|
-
this.testDatabase = undefined;
|
|
65
|
+
}
|
|
66
|
+
async function cleanup() {
|
|
67
|
+
if (testDatabase) {
|
|
68
|
+
await testDatabase.cleanup();
|
|
75
69
|
}
|
|
70
|
+
initPromise = null;
|
|
71
|
+
app = undefined;
|
|
72
|
+
database = undefined;
|
|
73
|
+
testDatabase = undefined;
|
|
76
74
|
}
|
|
75
|
+
export const TestExpressApp = {
|
|
76
|
+
init,
|
|
77
|
+
setupErrorHandling,
|
|
78
|
+
clearCollections,
|
|
79
|
+
cleanup,
|
|
80
|
+
};
|
|
@@ -4,6 +4,11 @@ export declare let TEST_META_ORG_USER_ID: string | number;
|
|
|
4
4
|
export declare function setTestMetaOrgId(metaOrgId: string | number): void;
|
|
5
5
|
export declare function setTestMetaOrgUserId(userId: string | number): void;
|
|
6
6
|
export declare const TEST_META_ORG_USER_PASSWORD = "test-meta-org-user-password";
|
|
7
|
+
export declare const TEST_META_ORG_DOMAIN = "test-meta-org.example.com";
|
|
8
|
+
export declare const TEST_ORG_DOMAIN = "test-org.example.com";
|
|
9
|
+
export declare function getRefererUrlForOrg(org: Pick<IOrganization, "domain">): string;
|
|
10
|
+
export declare function getTestMetaOrgRefererUrl(): string;
|
|
11
|
+
export declare function getTestOrgRefererUrl(): string;
|
|
7
12
|
export declare function getTestMetaOrg(): IOrganization;
|
|
8
13
|
export declare function getTestMetaOrgUser(): IUser;
|
|
9
14
|
export declare function getTestMetaOrgUserContext(): IUserContext;
|
|
@@ -7,11 +7,26 @@ export function setTestMetaOrgUserId(userId) {
|
|
|
7
7
|
TEST_META_ORG_USER_ID = userId;
|
|
8
8
|
}
|
|
9
9
|
export const TEST_META_ORG_USER_PASSWORD = "test-meta-org-user-password";
|
|
10
|
+
export const TEST_META_ORG_DOMAIN = "test-meta-org.example.com";
|
|
11
|
+
export const TEST_ORG_DOMAIN = "test-org.example.com";
|
|
12
|
+
export function getRefererUrlForOrg(org) {
|
|
13
|
+
if (!org.domain) {
|
|
14
|
+
throw new Error("Organization domain is required for auth referer URL");
|
|
15
|
+
}
|
|
16
|
+
return `https://${org.domain}`;
|
|
17
|
+
}
|
|
18
|
+
export function getTestMetaOrgRefererUrl() {
|
|
19
|
+
return getRefererUrlForOrg(getTestMetaOrg());
|
|
20
|
+
}
|
|
21
|
+
export function getTestOrgRefererUrl() {
|
|
22
|
+
return getRefererUrlForOrg(getTestOrg());
|
|
23
|
+
}
|
|
10
24
|
export function getTestMetaOrg() {
|
|
11
25
|
return {
|
|
12
26
|
_id: TEST_META_ORG_ID,
|
|
13
27
|
name: "Test Meta Organization",
|
|
14
28
|
code: "test-meta-org",
|
|
29
|
+
domain: TEST_META_ORG_DOMAIN,
|
|
15
30
|
status: 1,
|
|
16
31
|
isMetaOrg: true,
|
|
17
32
|
_created: new Date(),
|
|
@@ -58,6 +73,7 @@ export function getTestOrg() {
|
|
|
58
73
|
_id: TEST_ORG_ID,
|
|
59
74
|
name: "Test Organization",
|
|
60
75
|
code: "test-org",
|
|
76
|
+
domain: TEST_ORG_DOMAIN,
|
|
61
77
|
status: 1,
|
|
62
78
|
isMetaOrg: false,
|
|
63
79
|
_created: new Date(),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { IDatabase } from "../databases/models/index.js";
|
|
2
|
-
import { IBaseApiConfig } from "../models/index.js";
|
|
1
|
+
import type { IDatabase } from "../databases/models/index.js";
|
|
2
|
+
import type { IBaseApiConfig } from "../models/index.js";
|
|
3
3
|
export declare let config: IBaseApiConfig;
|
|
4
4
|
export declare function setBaseApiConfig(theConfig: IBaseApiConfig): void;
|
|
5
5
|
export declare function initSystemUserContext(database: IDatabase): Promise<void>;
|
|
@@ -36,7 +36,7 @@ export async function initSystemUserContext(database) {
|
|
|
36
36
|
}
|
|
37
37
|
if (!isSystemUserContextSet) {
|
|
38
38
|
const systemEmail = config.email?.systemEmailAddress || "system@example.com";
|
|
39
|
-
let metaOrg =
|
|
39
|
+
let metaOrg = null;
|
|
40
40
|
if (config.app.isMultiTenant) {
|
|
41
41
|
const { OrganizationService } = await import("../services/organization.service.js");
|
|
42
42
|
const organizationService = new OrganizationService(database);
|
|
@@ -45,7 +45,7 @@ export async function initSystemUserContext(database) {
|
|
|
45
45
|
throw new Error("Meta organization not found. Please create an organization with isMetaOrg=true before starting the API.");
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
-
initializeSystemUserContext(systemEmail, metaOrg);
|
|
48
|
+
initializeSystemUserContext(systemEmail, metaOrg ?? undefined);
|
|
49
49
|
isSystemUserContextSet = true;
|
|
50
50
|
}
|
|
51
51
|
else if (config.env !== "test") {
|
|
@@ -8,7 +8,6 @@ export declare class AuthController {
|
|
|
8
8
|
constructor(app: Application, database: IDatabase);
|
|
9
9
|
mapRoutes(app: Application): void;
|
|
10
10
|
login(req: Request, res: Response): Promise<void>;
|
|
11
|
-
registerUser(req: Request, res: Response): Promise<void>;
|
|
12
11
|
requestTokenUsingRefreshToken(req: Request, res: Response): Promise<void>;
|
|
13
12
|
getUserContext(req: Request, res: Response): Promise<void>;
|
|
14
13
|
afterAuth(_req: Request, _res: Response, _loginResponse: any): void;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { EmptyUserContext, LoginResponseSpec, PublicUserContextSpec,
|
|
1
|
+
import { EmptyUserContext, LoginResponseSpec, PublicUserContextSpec, passwordValidator, TokenResponseSpec, UserSpec, } from "@loomcore/common/models";
|
|
2
2
|
import { entityUtils } from "@loomcore/common/utils";
|
|
3
3
|
import { config } from "../config/base-api-config.js";
|
|
4
|
-
import { BadRequestError,
|
|
4
|
+
import { BadRequestError, UnauthenticatedError } from "../errors/index.js";
|
|
5
5
|
import { isAuthorized } from "../middleware/index.js";
|
|
6
6
|
import { OrganizationService, UserService } from "../services/index.js";
|
|
7
7
|
import { attemptLogin, changePassword, getAndSetDeviceIdCookie, getDeviceIdFromCookie, requestTokenUsingRefreshToken, resetPassword, sendResetPasswordEmail, } from "../utils/auth/index.js";
|
|
@@ -18,7 +18,6 @@ export class AuthController {
|
|
|
18
18
|
}
|
|
19
19
|
mapRoutes(app) {
|
|
20
20
|
app.post(`/api/auth/login`, this.login.bind(this), this.afterAuth.bind(this));
|
|
21
|
-
app.post(`/api/auth/register`, isAuthorized(), this.registerUser.bind(this));
|
|
22
21
|
app.get(`/api/auth/refresh`, this.requestTokenUsingRefreshToken.bind(this));
|
|
23
22
|
app.get(`/api/auth/get-user-context`, isAuthorized(), this.getUserContext.bind(this));
|
|
24
23
|
app.patch(`/api/auth/change-password`, isAuthorized(), this.changePassword.bind(this));
|
|
@@ -26,35 +25,31 @@ export class AuthController {
|
|
|
26
25
|
app.post(`/api/auth/reset-password`, this.resetPassword.bind(this));
|
|
27
26
|
}
|
|
28
27
|
async login(req, res) {
|
|
29
|
-
const { email, password
|
|
28
|
+
const { email, password } = req.body;
|
|
30
29
|
if (!email || typeof email !== "string") {
|
|
31
30
|
throw new BadRequestError("Missing required fields: email is required.");
|
|
32
31
|
}
|
|
33
32
|
if (!password || typeof password !== "string") {
|
|
34
33
|
throw new BadRequestError("Missing required fields: password is required.");
|
|
35
34
|
}
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
let organization = null;
|
|
36
|
+
if (config.app.isMultiTenant) {
|
|
37
|
+
const referer = req.get("referer") || req.headers.referer;
|
|
38
|
+
if (!referer) {
|
|
39
|
+
throw new BadRequestError("Missing required fields: referer is required.");
|
|
40
|
+
}
|
|
41
|
+
organization = await this.organizationService.findOne(EmptyUserContext, {
|
|
42
|
+
filters: { domain: { eq: referer.split("/")[2] } },
|
|
43
|
+
});
|
|
44
|
+
if (!organization) {
|
|
45
|
+
throw new BadRequestError("Missing required fields: organization is required.");
|
|
46
|
+
}
|
|
38
47
|
}
|
|
39
48
|
res.set("Content-Type", "application/json");
|
|
40
49
|
const deviceId = getAndSetDeviceIdCookie(req, res);
|
|
41
|
-
const loginResponse = await attemptLogin(this.database, email, password, deviceId,
|
|
50
|
+
const loginResponse = await attemptLogin(this.database, email, password, deviceId, organization);
|
|
42
51
|
apiUtils.apiResponse(res, 200, { data: loginResponse }, LoginResponseSpec);
|
|
43
52
|
}
|
|
44
|
-
async registerUser(req, res) {
|
|
45
|
-
const userContext = req.userContext;
|
|
46
|
-
if (!userContext) {
|
|
47
|
-
throw new BadRequestError("Missing required fields: userContext is required.");
|
|
48
|
-
}
|
|
49
|
-
const body = req.body;
|
|
50
|
-
const validationErrors = this.userService.validate(body.user);
|
|
51
|
-
entityUtils.handleValidationResult(validationErrors, "AuthController.registerUser");
|
|
52
|
-
const user = await this.userService.create(userContext, body.user);
|
|
53
|
-
if (!user) {
|
|
54
|
-
throw new ServerError("Failed to create user");
|
|
55
|
-
}
|
|
56
|
-
apiUtils.apiResponse(res, 201, { data: user }, UserSpec, PublicUserSpec);
|
|
57
|
-
}
|
|
58
53
|
async requestTokenUsingRefreshToken(req, res) {
|
|
59
54
|
const userContext = req.userContext;
|
|
60
55
|
if (!userContext) {
|
|
@@ -91,23 +86,23 @@ export class AuthController {
|
|
|
91
86
|
}
|
|
92
87
|
async forgotPassword(req, res) {
|
|
93
88
|
const email = req.body?.email;
|
|
94
|
-
const organizationId = req.body?.organizationId;
|
|
95
89
|
if (!email || typeof email !== "string") {
|
|
96
90
|
throw new BadRequestError("Missing required fields: email is required.");
|
|
97
91
|
}
|
|
98
|
-
if (config.app.isMultiTenant && !organizationId) {
|
|
99
|
-
throw new BadRequestError("Missing required fields: organizationId is required.");
|
|
100
|
-
}
|
|
101
92
|
let referer = req.get("referer") || req.headers.referer;
|
|
102
93
|
if (!referer) {
|
|
103
94
|
throw new BadRequestError("Missing required fields: referer is required.");
|
|
104
95
|
}
|
|
105
96
|
referer = referer.replace(/\/$/, "");
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
97
|
+
let organization = null;
|
|
98
|
+
if (config.app.isMultiTenant) {
|
|
99
|
+
organization = await this.organizationService.findOne(EmptyUserContext, {
|
|
100
|
+
filters: { domain: { eq: referer.split("/")[2] } },
|
|
101
|
+
});
|
|
102
|
+
if (!organization) {
|
|
103
|
+
throw new BadRequestError("Missing required fields: organization is required.");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
111
106
|
const userContext = {
|
|
112
107
|
...EmptyUserContext,
|
|
113
108
|
organization: organization || undefined,
|
|
@@ -121,14 +116,24 @@ export class AuthController {
|
|
|
121
116
|
apiUtils.apiResponse(res, 200);
|
|
122
117
|
}
|
|
123
118
|
async resetPassword(req, res) {
|
|
124
|
-
const { email, token, password
|
|
119
|
+
const { email, token, password } = req.body;
|
|
125
120
|
if (!email || !token || !password) {
|
|
126
121
|
throw new BadRequestError("Missing required fields: email, token, and password are required.");
|
|
127
122
|
}
|
|
128
|
-
|
|
129
|
-
|
|
123
|
+
let organization = null;
|
|
124
|
+
if (config.app.isMultiTenant) {
|
|
125
|
+
const referer = req.get("referer") || req.headers.referer;
|
|
126
|
+
if (!referer) {
|
|
127
|
+
throw new BadRequestError("Missing required fields: referer is required.");
|
|
128
|
+
}
|
|
129
|
+
organization = await this.organizationService.findOne(EmptyUserContext, {
|
|
130
|
+
filters: { domain: { eq: referer.split("/")[2] } },
|
|
131
|
+
});
|
|
132
|
+
if (!organization) {
|
|
133
|
+
throw new BadRequestError("Missing required fields: organization is required.");
|
|
134
|
+
}
|
|
130
135
|
}
|
|
131
|
-
const response = await resetPassword(this.database, email, token, password,
|
|
136
|
+
const response = await resetPassword(this.database, email, token, password, organization);
|
|
132
137
|
apiUtils.apiResponse(res, 200, { data: response });
|
|
133
138
|
}
|
|
134
139
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Umzug } from 'umzug';
|
|
2
|
+
import { TEST_META_ORG_DOMAIN } from '../../../../__tests__/test-objects.js';
|
|
2
3
|
import { getPostgresInitialSchema } from '../postgres-initial-schema.js';
|
|
3
4
|
import { getPostgresTestSchema } from '../../../../__tests__/postgres-test-migrations/postgres-test-schema.js';
|
|
4
5
|
export async function runInitialSchemaMigrations(pool, config) {
|
|
@@ -7,7 +8,7 @@ export async function runInitialSchemaMigrations(pool, config) {
|
|
|
7
8
|
app: config.app,
|
|
8
9
|
database: config.database,
|
|
9
10
|
adminUser: config.adminUser ?? { email: 'admin@test.com', password: 'admin-password' },
|
|
10
|
-
multiTenant: config.multiTenant ?? (config.app.isMultiTenant ? { metaOrgName: 'Test Meta Organization', metaOrgCode: 'TEST_META_ORG' } : { metaOrgName: '', metaOrgCode: '' }),
|
|
11
|
+
multiTenant: config.multiTenant ?? (config.app.isMultiTenant ? { metaOrgName: 'Test Meta Organization', metaOrgCode: 'TEST_META_ORG', metaOrgDomain: TEST_META_ORG_DOMAIN } : { metaOrgName: '', metaOrgCode: '', metaOrgDomain: '' }),
|
|
11
12
|
email: config.email,
|
|
12
13
|
};
|
|
13
14
|
const initialSchema = getPostgresInitialSchema(migrationConfig);
|
|
@@ -33,7 +33,8 @@ export const getPostgresInitialSchema = (dbConfig) => {
|
|
|
33
33
|
CREATE TABLE IF NOT EXISTS "organizations" (
|
|
34
34
|
"_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
35
35
|
"name" VARCHAR(255) NOT NULL UNIQUE,
|
|
36
|
-
"code" VARCHAR(255)
|
|
36
|
+
"code" VARCHAR(255) UNIQUE,
|
|
37
|
+
"domain" VARCHAR(255) UNIQUE,
|
|
37
38
|
"description" TEXT,
|
|
38
39
|
"status" INTEGER NOT NULL,
|
|
39
40
|
"is_meta_org" BOOLEAN NOT NULL,
|
|
@@ -297,12 +298,13 @@ export const getPostgresInitialSchema = (dbConfig) => {
|
|
|
297
298
|
name: "00000000000011_data-meta-org",
|
|
298
299
|
up: async ({ context: pool }) => {
|
|
299
300
|
const result = await pool.query(`
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
301
|
+
INSERT INTO "organizations"("name", "code", "domain", "status", "is_meta_org", "_created", "_createdBy")
|
|
302
|
+
VALUES($1, $2, $3, 1, true, NOW(), 0)
|
|
303
|
+
RETURNING "_id", "name", "code", "domain", "status", "is_meta_org", "_created", "_createdBy"
|
|
304
|
+
`, [
|
|
304
305
|
dbConfig.multiTenant?.metaOrgName,
|
|
305
306
|
dbConfig.multiTenant?.metaOrgCode,
|
|
307
|
+
dbConfig.multiTenant?.metaOrgDomain,
|
|
306
308
|
]);
|
|
307
309
|
if (result.rowCount === 0) {
|
|
308
310
|
throw new Error("Failed to create meta organization");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export * from
|
|
2
|
-
export * from
|
|
3
|
-
export * from
|
|
4
|
-
export * from
|
|
1
|
+
export * from "./ensure-user-context.js";
|
|
2
|
+
export * from "./error-handler.js";
|
|
3
|
+
export * from "./is-authorized.js";
|
|
4
|
+
export * from "./request-lifecycle.js";
|
package/dist/middleware/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export * from
|
|
2
|
-
export * from
|
|
3
|
-
export * from
|
|
4
|
-
export * from
|
|
1
|
+
export * from "./ensure-user-context.js";
|
|
2
|
+
export * from "./error-handler.js";
|
|
3
|
+
export * from "./is-authorized.js";
|
|
4
|
+
export * from "./request-lifecycle.js";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { IAppConfig } from "./app-config.interface.js";
|
|
2
|
-
import { IAuthConfig } from "./auth-config.interface.js";
|
|
3
|
-
import { IDatabaseConfig } from "./database-config.interface.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import type { IAppConfig } from "./app-config.interface.js";
|
|
2
|
+
import type { IAuthConfig } from "./auth-config.interface.js";
|
|
3
|
+
import type { IDatabaseConfig } from "./database-config.interface.js";
|
|
4
|
+
import type { IEmailClient } from "./email-client.interface.js";
|
|
5
|
+
import type { IEmailConfig } from "./email-config.interface.js";
|
|
6
6
|
export interface IBaseApiConfig {
|
|
7
7
|
app: IAppConfig;
|
|
8
8
|
auth?: IAuthConfig;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { IAppConfig } from "./app-config.interface.js";
|
|
2
|
-
import { IDatabaseConfig } from "./database-config.interface.js";
|
|
3
|
-
import { IEmailConfig } from "./email-config.interface.js";
|
|
1
|
+
import type { IAppConfig } from "./app-config.interface.js";
|
|
2
|
+
import type { IDatabaseConfig } from "./database-config.interface.js";
|
|
3
|
+
import type { IEmailConfig } from "./email-config.interface.js";
|
|
4
4
|
export interface IInitialDbMigrationConfig {
|
|
5
5
|
env: string;
|
|
6
6
|
app: IAppConfig;
|
|
@@ -12,6 +12,7 @@ export interface IInitialDbMigrationConfig {
|
|
|
12
12
|
multiTenant: {
|
|
13
13
|
metaOrgName: string;
|
|
14
14
|
metaOrgCode: string;
|
|
15
|
+
metaOrgDomain: string;
|
|
15
16
|
};
|
|
16
17
|
email?: IEmailConfig;
|
|
17
18
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { type ILoginResponse } from "@loomcore/common/models";
|
|
2
|
-
import type { AppIdType } from "@loomcore/common/types";
|
|
1
|
+
import { type ILoginResponse, type IOrganization } from "@loomcore/common/models";
|
|
3
2
|
import type { IDatabase } from "../../databases/models/index.js";
|
|
4
|
-
export declare function attemptLogin(database: IDatabase, email: string, password: string, deviceId: string,
|
|
3
|
+
export declare function attemptLogin(database: IDatabase, email: string, password: string, deviceId: string, organization: IOrganization | null): Promise<ILoginResponse | null>;
|
|
@@ -1,20 +1,10 @@
|
|
|
1
1
|
import { EmptyUserContext, } from "@loomcore/common/models";
|
|
2
2
|
import { BadRequestError } from "../../errors/index.js";
|
|
3
|
-
import { OrganizationService } from "../../services/organization.service.js";
|
|
4
3
|
import { UserService } from "../../services/user.service.js";
|
|
5
4
|
import { getUserContextAuthorizations } from "../../services/utils/getUserContextAuthorizations.util.js";
|
|
6
5
|
import { passwordUtils } from "../password.utils.js";
|
|
7
6
|
import { logUserIn } from "./log-user-in.util.js";
|
|
8
|
-
export async function attemptLogin(database, email, password, deviceId,
|
|
9
|
-
const organizationService = new OrganizationService(database);
|
|
10
|
-
const organization = organizationId
|
|
11
|
-
? await organizationService.findOne(EmptyUserContext, {
|
|
12
|
-
filters: { _id: { eq: organizationId } },
|
|
13
|
-
})
|
|
14
|
-
: null;
|
|
15
|
-
if (organizationId && !organization) {
|
|
16
|
-
throw new BadRequestError("Invalid Credentials");
|
|
17
|
-
}
|
|
7
|
+
export async function attemptLogin(database, email, password, deviceId, organization) {
|
|
18
8
|
const lowerCaseEmail = email.toLowerCase();
|
|
19
9
|
const userContext = {
|
|
20
10
|
...EmptyUserContext,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type IOrganization } from "@loomcore/common/models";
|
|
2
2
|
import type { IDatabase } from "../../databases/models/index.js";
|
|
3
3
|
import type { UpdateResult } from "../../databases/models/update-result.js";
|
|
4
|
-
export declare function resetPassword(database: IDatabase, email: string, passwordResetToken: string, password: string,
|
|
4
|
+
export declare function resetPassword(database: IDatabase, email: string, passwordResetToken: string, password: string, organization: IOrganization | null): Promise<UpdateResult>;
|
|
@@ -1,26 +1,15 @@
|
|
|
1
1
|
import { EmptyUserContext, passwordValidator, UserSpec, } from "@loomcore/common/models";
|
|
2
2
|
import { entityUtils } from "@loomcore/common/utils";
|
|
3
|
-
import { config } from "../../config/base-api-config.js";
|
|
4
3
|
import { BadRequestError, ServerError } from "../../errors/index.js";
|
|
5
|
-
import { OrganizationService } from "../../services/organization.service.js";
|
|
6
4
|
import { PasswordResetTokenService } from "../../services/password-reset-token.service.js";
|
|
7
5
|
import { UserService } from "../../services/user.service.js";
|
|
8
6
|
import { changePassword } from "./change-password.util.js";
|
|
9
|
-
export async function resetPassword(database, email, passwordResetToken, password,
|
|
7
|
+
export async function resetPassword(database, email, passwordResetToken, password, organization) {
|
|
10
8
|
const validationErrors = entityUtils.validate(UserSpec, { password: password }, true, passwordValidator);
|
|
11
9
|
entityUtils.handleValidationResult(validationErrors, "AuthService.resetPassword");
|
|
12
|
-
if (config.app.isMultiTenant && !organizationId) {
|
|
13
|
-
throw new BadRequestError("Missing required fields: organizationId is required.");
|
|
14
|
-
}
|
|
15
10
|
const lowerCaseEmail = email.toLowerCase();
|
|
16
|
-
const organizationService = new OrganizationService(database);
|
|
17
11
|
const passwordResetTokenService = new PasswordResetTokenService(database);
|
|
18
12
|
const userService = new UserService(database);
|
|
19
|
-
const organization = organizationId
|
|
20
|
-
? await organizationService.findOne(EmptyUserContext, {
|
|
21
|
-
filters: { _id: { eq: organizationId } },
|
|
22
|
-
})
|
|
23
|
-
: null;
|
|
24
13
|
const userContext = {
|
|
25
14
|
...EmptyUserContext,
|
|
26
15
|
organization: organization ?? undefined,
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type IOrganization } from "@loomcore/common/models";
|
|
2
2
|
import type { IDatabase } from "../../databases/models/index.js";
|
|
3
|
-
export declare function sendResetPasswordEmail(database: IDatabase, emailAddress: string, clientBaseUrl: string,
|
|
3
|
+
export declare function sendResetPasswordEmail(database: IDatabase, emailAddress: string, clientBaseUrl: string, organization?: IOrganization): Promise<void>;
|
|
@@ -1,20 +1,13 @@
|
|
|
1
1
|
import { EmptyUserContext, } from "@loomcore/common/models";
|
|
2
2
|
import { ServerError } from "../../errors/index.js";
|
|
3
3
|
import { EmailService } from "../../services/email.service.js";
|
|
4
|
-
import { OrganizationService } from "../../services/organization.service.js";
|
|
5
4
|
import { PasswordResetTokenService } from "../../services/password-reset-token.service.js";
|
|
6
5
|
import { getAuthConfig } from "./get-auth-config.util.js";
|
|
7
6
|
import { getExpiresOnFromMinutes } from "./get-expires-on-from-minutes.util.js";
|
|
8
|
-
export async function sendResetPasswordEmail(database, emailAddress, clientBaseUrl,
|
|
7
|
+
export async function sendResetPasswordEmail(database, emailAddress, clientBaseUrl, organization) {
|
|
9
8
|
const authConfig = getAuthConfig();
|
|
10
|
-
const organizationService = new OrganizationService(database);
|
|
11
9
|
const passwordResetTokenService = new PasswordResetTokenService(database);
|
|
12
10
|
const emailService = new EmailService();
|
|
13
|
-
const organization = organizationId
|
|
14
|
-
? await organizationService.findOne(EmptyUserContext, {
|
|
15
|
-
filters: { _id: { eq: organizationId } },
|
|
16
|
-
})
|
|
17
|
-
: null;
|
|
18
11
|
const userContext = {
|
|
19
12
|
...EmptyUserContext,
|
|
20
13
|
organization: organization ?? undefined,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loomcore/api",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
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": {
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"qs": "^6.15.2"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
|
-
"@loomcore/common": "^0.0.
|
|
61
|
+
"@loomcore/common": "^0.0.74",
|
|
62
62
|
"@sinclair/typebox": "0.34.33",
|
|
63
63
|
"cookie-parser": "^1.4.6",
|
|
64
64
|
"cors": "^2.8.5",
|