@loomcore/api 0.2.32 → 0.2.35

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.
@@ -110,16 +110,12 @@ let AuthController = (() => {
110
110
  apiUtils.apiResponse(res, 200, { data: loginResponse }, this.loginResponseSpec);
111
111
  }
112
112
  async requestTokenUsingRefreshToken(req, res) {
113
- const userContext = req.userContext;
114
- if (!userContext) {
115
- throw new BadRequestError("Missing required fields: userContext is required.");
116
- }
117
113
  const refreshToken = req.query.refreshToken;
118
114
  if (!refreshToken || typeof refreshToken !== "string") {
119
115
  throw new BadRequestError("Missing required fields: refreshToken is required.");
120
116
  }
121
117
  const deviceId = getDeviceIdFromCookie(req);
122
- const tokens = await requestTokenUsingRefreshToken(this.database, userContext, refreshToken, deviceId);
118
+ const tokens = await requestTokenUsingRefreshToken(this.database, refreshToken, deviceId);
123
119
  if (!tokens) {
124
120
  throw new UnauthenticatedError();
125
121
  }
@@ -58,6 +58,9 @@ export const getMongoInitialSchema = (dbConfig) => {
58
58
  .collection("users")
59
59
  .createIndex({ email: 1 }, { unique: true });
60
60
  }
61
+ await db
62
+ .collection("users")
63
+ .createIndex({ externalId: 1 }, { unique: true, sparse: true });
61
64
  },
62
65
  down: async ({ context: db }) => {
63
66
  await db.collection("users").drop();
@@ -256,7 +259,6 @@ export const getMongoInitialSchema = (dbConfig) => {
256
259
  const email = dbConfig.adminUser.email.toLowerCase();
257
260
  await db.collection("users").insertOne({
258
261
  ...orgDoc,
259
- externalId: "admin-user-external-id",
260
262
  email,
261
263
  password: hashedPassword,
262
264
  displayName: "Admin User",
@@ -18,5 +18,5 @@ export function buildMongoUrl(config) {
18
18
  if (!port) {
19
19
  throw new Error("Database configuration must include port to build a non-SRV MongoDB URL.");
20
20
  }
21
- return `mongodb://${encodedUsername}:${encodedPassword}@${host}:${port}/${name}`;
21
+ return `mongodb://${encodedUsername}:${encodedPassword}@${host}:${port}/${name}?authSource=admin`;
22
22
  }
@@ -30,5 +30,4 @@ export declare class PostgresDatabase implements IDatabase {
30
30
  deleteMany(queryObject: IQueryOptions, pluralResourceName: string): Promise<DeleteResult>;
31
31
  find<T extends IEntity>(queryObject: IQueryOptions, pluralResourceName: string): Promise<T[]>;
32
32
  findOne<T extends IEntity>(queryObject: IQueryOptions, pluralResourceName: string): Promise<T | null>;
33
- getUserFeatures(userId: AppIdType, orgId?: AppIdType): Promise<string[]>;
34
33
  }
@@ -77,31 +77,4 @@ export class PostgresDatabase {
77
77
  async findOne(queryObject, pluralResourceName) {
78
78
  return findOneQuery(this.connection, queryObject, pluralResourceName);
79
79
  }
80
- async getUserFeatures(userId, orgId) {
81
- const now = new Date();
82
- let query = `
83
- SELECT DISTINCT
84
- r."name" as "role",
85
- f."name" as "feature",
86
- a."config",
87
- a."_id",
88
- a."_orgId"
89
- FROM "user_roles" ur
90
- INNER JOIN "roles" r ON ur."role_id" = r."_id"
91
- INNER JOIN "authorizations" a ON r."_id" = a."role_id"
92
- INNER JOIN "features" f ON a."feature_id" = f."_id"
93
- WHERE ur."user_id" = $1
94
- AND ur."_deleted" IS NULL
95
- AND a."_deleted" IS NULL
96
- AND (a."start_date" IS NULL OR a."start_date" <= $2)
97
- AND (a."end_date" IS NULL OR a."end_date" >= $2)
98
- `;
99
- const values = [userId, now];
100
- if (orgId) {
101
- query += ` AND ur."_orgId" = $3 AND r."_orgId" = $3 AND a."_orgId" = $3 AND f."_orgId" = $3`;
102
- values.push(orgId);
103
- }
104
- const result = await this.connection.query(query, values);
105
- return result.rows.map((row) => row.feature);
106
- }
107
80
  }
@@ -1,7 +1,10 @@
1
- import { IAuthorization, IUserContext } from "@loomcore/common/models";
2
- import { IDatabase } from "../databases/index.js";
1
+ import { type IAuthorization, type IUser, type IUserContext } from "@loomcore/common/models";
2
+ import type { IDatabase } from "../databases/index.js";
3
3
  import { MultiTenantApiService } from "./multi-tenant-api.service.js";
4
4
  export declare class AuthorizationsService extends MultiTenantApiService<IAuthorization> {
5
+ private userRolesService;
6
+ private featuresService;
5
7
  constructor(database: IDatabase);
6
8
  preProcessEntity(userContext: IUserContext, entity: Partial<IAuthorization>, isCreate: boolean, allowId?: boolean): Promise<Partial<IAuthorization>>;
9
+ getUserContextFeatures(user: IUser): Promise<string[]>;
7
10
  }
@@ -1,12 +1,62 @@
1
- import { AuthorizationModelSpec } from "@loomcore/common/models";
2
- import { MultiTenantApiService } from "./multi-tenant-api.service.js";
1
+ import { AuthorizationModelSpec, getSystemUserContext, } from "@loomcore/common/models";
3
2
  import { assertUserHasFeature } from "../utils/index.js";
3
+ import { FeaturesService } from "./features.service.js";
4
+ import { MultiTenantApiService } from "./multi-tenant-api.service.js";
5
+ import { UserRolesService } from "./user-roles.service.js";
4
6
  export class AuthorizationsService extends MultiTenantApiService {
7
+ userRolesService;
8
+ featuresService;
5
9
  constructor(database) {
6
10
  super(database, "authorizations", "authorization", AuthorizationModelSpec);
11
+ this.userRolesService = new UserRolesService(database);
12
+ this.featuresService = new FeaturesService(database);
7
13
  }
8
14
  async preProcessEntity(userContext, entity, isCreate, allowId = true) {
9
- assertUserHasFeature(userContext, ['admin', 'system', 'authorizations']);
15
+ assertUserHasFeature(userContext, ["admin", "system", "authorizations"]);
10
16
  return super.preProcessEntity(userContext, entity, isCreate, allowId);
11
17
  }
18
+ async getUserContextFeatures(user) {
19
+ const systemUserContext = getSystemUserContext();
20
+ const now = new Date();
21
+ const userRoleQueryOptions = {
22
+ filters: { userId: { eq: user._id } },
23
+ };
24
+ if (user._orgId !== undefined) {
25
+ userRoleQueryOptions.filters._orgId = { eq: user._orgId };
26
+ }
27
+ const userRoles = await this.userRolesService.find(systemUserContext, userRoleQueryOptions);
28
+ const roleIds = [
29
+ ...new Set(userRoles
30
+ .filter((userRole) => !userRole._deleted)
31
+ .map((userRole) => userRole.roleId)),
32
+ ];
33
+ if (roleIds.length === 0) {
34
+ return [];
35
+ }
36
+ const authorizationQuery = {
37
+ filters: { roleId: { in: roleIds } },
38
+ };
39
+ if (user._orgId !== undefined) {
40
+ authorizationQuery.filters._orgId = { eq: user._orgId };
41
+ }
42
+ const authorizations = await this.find(systemUserContext, authorizationQuery);
43
+ const featureIds = [
44
+ ...new Set(authorizations
45
+ .filter((authorization) => !authorization._deleted &&
46
+ (!authorization.startDate || authorization.startDate <= now) &&
47
+ (!authorization.endDate || authorization.endDate >= now))
48
+ .map((authorization) => authorization.featureId)),
49
+ ];
50
+ if (featureIds.length === 0) {
51
+ return [];
52
+ }
53
+ const featureQuery = {
54
+ filters: { _id: { in: featureIds } },
55
+ };
56
+ if (user._orgId !== undefined) {
57
+ featureQuery.filters._orgId = { eq: user._orgId };
58
+ }
59
+ const features = await this.featuresService.find(systemUserContext, featureQuery);
60
+ return [...new Set(features.map((feature) => feature.name))];
61
+ }
12
62
  }
@@ -1,3 +1,4 @@
1
+ import { getSystemUserId } from "@loomcore/common/validation";
1
2
  import { config } from "../config/base-api-config.js";
2
3
  import { BadRequestError } from "../errors/bad-request.error.js";
3
4
  import { ServerError } from "../errors/index.js";
@@ -12,7 +13,8 @@ export class MultiTenantApiService extends GenericApiService {
12
13
  }
13
14
  }
14
15
  prepareQuery(userContext, queryOptions, operations) {
15
- if (!config?.app?.isMultiTenant || userContext?.user?._id === "system") {
16
+ if (!config?.app?.isMultiTenant ||
17
+ userContext?.user?._id === getSystemUserId()) {
16
18
  return super.prepareQuery(userContext, queryOptions, operations);
17
19
  }
18
20
  if (!userContext?.organization?._id) {
@@ -32,7 +34,7 @@ export class MultiTenantApiService extends GenericApiService {
32
34
  throw new BadRequestError("A valid userContext was not provided to MultiTenantApiService.prepareEntity");
33
35
  }
34
36
  const preparedEntity = await super.preProcessEntity(userContext, entity, isCreate, allowId);
35
- if (isCreate && userContext.user._id !== "system") {
37
+ if (isCreate && userContext.user._id !== getSystemUserId()) {
36
38
  preparedEntity._orgId = userContext.organization?._id;
37
39
  }
38
40
  return preparedEntity;
@@ -1,4 +1,5 @@
1
1
  import { type ILoginResponse, type IOrganization } from "@loomcore/common/models";
2
2
  import type { IDatabase } from "../../databases/models/index.js";
3
+ import { AuthorizationsService } from "../../services/authorizations.service.js";
3
4
  import { UserService } from "../../services/user.service.js";
4
- export declare function attemptLogin(database: IDatabase, email: string, password: string, deviceId: string, organization: IOrganization | null, userService?: UserService): Promise<ILoginResponse>;
5
+ export declare function attemptLogin(database: IDatabase, email: string, password: string, deviceId: string, organization: IOrganization | null, userService?: UserService, authorizationsService?: AuthorizationsService): Promise<ILoginResponse>;
@@ -1,10 +1,10 @@
1
1
  import { EmptyUserContext, } from "@loomcore/common/models";
2
2
  import { BadRequestError } from "../../errors/index.js";
3
+ import { AuthorizationsService } from "../../services/authorizations.service.js";
3
4
  import { UserService } from "../../services/user.service.js";
4
5
  import { passwordUtils } from "../password.utils.js";
5
6
  import { logUserIn } from "./log-user-in.util.js";
6
- import { getUserContextFeatures } from "../../services/utils/getUserContextAuthorizations.util.js";
7
- export async function attemptLogin(database, email, password, deviceId, organization, userService = new UserService(database)) {
7
+ export async function attemptLogin(database, email, password, deviceId, organization, userService = new UserService(database), authorizationsService = new AuthorizationsService(database)) {
8
8
  const lowerCaseEmail = email.toLowerCase();
9
9
  const userContext = {
10
10
  ...EmptyUserContext,
@@ -22,7 +22,7 @@ export async function attemptLogin(database, email, password, deviceId, organiza
22
22
  if (!passwordsMatch) {
23
23
  throw new BadRequestError("Invalid Credentials");
24
24
  }
25
- const features = await getUserContextFeatures(database, user);
25
+ const features = await authorizationsService.getUserContextFeatures(user);
26
26
  const authenticatedUserContext = {
27
27
  user: user,
28
28
  organization: organization ?? undefined,
@@ -1,3 +1,6 @@
1
- import type { ITokenResponse, IUserContext } from "@loomcore/common/models";
1
+ import { type ITokenResponse } from "@loomcore/common/models";
2
2
  import type { IDatabase } from "../../databases/models/index.js";
3
- export declare function requestTokenUsingRefreshToken(database: IDatabase, userContext: IUserContext, refreshToken: string, deviceId: string): Promise<ITokenResponse | null>;
3
+ import { AuthorizationsService } from "../../services/authorizations.service.js";
4
+ import { OrganizationService } from "../../services/organization.service.js";
5
+ import { UserService } from "../../services/user.service.js";
6
+ export declare function requestTokenUsingRefreshToken(database: IDatabase, refreshToken: string, deviceId: string, userService?: UserService, organizationService?: OrganizationService, authorizationsService?: AuthorizationsService): Promise<ITokenResponse | null>;
@@ -1,9 +1,42 @@
1
+ import { getSystemUserContext, } from "@loomcore/common/models";
2
+ import { AuthorizationsService } from "../../services/authorizations.service.js";
3
+ import { OrganizationService } from "../../services/organization.service.js";
4
+ import { UserService } from "../../services/user.service.js";
5
+ import { config } from "../../config/base-api-config.js";
1
6
  import { createNewTokens } from "./create-new-tokens.util.js";
2
7
  import { getActiveRefreshToken } from "./get-active-refresh-token.util.js";
3
- export async function requestTokenUsingRefreshToken(database, userContext, refreshToken, deviceId) {
4
- const activeRefreshToken = await getActiveRefreshToken(database, userContext, refreshToken, deviceId);
5
- if (activeRefreshToken) {
6
- return createNewTokens(userContext, activeRefreshToken);
8
+ import { BadRequestError } from "../../errors/index.js";
9
+ export async function requestTokenUsingRefreshToken(database, refreshToken, deviceId, userService = new UserService(database), organizationService = new OrganizationService(database), authorizationsService = new AuthorizationsService(database)) {
10
+ const systemUserContext = getSystemUserContext();
11
+ const activeRefreshToken = await getActiveRefreshToken(database, systemUserContext, refreshToken, deviceId);
12
+ if (!activeRefreshToken) {
13
+ return null;
7
14
  }
8
- return null;
15
+ let organization = null;
16
+ if (config.app.isMultiTenant && activeRefreshToken._orgId) {
17
+ organization =
18
+ await organizationService.findOne(systemUserContext, {
19
+ filters: { _id: { eq: activeRefreshToken._orgId } },
20
+ });
21
+ if (!organization) {
22
+ throw new BadRequestError("Organization not found");
23
+ }
24
+ }
25
+ let userQueryOptions = {
26
+ filters: { _id: { eq: activeRefreshToken.userId } },
27
+ };
28
+ if (organization) {
29
+ userQueryOptions.filters = { ...userQueryOptions.filters, _orgId: { eq: organization._id } };
30
+ }
31
+ const user = await userService.findOne(systemUserContext, userQueryOptions);
32
+ if (!user) {
33
+ throw new BadRequestError("User not found");
34
+ }
35
+ const features = await authorizationsService.getUserContextFeatures(user);
36
+ const userContext = {
37
+ user,
38
+ organization: organization ?? undefined,
39
+ features,
40
+ };
41
+ return createNewTokens(userContext, activeRefreshToken);
9
42
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loomcore/api",
3
- "version": "0.2.32",
3
+ "version": "0.2.35",
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": {
@@ -59,7 +59,7 @@
59
59
  "reflect-metadata": "^0.2.2"
60
60
  },
61
61
  "peerDependencies": {
62
- "@loomcore/common": "^0.0.80",
62
+ "@loomcore/common": "^0.0.81",
63
63
  "@sinclair/typebox": "0.34.33",
64
64
  "cookie-parser": "^1.4.6",
65
65
  "cors": "^2.8.5",
@@ -1,3 +0,0 @@
1
- import type { IUser } from "@loomcore/common/models";
2
- import type { IDatabase } from "../../databases/models/index.js";
3
- export declare function getUserContextFeatures(database: IDatabase, user: IUser): Promise<string[]>;
@@ -1,9 +0,0 @@
1
- import { PostgresDatabase } from "../../databases/postgres/postgres.database.js";
2
- export async function getUserContextFeatures(database, user) {
3
- if (!(database instanceof PostgresDatabase)) {
4
- return [];
5
- }
6
- const orgId = user._orgId;
7
- const features = await database.getUserFeatures(user._id, orgId);
8
- return features;
9
- }