@7365admin1/layer-common 3.1.4-staging.56 → 3.1.4-staging.58
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/components/HidIntercomManagement.vue +286 -92
- 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/useSipWebPhone.ts +105 -9
- package/package.json +1 -1
- package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
- package/types/site.d.ts +6 -0
|
@@ -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,
|
|
@@ -25,6 +25,9 @@ export default function useSipWebPhone() {
|
|
|
25
25
|
const callState = ref<CallState>("idle");
|
|
26
26
|
const errorMessage = ref("");
|
|
27
27
|
const muted = ref(false);
|
|
28
|
+
const cameraEnabled = ref(false);
|
|
29
|
+
const localVideoAvailable = ref(false);
|
|
30
|
+
const remoteVideoAvailable = ref(false);
|
|
28
31
|
const currentTarget = ref("");
|
|
29
32
|
const activeConfig = shallowRef<SipWebPhoneConfig>();
|
|
30
33
|
|
|
@@ -38,9 +41,30 @@ export default function useSipWebPhone() {
|
|
|
38
41
|
connectionState.value = "connecting";
|
|
39
42
|
errorMessage.value = "";
|
|
40
43
|
activeConfig.value = { ...config };
|
|
44
|
+
let clearRegistrationTimer = () => {};
|
|
41
45
|
|
|
42
46
|
try {
|
|
43
47
|
const { SimpleUser: SimpleUserConstructor } = await import("sip.js/lib/platform/web");
|
|
48
|
+
let registrationSettled = false;
|
|
49
|
+
let resolveRegistration: (() => void) | undefined;
|
|
50
|
+
let rejectRegistration: ((error: Error) => void) | undefined;
|
|
51
|
+
const registrationPromise = new Promise<void>((resolve, reject) => {
|
|
52
|
+
resolveRegistration = resolve;
|
|
53
|
+
rejectRegistration = reject;
|
|
54
|
+
});
|
|
55
|
+
const registrationTimeout = globalThis.setTimeout(() => {
|
|
56
|
+
if (registrationSettled) return;
|
|
57
|
+
registrationSettled = true;
|
|
58
|
+
rejectRegistration?.(new Error("SIP registration timed out. Check the WebSocket server and account credentials."));
|
|
59
|
+
}, 15000);
|
|
60
|
+
clearRegistrationTimer = () => globalThis.clearTimeout(registrationTimeout);
|
|
61
|
+
const settleRegistration = (error?: Error) => {
|
|
62
|
+
if (registrationSettled) return;
|
|
63
|
+
registrationSettled = true;
|
|
64
|
+
clearRegistrationTimer();
|
|
65
|
+
if (error) rejectRegistration?.(error);
|
|
66
|
+
else resolveRegistration?.();
|
|
67
|
+
};
|
|
44
68
|
const delegate: SimpleUserDelegate = {
|
|
45
69
|
onCallCreated: () => {
|
|
46
70
|
if (callState.value !== "incoming") callState.value = "calling";
|
|
@@ -51,17 +75,30 @@ export default function useSipWebPhone() {
|
|
|
51
75
|
},
|
|
52
76
|
onCallAnswered: () => {
|
|
53
77
|
callState.value = "active";
|
|
78
|
+
syncVideoState();
|
|
79
|
+
},
|
|
80
|
+
onCallHangup: () => {
|
|
81
|
+
const previousState = callState.value;
|
|
82
|
+
resetCall();
|
|
83
|
+
if (previousState === "calling") {
|
|
84
|
+
errorMessage.value = "The call was not answered or was rejected by the destination.";
|
|
85
|
+
}
|
|
54
86
|
},
|
|
55
|
-
onCallHangup: () => resetCall(),
|
|
56
87
|
onRegistered: () => {
|
|
57
88
|
connectionState.value = "registered";
|
|
89
|
+
settleRegistration();
|
|
58
90
|
},
|
|
59
91
|
onUnregistered: () => {
|
|
92
|
+
if (connectionState.value === "connecting") {
|
|
93
|
+
settleRegistration(new Error("SIP registration was rejected. Check the username and password."));
|
|
94
|
+
}
|
|
60
95
|
connectionState.value = "disconnected";
|
|
61
96
|
},
|
|
62
|
-
onServerDisconnect: (
|
|
63
|
-
|
|
64
|
-
|
|
97
|
+
onServerDisconnect: () => {
|
|
98
|
+
const error = new Error("The SIP WebSocket server disconnected during registration.");
|
|
99
|
+
connectionState.value = "error";
|
|
100
|
+
errorMessage.value = error.message;
|
|
101
|
+
settleRegistration(error);
|
|
65
102
|
resetCall();
|
|
66
103
|
},
|
|
67
104
|
};
|
|
@@ -87,7 +124,9 @@ export default function useSipWebPhone() {
|
|
|
87
124
|
simpleUser.value = user;
|
|
88
125
|
await user.connect();
|
|
89
126
|
await user.register();
|
|
127
|
+
await registrationPromise;
|
|
90
128
|
} catch (error) {
|
|
129
|
+
clearRegistrationTimer();
|
|
91
130
|
await disconnect(false);
|
|
92
131
|
connectionState.value = "error";
|
|
93
132
|
errorMessage.value = getErrorMessage(error);
|
|
@@ -159,18 +198,65 @@ export default function useSipWebPhone() {
|
|
|
159
198
|
|
|
160
199
|
function toggleMute() {
|
|
161
200
|
const user = requireUser();
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
201
|
+
const tracks = user.localMediaStream?.getAudioTracks() || [];
|
|
202
|
+
if (!tracks.length) throw new Error("No microphone track is available for this call.");
|
|
203
|
+
const enable = muted.value;
|
|
204
|
+
tracks.forEach((track) => {
|
|
205
|
+
track.enabled = enable;
|
|
206
|
+
});
|
|
207
|
+
muted.value = !enable;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function toggleCamera() {
|
|
211
|
+
const user = requireUser();
|
|
212
|
+
const tracks = user.localMediaStream?.getVideoTracks() || [];
|
|
213
|
+
if (!tracks.length) throw new Error("No camera track is available. Reconnect the web phone with video enabled.");
|
|
214
|
+
const enable = !cameraEnabled.value;
|
|
215
|
+
tracks.forEach((track) => {
|
|
216
|
+
track.enabled = enable;
|
|
217
|
+
});
|
|
218
|
+
cameraEnabled.value = enable;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function syncVideoState() {
|
|
222
|
+
const user = simpleUser.value;
|
|
223
|
+
const localStream = user?.localMediaStream;
|
|
224
|
+
const remoteStream = user?.remoteMediaStream;
|
|
225
|
+
const localTrack = localStream?.getVideoTracks()[0];
|
|
226
|
+
const remoteTrack = remoteStream?.getVideoTracks()[0];
|
|
227
|
+
|
|
228
|
+
const updateState = () => {
|
|
229
|
+
const currentLocalTrack = localStream?.getVideoTracks()[0];
|
|
230
|
+
const currentRemoteTrack = remoteStream?.getVideoTracks()[0];
|
|
231
|
+
localVideoAvailable.value = Boolean(currentLocalTrack && currentLocalTrack.readyState === "live");
|
|
232
|
+
remoteVideoAvailable.value = Boolean(currentRemoteTrack && currentRemoteTrack.readyState === "live");
|
|
233
|
+
cameraEnabled.value = Boolean(currentLocalTrack?.enabled);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
updateState();
|
|
237
|
+
if (localStream) localStream.onaddtrack = updateState;
|
|
238
|
+
if (remoteStream) remoteStream.onaddtrack = updateState;
|
|
239
|
+
|
|
240
|
+
if (localTrack) {
|
|
241
|
+
localTrack.onended = () => {
|
|
242
|
+
localVideoAvailable.value = false;
|
|
243
|
+
cameraEnabled.value = false;
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
if (remoteTrack) {
|
|
247
|
+
remoteTrack.onended = () => {
|
|
248
|
+
remoteVideoAvailable.value = false;
|
|
249
|
+
};
|
|
166
250
|
}
|
|
167
|
-
muted.value = user.isMuted();
|
|
168
251
|
}
|
|
169
252
|
|
|
170
253
|
function resetCall() {
|
|
171
254
|
callState.value = "idle";
|
|
172
255
|
currentTarget.value = "";
|
|
173
256
|
muted.value = false;
|
|
257
|
+
cameraEnabled.value = false;
|
|
258
|
+
localVideoAvailable.value = false;
|
|
259
|
+
remoteVideoAvailable.value = false;
|
|
174
260
|
}
|
|
175
261
|
|
|
176
262
|
function requireRegisteredUser() {
|
|
@@ -193,6 +279,9 @@ export default function useSipWebPhone() {
|
|
|
193
279
|
callState: readonly(callState),
|
|
194
280
|
errorMessage: readonly(errorMessage),
|
|
195
281
|
muted: readonly(muted),
|
|
282
|
+
cameraEnabled: readonly(cameraEnabled),
|
|
283
|
+
localVideoAvailable: readonly(localVideoAvailable),
|
|
284
|
+
remoteVideoAvailable: readonly(remoteVideoAvailable),
|
|
196
285
|
currentTarget: readonly(currentTarget),
|
|
197
286
|
isRegistered,
|
|
198
287
|
hasCall,
|
|
@@ -203,6 +292,7 @@ export default function useSipWebPhone() {
|
|
|
203
292
|
decline,
|
|
204
293
|
hangup,
|
|
205
294
|
toggleMute,
|
|
295
|
+
toggleCamera,
|
|
206
296
|
};
|
|
207
297
|
}
|
|
208
298
|
|
|
@@ -232,5 +322,11 @@ function normalizeDestination(target: string, aor: string) {
|
|
|
232
322
|
}
|
|
233
323
|
|
|
234
324
|
function getErrorMessage(error: unknown) {
|
|
325
|
+
if (error instanceof DOMException && error.name === "NotAllowedError") {
|
|
326
|
+
return "Camera or microphone permission was denied. Allow media access in the browser and reconnect the web phone.";
|
|
327
|
+
}
|
|
328
|
+
if (error instanceof DOMException && error.name === "NotFoundError") {
|
|
329
|
+
return "No camera or microphone was found for this video call.";
|
|
330
|
+
}
|
|
235
331
|
return error instanceof Error ? error.message : "SIP connection failed.";
|
|
236
332
|
}
|
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;
|