@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.
Files changed (102) hide show
  1. package/.github/copilot-instructions.md +77 -0
  2. package/CHANGELOG.md +11 -0
  3. package/README.md +52 -0
  4. package/package.json +112 -0
  5. package/src/accounts/accounts.controller.ts +166 -0
  6. package/src/accounts/accounts.route.ts +107 -0
  7. package/src/accounts/accounts.schemas.ts +16 -0
  8. package/src/accounts/accounts.service.ts +85 -0
  9. package/src/accounts/accounts.validation.ts +118 -0
  10. package/src/accounts/auth0.service.ts +226 -0
  11. package/src/config/config.ts +49 -0
  12. package/src/config/logger.ts +33 -0
  13. package/src/config/morgan.ts +22 -0
  14. package/src/config/passport.cjs +30 -0
  15. package/src/config/roles.ts +13 -0
  16. package/src/config/tokens.cjs +10 -0
  17. package/src/devices/devices.controller.ts +276 -0
  18. package/src/devices/devices.model.ts +126 -0
  19. package/src/devices/devices.route.ts +198 -0
  20. package/src/devices/devices.schemas.ts +94 -0
  21. package/src/devices/devices.service.ts +320 -0
  22. package/src/devices/devices.validation.ts +221 -0
  23. package/src/devicesNotifications/devicesNotifications.controller.ts +72 -0
  24. package/src/devicesNotifications/devicesNotifications.model.ts +67 -0
  25. package/src/devicesNotifications/devicesNotifications.route.ts +150 -0
  26. package/src/devicesNotifications/devicesNotifications.schemas.ts +11 -0
  27. package/src/devicesNotifications/devicesNotifications.service.ts +222 -0
  28. package/src/devicesNotifications/devicesNotifications.validation.ts +56 -0
  29. package/src/email/email.service.ts +609 -0
  30. package/src/files/upload.service.ts +145 -0
  31. package/src/i18n/i18n.ts +51 -0
  32. package/src/i18n/saveMissingLocalJsonBackend.ts +92 -0
  33. package/src/index.ts +7 -0
  34. package/src/iotdevice/iotdevice.controller.ts +136 -0
  35. package/src/iotdevice/iotdevice.model.ts +32 -0
  36. package/src/iotdevice/iotdevice.route.ts +181 -0
  37. package/src/iotdevice/iotdevice.schemas.ts +79 -0
  38. package/src/iotdevice/iotdevice.service.ts +732 -0
  39. package/src/iotdevice/iotdevice.validation.ts +61 -0
  40. package/src/middlewares/auth.ts +110 -0
  41. package/src/middlewares/checkJwt.cjs +19 -0
  42. package/src/middlewares/error.js.legacy +44 -0
  43. package/src/middlewares/error.ts +41 -0
  44. package/src/middlewares/mongooseValidations/ensureSameOrganization.ts +15 -0
  45. package/src/middlewares/rateLimiter.ts +10 -0
  46. package/src/middlewares/validate.ts +25 -0
  47. package/src/middlewares/validateAction.ts +41 -0
  48. package/src/middlewares/validateAdmin.ts +21 -0
  49. package/src/middlewares/validateAi.ts +24 -0
  50. package/src/middlewares/validateCurrentAuthUser.ts +23 -0
  51. package/src/middlewares/validateCurrentUser.ts +35 -0
  52. package/src/middlewares/validateDevice.ts +191 -0
  53. package/src/middlewares/validateDeviceUserOrganization.ts +54 -0
  54. package/src/middlewares/validateOrganization.ts +109 -0
  55. package/src/middlewares/validateQuerySearchUserAndOrganization.ts +75 -0
  56. package/src/middlewares/validateTokens.ts +36 -0
  57. package/src/middlewares/validateUser.ts +75 -0
  58. package/src/middlewares/validateZod.ts +54 -0
  59. package/src/models/plugins/index.ts +7 -0
  60. package/src/models/plugins/paginate.plugin.ts +145 -0
  61. package/src/models/plugins/paginateNew.plugin.ts +206 -0
  62. package/src/models/plugins/simplePopulate.ts +12 -0
  63. package/src/models/plugins/toJSON.plugin.ts +51 -0
  64. package/src/organizations/organizations.controller.ts +101 -0
  65. package/src/organizations/organizations.model.ts +62 -0
  66. package/src/organizations/organizations.route.ts +119 -0
  67. package/src/organizations/organizations.schemas.ts +8 -0
  68. package/src/organizations/organizations.service.ts +85 -0
  69. package/src/organizations/organizations.validation.ts +76 -0
  70. package/src/pdf/pdf.controller.ts +18 -0
  71. package/src/pdf/pdf.route.ts +28 -0
  72. package/src/pdf/pdf.schemas.ts +7 -0
  73. package/src/pdf/pdf.service.ts +89 -0
  74. package/src/pdf/pdf.validation.ts +30 -0
  75. package/src/tokens/tokens.controller.ts +81 -0
  76. package/src/tokens/tokens.model.ts +24 -0
  77. package/src/tokens/tokens.route.ts +66 -0
  78. package/src/tokens/tokens.schemas.ts +15 -0
  79. package/src/tokens/tokens.service.ts +46 -0
  80. package/src/tokens/tokens.validation.ts +13 -0
  81. package/src/types/routeSpec.ts +1 -0
  82. package/src/users/users.controller.ts +234 -0
  83. package/src/users/users.model.ts +89 -0
  84. package/src/users/users.route.ts +171 -0
  85. package/src/users/users.schemas.ts +79 -0
  86. package/src/users/users.service.ts +393 -0
  87. package/src/users/users.validation.ts +166 -0
  88. package/src/utils/ApiError.ts +18 -0
  89. package/src/utils/buildRouterAndDocs.ts +85 -0
  90. package/src/utils/catchAsync.ts +9 -0
  91. package/src/utils/comparePapers.service.ts +48 -0
  92. package/src/utils/filterOptions.ts +37 -0
  93. package/src/utils/medicationName.ts +12 -0
  94. package/src/utils/pick.ts +16 -0
  95. package/src/utils/registerOpenApi.ts +32 -0
  96. package/src/utils/urlUtils.ts +14 -0
  97. package/src/utils/userName.ts +27 -0
  98. package/src/utils/zValidations.ts +89 -0
  99. package/src/validations/auth.validation.cjs +60 -0
  100. package/src/validations/custom.validation.ts +26 -0
  101. package/src/validations/index.cjs +2 -0
  102. 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;