@7365admin1/layer-common 4.149.2 → 4.151.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/OvernightParkingAvailability.vue +123 -11
- package/components/Robot/Detail.vue +23 -4
- 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
- package/utils/robot-credential-form.ts +4 -2
- package/utils/robot-display.ts +28 -6
- package/utils/robot-setup.ts +1 -0
|
@@ -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;
|
|
@@ -204,6 +204,22 @@ export default function useHidAmico() {
|
|
|
204
204
|
});
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
/**
|
|
208
|
+
* The readers a member may enrol THEMSELVES on: the site's enabled readers
|
|
209
|
+
* with a portal, each with its credential capabilities, and nothing else.
|
|
210
|
+
*
|
|
211
|
+
* The self service screens (own QR, face, PIN, card) load this instead of
|
|
212
|
+
* `getReaders`: that one is an admin list behind the HID view right, and a
|
|
213
|
+
* member holding only `hid-mgmt:use-hid-access` is refused on it. This route
|
|
214
|
+
* asks for site membership and the use right. Response: `{ data: { items } }`.
|
|
215
|
+
*/
|
|
216
|
+
function getEnrollmentReaders(siteId: string) {
|
|
217
|
+
return useNuxtApp().$api<{ data: { items: HidApiRecord[] } }>(
|
|
218
|
+
`${basePath}/sites/${siteId}/enrollment-readers`,
|
|
219
|
+
{ method: "GET" },
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
207
223
|
/**
|
|
208
224
|
* The signed-in member's own QR code on one reader.
|
|
209
225
|
*
|
|
@@ -398,6 +414,25 @@ export default function useHidAmico() {
|
|
|
398
414
|
}
|
|
399
415
|
|
|
400
416
|
|
|
417
|
+
/**
|
|
418
|
+
* Put a person's STORED enrolment photo on a reader.
|
|
419
|
+
*
|
|
420
|
+
* No body: the image is the one already on file from when they were first
|
|
421
|
+
* captured. This is what makes a reader that was offline during an enrolment
|
|
422
|
+
* retryable WITHOUT the person coming back - a face template lives on one
|
|
423
|
+
* device and cannot be copied, so the alternative is a fresh capture at every
|
|
424
|
+
* reader.
|
|
425
|
+
*
|
|
426
|
+
* Keyed by the iService account, not a HID user id: the caller does not know
|
|
427
|
+
* which number the person carries on which reader, and the server looks it up.
|
|
428
|
+
*/
|
|
429
|
+
function pushStoredFace(readerId: string, userId: string) {
|
|
430
|
+
return useNuxtApp().$api<{ data: HidFacialEnrollmentResult; message?: string }>(
|
|
431
|
+
`${basePath}/readers/${readerId}/accounts/${userId}/stored-face`,
|
|
432
|
+
{ method: "POST" },
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
401
436
|
function setUserImage(
|
|
402
437
|
readerId: string,
|
|
403
438
|
hidUserId: string | number,
|
|
@@ -660,6 +695,7 @@ export default function useHidAmico() {
|
|
|
660
695
|
|
|
661
696
|
return {
|
|
662
697
|
getReaders,
|
|
698
|
+
getEnrollmentReaders,
|
|
663
699
|
getProfileQr,
|
|
664
700
|
issueProfileQr,
|
|
665
701
|
deleteProfileQr,
|
|
@@ -681,6 +717,7 @@ export default function useHidAmico() {
|
|
|
681
717
|
reconcileReaderIdentificationMethods,
|
|
682
718
|
getLogs,
|
|
683
719
|
setUserImage,
|
|
720
|
+
pushStoredFace,
|
|
684
721
|
deleteUserImage,
|
|
685
722
|
getUserCards,
|
|
686
723
|
assignUserCard,
|
|
@@ -3,23 +3,23 @@ import type { ComputedRef, Ref } from "vue";
|
|
|
3
3
|
/**
|
|
4
4
|
* THE HID FACE READER MENU IS A PERMISSION, NOT A SERVICE FLAG.
|
|
5
5
|
*
|
|
6
|
-
* This composable used to draw the
|
|
7
|
-
* `getHidServiceEnabled(site)` was true. That is a SITE SERVICE FLAG
|
|
8
|
-
* the estate bought HID, not that this member may use it.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* be in the menu at all.
|
|
6
|
+
* This composable used to draw the six item "HID Reader" group whenever
|
|
7
|
+
* `getHidServiceEnabled(site)` was true. That is a SITE SERVICE FLAG: it says
|
|
8
|
+
* the estate bought HID, not that this member may use it. A module a member
|
|
9
|
+
* cannot use should not be in the menu at all, so the menu asks the same
|
|
10
|
+
* questions the pages ask (`useHidAccessPermission`), ANDed with the service
|
|
11
|
+
* flag: no HID service still means no menu.
|
|
13
12
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* Decision of 2026-10-16: the admin entries (Face Reader, HID Reader Users, HID
|
|
14
|
+
* Cards, Intercom, Access Logs) follow `canViewHid`; the member's own "HID QR
|
|
15
|
+
* Code" entry follows `canUseHid`. The group is drawn when at least one entry
|
|
16
|
+
* is, so a member holding only `use-hid-access` still finds their QR page, and
|
|
17
|
+
* a member holding only the admin rights does not see a page the server would
|
|
18
|
+
* refuse them. `site-settings:manage-entry-pass` no longer draws anything here.
|
|
17
19
|
*
|
|
18
|
-
* `Layout/NavigationDrawer.vue`
|
|
19
|
-
*
|
|
20
|
-
* `
|
|
21
|
-
* here therefore WIDENS — nobody who could see the menu can lose it — and
|
|
22
|
-
* leaves one list in one place.
|
|
20
|
+
* `Layout/NavigationDrawer.vue` keeps no permission list of its own: it takes
|
|
21
|
+
* `menuItem` from here and runs it through `withoutDeniedRoutes`, where the
|
|
22
|
+
* client's module list (`hid-mgmt`) can take the whole group away again.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
type HidNavigationParams = {
|
|
@@ -32,14 +32,21 @@ type HidNavigationSite = Record<string, unknown>;
|
|
|
32
32
|
export default function useHidNavigation() {
|
|
33
33
|
const { getSiteById } = useSiteSettings();
|
|
34
34
|
const { getHidServiceEnabled } = useOptionalServices();
|
|
35
|
-
const {
|
|
35
|
+
const { canViewHid, canUseHid } = useHidAccessPermission();
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* The whole group, unfiltered. Exported for callers that compose their own
|
|
39
|
+
* menus; `useHidFaceReaderMenu` below is the one that applies the rights.
|
|
40
|
+
* Each entry says which right draws it so the filter cannot drift from the
|
|
41
|
+
* list.
|
|
42
|
+
*/
|
|
37
43
|
function getHidFaceReaderMenu({ org, site }: HidNavigationParams) {
|
|
38
44
|
return {
|
|
39
|
-
title: "HID
|
|
45
|
+
title: "HID Reader",
|
|
40
46
|
children: [
|
|
41
47
|
{
|
|
42
48
|
title: "Face Reader",
|
|
49
|
+
hidRight: "view" as const,
|
|
43
50
|
route: {
|
|
44
51
|
name: "org-site-access-mgmt-hid-readers",
|
|
45
52
|
params: { org, site },
|
|
@@ -61,6 +68,7 @@ export default function useHidNavigation() {
|
|
|
61
68
|
// `hid-users` is left in place and simply unlisted - the same state
|
|
62
69
|
// `administrator` was in - so any existing link to it still resolves.
|
|
63
70
|
title: "HID Reader Users",
|
|
71
|
+
hidRight: "view" as const,
|
|
64
72
|
route: {
|
|
65
73
|
name: "org-site-access-mgmt-administrator",
|
|
66
74
|
params: { org, site },
|
|
@@ -71,6 +79,7 @@ export default function useHidNavigation() {
|
|
|
71
79
|
// entry in this group acts on a resident or a visitor; this one has
|
|
72
80
|
// no subject but the caller.
|
|
73
81
|
title: "HID QR Code",
|
|
82
|
+
hidRight: "use" as const,
|
|
74
83
|
route: {
|
|
75
84
|
name: "org-site-access-mgmt-hid-qr-code",
|
|
76
85
|
params: { org, site },
|
|
@@ -78,6 +87,7 @@ export default function useHidNavigation() {
|
|
|
78
87
|
},
|
|
79
88
|
{
|
|
80
89
|
title: "HID Cards",
|
|
90
|
+
hidRight: "view" as const,
|
|
81
91
|
route: {
|
|
82
92
|
name: "org-site-access-mgmt-hid-cards",
|
|
83
93
|
params: { org, site },
|
|
@@ -85,6 +95,7 @@ export default function useHidNavigation() {
|
|
|
85
95
|
},
|
|
86
96
|
{
|
|
87
97
|
title: "Intercom",
|
|
98
|
+
hidRight: "view" as const,
|
|
88
99
|
route: {
|
|
89
100
|
name: "org-site-access-mgmt-intercom",
|
|
90
101
|
params: { org, site },
|
|
@@ -92,6 +103,7 @@ export default function useHidNavigation() {
|
|
|
92
103
|
},
|
|
93
104
|
{
|
|
94
105
|
title: "Access Logs",
|
|
106
|
+
hidRight: "view" as const,
|
|
95
107
|
route: {
|
|
96
108
|
name: "org-site-access-mgmt-access-logs",
|
|
97
109
|
params: { org, site },
|
|
@@ -138,14 +150,23 @@ export default function useHidNavigation() {
|
|
|
138
150
|
getHidServiceEnabled(site.value, siteDetails.value || fallbackSite?.value),
|
|
139
151
|
);
|
|
140
152
|
|
|
141
|
-
// ...AND this member may open
|
|
142
|
-
// name and its meaning for callers: "should the HID menu be drawn".
|
|
143
|
-
const enabled = computed(
|
|
144
|
-
|
|
145
|
-
const menuItem = computed(() =>
|
|
146
|
-
enabled.value ? getHidFaceReaderMenu({ org: org.value, site: site.value }) : null,
|
|
153
|
+
// ...AND this member may open at least one screen behind it. `enabled`
|
|
154
|
+
// keeps its name and its meaning for callers: "should the HID menu be drawn".
|
|
155
|
+
const enabled = computed(
|
|
156
|
+
() => serviceEnabled.value && (canViewHid.value || canUseHid.value),
|
|
147
157
|
);
|
|
148
158
|
|
|
159
|
+
// Only the entries this member's rights draw. The admin entries need
|
|
160
|
+
// `canViewHid`; the member's own QR page needs `canUseHid`.
|
|
161
|
+
const menuItem = computed(() => {
|
|
162
|
+
if (!enabled.value) return null;
|
|
163
|
+
const group = getHidFaceReaderMenu({ org: org.value, site: site.value });
|
|
164
|
+
const children = group.children.filter((child) =>
|
|
165
|
+
child.hidRight === "use" ? canUseHid.value : canViewHid.value,
|
|
166
|
+
);
|
|
167
|
+
return children.length ? { ...group, children } : null;
|
|
168
|
+
});
|
|
169
|
+
|
|
149
170
|
return {
|
|
150
171
|
enabled,
|
|
151
172
|
serviceEnabled,
|
|
@@ -285,6 +285,17 @@ export default function useLandscapingPermission() {
|
|
|
285
285
|
description: "Allows the user to manage site settings.",
|
|
286
286
|
},
|
|
287
287
|
},
|
|
288
|
+
// HID access, user side only (decision of 2026-10-16). The service apps
|
|
289
|
+
// have no HID admin screens yet, so a role here can be prepared with the
|
|
290
|
+
// one string that lets a member use their own QR code, face, PIN, card and
|
|
291
|
+
// intercom. Mirrored from core `src/utils/hid-access.util.ts`.
|
|
292
|
+
"hid-mgmt": {
|
|
293
|
+
"use-hid-access": {
|
|
294
|
+
check: true,
|
|
295
|
+
description:
|
|
296
|
+
"Allows the user to see HID Access in the app and use their own QR code, face, PIN, card and intercom at this site.",
|
|
297
|
+
},
|
|
298
|
+
},
|
|
288
299
|
};
|
|
289
300
|
|
|
290
301
|
return {
|
|
@@ -52,6 +52,11 @@ export default function useMechanicalElectricalPermission() {
|
|
|
52
52
|
"area-mgmt": schedulePermissions["area-mgmt"],
|
|
53
53
|
"unit-mgmt": schedulePermissions["unit-mgmt"],
|
|
54
54
|
"site-settings": schedulePermissions["site-settings"],
|
|
55
|
+
// HID access, user side only (2026-10-19): the M&E mobile app gains the
|
|
56
|
+
// HID user side, so an M&E role must be able to hold `use-hid-access`.
|
|
57
|
+
// Taken from the cleaning catalogue like the keys above (one action, same
|
|
58
|
+
// description), so the two cannot drift. Catalogue-only: grants nothing.
|
|
59
|
+
"hid-mgmt": schedulePermissions["hid-mgmt"],
|
|
55
60
|
"cleaning-schedule-mgmt": schedulePermissions["cleaning-schedule-mgmt"],
|
|
56
61
|
"schedule-task-mgmt": schedulePermissions["schedule-task-mgmt"],
|
|
57
62
|
"attendance-mgmt": schedulePermissions["attendance-mgmt"],
|
|
@@ -93,6 +93,17 @@ export default function usePestControlPermission() {
|
|
|
93
93
|
description: "Allows the user to manage and update site settings.",
|
|
94
94
|
},
|
|
95
95
|
},
|
|
96
|
+
// HID access, user side only (decision of 2026-10-16). The service apps
|
|
97
|
+
// have no HID admin screens yet, so a role here can be prepared with the
|
|
98
|
+
// one string that lets a member use their own QR code, face, PIN, card and
|
|
99
|
+
// intercom. Mirrored from core `src/utils/hid-access.util.ts`.
|
|
100
|
+
"hid-mgmt": {
|
|
101
|
+
"use-hid-access": {
|
|
102
|
+
check: true,
|
|
103
|
+
description:
|
|
104
|
+
"Allows the user to see HID Access in the app and use their own QR code, face, PIN, card and intercom at this site.",
|
|
105
|
+
},
|
|
106
|
+
},
|
|
96
107
|
// area management permissions
|
|
97
108
|
"area-mgmt": {
|
|
98
109
|
"see-all-areas": {
|
|
@@ -93,6 +93,17 @@ export default function usePoolMaintenancePermission() {
|
|
|
93
93
|
description: "Allows the user to manage and update site settings.",
|
|
94
94
|
},
|
|
95
95
|
},
|
|
96
|
+
// HID access, user side only (decision of 2026-10-16). The service apps
|
|
97
|
+
// have no HID admin screens yet, so a role here can be prepared with the
|
|
98
|
+
// one string that lets a member use their own QR code, face, PIN, card and
|
|
99
|
+
// intercom. Mirrored from core `src/utils/hid-access.util.ts`.
|
|
100
|
+
"hid-mgmt": {
|
|
101
|
+
"use-hid-access": {
|
|
102
|
+
check: true,
|
|
103
|
+
description:
|
|
104
|
+
"Allows the user to see HID Access in the app and use their own QR code, face, PIN, card and intercom at this site.",
|
|
105
|
+
},
|
|
106
|
+
},
|
|
96
107
|
// area management permissions
|
|
97
108
|
"area-mgmt": {
|
|
98
109
|
"see-all-areas": {
|
|
@@ -14,7 +14,8 @@ import { useCommonPermissions } from "./useCommonPermission";
|
|
|
14
14
|
* Two copies is how they drift. On 2026-09-05 the `-org` copy was found to be a
|
|
15
15
|
* strict SUBSET of `-property-management`'s — **32 permissions short**, nine
|
|
16
16
|
* whole families plus all six `access-card-mgmt` strings that
|
|
17
|
-
* `core/utils/hid-access.util.ts`
|
|
17
|
+
* `core/utils/hid-access.util.ts` enforced at the time (HID reads its own
|
|
18
|
+
* `hid-mgmt` family since 2026-10-16) — so a role built through
|
|
18
19
|
* the onboarding wizard could never be granted them, and the modules were
|
|
19
20
|
* unreachable for anybody who joined that way. web-app-org #189 closed it by
|
|
20
21
|
* copying the file across, which left two identical copies and the same trap
|
|
@@ -256,10 +257,6 @@ export default function usePropertyPermission() {
|
|
|
256
257
|
description:
|
|
257
258
|
"Allows the user to remove an access card from the system permanently.",
|
|
258
259
|
},
|
|
259
|
-
view: {
|
|
260
|
-
check: true,
|
|
261
|
-
description: "Allows the user to view an access card's details.",
|
|
262
|
-
},
|
|
263
260
|
"assign-access-card": {
|
|
264
261
|
check: true,
|
|
265
262
|
description: "Allows the user to assign an access card to a person.",
|
|
@@ -269,10 +266,37 @@ export default function usePropertyPermission() {
|
|
|
269
266
|
description:
|
|
270
267
|
"Allows the user to replace a lost or damaged access card with a new one.",
|
|
271
268
|
},
|
|
272
|
-
|
|
269
|
+
},
|
|
270
|
+
// HID access management (decision of 2026-10-16). Five strings under one
|
|
271
|
+
// resource, mirrored from core `src/utils/hid-access.util.ts`; the HID
|
|
272
|
+
// screens, menu and buttons (`useHidAccessPermission`) and every
|
|
273
|
+
// `/api/access-management/hid` route read them. Wildcard passes all five,
|
|
274
|
+
// an empty list passes none, either manage right implies view.
|
|
275
|
+
"hid-mgmt": {
|
|
276
|
+
"use-hid-access": {
|
|
277
|
+
check: true,
|
|
278
|
+
description:
|
|
279
|
+
"Allows the user to see HID Access in the app and use their own QR code, face, PIN, card and intercom at this site.",
|
|
280
|
+
},
|
|
281
|
+
"see-all-hid": {
|
|
282
|
+
check: true,
|
|
283
|
+
description:
|
|
284
|
+
"Allows the user to view the site's HID readers, enrolled people, cards and access logs.",
|
|
285
|
+
},
|
|
286
|
+
"manage-hid-readers": {
|
|
287
|
+
check: true,
|
|
288
|
+
description:
|
|
289
|
+
"Allows the user to add, configure, test, sync and remove HID readers. Includes viewing.",
|
|
290
|
+
},
|
|
291
|
+
"manage-hid-users": {
|
|
292
|
+
check: true,
|
|
293
|
+
description:
|
|
294
|
+
"Allows the user to enrol and remove people, cards and PINs, and assign HID access groups. Includes viewing.",
|
|
295
|
+
},
|
|
296
|
+
"manage-hid-settings": {
|
|
273
297
|
check: true,
|
|
274
298
|
description:
|
|
275
|
-
"Allows the user to
|
|
299
|
+
"Allows the user to switch HID on or off for the site and configure the HID QR code.",
|
|
276
300
|
},
|
|
277
301
|
},
|
|
278
302
|
"service-provider-mgmt": {
|