@internetderdinge/api 1.224.2
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/.github/copilot-instructions.md +77 -0
- package/CHANGELOG.md +11 -0
- package/README.md +52 -0
- package/package.json +112 -0
- package/src/accounts/accounts.controller.ts +166 -0
- package/src/accounts/accounts.route.ts +107 -0
- package/src/accounts/accounts.schemas.ts +16 -0
- package/src/accounts/accounts.service.ts +85 -0
- package/src/accounts/accounts.validation.ts +118 -0
- package/src/accounts/auth0.service.ts +226 -0
- package/src/config/config.ts +49 -0
- package/src/config/logger.ts +33 -0
- package/src/config/morgan.ts +22 -0
- package/src/config/passport.cjs +30 -0
- package/src/config/roles.ts +13 -0
- package/src/config/tokens.cjs +10 -0
- package/src/devices/devices.controller.ts +276 -0
- package/src/devices/devices.model.ts +126 -0
- package/src/devices/devices.route.ts +198 -0
- package/src/devices/devices.schemas.ts +94 -0
- package/src/devices/devices.service.ts +320 -0
- package/src/devices/devices.validation.ts +221 -0
- package/src/devicesNotifications/devicesNotifications.controller.ts +72 -0
- package/src/devicesNotifications/devicesNotifications.model.ts +67 -0
- package/src/devicesNotifications/devicesNotifications.route.ts +150 -0
- package/src/devicesNotifications/devicesNotifications.schemas.ts +11 -0
- package/src/devicesNotifications/devicesNotifications.service.ts +222 -0
- package/src/devicesNotifications/devicesNotifications.validation.ts +56 -0
- package/src/email/email.service.ts +609 -0
- package/src/files/upload.service.ts +145 -0
- package/src/i18n/i18n.ts +51 -0
- package/src/i18n/saveMissingLocalJsonBackend.ts +92 -0
- package/src/index.ts +7 -0
- package/src/iotdevice/iotdevice.controller.ts +136 -0
- package/src/iotdevice/iotdevice.model.ts +32 -0
- package/src/iotdevice/iotdevice.route.ts +181 -0
- package/src/iotdevice/iotdevice.schemas.ts +79 -0
- package/src/iotdevice/iotdevice.service.ts +732 -0
- package/src/iotdevice/iotdevice.validation.ts +61 -0
- package/src/middlewares/auth.ts +110 -0
- package/src/middlewares/checkJwt.cjs +19 -0
- package/src/middlewares/error.js.legacy +44 -0
- package/src/middlewares/error.ts +41 -0
- package/src/middlewares/mongooseValidations/ensureSameOrganization.ts +15 -0
- package/src/middlewares/rateLimiter.ts +10 -0
- package/src/middlewares/validate.ts +25 -0
- package/src/middlewares/validateAction.ts +41 -0
- package/src/middlewares/validateAdmin.ts +21 -0
- package/src/middlewares/validateAi.ts +24 -0
- package/src/middlewares/validateCurrentAuthUser.ts +23 -0
- package/src/middlewares/validateCurrentUser.ts +35 -0
- package/src/middlewares/validateDevice.ts +191 -0
- package/src/middlewares/validateDeviceUserOrganization.ts +54 -0
- package/src/middlewares/validateOrganization.ts +109 -0
- package/src/middlewares/validateQuerySearchUserAndOrganization.ts +75 -0
- package/src/middlewares/validateTokens.ts +36 -0
- package/src/middlewares/validateUser.ts +75 -0
- package/src/middlewares/validateZod.ts +54 -0
- package/src/models/plugins/index.ts +7 -0
- package/src/models/plugins/paginate.plugin.ts +145 -0
- package/src/models/plugins/paginateNew.plugin.ts +206 -0
- package/src/models/plugins/simplePopulate.ts +12 -0
- package/src/models/plugins/toJSON.plugin.ts +51 -0
- package/src/organizations/organizations.controller.ts +101 -0
- package/src/organizations/organizations.model.ts +62 -0
- package/src/organizations/organizations.route.ts +119 -0
- package/src/organizations/organizations.schemas.ts +8 -0
- package/src/organizations/organizations.service.ts +85 -0
- package/src/organizations/organizations.validation.ts +76 -0
- package/src/pdf/pdf.controller.ts +18 -0
- package/src/pdf/pdf.route.ts +28 -0
- package/src/pdf/pdf.schemas.ts +7 -0
- package/src/pdf/pdf.service.ts +89 -0
- package/src/pdf/pdf.validation.ts +30 -0
- package/src/tokens/tokens.controller.ts +81 -0
- package/src/tokens/tokens.model.ts +24 -0
- package/src/tokens/tokens.route.ts +66 -0
- package/src/tokens/tokens.schemas.ts +15 -0
- package/src/tokens/tokens.service.ts +46 -0
- package/src/tokens/tokens.validation.ts +13 -0
- package/src/types/routeSpec.ts +1 -0
- package/src/users/users.controller.ts +234 -0
- package/src/users/users.model.ts +89 -0
- package/src/users/users.route.ts +171 -0
- package/src/users/users.schemas.ts +79 -0
- package/src/users/users.service.ts +393 -0
- package/src/users/users.validation.ts +166 -0
- package/src/utils/ApiError.ts +18 -0
- package/src/utils/buildRouterAndDocs.ts +85 -0
- package/src/utils/catchAsync.ts +9 -0
- package/src/utils/comparePapers.service.ts +48 -0
- package/src/utils/filterOptions.ts +37 -0
- package/src/utils/medicationName.ts +12 -0
- package/src/utils/pick.ts +16 -0
- package/src/utils/registerOpenApi.ts +32 -0
- package/src/utils/urlUtils.ts +14 -0
- package/src/utils/userName.ts +27 -0
- package/src/utils/zValidations.ts +89 -0
- package/src/validations/auth.validation.cjs +60 -0
- package/src/validations/custom.validation.ts +26 -0
- package/src/validations/index.cjs +2 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import httpStatus from "http-status";
|
|
2
|
+
import type { FilterQuery, PaginateOptions } from "mongoose";
|
|
3
|
+
import { User } from "./users.model.js";
|
|
4
|
+
import type { IUser, IUserDocument, IUserModel } from "./users.model.js";
|
|
5
|
+
import ApiError from "../utils/ApiError.js";
|
|
6
|
+
import auth0Service from "../accounts/auth0.service";
|
|
7
|
+
import organizationsService from "../organizations/organizations.service";
|
|
8
|
+
import { sendEmail } from "../email/email.service";
|
|
9
|
+
import i18n from "../i18n/i18n";
|
|
10
|
+
|
|
11
|
+
export type UpdateTimesByIdHook = (
|
|
12
|
+
userId: string,
|
|
13
|
+
updateBody: any,
|
|
14
|
+
dryRun?: boolean,
|
|
15
|
+
) => Promise<any>;
|
|
16
|
+
|
|
17
|
+
let updateTimesByIdHook: UpdateTimesByIdHook | null = null;
|
|
18
|
+
|
|
19
|
+
export const setUpdateTimesByIdHook = (hook?: UpdateTimesByIdHook): void => {
|
|
20
|
+
updateTimesByIdHook = hook ?? null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create a new user
|
|
25
|
+
*/
|
|
26
|
+
export const createUser = async (
|
|
27
|
+
userBody: Partial<IUser>,
|
|
28
|
+
): Promise<IUserDocument> => {
|
|
29
|
+
return User.create(userBody);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create the “current” user (alias of createUser)
|
|
34
|
+
*/
|
|
35
|
+
export const createCurrentUser = async (
|
|
36
|
+
userBody: Partial<IUser>,
|
|
37
|
+
): Promise<IUserDocument> => {
|
|
38
|
+
return createUser(userBody);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Populate a single Auth0 user
|
|
43
|
+
*/
|
|
44
|
+
const populateAuth0User = async (
|
|
45
|
+
user: IUserDocument | null,
|
|
46
|
+
): Promise<any | undefined> => {
|
|
47
|
+
if (!user) return undefined;
|
|
48
|
+
const auth0users = await auth0Service.getUsersByIds([user.owner]);
|
|
49
|
+
|
|
50
|
+
return auth0users?.data?.find((u) => u.user_id === user.owner);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Populate many Auth0 users
|
|
55
|
+
*/
|
|
56
|
+
const populateAuth0Users = async (data: IUserDocument[]): Promise<any[]> => {
|
|
57
|
+
const owners = data.map((u) => u.owner);
|
|
58
|
+
const auth0users = await auth0Service.getUsersByIds(owners);
|
|
59
|
+
if (!auth0users) return data;
|
|
60
|
+
return data.map((doc) => ({
|
|
61
|
+
...doc.toJSON(),
|
|
62
|
+
auth0User: auth0users?.data?.find((u) => u.user_id === doc.owner),
|
|
63
|
+
}));
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Query for users with pagination + Auth0 enrichment
|
|
68
|
+
*/
|
|
69
|
+
export const queryUsers = async (
|
|
70
|
+
filter: FilterQuery<IUser>,
|
|
71
|
+
options: PaginateOptions,
|
|
72
|
+
): Promise<{
|
|
73
|
+
results: any[];
|
|
74
|
+
page: number;
|
|
75
|
+
totalPages: number;
|
|
76
|
+
totalResults: number;
|
|
77
|
+
}> => {
|
|
78
|
+
const result = await (User as IUserModel).paginate(filter, options);
|
|
79
|
+
result.results = await populateAuth0Users(result.results as IUserDocument[]);
|
|
80
|
+
return result;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get user by Mongo _id
|
|
85
|
+
*/
|
|
86
|
+
export const getById = async (id: string): Promise<IUserDocument | null> => {
|
|
87
|
+
return User.findById(id);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get user by _id with Auth0 info
|
|
92
|
+
*/
|
|
93
|
+
export const getByIdWithAuth0 = async (id: string): Promise<any | null> => {
|
|
94
|
+
const user = await getById(id);
|
|
95
|
+
if (!user) return null;
|
|
96
|
+
const auth0User = await populateAuth0User(user);
|
|
97
|
+
const json = user.toJSON();
|
|
98
|
+
json.auth0User = auth0User;
|
|
99
|
+
return json;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get all users in a given category (and optional organization)
|
|
104
|
+
*/
|
|
105
|
+
export const getUsersByCategory = async (
|
|
106
|
+
category: string,
|
|
107
|
+
organization?: string,
|
|
108
|
+
): Promise<IUserDocument[]> => {
|
|
109
|
+
const filter: any = { category };
|
|
110
|
+
if (organization) filter.organization = organization;
|
|
111
|
+
return User.find(filter);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get all users for an organization
|
|
116
|
+
*/
|
|
117
|
+
export const getUsersByOrganization = async (
|
|
118
|
+
organization: string,
|
|
119
|
+
): Promise<IUserDocument[]> => {
|
|
120
|
+
return User.find({ organization }).lean();
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get one user by organization + userId
|
|
125
|
+
*/
|
|
126
|
+
export const getUsersByOrganizationAndId = async (
|
|
127
|
+
organization: string,
|
|
128
|
+
userId: string,
|
|
129
|
+
): Promise<IUserDocument | null> => {
|
|
130
|
+
return User.findOne({ organization, _id: userId }).lean();
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get all users for a given owner
|
|
135
|
+
*/
|
|
136
|
+
export const getUsersByOwner = async (
|
|
137
|
+
owner: string,
|
|
138
|
+
): Promise<IUserDocument[]> => {
|
|
139
|
+
return User.find({ owner });
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get single user by owner + organization, with Auth0 info
|
|
144
|
+
*/
|
|
145
|
+
export const getUserByOwner = async (
|
|
146
|
+
owner: string,
|
|
147
|
+
organization: string,
|
|
148
|
+
): Promise<any | null> => {
|
|
149
|
+
const user = await User.findOne({ owner, organization });
|
|
150
|
+
if (!user) return null;
|
|
151
|
+
const auth0User = await populateAuth0User(user);
|
|
152
|
+
const json = user.toJSON();
|
|
153
|
+
json.auth0User = auth0User;
|
|
154
|
+
return json;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get user by email
|
|
159
|
+
*/
|
|
160
|
+
export const getUserByEmail = async (
|
|
161
|
+
email: string,
|
|
162
|
+
): Promise<IUserDocument | null> => {
|
|
163
|
+
return User.findOne({ email });
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Send an invite email
|
|
168
|
+
*/
|
|
169
|
+
export const sendInviteEmail = async (params: {
|
|
170
|
+
auth: { sub: string };
|
|
171
|
+
user: IUserDocument;
|
|
172
|
+
inviteCode: string;
|
|
173
|
+
email: string;
|
|
174
|
+
}): Promise<void> => {
|
|
175
|
+
const { auth, user, inviteCode, email } = params;
|
|
176
|
+
const organization = await organizationsService.getOrganizationById(
|
|
177
|
+
user.organization,
|
|
178
|
+
);
|
|
179
|
+
const auth0User = await auth0Service.getUserById(auth.sub);
|
|
180
|
+
const lng = auth0User.data?.app_metadata?.language as string | "en";
|
|
181
|
+
|
|
182
|
+
const title = `${i18n.t("Invite to ", { lng })}${
|
|
183
|
+
organization.kind === "private-wirewire" ? "paperlesspaper" : "ANABOX smart"
|
|
184
|
+
}`;
|
|
185
|
+
const body = i18n.t(
|
|
186
|
+
"You have been invited to join the group. Click on the link to accept the invitation.",
|
|
187
|
+
{ lng },
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
await sendEmail({
|
|
191
|
+
title,
|
|
192
|
+
body,
|
|
193
|
+
url: `/${user.organization}/invite/${inviteCode}`,
|
|
194
|
+
actionButtonText: "Accept invite",
|
|
195
|
+
domain: organization.kind === "private-wirewire" ? "web" : "memo",
|
|
196
|
+
email,
|
|
197
|
+
});
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Update a user by ID
|
|
202
|
+
*/
|
|
203
|
+
export const updateUserById = async (
|
|
204
|
+
userId: string,
|
|
205
|
+
updateBody: Partial<IUser> & { meta?: Record<string, any> },
|
|
206
|
+
auth: { sub: string },
|
|
207
|
+
): Promise<IUserDocument> => {
|
|
208
|
+
const user = await getById(userId);
|
|
209
|
+
|
|
210
|
+
if (!user) {
|
|
211
|
+
throw new ApiError(httpStatus.NOT_FOUND, `User not found: ${userId}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (user.status === "invited") {
|
|
215
|
+
await sendInviteEmail({
|
|
216
|
+
auth,
|
|
217
|
+
user,
|
|
218
|
+
inviteCode: user.inviteCode!,
|
|
219
|
+
email: updateBody.email!,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
//TODO: restrict fields that can be updated, temporarily excluding role
|
|
223
|
+
const { role, ...updateBodyRest } = updateBody;
|
|
224
|
+
const meta = { ...user.meta, ...updateBodyRest.meta };
|
|
225
|
+
Object.assign(user, { ...updateBodyRest, meta });
|
|
226
|
+
await user.save();
|
|
227
|
+
return user;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Delete a user by ID
|
|
232
|
+
*/
|
|
233
|
+
export const deleteUserById = async (
|
|
234
|
+
userId: string,
|
|
235
|
+
): Promise<IUserDocument> => {
|
|
236
|
+
const user = await getById(userId);
|
|
237
|
+
if (!user) {
|
|
238
|
+
throw new ApiError(httpStatus.NOT_FOUND, `User not found: ${userId}`);
|
|
239
|
+
}
|
|
240
|
+
if (user.role === "admin") {
|
|
241
|
+
const admins = await User.find({
|
|
242
|
+
organization: user.organization,
|
|
243
|
+
role: "admin",
|
|
244
|
+
});
|
|
245
|
+
if (admins.length < 2) {
|
|
246
|
+
throw new ApiError(
|
|
247
|
+
httpStatus.BAD_REQUEST,
|
|
248
|
+
"At least one admin is required",
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
await user.deleteOne();
|
|
253
|
+
return user;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* (Misnamed) delete a user’s image by ID
|
|
258
|
+
*/
|
|
259
|
+
export const userImageById = async (userId: string): Promise<IUserDocument> => {
|
|
260
|
+
const user = await getById(userId);
|
|
261
|
+
if (!user) {
|
|
262
|
+
throw new ApiError(httpStatus.NOT_FOUND, `User not found: ${userId}`);
|
|
263
|
+
}
|
|
264
|
+
await user.deleteOne();
|
|
265
|
+
return user;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Invite a user to an organization
|
|
270
|
+
*/
|
|
271
|
+
export const organizationInvite = async (
|
|
272
|
+
body: { organizationId: string; role: string },
|
|
273
|
+
oldUser: { id: string },
|
|
274
|
+
status = "invited",
|
|
275
|
+
): Promise<IUserDocument> => {
|
|
276
|
+
const user = await getById(oldUser.id);
|
|
277
|
+
if (!user) throw new ApiError(httpStatus.NOT_FOUND, "User not found");
|
|
278
|
+
if (user.organizations.some((o) => o.id.equals(body.organizationId))) {
|
|
279
|
+
throw new ApiError(httpStatus.BAD_REQUEST, "Invite already exists");
|
|
280
|
+
}
|
|
281
|
+
user.organizations.push({ id: body.organizationId, role: body.role, status });
|
|
282
|
+
await user.save();
|
|
283
|
+
return user;
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Get an invite by code
|
|
288
|
+
*/
|
|
289
|
+
export const getInvite = async (params: {
|
|
290
|
+
inviteCode: string;
|
|
291
|
+
}): Promise<IUserDocument | null> => {
|
|
292
|
+
return User.findOne({
|
|
293
|
+
inviteCode: params.inviteCode,
|
|
294
|
+
owner: { $exists: false },
|
|
295
|
+
}).populate("organizationData");
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Accept or decline an invite
|
|
300
|
+
*/
|
|
301
|
+
export const updateInvite = async (params: {
|
|
302
|
+
inviteCode: string;
|
|
303
|
+
status: string;
|
|
304
|
+
owner: string;
|
|
305
|
+
}): Promise<IUserDocument> => {
|
|
306
|
+
const user = await User.findOne({
|
|
307
|
+
inviteCode: params.inviteCode,
|
|
308
|
+
owner: { $exists: false },
|
|
309
|
+
});
|
|
310
|
+
if (!user) throw new ApiError(httpStatus.NOT_FOUND, "Invite not found");
|
|
311
|
+
user.status = params.status;
|
|
312
|
+
user.owner = params.owner;
|
|
313
|
+
user.inviteCode = null;
|
|
314
|
+
await user.save();
|
|
315
|
+
return user;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Remove a user from an organization
|
|
320
|
+
*/
|
|
321
|
+
export const organizationRemove = async (body: {
|
|
322
|
+
userId: string;
|
|
323
|
+
organizationId: string;
|
|
324
|
+
}): Promise<IUserDocument> => {
|
|
325
|
+
const user = await getById(body.userId);
|
|
326
|
+
if (!user) throw new ApiError(httpStatus.NOT_FOUND, "User not found");
|
|
327
|
+
user.organizations = user.organizations.filter(
|
|
328
|
+
(o) => !o.id.equals(body.organizationId),
|
|
329
|
+
);
|
|
330
|
+
await user.save();
|
|
331
|
+
return user;
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Fetch up to 100k users with organization populated
|
|
336
|
+
*/
|
|
337
|
+
export const queryAllCalendars = async (): Promise<IUserDocument[]> => {
|
|
338
|
+
return User.find({}, null, { limit: 100000 }).populate("organizationData");
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Delete many users by ID list
|
|
343
|
+
*/
|
|
344
|
+
export const deleteMany = async (
|
|
345
|
+
idList: string[],
|
|
346
|
+
): Promise<{ deletedCount?: number }> => {
|
|
347
|
+
return User.deleteMany({ _id: { $in: idList } });
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Update times for a user by ID (hooked from memo-api)
|
|
352
|
+
*/
|
|
353
|
+
export const updateTimesById = async (
|
|
354
|
+
userId: string,
|
|
355
|
+
updateBody: any,
|
|
356
|
+
dryRun = true,
|
|
357
|
+
): Promise<any> => {
|
|
358
|
+
if (!updateTimesByIdHook) {
|
|
359
|
+
throw new ApiError(
|
|
360
|
+
httpStatus.NOT_IMPLEMENTED,
|
|
361
|
+
"updateTimesById not configured",
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
return updateTimesByIdHook(userId, updateBody, dryRun);
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
export default {
|
|
368
|
+
createUser,
|
|
369
|
+
createCurrentUser,
|
|
370
|
+
getById,
|
|
371
|
+
getByIdWithAuth0,
|
|
372
|
+
getUsersByCategory,
|
|
373
|
+
getUsersByOrganization,
|
|
374
|
+
getUsersByOrganizationAndId,
|
|
375
|
+
getUsersByOwner,
|
|
376
|
+
getUserByEmail,
|
|
377
|
+
getUserByOwner,
|
|
378
|
+
sendInviteEmail,
|
|
379
|
+
updateUserById,
|
|
380
|
+
updateTimesById,
|
|
381
|
+
deleteUserById,
|
|
382
|
+
userImageById,
|
|
383
|
+
organizationInvite,
|
|
384
|
+
getInvite,
|
|
385
|
+
updateInvite,
|
|
386
|
+
organizationRemove,
|
|
387
|
+
queryUsers,
|
|
388
|
+
queryAllCalendars,
|
|
389
|
+
deleteMany,
|
|
390
|
+
sendEmail,
|
|
391
|
+
populateAuth0User,
|
|
392
|
+
populateAuth0Users,
|
|
393
|
+
};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi";
|
|
3
|
+
import { objectId, password } from "../validations/custom.validation.js";
|
|
4
|
+
import {
|
|
5
|
+
zPagination,
|
|
6
|
+
zGet,
|
|
7
|
+
zObjectId,
|
|
8
|
+
zPatchBody,
|
|
9
|
+
zUpdate,
|
|
10
|
+
zDelete,
|
|
11
|
+
} from "../utils/zValidations.js";
|
|
12
|
+
|
|
13
|
+
extendZodWithOpenApi(z);
|
|
14
|
+
|
|
15
|
+
export const createUserSchema = {
|
|
16
|
+
body: z.object({
|
|
17
|
+
meta: z
|
|
18
|
+
.record(z.any())
|
|
19
|
+
.optional()
|
|
20
|
+
.openapi({
|
|
21
|
+
example: { key: "value" },
|
|
22
|
+
description: "Additional metadata for the user",
|
|
23
|
+
}),
|
|
24
|
+
organization: zObjectId.openapi({
|
|
25
|
+
description: "Organization ObjectId",
|
|
26
|
+
}),
|
|
27
|
+
email: z.string().email().optional().nullable().openapi({
|
|
28
|
+
example: "user@example.com",
|
|
29
|
+
description: "User email address",
|
|
30
|
+
}),
|
|
31
|
+
timezone: z.string().optional().openapi({
|
|
32
|
+
example: "Europe/Berlin",
|
|
33
|
+
description: "IANA timezone string",
|
|
34
|
+
}),
|
|
35
|
+
role: z.enum(["user", "admin", "patient", "onlyself"]).optional().openapi({
|
|
36
|
+
description: "Role assigned to the user",
|
|
37
|
+
}),
|
|
38
|
+
category: z
|
|
39
|
+
.enum(["doctor", "nurse", "patient", "pharmacist", "relative"])
|
|
40
|
+
.optional()
|
|
41
|
+
.openapi({
|
|
42
|
+
description: "Category of the user",
|
|
43
|
+
}),
|
|
44
|
+
}),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const createCurrentUserSchema = createUserSchema;
|
|
48
|
+
|
|
49
|
+
export const queryUsersSchema = {
|
|
50
|
+
...zPagination,
|
|
51
|
+
query: zPagination.query.extend({
|
|
52
|
+
organization: zObjectId.optional().openapi({
|
|
53
|
+
description: "Filter users by organization ObjectId",
|
|
54
|
+
example: "60c72b2f9b1e8d001c8e4f3a",
|
|
55
|
+
}),
|
|
56
|
+
}),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const getUserSchema = zGet("userId");
|
|
60
|
+
|
|
61
|
+
export const getCurrentUserSchema = {
|
|
62
|
+
query: z.object({
|
|
63
|
+
organization: zObjectId,
|
|
64
|
+
}),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const updateUserSchema = {
|
|
68
|
+
...zUpdate("userId"),
|
|
69
|
+
body: zPatchBody({
|
|
70
|
+
password: z
|
|
71
|
+
.string()
|
|
72
|
+
.refine(
|
|
73
|
+
(val) => {
|
|
74
|
+
try {
|
|
75
|
+
password(val);
|
|
76
|
+
return true;
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
{ message: "Invalid password format" },
|
|
82
|
+
)
|
|
83
|
+
.optional()
|
|
84
|
+
.openapi({ description: "New user password" }),
|
|
85
|
+
name: z.string().optional().openapi({ description: "User full name" }),
|
|
86
|
+
timezone: z.string().optional().openapi({ description: "IANA timezone" }),
|
|
87
|
+
avatar: z.string().optional().openapi({ description: "Avatar URL" }),
|
|
88
|
+
meta: z
|
|
89
|
+
.record(z.any())
|
|
90
|
+
.optional()
|
|
91
|
+
.openapi({ description: "Additional metadata" }),
|
|
92
|
+
category: z
|
|
93
|
+
.enum(["doctor", "nurse", "patient", "pharmacist", "relative"])
|
|
94
|
+
.optional()
|
|
95
|
+
.openapi({ description: "User category" }),
|
|
96
|
+
email: z
|
|
97
|
+
.string()
|
|
98
|
+
.email()
|
|
99
|
+
.nullable()
|
|
100
|
+
.optional()
|
|
101
|
+
.openapi({ description: "User email address" }),
|
|
102
|
+
role: z
|
|
103
|
+
.enum(["user", "admin", "patient", "onlyself"])
|
|
104
|
+
.optional()
|
|
105
|
+
.openapi({ description: "User role" }),
|
|
106
|
+
inviteCode: z.string().optional().openapi({ description: "Invite code" }),
|
|
107
|
+
organization: zObjectId
|
|
108
|
+
.optional()
|
|
109
|
+
.openapi({ description: "Organization ObjectId" }),
|
|
110
|
+
}),
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const deleteUserSchema = zDelete("userId");
|
|
114
|
+
|
|
115
|
+
export const organizationInviteSchema = {
|
|
116
|
+
body: z.object({
|
|
117
|
+
organizationId: zObjectId.openapi({ description: "Organization ObjectId" }),
|
|
118
|
+
action: z.string().optional().openapi({ description: "Invite action" }),
|
|
119
|
+
role: z.string().optional().openapi({ description: "Role on invite" }),
|
|
120
|
+
}),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const updateInviteSchema = {
|
|
124
|
+
body: z.object({
|
|
125
|
+
organization: zObjectId.openapi({ description: "Organization ObjectId" }),
|
|
126
|
+
status: z.enum(["accepted"]).openapi({ description: "Invite status" }),
|
|
127
|
+
inviteCode: z.string().optional().openapi({ description: "Invite code" }),
|
|
128
|
+
}),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export const organizationRemoveSchema = {
|
|
132
|
+
body: z.object({
|
|
133
|
+
userId: zObjectId.openapi({ description: "User ObjectId" }),
|
|
134
|
+
organizationId: zObjectId.openapi({ description: "Organization ObjectId" }),
|
|
135
|
+
}),
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export const updateTimesByIdSchema = {
|
|
139
|
+
...zUpdate("userId"),
|
|
140
|
+
body: z
|
|
141
|
+
.object({})
|
|
142
|
+
.catchall(z.string())
|
|
143
|
+
.openapi({
|
|
144
|
+
description: "Arbitrary key/value map of intake times",
|
|
145
|
+
example: {
|
|
146
|
+
"intake-morning": "10:00",
|
|
147
|
+
"intake-noon": "",
|
|
148
|
+
"intake-afternoon": "",
|
|
149
|
+
"intake-night": "15:00",
|
|
150
|
+
},
|
|
151
|
+
}),
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export const updateTimesByIdSchemas = {
|
|
155
|
+
// if you need a path param, uncomment and adjust:
|
|
156
|
+
// ...zUpdate('timeId'),
|
|
157
|
+
...updateTimesByIdSchema,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export const validateGetInviteSchema = {
|
|
161
|
+
params: z.object({
|
|
162
|
+
inviteCode: z.string().openapi({ description: "Invite code to validate" }),
|
|
163
|
+
}),
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export const sendVerificationEmailValidationSchema = {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export class ApiError extends Error {
|
|
2
|
+
statusCode: number;
|
|
3
|
+
isOperational: boolean;
|
|
4
|
+
|
|
5
|
+
constructor(statusCode: number, message: string, isOperational = true, stack = '', raw: any = null) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.statusCode = statusCode;
|
|
8
|
+
this.isOperational = isOperational;
|
|
9
|
+
this.raw = raw;
|
|
10
|
+
if (stack) {
|
|
11
|
+
this.stack = stack;
|
|
12
|
+
} else {
|
|
13
|
+
Error.captureStackTrace(this, this.constructor);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default ApiError;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { registry } from '../utils/registerOpenApi';
|
|
2
|
+
|
|
3
|
+
import { validateZod } from '../middlewares/validateZod';
|
|
4
|
+
import { bearerAuth, xApiKey } from '../utils/registerOpenApi';
|
|
5
|
+
|
|
6
|
+
import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
|
|
9
|
+
extendZodWithOpenApi(z);
|
|
10
|
+
|
|
11
|
+
const roleValidatorNames = ['validateAiRole', 'validateAdmin'];
|
|
12
|
+
function hasRoleValidation(validators: Function[] = []): boolean {
|
|
13
|
+
return validators.some((fn) => roleValidatorNames.includes(fn.name));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type RouteSpec = {
|
|
17
|
+
method: 'post';
|
|
18
|
+
path: string;
|
|
19
|
+
requestBody: AnyZodObject;
|
|
20
|
+
responseSchema?: AnyZodObject;
|
|
21
|
+
handler: RequestHandler;
|
|
22
|
+
summary: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default function buildAiRouterAndDocs(router: Router, routeSpecs: any, basePath = '/', tags: string[] = []) {
|
|
26
|
+
routeSpecs.forEach((spec) => {
|
|
27
|
+
// mount Express
|
|
28
|
+
|
|
29
|
+
if (!spec.validate) {
|
|
30
|
+
spec.validate = [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (spec.requestSchema) {
|
|
34
|
+
spec.validateWithRequestSchema = [validateZod(spec.requestSchema), ...spec.validate];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (spec.validateWithRequestSchema) {
|
|
38
|
+
router[spec.method](spec.path, ...spec.validateWithRequestSchema, spec.handler);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
var { body, ...rest } = spec.requestSchema || {};
|
|
42
|
+
|
|
43
|
+
if (body) {
|
|
44
|
+
rest.body = {
|
|
45
|
+
content: {
|
|
46
|
+
'application/json': {
|
|
47
|
+
schema: body,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// console.log('spec.requestSchema', body);
|
|
54
|
+
|
|
55
|
+
if (spec.responseSchema && !hasRoleValidation(spec.validate) && spec.privateDocs !== true && spec.memoOnly !== true) {
|
|
56
|
+
// collect all middleware fn names (falls back to '<anonymous>' if unnamed)
|
|
57
|
+
const middlewareNames = (spec.validate || []).map((fn) => `\`${fn.name}\`` || '<anonymous>');
|
|
58
|
+
|
|
59
|
+
registry.registerPath({
|
|
60
|
+
method: spec.method,
|
|
61
|
+
path: basePath + spec.path,
|
|
62
|
+
summary: spec.summary,
|
|
63
|
+
request: rest,
|
|
64
|
+
|
|
65
|
+
// append middleware names to the description
|
|
66
|
+
description: [spec.description, `\n\nMiddlewares: ${middlewareNames.join(', ')}`].filter(Boolean).join('\n'),
|
|
67
|
+
|
|
68
|
+
// (optionally) expose them as a custom extension instead:
|
|
69
|
+
'x-middlewares': middlewareNames,
|
|
70
|
+
|
|
71
|
+
security: [{ [bearerAuth.name]: [] }, { [xApiKey.name]: [] }],
|
|
72
|
+
responses: {
|
|
73
|
+
200: {
|
|
74
|
+
description: 'Object with user data.',
|
|
75
|
+
content: {
|
|
76
|
+
'application/json': { schema: spec.responseSchema },
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
tags,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
// else: streaming endpoint, we don’t register it in OpenAPI
|
|
84
|
+
});
|
|
85
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
2
|
+
|
|
3
|
+
const catchAsync =
|
|
4
|
+
(fn: (req: Request, res: Response, next: NextFunction) => Promise<any>) =>
|
|
5
|
+
(req: Request, res: Response, next: NextFunction): void => {
|
|
6
|
+
Promise.resolve(fn(req, res, next)).catch((err) => next(err));
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default catchAsync;
|