@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
@@ -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.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@internetderdinge/api",
3
- "version": "1.229.31",
3
+ "version": "1.229.37",
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,11 +1,13 @@
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
+ import dotenv from "dotenv";
5
6
  import semver from "semver";
6
7
 
7
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
9
  const repoRoot = path.resolve(__dirname, "..");
10
+ dotenv.config({ path: path.join(repoRoot, ".env") });
9
11
 
10
12
  const args = process.argv.slice(2);
11
13
  const shouldPublish = !args.includes("--no-publish");
@@ -20,6 +22,25 @@ const writeJson = (filePath, data) => {
20
22
  fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`);
21
23
  };
22
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
+
23
44
  const resolveNextVersion = (current, input) => {
24
45
  const cleanedCurrent = semver.valid(semver.clean(current));
25
46
  if (!cleanedCurrent) {
@@ -44,12 +65,23 @@ const resolveNextVersion = (current, input) => {
44
65
  );
45
66
  };
46
67
 
47
- const resolvePaperlesspaperWebRoot = () => {
48
- if (process.env.PAPERLESSPAPER_WEB_PATH) {
49
- return path.resolve(process.env.PAPERLESSPAPER_WEB_PATH);
68
+ const resolveUpdatePackagePaths = () => {
69
+ const updatePaths = process.env.UPDATE_PATHS?.split(",")
70
+ .map((value) => value.trim())
71
+ .filter(Boolean);
72
+
73
+ if (!updatePaths?.length) {
74
+ throw new Error(
75
+ "UPDATE_PATHS must be set in .env to a comma-separated list of package.json paths.",
76
+ );
50
77
  }
51
78
 
52
- return path.resolve(repoRoot, "../../paperlesspaper-web");
79
+ return updatePaths.map((updatePath) => {
80
+ const resolvedPath = path.resolve(repoRoot, updatePath);
81
+ return path.basename(resolvedPath) === "package.json"
82
+ ? resolvedPath
83
+ : path.join(resolvedPath, "package.json");
84
+ });
53
85
  };
54
86
 
55
87
  const updateDependencyVersion = (
@@ -88,8 +120,9 @@ const updateDependencyVersion = (
88
120
  currentDependencyVersion &&
89
121
  currentDependencyVersion !== expectedVersion
90
122
  ) {
123
+ const packageName = path.basename(path.dirname(packageJsonPath));
91
124
  throw new Error(
92
- `paperlesspaper-api dependency is ${currentRange} (expected ${expectedVersion}). Update it before bumping.`,
125
+ `${packageName} dependency is ${currentRange} (expected ${expectedVersion}). Update it before bumping.`,
93
126
  );
94
127
  }
95
128
 
@@ -117,34 +150,33 @@ if (!cleanedCurrent) {
117
150
  }
118
151
 
119
152
  const nextVersion = resolveNextVersion(currentVersion, versionInput);
153
+ const updatePackagePaths = resolveUpdatePackagePaths();
154
+
155
+ for (const updatePackagePath of updatePackagePaths) {
156
+ if (!fs.existsSync(updatePackagePath)) {
157
+ throw new Error(`Could not find package.json: ${updatePackagePath}`);
158
+ }
159
+ }
120
160
 
121
161
  apiPackage.version = nextVersion;
122
162
  writeJson(apiPackagePath, apiPackage);
123
163
 
124
164
  if (shouldPublish) {
125
- execSync("npm publish", { cwd: repoRoot, stdio: "inherit" });
165
+ // Always publish through npm, regardless of the package manager used to run this script.
166
+ publishPackage();
126
167
  }
127
168
 
128
- const paperlesspaperRoot = resolvePaperlesspaperWebRoot();
129
- const paperlessApiPath = path.join(
130
- paperlesspaperRoot,
131
- "packages/paperlesspaper-api/package.json",
132
- );
133
-
134
- if (!fs.existsSync(paperlessApiPath)) {
135
- throw new Error(
136
- "Could not find paperlesspaper-api package.json. Set PAPERLESSPAPER_WEB_PATH to the repo root.",
137
- );
138
- }
139
-
140
- const paperlessUpdate = updateDependencyVersion(
141
- paperlessApiPath,
142
- "@internetderdinge/api",
143
- nextVersion,
144
- cleanedCurrent,
145
- );
169
+ const updates = updatePackagePaths.map((updatePackagePath) => ({
170
+ packageName: readJson(updatePackagePath).name,
171
+ ...updateDependencyVersion(
172
+ updatePackagePath,
173
+ "@internetderdinge/api",
174
+ nextVersion,
175
+ cleanedCurrent,
176
+ ),
177
+ }));
146
178
 
147
179
  console.log(`Updated @internetderdinge/api to ${nextVersion}`);
148
- console.log(
149
- `paperlesspaper-api: ${paperlessUpdate.previous} -> ${paperlessUpdate.next}`,
150
- );
180
+ for (const update of updates) {
181
+ console.log(`${update.packageName}: ${update.previous} -> ${update.next}`);
182
+ }
@@ -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,
@@ -195,17 +195,6 @@ export const resetDevice = catchAsync(
195
195
  },
196
196
  );
197
197
 
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
198
  const rebootDevice = catchAsync(
210
199
  async (req: Request, res: Response): Promise<void> => {
211
200
  const device = await devicesService.getByIdWithIoT(req.params.deviceId);
@@ -221,7 +210,6 @@ export {
221
210
  getEvents,
222
211
  registerDevice,
223
212
  pingDevice,
224
- ledLight,
225
213
  rebootDevice,
226
214
  getEntry,
227
215
  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";
@@ -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",