@loomcore/api 0.2.4 → 0.2.5
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 +96 -90
- package/dist/__tests__/postgres.test-database.d.ts +2 -2
- package/dist/__tests__/postgres.test-database.js +16 -15
- package/dist/config/base-api-config.d.ts +2 -2
- package/dist/config/base-api-config.js +17 -10
- 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 +7 -7
- package/dist/services/auth.service.js +71 -58
- 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/person.service.d.ts +4 -4
- package/dist/services/person.service.js +3 -3
- 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 +1 -1
|
@@ -1,18 +1,17 @@
|
|
|
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
|
|
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 { PersonService } from "./person.service.js";
|
|
14
|
+
import { getUserContextAuthorizations } from "./utils/getUserContextAuthorizations.util.js";
|
|
16
15
|
export class AuthService extends MultiTenantApiService {
|
|
17
16
|
refreshTokenService;
|
|
18
17
|
passwordResetTokenService;
|
|
@@ -21,14 +20,14 @@ export class AuthService extends MultiTenantApiService {
|
|
|
21
20
|
authConfig;
|
|
22
21
|
personService;
|
|
23
22
|
constructor(database) {
|
|
24
|
-
super(database,
|
|
25
|
-
this.refreshTokenService = new
|
|
23
|
+
super(database, "users", "user", UserSpec);
|
|
24
|
+
this.refreshTokenService = new MultiTenantApiService(database, "refresh_tokens", "refresh_token", refreshTokenModelSpec);
|
|
26
25
|
this.passwordResetTokenService = new PasswordResetTokenService(database);
|
|
27
26
|
this.emailService = new EmailService();
|
|
28
27
|
this.organizationService = new OrganizationService(database);
|
|
29
28
|
this.personService = new PersonService(database);
|
|
30
29
|
if (!config.auth) {
|
|
31
|
-
throw new ServerError(
|
|
30
|
+
throw new ServerError("Auth configuration is not set");
|
|
32
31
|
}
|
|
33
32
|
this.authConfig = config.auth;
|
|
34
33
|
}
|
|
@@ -37,19 +36,21 @@ export class AuthService extends MultiTenantApiService {
|
|
|
37
36
|
const user = await this.getUserByEmail(lowerCaseEmail);
|
|
38
37
|
const organization = await this.organizationService.findOne(EmptyUserContext, { filters: { _id: { eq: user?._orgId } } });
|
|
39
38
|
if (!user) {
|
|
40
|
-
throw new BadRequestError(
|
|
39
|
+
throw new BadRequestError("Invalid Credentials");
|
|
41
40
|
}
|
|
42
41
|
const passwordsMatch = await passwordUtils.comparePasswords(user.password, password);
|
|
43
42
|
if (!passwordsMatch) {
|
|
44
|
-
throw new BadRequestError(
|
|
43
|
+
throw new BadRequestError("Invalid Credentials");
|
|
45
44
|
}
|
|
46
|
-
const person = await this.personService.findOne(
|
|
45
|
+
const person = await this.personService.findOne(getSystemUserContext(), {
|
|
46
|
+
filters: { _id: { eq: user.personId } },
|
|
47
|
+
});
|
|
47
48
|
const authorizations = await getUserContextAuthorizations(this.database, user);
|
|
48
49
|
const userContext = {
|
|
49
50
|
user: user,
|
|
50
51
|
person: person ?? undefined,
|
|
51
52
|
organization: organization ?? undefined,
|
|
52
|
-
authorizations: authorizations
|
|
53
|
+
authorizations: authorizations,
|
|
53
54
|
};
|
|
54
55
|
const deviceId = this.getAndSetDeviceIdCookie(req, res);
|
|
55
56
|
const loginResponse = await this.logUserIn(userContext, deviceId);
|
|
@@ -65,10 +66,9 @@ export class AuthService extends MultiTenantApiService {
|
|
|
65
66
|
const tokenResponse = {
|
|
66
67
|
accessToken,
|
|
67
68
|
refreshToken: refreshTokenObject.token,
|
|
68
|
-
expiresOn: accessTokenExpiresOn
|
|
69
|
+
expiresOn: accessTokenExpiresOn,
|
|
69
70
|
};
|
|
70
|
-
this.updateLastLoggedIn(userContext.user._id)
|
|
71
|
-
.catch(err => console.log(`Error updating lastLoggedIn: ${err}`));
|
|
71
|
+
this.updateLastLoggedIn(userContext.user._id).catch((err) => console.log(`Error updating lastLoggedIn: ${err}`));
|
|
72
72
|
userContext.user = this.postProcessEntity(userContext, userContext.user);
|
|
73
73
|
loginResponse = { tokens: tokenResponse, userContext };
|
|
74
74
|
}
|
|
@@ -76,28 +76,31 @@ export class AuthService extends MultiTenantApiService {
|
|
|
76
76
|
}
|
|
77
77
|
async getUserByEmail(email) {
|
|
78
78
|
const queryOptions = { filters: { email: { eq: email.toLowerCase() } } };
|
|
79
|
-
const rawUser = await this.database.findOne(queryOptions,
|
|
79
|
+
const rawUser = await this.database.findOne(queryOptions, "users");
|
|
80
80
|
if (!rawUser) {
|
|
81
81
|
return null;
|
|
82
82
|
}
|
|
83
83
|
return this.database.postProcessEntity(rawUser, this.modelSpec.fullSchema);
|
|
84
84
|
}
|
|
85
85
|
async createUser(userContext, user, person) {
|
|
86
|
-
if (userContext.user._id ===
|
|
87
|
-
if (config.app.isMultiTenant &&
|
|
88
|
-
|
|
86
|
+
if (userContext.user._id === "system") {
|
|
87
|
+
if (config.app.isMultiTenant &&
|
|
88
|
+
userContext.organization?._id !== user._orgId) {
|
|
89
|
+
throw new BadRequestError("User is not authorized to create a user in this organization");
|
|
89
90
|
}
|
|
90
91
|
if (user._orgId) {
|
|
91
|
-
const org = await this.organizationService.findOne(userContext, {
|
|
92
|
+
const org = await this.organizationService.findOne(userContext, {
|
|
93
|
+
filters: { _id: { eq: user._orgId } },
|
|
94
|
+
});
|
|
92
95
|
if (!org) {
|
|
93
|
-
throw new BadRequestError(
|
|
96
|
+
throw new BadRequestError("The specified organization does not exist");
|
|
94
97
|
}
|
|
95
98
|
}
|
|
96
99
|
}
|
|
97
100
|
if (user.email) {
|
|
98
101
|
const existingUser = await this.getUserByEmail(user.email);
|
|
99
102
|
if (existingUser) {
|
|
100
|
-
throw new BadRequestError(
|
|
103
|
+
throw new BadRequestError("A user with this email address already exists");
|
|
101
104
|
}
|
|
102
105
|
}
|
|
103
106
|
let personId = user.personId;
|
|
@@ -110,7 +113,7 @@ export class AuthService extends MultiTenantApiService {
|
|
|
110
113
|
personId = newPerson._id;
|
|
111
114
|
}
|
|
112
115
|
else {
|
|
113
|
-
throw new ServerError(
|
|
116
|
+
throw new ServerError("authService.createUser: Failed to create person");
|
|
114
117
|
}
|
|
115
118
|
}
|
|
116
119
|
user.personId = personId;
|
|
@@ -122,17 +125,19 @@ export class AuthService extends MultiTenantApiService {
|
|
|
122
125
|
if (activeRefreshToken) {
|
|
123
126
|
const systemUserContext = getSystemUserContext();
|
|
124
127
|
const user = await this.getById(systemUserContext, activeRefreshToken.userId);
|
|
125
|
-
const person = await this.personService.findOne(
|
|
128
|
+
const person = await this.personService.findOne(systemUserContext, {
|
|
129
|
+
filters: { _id: { eq: user?.personId } },
|
|
130
|
+
});
|
|
126
131
|
const organization = await this.organizationService.findOne(EmptyUserContext, { filters: { _id: { eq: user?._orgId } } });
|
|
127
132
|
const authorizations = await getUserContextAuthorizations(this.database, user);
|
|
128
133
|
let userContext = {
|
|
129
134
|
user: user,
|
|
130
135
|
person: person ?? undefined,
|
|
131
136
|
organization: organization ?? undefined,
|
|
132
|
-
authorizations: authorizations
|
|
137
|
+
authorizations: authorizations,
|
|
133
138
|
};
|
|
134
139
|
if (user.personId) {
|
|
135
|
-
const person = await this.personService.getById(
|
|
140
|
+
const person = await this.personService.getById(systemUserContext, user.personId);
|
|
136
141
|
if (person) {
|
|
137
142
|
userContext.person = person;
|
|
138
143
|
}
|
|
@@ -147,7 +152,10 @@ export class AuthService extends MultiTenantApiService {
|
|
|
147
152
|
return result;
|
|
148
153
|
}
|
|
149
154
|
async changePassword(userContext, queryObject, password) {
|
|
150
|
-
const updates = {
|
|
155
|
+
const updates = {
|
|
156
|
+
password: password,
|
|
157
|
+
_lastPasswordChange: moment().utc().toDate(),
|
|
158
|
+
};
|
|
151
159
|
const updatedUsers = await super.update(userContext, queryObject, updates);
|
|
152
160
|
const result = {
|
|
153
161
|
success: true,
|
|
@@ -162,12 +170,12 @@ export class AuthService extends MultiTenantApiService {
|
|
|
162
170
|
const tokenResponse = {
|
|
163
171
|
accessToken,
|
|
164
172
|
refreshToken: activeRefreshToken.token,
|
|
165
|
-
expiresOn: accessTokenExpiresOn
|
|
173
|
+
expiresOn: accessTokenExpiresOn,
|
|
166
174
|
};
|
|
167
175
|
return tokenResponse;
|
|
168
176
|
}
|
|
169
177
|
async getActiveRefreshToken(refreshToken, deviceId) {
|
|
170
|
-
const refreshTokenResult = await this.refreshTokenService.findOne(
|
|
178
|
+
const refreshTokenResult = await this.refreshTokenService.findOne(getSystemUserContext(), { filters: { token: { eq: refreshToken }, deviceId: { eq: deviceId } } });
|
|
171
179
|
let activeRefreshToken = null;
|
|
172
180
|
if (refreshTokenResult) {
|
|
173
181
|
const now = Date.now();
|
|
@@ -187,10 +195,10 @@ export class AuthService extends MultiTenantApiService {
|
|
|
187
195
|
userId,
|
|
188
196
|
expiresOn: expiresOn,
|
|
189
197
|
created: moment().utc().toDate(),
|
|
190
|
-
createdBy: userId
|
|
198
|
+
createdBy: userId,
|
|
191
199
|
};
|
|
192
200
|
const deleteResult = await this.deleteRefreshTokensForDevice(deviceId);
|
|
193
|
-
const insertResult = await this.refreshTokenService.create(
|
|
201
|
+
const insertResult = await this.refreshTokenService.create(getSystemUserContext(), newRefreshToken);
|
|
194
202
|
return insertResult;
|
|
195
203
|
}
|
|
196
204
|
async sendResetPasswordEmail(emailAddress, clientBaseUrl) {
|
|
@@ -209,38 +217,42 @@ export class AuthService extends MultiTenantApiService {
|
|
|
209
217
|
if (!retrievedPasswordResetToken) {
|
|
210
218
|
throw new ServerError(`Unable to retrieve password reset token for email: ${lowerCaseEmail}`);
|
|
211
219
|
}
|
|
212
|
-
if (retrievedPasswordResetToken.token !== passwordResetToken ||
|
|
213
|
-
|
|
220
|
+
if (retrievedPasswordResetToken.token !== passwordResetToken ||
|
|
221
|
+
retrievedPasswordResetToken.expiresOn < Date.now()) {
|
|
222
|
+
throw new BadRequestError("Invalid password reset token");
|
|
214
223
|
}
|
|
215
224
|
const validationErrors = entityUtils.validate(UserSpec, { password: password }, true, passwordValidator);
|
|
216
|
-
entityUtils.handleValidationResult(validationErrors,
|
|
225
|
+
entityUtils.handleValidationResult(validationErrors, "AuthService.resetPassword");
|
|
217
226
|
const result = await this.changePassword(getSystemUserContext(), { email: lowerCaseEmail }, password);
|
|
218
227
|
console.log(`password changed using forgot-password for email: ${lowerCaseEmail}`);
|
|
219
|
-
await this.passwordResetTokenService.deleteById(
|
|
228
|
+
await this.passwordResetTokenService.deleteById(getSystemUserContext(), retrievedPasswordResetToken._id.toString());
|
|
220
229
|
console.log(`passwordResetToken deleted for email: ${lowerCaseEmail}`);
|
|
221
230
|
return result;
|
|
222
231
|
}
|
|
223
232
|
deleteRefreshTokensForDevice(deviceId) {
|
|
224
|
-
return this.refreshTokenService.deleteMany(
|
|
233
|
+
return this.refreshTokenService.deleteMany(getSystemUserContext(), {
|
|
234
|
+
filters: { deviceId: { eq: deviceId } },
|
|
235
|
+
});
|
|
225
236
|
}
|
|
226
237
|
generateJwt(userContext) {
|
|
227
238
|
const jwtExpiryConfig = this.authConfig.jwtExpirationInSeconds;
|
|
228
|
-
const jwtExpirationInSeconds =
|
|
239
|
+
const jwtExpirationInSeconds = typeof jwtExpiryConfig === "string"
|
|
240
|
+
? parseInt(jwtExpiryConfig)
|
|
241
|
+
: jwtExpiryConfig;
|
|
229
242
|
const accessToken = JwtService.sign(userContext, this.authConfig.clientSecret, {
|
|
230
|
-
expiresIn: jwtExpirationInSeconds
|
|
243
|
+
expiresIn: jwtExpirationInSeconds,
|
|
231
244
|
});
|
|
232
245
|
return accessToken;
|
|
233
246
|
}
|
|
234
|
-
;
|
|
235
247
|
generateRefreshToken() {
|
|
236
|
-
return crypto.randomBytes(40).toString(
|
|
248
|
+
return crypto.randomBytes(40).toString("hex");
|
|
237
249
|
}
|
|
238
250
|
generateDeviceId() {
|
|
239
|
-
return crypto.randomBytes(40).toString(
|
|
251
|
+
return crypto.randomBytes(40).toString("hex");
|
|
240
252
|
}
|
|
241
253
|
getAndSetDeviceIdCookie(req, res) {
|
|
242
254
|
let isNewDeviceId = false;
|
|
243
|
-
let deviceId =
|
|
255
|
+
let deviceId = "";
|
|
244
256
|
const deviceIdFromCookie = this.getDeviceIdFromCookie(req);
|
|
245
257
|
if (deviceIdFromCookie) {
|
|
246
258
|
deviceId = deviceIdFromCookie;
|
|
@@ -252,14 +264,14 @@ export class AuthService extends MultiTenantApiService {
|
|
|
252
264
|
if (isNewDeviceId) {
|
|
253
265
|
const cookieOptions = {
|
|
254
266
|
maxAge: this.authConfig.deviceIdCookieMaxAgeInDays * 24 * 60 * 60 * 1000,
|
|
255
|
-
httpOnly: true
|
|
267
|
+
httpOnly: true,
|
|
256
268
|
};
|
|
257
|
-
res.cookie(
|
|
269
|
+
res.cookie("deviceId", deviceId, cookieOptions);
|
|
258
270
|
}
|
|
259
271
|
return deviceId;
|
|
260
272
|
}
|
|
261
273
|
getDeviceIdFromCookie(req) {
|
|
262
|
-
return req.cookies[
|
|
274
|
+
return req.cookies["deviceId"];
|
|
263
275
|
}
|
|
264
276
|
getExpiresOnFromSeconds(expiresInSeconds) {
|
|
265
277
|
return Date.now() + expiresInSeconds * 1000;
|
|
@@ -283,11 +295,12 @@ export class AuthService extends MultiTenantApiService {
|
|
|
283
295
|
}
|
|
284
296
|
async updateLastLoggedIn(userId) {
|
|
285
297
|
try {
|
|
286
|
-
const updates = {
|
|
298
|
+
const updates = {
|
|
299
|
+
_lastLoggedIn: moment().utc().toDate(),
|
|
300
|
+
};
|
|
287
301
|
const systemUserContext = getSystemUserContext();
|
|
288
302
|
await this.partialUpdateById(systemUserContext, userId, updates);
|
|
289
303
|
}
|
|
290
|
-
catch (error) {
|
|
291
|
-
}
|
|
304
|
+
catch (error) { }
|
|
292
305
|
}
|
|
293
306
|
}
|
|
@@ -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,6 +1,6 @@
|
|
|
1
|
-
import { IPersonModel } from "@loomcore/common/models";
|
|
2
|
-
import { IDatabase } from "../databases/index.js";
|
|
3
|
-
import {
|
|
4
|
-
export declare class PersonService extends
|
|
1
|
+
import { type IPersonModel } 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 PersonService extends MultiTenantApiService<IPersonModel> {
|
|
5
5
|
constructor(database: IDatabase);
|
|
6
6
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { personModelSpec } from "@loomcore/common/models";
|
|
2
|
-
import {
|
|
3
|
-
export class PersonService extends
|
|
2
|
+
import { MultiTenantApiService } from "./multi-tenant-api.service.js";
|
|
3
|
+
export class PersonService extends MultiTenantApiService {
|
|
4
4
|
constructor(database) {
|
|
5
|
-
super(database,
|
|
5
|
+
super(database, "persons", "person", personModelSpec);
|
|
6
6
|
}
|
|
7
7
|
}
|
|
@@ -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();
|