@loomcore/api 0.2.4 → 0.2.6
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 +12 -12
- package/dist/__tests__/common-test.utils.js +94 -101
- package/dist/__tests__/postgres.test-database.d.ts +2 -2
- package/dist/__tests__/postgres.test-database.js +16 -15
- package/dist/__tests__/test-objects.d.ts +1 -6
- package/dist/__tests__/test-objects.js +34 -73
- package/dist/config/base-api-config.d.ts +2 -2
- package/dist/config/base-api-config.js +17 -10
- package/dist/controllers/auth.controller.d.ts +3 -5
- package/dist/controllers/auth.controller.js +22 -26
- package/dist/controllers/index.d.ts +10 -11
- package/dist/controllers/index.js +10 -11
- package/dist/databases/mongo-db/migrations/mongo-initial-schema.d.ts +2 -2
- package/dist/databases/mongo-db/migrations/mongo-initial-schema.js +185 -128
- package/dist/services/auth.service.d.ts +8 -9
- package/dist/services/auth.service.js +66 -82
- package/dist/services/email.service.js +5 -5
- package/dist/services/jwt.service.js +2 -2
- package/dist/services/multi-tenant-api.service.d.ts +4 -4
- package/dist/services/multi-tenant-api.service.js +8 -8
- package/dist/services/organization.service.d.ts +4 -4
- package/dist/services/organization.service.js +12 -8
- package/dist/services/password-reset-token.service.d.ts +4 -4
- package/dist/services/password-reset-token.service.js +13 -9
- package/dist/services/tenant-query-decorator.d.ts +1 -1
- package/dist/services/tenant-query-decorator.js +17 -11
- package/dist/services/user.service.d.ts +6 -6
- package/dist/services/user.service.js +7 -7
- package/dist/services/utils/audit-for-create.util.d.ts +1 -1
- package/dist/services/utils/audit-for-update.util.d.ts +1 -1
- package/dist/services/utils/audit-for-update.util.js +0 -1
- package/dist/services/utils/getUserContextAuthorizations.util.d.ts +2 -2
- package/dist/services/utils/strip-sender-provided-system-properties.util.d.ts +1 -1
- package/dist/services/utils/strip-sender-provided-system-properties.util.js +5 -3
- package/package.json +5 -2
- package/dist/controllers/persons.controller.d.ts +0 -8
- package/dist/controllers/persons.controller.js +0 -18
- package/dist/services/person.service.d.ts +0 -6
- package/dist/services/person.service.js +0 -7
|
@@ -1,34 +1,30 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { getUserContextAuthorizations } from './utils/getUserContextAuthorizations.util.js';
|
|
15
|
-
import { PersonService } from './person.service.js';
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { EmptyUserContext, getSystemUserContext, passwordValidator, UserSpec, } from "@loomcore/common/models";
|
|
3
|
+
import { entityUtils } from "@loomcore/common/utils";
|
|
4
|
+
import moment from "moment";
|
|
5
|
+
import { config } from "../config/index.js";
|
|
6
|
+
import { BadRequestError, ServerError } from "../errors/index.js";
|
|
7
|
+
import { refreshTokenModelSpec, } from "../models/refresh-token.model.js";
|
|
8
|
+
import { passwordUtils } from "../utils/index.js";
|
|
9
|
+
import { EmailService, JwtService } from "./index.js";
|
|
10
|
+
import { MultiTenantApiService } from "./multi-tenant-api.service.js";
|
|
11
|
+
import { OrganizationService } from "./organization.service.js";
|
|
12
|
+
import { PasswordResetTokenService } from "./password-reset-token.service.js";
|
|
13
|
+
import { getUserContextAuthorizations } from "./utils/getUserContextAuthorizations.util.js";
|
|
16
14
|
export class AuthService extends MultiTenantApiService {
|
|
17
15
|
refreshTokenService;
|
|
18
16
|
passwordResetTokenService;
|
|
19
17
|
emailService;
|
|
20
18
|
organizationService;
|
|
21
19
|
authConfig;
|
|
22
|
-
personService;
|
|
23
20
|
constructor(database) {
|
|
24
|
-
super(database,
|
|
25
|
-
this.refreshTokenService = new
|
|
21
|
+
super(database, "users", "user", UserSpec);
|
|
22
|
+
this.refreshTokenService = new MultiTenantApiService(database, "refresh_tokens", "refresh_token", refreshTokenModelSpec);
|
|
26
23
|
this.passwordResetTokenService = new PasswordResetTokenService(database);
|
|
27
24
|
this.emailService = new EmailService();
|
|
28
25
|
this.organizationService = new OrganizationService(database);
|
|
29
|
-
this.personService = new PersonService(database);
|
|
30
26
|
if (!config.auth) {
|
|
31
|
-
throw new ServerError(
|
|
27
|
+
throw new ServerError("Auth configuration is not set");
|
|
32
28
|
}
|
|
33
29
|
this.authConfig = config.auth;
|
|
34
30
|
}
|
|
@@ -37,19 +33,17 @@ export class AuthService extends MultiTenantApiService {
|
|
|
37
33
|
const user = await this.getUserByEmail(lowerCaseEmail);
|
|
38
34
|
const organization = await this.organizationService.findOne(EmptyUserContext, { filters: { _id: { eq: user?._orgId } } });
|
|
39
35
|
if (!user) {
|
|
40
|
-
throw new BadRequestError(
|
|
36
|
+
throw new BadRequestError("Invalid Credentials");
|
|
41
37
|
}
|
|
42
38
|
const passwordsMatch = await passwordUtils.comparePasswords(user.password, password);
|
|
43
39
|
if (!passwordsMatch) {
|
|
44
|
-
throw new BadRequestError(
|
|
40
|
+
throw new BadRequestError("Invalid Credentials");
|
|
45
41
|
}
|
|
46
|
-
const person = await this.personService.findOne(EmptyUserContext, { filters: { _id: { eq: user.personId } } });
|
|
47
42
|
const authorizations = await getUserContextAuthorizations(this.database, user);
|
|
48
43
|
const userContext = {
|
|
49
44
|
user: user,
|
|
50
|
-
person: person ?? undefined,
|
|
51
45
|
organization: organization ?? undefined,
|
|
52
|
-
authorizations: authorizations
|
|
46
|
+
authorizations: authorizations,
|
|
53
47
|
};
|
|
54
48
|
const deviceId = this.getAndSetDeviceIdCookie(req, res);
|
|
55
49
|
const loginResponse = await this.logUserIn(userContext, deviceId);
|
|
@@ -65,10 +59,9 @@ export class AuthService extends MultiTenantApiService {
|
|
|
65
59
|
const tokenResponse = {
|
|
66
60
|
accessToken,
|
|
67
61
|
refreshToken: refreshTokenObject.token,
|
|
68
|
-
expiresOn: accessTokenExpiresOn
|
|
62
|
+
expiresOn: accessTokenExpiresOn,
|
|
69
63
|
};
|
|
70
|
-
this.updateLastLoggedIn(userContext.user._id)
|
|
71
|
-
.catch(err => console.log(`Error updating lastLoggedIn: ${err}`));
|
|
64
|
+
this.updateLastLoggedIn(userContext.user._id).catch((err) => console.log(`Error updating lastLoggedIn: ${err}`));
|
|
72
65
|
userContext.user = this.postProcessEntity(userContext, userContext.user);
|
|
73
66
|
loginResponse = { tokens: tokenResponse, userContext };
|
|
74
67
|
}
|
|
@@ -76,44 +69,33 @@ export class AuthService extends MultiTenantApiService {
|
|
|
76
69
|
}
|
|
77
70
|
async getUserByEmail(email) {
|
|
78
71
|
const queryOptions = { filters: { email: { eq: email.toLowerCase() } } };
|
|
79
|
-
const rawUser = await this.database.findOne(queryOptions,
|
|
72
|
+
const rawUser = await this.database.findOne(queryOptions, "users");
|
|
80
73
|
if (!rawUser) {
|
|
81
74
|
return null;
|
|
82
75
|
}
|
|
83
76
|
return this.database.postProcessEntity(rawUser, this.modelSpec.fullSchema);
|
|
84
77
|
}
|
|
85
|
-
async createUser(userContext, user
|
|
86
|
-
if (userContext.user._id ===
|
|
87
|
-
if (config.app.isMultiTenant &&
|
|
88
|
-
|
|
78
|
+
async createUser(userContext, user) {
|
|
79
|
+
if (userContext.user._id === "system") {
|
|
80
|
+
if (config.app.isMultiTenant &&
|
|
81
|
+
userContext.organization?._id !== user._orgId) {
|
|
82
|
+
throw new BadRequestError("User is not authorized to create a user in this organization");
|
|
89
83
|
}
|
|
90
84
|
if (user._orgId) {
|
|
91
|
-
const org = await this.organizationService.findOne(userContext, {
|
|
85
|
+
const org = await this.organizationService.findOne(userContext, {
|
|
86
|
+
filters: { _id: { eq: user._orgId } },
|
|
87
|
+
});
|
|
92
88
|
if (!org) {
|
|
93
|
-
throw new BadRequestError(
|
|
89
|
+
throw new BadRequestError("The specified organization does not exist");
|
|
94
90
|
}
|
|
95
91
|
}
|
|
96
92
|
}
|
|
97
93
|
if (user.email) {
|
|
98
94
|
const existingUser = await this.getUserByEmail(user.email);
|
|
99
95
|
if (existingUser) {
|
|
100
|
-
throw new BadRequestError(
|
|
96
|
+
throw new BadRequestError("A user with this email address already exists");
|
|
101
97
|
}
|
|
102
98
|
}
|
|
103
|
-
let personId = user.personId;
|
|
104
|
-
if (personId && person) {
|
|
105
|
-
await this.personService.partialUpdateById(userContext, personId, person);
|
|
106
|
-
}
|
|
107
|
-
if (!personId && person) {
|
|
108
|
-
const newPerson = await this.personService.create(userContext, person);
|
|
109
|
-
if (newPerson) {
|
|
110
|
-
personId = newPerson._id;
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
113
|
-
throw new ServerError('authService.createUser: Failed to create person');
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
user.personId = personId;
|
|
117
99
|
return await this.create(userContext, user);
|
|
118
100
|
}
|
|
119
101
|
async requestTokenUsingRefreshToken(refreshToken, deviceId) {
|
|
@@ -122,21 +104,13 @@ export class AuthService extends MultiTenantApiService {
|
|
|
122
104
|
if (activeRefreshToken) {
|
|
123
105
|
const systemUserContext = getSystemUserContext();
|
|
124
106
|
const user = await this.getById(systemUserContext, activeRefreshToken.userId);
|
|
125
|
-
const person = await this.personService.findOne(EmptyUserContext, { filters: { _id: { eq: user?.personId } } });
|
|
126
107
|
const organization = await this.organizationService.findOne(EmptyUserContext, { filters: { _id: { eq: user?._orgId } } });
|
|
127
108
|
const authorizations = await getUserContextAuthorizations(this.database, user);
|
|
128
|
-
|
|
109
|
+
const userContext = {
|
|
129
110
|
user: user,
|
|
130
|
-
person: person ?? undefined,
|
|
131
111
|
organization: organization ?? undefined,
|
|
132
|
-
authorizations: authorizations
|
|
112
|
+
authorizations: authorizations,
|
|
133
113
|
};
|
|
134
|
-
if (user.personId) {
|
|
135
|
-
const person = await this.personService.getById(EmptyUserContext, user.personId);
|
|
136
|
-
if (person) {
|
|
137
|
-
userContext.person = person;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
114
|
tokens = await this.createNewTokens(userContext, activeRefreshToken);
|
|
141
115
|
}
|
|
142
116
|
return tokens;
|
|
@@ -147,7 +121,10 @@ export class AuthService extends MultiTenantApiService {
|
|
|
147
121
|
return result;
|
|
148
122
|
}
|
|
149
123
|
async changePassword(userContext, queryObject, password) {
|
|
150
|
-
const updates = {
|
|
124
|
+
const updates = {
|
|
125
|
+
password: password,
|
|
126
|
+
_lastPasswordChange: moment().utc().toDate(),
|
|
127
|
+
};
|
|
151
128
|
const updatedUsers = await super.update(userContext, queryObject, updates);
|
|
152
129
|
const result = {
|
|
153
130
|
success: true,
|
|
@@ -162,12 +139,12 @@ export class AuthService extends MultiTenantApiService {
|
|
|
162
139
|
const tokenResponse = {
|
|
163
140
|
accessToken,
|
|
164
141
|
refreshToken: activeRefreshToken.token,
|
|
165
|
-
expiresOn: accessTokenExpiresOn
|
|
142
|
+
expiresOn: accessTokenExpiresOn,
|
|
166
143
|
};
|
|
167
144
|
return tokenResponse;
|
|
168
145
|
}
|
|
169
146
|
async getActiveRefreshToken(refreshToken, deviceId) {
|
|
170
|
-
const refreshTokenResult = await this.refreshTokenService.findOne(
|
|
147
|
+
const refreshTokenResult = await this.refreshTokenService.findOne(getSystemUserContext(), { filters: { token: { eq: refreshToken }, deviceId: { eq: deviceId } } });
|
|
171
148
|
let activeRefreshToken = null;
|
|
172
149
|
if (refreshTokenResult) {
|
|
173
150
|
const now = Date.now();
|
|
@@ -187,10 +164,10 @@ export class AuthService extends MultiTenantApiService {
|
|
|
187
164
|
userId,
|
|
188
165
|
expiresOn: expiresOn,
|
|
189
166
|
created: moment().utc().toDate(),
|
|
190
|
-
createdBy: userId
|
|
167
|
+
createdBy: userId,
|
|
191
168
|
};
|
|
192
|
-
|
|
193
|
-
const insertResult = await this.refreshTokenService.create(
|
|
169
|
+
await this.deleteRefreshTokensForDevice(deviceId);
|
|
170
|
+
const insertResult = await this.refreshTokenService.create(getSystemUserContext(), newRefreshToken);
|
|
194
171
|
return insertResult;
|
|
195
172
|
}
|
|
196
173
|
async sendResetPasswordEmail(emailAddress, clientBaseUrl) {
|
|
@@ -209,38 +186,42 @@ export class AuthService extends MultiTenantApiService {
|
|
|
209
186
|
if (!retrievedPasswordResetToken) {
|
|
210
187
|
throw new ServerError(`Unable to retrieve password reset token for email: ${lowerCaseEmail}`);
|
|
211
188
|
}
|
|
212
|
-
if (retrievedPasswordResetToken.token !== passwordResetToken ||
|
|
213
|
-
|
|
189
|
+
if (retrievedPasswordResetToken.token !== passwordResetToken ||
|
|
190
|
+
retrievedPasswordResetToken.expiresOn < Date.now()) {
|
|
191
|
+
throw new BadRequestError("Invalid password reset token");
|
|
214
192
|
}
|
|
215
193
|
const validationErrors = entityUtils.validate(UserSpec, { password: password }, true, passwordValidator);
|
|
216
|
-
entityUtils.handleValidationResult(validationErrors,
|
|
194
|
+
entityUtils.handleValidationResult(validationErrors, "AuthService.resetPassword");
|
|
217
195
|
const result = await this.changePassword(getSystemUserContext(), { email: lowerCaseEmail }, password);
|
|
218
196
|
console.log(`password changed using forgot-password for email: ${lowerCaseEmail}`);
|
|
219
|
-
await this.passwordResetTokenService.deleteById(
|
|
197
|
+
await this.passwordResetTokenService.deleteById(getSystemUserContext(), retrievedPasswordResetToken._id.toString());
|
|
220
198
|
console.log(`passwordResetToken deleted for email: ${lowerCaseEmail}`);
|
|
221
199
|
return result;
|
|
222
200
|
}
|
|
223
201
|
deleteRefreshTokensForDevice(deviceId) {
|
|
224
|
-
return this.refreshTokenService.deleteMany(
|
|
202
|
+
return this.refreshTokenService.deleteMany(getSystemUserContext(), {
|
|
203
|
+
filters: { deviceId: { eq: deviceId } },
|
|
204
|
+
});
|
|
225
205
|
}
|
|
226
206
|
generateJwt(userContext) {
|
|
227
207
|
const jwtExpiryConfig = this.authConfig.jwtExpirationInSeconds;
|
|
228
|
-
const jwtExpirationInSeconds =
|
|
208
|
+
const jwtExpirationInSeconds = typeof jwtExpiryConfig === "string"
|
|
209
|
+
? parseInt(jwtExpiryConfig)
|
|
210
|
+
: jwtExpiryConfig;
|
|
229
211
|
const accessToken = JwtService.sign(userContext, this.authConfig.clientSecret, {
|
|
230
|
-
expiresIn: jwtExpirationInSeconds
|
|
212
|
+
expiresIn: jwtExpirationInSeconds,
|
|
231
213
|
});
|
|
232
214
|
return accessToken;
|
|
233
215
|
}
|
|
234
|
-
;
|
|
235
216
|
generateRefreshToken() {
|
|
236
|
-
return crypto.randomBytes(40).toString(
|
|
217
|
+
return crypto.randomBytes(40).toString("hex");
|
|
237
218
|
}
|
|
238
219
|
generateDeviceId() {
|
|
239
|
-
return crypto.randomBytes(40).toString(
|
|
220
|
+
return crypto.randomBytes(40).toString("hex");
|
|
240
221
|
}
|
|
241
222
|
getAndSetDeviceIdCookie(req, res) {
|
|
242
223
|
let isNewDeviceId = false;
|
|
243
|
-
let deviceId =
|
|
224
|
+
let deviceId = "";
|
|
244
225
|
const deviceIdFromCookie = this.getDeviceIdFromCookie(req);
|
|
245
226
|
if (deviceIdFromCookie) {
|
|
246
227
|
deviceId = deviceIdFromCookie;
|
|
@@ -252,14 +233,14 @@ export class AuthService extends MultiTenantApiService {
|
|
|
252
233
|
if (isNewDeviceId) {
|
|
253
234
|
const cookieOptions = {
|
|
254
235
|
maxAge: this.authConfig.deviceIdCookieMaxAgeInDays * 24 * 60 * 60 * 1000,
|
|
255
|
-
httpOnly: true
|
|
236
|
+
httpOnly: true,
|
|
256
237
|
};
|
|
257
|
-
res.cookie(
|
|
238
|
+
res.cookie("deviceId", deviceId, cookieOptions);
|
|
258
239
|
}
|
|
259
240
|
return deviceId;
|
|
260
241
|
}
|
|
261
242
|
getDeviceIdFromCookie(req) {
|
|
262
|
-
return req.cookies
|
|
243
|
+
return req.cookies.deviceId;
|
|
263
244
|
}
|
|
264
245
|
getExpiresOnFromSeconds(expiresInSeconds) {
|
|
265
246
|
return Date.now() + expiresInSeconds * 1000;
|
|
@@ -272,7 +253,7 @@ export class AuthService extends MultiTenantApiService {
|
|
|
272
253
|
}
|
|
273
254
|
async preProcessEntity(userContext, entity, isCreate, allowId) {
|
|
274
255
|
if (entity.email) {
|
|
275
|
-
entity.email = entity.email
|
|
256
|
+
entity.email = entity.email?.toLowerCase();
|
|
276
257
|
}
|
|
277
258
|
if (entity.password) {
|
|
278
259
|
const hash = await passwordUtils.hashPassword(entity.password);
|
|
@@ -283,11 +264,14 @@ export class AuthService extends MultiTenantApiService {
|
|
|
283
264
|
}
|
|
284
265
|
async updateLastLoggedIn(userId) {
|
|
285
266
|
try {
|
|
286
|
-
const updates = {
|
|
267
|
+
const updates = {
|
|
268
|
+
_lastLoggedIn: moment().utc().toDate(),
|
|
269
|
+
};
|
|
287
270
|
const systemUserContext = getSystemUserContext();
|
|
288
271
|
await this.partialUpdateById(systemUserContext, userId, updates);
|
|
289
272
|
}
|
|
290
273
|
catch (error) {
|
|
274
|
+
console.error(`Error updating lastLoggedIn: ${error}`);
|
|
291
275
|
}
|
|
292
276
|
}
|
|
293
277
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { config } from "../config/index.js";
|
|
2
|
+
import { ServerError } from "../errors/index.js";
|
|
3
3
|
export class EmailService {
|
|
4
4
|
emailClient;
|
|
5
5
|
constructor() {
|
|
@@ -7,7 +7,7 @@ export class EmailService {
|
|
|
7
7
|
this.emailClient = config.thirdPartyClients.emailClient;
|
|
8
8
|
}
|
|
9
9
|
else {
|
|
10
|
-
throw new ServerError(
|
|
10
|
+
throw new ServerError("Email client is not available. Email client is not set in the config.");
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
async sendResetPasswordEmail(emailAddress, resetPasswordLink) {
|
|
@@ -16,8 +16,8 @@ export class EmailService {
|
|
|
16
16
|
console.log(`Reset password email sent to ${emailAddress} with reset password link ${resetPasswordLink}`);
|
|
17
17
|
}
|
|
18
18
|
catch (error) {
|
|
19
|
-
console.error(
|
|
20
|
-
throw new ServerError(
|
|
19
|
+
console.error("Error sending reset password email:", error);
|
|
20
|
+
throw new ServerError("Error sending reset password email");
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import jwt from
|
|
1
|
+
import jwt from "jsonwebtoken";
|
|
2
2
|
export class JwtService {
|
|
3
3
|
static sign(payload, secret, options) {
|
|
4
4
|
return jwt.sign(payload, secret, options);
|
|
5
5
|
}
|
|
6
6
|
static verify(token, secret) {
|
|
7
7
|
if (!secret) {
|
|
8
|
-
throw new Error(
|
|
8
|
+
throw new Error("JWT secret is required for verification");
|
|
9
9
|
}
|
|
10
10
|
try {
|
|
11
11
|
const decoded = jwt.verify(token, secret);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import type { IEntity, IModelSpec, IQueryOptions, IUserContext } from "@loomcore/common/models";
|
|
2
|
+
import type { IDatabase } from "../databases/models/index.js";
|
|
3
|
+
import type { Operation } from "../databases/operations/operation.js";
|
|
4
|
+
import { GenericApiService } from "./generic-api-service/generic-api.service.js";
|
|
5
5
|
export declare class MultiTenantApiService<T extends IEntity> extends GenericApiService<T> {
|
|
6
6
|
private tenantDecorator?;
|
|
7
7
|
constructor(database: IDatabase, pluralResourceName: string, singularResourceName: string, modelSpec: IModelSpec);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { BadRequestError } from
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { config } from "../config/base-api-config.js";
|
|
2
|
+
import { BadRequestError } from "../errors/bad-request.error.js";
|
|
3
|
+
import { GenericApiService } from "./generic-api-service/generic-api.service.js";
|
|
4
|
+
import { TenantQueryDecorator } from "./tenant-query-decorator.js";
|
|
5
5
|
export class MultiTenantApiService extends GenericApiService {
|
|
6
6
|
tenantDecorator;
|
|
7
7
|
constructor(database, pluralResourceName, singularResourceName, modelSpec) {
|
|
@@ -11,11 +11,11 @@ export class MultiTenantApiService extends GenericApiService {
|
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
prepareQuery(userContext, queryOptions, operations) {
|
|
14
|
-
if (!config?.app?.isMultiTenant || userContext?.user?._id ===
|
|
14
|
+
if (!config?.app?.isMultiTenant || userContext?.user?._id === "system") {
|
|
15
15
|
return super.prepareQuery(userContext, queryOptions, operations);
|
|
16
16
|
}
|
|
17
17
|
if (!userContext || !userContext.organization?._id) {
|
|
18
|
-
throw new BadRequestError(
|
|
18
|
+
throw new BadRequestError("A valid userContext was not provided to MultiTenantApiService.prepareQuery");
|
|
19
19
|
}
|
|
20
20
|
const queryObject = this.tenantDecorator.applyTenantToQuery(userContext, queryOptions, this.pluralResourceName);
|
|
21
21
|
return { queryObject, operations };
|
|
@@ -25,10 +25,10 @@ export class MultiTenantApiService extends GenericApiService {
|
|
|
25
25
|
return super.preProcessEntity(userContext, entity, isCreate, allowId);
|
|
26
26
|
}
|
|
27
27
|
if (!userContext || !userContext.organization?._id) {
|
|
28
|
-
throw new BadRequestError(
|
|
28
|
+
throw new BadRequestError("A valid userContext was not provided to MultiTenantApiService.prepareEntity");
|
|
29
29
|
}
|
|
30
30
|
const preparedEntity = await super.preProcessEntity(userContext, entity, isCreate, allowId);
|
|
31
|
-
if (isCreate && userContext.user._id !==
|
|
31
|
+
if (isCreate && userContext.user._id !== "system") {
|
|
32
32
|
preparedEntity._orgId = userContext.organization?._id;
|
|
33
33
|
}
|
|
34
34
|
return preparedEntity;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { IOrganization, IUserContext } from
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { type IOrganization, type IUserContext } from "@loomcore/common/models";
|
|
2
|
+
import type { AppIdType } from "@loomcore/common/types";
|
|
3
|
+
import type { IDatabase } from "../databases/models/index.js";
|
|
4
|
+
import { GenericApiService } from "./generic-api-service/generic-api.service.js";
|
|
5
5
|
export declare class OrganizationService extends GenericApiService<IOrganization> {
|
|
6
6
|
constructor(database: IDatabase);
|
|
7
7
|
preProcessEntity(userContext: IUserContext, entity: Partial<IOrganization>, isCreate: boolean, allowId?: boolean): Promise<Partial<IOrganization>>;
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import { OrganizationSpec } from
|
|
2
|
-
import { BadRequestError } from
|
|
3
|
-
import { GenericApiService } from
|
|
1
|
+
import { OrganizationSpec, } from "@loomcore/common/models";
|
|
2
|
+
import { BadRequestError } from "../errors/index.js";
|
|
3
|
+
import { GenericApiService } from "./generic-api-service/generic-api.service.js";
|
|
4
4
|
export class OrganizationService extends GenericApiService {
|
|
5
5
|
constructor(database) {
|
|
6
|
-
super(database,
|
|
6
|
+
super(database, "organizations", "organization", OrganizationSpec);
|
|
7
7
|
}
|
|
8
8
|
async preProcessEntity(userContext, entity, isCreate, allowId = true) {
|
|
9
9
|
if (isCreate) {
|
|
10
10
|
const metaOrg = await this.getMetaOrg(userContext);
|
|
11
11
|
if (metaOrg && entity.isMetaOrg) {
|
|
12
|
-
throw new BadRequestError(
|
|
12
|
+
throw new BadRequestError("Meta organization already exists");
|
|
13
13
|
}
|
|
14
14
|
if (metaOrg && userContext.organization?._id !== metaOrg._id) {
|
|
15
|
-
throw new BadRequestError(
|
|
15
|
+
throw new BadRequestError("User is not authorized to create an organization");
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
const result = await super.preProcessEntity(userContext, entity, isCreate, allowId);
|
|
@@ -23,7 +23,9 @@ export class OrganizationService extends GenericApiService {
|
|
|
23
23
|
return org?.authToken ?? null;
|
|
24
24
|
}
|
|
25
25
|
async validateRepoAuthToken(userContext, orgCode, authToken) {
|
|
26
|
-
const org = await this.findOne(userContext, {
|
|
26
|
+
const org = await this.findOne(userContext, {
|
|
27
|
+
filters: { code: { eq: orgCode } },
|
|
28
|
+
});
|
|
27
29
|
if (!org) {
|
|
28
30
|
return null;
|
|
29
31
|
}
|
|
@@ -31,7 +33,9 @@ export class OrganizationService extends GenericApiService {
|
|
|
31
33
|
return orgId;
|
|
32
34
|
}
|
|
33
35
|
async getMetaOrg(userContext) {
|
|
34
|
-
const org = await this.findOne(userContext, {
|
|
36
|
+
const org = await this.findOne(userContext, {
|
|
37
|
+
filters: { isMetaOrg: { eq: true } },
|
|
38
|
+
});
|
|
35
39
|
return org;
|
|
36
40
|
}
|
|
37
41
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { IPasswordResetToken } from
|
|
2
|
-
import { IDatabase } from
|
|
3
|
-
import {
|
|
4
|
-
export declare class PasswordResetTokenService extends
|
|
1
|
+
import { type IPasswordResetToken } from "@loomcore/common/models";
|
|
2
|
+
import type { IDatabase } from "../databases/models/index.js";
|
|
3
|
+
import { MultiTenantApiService } from "./multi-tenant-api.service.js";
|
|
4
|
+
export declare class PasswordResetTokenService extends MultiTenantApiService<IPasswordResetToken> {
|
|
5
5
|
constructor(database: IDatabase);
|
|
6
6
|
createPasswordResetToken(email: string, expiresOn: number): Promise<IPasswordResetToken | null>;
|
|
7
7
|
getByEmail(email: string): Promise<IPasswordResetToken | null>;
|
|
@@ -1,21 +1,25 @@
|
|
|
1
|
-
import crypto from
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
export class PasswordResetTokenService extends
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { getSystemUserContext, PasswordResetTokenSpec, } from "@loomcore/common/models";
|
|
3
|
+
import { MultiTenantApiService } from "./multi-tenant-api.service.js";
|
|
4
|
+
export class PasswordResetTokenService extends MultiTenantApiService {
|
|
5
5
|
constructor(database) {
|
|
6
|
-
super(database,
|
|
6
|
+
super(database, "password_reset_tokens", "password_reset_token", PasswordResetTokenSpec);
|
|
7
7
|
}
|
|
8
8
|
async createPasswordResetToken(email, expiresOn) {
|
|
9
9
|
const lowerCaseEmail = email.toLowerCase();
|
|
10
|
-
await this.deleteMany(
|
|
10
|
+
await this.deleteMany(getSystemUserContext(), {
|
|
11
|
+
filters: { email: { eq: lowerCaseEmail } },
|
|
12
|
+
});
|
|
11
13
|
const passwordResetToken = {
|
|
12
14
|
email: lowerCaseEmail,
|
|
13
|
-
token: crypto.randomBytes(40).toString(
|
|
15
|
+
token: crypto.randomBytes(40).toString("hex"),
|
|
14
16
|
expiresOn: expiresOn,
|
|
15
17
|
};
|
|
16
|
-
return super.create(
|
|
18
|
+
return super.create(getSystemUserContext(), passwordResetToken);
|
|
17
19
|
}
|
|
18
20
|
async getByEmail(email) {
|
|
19
|
-
return await super.findOne(
|
|
21
|
+
return await super.findOne(getSystemUserContext(), {
|
|
22
|
+
filters: { email: { eq: email.toLowerCase() } },
|
|
23
|
+
});
|
|
20
24
|
}
|
|
21
25
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { ServerError } from
|
|
1
|
+
import { ServerError } from "../errors/index.js";
|
|
2
2
|
export const DEFAULT_TENANT_OPTIONS = {
|
|
3
|
-
orgIdField:
|
|
4
|
-
excludedCollections: []
|
|
3
|
+
orgIdField: "_orgId",
|
|
4
|
+
excludedCollections: [],
|
|
5
5
|
};
|
|
6
6
|
export class TenantQueryDecorator {
|
|
7
7
|
options;
|
|
@@ -13,12 +13,18 @@ export class TenantQueryDecorator {
|
|
|
13
13
|
const shouldApplyTenantFilter = !this.options.excludedCollections?.includes(collectionName) &&
|
|
14
14
|
userContext?.organization?._id;
|
|
15
15
|
if (shouldApplyTenantFilter) {
|
|
16
|
-
const orgIdField = this.options.orgIdField ||
|
|
17
|
-
result = {
|
|
16
|
+
const orgIdField = this.options.orgIdField || "_orgId";
|
|
17
|
+
result = {
|
|
18
|
+
...queryObject,
|
|
19
|
+
filters: {
|
|
20
|
+
...queryObject.filters,
|
|
21
|
+
[orgIdField]: { eq: userContext.organization?._id },
|
|
22
|
+
},
|
|
23
|
+
};
|
|
18
24
|
}
|
|
19
25
|
else if (!userContext?.organization?._id) {
|
|
20
26
|
if (!this.options.excludedCollections?.includes(collectionName)) {
|
|
21
|
-
throw new ServerError(
|
|
27
|
+
throw new ServerError("No _orgId found in userContext");
|
|
22
28
|
}
|
|
23
29
|
}
|
|
24
30
|
return result;
|
|
@@ -28,7 +34,7 @@ export class TenantQueryDecorator {
|
|
|
28
34
|
const shouldApplyTenantFilter = !this.options.excludedCollections?.includes(collectionName);
|
|
29
35
|
if (shouldApplyTenantFilter) {
|
|
30
36
|
if (!userContext?.organization?._id) {
|
|
31
|
-
throw new ServerError(
|
|
37
|
+
throw new ServerError("userContext must have an _orgId property to apply tenant filtering");
|
|
32
38
|
}
|
|
33
39
|
if (!result.filters) {
|
|
34
40
|
result.filters = {};
|
|
@@ -43,20 +49,20 @@ export class TenantQueryDecorator {
|
|
|
43
49
|
const shouldApplyTenantFilter = !this.options.excludedCollections?.includes(collectionName) &&
|
|
44
50
|
userContext?.organization?._id;
|
|
45
51
|
if (shouldApplyTenantFilter) {
|
|
46
|
-
const orgIdField = this.options.orgIdField ||
|
|
52
|
+
const orgIdField = this.options.orgIdField || "_orgId";
|
|
47
53
|
result = {
|
|
48
54
|
...entity,
|
|
49
|
-
[orgIdField]: userContext.organization?._id
|
|
55
|
+
[orgIdField]: userContext.organization?._id,
|
|
50
56
|
};
|
|
51
57
|
}
|
|
52
58
|
else if (!userContext?.organization?._id) {
|
|
53
59
|
if (!this.options.excludedCollections?.includes(collectionName)) {
|
|
54
|
-
throw new ServerError(
|
|
60
|
+
throw new ServerError("No _orgId found in userContext");
|
|
55
61
|
}
|
|
56
62
|
}
|
|
57
63
|
return result;
|
|
58
64
|
}
|
|
59
65
|
getOrgIdField() {
|
|
60
|
-
return this.options.orgIdField ||
|
|
66
|
+
return this.options.orgIdField || "_orgId";
|
|
61
67
|
}
|
|
62
68
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { IUser, IUserContext } from
|
|
2
|
-
import type { AppIdType } from
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { type IUser, type IUserContext } from "@loomcore/common/models";
|
|
2
|
+
import type { AppIdType } from "@loomcore/common/types";
|
|
3
|
+
import type { IDatabase } from "../databases/models/index.js";
|
|
4
|
+
import { MultiTenantApiService } from "./multi-tenant-api.service.js";
|
|
5
5
|
export declare class UserService extends MultiTenantApiService<IUser> {
|
|
6
6
|
constructor(database: IDatabase);
|
|
7
|
-
fullUpdateById(
|
|
8
|
-
preProcessEntity(userContext: IUserContext, entity: Partial<IUser>, isCreate: boolean,
|
|
7
|
+
fullUpdateById(_userContext: IUserContext, _id: AppIdType, _entity: IUser): Promise<IUser>;
|
|
8
|
+
preProcessEntity(userContext: IUserContext, entity: Partial<IUser>, isCreate: boolean, _allowId?: boolean): Promise<Partial<IUser>>;
|
|
9
9
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { UserSpec } from
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { UserSpec, } from "@loomcore/common/models";
|
|
2
|
+
import { ServerError } from "../errors/index.js";
|
|
3
|
+
import { MultiTenantApiService } from "./multi-tenant-api.service.js";
|
|
4
4
|
export class UserService extends MultiTenantApiService {
|
|
5
5
|
constructor(database) {
|
|
6
|
-
super(database,
|
|
6
|
+
super(database, "users", "user", UserSpec);
|
|
7
7
|
}
|
|
8
|
-
async fullUpdateById(
|
|
9
|
-
throw new ServerError(
|
|
8
|
+
async fullUpdateById(_userContext, _id, _entity) {
|
|
9
|
+
throw new ServerError("Cannot full update a user. Either use PATCH or /auth/change-password to update password.");
|
|
10
10
|
}
|
|
11
|
-
async preProcessEntity(userContext, entity, isCreate,
|
|
11
|
+
async preProcessEntity(userContext, entity, isCreate, _allowId = false) {
|
|
12
12
|
const preparedEntity = await super.preProcessEntity(userContext, entity, isCreate);
|
|
13
13
|
if (preparedEntity.email) {
|
|
14
14
|
preparedEntity.email = preparedEntity.email.toLowerCase();
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { IUserContext } from "@loomcore/common/models";
|
|
1
|
+
import type { IUserContext } from "@loomcore/common/models";
|
|
2
2
|
export declare function auditForCreate(userContext: IUserContext, doc: any): void;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { IUserContext } from "@loomcore/common/models";
|
|
1
|
+
import type { IUserContext } from "@loomcore/common/models";
|
|
2
2
|
export declare function auditForUpdate(userContext: IUserContext, doc: any): void;
|