@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.
- package/dist/src/accounts/accounts.route.js +18 -5
- package/dist/src/admin/adminSearch.controller.js +0 -19
- package/dist/src/admin/adminSearch.route.js +5 -5
- package/dist/src/devices/devices.controller.js +4 -6
- package/dist/src/devices/devices.route.js +14 -14
- package/dist/src/devices/devices.validation.js +15 -54
- package/dist/src/email/email.service.js +6 -6
- package/dist/src/index.js +5 -1
- package/dist/src/iotdevice/iotdevice.route.js +3 -1
- package/dist/src/iotdevice/iotdevice.service.js +5 -5
- package/dist/src/iotdevice/iotdevice.validation.js +12 -4
- package/dist/src/middlewares/validateAdminOrSupport.js +20 -0
- package/dist/src/organizations/organizations.controller.js +17 -6
- package/dist/src/organizations/organizations.route.js +2 -1
- package/dist/src/users/users.model.js +4 -2
- package/dist/src/users/users.route.js +2 -2
- package/dist/src/users/users.service.js +26 -10
- package/dist/src/users/users.validation.js +60 -61
- package/dist/src/utils/buildRouterAndDocs.js +7 -4
- package/dist/src/utils/zValidations.js +7 -0
- package/package.json +5 -2
- package/scripts/release-and-sync-paperless.mjs +21 -2
- package/scripts/release-version.mjs +21 -2
- package/src/accounts/accounts.route.ts +21 -5
- package/src/admin/adminSearch.controller.ts +0 -35
- package/src/admin/adminSearch.route.ts +8 -6
- package/src/admin/adminSearch.service.ts +6 -10
- package/src/devices/devices.controller.ts +4 -12
- package/src/devices/devices.route.ts +14 -15
- package/src/devices/devices.validation.ts +20 -54
- package/src/email/email.service.ts +15 -7
- package/src/index.ts +5 -1
- package/src/iotdevice/iotdevice.route.ts +3 -1
- package/src/iotdevice/iotdevice.service.ts +8 -7
- package/src/iotdevice/iotdevice.validation.ts +12 -4
- package/src/middlewares/validateAdminOrSupport.ts +34 -0
- package/src/organizations/organizations.controller.ts +38 -7
- package/src/organizations/organizations.route.ts +3 -1
- package/src/users/users.model.ts +7 -3
- package/src/users/users.route.ts +3 -2
- package/src/users/users.service.ts +50 -14
- package/src/users/users.validation.ts +62 -60
- package/src/utils/buildRouterAndDocs.ts +14 -5
- package/src/utils/zValidations.ts +8 -0
- package/dist/src/pdf/pdf.controller.js +0 -24
- package/dist/src/pdf/pdf.route.js +0 -22
- package/dist/src/pdf/pdf.schemas.js +0 -6
- package/dist/src/pdf/pdf.service.js +0 -64
- package/dist/src/pdf/pdf.validation.js +0 -27
- package/src/pdf/pdf.controller.ts +0 -35
- package/src/pdf/pdf.route.ts +0 -28
- package/src/pdf/pdf.schemas.ts +0 -7
- package/src/pdf/pdf.service.ts +0 -103
- 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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: [{ [
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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: "
|
|
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, {
|
|
2
|
+
import buildRouterAndDocs, {
|
|
3
|
+
type RouteSpec,
|
|
4
|
+
} from "../utils/buildRouterAndDocs.js";
|
|
3
5
|
import auth from "../middlewares/auth.js";
|
|
4
|
-
import {
|
|
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"),
|
|
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"),
|
|
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"),
|
|
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"),
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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",
|