@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.
- 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 +1 -6
- package/dist/src/devices/devices.route.js +13 -13
- package/dist/src/devices/devices.validation.js +0 -47
- 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/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/package.json +5 -2
- package/scripts/release-and-sync-paperless.mjs +60 -28
- 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 +0 -12
- package/src/devices/devices.route.ts +13 -14
- package/src/devices/devices.validation.ts +0 -47
- package/src/email/email.service.ts +15 -7
- package/src/index.ts +5 -1
- package/src/iotdevice/iotdevice.route.ts +3 -1
- 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/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
|
@@ -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.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@internetderdinge/api",
|
|
3
|
-
"version": "1.229.
|
|
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 {
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
165
|
+
// Always publish through npm, regardless of the package manager used to run this script.
|
|
166
|
+
publishPackage();
|
|
126
167
|
}
|
|
127
168
|
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
149
|
-
|
|
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 {
|
|
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,
|
|
@@ -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",
|