@internetderdinge/api 1.229.32 → 1.229.39

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 (54) 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 +4 -6
  5. package/dist/src/devices/devices.route.js +14 -14
  6. package/dist/src/devices/devices.validation.js +15 -54
  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/iotdevice/iotdevice.service.js +5 -5
  11. package/dist/src/iotdevice/iotdevice.validation.js +12 -4
  12. package/dist/src/middlewares/validateAdminOrSupport.js +20 -0
  13. package/dist/src/organizations/organizations.controller.js +17 -6
  14. package/dist/src/organizations/organizations.route.js +2 -1
  15. package/dist/src/users/users.model.js +4 -2
  16. package/dist/src/users/users.route.js +2 -2
  17. package/dist/src/users/users.service.js +26 -10
  18. package/dist/src/users/users.validation.js +60 -61
  19. package/dist/src/utils/buildRouterAndDocs.js +7 -4
  20. package/dist/src/utils/zValidations.js +7 -0
  21. package/package.json +5 -2
  22. package/scripts/release-and-sync-paperless.mjs +21 -2
  23. package/scripts/release-version.mjs +21 -2
  24. package/src/accounts/accounts.route.ts +21 -5
  25. package/src/admin/adminSearch.controller.ts +0 -35
  26. package/src/admin/adminSearch.route.ts +8 -6
  27. package/src/admin/adminSearch.service.ts +6 -10
  28. package/src/devices/devices.controller.ts +4 -12
  29. package/src/devices/devices.route.ts +14 -15
  30. package/src/devices/devices.validation.ts +20 -54
  31. package/src/email/email.service.ts +15 -7
  32. package/src/index.ts +5 -1
  33. package/src/iotdevice/iotdevice.route.ts +3 -1
  34. package/src/iotdevice/iotdevice.service.ts +8 -7
  35. package/src/iotdevice/iotdevice.validation.ts +12 -4
  36. package/src/middlewares/validateAdminOrSupport.ts +34 -0
  37. package/src/organizations/organizations.controller.ts +38 -7
  38. package/src/organizations/organizations.route.ts +3 -1
  39. package/src/users/users.model.ts +7 -3
  40. package/src/users/users.route.ts +3 -2
  41. package/src/users/users.service.ts +50 -14
  42. package/src/users/users.validation.ts +62 -60
  43. package/src/utils/buildRouterAndDocs.ts +14 -5
  44. package/src/utils/zValidations.ts +8 -0
  45. package/dist/src/pdf/pdf.controller.js +0 -24
  46. package/dist/src/pdf/pdf.route.js +0 -22
  47. package/dist/src/pdf/pdf.schemas.js +0 -6
  48. package/dist/src/pdf/pdf.service.js +0 -64
  49. package/dist/src/pdf/pdf.validation.js +0 -27
  50. package/src/pdf/pdf.controller.ts +0 -35
  51. package/src/pdf/pdf.route.ts +0 -28
  52. package/src/pdf/pdf.schemas.ts +0 -7
  53. package/src/pdf/pdf.service.ts +0 -103
  54. package/src/pdf/pdf.validation.ts +0 -30
@@ -10,6 +10,10 @@ let updateTimesByIdHook = null;
10
10
  export const setUpdateTimesByIdHook = (hook) => {
11
11
  updateTimesByIdHook = hook ?? null;
12
12
  };
13
+ let buildInviteEmailHook = null;
14
+ export const setBuildInviteEmailHook = (hook) => {
15
+ buildInviteEmailHook = hook ?? null;
16
+ };
13
17
  /**
14
18
  * Create a new user
15
19
  */
@@ -70,11 +74,8 @@ export const getByIdWithAuth0 = async (id) => {
70
74
  json.auth0User = auth0User;
71
75
  return json;
72
76
  };
