@internetderdinge/api 1.224.2 → 1.229.1

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 (169) hide show
  1. package/dist/src/accounts/accounts.controller.js +89 -0
  2. package/dist/src/accounts/accounts.route.js +101 -0
  3. package/dist/src/accounts/accounts.schemas.js +12 -0
  4. package/dist/src/accounts/accounts.service.js +65 -0
  5. package/dist/src/accounts/accounts.validation.js +99 -0
  6. package/dist/src/accounts/auth0.service.js +188 -0
  7. package/dist/src/config/config.js +48 -0
  8. package/dist/src/config/logger.js +27 -0
  9. package/dist/src/config/morgan.js +16 -0
  10. package/dist/src/config/passport.cjs +28 -0
  11. package/dist/src/config/roles.js +11 -0
  12. package/dist/src/config/tokens.cjs +10 -0
  13. package/dist/src/devices/devices.controller.js +172 -0
  14. package/dist/src/devices/devices.model.js +94 -0
  15. package/dist/src/devices/devices.route.js +153 -0
  16. package/dist/src/devices/devices.schemas.js +84 -0
  17. package/dist/src/devices/devices.service.js +198 -0
  18. package/dist/src/devices/devices.types.js +1 -0
  19. package/dist/src/devices/devices.validation.js +257 -0
  20. package/dist/src/devicesNotifications/devicesNotifications.controller.js +69 -0
  21. package/dist/src/devicesNotifications/devicesNotifications.model.js +39 -0
  22. package/dist/src/devicesNotifications/devicesNotifications.route.js +124 -0
  23. package/dist/src/devicesNotifications/devicesNotifications.schemas.js +10 -0
  24. package/dist/src/devicesNotifications/devicesNotifications.service.js +181 -0
  25. package/dist/src/devicesNotifications/devicesNotifications.validation.js +46 -0
  26. package/dist/src/email/email.service.js +580 -0
  27. package/dist/src/files/upload.service.js +124 -0
  28. package/dist/src/i18n/i18n.js +38 -0
  29. package/dist/src/i18n/saveMissingLocalJsonBackend.js +53 -0
  30. package/dist/src/i18n/types.js +1 -0
  31. package/dist/src/index.js +48 -0
  32. package/dist/src/iotdevice/iotdevice.controller.js +96 -0
  33. package/dist/src/iotdevice/iotdevice.model.js +17 -0
  34. package/dist/src/iotdevice/iotdevice.route.js +143 -0
  35. package/dist/src/iotdevice/iotdevice.schemas.js +60 -0
  36. package/dist/src/iotdevice/iotdevice.service.js +579 -0
  37. package/dist/src/iotdevice/iotdevice.types.js +1 -0
  38. package/dist/src/iotdevice/iotdevice.validation.js +54 -0
  39. package/dist/src/middlewares/auth.js +75 -0
  40. package/dist/src/middlewares/checkJwt.cjs +17 -0
  41. package/dist/src/middlewares/error.js +36 -0
  42. package/dist/src/middlewares/mongooseValidations/ensureSameOrganization.js +13 -0
  43. package/dist/src/middlewares/rateLimiter.js +7 -0
  44. package/dist/src/middlewares/validate.js +18 -0
  45. package/dist/src/middlewares/validateAction.js +35 -0
  46. package/dist/src/middlewares/validateAdmin.js +18 -0
  47. package/dist/src/middlewares/validateAi.js +16 -0
  48. package/dist/src/middlewares/validateCurrentAuthUser.js +17 -0
  49. package/dist/src/middlewares/validateCurrentUser.js +20 -0
  50. package/dist/src/middlewares/validateDevice.js +98 -0
  51. package/dist/src/middlewares/validateDeviceUserOrganization.js +26 -0
  52. package/dist/src/middlewares/validateOrganization.js +63 -0
  53. package/dist/src/middlewares/validateQuerySearchUserAndOrganization.js +44 -0
  54. package/dist/src/middlewares/validateTokens.js +23 -0
  55. package/dist/src/middlewares/validateUser.js +38 -0
  56. package/dist/src/middlewares/validateZod.js +33 -0
  57. package/dist/src/models/plugins/index.js +4 -0
  58. package/dist/src/models/plugins/paginate.plugin.js +117 -0
  59. package/dist/src/models/plugins/paginateNew.plugin.js +185 -0
  60. package/dist/src/models/plugins/simplePopulate.js +16 -0
  61. package/dist/src/models/plugins/toJSON.plugin.js +35 -0
  62. package/dist/src/organizations/organizations.controller.js +64 -0
  63. package/dist/src/organizations/organizations.model.js +41 -0
  64. package/dist/src/organizations/organizations.route.js +98 -0
  65. package/dist/src/organizations/organizations.schemas.js +7 -0
  66. package/dist/src/organizations/organizations.service.js +59 -0
  67. package/dist/src/organizations/organizations.validation.js +62 -0
  68. package/dist/src/pdf/pdf.controller.js +24 -0
  69. package/dist/src/pdf/pdf.route.js +22 -0
  70. package/dist/src/pdf/pdf.schemas.js +6 -0
  71. package/dist/src/pdf/pdf.service.js +65 -0
  72. package/dist/src/pdf/pdf.validation.js +27 -0
  73. package/dist/src/tokens/tokens.controller.js +60 -0
  74. package/dist/src/tokens/tokens.model.js +18 -0
  75. package/dist/src/tokens/tokens.route.js +52 -0
  76. package/dist/src/tokens/tokens.schemas.js +14 -0
  77. package/dist/src/tokens/tokens.service.js +30 -0
  78. package/dist/src/tokens/tokens.validation.js +9 -0
  79. package/dist/src/types/routeSpec.js +1 -0
  80. package/dist/src/users/users.controller.js +147 -0
  81. package/dist/src/users/users.model.js +50 -0
  82. package/dist/src/users/users.route.js +137 -0
  83. package/dist/src/users/users.schemas.js +69 -0
  84. package/dist/src/users/users.service.js +295 -0
  85. package/dist/src/users/users.types.js +1 -0
  86. package/dist/src/users/users.validation.js +144 -0
  87. package/dist/src/utils/ApiError.js +16 -0
  88. package/dist/src/utils/buildRouterAndDocs.js +72 -0
  89. package/dist/src/utils/catchAsync.js +4 -0
  90. package/dist/src/utils/comparePapers.service.js +32 -0
  91. package/dist/src/utils/deviceUtils.js +63 -0
  92. package/dist/src/utils/filterOptions.js +24 -0
  93. package/dist/src/utils/medicationName.js +10 -0
  94. package/dist/src/utils/pick.js +16 -0
  95. package/dist/src/utils/registerOpenApi.js +28 -0
  96. package/dist/src/utils/urlUtils.js +15 -0
  97. package/dist/src/utils/userName.js +22 -0
  98. package/dist/src/utils/zValidations.js +124 -0
  99. package/dist/src/validations/auth.validation.cjs +53 -0
  100. package/dist/src/validations/custom.validation.js +19 -0
  101. package/dist/src/validations/index.cjs +3 -0
  102. package/package.json +15 -6
  103. package/scripts/release-and-sync-paperless.mjs +135 -0
  104. package/scripts/release-version.mjs +145 -0
  105. package/src/accounts/accounts.controller.ts +1 -0
  106. package/src/accounts/accounts.service.ts +1 -0
  107. package/src/accounts/accounts.validation.ts +6 -3
  108. package/src/accounts/auth0.service.ts +55 -28
  109. package/src/config/config.ts +6 -0
  110. package/src/config/logger.ts +15 -9
  111. package/src/devices/devices.controller.ts +7 -1
  112. package/src/devices/devices.model.ts +4 -1
  113. package/src/devices/devices.schemas.ts +10 -8
  114. package/src/devices/devices.service.ts +2 -1
  115. package/src/devices/devices.types.ts +1 -0
  116. package/src/devices/devices.validation.ts +85 -23
  117. package/src/devicesNotifications/devicesNotifications.controller.ts +57 -28
  118. package/src/devicesNotifications/devicesNotifications.model.ts +20 -12
  119. package/src/devicesNotifications/devicesNotifications.service.ts +35 -17
  120. package/src/files/upload.service.ts +52 -28
  121. package/src/i18n/i18n.ts +2 -2
  122. package/src/i18n/types.ts +1 -0
  123. package/src/index.ts +47 -0
  124. package/src/iotdevice/iotdevice.controller.ts +1 -0
  125. package/src/iotdevice/iotdevice.model.ts +6 -3
  126. package/src/iotdevice/iotdevice.route.ts +85 -76
  127. package/src/iotdevice/iotdevice.service.ts +5 -4
  128. package/src/iotdevice/iotdevice.types.ts +6 -0
  129. package/src/middlewares/auth.ts +2 -8
  130. package/src/middlewares/error.ts +26 -12
  131. package/src/middlewares/mongooseValidations/ensureSameOrganization.ts +4 -3
  132. package/src/middlewares/validateAi.ts +17 -9
  133. package/src/middlewares/validateDevice.ts +1 -0
  134. package/src/middlewares/validateDeviceUserOrganization.ts +1 -0
  135. package/src/middlewares/validateOrganization.ts +1 -1
  136. package/src/middlewares/validateQuerySearchUserAndOrganization.ts +1 -0
  137. package/src/middlewares/validateTokens.ts +2 -1
  138. package/src/middlewares/validateUser.ts +1 -0
  139. package/src/models/plugins/index.ts +5 -4
  140. package/src/models/plugins/paginate.plugin.ts +26 -16
  141. package/src/models/plugins/paginateNew.plugin.ts +33 -21
  142. package/src/models/plugins/simplePopulate.ts +8 -3
  143. package/src/models/plugins/toJSON.plugin.ts +12 -5
  144. package/src/organizations/organizations.controller.ts +1 -2
  145. package/src/organizations/organizations.model.ts +4 -4
  146. package/src/organizations/organizations.route.ts +1 -1
  147. package/src/organizations/organizations.service.ts +15 -6
  148. package/src/pdf/pdf.controller.ts +18 -1
  149. package/src/pdf/pdf.service.ts +25 -16
  150. package/src/tokens/tokens.controller.ts +6 -8
  151. package/src/tokens/tokens.model.ts +3 -1
  152. package/src/tokens/tokens.service.ts +3 -2
  153. package/src/types/express.d.ts +17 -0
  154. package/src/types/mongoose.d.ts +22 -0
  155. package/src/users/users.controller.ts +8 -9
  156. package/src/users/users.model.ts +6 -5
  157. package/src/users/users.route.ts +0 -1
  158. package/src/users/users.service.ts +16 -0
  159. package/src/users/users.types.ts +1 -0
  160. package/src/users/users.validation.ts +6 -2
  161. package/src/utils/ApiError.ts +8 -1
  162. package/src/utils/buildRouterAndDocs.ts +56 -21
  163. package/src/utils/catchAsync.ts +27 -3
  164. package/src/utils/deviceUtils.ts +109 -0
  165. package/src/utils/medicationName.ts +5 -4
  166. package/src/utils/pick.ts +5 -1
  167. package/src/utils/userName.ts +1 -0
  168. package/src/utils/zValidations.ts +78 -26
  169. package/tsconfig.json +13 -4
