@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.
@@ -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: (error) => {
63
- connectionState.value = error ? "error" : "disconnected";
64
- if (error) errorMessage.value = error.message;
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
- if (muted.value) {
163
- user.unmute();
164
- } else {
165
- user.mute();
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
@@ -2,7 +2,7 @@
2
2
  "name": "@7365admin1/layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "3.1.4-staging.56",
5
+ "version": "3.1.4-staging.58",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "publishConfig": {
@@ -5,7 +5,7 @@
5
5
  :org="orgId"
6
6
  message="Enable HID as a service for this site before configuring HID intercom."
7
7
  >
8
- <HidIntercomManagement :site="siteId" />
8
+ <HidIntercomManagement :site="siteId" :org="orgId" />
9
9
  </HidEnabledGate>
10
10
  </v-container>
11
11
  </template>
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;