@7365admin1/layer-common 4.149.1 → 4.150.0
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/CHANGELOG.md +16 -4
- package/components/EntryPassInformation.vue +13 -0
- package/components/HidAccessPermissions.vue +202 -40
- package/components/HidEnabledGate.vue +29 -10
- package/components/HidIntercomManagement.vue +26 -13
- package/components/HidProfileQrCode.vue +6 -2
- package/components/HidQrCodeConfiguration.vue +2 -2
- package/components/HidReaderForm.vue +239 -1
- package/components/HidReaderManagement.vue +26 -8
- package/components/HidReaderUserRoster.vue +3 -1
- package/components/HidServiceSettingsPanel.vue +2 -2
- package/components/HidUserEnrollment.vue +1662 -162
- package/components/MemberMain.vue +1 -1
- package/components/OvernightParkingAvailability.vue +123 -11
- package/components/SiteSettings.vue +5 -2
- package/components/VisitorForm.vue +39 -11
- package/composables/useCleaningPermission.ts +11 -0
- package/composables/useHidAccessPermission.ts +176 -81
- package/composables/useHidAmico.ts +37 -0
- package/composables/useHidNavigation.ts +44 -23
- package/composables/useLandscapingPermission.ts +11 -0
- package/composables/useMechanicalElectricalPermission.ts +5 -0
- package/composables/usePestControlPermission.ts +11 -0
- package/composables/usePoolMaintenancePermission.ts +11 -0
- package/composables/usePropertyPermission.ts +31 -7
- package/composables/useSecurityPermission.ts +33 -13
- package/composables/useServiceProvider.ts +9 -4
- package/constants/permissions.ts +73 -21
- package/package.json +1 -1
- package/pages/[org]/[site]/access-mgmt/hid-qr-code/index.vue +3 -0
- package/types/overnight-parking.d.ts +14 -0
- package/types/site.d.ts +7 -1
- package/utils/hid-enrolment-subject.ts +226 -4
- package/utils/hid-permission-assignments.ts +75 -6
- package/utils/hid-qr-validity.ts +25 -0
- package/utils/hid-reader-audiences.ts +557 -0
- package/utils/hid-registration.ts +24 -0
- package/utils/module-routes.ts +15 -2
- package/utils/nav-sections.ts +1 -1
|
@@ -6,17 +6,67 @@
|
|
|
6
6
|
>
|
|
7
7
|
<v-row no-gutters>
|
|
8
8
|
<v-col cols="12">
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
<!-- Which QR code types a resident's visitor invitation can carry. -->
|
|
10
|
+
<template v-for="option in visitorQrCodes" :key="option.type">
|
|
11
|
+
<div class="d-flex align-center justify-space-between py-3">
|
|
12
|
+
<div>
|
|
13
|
+
<div class="text-body-2 font-weight-medium">{{ visitorQrLabels[option.type].title }}</div>
|
|
14
|
+
<p class="text-caption text-medium-emphasis mt-1 mb-0">
|
|
15
|
+
{{ visitorQrLabels[option.type].description }}
|
|
16
|
+
</p>
|
|
17
|
+
</div>
|
|
12
18
|
<v-switch
|
|
13
|
-
v-model="
|
|
14
|
-
hide-details
|
|
19
|
+
v-model="option.isEnabled"
|
|
15
20
|
color="success"
|
|
16
|
-
|
|
21
|
+
inset
|
|
22
|
+
density="compact"
|
|
23
|
+
hide-details
|
|
24
|
+
class="flex-grow-0 ml-4"
|
|
17
25
|
/>
|
|
18
|
-
</
|
|
19
|
-
|
|
26
|
+
</div>
|
|
27
|
+
<!-- How long an HID visitor pass lasts. Only asked once HID is on. -->
|
|
28
|
+
<v-radio-group
|
|
29
|
+
v-if="option.type === 'hid' && option.isEnabled"
|
|
30
|
+
v-model="option.usage"
|
|
31
|
+
inline
|
|
32
|
+
hide-details
|
|
33
|
+
density="compact"
|
|
34
|
+
color="success"
|
|
35
|
+
class="pb-3"
|
|
36
|
+
>
|
|
37
|
+
<v-radio
|
|
38
|
+
v-for="usage in hidQrUsageOptions"
|
|
39
|
+
:key="usage.value"
|
|
40
|
+
:value="usage.value"
|
|
41
|
+
class="mr-6"
|
|
42
|
+
>
|
|
43
|
+
<template #label>
|
|
44
|
+
<div>
|
|
45
|
+
<div class="text-body-2">{{ usage.title }}</div>
|
|
46
|
+
<div class="text-caption text-medium-emphasis">{{ usage.description }}</div>
|
|
47
|
+
</div>
|
|
48
|
+
</template>
|
|
49
|
+
</v-radio>
|
|
50
|
+
</v-radio-group>
|
|
51
|
+
<v-divider />
|
|
52
|
+
</template>
|
|
53
|
+
<div class="d-flex align-center justify-space-between py-3">
|
|
54
|
+
<div>
|
|
55
|
+
<div class="text-body-2 font-weight-medium">Allow Opening Barrier via Plate Number?</div>
|
|
56
|
+
<p class="text-caption text-medium-emphasis mt-1 mb-0">
|
|
57
|
+
When enabled, the barrier can be opened for visitors by matching their vehicle plate number.
|
|
58
|
+
</p>
|
|
59
|
+
</div>
|
|
60
|
+
<v-switch
|
|
61
|
+
v-model="isAllowPlateNumberOpenBarrier"
|
|
62
|
+
color="success"
|
|
63
|
+
inset
|
|
64
|
+
density="compact"
|
|
65
|
+
hide-details
|
|
66
|
+
class="flex-grow-0 ml-4"
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
<v-divider class="mb-4" />
|
|
20
70
|
<v-row
|
|
21
71
|
no-gutters
|
|
22
72
|
class="d-flex align-center ga-2 justify-space-between"
|
|
@@ -220,6 +270,41 @@ const isAllowPlateNumberOpenBarrier = ref(false);
|
|
|
220
270
|
const autoApproveOvernightParking = ref(false);
|
|
221
271
|
const isHydrating = ref(true);
|
|
222
272
|
|
|
273
|
+
const visitorQrLabels: Record<TVisitorQrCodeType, { title: string; description: string }> = {
|
|
274
|
+
hid: {
|
|
275
|
+
title: "HID QR Code Visitor Invitation",
|
|
276
|
+
description: "When enabled, HID QR codes will be generated for visitor invitations instead of standard entry pass QR codes.",
|
|
277
|
+
},
|
|
278
|
+
entryPass: {
|
|
279
|
+
title: "Entry Pass QR Code Visitor Invitation",
|
|
280
|
+
description: "When enabled, entry pass QR codes will be generated for visitor invitations.",
|
|
281
|
+
},
|
|
282
|
+
security: {
|
|
283
|
+
title: "Security QR Code Visitor Invitation",
|
|
284
|
+
description: "When enabled, security QR codes will be generated for visitor invitations.",
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
// Whole day until the end of the visit day, or one entry and then it expires.
|
|
288
|
+
// Sites that saved before this existed have none, and read as whole day.
|
|
289
|
+
const DEFAULT_HID_QR_USAGE: TVisitorQrUsage = "wholeDay";
|
|
290
|
+
const hidQrUsageOptions: { value: TVisitorQrUsage; title: string; description: string }[] = [
|
|
291
|
+
{
|
|
292
|
+
value: "wholeDay",
|
|
293
|
+
title: "Whole day usage",
|
|
294
|
+
description: "The visitor can use the QR code as many times as needed until the end of the visit day.",
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
value: "singleUse",
|
|
298
|
+
title: "Single usage",
|
|
299
|
+
description: "The QR code expires after the visitor's first entry.",
|
|
300
|
+
},
|
|
301
|
+
];
|
|
302
|
+
const visitorQrCodes = ref<TVisitorQrCode[]>([
|
|
303
|
+
{ type: "hid", isEnabled: false, usage: DEFAULT_HID_QR_USAGE },
|
|
304
|
+
{ type: "security", isEnabled: false },
|
|
305
|
+
{ type: "entryPass", isEnabled: false },
|
|
306
|
+
]);
|
|
307
|
+
|
|
223
308
|
const { requiredRule } = useUtils();
|
|
224
309
|
const { generateTimeSlots, generateTimeSlotsFromStart } = useFacilityUtils();
|
|
225
310
|
const { currentUser } = useLocalAuth();
|
|
@@ -290,6 +375,16 @@ watch(
|
|
|
290
375
|
autoApproveOvernightParking.value =
|
|
291
376
|
data?.autoApproveOvernightParking === true ||
|
|
292
377
|
data?.isAutoApproved === true;
|
|
378
|
+
// Set in place so the toggle watcher only fires on a real change.
|
|
379
|
+
visitorQrCodes.value.forEach((option) => {
|
|
380
|
+
const saved = (data?.visitorQrCodes as TVisitorQrCode[] | undefined)?.find(
|
|
381
|
+
(q) => q.type === option.type
|
|
382
|
+
);
|
|
383
|
+
option.isEnabled = saved?.isEnabled === true;
|
|
384
|
+
if (option.type === "hid") {
|
|
385
|
+
option.usage = saved?.usage ?? DEFAULT_HID_QR_USAGE;
|
|
386
|
+
}
|
|
387
|
+
});
|
|
293
388
|
setTimeout(() => {
|
|
294
389
|
isHydrating.value = false;
|
|
295
390
|
}, 500);
|
|
@@ -374,7 +469,7 @@ watch(
|
|
|
374
469
|
isAllowPlateNumberOpenBarrier,
|
|
375
470
|
(newValue, oldValue) => {
|
|
376
471
|
if (oldValue === newValue || isHydrating.value) return;
|
|
377
|
-
handleSave("toggle");
|
|
472
|
+
handleSave("toggle", "Plate number barrier setting updated successfully.");
|
|
378
473
|
},
|
|
379
474
|
{ immediate: false }
|
|
380
475
|
);
|
|
@@ -388,7 +483,19 @@ watch(
|
|
|
388
483
|
{ immediate: false }
|
|
389
484
|
);
|
|
390
485
|
|
|
391
|
-
|
|
486
|
+
watch(
|
|
487
|
+
visitorQrCodes,
|
|
488
|
+
() => {
|
|
489
|
+
if (isHydrating.value) return;
|
|
490
|
+
handleSave("toggle", "Visitor QR settings updated successfully.");
|
|
491
|
+
},
|
|
492
|
+
{ deep: true }
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
async function handleSave(
|
|
496
|
+
action: "toggle" | "save",
|
|
497
|
+
successMessage = "Overnight parking settings updated successfully."
|
|
498
|
+
) {
|
|
392
499
|
if (!isValid.value && action === "save") {
|
|
393
500
|
message.value = "Please fill in all required fields.";
|
|
394
501
|
messageColor.value = "error";
|
|
@@ -403,6 +510,11 @@ async function handleSave(action: "toggle" | "save") {
|
|
|
403
510
|
isAllowPlateNumberOpenBarrier: isAllowPlateNumberOpenBarrier.value,
|
|
404
511
|
autoApproveOvernightParking: autoApproveOvernightParking.value,
|
|
405
512
|
isAutoApproved: autoApproveOvernightParking.value,
|
|
513
|
+
visitorQrCodes: visitorQrCodes.value.map(({ type, isEnabled, usage }) => ({
|
|
514
|
+
type,
|
|
515
|
+
isEnabled,
|
|
516
|
+
...(type === "hid" ? { usage: usage ?? DEFAULT_HID_QR_USAGE } : {}),
|
|
517
|
+
})),
|
|
406
518
|
updatedBy: updatedByValue.value,
|
|
407
519
|
};
|
|
408
520
|
|
|
@@ -414,7 +526,7 @@ async function handleSave(action: "toggle" | "save") {
|
|
|
414
526
|
userType.value
|
|
415
527
|
);
|
|
416
528
|
refreshAvailability();
|
|
417
|
-
message.value =
|
|
529
|
+
message.value = successMessage;
|
|
418
530
|
messageColor.value = "success";
|
|
419
531
|
messageSnackbar.value = true;
|
|
420
532
|
} catch (error) {
|
|
@@ -186,10 +186,12 @@
|
|
|
186
186
|
</v-expansion-panel-text>
|
|
187
187
|
</v-expansion-panel>
|
|
188
188
|
|
|
189
|
-
|
|
189
|
+
<!-- HID on or off for the site and the HID QR configuration:
|
|
190
|
+
`hid-mgmt:manage-hid-settings` (`useHidAccessPermission`). -->
|
|
191
|
+
<v-expansion-panel v-if="canManageHidSettings">
|
|
190
192
|
<v-expansion-panel-title>
|
|
191
193
|
<v-icon class="mr-2">mdi-face-recognition</v-icon>
|
|
192
|
-
HID
|
|
194
|
+
HID Reader
|
|
193
195
|
</v-expansion-panel-title>
|
|
194
196
|
<v-expansion-panel-text>
|
|
195
197
|
<HidServiceSettingsPanel
|
|
@@ -312,6 +314,7 @@ const { canManageSiteInformation,
|
|
|
312
314
|
canManageVehicleQr,
|
|
313
315
|
canManageSiteSettings: canManageAnySiteSetting,
|
|
314
316
|
} = useSettingsPermission();
|
|
317
|
+
const { canManageHidSettings } = useHidAccessPermission();
|
|
315
318
|
|
|
316
319
|
const { getSiteById, updateSitebyId, updateSiteInformation } =
|
|
317
320
|
useSiteSettings();
|
|
@@ -350,6 +350,7 @@
|
|
|
350
350
|
:hid-qr-printer-configured="hasHidQrPrinterConfig"
|
|
351
351
|
:hid-reader-name="hidQrReader?.name || ''"
|
|
352
352
|
:hid-portal-name="hidQrReader?.portalName || ''"
|
|
353
|
+
:hid-qr-usage="hidQrUsage"
|
|
353
354
|
@update:available-qr-count="(val) => availableQrCount = val" />
|
|
354
355
|
</v-col>
|
|
355
356
|
|
|
@@ -838,6 +839,7 @@ import useLocalAuth from "../composables/useLocalAuth";
|
|
|
838
839
|
import useBuilding from "../composables/useBuilding";
|
|
839
840
|
import useVisitor, { getVisitorFieldSwitches, NRIC_LAST4_HINT, VEHICLE_REQUIRED_MESSAGE } from "../composables/useVisitor";
|
|
840
841
|
import useSiteEntryPassSettings from "../composables/useSiteEntryPassSettings";
|
|
842
|
+
import { hidQrUsageFromSettings, minutesUntilEndOfDay } from "../utils/hid-qr-validity";
|
|
841
843
|
import useAccessManagement from "../composables/useAccessManagement";
|
|
842
844
|
import usePeople from "../composables/usePeople";
|
|
843
845
|
import useHidAmico from "../composables/useHidAmico";
|
|
@@ -912,7 +914,7 @@ const currentAutofillSource = ref<AutofillSource>(null);
|
|
|
912
914
|
|
|
913
915
|
const { requiredRule, debounce, UTCToLocalTIme, maskNRIC, getImage } = useUtils();
|
|
914
916
|
const { addFile } = useFile();
|
|
915
|
-
const { getSiteById, updateSitebyId } = useSiteSettings();
|
|
917
|
+
const { getSiteById, getOvernightParkingAvailabilityV2, updateSitebyId } = useSiteSettings();
|
|
916
918
|
const { currentUser } = useLocalAuth();
|
|
917
919
|
const { getBlocksSelection, getLevelsSelection, getUnitSelection } =
|
|
918
920
|
useBuilding();
|
|
@@ -927,7 +929,7 @@ const {
|
|
|
927
929
|
const { getBySiteId: getEntryPassSettingsBySiteId } =
|
|
928
930
|
useSiteEntryPassSettings();
|
|
929
931
|
const { createVisitorPass, signQr, visitorCheckout } = useAccessManagement();
|
|
930
|
-
const {
|
|
932
|
+
const { getEnrollmentReaders, issueVisitorQr } = useHidAmico();
|
|
931
933
|
const { testConnection } = useWebUsb();
|
|
932
934
|
const { findVisitorPeopleAnyType, deletePersonRecords } = usePeople();
|
|
933
935
|
const { getPassKeysByPageSearch } = usePassKey();
|
|
@@ -1230,11 +1232,6 @@ const hidQrTemplate = computed(() => {
|
|
|
1230
1232
|
return hidQrCodePassConfig.value?.template || {};
|
|
1231
1233
|
});
|
|
1232
1234
|
|
|
1233
|
-
const hidQrValidityMinutes = computed(() => {
|
|
1234
|
-
const value = Number(hidQrCodePassConfig.value?.validityMinutes);
|
|
1235
|
-
return Number.isFinite(value) && value > 0 ? value : 60;
|
|
1236
|
-
});
|
|
1237
|
-
|
|
1238
1235
|
const hidQrConfiguredReaderId = computed(() => String(hidQrCodePassConfig.value?.readerId || ""));
|
|
1239
1236
|
type HidQrReaderSummary = {
|
|
1240
1237
|
_id: string;
|
|
@@ -1256,7 +1253,7 @@ const hidQrReaderId = computed(() => (
|
|
|
1256
1253
|
* `hidQrCodePassAllowed`, which is why the switch belongs here and not in each
|
|
1257
1254
|
* of them.
|
|
1258
1255
|
*/
|
|
1259
|
-
const HID_VISITOR_QR_ENABLED =
|
|
1256
|
+
const HID_VISITOR_QR_ENABLED = true;
|
|
1260
1257
|
|
|
1261
1258
|
const hidQrCodePassAllowed = computed(() => {
|
|
1262
1259
|
return (
|
|
@@ -1266,6 +1263,12 @@ const hidQrCodePassAllowed = computed(() => {
|
|
|
1266
1263
|
);
|
|
1267
1264
|
});
|
|
1268
1265
|
|
|
1266
|
+
// The site's Whole day / Single usage choice for HID visitor passes, shown
|
|
1267
|
+
// under the HID QR Code card as the resident app's invite form shows it.
|
|
1268
|
+
// Null until read, and when it cannot be read: then no hint, not a guess.
|
|
1269
|
+
// Loaded by the watcher below `siteData`.
|
|
1270
|
+
const hidQrUsage = ref<"wholeDay" | "singleUse" | null>(null);
|
|
1271
|
+
|
|
1269
1272
|
// The entry pass is a contractor's, with the site's NFC pass switch on, as
|
|
1270
1273
|
// the security mobile app (the web-only HID QR pass, switched off above, keeps
|
|
1271
1274
|
// its own types).
|
|
@@ -2298,8 +2301,14 @@ watch(
|
|
|
2298
2301
|
hidQrReader.value = null;
|
|
2299
2302
|
if (!site) return;
|
|
2300
2303
|
try {
|
|
2301
|
-
|
|
2302
|
-
|
|
2304
|
+
/* The member's list, not the admin one. `POST /readers/:id/visitor-qr`
|
|
2305
|
+
asks for membership plus `hid-mgmt:use-hid-access` (API-core #1772),
|
|
2306
|
+
while `GET /readers` asks for the HID view right, so a member who may
|
|
2307
|
+
issue a visitor pass but holds no admin right got the pass route and
|
|
2308
|
+
not the reader it needs. `enrollment-readers` is the site's enabled
|
|
2309
|
+
readers with a portal, `{ data: { items } }`, for any member. */
|
|
2310
|
+
const response = await getEnrollmentReaders(site);
|
|
2311
|
+
const readers: unknown = response?.data?.items ?? [];
|
|
2303
2312
|
const availableReaders = Array.isArray(readers)
|
|
2304
2313
|
? readers.filter((reader: unknown): reader is HidQrReaderSummary => (
|
|
2305
2314
|
typeof reader === "object"
|
|
@@ -2326,6 +2335,23 @@ watch(
|
|
|
2326
2335
|
{ immediate: true },
|
|
2327
2336
|
);
|
|
2328
2337
|
|
|
2338
|
+
// Below `siteData` for the same reason as the reader watcher above:
|
|
2339
|
+
// `hidQrCodePassAllowed` reads it. Only fetched where HID QR is offered.
|
|
2340
|
+
watch(
|
|
2341
|
+
[hidQrCodePassAllowed, () => prop.site],
|
|
2342
|
+
async ([allowed, site]) => {
|
|
2343
|
+
hidQrUsage.value = null;
|
|
2344
|
+
if (!allowed || !site) return;
|
|
2345
|
+
try {
|
|
2346
|
+
const settings = await getOvernightParkingAvailabilityV2(String(site), "site");
|
|
2347
|
+
hidQrUsage.value = hidQrUsageFromSettings(settings);
|
|
2348
|
+
} catch {
|
|
2349
|
+
hidQrUsage.value = null;
|
|
2350
|
+
}
|
|
2351
|
+
},
|
|
2352
|
+
{ immediate: true },
|
|
2353
|
+
);
|
|
2354
|
+
|
|
2329
2355
|
const {
|
|
2330
2356
|
data: blocksListData,
|
|
2331
2357
|
refresh: refreshBlockListData,
|
|
@@ -3388,7 +3414,9 @@ async function prepareHidQrCodePreview(visitorId: string) {
|
|
|
3388
3414
|
hidQrVisitorId.value = visitorId;
|
|
3389
3415
|
const response = await issueVisitorQr(hidQrReaderId.value, {
|
|
3390
3416
|
visitorId,
|
|
3391
|
-
|
|
3417
|
+
// The whole visit day, as the resident app issues it. Worked out per
|
|
3418
|
+
// call so a regenerate later in the day still ends at midnight.
|
|
3419
|
+
validityMinutes: minutesUntilEndOfDay(),
|
|
3392
3420
|
});
|
|
3393
3421
|
const credential = "data" in response ? response.data : response;
|
|
3394
3422
|
hidQrPrintPayload.value = credential.qrValue;
|
|
@@ -84,6 +84,17 @@ export default function useCleaningPermission() {
|
|
|
84
84
|
description: "Allows the user to manage and update site settings.",
|
|
85
85
|
},
|
|
86
86
|
},
|
|
87
|
+
// HID access, user side only (decision of 2026-10-16). The service apps
|
|
88
|
+
// have no HID admin screens yet, so a role here can be prepared with the
|
|
89
|
+
// one string that lets a member use their own QR code, face, PIN, card and
|
|
90
|
+
// intercom. Mirrored from core `src/utils/hid-access.util.ts`.
|
|
91
|
+
"hid-mgmt": {
|
|
92
|
+
"use-hid-access": {
|
|
93
|
+
check: true,
|
|
94
|
+
description:
|
|
95
|
+
"Allows the user to see HID Access in the app and use their own QR code, face, PIN, card and intercom at this site.",
|
|
96
|
+
},
|
|
97
|
+
},
|
|
87
98
|
toilet_locations: {
|
|
88
99
|
"create-toilet-location": {
|
|
89
100
|
check: true,
|
|
@@ -1,124 +1,219 @@
|
|
|
1
|
+
import { permissionsDenied } from "../utils/module-routes";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
* WHO MAY
|
|
3
|
-
*
|
|
4
|
-
* `pages/[org]/[site]/access-mgmt/{access-logs, administrator, hid-cards,
|
|
5
|
-
* hid-readers, hid-users, intercom}/index.vue` shipped with
|
|
6
|
-
* `middleware: ["01-auth","02-org"], memberOnly: true` and no permission check
|
|
7
|
-
* at all. Every one of the eleven Nuxt apps inherits all six, so that was 66
|
|
8
|
-
* routes on which any member of any organisation could type the URL and reach
|
|
9
|
-
* door access, biometric enrolment, physical cards and the access log. The menu
|
|
10
|
-
* that links five of them (`useHidNavigation`) is gated on
|
|
11
|
-
* `getHidServiceEnabled(site)` -- a SITE SERVICE FLAG, not a permission -- so
|
|
12
|
-
* the menu was never a boundary either.
|
|
4
|
+
* WHO MAY DO WHAT ON HID: the five flags every HID screen, menu and button asks.
|
|
13
5
|
*
|
|
14
|
-
*
|
|
6
|
+
* Decision of 2026-10-16. HID has its own permission resource, `hid-mgmt`, with
|
|
7
|
+
* five actions. Core defines the lists in `src/utils/hid-access.util.ts` and
|
|
8
|
+
* every `/api/access-management/hid` route reads them; this package does not
|
|
9
|
+
* depend on `@7365admin1/core`, so the strings are copied here and
|
|
10
|
+
* `test/hid-access-gate.test.mjs` pins the copy to the five.
|
|
15
11
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
12
|
+
* canUseHid hid-mgmt:use-hid-access own QR, face, PIN, card,
|
|
13
|
+
* intercom; the HID QR Code page
|
|
14
|
+
* canViewHid hid-mgmt:see-all-hid open any HID admin screen
|
|
15
|
+
* canManageHidReaders hid-mgmt:manage-hid-readers add, configure, test, sync,
|
|
16
|
+
* remove readers
|
|
17
|
+
* canManageHidUsers hid-mgmt:manage-hid-users enrol and remove people,
|
|
18
|
+
* cards, PINs, access groups
|
|
19
|
+
* canManageHidSettings hid-mgmt:manage-hid-settings HID on or off, QR configuration
|
|
21
20
|
*
|
|
22
|
-
*
|
|
23
|
-
* are NOT the same list. `hid-amico-authz.util.ts` exports
|
|
24
|
-
* `["site-settings:manage-entry-pass"]`; `hid-access.util.ts` exports the
|
|
25
|
-
* `access-card-mgmt` family. `hid-amico.service.ts` imports the one from
|
|
26
|
-
* `hid-access.util.ts` -- and `authorizeSiteForCaller`'s own doc comment is
|
|
27
|
-
* stale and still claims the other. Follow the import, not the name.
|
|
21
|
+
* ## The rules, in the order each flag applies them
|
|
28
22
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
23
|
+
* 1. No role loaded: false. `userAppRole` is null on the first tick for
|
|
24
|
+
* everyone, the `*` owner included; `HidEnabledGate` waits on
|
|
25
|
+
* `isAppRoleResolved` before it draws a refusal.
|
|
26
|
+
* 2. The client's module list (RP-5): `hid-mgmt` denied closes every flag,
|
|
27
|
+
* including for a wildcard role. A wildcard role is exactly the role a
|
|
28
|
+
* module list exists to narrow, so this is asked BEFORE the `*` check.
|
|
29
|
+
* 3. `*` passes every flag.
|
|
30
|
+
* 4. An EMPTY permission list passes nothing. This is deliberately not the
|
|
31
|
+
* estate rule `hasPermission` applies (`grantsPermission`: empty means
|
|
32
|
+
* everything). The owner's decision for HID is strict on empty, everywhere,
|
|
33
|
+
* so the empty list is refused here before `hasPermission` can widen it.
|
|
34
|
+
* 5. Either manage right implies view: a role holding only
|
|
35
|
+
* `manage-hid-readers` could otherwise create a reader and not list them.
|
|
36
|
+
* Settings does NOT imply view, and use implies nothing.
|
|
35
37
|
*
|
|
36
|
-
*
|
|
37
|
-
* `hid-amico-authz.util.ts` enforces on the entry-pass paths and the string the
|
|
38
|
-
* HID menu has always been associated with -- and `*` keeps its short-circuit.
|
|
38
|
+
* ## The alias period
|
|
39
39
|
*
|
|
40
|
-
*
|
|
40
|
+
* Until the live roles are backfilled onto `hid-mgmt`, core still accepts the
|
|
41
|
+
* Access Cards strings HID borrowed before, at the levels
|
|
42
|
+
* `HID_LEGACY_VIEW_PERMISSIONS` and `HID_LEGACY_MANAGE_PERMISSIONS` give them:
|
|
43
|
+
* `see-all-card`, `see-all-qr-tagging` and `view` open the admin screens; the
|
|
44
|
+
* five card write strings and `manage-hid` manage readers and users; only
|
|
45
|
+
* `manage-hid` reaches settings. The same strings pass here at the same levels,
|
|
46
|
+
* so nothing that works today stops working before the backfill. The follow-up
|
|
47
|
+
* that drops the aliases deletes the `legacy*` branches below and the two
|
|
48
|
+
* `RETIRED_PERMISSIONS` in `constants/permissions.ts` together.
|
|
41
49
|
*
|
|
42
|
-
* `
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* `getReaders()` (`GET /readers`, `authorizeView`) and filters its rows out of
|
|
46
|
-
* that response; the contacts tab, the dial-code dialog, the make-call dialog
|
|
47
|
-
* and the web phone are all reached from a row, and `loadSipAccount()` is only
|
|
48
|
-
* ever called from one of those dialogs. Without `authorizeView` the screen is
|
|
49
|
-
* an empty table and an error, so gating it removes nothing that works.
|
|
50
|
+
* `site-settings:manage-entry-pass` is Entry Pass only again. It opened the HID
|
|
51
|
+
* screens from 4.2.14 to this release because a core utility of the same name
|
|
52
|
+
* once listed it; it never opened a HID endpoint.
|
|
50
53
|
*
|
|
51
54
|
* ## Drawing, not securing
|
|
52
55
|
*
|
|
53
|
-
* The server re-decides every HID call on its own.
|
|
54
|
-
*
|
|
56
|
+
* The server re-decides every HID call on its own. These flags hide screens and
|
|
57
|
+
* buttons that would be refused; they add no protection the API lacks.
|
|
55
58
|
*/
|
|
56
59
|
|
|
57
60
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* harvests what this package checks by reading literals out of the source, and a
|
|
62
|
-
* bare action fed through a loop into `hasPermission(role, cat, "access-card-mgmt",
|
|
63
|
-
* action)` is invisible to it. Spelling the strings out keeps all nine visible.
|
|
61
|
+
* Written out in full `resource:action` form on purpose: `permission-vocabulary`
|
|
62
|
+
* harvests what this package checks by reading literals out of the source, and
|
|
63
|
+
* a string built from a loop is invisible to it.
|
|
64
64
|
*/
|
|
65
|
-
const
|
|
65
|
+
export const HID_RESOURCE = "hid-mgmt";
|
|
66
|
+
|
|
67
|
+
export const HID_PERMISSION_STRINGS = [
|
|
68
|
+
"hid-mgmt:use-hid-access",
|
|
69
|
+
"hid-mgmt:see-all-hid",
|
|
70
|
+
"hid-mgmt:manage-hid-readers",
|
|
71
|
+
"hid-mgmt:manage-hid-users",
|
|
72
|
+
"hid-mgmt:manage-hid-settings",
|
|
73
|
+
] as const;
|
|
74
|
+
|
|
75
|
+
/** Core `HID_LEGACY_VIEW_PERMISSIONS`: open the admin screens during the alias period. */
|
|
76
|
+
export const HID_LEGACY_VIEW_PERMISSIONS = [
|
|
66
77
|
"access-card-mgmt:see-all-card",
|
|
67
78
|
"access-card-mgmt:see-all-qr-tagging",
|
|
68
79
|
"access-card-mgmt:view",
|
|
80
|
+
] as const;
|
|
81
|
+
|
|
82
|
+
/** Core `HID_LEGACY_MANAGE_PERMISSIONS`: manage readers and users during the alias period. */
|
|
83
|
+
export const HID_LEGACY_MANAGE_PERMISSIONS = [
|
|
69
84
|
"access-card-mgmt:add-access-card",
|
|
70
85
|
"access-card-mgmt:update-access-card",
|
|
71
|
-
"access-card-mgmt:replace-access-card",
|
|
72
86
|
"access-card-mgmt:delete-access-card",
|
|
73
87
|
"access-card-mgmt:assign-access-card",
|
|
88
|
+
"access-card-mgmt:replace-access-card",
|
|
74
89
|
"access-card-mgmt:manage-hid",
|
|
75
90
|
] as const;
|
|
76
91
|
|
|
77
|
-
const HID_ACTIONS =
|
|
92
|
+
const HID_ACTIONS = HID_PERMISSION_STRINGS.map((p) => p.split(":")[1]);
|
|
93
|
+
const LEGACY_ACTIONS = [...HID_LEGACY_VIEW_PERMISSIONS, ...HID_LEGACY_MANAGE_PERMISSIONS].map(
|
|
94
|
+
(p) => p.split(":")[1],
|
|
95
|
+
);
|
|
78
96
|
|
|
79
97
|
export function useHidAccessPermission() {
|
|
80
98
|
const { hasPermission } = usePermission();
|
|
81
|
-
const {
|
|
82
|
-
const { userAppRole } = useLocalSetup();
|
|
99
|
+
const { userAppRole, userAppModulesDenied } = useLocalSetup();
|
|
83
100
|
|
|
84
|
-
// `hasPermission` needs BOTH the role's string AND a catalogue entry for it
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
101
|
+
// `hasPermission` needs BOTH the role's string AND a catalogue entry for it.
|
|
102
|
+
// The property and security catalogues carry `hid-mgmt`, the five service
|
|
103
|
+
// catalogues (cleaning, landscaping, pest control, pool maintenance and,
|
|
104
|
+
// since 2026-10-19, M&E) carry only `use-hid-access`, and no catalogue
|
|
105
|
+
// offers the retired `view` and `manage-hid` any more, so the lookup table
|
|
106
|
+
// is built here: these flags must answer the same in every app that mounts
|
|
107
|
+
// a HID screen.
|
|
88
108
|
const permissions: TPermissions = {
|
|
109
|
+
"hid-mgmt": Object.fromEntries(
|
|
110
|
+
HID_ACTIONS.map((action) => [action, { check: true, description: `HID: ${action}.` }]),
|
|
111
|
+
),
|
|
89
112
|
"access-card-mgmt": Object.fromEntries(
|
|
90
|
-
|
|
113
|
+
LEGACY_ACTIONS.map((action) => [
|
|
91
114
|
action,
|
|
92
|
-
{
|
|
93
|
-
check: true,
|
|
94
|
-
description: `Allows the user to reach the HID access management screens (${action}).`,
|
|
95
|
-
},
|
|
115
|
+
{ check: true, description: `HID alias period: access-card-mgmt:${action}.` },
|
|
96
116
|
]),
|
|
97
117
|
),
|
|
98
|
-
"site-settings": siteSettingsPermissions,
|
|
99
118
|
};
|
|
100
119
|
|
|
120
|
+
// `?.` on purpose: an app pinned to an older layer-common, or a caller that
|
|
121
|
+
// stubs `useLocalSetup` without this field, hands back `undefined` here. That
|
|
122
|
+
// must mean "nothing denied", never a TypeError inside every gate.
|
|
123
|
+
const moduleDenied = (...modules: string[]) =>
|
|
124
|
+
permissionsDenied(modules, userAppModulesDenied?.value);
|
|
125
|
+
|
|
101
126
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* still has to open the screen to use it.
|
|
127
|
+
* Rules 1 to 4. `null` means "decide from the strings"; a boolean is final.
|
|
128
|
+
* The empty list is refused HERE because `hasPermission` would admit it.
|
|
105
129
|
*/
|
|
106
|
-
|
|
130
|
+
function decided(): boolean | null {
|
|
107
131
|
const role = userAppRole.value;
|
|
108
132
|
if (!role) return false;
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Only
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
133
|
+
if (moduleDenied("hid-mgmt")) return false;
|
|
134
|
+
const held: unknown = role.permissions;
|
|
135
|
+
if (!Array.isArray(held) || !held.length) return false;
|
|
136
|
+
if (held.includes("*")) return true;
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Only reached once `decided()` has answered `null`, so the role is loaded.
|
|
141
|
+
const has = (resource: string, action: string) => {
|
|
142
|
+
const role = userAppRole.value;
|
|
143
|
+
return Boolean(role) && hasPermission(role, permissions, resource, action);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// The alias period, string by string so both source sweeps can see them.
|
|
147
|
+
const legacyView = () =>
|
|
148
|
+
has("access-card-mgmt", "see-all-card") ||
|
|
149
|
+
has("access-card-mgmt", "see-all-qr-tagging") ||
|
|
150
|
+
has("access-card-mgmt", "view");
|
|
151
|
+
const legacyManage = () =>
|
|
152
|
+
has("access-card-mgmt", "add-access-card") ||
|
|
153
|
+
has("access-card-mgmt", "update-access-card") ||
|
|
154
|
+
has("access-card-mgmt", "delete-access-card") ||
|
|
155
|
+
has("access-card-mgmt", "assign-access-card") ||
|
|
156
|
+
has("access-card-mgmt", "replace-access-card") ||
|
|
157
|
+
has("access-card-mgmt", "manage-hid");
|
|
158
|
+
const legacySettings = () => has("access-card-mgmt", "manage-hid");
|
|
159
|
+
|
|
160
|
+
// The five, each as a literal `hasPermission` call so the gate sweep
|
|
161
|
+
// (`gate-defaults.test.mjs`) and the vocabulary sweep both read them.
|
|
162
|
+
const useHid = () => {
|
|
163
|
+
const role = userAppRole.value;
|
|
164
|
+
return Boolean(role) && hasPermission(role, permissions, "hid-mgmt", "use-hid-access");
|
|
165
|
+
};
|
|
166
|
+
const viewHid = () => {
|
|
167
|
+
const role = userAppRole.value;
|
|
168
|
+
return Boolean(role) && hasPermission(role, permissions, "hid-mgmt", "see-all-hid");
|
|
169
|
+
};
|
|
170
|
+
const manageReaders = () => {
|
|
171
|
+
const role = userAppRole.value;
|
|
172
|
+
return Boolean(role) && hasPermission(role, permissions, "hid-mgmt", "manage-hid-readers");
|
|
173
|
+
};
|
|
174
|
+
const manageUsers = () => {
|
|
175
|
+
const role = userAppRole.value;
|
|
176
|
+
return Boolean(role) && hasPermission(role, permissions, "hid-mgmt", "manage-hid-users");
|
|
177
|
+
};
|
|
178
|
+
const manageSettings = () => {
|
|
179
|
+
const role = userAppRole.value;
|
|
180
|
+
return Boolean(role) && hasPermission(role, permissions, "hid-mgmt", "manage-hid-settings");
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/** The user side: HID Access in the app, own QR, face, PIN, card, intercom. */
|
|
184
|
+
const canUseHid = computed(() => decided() ?? useHid());
|
|
185
|
+
|
|
186
|
+
/** Any manage right implies view (rule 5). */
|
|
187
|
+
const canManageHidReaders = computed(() => decided() ?? (manageReaders() || legacyManage()));
|
|
188
|
+
const canManageHidUsers = computed(() => decided() ?? (manageUsers() || legacyManage()));
|
|
189
|
+
const canViewHid = computed(
|
|
190
|
+
() =>
|
|
191
|
+
decided() ??
|
|
192
|
+
(viewHid() || legacyView() || manageReaders() || manageUsers() || legacyManage()),
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
/** HID on or off for the site, and the HID QR code configuration. */
|
|
196
|
+
const canManageHidSettings = computed(() => decided() ?? (manageSettings() || legacySettings()));
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* @deprecated The pre 2026-10 single gate: "may this member open a HID admin
|
|
200
|
+
* screen". Kept for any app on this layer that still reads it; it is
|
|
201
|
+
* `canViewHid` and nothing else. Use the five flags above.
|
|
202
|
+
*/
|
|
203
|
+
const canAccessHidScreens = canViewHid;
|
|
204
|
+
|
|
205
|
+
// `isAppRoleResolved` is deliberately NOT re-exported: `gate-defaults.test.mjs`
|
|
206
|
+
// sweeps every ref a permission composable returns and requires it to read
|
|
207
|
+
// false for an unresolved role, which a "has the lookup finished" flag is not.
|
|
208
|
+
// Callers that need it take it from `useLocalSetup()`, which is where it lives.
|
|
209
|
+
return {
|
|
210
|
+
canUseHid,
|
|
211
|
+
canViewHid,
|
|
212
|
+
canManageHidReaders,
|
|
213
|
+
canManageHidUsers,
|
|
214
|
+
canManageHidSettings,
|
|
215
|
+
canAccessHidScreens,
|
|
216
|
+
};
|
|
122
217
|
}
|
|
123
218
|
|
|
124
219
|
export default useHidAccessPermission;
|