@@ -0,0 +1,69 @@
1
+ import { z } from 'zod';
2
+ import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
3
+ extendZodWithOpenApi(z);
4
+ export const createUserResponseSchema = z.object({
5
+ id: z.string(),
6
+ name: z.string(),
7
+ email: z.string(),
8
+ });
9
+ export const getUsersResponseSchema = z.array(z.object({
10
+ id: z.string(),
11
+ name: z.string(),
12
+ email: z.string(),
13
+ }));
14
+ export const updateUserSchema = z.object({
15
+ name: z.string().optional(),
16
+ email: z.string().email().optional(),
17
+ });
18
+ export const updateUserResponseSchema = z.object({
19
+ id: z.string(),
20
+ name: z.string(),
21
+ email: z.string(),
22
+ });
23
+ export const deleteUserResponseSchema = z.object({
24
+ success: z.boolean(),
25
+ });
26
+ export const updateTimesByIdResponseSchema = z
27
+ .array(z.object({
28
+ rrule: z
29
+ .object({
30
+ freq: z.string().optional().openapi({ example: 'DAILY', description: 'Recurrence frequency' }),
31
+ byweekday: z
32
+ .array(z.number())
33
+ .openapi({ example: [0, 1, 2, 6, 3], description: 'Days of week to repeat (0=Sunday)' }),
34
+ exclude: z.array(z.string()).openapi({ example: ['2024-03-28T10:45:00.000Z'], description: 'Dates to skip' }),
35
+ })
36
+ .openapi({ description: 'Recurrence rule object' }),
37
+ medication: z.string().optional().openapi({ example: '6152c5f3902e7f91374d9f75', description: 'Medication ObjectId' }),
38
+ patient: z.string().openapi({ example: '614fb1d709dd9f6de85d6374', description: 'Patient ObjectId' }),
39
+ date: z.string().openapi({ example: '2024-03-25T00:30:00.000Z', description: 'Scheduled date/time (ISO)' }),
40
+ timeCategory: z.string().openapi({ example: 'noon', description: 'Time category (e.g. morning, noon)' }),
41
+ amount: z.number().openapi({ example: 1, description: 'Dosage amount' }),
42
+ emptyStomach: z.boolean().openapi({ example: false, description: 'Whether to take on empty stomach' }),
43
+ instruction: z.string().optional().openapi({ example: '', description: 'Additional instructions' }),
44
+ unit: z.string().optional().openapi({ example: 'St', description: 'Dosage unit' }),
45
+ bake: z.boolean().openapi({ example: false, description: 'Baking flag (if applicable)' }),
46
+ id: z.string().openapi({ example: '660079fd11fdc2dd935e43af', description: 'Entry identifier' }),
47
+ }))
48
+ .openapi({
49
+ example: [
50
+ {
51
+ rrule: {
52
+ freq: 'DAILY',
53
+ byweekday: [0, 1, 2, 6, 3],
54
+ exclude: ['2024-03-28T10:45:00.000Z'],
55
+ },
56
+ patient: '614fb1d709dd9f6de85d6374',
57
+ date: '2024-03-25T00:30:00.000Z',
58
+ timeCategory: 'noon',
59
+ amount: 1,
60
+ emptyStomach: false,
61
+ instruction: '',
62
+ unit: 'St',
63
+ bake: false,
64
+ id: '660079fd11fdc2dd935e43af',
65
+ },
66
+ // ...other items
67
+ ],
68
+ description: 'Array of updated time entries by ID',
69
+ });
@@ -0,0 +1,295 @@
1
+ // @ts-nocheck
2
+ import httpStatus from "http-status";
3
+ import { User } from "./users.model.js";
4
+ import ApiError from "../utils/ApiError.js";
5
+ import auth0Service from "../accounts/auth0.service";
6
+ import organizationsService from "../organizations/organizations.service";
7
+ import { sendEmail } from "../email/email.service";
8
+ import i18n from "../i18n/i18n";
9
+ let updateTimesByIdHook = null;
10
+ export const setUpdateTimesByIdHook = (hook) => {
11
+ updateTimesByIdHook = hook ?? null;
12
+ };
13
+ /**
14
+ * Create a new user
15
+ */
16
+ export const createUser = async (userBody) => {
17
+ return User.create(userBody);
18
+ };
19
+ /**
20
+ * Create the “current” user (alias of createUser)
21
+ */
22
+ export const createCurrentUser = async (userBody) => {
23
+ return createUser(userBody);
24
+ };
25
+ /**
26
+ * Populate a single Auth0 user
27
+ */
28
+ const populateAuth0User = async (user) => {
29
+ if (!user)
30
+ return undefined;
31
+ const auth0users = await auth0Service.getUsersByIds([user.owner]);
32
+ return auth0users?.data?.find((u) => u.user_id === user.owner);
33
+ };
34
+ /**
35
+ * Populate many Auth0 users
36
+ */
37
+ const populateAuth0Users = async (data) => {
38
+ const owners = data.map((u) => u.owner);
39
+ const auth0users = await auth0Service.getUsersByIds(owners);
40
+ if (!auth0users)
41
+ return data;
42
+ return data.map((doc) => ({
43
+ ...doc.toJSON(),
44
+ auth0User: auth0users?.data?.find((u) => u.user_id === doc.owner),
45
+ }));
46
+ };
47
+ /**
48
+ * Query for users with pagination + Auth0 enrichment
49
+ */
50
+ export const queryUsers = async (filter, options) => {
51
+ const result = await User.paginate(filter, options);
52
+ result.results = await populateAuth0Users(result.results);
53
+ return result;
54
+ };
55
+ /**
56
+ * Get user by Mongo _id
57
+ */
58
+ export const getById = async (id) => {
59
+ return User.findById(id);
60
+ };
61
+ /**
62
+ * Get user by _id with Auth0 info
63
+ */
64
+ export const getByIdWithAuth0 = async (id) => {
65
+ const user = await getById(id);
66
+ if (!user)
67
+ return null;
68
+ const auth0User = await populateAuth0User(user);
69
+ const json = user.toJSON();
70
+ json.auth0User = auth0User;
71
+ return json;
72
+ };
73
+ /**
74
+ * Get all users in a given category (and optional organization)
75
+ */
76
+ export const getUsersByCategory = async (category, organization) => {
77
+ const filter = { category };
78
+ if (organization)
79
+ filter.organization = organization;
80
+ return User.find(filter);
81
+ };
82
+ /**
83
+ * Get all users for an organization
84
+ */
85
+ export const getUsersByOrganization = async (organization) => {
86
+ return User.find({ organization }).lean();
87
+ };
88
+ /**
89
+ * Get one user by organization + userId
90
+ */
91
+ export const getUsersByOrganizationAndId = async (organization, userId) => {
92
+ return User.findOne({ organization, _id: userId }).lean();
93
+ };
94
+ /**
95
+ * Get all users for a given owner
96
+ */
97
+ export const getUsersByOwner = async (owner) => {
98
+ return User.find({ owner });
99
+ };
100
+ /**
101
+ * Get single user by owner + organization, with Auth0 info
102
+ */
103
+ export const getUserByOwner = async (owner, organization) => {
104
+ const user = await User.findOne({ owner, organization });
105
+ if (!user)
106
+ return null;
107
+ const auth0User = await populateAuth0User(user);
108
+ const json = user.toJSON();
109
+ json.auth0User = auth0User;
110
+ return json;
111
+ };
112
+ /**
113
+ * Get user by email
114
+ */
115
+ export const getUserByEmail = async (email) => {
116
+ return User.findOne({ email });
117
+ };
118
+ /**
119
+ * Send an invite email
120
+ */
121
+ export const sendInviteEmail = async (params) => {
122
+ const { auth, user, inviteCode, email } = params;
123
+ const organization = await organizationsService.getOrganizationById(user.organization);
124
+ const auth0User = await auth0Service.getUserById(auth.sub);
125
+ const lng = auth0User.data?.app_metadata?.language;
126
+ const title = `${i18n.t("Invite to ", { lng })}${organization.kind === "private-wirewire" ? "paperlesspaper" : "ANABOX smart"}`;
127
+ const body = i18n.t("You have been invited to join the group. Click on the link to accept the invitation.", { lng });
128
+ await sendEmail({
129
+ title,
130
+ body,
131
+ url: `/${user.organization}/invite/${inviteCode}`,
132
+ actionButtonText: "Accept invite",
133
+ domain: organization.kind === "private-wirewire" ? "web" : "memo",
134
+ email,
135
+ });
136
+ };
137
+ /**
138
+ * Update a user by ID
139
+ */
140
+ export const updateUserById = async (userId, updateBody, auth) => {
141
+ const user = await getById(userId);
142
+ if (!user) {
143
+ throw new ApiError(httpStatus.NOT_FOUND, `User not found: ${userId}`);
144
+ }
145
+ if (user.status === "invited") {
146
+ await sendInviteEmail({
147
+ auth,
148
+ user,
149
+ inviteCode: user.inviteCode,
150
+ email: updateBody.email,
151
+ });
152
+ }
153
+ //TODO: restrict fields that can be updated, temporarily excluding role
154
+ const { role, ...updateBodyRest } = updateBody;
155
+ const meta = { ...user.meta, ...updateBodyRest.meta };
156
+ Object.assign(user, { ...updateBodyRest, meta });
157
+ await user.save();
158
+ return user;
159
+ };
160
+ /**
161
+ * Delete a user by ID
162
+ */
163
+ export const deleteUserById = async (userId) => {
164
+ const user = await getById(userId);
165
+ if (!user) {
166
+ throw new ApiError(httpStatus.NOT_FOUND, `User not found: ${userId}`);
167
+ }
168
+ if (user.role === "admin") {
169
+ const admins = await User.find({
170
+ organization: user.organization,
171
+ role: "admin",
172
+ });
173
+ if (admins.length < 2) {
174
+ throw new ApiError(httpStatus.BAD_REQUEST, "At least one admin is required");
175
+ }
176
+ }
177
+ await user.deleteOne();
178
+ return user;
179
+ };
180
+ /**
181
+ * (Misnamed) delete a user’s image by ID
182
+ */
183
+ export const userImageById = async (userId) => {
184
+ const user = await getById(userId);
185
+ if (!user) {
186
+ throw new ApiError(httpStatus.NOT_FOUND, `User not found: ${userId}`);
187
+ }
188
+ await user.deleteOne();
189
+ return user;
190
+ };
191
+ /**
192
+ * Invite a user to an organization
193
+ */
194
+ export const organizationInvite = async (body, oldUser, status = "invited") => {
195
+ const user = await getById(oldUser.id);
196
+ if (!user)
197
+ throw new ApiError(httpStatus.NOT_FOUND, "User not found");
198
+ if (user.organizations.some((o) => o.id.equals(body.organizationId))) {
199
+ throw new ApiError(httpStatus.BAD_REQUEST, "Invite already exists");
200
+ }
201
+ user.organizations.push({ id: body.organizationId, role: body.role, status });
202
+ await user.save();
203
+ return user;
204
+ };
205
+ /**
206
+ * Get an invite by code
207
+ */
208
+ export const getInvite = async (params) => {
209
+ return User.findOne({
210
+ inviteCode: params.inviteCode,
211
+ owner: { $exists: false },
212
+ }).populate("organizationData");
213
+ };
214
+ /**
215
+ * Accept or decline an invite
216
+ */
217
+ export const updateInvite = async (params) => {
218
+ const user = await User.findOne({
219
+ inviteCode: params.inviteCode,
220
+ owner: { $exists: false },
221
+ });
222
+ if (!user)
223
+ throw new ApiError(httpStatus.NOT_FOUND, "Invite not found");
224
+ user.status = params.status;
225
+ user.owner = params.owner;
226
+ user.inviteCode = null;
227
+ await user.save();
228
+ return user;
229
+ };
230
+ /**
231
+ * Update a user's organization membership (placeholder for legacy callers)
232
+ */
233
+ export const organizationUpdate = async (_body) => {
234
+ throw new ApiError(httpStatus.NOT_IMPLEMENTED, "organizationUpdate not implemented");
235
+ };
236
+ /**
237
+ * Remove a user from an organization
238
+ */
239
+ export const organizationRemove = async (body) => {
240
+ const user = await getById(body.userId);
241
+ if (!user)
242
+ throw new ApiError(httpStatus.NOT_FOUND, "User not found");
243
+ user.organizations = user.organizations.filter((o) => !o.id.equals(body.organizationId));
244
+ await user.save();
245
+ return user;
246
+ };
247
+ /**
248
+ * Fetch up to 100k users with organization populated
249
+ */
250
+ export const queryAllCalendars = async () => {
251
+ return User.find({}, null, { limit: 100000 }).populate("organizationData");
252
+ };
253
+ /**
254
+ * Delete many users by ID list
255
+ */
256
+ export const deleteMany = async (idList) => {
257
+ return User.deleteMany({ _id: { $in: idList } });
258
+ };
259
+ /**
260
+ * Update times for a user by ID (hooked from memo-api)
261
+ */
262
+ export const updateTimesById = async (userId, updateBody, dryRun = true) => {
263
+ if (!updateTimesByIdHook) {
264
+ throw new ApiError(httpStatus.NOT_IMPLEMENTED, "updateTimesById not configured");
265
+ }
266
+ return updateTimesByIdHook(userId, updateBody, dryRun);
267
+ };
268
+ export default {
269
+ createUser,
270
+ createCurrentUser,
271
+ getById,
272
+ getByIdWithAuth0,
273
+ getUsersByCategory,
274
+ getUsersByOrganization,
275
+ getUsersByOrganizationAndId,
276
+ getUsersByOwner,
277
+ getUserByEmail,
278
+ getUserByOwner,
279
+ sendInviteEmail,
280
+ updateUserById,
281
+ updateTimesById,
282
+ deleteUserById,
283
+ userImageById,
284
+ organizationInvite,
285
+ getInvite,
286
+ updateInvite,
287
+ organizationUpdate,
288
+ organizationRemove,
289
+ queryUsers,
290
+ queryAllCalendars,
291
+ deleteMany,
292
+ sendEmail,
293
+ populateAuth0User,
294
+ populateAuth0Users,
295
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,144 @@
1
+ // @ts-nocheck
2
+ import { z } from "zod";
3
+ import { extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi";
4
+ import { password } from "../validations/custom.validation.js";
5
+ import { zPagination, zGet, zObjectId, zObjectIdFor, zPatchBody, zUpdate, zDelete, } from "../utils/zValidations.js";
6
+ extendZodWithOpenApi(z);
7
+ export const createUserSchema = {
8
+ body: z.object({
9
+ meta: z
10
+ .record(z.any())
11
+ .optional()
12
+ .openapi({
13
+ example: { key: "value" },
14
+ description: "Additional metadata for the user",
15
+ }),
16
+ organization: zObjectId.openapi({
17
+ description: "Organization ObjectId",
18
+ }),
19
+ email: z.string().email().optional().nullable().openapi({
20
+ example: "user@example.com",
21
+ description: "User email address",
22
+ }),
23
+ timezone: z.string().optional().openapi({
24
+ example: "Europe/Berlin",
25
+ description: "IANA timezone string",
26
+ }),
27
+ role: z.enum(["user", "admin", "patient", "onlyself"]).optional().openapi({
28
+ description: "Role assigned to the user",
29
+ }),
30
+ category: z
31
+ .enum(["doctor", "nurse", "patient", "pharmacist", "relative"])
32
+ .optional()
33
+ .openapi({
34
+ description: "Category of the user",
35
+ }),
36
+ }),
37
+ };
38
+ export const createCurrentUserSchema = createUserSchema;
39
+ export const queryUsersSchema = {
40
+ ...zPagination,
41
+ query: zPagination.query.extend({
42
+ organization: zObjectIdFor("organization").openapi({
43
+ description: "Filter users by organization ObjectId",
44
+ example: process.env.SCHEMA_EXAMPLE_ORGANIZATION_ID ||
45
+ "60c72b2f9b1e8d001c8e4f3a",
46
+ }),
47
+ }),
48
+ };
49
+ export const getUserSchema = zGet("userId");
50
+ export const getCurrentUserSchema = {
51
+ query: z.object({
52
+ organization: zObjectId,
53
+ }),
54
+ };
55
+ export const updateUserSchema = {
56
+ ...zUpdate("userId"),
57
+ body: zPatchBody({
58
+ password: z
59
+ .string()
60
+ .refine((val) => {
61
+ try {
62
+ password(val);
63
+ return true;
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ }, { message: "Invalid password format" })
69
+ .optional()
70
+ .openapi({ description: "New user password" }),
71
+ name: z.string().optional().openapi({ description: "User full name" }),
72
+ timezone: z.string().optional().openapi({ description: "IANA timezone" }),
73
+ avatar: z.string().optional().openapi({ description: "Avatar URL" }),
74
+ meta: z
75
+ .record(z.any())
76
+ .optional()
77
+ .openapi({ description: "Additional metadata" }),
78
+ category: z
79
+ .enum(["doctor", "nurse", "patient", "pharmacist", "relative"])
80
+ .optional()
81
+ .openapi({ description: "User category" }),
82
+ email: z
83
+ .string()
84
+ .email()
85
+ .nullable()
86
+ .optional()
87
+ .openapi({ description: "User email address" }),
88
+ role: z
89
+ .enum(["user", "admin", "patient", "onlyself"])
90
+ .optional()
91
+ .openapi({ description: "User role" }),
92
+ inviteCode: z.string().optional().openapi({ description: "Invite code" }),
93
+ organization: zObjectId
94
+ .optional()
95
+ .openapi({ description: "Organization ObjectId" }),
96
+ }),
97
+ };
98
+ export const deleteUserSchema = zDelete("userId");
99
+ export const organizationInviteSchema = {
100
+ body: z.object({
101
+ organizationId: zObjectId.openapi({ description: "Organization ObjectId" }),
102
+ action: z.string().optional().openapi({ description: "Invite action" }),
103
+ role: z.string().optional().openapi({ description: "Role on invite" }),
104
+ }),
105
+ };
106
+ export const updateInviteSchema = {
107
+ body: z.object({
108
+ organization: zObjectId.openapi({ description: "Organization ObjectId" }),
109
+ status: z.enum(["accepted"]).openapi({ description: "Invite status" }),
110
+ inviteCode: z.string().optional().openapi({ description: "Invite code" }),
111
+ }),
112
+ };
113
+ export const organizationRemoveSchema = {
114
+ body: z.object({
115
+ userId: zObjectId.openapi({ description: "User ObjectId" }),
116
+ organizationId: zObjectId.openapi({ description: "Organization ObjectId" }),
117
+ }),
118
+ };
119
+ export const updateTimesByIdSchema = {
120
+ ...zUpdate("userId"),
121
+ body: z
122
+ .object({})
123
+ .catchall(z.string())
124
+ .openapi({
125
+ description: "Arbitrary key/value map of intake times",
126
+ example: {
127
+ "intake-morning": "10:00",
128
+ "intake-noon": "",
129
+ "intake-afternoon": "",
130
+ "intake-night": "15:00",
131
+ },
132
+ }),
133
+ };
134
+ export const updateTimesByIdSchemas = {
135
+ // if you need a path param, uncomment and adjust:
136
+ // ...zUpdate('timeId'),
137
+ ...updateTimesByIdSchema,
138
+ };
139
+ export const validateGetInviteSchema = {
140
+ params: z.object({
141
+ inviteCode: z.string().openapi({ description: "Invite code to validate" }),
142
+ }),
143
+ };
144
+ export const sendVerificationEmailValidationSchema = {};
@@ -0,0 +1,16 @@
1
+ // @ts-nocheck
2
+ export class ApiError extends Error {
3
+ constructor(statusCode, message, isOperational = true, stack = "", raw = null) {
4
+ super(message);
5
+ this.statusCode = statusCode;
6
+ this.isOperational = isOperational;
7
+ this.raw = raw;
8
+ if (stack) {
9
+ this.stack = stack;
10
+ }
11
+ else {
12
+ Error.captureStackTrace(this, this.constructor);
13
+ }
14
+ }
15
+ }
16
+ export default ApiError;
@@ -0,0 +1,72 @@
1
+ import { registry } from "../utils/registerOpenApi";
2
+ import { validateZod } from "../middlewares/validateZod";
3
+ import { bearerAuth, xApiKey } from "../utils/registerOpenApi";
4
+ import { extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi";
5
+ import { z } from "zod";
6
+ extendZodWithOpenApi(z);
7
+ const roleValidatorNames = ["validateAiRole", "validateAdmin"];
8
+ function hasRoleValidation(validators = []) {
9
+ return validators.some((fn) => roleValidatorNames.includes(fn.name));
10
+ }
11
+ export default function buildAiRouterAndDocs(router, routeSpecs, basePath = "/", tags = []) {
12
+ routeSpecs.forEach((spec) => {
13
+ // mount Express
14
+ if (!spec.validate) {
15
+ spec.validate = [];
16
+ }
17
+ if (spec.requestSchema) {
18
+ spec.validateWithRequestSchema = [
19
+ validateZod(spec.requestSchema),
20
+ ...spec.validate,
21
+ ];
22
+ }
23
+ if (spec.validateWithRequestSchema) {
24
+ router[spec.method](spec.path, ...spec.validateWithRequestSchema, spec.handler);
25
+ }
26
+ var { body, ...rest } = spec.requestSchema || {};
27
+ if (body) {
28
+ rest.body = {
29
+ content: {
30
+ "application/json": {
31
+ schema: body,
32
+ },
33
+ },
34
+ };
35
+ }
36
+ // console.log('spec.requestSchema', body);
37
+ if (spec.responseSchema &&
38
+ !hasRoleValidation(spec.validate) &&
39
+ spec.privateDocs !== true &&
40
+ spec.memoOnly !== true) {
41
+ // collect all middleware fn names (falls back to '<anonymous>' if unnamed)
42
+ const middlewareNames = (spec.validate || []).map((fn) => `\`${fn.name}\`` || "<anonymous>");
43
+ const openApiPath = (basePath + spec.path).replace(/:([A-Za-z0-9_]+)/g, "{$1}");
44
+ registry.registerPath({
45
+ method: spec.method,
46
+ path: openApiPath,
47
+ summary: spec.summary,
48
+ request: rest,
49
+ // append middleware names to the description
50
+ description: [
51
+ spec.description,
52
+ `\n\nMiddlewares: ${middlewareNames.join(", ")}`,
53
+ ]
54
+ .filter(Boolean)
55
+ .join("\n"),
56
+ // (optionally) expose them as a custom extension instead:
57
+ "x-middlewares": middlewareNames,
58
+ security: [{ [bearerAuth.name]: [] }, { [xApiKey.name]: [] }],
59
+ responses: {
60
+ 200: {
61
+ description: "Object with user data.",
62
+ content: {
63
+ "application/json": { schema: spec.responseSchema },
64
+ },
65
+ },
66
+ },
67
+ tags,
68
+ });
69
+ }
70
+ // else: streaming endpoint, we don’t register it in OpenAPI
71
+ });
72
+ }
@@ -0,0 +1,4 @@
1
+ const catchAsync = (fn) => (req, res, next) => {
2
+ Promise.resolve(fn(req, res, next)).catch((err) => next(err));
3
+ };
4
+ export default catchAsync;
@@ -0,0 +1,32 @@
1
+ import sharp from 'sharp';
2
+ import pixelmatch from 'pixelmatch';
3
+ /**
4
+ * Compares two image buffers and returns the similarity percentage
5
+ * @param {Buffer} imgBuffer1 - First image buffer
6
+ * @param {Buffer} imgBuffer2 - Second image buffer
7
+ * @returns {Promise<number>} - Percentage of similarity (0-100)
8
+ */
9
+ export async function compareImages(imgBuffer1, imgBuffer2) {
10
+ // Load the images using sharp and resize them to the same size
11
+ const image1 = await sharp(imgBuffer1).raw().ensureAlpha().toBuffer({ resolveWithObject: true });
12
+ const image2 = await sharp(imgBuffer2).raw().ensureAlpha().toBuffer({ resolveWithObject: true });
13
+ // Ensure both images are of the same dimensions
14
+ const width = Math.min(image1.info.width, image2.info.width);
15
+ const height = Math.min(image1.info.height, image2.info.height);
16
+ if (image1.info.width !== image2.info.width || image1.info.height !== image2.info.height) {
17
+ return 0;
18
+ }
19
+ // Resize the images to match in size
20
+ const resizedImage1 = await sharp(imgBuffer1).resize(width, height).raw().ensureAlpha().toBuffer();
21
+ const resizedImage2 = await sharp(imgBuffer2).resize(width, height).raw().ensureAlpha().toBuffer();
22
+ // Create an empty array to store pixel differences
23
+ const diff = new Uint8Array(width * height * 4);
24
+ // Compare the two images pixel by pixel
25
+ const numDiffPixels = pixelmatch(resizedImage1, resizedImage2, diff, width, height, { threshold: 0.3 } // Threshold for pixel comparison
26
+ );
27
+ // Calculate similarity percentage
28
+ const totalPixels = width * height;
29
+ const similarityPercentage = ((totalPixels - numDiffPixels) / totalPixels) * 100;
30
+ return similarityPercentage;
31
+ }
32
+ export default { compareImages };