@7365admin1/layer-common 3.1.4-staging.55 → 3.1.4-staging.57
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/.changeset/camera-wall-gated-endpoint.md +47 -0
- package/components/CameraWall.vue +19 -44
- package/components/HidIntercomManagement.vue +184 -60
- package/components/HidQrCodeConfiguration.vue +195 -31
- package/components/HidUserEnrollment.vue +94 -2
- package/components/SiteSettings.vue +4 -2
- package/composables/useHidAmico.ts +64 -3
- package/composables/useSiteSettings.ts +28 -0
- package/package.json +1 -1
- package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
- package/types/site.d.ts +6 -0
- package/utils/camera-wall.test.ts +28 -0
- package/utils/camera-wall.ts +18 -0
- package/middleware/member.ts +0 -4
|
@@ -48,6 +48,26 @@ type HidPermissionListResponse = {
|
|
|
48
48
|
limit: number;
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
+
type HidFacialEnrollmentResult = {
|
|
52
|
+
user_id?: number;
|
|
53
|
+
success?: boolean;
|
|
54
|
+
scores?: Record<string, unknown>;
|
|
55
|
+
errors?: Array<{ code?: number; message?: string }>;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type HidSipAccountData = {
|
|
59
|
+
orgId: string;
|
|
60
|
+
site: string;
|
|
61
|
+
userId: string;
|
|
62
|
+
extension: string;
|
|
63
|
+
username: string;
|
|
64
|
+
password: string;
|
|
65
|
+
sipAddress: string;
|
|
66
|
+
websocketUrl: string;
|
|
67
|
+
status: "active";
|
|
68
|
+
provisionedAt: string;
|
|
69
|
+
};
|
|
70
|
+
|
|
51
71
|
export default function useHidAmico() {
|
|
52
72
|
const basePath = "/api/access-management/hid";
|
|
53
73
|
|
|
@@ -142,6 +162,33 @@ export default function useHidAmico() {
|
|
|
142
162
|
});
|
|
143
163
|
}
|
|
144
164
|
|
|
165
|
+
function setUserImage(
|
|
166
|
+
readerId: string,
|
|
167
|
+
hidUserId: string | number,
|
|
168
|
+
image: Blob,
|
|
169
|
+
options: { timestamp?: number; match?: boolean } = {},
|
|
170
|
+
) {
|
|
171
|
+
return useNuxtApp().$api<{ data: HidFacialEnrollmentResult }>(
|
|
172
|
+
`${basePath}/readers/${readerId}/users/${hidUserId}/image`,
|
|
173
|
+
{
|
|
174
|
+
method: "PUT",
|
|
175
|
+
query: {
|
|
176
|
+
...(options.timestamp ? { timestamp: options.timestamp } : {}),
|
|
177
|
+
match: options.match ?? true,
|
|
178
|
+
},
|
|
179
|
+
headers: { "Content-Type": "application/octet-stream" },
|
|
180
|
+
body: image,
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function deleteUserImage(readerId: string, hidUserId: string | number) {
|
|
186
|
+
return useNuxtApp().$api<Record<string, unknown>>(
|
|
187
|
+
`${basePath}/readers/${readerId}/users/${hidUserId}/image`,
|
|
188
|
+
{ method: "DELETE" },
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
145
192
|
function createIdentity(readerId: string, payload: HidIdentityPayload) {
|
|
146
193
|
return useNuxtApp().$api<Record<string, any>>(`${basePath}/readers/${readerId}/identities`, {
|
|
147
194
|
method: "POST",
|
|
@@ -181,16 +228,25 @@ export default function useHidAmico() {
|
|
|
181
228
|
});
|
|
182
229
|
}
|
|
183
230
|
|
|
184
|
-
function getSitePermissions(siteId: string) {
|
|
231
|
+
function getSitePermissions(siteId: string, orgId: string, readerId: string) {
|
|
185
232
|
return useNuxtApp().$api<{ data: THidSitePermissions }>(
|
|
186
233
|
`${basePath}/sites/${siteId}/permissions`,
|
|
187
|
-
{ method: "GET" },
|
|
234
|
+
{ method: "GET", query: { orgId, readerId } },
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function ensureSipAccount(siteId: string, orgId: string) {
|
|
239
|
+
return useNuxtApp().$api<{ data: HidSipAccountData }>(
|
|
240
|
+
`${basePath}/sites/${siteId}/intercom/sip-account`,
|
|
241
|
+
{ method: "POST", body: { orgId } },
|
|
188
242
|
);
|
|
189
243
|
}
|
|
190
244
|
|
|
191
245
|
function getPermissionCandidates(
|
|
192
246
|
siteId: string,
|
|
193
247
|
query: {
|
|
248
|
+
orgId: string;
|
|
249
|
+
readerId: string;
|
|
194
250
|
category: THidPermissionCategory | "intercom";
|
|
195
251
|
search?: string;
|
|
196
252
|
page?: number;
|
|
@@ -205,11 +261,13 @@ export default function useHidAmico() {
|
|
|
205
261
|
|
|
206
262
|
function updateSitePermissions(
|
|
207
263
|
siteId: string,
|
|
264
|
+
orgId: string,
|
|
265
|
+
readerId: string,
|
|
208
266
|
assignments: Array<Pick<THidPermissionAssignment, "subjectId" | "category" | "intercom">>,
|
|
209
267
|
) {
|
|
210
268
|
return useNuxtApp().$api<{ data: THidSitePermissions; message: string }>(
|
|
211
269
|
`${basePath}/sites/${siteId}/permissions`,
|
|
212
|
-
{ method: "PUT", body: { assignments } },
|
|
270
|
+
{ method: "PUT", body: { orgId, readerId, assignments } },
|
|
213
271
|
);
|
|
214
272
|
}
|
|
215
273
|
|
|
@@ -226,6 +284,8 @@ export default function useHidAmico() {
|
|
|
226
284
|
setReaderConfiguration,
|
|
227
285
|
getLogs,
|
|
228
286
|
getUserImage,
|
|
287
|
+
setUserImage,
|
|
288
|
+
deleteUserImage,
|
|
229
289
|
getIdentities,
|
|
230
290
|
createIdentity,
|
|
231
291
|
updateIdentity,
|
|
@@ -233,6 +293,7 @@ export default function useHidAmico() {
|
|
|
233
293
|
getIntercomStatus,
|
|
234
294
|
makeIntercomCall,
|
|
235
295
|
finalizeIntercomCall,
|
|
296
|
+
ensureSipAccount,
|
|
236
297
|
getSitePermissions,
|
|
237
298
|
getPermissionCandidates,
|
|
238
299
|
updateSitePermissions,
|
|
@@ -99,6 +99,33 @@ export default function () {
|
|
|
99
99
|
});
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* The monitoring wall for one site.
|
|
104
|
+
*
|
|
105
|
+
* A different endpoint from `getAllSiteCameras`, and the difference is the
|
|
106
|
+
* point. That one is the Settings panel's paginated CRUD list; this one is
|
|
107
|
+
* purpose-built for a wall and is what the React Native app already uses:
|
|
108
|
+
*
|
|
109
|
+
* - it is **site-scoped and authorised server-side** — the caller must be a
|
|
110
|
+
* member of the site, of its owning organisation, or work for an
|
|
111
|
+
* organisation engaged to serve it;
|
|
112
|
+
* - it returns `type: "ip"` cameras only, so an ANPR unit can never land on a
|
|
113
|
+
* monitoring wall;
|
|
114
|
+
* - it returns each camera's **capability descriptor** and the server's own
|
|
115
|
+
* `unavailableReason`, so a tile explains itself instead of showing a black
|
|
116
|
+
* rectangle;
|
|
117
|
+
* - it never returns a credential, and it contacts no device.
|
|
118
|
+
*
|
|
119
|
+
* Unpaginated by design: a wall shows a site's cameras, and paging them was
|
|
120
|
+
* an artefact of borrowing the CRUD list.
|
|
121
|
+
*/
|
|
122
|
+
async function getSiteWall(siteId: string) {
|
|
123
|
+
return await useNuxtApp().$api<Record<string, any>>(
|
|
124
|
+
`/api/site-cameras/site/${siteId}/wall`,
|
|
125
|
+
{ method: "GET" }
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
102
129
|
async function updateSiteInformation(
|
|
103
130
|
siteId: string,
|
|
104
131
|
payload: { bgImage: string; description: string; docs: { id: string; name: string }[] }
|
|
@@ -175,6 +202,7 @@ export default function () {
|
|
|
175
202
|
updateSite,
|
|
176
203
|
addCamera,
|
|
177
204
|
getAllSiteCameras,
|
|
205
|
+
getSiteWall,
|
|
178
206
|
setSiteGuardPosts,
|
|
179
207
|
updateSiteCamera,
|
|
180
208
|
deleteSiteCameraById,
|
package/package.json
CHANGED
package/types/site.d.ts
CHANGED
|
@@ -13,10 +13,16 @@ declare type THidPermissionAssignment = {
|
|
|
13
13
|
name: string;
|
|
14
14
|
subtitle?: string;
|
|
15
15
|
intercom: boolean;
|
|
16
|
+
readerId?: string;
|
|
17
|
+
location?: string;
|
|
18
|
+
locationKey?: string;
|
|
16
19
|
};
|
|
17
20
|
|
|
18
21
|
declare type THidSitePermissions = {
|
|
22
|
+
orgId: string;
|
|
19
23
|
site: string;
|
|
24
|
+
readerId: string;
|
|
25
|
+
location: string;
|
|
20
26
|
assignments: THidPermissionAssignment[];
|
|
21
27
|
counts: {
|
|
22
28
|
resident: number;
|
|
@@ -119,3 +119,31 @@ test("a server that says live video is unsupported is believed", () => {
|
|
|
119
119
|
assert.equal(v.showPlayer, false);
|
|
120
120
|
assert.equal(v.detail, "This camera is not active.");
|
|
121
121
|
});
|
|
122
|
+
|
|
123
|
+
/* ------------------------------------------- the server's reason wins */
|
|
124
|
+
|
|
125
|
+
test("the server's own refusal is shown verbatim, not replaced with a guess", () => {
|
|
126
|
+
// The one that matters: the browser cannot know this, and until the API host
|
|
127
|
+
// is configured it is the true answer for every camera in the estate.
|
|
128
|
+
const noRecorder = "No recorder is configured for this camera's relay.";
|
|
129
|
+
const view = tileView(ip({ unavailableReason: noRecorder }));
|
|
130
|
+
assert.equal(view.detail, noRecorder);
|
|
131
|
+
assert.equal(view.state, "unavailable");
|
|
132
|
+
assert.equal(view.showPlayer, false);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("the server's reason beats the reasons the wall could work out itself", () => {
|
|
136
|
+
const reason = "This camera has no address configured.";
|
|
137
|
+
assert.equal(tileView(ip({ status: "inactive", unavailableReason: reason })).detail, reason);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("an absent or blank reason changes nothing", () => {
|
|
141
|
+
assert.equal(tileView(ip({ unavailableReason: null })).state, "connecting");
|
|
142
|
+
assert.equal(tileView(ip({ unavailableReason: " " })).state, "connecting");
|
|
143
|
+
assert.equal(tileView(ip()).state, "connecting");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("being offline still explains every tile at once, ahead of the server's reason", () => {
|
|
147
|
+
const view = tileView(ip({ unavailableReason: "Something server-side." }), { online: false });
|
|
148
|
+
assert.equal(view.state, "offline");
|
|
149
|
+
});
|
package/utils/camera-wall.ts
CHANGED
|
@@ -51,6 +51,17 @@ export type TWallCamera = {
|
|
|
51
51
|
type?: string;
|
|
52
52
|
status?: string;
|
|
53
53
|
capabilities?: Record<string, TWallCapability>;
|
|
54
|
+
/**
|
|
55
|
+
* The server's own sentence for why this camera cannot show a picture, or
|
|
56
|
+
* `null`/absent when it can. Answered from the record before anything is
|
|
57
|
+
* requested, so it costs no device traffic.
|
|
58
|
+
*
|
|
59
|
+
* **Preferred over anything worked out here.** The server knows things the
|
|
60
|
+
* browser cannot — that no recorder is configured for this camera's relay, for
|
|
61
|
+
* instance, which is the true answer for every camera until the API host is
|
|
62
|
+
* configured. Replacing it with our own wording hides that.
|
|
63
|
+
*/
|
|
64
|
+
unavailableReason?: string | null;
|
|
54
65
|
};
|
|
55
66
|
|
|
56
67
|
/* -------------------------------------------------------------------------- */
|
|
@@ -251,6 +262,13 @@ export function tileView(
|
|
|
251
262
|
if (!camera) return dead("unavailable", "No camera in this position.");
|
|
252
263
|
if (!online)
|
|
253
264
|
return dead("offline", "This browser has no internet connection.");
|
|
265
|
+
// The server's refusal wins over anything reconstructed here. It covers the
|
|
266
|
+
// same two cases below AND the ones only it can know — chiefly "no recorder is
|
|
267
|
+
// configured for this camera's relay", which is the honest answer for every
|
|
268
|
+
// camera until the API host is configured, and which used to be replaced by
|
|
269
|
+
// our own guess.
|
|
270
|
+
const reason = camera.unavailableReason?.trim();
|
|
271
|
+
if (reason) return dead("unavailable", reason);
|
|
254
272
|
if (camera.status && camera.status !== "active")
|
|
255
273
|
return dead("unavailable", "This camera is not active.");
|
|
256
274
|
if (!camera.host) return dead("unavailable", "This camera has no address configured.");
|
package/middleware/member.ts
DELETED