73
- /**
74
- * Get all users in a given category (and optional organization)
75
- */
76
- export const getUsersByCategory = async (category, organization) => {
77
- const filter = { category };
77
+ export const getUsersByAppField = async (appName, fieldName, value, organization) => {
78
+ const filter = { [`apps.${appName}.${fieldName}`]: value };
78
79
  if (organization)
79
80
  filter.organization = organization;
80
81
  return User.find(filter);
@@ -123,15 +124,29 @@ export const sendInviteEmail = async (params) => {
123
124
  const organization = await organizationsService.getOrganizationById(user.organization);
124
125
  const auth0User = await auth0Service.getUserById(auth.sub);
125
126
  const lng = auth0User.data?.app_metadata?.language;
126
- const title = `${i18n.t("Invite to ", { lng })}${organization.kind === "private-wirewire" ? "paperlesspaper" : "ANABOX smart"}`;
127
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,
128
+ const baseEmail = {
129
+ title: `${i18n.t("Invite to ", { lng })}${organization?.name || "Application"}`,
130
130
  body,
131
131
  url: `/${user.organization}/invite/${inviteCode}`,
132
132
  actionButtonText: "Accept invite",
133
- domain: organization.kind === "private-wirewire" ? "web" : "memo",
133
+ domain: "web",
134
+ productName: organization?.name || "Application",
134
135
  email,
136
+ lng,
137
+ };
138
+ await sendEmail({
139
+ ...baseEmail,
140
+ ...(buildInviteEmailHook
141
+ ? buildInviteEmailHook({
142
+ auth,
143
+ user,
144
+ inviteCode,
145
+ email,
146
+ organization,
147
+ lng,
148
+ })
149
+ : {}),
135
150
  });
136
151
  };
137
152
  /**
@@ -270,7 +285,7 @@ export default {
270
285
  createCurrentUser,
271
286
  getById,
272
287
  getByIdWithAuth0,
273
- getUsersByCategory,
288
+ getUsersByAppField,
274
289
  getUsersByOrganization,
275
290
  getUsersByOrganizationAndId,
276
291
  getUsersByOwner,
@@ -293,4 +308,5 @@ export default {
293
308
  populateAuth0User,
294
309
  populateAuth0Users,
295
310
  setUpdateTimesByIdHook,
311
+ setBuildInviteEmailHook,
296
312
  };
@@ -3,38 +3,38 @@ import { z } from "zod";
3
3
  import { extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi";
4
4
  import { zPagination, zGet, zObjectId, zObjectIdFor, zPatchBody, zUpdate, zDelete, } from "../utils/zValidations.js";
5
5
  extendZodWithOpenApi(z);
6
- export const createUserSchema = {
7
- body: z.object({
8
- meta: z
9
- .object({})
10
- .passthrough()
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
- }),
6
+ export const userAppsSchema = z
7
+ .record(z.string(), z.unknown())
8
+ .optional()
9
+ .openapi({ description: "Application-specific user fields" });
10
+ export const createUserBodyShape = {
11
+ meta: z
12
+ .object({})
13
+ .passthrough()
14
+ .optional()
15
+ .openapi({
16
+ example: { key: "value" },
17
+ description: "Additional metadata for the user",
18
+ }),
19
+ apps: userAppsSchema,
20
+ organization: zObjectId.openapi({
21
+ description: "Organization ObjectId",
22
+ }),
23
+ email: z.string().email().optional().nullable().openapi({
24
+ example: "user@example.com",
25
+ description: "User email address",
26
+ }),
27
+ timezone: z.string().optional().openapi({
28
+ example: "Europe/Berlin",
29
+ description: "IANA timezone string",
30
+ }),
31
+ role: z.enum(["user", "admin", "patient", "onlyself"]).optional().openapi({
32
+ description: "Role assigned to the user",
36
33
  }),
37
34
  };
35
+ export const createUserSchema = {
36
+ body: z.object(createUserBodyShape),
37
+ };
38
38
  export const createCurrentUserSchema = createUserSchema;
39
39
  export const queryUsersSchema = {
40
40
  ...zPagination,
@@ -52,39 +52,38 @@ export const getCurrentUserSchema = {
52
52
  organization: zObjectId,
53
53
  }),
54
54
  };
55
+ export const updateUserBodyShape = {
56
+ name: z.string().optional().openapi({ description: "User full name" }),
57
+ timezone: z.string().optional().openapi({ description: "IANA timezone" }),
58
+ avatar: z.string().optional().openapi({ description: "Avatar URL" }),
59
+ meta: z
60
+ .object({})
61
+ .passthrough()
62
+ .optional()
63
+ .openapi({ description: "Additional metadata" }),
64
+ apps: userAppsSchema,
65
+ email: z
66
+ .string()
67
+ .email()
68
+ .nullable()
69
+ .optional()
70
+ .openapi({ description: "User email address" }),
71
+ role: z
72
+ .enum(["user", "admin", "patient", "onlyself"])
73
+ .optional()
74
+ .openapi({ description: "User role" }),
75
+ inviteCode: z
76
+ .string()
77
+ .nullable()
78
+ .optional()
79
+ .openapi({ description: "Invite code" }),
80
+ organization: zObjectId
81
+ .optional()
82
+ .openapi({ description: "Organization ObjectId" }),
83
+ };
55
84
  export const updateUserSchema = {
56
85
  ...zUpdate("userId"),
57
- body: zPatchBody({
58
- name: z.string().optional().openapi({ description: "User full name" }),
59
- timezone: z.string().optional().openapi({ description: "IANA timezone" }),
60
- avatar: z.string().optional().openapi({ description: "Avatar URL" }),
61
- meta: z
62
- .object({})
63
- .optional()
64
- .openapi({ description: "Additional metadata" }),
65
- category: z
66
- .enum(["doctor", "nurse", "patient", "pharmacist", "relative"])
67
- .optional()
68
- .openapi({ description: "User category" }),
69
- email: z
70
- .string()
71
- .email()
72
- .nullable()
73
- .optional()
74
- .openapi({ description: "User email address" }),
75
- role: z
76
- .enum(["user", "admin", "patient", "onlyself"])
77
- .optional()
78
- .openapi({ description: "User role" }),
79
- inviteCode: z
80
- .string()
81
- .nullable()
82
- .optional()
83
- .openapi({ description: "Invite code" }),
84
- organization: zObjectId
85
- .optional()
86
- .openapi({ description: "Organization ObjectId" }),
87
- }),
86
+ body: zPatchBody(updateUserBodyShape),
88
87
  };
89
88
  export const deleteUserSchema = zDelete("userId");
90
89
  export const organizationInviteSchema = {
@@ -8,8 +8,11 @@ const roleValidatorNames = ["validateAiRole", "validateAdmin"];
8
8
  function hasRoleValidation(validators = []) {
9
9
  return validators.some((fn) => roleValidatorNames.includes(fn.name));
10
10
  }
11
- export default function buildAiRouterAndDocs(router, routeSpecs, basePath = "/", tags = []) {
12
- routeSpecs.forEach((spec) => {
11
+ export default function buildAiRouterAndDocs(router, routeSpecs, basePath = "/", tags = [], options = {}) {
12
+ const effectiveRouteSpecs = options.routeSpecs
13
+ ? options.routeSpecs(routeSpecs)
14
+ : routeSpecs;
15
+ effectiveRouteSpecs.forEach((spec) => {
13
16
  const validate = spec.validate || [];
14
17
  const routeMiddleware = spec.validateWithRequestSchema ||
15
18
  (spec.requestSchema
@@ -33,7 +36,7 @@ export default function buildAiRouterAndDocs(router, routeSpecs, basePath = "/",
33
36
  if (spec.responseSchema &&
34
37
  !hasRoleValidation(spec.validateWithRequestSchema || validate) &&
35
38
  spec.privateDocs !== true &&
36
- spec.memoOnly !== true) {
39
+ (options.includeInDocs ? options.includeInDocs(spec) : true)) {
37
40
  // collect all middleware fn names (falls back to '<anonymous>' if unnamed)
38
41
  const middlewareNames = (spec.validateWithRequestSchema || validate).map((fn) => `\`${fn.name}\`` || "<anonymous>");
39
42
  const openApiPath = (basePath + spec.path).replace(/:([A-Za-z0-9_]+)/g, "{$1}");
@@ -51,7 +54,7 @@ export default function buildAiRouterAndDocs(router, routeSpecs, basePath = "/",
51
54
  .join("\n"),
52
55
  // (optionally) expose them as a custom extension instead:
53
56
  "x-middlewares": middlewareNames,
54
- security: [{ [bearerAuth.name]: [] }, { [xApiKey.name]: [] }],
57
+ security: [{ [xApiKey.name]: [] }, { [bearerAuth.name]: [] }],
55
58
  responses: {
56
59
  200: {
57
60
  description: "Object with user data.",
@@ -141,3 +141,10 @@ export const zDelete = (id) => ({
141
141
  });
142
142
  export const zObjectId = zObjectIdFor();
143
143
  export const zDate = () => z.string().pipe(z.coerce.date());
144
+ export const zTypeFilter = z
145
+ .string()
146
+ .openapi({
147
+ description: "Event type filter. Common values include activate and state; any other string is accepted.",
148
+ example: "activate",
149
+ })
150
+ .optional();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@internetderdinge/api",
3
- "version": "1.229.32",
3
+ "version": "1.229.39",
4
4
  "description": "Shared OpenIoT API modules",
5
5
  "main": "dist/src/index.js",
6
6
  "type": "module",
@@ -122,5 +122,8 @@
122
122
  "vitest": "^4.0.18",
123
123
  "zod": "^4.3.6"
124
124
  },
125
- "gitHead": "9556e8e376045c1e532aded7ec7132818190fa91"
125
+ "gitHead": "9556e8e376045c1e532aded7ec7132818190fa91",
126
+ "publishConfig": {
127
+ "registry": "https://registry.npmjs.org/"
128
+ }
126
129
  }
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import { execSync } from "node:child_process";
4
+ import { execFileSync } from "node:child_process";
5
5
  import dotenv from "dotenv";
6
6
  import semver from "semver";
7
7
 
@@ -22,6 +22,25 @@ const writeJson = (filePath, data) => {
22
22
  fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`);
23
23
  };
24
24
 
25
+ const publishPackage = () => {
26
+ const npmRegistry = "https://registry.npmjs.org/";
27
+ const npmUserConfig = path.join(repoRoot, ".npmrc");
28
+
29
+ execFileSync(
30
+ "npm",
31
+ ["publish", `--userconfig=${npmUserConfig}`, `--registry=${npmRegistry}`],
32
+ {
33
+ cwd: repoRoot,
34
+ stdio: "inherit",
35
+ env: {
36
+ ...process.env,
37
+ npm_config_registry: npmRegistry,
38
+ npm_config_userconfig: npmUserConfig,
39
+ },
40
+ },
41
+ );
42
+ };
43
+
25
44
  const resolveNextVersion = (current, input) => {
26
45
  const cleanedCurrent = semver.valid(semver.clean(current));
27
46
  if (!cleanedCurrent) {
@@ -144,7 +163,7 @@ writeJson(apiPackagePath, apiPackage);
144
163
 
145
164
  if (shouldPublish) {
146
165
  // Always publish through npm, regardless of the package manager used to run this script.
147
- execSync("npm publish", { cwd: repoRoot, stdio: "inherit" });
166
+ publishPackage();
148
167
  }
149
168
 
150
169
  const updates = updatePackagePaths.map((updatePackagePath) => ({
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import { execSync } from "node:child_process";
4
+ import { execFileSync } from "node:child_process";
5
5
  import semver from "semver";
6
6
 
7
7
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -20,6 +20,25 @@ const writeJson = (filePath, data) => {
20
20
  fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`);
21
21
  };
22
22
 
23
+ const publishPackage = () => {
24
+ const npmRegistry = "https://registry.npmjs.org/";
25
+ const npmUserConfig = path.join(repoRoot, ".npmrc");
26
+
27
+ execFileSync(
28
+ "npm",
29
+ ["publish", `--userconfig=${npmUserConfig}`, `--registry=${npmRegistry}`],
30
+ {
31
+ cwd: repoRoot,
32
+ stdio: "inherit",
33
+ env: {
34
+ ...process.env,
35
+ npm_config_registry: npmRegistry,
36
+ npm_config_userconfig: npmUserConfig,
37
+ },
38
+ },
39
+ );
40
+ };
41
+
23
42
  const resolveNextVersion = (current, input) => {
24
43
  const cleanedCurrent = semver.valid(semver.clean(current));
25
44
  if (!cleanedCurrent) {
@@ -141,5 +160,5 @@ console.log(
141
160
  );
142
161
 
143
162
  if (shouldPublish) {
144
- execSync("npm publish", { cwd: repoRoot, stdio: "inherit" });
163
+ publishPackage();
145
164
  }
@@ -16,9 +16,9 @@ export const accountsRouteSpecs: RouteSpec[] = [
16
16
  validate: [auth("manageUsers")],
17
17
  requestSchema: accountsValidation.currentAccountSchema,
18
18
  responseSchema: accountResponseSchema,
19
- privateDocs: true,
20
19
  handler: accountsController.current,
21
20
  summary: "Get the current account",
21
+ description: "Fetches the details of the currently authenticated account.",
22
22
  },
23
23
  {
24
24
  method: "post",
@@ -60,15 +60,28 @@ export const accountsRouteSpecs: RouteSpec[] = [
60
60
  handler: accountsController.avatar,
61
61
  summary: "Fetch account avatar",
62
62
  },
63
+ {
64
+ method: "delete",
65
+ path: "/current",
66
+ validate: [auth("manageUsers")],
67
+ requestSchema: accountsValidation.deleteCurrentSchema,
68
+ responseSchema: accountResponseSchema,
69
+ handler: accountsController.deleteCurrent,
70
+ summary: "Delete the current user's account",
71
+ description:
72
+ "Permanently deletes the current user's account. Will not delete associated data. This action is irreversible.",
73
+ },
63
74
  {
64
75
  method: "delete",
65
76
  path: "/deleteCurrent",
66
77
  validate: [auth("manageUsers")],
67
78
  requestSchema: accountsValidation.deleteCurrentSchema,
68
79
  responseSchema: accountResponseSchema,
69
- privateDocs: true,
70
80
  handler: accountsController.deleteCurrent,
71
- summary: "Delete the current account",
81
+ summary: "Delete the current user's account",
82
+ privateDocs: true,
83
+ description:
84
+ "LEGACY: Permanently deletes the current user's account. Will not delete associated data. This action is irreversible. (Replaced by DELETE /current)",
72
85
  },
73
86
  {
74
87
  method: "get",
@@ -78,6 +91,7 @@ export const accountsRouteSpecs: RouteSpec[] = [
78
91
  responseSchema: accountResponseSchema,
79
92
  handler: accountsController.getAccountById,
80
93
  summary: "Get an account by ID",
94
+ description: "Fetches the details of a single account by its ID.",
81
95
  },
82
96
  {
83
97
  method: "post",
@@ -87,7 +101,8 @@ export const accountsRouteSpecs: RouteSpec[] = [
87
101
  responseSchema: accountResponseSchema,
88
102
  privateDocs: true,
89
103
  handler: accountsController.updateEntry,
90
- summary: "Create or replace an account by ID",
104
+ summary: "Update an account by ID",
105
+ description: "LEGACY: Updates an existing account with a specified ID.",
91
106
  },
92
107
  {
93
108
  method: "patch",
@@ -95,9 +110,10 @@ export const accountsRouteSpecs: RouteSpec[] = [
95
110
  validate: [auth("manageUsers"), validateParamsAccount],
96
111
  requestSchema: accountsValidation.updateAccountSchema,
97
112
  responseSchema: accountResponseSchema,
98
- privateDocs: true,
99
113
  handler: accountsController.updateEntry,
100
114
  summary: "Update fields on an account by ID",
115
+ description:
116
+ "Updates specific fields of an existing account identified by its ID.",
101
117
  },
102
118
  ];
103
119
 
@@ -9,17 +9,10 @@ import {
9
9
  searchAdminCollections,
10
10
  } from "./adminSearch.service.js";
11
11
 
12
- const ADMIN_ROLE_CLAIM = "https://memo.wirewire.de/roles";
13
-
14
12
  type AuthRequest = Request & {
15
13
  auth?: Record<string, unknown>;
16
14
  };
17
15
 
18
- const hasAdminRole = (auth: Record<string, unknown> | undefined): boolean => {
19
- const roles = auth?.[ADMIN_ROLE_CLAIM];
20
- return Array.isArray(roles) && roles.includes("admin");
21
- };
22
-
23
16
  const readListQuery = (
24
17
  req: Request,
25
18
  ): {
@@ -42,13 +35,6 @@ const readListQuery = (
42
35
 
43
36
  export const searchAdmin = catchAsync(
44
37
  async (req: AuthRequest, res: Response) => {
45
- if (!hasAdminRole(req.auth)) {
46
- throw new ApiError(
47
- httpStatus.FORBIDDEN,
48
- "User is not part of the admin group",
49
- );
50
- }
51
-
52
38
  const search = String(req.query.search || "").trim();
53
39
  const rawLimit = Number(req.query.limit);
54
40
  const limit = Number.isFinite(rawLimit)
@@ -61,26 +47,12 @@ export const searchAdmin = catchAsync(
61
47
  );
62
48
 
63
49
  export const getStats = catchAsync(async (req: AuthRequest, res: Response) => {
64
- if (!hasAdminRole(req.auth)) {
65
- throw new ApiError(
66
- httpStatus.FORBIDDEN,
67
- "User is not part of the admin group",
68
- );
69
- }
70
-
71
50
  const result = await getAdminStats();
72
51
  res.send(result);
73
52
  });
74
53
 
75
54
  export const getIotDevices = catchAsync(
76
55
  async (req: AuthRequest, res: Response) => {
77
- if (!hasAdminRole(req.auth)) {
78
- throw new ApiError(
79
- httpStatus.FORBIDDEN,
80
- "User is not part of the admin group",
81
- );
82
- }
83
-
84
56
  const result = await getAdminIotDevices(readListQuery(req));
85
57
  res.send(result);
86
58
  },
@@ -88,13 +60,6 @@ export const getIotDevices = catchAsync(
88
60
 
89
61
  export const getDevices = catchAsync(
90
62
  async (req: AuthRequest, res: Response) => {
91
- if (!hasAdminRole(req.auth)) {
92
- throw new ApiError(
93
- httpStatus.FORBIDDEN,
94
- "User is not part of the admin group",
95
- );
96
- }
97
-
98
63
  const result = await getAdminDevices(readListQuery(req));
99
64
  res.send(result);
100
65
  },
@@ -1,7 +1,9 @@
1
1
  import { Router } from "express";
2
- import buildRouterAndDocs, { type RouteSpec } from "../utils/buildRouterAndDocs.js";
2
+ import buildRouterAndDocs, {
3
+ type RouteSpec,
4
+ } from "../utils/buildRouterAndDocs.js";
3
5
  import auth from "../middlewares/auth.js";
4
- import { validateAdmin } from "../middlewares/validateAdmin.js";
6
+ import { validateAdminOrSupport } from "../middlewares/validateAdminOrSupport.js";
5
7
  import {
6
8
  getDevices,
7
9
  getIotDevices,
@@ -25,7 +27,7 @@ export const adminSearchRouteSpecs: RouteSpec[] = [
25
27
  {
26
28
  method: "get",
27
29
  path: "/stats",
28
- validate: [auth("getUsers"), validateAdmin],
30
+ validate: [auth("getUsers"), validateAdminOrSupport],
29
31
  requestSchema: adminStatsSchema,
30
32
  responseSchema: adminStatsResponseSchema,
31
33
  handler: getStats,
@@ -35,7 +37,7 @@ export const adminSearchRouteSpecs: RouteSpec[] = [
35
37
  {
36
38
  method: "get",
37
39
  path: "/search",
38
- validate: [auth("getUsers"), validateAdmin],
40
+ validate: [auth("getUsers"), validateAdminOrSupport],
39
41
  requestSchema: adminSearchSchema,
40
42
  responseSchema: adminSearchResponseSchema,
41
43
  handler: searchAdmin,
@@ -46,7 +48,7 @@ export const adminSearchRouteSpecs: RouteSpec[] = [
46
48
  {
47
49
  method: "get",
48
50
  path: "/iotDevices",
49
- validate: [auth("getUsers"), validateAdmin],
51
+ validate: [auth("getUsers"), validateAdminOrSupport],
50
52
  requestSchema: adminIotDevicesSchema,
51
53
  responseSchema: adminIotDevicesResponseSchema,
52
54
  handler: getIotDevices,
@@ -57,7 +59,7 @@ export const adminSearchRouteSpecs: RouteSpec[] = [
57
59
  {
58
60
  method: "get",
59
61
  path: "/devices",
60
- validate: [auth("getUsers"), validateAdmin],
62
+ validate: [auth("getUsers"), validateAdminOrSupport],
61
63
  requestSchema: adminDevicesSchema,
62
64
  responseSchema: adminDevicesResponseSchema,
63
65
  handler: getDevices,
@@ -89,13 +89,11 @@ type AdminListQuery = {
89
89
 
90
90
  const IOT_DEVICES_CACHE_TTL_MS = 60_000;
91
91
 
92
- let iotDevicesCache:
93
- | {
94
- loadedAt: number;
95
- devices: unknown[];
96
- pending?: Promise<unknown[]>;
97
- }
98
- | null = null;
92
+ let iotDevicesCache: {
93
+ loadedAt: number;
94
+ devices: unknown[];
95
+ pending?: Promise<unknown[]>;
96
+ } | null = null;
99
97
 
100
98
  const getCachedIotDevices = async (): Promise<unknown[]> => {
101
99
  const now = Date.now();
@@ -112,9 +110,7 @@ const getCachedIotDevices = async (): Promise<unknown[]> => {
112
110
 
113
111
  const pending = Promise.resolve(
114
112
  iotDevicesService.getDeviceStatusList(undefined),
115
- ).then(
116
- (devices) => (Array.isArray(devices) ? devices : []),
117
- );
113
+ ).then((devices) => (Array.isArray(devices) ? devices : []));
118
114
 
119
115
  iotDevicesCache = {
120
116
  loadedAt: now,
@@ -159,6 +159,10 @@ const updateEntry = catchAsync(
159
159
  const getEvents = catchAsync(
160
160
  async (req: Request, res: Response): Promise<void> => {
161
161
  const device = await devicesService.getById(req.params.deviceId);
162
+ if (!device) {
163
+ throw new ApiError(httpStatus.NOT_FOUND, "Device not found");
164
+ }
165
+
162
166
  const events = await iotDevicesService.getEvents({
163
167
  ...req.query,
164
168
  createdAt: device.createdAt,
@@ -195,17 +199,6 @@ export const resetDevice = catchAsync(
195
199
  },
196
200
  );
197
201
 
198
- const ledLight = catchAsync(
199
- async (req: Request, res: Response): Promise<void> => {
200
- const device = await devicesService.getByIdWithIoT(req.params.deviceId);
201
- const ping = await iotDevicesService.ledLightHint(
202
- device.deviceId,
203
- req.body,
204
- );
205
- res.send({ device, ping });
206
- },
207
- );
208
-
209
202
  const rebootDevice = catchAsync(
210
203
  async (req: Request, res: Response): Promise<void> => {
211
204
  const device = await devicesService.getByIdWithIoT(req.params.deviceId);
@@ -221,7 +214,6 @@ export {
221
214
  getEvents,
222
215
  registerDevice,
223
216
  pingDevice,
224
- ledLight,
225
217
  rebootDevice,
226
218
  getEntry,
227
219
  updateEntry,
@@ -20,7 +20,6 @@ import {
20
20
  getEventsSchema,
21
21
  pingDeviceSchema,
22
22
  registerDeviceSchema,
23
- ledLightSchema,
24
23
  rebootDeviceSchema,
25
24
  resetDeviceSchema,
26
25
  } from "./devices.validation.js";
@@ -61,7 +60,7 @@ export const devicesRouteSpecs: RouteSpec[] = [
61
60
  handler: devicesController.queryDevicesByUser,
62
61
  summary: "Query devices by user",
63
62
  description:
64
- "Retrieve a paginated list of devices visible to the authenticated user.",
63
+ "Retrieve a paginated list of devices visible to the authenticated user. Either patient or organization is required.",
65
64
  },
66
65
  {
67
66
  method: "get",
@@ -92,9 +91,21 @@ export const devicesRouteSpecs: RouteSpec[] = [
92
91
  requestSchema: updateDeviceSchema,
93
92
  responseSchema: deviceResponseSchema,
94
93
  handler: devicesController.updateEntry,
94
+ privateDocs: true,
95
95
  summary: "Update a device",
96
96
  description:
97
- "Modify the properties of an existing device identified by its ID.",
97
+ "LEGACY: Modify the properties of an existing device identified by its ID.",
98
+ },
99
+ {
100
+ method: "patch",
101
+ path: "/:deviceId",
102
+ validate: [auth("manageUsers"), validateDevice, validateOrganizationUpdate],
103
+ requestSchema: updateDeviceSchema,
104
+ responseSchema: deviceResponseSchema,
105
+ handler: devicesController.updateEntry,
106
+ summary: "Update a device",
107
+ description:
108
+ "Updates specific fields of an existing device identified by its ID.",
98
109
  },
99
110
  {
100
111
  method: "delete",
@@ -121,18 +132,6 @@ export const devicesRouteSpecs: RouteSpec[] = [
121
132
  description:
122
133
  "Associate an existing device with the authenticated organization.",
123
134
  },
124
- {
125
- method: "post",
126
- path: "/ledlight/:deviceId",
127
- validate: [auth("getUsers"), validateDevice],
128
- requestSchema: ledLightSchema,
129
- responseSchema: genericResponseSchema,
130
- handler: devicesController.ledLight,
131
- summary: "Set LED light on device",
132
- description:
133
- "Turn the device’s LED on or off, or set its color/brightness.",
134
- memoOnly: true,
135
- },
136
135
  {
137
136
  method: "get",
138
137
  path: "/ping/:deviceId",