@internetderdinge/api 1.229.31 → 1.229.37

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 (48) hide show
  1. package/dist/src/accounts/accounts.route.js +18 -5
  2. package/dist/src/admin/adminSearch.controller.js +0 -19
  3. package/dist/src/admin/adminSearch.route.js +5 -5
  4. package/dist/src/devices/devices.controller.js +1 -6
  5. package/dist/src/devices/devices.route.js +13 -13
  6. package/dist/src/devices/devices.validation.js +0 -47
  7. package/dist/src/email/email.service.js +6 -6
  8. package/dist/src/index.js +5 -1
  9. package/dist/src/iotdevice/iotdevice.route.js +3 -1
  10. package/dist/src/middlewares/validateAdminOrSupport.js +20 -0
  11. package/dist/src/organizations/organizations.controller.js +17 -6
  12. package/dist/src/organizations/organizations.route.js +2 -1
  13. package/dist/src/users/users.model.js +4 -2
  14. package/dist/src/users/users.route.js +2 -2
  15. package/dist/src/users/users.service.js +26 -10
  16. package/dist/src/users/users.validation.js +60 -61
  17. package/dist/src/utils/buildRouterAndDocs.js +7 -4
  18. package/package.json +5 -2
  19. package/scripts/release-and-sync-paperless.mjs +60 -28
  20. package/scripts/release-version.mjs +21 -2
  21. package/src/accounts/accounts.route.ts +21 -5
  22. package/src/admin/adminSearch.controller.ts +0 -35
  23. package/src/admin/adminSearch.route.ts +8 -6
  24. package/src/admin/adminSearch.service.ts +6 -10
  25. package/src/devices/devices.controller.ts +0 -12
  26. package/src/devices/devices.route.ts +13 -14
  27. package/src/devices/devices.validation.ts +0 -47
  28. package/src/email/email.service.ts +15 -7
  29. package/src/index.ts +5 -1
  30. package/src/iotdevice/iotdevice.route.ts +3 -1
  31. package/src/middlewares/validateAdminOrSupport.ts +34 -0
  32. package/src/organizations/organizations.controller.ts +38 -7
  33. package/src/organizations/organizations.route.ts +3 -1
  34. package/src/users/users.model.ts +7 -3
  35. package/src/users/users.route.ts +3 -2
  36. package/src/users/users.service.ts +50 -14
  37. package/src/users/users.validation.ts +62 -60
  38. package/src/utils/buildRouterAndDocs.ts +14 -5
  39. package/dist/src/pdf/pdf.controller.js +0 -24
  40. package/dist/src/pdf/pdf.route.js +0 -22
  41. package/dist/src/pdf/pdf.schemas.js +0 -6
  42. package/dist/src/pdf/pdf.service.js +0 -64
  43. package/dist/src/pdf/pdf.validation.js +0 -27
  44. package/src/pdf/pdf.controller.ts +0 -35
  45. package/src/pdf/pdf.route.ts +0 -28
  46. package/src/pdf/pdf.schemas.ts +0 -7
  47. package/src/pdf/pdf.service.ts +0 -103
  48. package/src/pdf/pdf.validation.ts +0 -30
@@ -112,53 +112,6 @@ export const createDeviceSchema = {
112
112
  };
113
113
  export const deleteDeviceSchema = zDelete("deviceId");
114
114
  export const getDeviceSchema = zGet("deviceId");
