@7365admin1/layer-common 3.2.0 → 3.2.1-staging.60
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 +625 -0
- package/components/CameraWallTile.vue +333 -0
- package/components/EntryPassInformation.vue +4 -4
- package/components/HidAccessLogDashboard.vue +53 -8
- package/components/HidIntercomManagement.vue +842 -201
- package/components/HidQrCodeConfiguration.vue +907 -95
- package/components/HidServiceSettingsPanel.vue +20 -1
- package/components/HidUserEnrollment.vue +96 -2
- package/components/Nfc/NFCPatrolRouteMain.vue +52 -7
- package/components/OnlineFormConfigurationForm.vue +620 -224
- package/components/OnlineFormFill.vue +36 -8
- package/components/OnlineFormsConfiguration.vue +6 -2
- package/components/QrTemplate/CreditCardLandscape.vue +19 -12
- package/components/QrTemplate/PrintDialog.vue +209 -196
- package/components/SiteSettings.vue +4 -2
- package/components/VisitorForm.vue +201 -78
- package/components/VisitorManagement.vue +0 -1
- package/composables/useHidAmico.ts +127 -0
- package/composables/useHidNavigation.ts +0 -7
- package/composables/useOnlineFormPages.ts +2 -0
- package/composables/useSipWebPhone.ts +332 -0
- package/composables/useSiteSettings.ts +28 -0
- package/composables/useWebUsb.ts +127 -37
- package/package.json +2 -1
- package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
- package/plugins/secure-member.client.ts +21 -56
- package/types/site.d.ts +71 -0
- package/utils/camera-wall.test.ts +149 -0
- package/utils/camera-wall.ts +299 -0
- package/components/HidIdentityMapping.vue +0 -975
- package/middleware/member.ts +0 -4
- package/pages/[org]/[site]/access-mgmt/identity-mapping/index.vue +0 -23
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
ref="root"
|
|
4
|
+
class="vms-tile"
|
|
5
|
+
:class="{ 'vms-tile--dense': dense, 'vms-tile--focused': focused, 'vms-tile--empty': !camera }"
|
|
6
|
+
:tabindex="camera ? 0 : -1"
|
|
7
|
+
:aria-label="camera ? `${camera.name || camera._id}, ${view.label}` : 'Empty position'"
|
|
8
|
+
@click="camera && $emit('focus', camera)"
|
|
9
|
+
@dblclick="camera && toggleFullscreen()"
|
|
10
|
+
@keydown.enter="camera && toggleFullscreen()"
|
|
11
|
+
>
|
|
12
|
+
<!--
|
|
13
|
+
The picture, when there is one to show. This is an embed of the video
|
|
14
|
+
service's own player page: the stored address IS the live view, and there
|
|
15
|
+
is no other transport for live web video today.
|
|
16
|
+
-->
|
|
17
|
+
<iframe
|
|
18
|
+
v-if="camera && view.showPlayer"
|
|
19
|
+
:key="camera.host"
|
|
20
|
+
:src="camera.host"
|
|
21
|
+
class="vms-tile__frame"
|
|
22
|
+
:title="`Live view of ${camera.name || 'camera'}`"
|
|
23
|
+
allowfullscreen
|
|
24
|
+
referrerpolicy="no-referrer"
|
|
25
|
+
@load="onPlayerLoad"
|
|
26
|
+
/>
|
|
27
|
+
|
|
28
|
+
<!--
|
|
29
|
+
A tile with nothing to show says so across the whole tile, not in a
|
|
30
|
+
corner. At nine tiles a supervisor is scanning, and a one-line reason
|
|
31
|
+
tucked under a black rectangle is exactly the thing that gets missed.
|
|
32
|
+
-->
|
|
33
|
+
<div v-if="view.detail" class="vms-tile__overlay">
|
|
34
|
+
<p class="vms-tile__reason">{{ view.detail }}</p>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<!-- Chrome sits OVER the picture, never around it. -->
|
|
38
|
+
<div v-if="camera" class="vms-tile__status">
|
|
39
|
+
<span class="vms-tile__dot" :class="`vms-tile__dot--${view.state}`" />
|
|
40
|
+
<!--
|
|
41
|
+
At nine tiles the badge is the DOT ALONE: a status word beside a 6 px
|
|
42
|
+
dot on a small tile is unreadable at the distance a wall is scanned
|
|
43
|
+
from, and the dot is what is being read anyway. The word returns at
|
|
44
|
+
four tiles and one.
|
|
45
|
+
-->
|
|
46
|
+
<span v-if="!dense" class="vms-tile__state">{{ view.label }}</span>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div v-if="camera" class="vms-tile__chrome">
|
|
50
|
+
<!-- The NAME wins the fight for room: it is the tile's entire job. -->
|
|
51
|
+
<span class="vms-tile__name">{{ camera.name || camera._id }}</span>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<button
|
|
55
|
+
v-if="camera"
|
|
56
|
+
type="button"
|
|
57
|
+
class="vms-tile__expand"
|
|
58
|
+
title="Fill the screen with this camera"
|
|
59
|
+
aria-label="Fill the screen with this camera"
|
|
60
|
+
@click.stop="toggleFullscreen()"
|
|
61
|
+
>
|
|
62
|
+
<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true">
|
|
63
|
+
<path
|
|
64
|
+
d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"
|
|
65
|
+
fill="none"
|
|
66
|
+
stroke="currentColor"
|
|
67
|
+
stroke-width="2"
|
|
68
|
+
stroke-linecap="square"
|
|
69
|
+
/>
|
|
70
|
+
</svg>
|
|
71
|
+
</button>
|
|
72
|
+
</div>
|
|
73
|
+
</template>
|
|
74
|
+
|
|
75
|
+
<script setup lang="ts">
|
|
76
|
+
import { computed, onBeforeUnmount, ref, watch } from "vue";
|
|
77
|
+
|
|
78
|
+
import {
|
|
79
|
+
LIVE_CONNECT_TIMEOUT_MS,
|
|
80
|
+
tileView,
|
|
81
|
+
type TPlayerState,
|
|
82
|
+
type TWallCamera,
|
|
83
|
+
} from "../utils/camera-wall";
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* ONE TILE OF THE CAMERA WALL.
|
|
87
|
+
*
|
|
88
|
+
* Everything this component decides comes from `utils/camera-wall.ts`; what is
|
|
89
|
+
* left here is the picture, the chrome over it, and the one thing only a
|
|
90
|
+
* browser can know — whether the player page ever loaded.
|
|
91
|
+
*
|
|
92
|
+
* ## The ceiling worth knowing before "improving" the live badge
|
|
93
|
+
*
|
|
94
|
+
* We can tell when the player PAGE loads. We cannot tell when a FRAME decodes:
|
|
95
|
+
* the page belongs to the video service, not to us, and learning more would
|
|
96
|
+
* mean reaching into another origin's document — which is both blocked by the
|
|
97
|
+
* browser and would break silently the day that page changes. So **"Live" means
|
|
98
|
+
* the player page is up**. If the service is up and the camera behind it is
|
|
99
|
+
* dead, the operator sees that service's own empty player, exactly as they do
|
|
100
|
+
* in the app they use today. Closing that honestly is a change on the video
|
|
101
|
+
* service's side, not here.
|
|
102
|
+
*/
|
|
103
|
+
|
|
104
|
+
const props = withDefaults(
|
|
105
|
+
defineProps<{
|
|
106
|
+
camera: TWallCamera | null;
|
|
107
|
+
/** Nine tiles: drop the status word, keep the dot. */
|
|
108
|
+
dense?: boolean;
|
|
109
|
+
/** Browser connectivity. One dead tile and a dead browser look identical otherwise. */
|
|
110
|
+
online?: boolean;
|
|
111
|
+
focused?: boolean;
|
|
112
|
+
}>(),
|
|
113
|
+
{ dense: false, online: true, focused: false }
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
defineEmits<{ (e: "focus", camera: TWallCamera): void }>();
|
|
117
|
+
|
|
118
|
+
const root = ref<HTMLElement | null>(null);
|
|
119
|
+
const player = ref<TPlayerState>("loading");
|
|
120
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
121
|
+
|
|
122
|
+
const view = computed(() =>
|
|
123
|
+
tileView(props.camera, { online: props.online, player: player.value })
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
function onPlayerLoad() {
|
|
127
|
+
player.value = "ready";
|
|
128
|
+
clearTimer();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function clearTimer() {
|
|
132
|
+
if (timer) {
|
|
133
|
+
clearTimeout(timer);
|
|
134
|
+
timer = null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Restart the clock whenever the address changes — a tile reused for a
|
|
140
|
+
* different camera must not inherit the previous one's verdict, in either
|
|
141
|
+
* direction.
|
|
142
|
+
*/
|
|
143
|
+
watch(
|
|
144
|
+
() => props.camera?.host,
|
|
145
|
+
(host) => {
|
|
146
|
+
clearTimer();
|
|
147
|
+
player.value = "loading";
|
|
148
|
+
if (!host) return;
|
|
149
|
+
timer = setTimeout(() => {
|
|
150
|
+
if (player.value === "loading") player.value = "timeout";
|
|
151
|
+
}, LIVE_CONNECT_TIMEOUT_MS);
|
|
152
|
+
},
|
|
153
|
+
{ immediate: true }
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
onBeforeUnmount(clearTimer);
|
|
157
|
+
|
|
158
|
+
/** The browser's own fullscreen. Nothing to build, and it survives the F11 key. */
|
|
159
|
+
async function toggleFullscreen() {
|
|
160
|
+
const el = root.value;
|
|
161
|
+
if (!el) return;
|
|
162
|
+
try {
|
|
163
|
+
if (document.fullscreenElement) await document.exitFullscreen();
|
|
164
|
+
else await el.requestFullscreen();
|
|
165
|
+
} catch {
|
|
166
|
+
// A browser that refuses fullscreen (permissions policy, an iframe, an old
|
|
167
|
+
// Safari) is not an error worth interrupting a supervisor over — the tile
|
|
168
|
+
// keeps working exactly as it did.
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
</script>
|
|
172
|
+
|
|
173
|
+
<style scoped>
|
|
174
|
+
.vms-tile {
|
|
175
|
+
position: relative;
|
|
176
|
+
overflow: hidden;
|
|
177
|
+
background: var(--vms-tile, #101418);
|
|
178
|
+
border-radius: var(--vms-radius, 3px);
|
|
179
|
+
outline: none;
|
|
180
|
+
min-height: 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.vms-tile--focused {
|
|
184
|
+
box-shadow: inset 0 0 0 2px var(--vms-accent, #5b9be0);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.vms-tile:focus-visible {
|
|
188
|
+
box-shadow: inset 0 0 0 2px var(--vms-accent, #5b9be0);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.vms-tile--empty {
|
|
192
|
+
background: transparent;
|
|
193
|
+
border: 1px dashed var(--vms-line, #232b36);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.vms-tile__frame {
|
|
197
|
+
width: 100%;
|
|
198
|
+
height: 100%;
|
|
199
|
+
border: none;
|
|
200
|
+
display: block;
|
|
201
|
+
background: #000;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.vms-tile__overlay {
|
|
205
|
+
position: absolute;
|
|
206
|
+
inset: 0;
|
|
207
|
+
display: flex;
|
|
208
|
+
align-items: center;
|
|
209
|
+
justify-content: center;
|
|
210
|
+
padding: 10px;
|
|
211
|
+
background: var(--vms-scrim-solid, rgba(7, 9, 12, 0.82));
|
|
212
|
+
pointer-events: none;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.vms-tile__reason {
|
|
216
|
+
margin: 0;
|
|
217
|
+
text-align: center;
|
|
218
|
+
color: var(--vms-text-dim, #94a0b0);
|
|
219
|
+
font-size: 12px;
|
|
220
|
+
line-height: 16px;
|
|
221
|
+
max-width: 32ch;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.vms-tile--dense .vms-tile__reason {
|
|
225
|
+
font-size: 11px;
|
|
226
|
+
line-height: 15px;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.vms-tile__status {
|
|
230
|
+
position: absolute;
|
|
231
|
+
top: 6px;
|
|
232
|
+
right: 6px;
|
|
233
|
+
display: flex;
|
|
234
|
+
align-items: center;
|
|
235
|
+
gap: 5px;
|
|
236
|
+
padding: 2px 6px;
|
|
237
|
+
border-radius: var(--vms-radius, 3px);
|
|
238
|
+
background: var(--vms-scrim, rgba(7, 9, 12, 0.62));
|
|
239
|
+
pointer-events: none;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.vms-tile__dot {
|
|
243
|
+
width: 7px;
|
|
244
|
+
height: 7px;
|
|
245
|
+
border-radius: 50%;
|
|
246
|
+
background: var(--vms-text-faint, #5c6675);
|
|
247
|
+
flex: none;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/* State colours only. The chrome itself stays achromatic on purpose. */
|
|
251
|
+
.vms-tile__dot--live {
|
|
252
|
+
background: #4caf50;
|
|
253
|
+
}
|
|
254
|
+
.vms-tile__dot--connecting {
|
|
255
|
+
background: #fb8c00;
|
|
256
|
+
}
|
|
257
|
+
.vms-tile__dot--no-signal,
|
|
258
|
+
.vms-tile__dot--offline {
|
|
259
|
+
background: #e0241c;
|
|
260
|
+
}
|
|
261
|
+
.vms-tile__dot--unavailable {
|
|
262
|
+
background: var(--vms-text-faint, #5c6675);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.vms-tile__state {
|
|
266
|
+
color: var(--vms-text, #e8ecf1);
|
|
267
|
+
font-size: 11px;
|
|
268
|
+
line-height: 14px;
|
|
269
|
+
white-space: nowrap;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.vms-tile__chrome {
|
|
273
|
+
position: absolute;
|
|
274
|
+
left: 6px;
|
|
275
|
+
right: 6px;
|
|
276
|
+
bottom: 5px;
|
|
277
|
+
display: flex;
|
|
278
|
+
pointer-events: none;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.vms-tile__name {
|
|
282
|
+
color: var(--vms-text, #e8ecf1);
|
|
283
|
+
font-size: 12px;
|
|
284
|
+
line-height: 16px;
|
|
285
|
+
font-weight: 600;
|
|
286
|
+
padding: 1px 6px;
|
|
287
|
+
border-radius: var(--vms-radius, 3px);
|
|
288
|
+
background: var(--vms-scrim, rgba(7, 9, 12, 0.62));
|
|
289
|
+
overflow: hidden;
|
|
290
|
+
text-overflow: ellipsis;
|
|
291
|
+
white-space: nowrap;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.vms-tile--dense .vms-tile__name {
|
|
295
|
+
font-size: 11px;
|
|
296
|
+
line-height: 15px;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.vms-tile__expand {
|
|
300
|
+
position: absolute;
|
|
301
|
+
top: 6px;
|
|
302
|
+
left: 6px;
|
|
303
|
+
display: flex;
|
|
304
|
+
align-items: center;
|
|
305
|
+
justify-content: center;
|
|
306
|
+
width: 24px;
|
|
307
|
+
height: 24px;
|
|
308
|
+
border-radius: var(--vms-radius, 3px);
|
|
309
|
+
background: var(--vms-scrim, rgba(7, 9, 12, 0.62));
|
|
310
|
+
color: var(--vms-text, #e8ecf1);
|
|
311
|
+
border: none;
|
|
312
|
+
cursor: pointer;
|
|
313
|
+
opacity: 0;
|
|
314
|
+
transition: opacity 120ms ease;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/* The control is always THERE; it only stops competing with the picture. */
|
|
318
|
+
.vms-tile:hover .vms-tile__expand,
|
|
319
|
+
.vms-tile:focus-within .vms-tile__expand {
|
|
320
|
+
opacity: 1;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.vms-tile__expand:focus-visible {
|
|
324
|
+
opacity: 1;
|
|
325
|
+
outline: 2px solid var(--vms-accent, #5b9be0);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/* Fullscreen: the picture takes the whole screen, the chrome stays legible. */
|
|
329
|
+
.vms-tile:fullscreen {
|
|
330
|
+
border-radius: 0;
|
|
331
|
+
background: #000;
|
|
332
|
+
}
|
|
333
|
+
</style>
|
|
@@ -168,13 +168,13 @@
|
|
|
168
168
|
<template v-if="passType === 'HID_QR'">
|
|
169
169
|
<v-alert
|
|
170
170
|
v-if="!hidQrPrinterConfigured"
|
|
171
|
-
type="
|
|
171
|
+
type="info"
|
|
172
172
|
variant="tonal"
|
|
173
173
|
density="compact"
|
|
174
174
|
class="mt-3"
|
|
175
|
-
icon="mdi-
|
|
175
|
+
icon="mdi-qrcode"
|
|
176
176
|
>
|
|
177
|
-
|
|
177
|
+
HID QR Code will be generated and shown on screen. You can download it after continuing.
|
|
178
178
|
</v-alert>
|
|
179
179
|
<v-alert
|
|
180
180
|
v-else
|
|
@@ -184,7 +184,7 @@
|
|
|
184
184
|
class="mt-3"
|
|
185
185
|
icon="mdi-qrcode-scan"
|
|
186
186
|
>
|
|
187
|
-
HID QR Code will be generated and
|
|
187
|
+
HID QR Code will be generated and shown on screen. Printing is also available.
|
|
188
188
|
</v-alert>
|
|
189
189
|
</template>
|
|
190
190
|
</template>
|
|
@@ -418,8 +418,17 @@ const selectedHistoryRows = computed<AccessRow[]>(() => {
|
|
|
418
418
|
if (!selectedHistoryRow.value) return [];
|
|
419
419
|
|
|
420
420
|
const selectedKey = selectedHistoryRow.value.identityKey;
|
|
421
|
-
const
|
|
422
|
-
const
|
|
421
|
+
const selectedIdentity = findIdentityByKey(selectedKey);
|
|
422
|
+
const matchedLogs = logs.value.filter((log) =>
|
|
423
|
+
selectedIdentity
|
|
424
|
+
? accessLogMatchesIdentity(log, selectedIdentity)
|
|
425
|
+
: getLogIdentityKey(log) === selectedKey,
|
|
426
|
+
);
|
|
427
|
+
const matchedLiveLogs = liveAccessLogs.value.filter((log) =>
|
|
428
|
+
selectedIdentity
|
|
429
|
+
? accessLogMatchesIdentity(log, selectedIdentity)
|
|
430
|
+
: getAccessLogIdentityKey(log) === selectedKey,
|
|
431
|
+
);
|
|
423
432
|
|
|
424
433
|
if (!matchedLogs.length && !matchedLiveLogs.length) {
|
|
425
434
|
return rows.value.filter((row) => row.identityKey === selectedKey);
|
|
@@ -835,7 +844,7 @@ function getIdentityKey(identity: Record<string, any>) {
|
|
|
835
844
|
return [
|
|
836
845
|
identity.hidUserId,
|
|
837
846
|
identity.registration,
|
|
838
|
-
identity.cardNo,
|
|
847
|
+
identity.cardNo || identity.metadata?.qrCodeValue,
|
|
839
848
|
].filter(Boolean).join("|");
|
|
840
849
|
}
|
|
841
850
|
|
|
@@ -851,7 +860,7 @@ function getLogIdentityKey(log: Record<string, any>) {
|
|
|
851
860
|
return [
|
|
852
861
|
identity.hidUserId || lookup.hidUserId || rawLog.user_id || rawLog.userId || rawLog.hidUserId,
|
|
853
862
|
identity.registration || lookup.registration,
|
|
854
|
-
identity.cardNo || lookup.cardNo || rawLog.card_value || rawLog.cardNo,
|
|
863
|
+
identity.cardNo || lookup.cardNo || rawLog.card_value || rawLog.cardNo || rawLog.qrcode_value,
|
|
855
864
|
].filter(Boolean).join("|");
|
|
856
865
|
}
|
|
857
866
|
|
|
@@ -1173,25 +1182,26 @@ function findIdentityForAccessLog(rawLog: Record<string, any>, event: Record<str
|
|
|
1173
1182
|
const hidUserId = rawLog.user_id ?? rawLog.userId ?? rawLog.hidUserId ?? event.payload?.identity?.hidUserId ?? event.payload?.identityLookup?.hidUserId;
|
|
1174
1183
|
const registration = rawLog.registration ?? event.payload?.identity?.registration ?? event.payload?.identityLookup?.registration;
|
|
1175
1184
|
const cardNo = rawLog.card_value || rawLog.cardNo || event.payload?.identity?.cardNo || event.payload?.identityLookup?.cardNo;
|
|
1185
|
+
const qrCodeValue = rawLog.qrcode_value || rawLog.qrCode || rawLog.qrcode;
|
|
1176
1186
|
|
|
1177
1187
|
return identities.value.find((identity) => {
|
|
1178
1188
|
return (
|
|
1179
1189
|
(hidUserId !== undefined && hidUserId !== null && String(identity.hidUserId) === String(hidUserId)) ||
|
|
1180
1190
|
(registration && identity.registration === registration) ||
|
|
1181
|
-
(cardNo && String(identity.cardNo) === String(cardNo))
|
|
1191
|
+
(cardNo && String(identity.cardNo) === String(cardNo)) ||
|
|
1192
|
+
(qrCodeValue && String(identity.metadata?.qrCodeValue || "") === String(qrCodeValue))
|
|
1182
1193
|
);
|
|
1183
1194
|
});
|
|
1184
1195
|
}
|
|
1185
1196
|
|
|
1186
1197
|
function getLatestAccessLogForIdentity(identity: Record<string, any>) {
|
|
1187
|
-
const identityKey = getIdentityKey(identity);
|
|
1188
1198
|
const accessLogs = [
|
|
1189
1199
|
...liveAccessLogs.value,
|
|
1190
1200
|
...logs.value.flatMap((event) => getRawAccessLogs(event)),
|
|
1191
1201
|
];
|
|
1192
1202
|
|
|
1193
1203
|
return accessLogs
|
|
1194
|
-
.filter((log) =>
|
|
1204
|
+
.filter((log) => accessLogMatchesIdentity(log, identity))
|
|
1195
1205
|
.sort((a, b) => {
|
|
1196
1206
|
const aTime = parseDate(getAccessLogTime(a, {}))?.getTime() || 0;
|
|
1197
1207
|
const bTime = parseDate(getAccessLogTime(b, {}))?.getTime() || 0;
|
|
@@ -1199,11 +1209,46 @@ function getLatestAccessLogForIdentity(identity: Record<string, any>) {
|
|
|
1199
1209
|
})[0];
|
|
1200
1210
|
}
|
|
1201
1211
|
|
|
1212
|
+
function toUnknownRecord(value: unknown): Record<string, unknown> {
|
|
1213
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
1214
|
+
? value as Record<string, unknown>
|
|
1215
|
+
: {};
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
function accessLogMatchesIdentity(log: unknown, identity: unknown) {
|
|
1219
|
+
const logRecord = toUnknownRecord(log);
|
|
1220
|
+
const identityRecord = toUnknownRecord(identity);
|
|
1221
|
+
const payload = toUnknownRecord(logRecord.payload);
|
|
1222
|
+
const rawLog = Object.keys(toUnknownRecord(payload.rawAccessLog)).length
|
|
1223
|
+
? toUnknownRecord(payload.rawAccessLog)
|
|
1224
|
+
: logRecord;
|
|
1225
|
+
const eventIdentity = Object.keys(toUnknownRecord(payload.identity)).length
|
|
1226
|
+
? toUnknownRecord(payload.identity)
|
|
1227
|
+
: toUnknownRecord(payload.identityLookup);
|
|
1228
|
+
const metadata = toUnknownRecord(identityRecord.metadata);
|
|
1229
|
+
const hidUserId = rawLog.user_id ?? rawLog.userId ?? rawLog.hidUserId ?? eventIdentity.hidUserId;
|
|
1230
|
+
const registration = rawLog.registration || eventIdentity.registration;
|
|
1231
|
+
const credential =
|
|
1232
|
+
rawLog.card_value ||
|
|
1233
|
+
rawLog.cardNo ||
|
|
1234
|
+
rawLog.qrcode_value ||
|
|
1235
|
+
rawLog.qrCode ||
|
|
1236
|
+
rawLog.qrcode ||
|
|
1237
|
+
eventIdentity.cardNo;
|
|
1238
|
+
const identityCredential = identityRecord.cardNo || metadata.qrCodeValue;
|
|
1239
|
+
|
|
1240
|
+
return Boolean(
|
|
1241
|
+
(hidUserId !== undefined && hidUserId !== null && String(identityRecord.hidUserId) === String(hidUserId)) ||
|
|
1242
|
+
(registration && identityRecord.registration === registration) ||
|
|
1243
|
+
(credential && identityCredential && String(identityCredential) === String(credential)),
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1202
1247
|
function getAccessLogIdentityKey(rawLog: Record<string, any>) {
|
|
1203
1248
|
return [
|
|
1204
1249
|
rawLog.user_id ?? rawLog.userId ?? rawLog.hidUserId,
|
|
1205
1250
|
rawLog.registration,
|
|
1206
|
-
rawLog.card_value || rawLog.cardNo,
|
|
1251
|
+
rawLog.card_value || rawLog.cardNo || rawLog.qrcode_value,
|
|
1207
1252
|
].filter(Boolean).join("|");
|
|
1208
1253
|
}
|
|
1209
1254
|
|