@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.
- package/dist/controllers/auth.controller.js +1 -5
- package/dist/databases/mongo-db/migrations/mongo-initial-schema.js +3 -1
- package/dist/databases/mongo-db/utils/build-mongo-url.util.js +1 -1
- package/dist/databases/postgres/postgres.database.d.ts +0 -1
- package/dist/databases/postgres/postgres.database.js +0 -27
- package/dist/services/authorizations.service.d.ts +5 -2
- package/dist/services/authorizations.service.js +53 -3
- package/dist/services/multi-tenant-api.service.js +4 -2
- package/dist/utils/auth/attempt-login.util.d.ts +2 -1
- package/dist/utils/auth/attempt-login.util.js +3 -3
- package/dist/utils/auth/request-token-using-refresh-token.util.d.ts +5 -2
- package/dist/utils/auth/request-token-using-refresh-token.util.js +38 -5
- package/package.json +2 -2
- package/dist/services/utils/getUserContextAuthorizations.util.d.ts +0 -3
- package/dist/services/utils/getUserContextAuthorizations.util.js +0 -9
|
@@ -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,
|
|
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, [
|
|
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 ||
|
|
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 !==
|
|
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
|
-
|
|
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(
|
|
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
|
|
1
|
+
import { type ITokenResponse } from "@loomcore/common/models";
|
|
2
2
|
import type { IDatabase } from "../../databases/models/index.js";
|
|
3
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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,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
|
-
}
|