@loomcore/api 0.2.8 → 0.2.9

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.
@@ -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
- SetupTestConfig: typeof setupTestConfig;
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 = "testone";
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
- await userService
139
- .deleteById(getTestMetaOrgUserContext(), getTestMetaOrgUser()._id)
140
- .catch((error) => {
141
- console.log("Error deleting test user:", error);
142
- });
143
- await organizationService
144
- .deleteById(getTestMetaOrgUserContext(), getTestOrg()._id)
145
- .catch((error) => {
146
- console.log("Error deleting test organization:", error);
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()._id);
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 = function (token, secret, options) {
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.post("/api/auth/login").send({
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
- SetupTestConfig: setupTestConfig,
393
+ setupTestConfig,
373
394
  loginWithTestUser,
374
395
  newUser1Email,
375
396
  newUser1Password,
@@ -1,19 +1,20 @@
1
- import { Application } from 'express';
2
- import { ITestDatabase } from './test-database.interface.js';
3
- import { IDatabase } from '../databases/models/database.interface.js';
4
- export declare class TestExpressApp {
5
- private static app;
6
- private static database;
7
- private static testDatabase;
8
- private static initPromise;
9
- static init(useMongoDb?: boolean): Promise<{
10
- app: Application;
11
- database: IDatabase;
12
- testDatabase: ITestDatabase;
13
- agent: any;
14
- }>;
15
- private static _performInit;
16
- static setupErrorHandling(): Promise<void>;
17
- static clearCollections(): Promise<void>;
18
- static cleanup(): Promise<void>;
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 express from 'express';
2
- import bodyParser from 'body-parser';
3
- import cookieParser from 'cookie-parser';
4
- import supertest from 'supertest';
5
- import { initializeTypeBox } from '@loomcore/common/validation';
6
- import { errorHandler } from '../middleware/error-handler.js';
7
- import { ensureUserContext } from '../middleware/ensure-user-context.js';
8
- import { TestMongoDatabase } from './mongo-db.test-database.js';
9
- import { TestPostgresDatabase } from './postgres.test-database.js';
10
- import { setupTestConfig } from './common-test.utils.js';
11
- export class TestExpressApp {
12
- static app;
13
- static database;
14
- static testDatabase;
15
- static initPromise = null;
16
- static async init(useMongoDb) {
17
- if (useMongoDb === undefined) {
18
- const testDb = process.env.TEST_DATABASE;
19
- useMongoDb = testDb === 'mongodb';
20
- }
21
- if (this.initPromise) {
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
- static async _performInit(useMongoDb) {
28
- setupTestConfig(true, useMongoDb ? 'mongodb' : 'postgres');
29
- initializeTypeBox();
30
- if (!this.database) {
31
- if (useMongoDb) {
32
- const testMongoDb = new TestMongoDatabase();
33
- this.testDatabase = testMongoDb;
34
- this.database = await testMongoDb.init();
35
- }
36
- else {
37
- const testPostgresDb = new TestPostgresDatabase();
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
- if (!this.app) {
43
- this.app = express();
44
- this.app.use(bodyParser.json());
45
- this.app.use(cookieParser());
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
- static async setupErrorHandling() {
60
- this.app.use(errorHandler);
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
- static async clearCollections() {
63
- if (this.testDatabase) {
64
- await this.testDatabase.clearCollections();
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
- static async cleanup() {
68
- if (this.testDatabase) {
69
- await this.testDatabase.cleanup();
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 = undefined;
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, PublicUserSpec, passwordValidator, TokenResponseSpec, UserSpec, } from "@loomcore/common/models";
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, ServerError, UnauthenticatedError, } from "../errors/index.js";
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, organizationId } = req.body;
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
- if (config.app.isMultiTenant && !organizationId) {
37
- throw new BadRequestError("Missing required fields: organizationId is required.");
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, organizationId);
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
- const organization = organizationId
107
- ? await this.organizationService.findOne(EmptyUserContext, {
108
- filters: { _id: { eq: organizationId } },
109
- })
110
- : null;
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, organizationId, } = req.body;
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
- if (config.app.isMultiTenant && !organizationId) {
129
- throw new BadRequestError("Missing required fields: organizationId is required.");
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, organizationId);
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) NOT NULL UNIQUE,
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
- INSERT INTO "organizations"("name", "code", "status", "is_meta_org", "_created", "_createdBy")
301
- VALUES($1, $2, 1, true, NOW(), 0)
302
- RETURNING "_id", "name", "code", "status", "is_meta_org", "_created", "_createdBy"
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 './error-handler.js';
2
- export * from './is-authorized.js';
3
- export * from './ensure-user-context.js';
4
- export * from './request-lifecycle.js';
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,4 +1,4 @@
1
- export * from './error-handler.js';
2
- export * from './is-authorized.js';
3
- export * from './ensure-user-context.js';
4
- export * from './request-lifecycle.js';
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,4 +1,4 @@
1
- import { DbType } from "../databases/db-type.type.js";
1
+ import type { DbType } from "../databases/db-type.type.js";
2
2
  export interface IAppConfig {
3
3
  dbType: DbType;
4
4
  isMultiTenant: boolean;
@@ -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 { IEmailConfig } from "./email-config.interface.js";
5
- import { IEmailClient } from "./email-client.interface.js";
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, organizationId?: AppIdType): Promise<ILoginResponse | null>;
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, organizationId) {
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 { AppIdType } from "@loomcore/common/types";
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, organizationId?: AppIdType): Promise<UpdateResult>;
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, organizationId) {
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 { AppIdType } from "@loomcore/common/types";
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, organizationId?: AppIdType): Promise<void>;
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, organizationId) {
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.8",
3
+ "version": "0.2.9",
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.72",
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",