@7365admin1/layer-common 3.2.2-staging.197 → 3.2.2-staging.198
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.
|
@@ -153,6 +153,13 @@ const props = defineProps({
|
|
|
153
153
|
type: Boolean,
|
|
154
154
|
default: false,
|
|
155
155
|
},
|
|
156
|
+
// The site this reader belongs to. Passed down from HidReaderManagement,
|
|
157
|
+
// which already has it as a prop from the page's route. Reader discovery is
|
|
158
|
+
// authorized against it server-side.
|
|
159
|
+
site: {
|
|
160
|
+
type: String,
|
|
161
|
+
default: "",
|
|
162
|
+
},
|
|
156
163
|
});
|
|
157
164
|
|
|
158
165
|
const emit = defineEmits<{
|
|
@@ -236,6 +243,7 @@ async function connectReader() {
|
|
|
236
243
|
baseUrl: draft.baseUrl.trim(),
|
|
237
244
|
username: draft.username.trim(),
|
|
238
245
|
password: draft.password,
|
|
246
|
+
site: props.site,
|
|
239
247
|
});
|
|
240
248
|
const nestedData = response?.data;
|
|
241
249
|
const data = typeof nestedData === "object" && nestedData !== null
|
|
@@ -90,6 +90,23 @@ export type HidSipAccountData = {
|
|
|
90
90
|
provisionedAt: string;
|
|
91
91
|
};
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* True only for the specific validation error an API that predates core #1878
|
|
95
|
+
* returns when the discover body carries `site`. Deliberately narrow: after
|
|
96
|
+
* #1878 `site` is required, so this message can no longer be produced and the
|
|
97
|
+
* fallback below can never hide a real authorization refusal.
|
|
98
|
+
*/
|
|
99
|
+
export function isUnknownSiteFieldError(error: unknown): boolean {
|
|
100
|
+
const record = typeof error === "object" && error !== null
|
|
101
|
+
? error as Record<string, unknown>
|
|
102
|
+
: {};
|
|
103
|
+
const data = typeof record.data === "object" && record.data !== null
|
|
104
|
+
? record.data as Record<string, unknown>
|
|
105
|
+
: {};
|
|
106
|
+
const message = String(data.message || record.message || "");
|
|
107
|
+
return message.includes("\"site\"") && message.includes("not allowed");
|
|
108
|
+
}
|
|
109
|
+
|
|
93
110
|
export default function useHidAmico() {
|
|
94
111
|
const basePath = "/api/access-management/hid";
|
|
95
112
|
|
|
@@ -260,11 +277,28 @@ export default function useHidAmico() {
|
|
|
260
277
|
);
|
|
261
278
|
}
|
|
262
279
|
|
|
263
|
-
function discoverReader(
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
280
|
+
async function discoverReader(
|
|
281
|
+
payload: Pick<HidReaderPayload, "baseUrl" | "username" | "password" | "site">,
|
|
282
|
+
) {
|
|
283
|
+
const post = (body: Record<string, unknown>) =>
|
|
284
|
+
useNuxtApp().$api<Record<string, unknown>>(`${basePath}/readers/discover`, {
|
|
285
|
+
method: "POST",
|
|
286
|
+
body,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
return await post(payload as Record<string, unknown>);
|
|
291
|
+
} catch (error: unknown) {
|
|
292
|
+
// The discover endpoint only started accepting (and requiring) `site` in
|
|
293
|
+
// core #1878. An API that predates it validates the body with Joi's
|
|
294
|
+
// default allowUnknown:false and answers `"site" is not allowed`, so the
|
|
295
|
+
// web layer would break the moment it shipped ahead of the API. Retry once
|
|
296
|
+
// without `site` so both API versions work.
|
|
297
|
+
// ponytail: remove this fallback once core #1878 is deployed everywhere.
|
|
298
|
+
if (!isUnknownSiteFieldError(error)) throw error;
|
|
299
|
+
const { site: _site, ...legacy } = payload;
|
|
300
|
+
return await post(legacy as Record<string, unknown>);
|
|
301
|
+
}
|
|
268
302
|
}
|
|
269
303
|
|
|
270
304
|
function getUserPinStatus(readerId: string, hidUserId: string | number) {
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@7365admin1/layer-common",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "3.2.2-staging.
|
|
5
|
+
"version": "3.2.2-staging.198",
|
|
6
6
|
"author": "7365admin1",
|
|
7
7
|
"main": "./nuxt.config.ts",
|
|
8
8
|
"//files": "What a consumer extending this layer actually loads. Without this npm ships the whole working tree - the changesets, the CI workflows, the render harness in tools/ and any scratch directory that happened to exist at publish time. Nuxt resolves a layer by directory, so every runtime directory below has to stay listed; adding a new top-level runtime directory means adding it here too.",
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
|
|
7
|
+
import useHidAmico, { isUnknownSiteFieldError } from "../composables/useHidAmico.ts";
|
|
8
|
+
|
|
9
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
|
+
|
|
11
|
+
type Call = { url: string; body: Record<string, unknown> };
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The composable calls the Nuxt auto-import `useNuxtApp().$api(...)`. Install a
|
|
15
|
+
* stub for it so the request body can be read without a Nuxt app.
|
|
16
|
+
*/
|
|
17
|
+
function stubApi(handler: (call: Call) => unknown) {
|
|
18
|
+
const calls: Call[] = [];
|
|
19
|
+
(globalThis as Record<string, unknown>).useNuxtApp = () => ({
|
|
20
|
+
$api: (url: string, options: { body: Record<string, unknown> }) => {
|
|
21
|
+
const call = { url, body: options.body };
|
|
22
|
+
calls.push(call);
|
|
23
|
+
return Promise.resolve(handler(call));
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
return calls;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function fetchError(message: string) {
|
|
30
|
+
return Object.assign(new Error("400 Bad Request"), { data: { message } });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
test("discoverReader sends site in the request body", async () => {
|
|
34
|
+
const calls = stubApi(() => ({ data: { deviceId: "D1", portals: [] } }));
|
|
35
|
+
|
|
36
|
+
await useHidAmico().discoverReader({
|
|
37
|
+
baseUrl: "https://reader.example",
|
|
38
|
+
username: "operator",
|
|
39
|
+
password: "secret",
|
|
40
|
+
site: "0123456789abcdef01234567",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
assert.equal(calls.length, 1);
|
|
44
|
+
assert.equal(calls[0].url, "/api/access-management/hid/readers/discover");
|
|
45
|
+
assert.equal(calls[0].body.site, "0123456789abcdef01234567");
|
|
46
|
+
assert.equal(calls[0].body.baseUrl, "https://reader.example");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("discoverReader retries without site when the API predates core #1878", async () => {
|
|
50
|
+
const calls = stubApi((call) => {
|
|
51
|
+
if ("site" in call.body) throw fetchError('"site" is not allowed');
|
|
52
|
+
return { data: { deviceId: "D1", portals: [] } };
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const response = await useHidAmico().discoverReader({
|
|
56
|
+
baseUrl: "https://reader.example",
|
|
57
|
+
username: "operator",
|
|
58
|
+
password: "secret",
|
|
59
|
+
site: "0123456789abcdef01234567",
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
assert.equal(calls.length, 2);
|
|
63
|
+
assert.equal("site" in calls[1].body, false);
|
|
64
|
+
assert.equal(calls[1].body.baseUrl, "https://reader.example");
|
|
65
|
+
assert.deepEqual(response, { data: { deviceId: "D1", portals: [] } });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("discoverReader does not retry a real refusal", async () => {
|
|
69
|
+
const calls = stubApi(() => {
|
|
70
|
+
throw fetchError("You are not authorized to manage readers for this site.");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await assert.rejects(
|
|
74
|
+
useHidAmico().discoverReader({
|
|
75
|
+
baseUrl: "https://reader.example",
|
|
76
|
+
username: "operator",
|
|
77
|
+
password: "secret",
|
|
78
|
+
site: "0123456789abcdef01234567",
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
assert.equal(calls.length, 1);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("isUnknownSiteFieldError only matches the unknown-key message", () => {
|
|
85
|
+
assert.equal(isUnknownSiteFieldError(fetchError('"site" is not allowed')), true);
|
|
86
|
+
assert.equal(isUnknownSiteFieldError(new Error('"site" is not allowed')), true);
|
|
87
|
+
assert.equal(isUnknownSiteFieldError(fetchError('"site" is required')), false);
|
|
88
|
+
assert.equal(isUnknownSiteFieldError(fetchError('"baseUrl" is not allowed')), false);
|
|
89
|
+
assert.equal(isUnknownSiteFieldError(fetchError("Unauthorized")), false);
|
|
90
|
+
assert.equal(isUnknownSiteFieldError(null), false);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("the reader form takes site as a prop and puts it in the discover call", () => {
|
|
94
|
+
const source = readFileSync(join(root, "components", "HidReaderForm.vue"), "utf8");
|
|
95
|
+
assert.match(source, /site:\s*\{\s*type:\s*String/);
|
|
96
|
+
assert.match(source, /discoverReader\(\{[\s\S]*?site:\s*props\.site[\s\S]*?\}\)/);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("reader management passes its site prop down to the form", () => {
|
|
100
|
+
const source = readFileSync(join(root, "components", "HidReaderManagement.vue"), "utf8");
|
|
101
|
+
assert.match(source, /<HidReaderForm[\s\S]*?:site="site"[\s\S]*?\/>/);
|
|
102
|
+
});
|