115
- export const ledLightSchema = {
116
- params: z.object({
117
- deviceId: zObjectId.openapi({ description: "Device ObjectId" }),
118
- }),
119
- body: z
120
- .object({
121
- led: z.array(z.tuple([z.number().int(), z.number().int()])),
122
- timeout: z.number().int(),
123
- })
124
- .openapi({
125
- description: "LED light configuration for device",
126
- example: {
127
- led: [
128
- [0, 0],
129
- [1, 0],
130
- [2, 0],
131
- [3, 0],
132
- [4, 0],
133
- [5, 0],
134
- [6, 0],
135
- [7, 0],
136
- [8, 0],
137
- [9, 0],
138
- [10, 0],
139
- [11, 0],
140
- [12, 0],
141
- [13, 0],
142
- [14, 0],
143
- [15, 100],
144
- [16, 100],
145
- [17, 100],
146
- [18, 100],
147
- [19, 100],
148
- [20, 100],
149
- [21, 100],
150
- [22, 0],
151
- [23, 0],
152
- [24, 0],
153
- [25, 0],
154
- [26, 0],
155
- [27, 0],
156
- [28, 0],
157
- ],
158
- timeout: 40,
159
- },
160
- }),
161
- };
162
115
  export const pingDeviceSchema = {
163
116
  params: z.object({
164
117
  deviceId: zObjectIdFor("deviceId").openapi({
@@ -54,11 +54,15 @@ const actionButton = ({
54
54
  </table>`;
55
55
  };
56
56
 
57
- interface SendEmailParams {
57
+ export interface SendEmailParams {
58
58
  title?: string;
59
59
  body?: string;
60
60
  url?: string;
61
61
  domain?: string;
62
+ appBaseUrl?: string;
63
+ productName?: string;
64
+ companyName?: string;
65
+ accountUrl?: string;
62
66
  image?: string;
63
67
  email: string;
64
68
  actionButtonText?: string;
@@ -69,7 +73,11 @@ export const sendEmail = async ({
69
73
  title = "Kein Titel",
70
74
  body = "Kein Inhalt",
71
75
  url = "",
72
- domain = "memo",
76
+ domain = "web",
77
+ appBaseUrl = `https://${domain}.wirewire.de`,
78
+ productName = domain,
79
+ companyName = "The Wire UG",
80
+ accountUrl = `${appBaseUrl}/account`,
73
81
  image,
74
82
  email,
75
83
  actionButtonText,
@@ -560,8 +568,8 @@ export const sendEmail = async ({
560
568
  <table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
561
569
  <tr>
562
570
  <td class="email-masthead">
563
- <a href="https://${domain}.wirewire.de" class="f-fallback email-masthead_name">
564
- ${domain === "memo" ? "ANABOX smart" : "paperlesspaper"}
571
+ <a href="${appBaseUrl}" class="f-fallback email-masthead_name">
572
+ ${productName}
565
573
  </a>
566
574
  </td>
567
575
  </tr>
@@ -580,7 +588,7 @@ export const sendEmail = async ({
580
588
  ${actionButton({
581
589
  link: urlStartsWithHttp(url)
582
590
  ? url
583
- : `http://${domain}.wirewire.de${url}`,
591
+ : `${appBaseUrl}${url}`,
584
592
  text: actionButtonTextWithLanguage,
585
593
  })}
586
594
  </div>
@@ -595,8 +603,8 @@ export const sendEmail = async ({
595
603
  <tr>
596
604
  <td class="content-cell" align="center">
597
605
  <p class="f-fallback sub align-center">
598
- ${domain === "web" ? "The Wire UG" : "wirewire GmbH"}
599
- <a href="http://${domain}.wirewire.de/account">Account</a>
606
+ ${companyName}
607
+ <a href="${accountUrl}">Account</a>
600
608
 
601
609
  ${config.env !== "production" ? `<br/><br/>Environment: ${config.env}` : ""}
602
610
  </p>
package/src/index.ts CHANGED
@@ -13,12 +13,16 @@ export {
13
13
  export { initI18n } from "../src/i18n/i18n";
14
14
  export { default as i18n } from "../src/i18n/i18n";
15
15
  export { default as usersRoute } from "../src/users/users.route";
16
+ export * from "../src/users/users.route";
16
17
  export { default as usersService } from "../src/users/users.service";
18
+ export * from "../src/users/users.service";
19
+ export * from "../src/users/users.validation";
17
20
  export { default as accountsRoute } from "../src/accounts/accounts.route";
18
21
  export { default as adminSearchRoute } from "../src/admin/adminSearch.route";
19
22
  export { default as accountsService } from "../src/accounts/accounts.service";
20
23
  export { auth0 } from "../src/accounts/auth0.service";
21
24
  export { default as organizationsRoute } from "../src/organizations/organizations.route";
25
+ export * from "../src/organizations/organizations.controller";
22
26
  export { default as organizationsService } from "../src/organizations/organizations.service";
23
27
  export { default as Organization } from "../src/organizations/organizations.model";
24
28
  export { default as devicesRoute } from "../src/devices/devices.route";
@@ -37,11 +41,11 @@ export {
37
41
  shadowAlarmGet,
38
42
  shadowAlarmUpdate,
39
43
  } from "../src/iotdevice/iotdevice.service";
40
- export { default as pdfRoute } from "../src/pdf/pdf.route";
41
44
  export { default as tokensRoute } from "../src/tokens/tokens.route";
42
45
  export * from "../src/tokens/tokens.service";
43
46
  export { default as Token } from "../src/tokens/tokens.model";
44
47
  export { User } from "../src/users/users.model";
48
+ export * from "../src/users/users.model";
45
49
  export { isAdmin, validateAdmin } from "../src/middlewares/validateAdmin";
46
50
  export { sendEmail } from "../src/email/email.service";
47
51
  export { default as catchAsync } from "../src/utils/catchAsync";
@@ -62,6 +62,7 @@ export const iotdeviceRouteSpecs: RouteSpec[] = [
62
62
  requestSchema: getEventsSchema,
63
63
  responseSchema: eventResponseSchema.array(),
64
64
  handler: getEvents,
65
+ privateDocs: true,
65
66
  summary: "Get events for a device",
66
67
  description: "Fetches event records for the specified device by its ID.",
67
68
  },
@@ -147,9 +148,10 @@ export const iotdeviceRouteSpecs: RouteSpec[] = [
147
148
  requestSchema: apiStatusRequestSchema,
148
149
  responseSchema: apiStatusSchema,
149
150
  handler: getApiStatus,
151
+ privateDocs: true,
150
152
  summary: "Get API status by kind",
151
153
  description:
152
- "Retrieves the IoT API status information for a given status kind to monitor system health or performance. Can be accessed without authentication for monitoring purposes.",
154
+ "Retrieves the IoT API status information for a given status kind to monitor system health or performance. Can be accessed without authentication for uptime monitoring purposes.",
153
155
  },
154
156
  {
155
157
  method: "get",
@@ -0,0 +1,34 @@
1
+ import httpStatus from "http-status";
2
+ import ApiError from "../utils/ApiError";
3
+ import type { Request, Response, NextFunction } from "express";
4
+
5
+ const ROLES_CLAIM = "https://memo.wirewire.de/roles";
6
+
7
+ const isAdminOrSupport = (user: Record<string, any> | undefined): boolean => {
8
+ if (!user) return false;
9
+
10
+ const roles = user[ROLES_CLAIM];
11
+
12
+ return Array.isArray(roles)
13
+ ? roles.includes("admin") || roles.includes("support")
14
+ : false;
15
+ };
16
+
17
+ const validateAdminOrSupport = async (
18
+ req: Request,
19
+ res: Response,
20
+ next: NextFunction,
21
+ ): Promise<void> => {
22
+ if (isAdminOrSupport(req.auth)) {
23
+ next();
24
+ } else {
25
+ next(
26
+ new ApiError(
27
+ httpStatus.FORBIDDEN,
28
+ "User is not part of the admin or support group (validateAdminOrSupport)",
29
+ ),
30
+ );
31
+ }
32
+ };
33
+
34
+ export { isAdminOrSupport, validateAdminOrSupport };
@@ -12,18 +12,49 @@ import { filterOptions } from "../utils/filterOptions.js";
12
12
 
13
13
  const ObjectId = mongoose.Types.ObjectId;
14
14
 
15
+ export type CreateOrganizationOwnerUserParams = {
16
+ organization: any;
17
+ owner: string;
18
+ request: Request;
19
+ };
20
+
21
+ export type CreateOrganizationOwnerUserHook = (
22
+ params: CreateOrganizationOwnerUserParams,
23
+ ) => Record<string, any>;
24
+
25
+ let createOrganizationOwnerUserHook: CreateOrganizationOwnerUserHook | null =
26
+ null;
27
+
28
+ export const setCreateOrganizationOwnerUserHook = (
29
+ hook?: CreateOrganizationOwnerUserHook,
30
+ ): void => {
31
+ createOrganizationOwnerUserHook = hook ?? null;
32
+ };
33
+
34
+ const createOrganizationOwnerUserBody = (
35
+ params: CreateOrganizationOwnerUserParams,
36
+ ) => ({
37
+ organization: params.organization._id,
38
+ owner: params.owner,
39
+ role: "admin",
40
+ status: "accept",
41
+ ...(createOrganizationOwnerUserHook
42
+ ? createOrganizationOwnerUserHook(params)
43
+ : {}),
44
+ });
45
+
15
46
  export const createOrganization = catchAsync(
16
47
  async (req: Request, res: Response): Promise<void> => {
17
48
  const organization = await organizationsService.createOrganization(
18
49
  req.body,
19
50
  );
20
- const user = await usersService.createUser({
21
- organization: organization._id,
22
- owner: res.req.auth.sub,
23
- role: "admin",
24
- category: "relative",
25
- status: "accept",
26
- });
51
+ const user = await usersService.createUser(
52
+ createOrganizationOwnerUserBody({
53
+ organization,
54
+ owner: res.req.auth.sub,
55
+ request: req,
56
+ }),
57
+ );
27
58
  res.status(httpStatus.CREATED).send(organization);
28
59
  },
29
60
  );
@@ -92,7 +92,9 @@ export const organizationsRouteSpecs: RouteSpec[] = [
92
92
  responseSchema: organizationResponseSchema,
93
93
  handler: updateOrganization,
94
94
  summary: "Update an organization by ID",
95
- description: "Updates the details of a specific organization by its ID.",
95
+ privateDocs: true,
96
+ description:
97
+ "LEGACY: Updates the details of a specific organization by its ID.",
96
98
  },
97
99
  {
98
100
  method: "delete",
@@ -14,7 +14,6 @@ export interface IUser {
14
14
  inviteCode?: string;
15
15
  email?: string;
16
16
  role: (typeof roles)[number];
17
- category: string;
18
17
  status?: string;
19
18
  meta?: Record<string, any>;
20
19
  }
@@ -27,7 +26,7 @@ export interface IUserModel extends Model<IUserDocument> {
27
26
  isEmailTaken(email: string, excludeUserId?: Types.ObjectId): Promise<boolean>;
28
27
  }
29
28
 
30
- const userSchema = new Schema<IUserDocument, IUserModel>(
29
+ export const userSchema = new Schema<IUserDocument, IUserModel>(
31
30
  {
32
31
  name: { type: String, trim: true },
33
32
  avatar: { type: String },
@@ -37,7 +36,6 @@ const userSchema = new Schema<IUserDocument, IUserModel>(
37
36
  inviteCode: { type: String },
38
37
  email: { type: String },
39
38
  role: { type: String, default: "patient" /* , enum: Object.keys(roles) */ },
40
- category: { type: String, default: "patient" },
41
39
  status: { type: String },
42
40
  meta: { type: Schema.Types.Mixed },
43
41
  },
@@ -59,6 +57,12 @@ userSchema.virtual("organizationData", {
59
57
  userSchema.plugin(toJSON);
60
58
  userSchema.plugin(paginate);
61
59
 
60
+ export const extendUserSchema = (
61
+ definition: Parameters<typeof userSchema.add>[0],
62
+ ): void => {
63
+ userSchema.add(definition);
64
+ };
65
+
62
66
  /**
63
67
  * Check if email is taken
64
68
  * @param {string} email - The user's email
@@ -45,7 +45,6 @@ export const userRouteSpecs: RouteSpec[] = [
45
45
  handler: userController.createUser,
46
46
  summary: "Create a new user",
47
47
  description: "Creates a new user within the current organization.",
48
- memoOnly: true,
49
48
  },
50
49
  {
51
50
  method: "get",
@@ -137,7 +136,9 @@ export const userRouteSpecs: RouteSpec[] = [
137
136
  responseSchema: updateUserResponseSchema,
138
137
  handler: userController.updateUser,
139
138
  summary: "Update a user by ID",
140
- description: "Replaces a user’s full record with the provided data.",
139
+ privateDocs: true,
140
+ description:
141
+ "LEGACY: Replaces a user’s full record with the provided data. (Replaced by PATCH /:userId)",
141
142
  },
142
143
  {
143
144
  method: "patch",
@@ -7,6 +7,7 @@ import ApiError from "../utils/ApiError.js";
7
7
  import auth0Service from "../accounts/auth0.service";
8
8
  import organizationsService from "../organizations/organizations.service";
9
9
  import { sendEmail } from "../email/email.service";
10
+ import type { SendEmailParams } from "../email/email.service";
10
11
  import i18n from "../i18n/i18n";
11
12
 
12
13
  export type UpdateTimesByIdHook = (
@@ -21,6 +22,27 @@ export const setUpdateTimesByIdHook = (hook?: UpdateTimesByIdHook): void => {
21
22
  updateTimesByIdHook = hook ?? null;
22
23
  };
23
24
 
25
+ export type BuildInviteEmailParams = {
26
+ auth: { sub: string };
27
+ user: IUserDocument;
28
+ inviteCode: string;
29
+ email: string;
30
+ organization: any;
31
+ lng: string;
32
+ };
33
+
34
+ export type BuildInviteEmailHook = (
35
+ params: BuildInviteEmailParams,
36
+ ) => Partial<SendEmailParams>;
37
+
38
+ let buildInviteEmailHook: BuildInviteEmailHook | null = null;
39
+
40
+ export const setBuildInviteEmailHook = (
41
+ hook?: BuildInviteEmailHook,
42
+ ): void => {
43
+ buildInviteEmailHook = hook ?? null;
44
+ };
45
+
24
46
  /**
25
47
  * Create a new user
26
48
  */
@@ -100,14 +122,13 @@ export const getByIdWithAuth0 = async (id: string): Promise<any | null> => {
100
122
  return json;
101
123
  };
102
124
 
103
- /**
104
- * Get all users in a given category (and optional organization)
105
- */
106
- export const getUsersByCategory = async (
107
- category: string,
125
+ export const getUsersByAppField = async (
126
+ appName: string,
127
+ fieldName: string,
128
+ value: unknown,
108
129
  organization?: string,
109
130
  ): Promise<IUserDocument[]> => {
110
- const filter: any = { category };
131
+ const filter: any = { [`apps.${appName}.${fieldName}`]: value };
111
132
  if (organization) filter.organization = organization;
112
133
  return User.find(filter);
113
134
  };
@@ -182,21 +203,35 @@ export const sendInviteEmail = async (params: {
182
203
  const auth0User = await auth0Service.getUserById(auth.sub);
183
204
  const lng = auth0User.data?.app_metadata?.language as string | "en";
184
205
 
185
- const title = `${i18n.t("Invite to ", { lng })}${
186
- organization.kind === "private-wirewire" ? "paperlesspaper" : "ANABOX smart"
187
- }`;
188
206
  const body = i18n.t(
189
207
  "You have been invited to join the group. Click on the link to accept the invitation.",
190
208
  { lng },
191
209
  );
192
-
193
- await sendEmail({
194
- title,
210
+ const baseEmail: SendEmailParams = {
211
+ title: `${i18n.t("Invite to ", { lng })}${
212
+ organization?.name || "Application"
213
+ }`,
195
214
  body,
196
215
  url: `/${user.organization}/invite/${inviteCode}`,
197
216
  actionButtonText: "Accept invite",
198
- domain: organization.kind === "private-wirewire" ? "web" : "memo",
217
+ domain: "web",
218
+ productName: organization?.name || "Application",
199
219
  email,
220
+ lng,
221
+ };
222
+
223
+ await sendEmail({
224
+ ...baseEmail,
225
+ ...(buildInviteEmailHook
226
+ ? buildInviteEmailHook({
227
+ auth,
228
+ user,
229
+ inviteCode,
230
+ email,
231
+ organization,
232
+ lng,
233
+ })
234
+ : {}),
200
235
  });
201
236
  };
202
237
 
@@ -384,7 +419,7 @@ export default {
384
419
  createCurrentUser,
385
420
  getById,
386
421
  getByIdWithAuth0,
387
- getUsersByCategory,
422
+ getUsersByAppField,
388
423
  getUsersByOrganization,
389
424
  getUsersByOrganizationAndId,
390
425
  getUsersByOwner,
@@ -407,6 +442,7 @@ export default {
407
442
  populateAuth0User,
408
443
  populateAuth0Users,
409
444
  setUpdateTimesByIdHook,
445
+ setBuildInviteEmailHook,
410
446
  };
411
447
 
412
448
  export type UserService = typeof import("./users.service");
@@ -14,37 +14,39 @@ import {
14
14
 
15
15
  extendZodWithOpenApi(z);
16
16
 
17
- export const createUserSchema = {
18
- body: z.object({
19
- meta: z
20
- .object({})
21
- .passthrough()
22
- .optional()
23
- .openapi({
24
- example: { key: "value" },
25
- description: "Additional metadata for the user",
26
- }),
27
- organization: zObjectId.openapi({
28
- description: "Organization ObjectId",
29
- }),
30
- email: z.string().email().optional().nullable().openapi({
31
- example: "user@example.com",
32
- description: "User email address",
33
- }),
34
- timezone: z.string().optional().openapi({
35
- example: "Europe/Berlin",
36
- description: "IANA timezone string",
37
- }),
38
- role: z.enum(["user", "admin", "patient", "onlyself"]).optional().openapi({
39
- description: "Role assigned to the user",
17
+ export const userAppsSchema = z
18
+ .record(z.string(), z.unknown())
19
+ .optional()
20
+ .openapi({ description: "Application-specific user fields" });
21
+
22
+ export const createUserBodyShape = {
23
+ meta: z
24
+ .object({})
25
+ .passthrough()
26
+ .optional()
27
+ .openapi({
28
+ example: { key: "value" },
29
+ description: "Additional metadata for the user",
40
30
  }),
41
- category: z
42
- .enum(["doctor", "nurse", "patient", "pharmacist", "relative"])
43
- .optional()
44
- .openapi({
45
- description: "Category of the user",
46
- }),
31
+ apps: userAppsSchema,
32
+ organization: zObjectId.openapi({
33
+ description: "Organization ObjectId",
34
+ }),
35
+ email: z.string().email().optional().nullable().openapi({
36
+ example: "user@example.com",
37
+ description: "User email address",
38
+ }),
39
+ timezone: z.string().optional().openapi({
40
+ example: "Europe/Berlin",
41
+ description: "IANA timezone string",
47
42
  }),
43
+ role: z.enum(["user", "admin", "patient", "onlyself"]).optional().openapi({
44
+ description: "Role assigned to the user",
45
+ }),
46
+ };
47
+
48
+ export const createUserSchema = {
49
+ body: z.object(createUserBodyShape),
48
50
  };
49
51
 
50
52
  export const createCurrentUserSchema = createUserSchema;
@@ -69,39 +71,39 @@ export const getCurrentUserSchema = {
69
71
  }),
70
72
  };
71
73
 
74
+ export const updateUserBodyShape = {
75
+ name: z.string().optional().openapi({ description: "User full name" }),
76
+ timezone: z.string().optional().openapi({ description: "IANA timezone" }),
77
+ avatar: z.string().optional().openapi({ description: "Avatar URL" }),
78
+ meta: z
79
+ .object({})
80
+ .passthrough()
81
+ .optional()
82
+ .openapi({ description: "Additional metadata" }),
83
+ apps: userAppsSchema,
84
+ email: z
85
+ .string()
86
+ .email()
87
+ .nullable()
88
+ .optional()
89
+ .openapi({ description: "User email address" }),
90
+ role: z
91
+ .enum(["user", "admin", "patient", "onlyself"])
92
+ .optional()
93
+ .openapi({ description: "User role" }),
94
+ inviteCode: z
95
+ .string()
96
+ .nullable()
97
+ .optional()
98
+ .openapi({ description: "Invite code" }),
99
+ organization: zObjectId
100
+ .optional()
101
+ .openapi({ description: "Organization ObjectId" }),
102
+ };
103
+
72
104
  export const updateUserSchema = {
73
105
  ...zUpdate("userId"),
74
- body: zPatchBody({
75
- name: z.string().optional().openapi({ description: "User full name" }),
76
- timezone: z.string().optional().openapi({ description: "IANA timezone" }),
77
- avatar: z.string().optional().openapi({ description: "Avatar URL" }),
78
- meta: z
79
- .object({})
80
- .optional()
81
- .openapi({ description: "Additional metadata" }),
82
- category: z
83
- .enum(["doctor", "nurse", "patient", "pharmacist", "relative"])
84
- .optional()
85
- .openapi({ description: "User category" }),
86
- email: z
87
- .string()
88
- .email()
89
- .nullable()
90
- .optional()
91
- .openapi({ description: "User email address" }),
92
- role: z
93
- .enum(["user", "admin", "patient", "onlyself"])
94
- .optional()
95
- .openapi({ description: "User role" }),
96
- inviteCode: z
97
- .string()
98
- .nullable()
99
- .optional()
100
- .openapi({ description: "Invite code" }),
101
- organization: zObjectId
102
- .optional()
103
- .openapi({ description: "Organization ObjectId" }),
104
- }),
106
+ body: zPatchBody(updateUserBodyShape),
105
107
  };
106
108
 
107
109
  export const deleteUserSchema = zDelete("userId");
@@ -30,16 +30,25 @@ export type RouteSpec = {
30
30
  summary: string;
31
31
  description?: string;
32
32
  privateDocs?: boolean;
33
- memoOnly?: boolean;
33
+ };
34
+
35
+ export type BuildRouterAndDocsOptions = {
36
+ routeSpecs?: (specs: RouteSpec[]) => RouteSpec[];
37
+ includeInDocs?: (spec: RouteSpec) => boolean;
34
38
  };
35
39
 
36
40
  export default function buildAiRouterAndDocs(
37
41
  router: Router,
38
- routeSpecs: any,
42
+ routeSpecs: RouteSpec[],
39
43
  basePath = "/",
40
44
  tags: string[] = [],
45
+ options: BuildRouterAndDocsOptions = {},
41
46
  ) {
42
- routeSpecs.forEach((spec) => {
47
+ const effectiveRouteSpecs = options.routeSpecs
48
+ ? options.routeSpecs(routeSpecs)
49
+ : routeSpecs;
50
+
51
+ effectiveRouteSpecs.forEach((spec) => {
43
52
  const validate = spec.validate || [];
44
53
  const routeMiddleware =
45
54
  spec.validateWithRequestSchema ||
@@ -68,7 +77,7 @@ export default function buildAiRouterAndDocs(
68
77
  spec.responseSchema &&
69
78
  !hasRoleValidation(spec.validateWithRequestSchema || validate) &&
70
79
  spec.privateDocs !== true &&
71
- spec.memoOnly !== true
80
+ (options.includeInDocs ? options.includeInDocs(spec) : true)
72
81
  ) {
73
82
  // collect all middleware fn names (falls back to '<anonymous>' if unnamed)
74
83
  const middlewareNames = (spec.validateWithRequestSchema || validate).map(
@@ -96,7 +105,7 @@ export default function buildAiRouterAndDocs(
96
105
  // (optionally) expose them as a custom extension instead:
97
106
  "x-middlewares": middlewareNames,
98
107
 
99
- security: [{ [bearerAuth.name]: [] }, { [xApiKey.name]: [] }],
108
+ security: [{ [xApiKey.name]: [] }, { [bearerAuth.name]: [] }],
100
109
  responses: {
101
110
  200: {
102
111
  description: "Object with user data.",
@@ -1,24 +0,0 @@
1
- import httpStatus from "http-status";
2
- import catchAsync from "../utils/catchAsync.js";
3
- import pdfService from "./pdf.service.js";
4
- import ApiError from "../utils/ApiError.js";
5
- export const generatePdfFromUrl = catchAsync(async (req, res) => {
6
- const fileName = "memo-print";
7
- const authHeader = req.headers["authorization"];
8
- if (!authHeader) {
9
- throw new ApiError(httpStatus.UNAUTHORIZED, "Missing Authorization header");
10
- }
11
- const token = authHeader.split(" ")[1];
12
- if (!token) {
13
- throw new ApiError(httpStatus.UNAUTHORIZED, "Missing bearer token");
14
- }
15
- const urlPath = typeof req.query.urlPath === "string" ? req.query.urlPath : undefined;
16
- if (!urlPath) {
17
- throw new ApiError(httpStatus.BAD_REQUEST, "Missing urlPath query parameter");
18
- }
19
- const result = await pdfService.generatePdfFromUrl({ urlPath, token });
20
- res.status(httpStatus.CREATED).send({ signed: result });
21
- });
22
- export default {
23
- generatePdfFromUrl,
24
- };
@@ -1,22 +0,0 @@
1
- import { Router } from "express";
2
- import buildRouterAndDocs from "../utils/buildRouterAndDocs.js";
3
- import { generatePdfSchema } from "./pdf.validation.js";
4
- import { pdfResponseSchema } from "./pdf.schemas.js";
5
- import { generatePdfFromUrl } from "./pdf.controller.js";
6
- import auth from "../middlewares/auth.js";
7
- export const pdfRouteSpecs = [
8
- {
9
- method: "get",
10
- path: "/",
11
- validate: [auth("manageUsers")],
12
- requestSchema: generatePdfSchema,
13
- responseSchema: pdfResponseSchema,
14
- handler: generatePdfFromUrl,
15
- summary: "Generate a PDF from a provided URL",
16
- description: "This endpoint allows users to generate a PDF document from a specified URL.",
17
- memoOnly: true,
18
- },
19
- ];
20
- const router = Router();
21
- buildRouterAndDocs(router, pdfRouteSpecs, "/pdf", ["PDF"]);
22
- export default router;