@7365admin1/layer-common 4.0.3-staging.236 → 4.0.4-staging.237
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 +267 -4
- package/constants/permissions.ts +680 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,268 @@
|
|
|
1
1
|
# @iservice365/layer-common
|
|
2
2
|
|
|
3
|
+
## 4.0.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 02e84c5: Default AccessCardPreviewDialog's Update, Replace and Delete gates to refused
|
|
8
|
+
|
|
9
|
+
The three remaining fail-open prop defaults in this package. They qualified for
|
|
10
|
+
the sweep in #1277 but sat inside #1276's own diff hunk, so they were deliberately
|
|
11
|
+
deferred until #1276 landed. It has, so they are flipped now.
|
|
12
|
+
|
|
13
|
+
`canUpdate`, `canReplaceAccessCard` and `canDeleteAccessCard` declared
|
|
14
|
+
`default: true`, so a caller that omitted one was handed the Update, Replace or
|
|
15
|
+
Delete control by the shared layer itself.
|
|
16
|
+
|
|
17
|
+
`AccessManagement` is the only caller anywhere — the layer and all eleven web
|
|
18
|
+
apps at `origin/staging` — and it binds all three to its own props, each of which
|
|
19
|
+
is declared with an explicit `default: false`. The bound expressions therefore
|
|
20
|
+
always resolve to a defined value, so the child default never fires and **no user
|
|
21
|
+
loses access**. This closes the trap for the next caller.
|
|
22
|
+
|
|
23
|
+
The contract in `test/gate-defaults.test.mjs` now covers 54 props across 22
|
|
24
|
+
components, up from 51 across 21.
|
|
25
|
+
|
|
26
|
+
- 6ba8bba: BulletinBoardManagement now derives canViewBulletinBoardDetails from its own permission gate when the caller omits it
|
|
27
|
+
|
|
28
|
+
`canViewBulletinBoardDetails` declared `default: true`. Six of the seven apps
|
|
29
|
+
that render this component (`-hygiene`, `-landscape`, `-mechanical-electrical`,
|
|
30
|
+
`-pest-control`, `-pool-mgmt`, `-security`) never bind it, so every user in
|
|
31
|
+
those apps could open the bulletin-board details/edit dialog regardless of
|
|
32
|
+
permission. `useBulletinBoardPermission().canViewBulletinBoardDetails` already
|
|
33
|
+
computed this gate correctly and failed closed -- it just was never called by
|
|
34
|
+
the component.
|
|
35
|
+
|
|
36
|
+
The component now reads that composable itself as the fallback when the prop
|
|
37
|
+
is unbound (`props.canViewBulletinBoardDetails ?? derived.value`). An app that
|
|
38
|
+
already passes the prop explicitly (`-property-management`) is unaffected --
|
|
39
|
+
its value still wins. This closes the gap in-package; no app-side PR needed
|
|
40
|
+
for this one prop.
|
|
41
|
+
|
|
42
|
+
- 3ac2550: Stop the still-frame refusal from blacking out the Virtual Patrol camera wall
|
|
43
|
+
|
|
44
|
+
On production every tile on the CCTV wall drew "Not set up yet" with "No recorder
|
|
45
|
+
is configured for this camera's relay.", while staging — the same build — played
|
|
46
|
+
live pictures. The difference was a recorder entry on the API host, and the live
|
|
47
|
+
picture never touches a recorder: `CameraWallTile.vue` embeds `camera.host`, the
|
|
48
|
+
relay's own player page, browser to relay, with no Seven365 server in the path.
|
|
49
|
+
|
|
50
|
+
`tileView` returned the dead tile on `camera.unavailableReason` before it ever
|
|
51
|
+
consulted `capabilities.liveVideo`. `unavailableReason` is the server's
|
|
52
|
+
**still-frame** refusal (`snapshotRefusalReason`), which genuinely does need a
|
|
53
|
+
recorder; live video does not. So a picture that was available the whole time was
|
|
54
|
+
never asked for.
|
|
55
|
+
|
|
56
|
+
The `liveVideo` capability is now read first, and the refusal only kills the tile
|
|
57
|
+
when the server has not said live video is supported. A camera that genuinely
|
|
58
|
+
cannot show anything — `unsupported`, or a server that never declared the
|
|
59
|
+
capability at all — still renders the honest "Not set up yet" tile with the
|
|
60
|
+
server's own sentence, unchanged.
|
|
61
|
+
|
|
62
|
+
When a tile draws past an outstanding still-frame refusal it carries
|
|
63
|
+
`RELAY_ONLY_NOTE` on its note, so a moving picture above a "Recorder not checked"
|
|
64
|
+
recorder line reads as two separate facts rather than a contradiction. The note
|
|
65
|
+
is used rather than `detail` because `detail` is drawn across the whole tile and
|
|
66
|
+
would cover the picture it is explaining.
|
|
67
|
+
|
|
68
|
+
- 6f9a69d: Make the dashboard's work-order and visitor widgets honour the spelling each app actually grants, and fail closed before the role loads
|
|
69
|
+
|
|
70
|
+
The three apps that render `DashboardMain` spell their resources differently:
|
|
71
|
+
work orders are `work_orders` in `-security`, `workOrder` in
|
|
72
|
+
`-property-management` and `work_order` in this layer's `useCleaningPermission`;
|
|
73
|
+
visitors are `visitor-mgmt` in `-security` and `visitorManagement` in
|
|
74
|
+
`-property-management`. The widget map asked for one spelling apiece, so a
|
|
75
|
+
property-management role granted "see all work orders" and "see all visitors"
|
|
76
|
+
through its own role editor got neither widget. Every spelling in use is now
|
|
77
|
+
listed, the same defensive union the feedback and incident widgets already had.
|
|
78
|
+
|
|
79
|
+
`canViewWidget` also returned `true` when no role was loaded, so the dashboard
|
|
80
|
+
drew every widget and then removed some -- and a failed role fetch left them all
|
|
81
|
+
drawn. It now returns `false`, matching every other gate in the package.
|
|
82
|
+
|
|
83
|
+
- 0ac386f: Forward two permission gates that were declared, passed by the apps, and then dropped
|
|
84
|
+
|
|
85
|
+
`BuildingManagement/units` declares `canUpdateUnit` and `canDeleteUnit`, and both
|
|
86
|
+
`-property-management` and `-security` bind them — but the component never handed
|
|
87
|
+
them to `BuildingUnitFormEdit`, which draws the Update and Delete items. That child
|
|
88
|
+
defaulted both to `true`, so a role denied update-unit or delete-unit still got both
|
|
89
|
+
actions inside the unit preview dialog.
|
|
90
|
+
|
|
91
|
+
`AccessManagement` never passed `can-assign-to-person` to `AccessCardPreviewDialog`,
|
|
92
|
+
which gates the Assign control and also defaulted to `true`. The list view in the
|
|
93
|
+
same file gates its own Assign on `canUpdateAccessCard && canAssignAccessCard`, so
|
|
94
|
+
the dialog was strictly looser than the list beside it. It now applies the same rule.
|
|
95
|
+
|
|
96
|
+
The three child defaults are flipped from `true` to `false` so a caller that forgets
|
|
97
|
+
the prop is refused rather than granted.
|
|
98
|
+
|
|
99
|
+
User-visible effect, and this one takes access away: in `-property-management` and
|
|
100
|
+
`-security`, a role without update-unit or delete-unit no longer sees Update or
|
|
101
|
+
Delete in the unit preview dialog, and a role without update-access-card or
|
|
102
|
+
assign-access-card no longer sees Assign in the card preview dialog. Those controls
|
|
103
|
+
were never meant to be there. Roles that hold the permissions are unchanged.
|
|
104
|
+
|
|
105
|
+
- b4e5dd5: Default every permission-shaped prop to refused instead of allowed
|
|
106
|
+
|
|
107
|
+
51 gate-shaped props across 21 shared components declared `default: true`. A
|
|
108
|
+
permission prop that defaults open means the shared layer answers "yes" on an
|
|
109
|
+
app's behalf whenever that app forgets to bind it — the same fail-open class
|
|
110
|
+
found in `web-app-org` and in the 25 `web-app-hygiene` gates, expressed as a
|
|
111
|
+
prop default instead of `if (!userAppRole.value) return true`.
|
|
112
|
+
|
|
113
|
+
All 51 are flipped to `default: false`. Every one was first confirmed bound at
|
|
114
|
+
every existing call site — all eleven web apps read at `origin/staging`, plus
|
|
115
|
+
every layer-internal parent — and the bound expression confirmed to resolve on
|
|
116
|
+
that page, because Vue falls back to the default when a bound value is
|
|
117
|
+
`undefined`. 154 bindings checked, none unresolved.
|
|
118
|
+
|
|
119
|
+
**No user loses access from this change.** Every current call site passes an
|
|
120
|
+
explicit value, so the default never fires today; the flip closes the trap for
|
|
121
|
+
the next caller. Props that some app does not bind are deliberately left open —
|
|
122
|
+
flipping those would take a control away from a real user — and are listed in
|
|
123
|
+
the PR as cross-repo follow-ups.
|
|
124
|
+
|
|
125
|
+
The package's 66 runtime permission gates across its 10 permission composables
|
|
126
|
+
were audited in the same sweep and already fail closed; a regression test now
|
|
127
|
+
drives all of them with real non-wildcard role documents to keep it that way.
|
|
128
|
+
|
|
129
|
+
- 050df56: Show the Members row menu when the only permission granted is Assign Role
|
|
130
|
+
|
|
131
|
+
The menu's `v-if` tested suspend, activate and delete only, while the Assign
|
|
132
|
+
Role item inside it tested `assignRole`. A role granted
|
|
133
|
+
`members:assign-member-role` and nothing else therefore got no row menu at all:
|
|
134
|
+
the permission was granted, the item existed, and there was no way to reach it.
|
|
135
|
+
The menu now opens for `assignRole` as well, and still fails closed when none of
|
|
136
|
+
the four is granted.
|
|
137
|
+
|
|
138
|
+
- f7cafee: Stop shipping this layer's own unit-test files in the published package. The
|
|
139
|
+
`files` list included `utils` wholesale, so seventeen `utils/*.test.ts` files
|
|
140
|
+
went out in every tarball. Consuming applications scan the installed layer's
|
|
141
|
+
`utils` folder for route references, and the fixture strings inside
|
|
142
|
+
`utils/breadcrumb.test.ts` were being read as ten real route names that no page
|
|
143
|
+
produces. Excluding `**/*.test.ts` removes the false readings and drops the
|
|
144
|
+
fixtures from what every application downloads. No runtime code changes.
|
|
145
|
+
- 4f69c0e: Add the missing `site-settings` and `toilet_locations` families to the cleaning permission catalogue
|
|
146
|
+
|
|
147
|
+
`useCleaningPermission()`'s catalogue had no `site-settings` key and no
|
|
148
|
+
`toilet_locations` key at all. `hasPermission` resolves an action as
|
|
149
|
+
`catalogue[resource][action]`, so both families answered `false` for every role
|
|
150
|
+
that did not hold `"*"` — on the published `4.0.1` and on `@staging` alike. This
|
|
151
|
+
is the same class as the earlier flat-catalogue fix but a different root cause:
|
|
152
|
+
the resource was missing rather than wrongly shaped, so that fix did not cover it.
|
|
153
|
+
|
|
154
|
+
Action names and nesting are copied from the sibling module catalogues that
|
|
155
|
+
already work — `usePoolMaintenancePermission` and `useLandscapingPermission` both
|
|
156
|
+
spell site settings exactly this way.
|
|
157
|
+
|
|
158
|
+
User-visible effect: in `web-app-hygiene`, a role granted "view site settings" or
|
|
159
|
+
"manage site settings" now actually reaches the Site Settings screen, and
|
|
160
|
+
"create toilet location" is grantable instead of permanently refused. Nobody
|
|
161
|
+
loses access — a full-access role behaved correctly before and is unchanged, and
|
|
162
|
+
a role that was never granted the permission is still refused.
|
|
163
|
+
|
|
164
|
+
- a9bd126: Fix site-settings and bulletin-board permission gates, which only ever opened for a full-access role
|
|
165
|
+
|
|
166
|
+
`useSettingsPermission` and `useBulletinBoardPermission` passed `hasPermission` a
|
|
167
|
+
flat `{action: ...}` map where it looks the action up as `catalogue[resource][action]`.
|
|
168
|
+
`catalogue["site-settings"]` was therefore `undefined` and all eleven site-settings
|
|
169
|
+
gates plus all five bulletin-board gates answered `false` for every role — the only
|
|
170
|
+
way through was the `"*"` short-circuit. Both now wrap the group under its resource
|
|
171
|
+
key, matching the nine composables that were already nested and working.
|
|
172
|
+
|
|
173
|
+
User-visible effect: ticking a site-settings or bulletin-board box in the role
|
|
174
|
+
editor now actually shows the matching panel or control. Previously the Site
|
|
175
|
+
Settings screen came up with only the one ungated panel no matter what was granted.
|
|
176
|
+
|
|
177
|
+
- 7574d24: Keep the Edit/Create Plan dialog's Cancel and Save buttons on screen
|
|
178
|
+
- 51aac24: Remove seven unread gate-shaped props left over from the fail-open sweep
|
|
179
|
+
|
|
180
|
+
While auditing the 26 remaining `default: true` gate props tracked after #1276/#1277/#1278,
|
|
181
|
+
seven were found declared but never read anywhere in their own component — no template
|
|
182
|
+
binding, no `v-if`, no computed, no method body, no forwarding:
|
|
183
|
+
|
|
184
|
+
- `AccessManagement.vue`: `canUpdate`, `canDelete`
|
|
185
|
+
- `BuildingManagement/buildings.vue`: `canDelete`
|
|
186
|
+
- `BuildingManagement/units.vue`: `canUpdate`, `canDelete`
|
|
187
|
+
- `DocumentManagement.vue`: `canUpdate`, `canDelete`
|
|
188
|
+
|
|
189
|
+
Each of these four components separately declares specific, actually-used gates for the
|
|
190
|
+
same actions (e.g. `canUpdateBuilding` / `canDeleteBuilding` on `buildings.vue`, alongside
|
|
191
|
+
the dead bare `canDelete`), so the dead prop was never wired to anything on screen. Deleting
|
|
192
|
+
it changes no behaviour for any caller — it was already inert — and removes a prop whose
|
|
193
|
+
`default: true` looked like a live fail-open gate on a quick read.
|
|
194
|
+
|
|
195
|
+
This is unrelated to the `test/gate-defaults.test.mjs` contract (54 props / 22 components,
|
|
196
|
+
landed in #1277/#1278): none of these seven appear in that list, since that sweep only
|
|
197
|
+
covers props confirmed to be both bound by every caller AND actually read by the component.
|
|
198
|
+
`yarn test` still reports 279 passing, 0 failing.
|
|
199
|
+
|
|
200
|
+
- e9931aa: Remove twelve inert `definePageMeta` calls from components
|
|
201
|
+
|
|
202
|
+
`definePageMeta` is a pages-only compiler macro. In a component it is a no-op,
|
|
203
|
+
so these twelve calls set neither `middleware` nor `memberOnly` — they only read
|
|
204
|
+
as though the component secured itself. `memberOnly` is what makes
|
|
205
|
+
`plugins/secure-member.client.ts` populate `userAppRole`, and without it every
|
|
206
|
+
`can*` gate in the package answers `false`; the flag has to be on the consuming
|
|
207
|
+
page. Deleting the calls changes no behaviour and stops them being mistaken for
|
|
208
|
+
protection that is already in place.
|
|
209
|
+
|
|
210
|
+
- f2f594b: Resolve `userAppRole` once per signed-in session, on every authenticated route
|
|
211
|
+
|
|
212
|
+
The `secure-member` plugin is the only writer of `userAppRole` in the whole
|
|
213
|
+
system, and it returned early unless the route carried `memberOnly: true`. On
|
|
214
|
+
every other page the role stayed `null` for the life of the page, so permission
|
|
215
|
+
gates that default open granted controls to accounts that had no such
|
|
216
|
+
permission, and gates that fail closed denied them to real staff permanently.
|
|
217
|
+
|
|
218
|
+
Role resolution now runs for any authenticated route and is awaited before the
|
|
219
|
+
navigation completes, so no gate is evaluated against an unresolved role. A new
|
|
220
|
+
`userAppRoleStatus` (`"idle" | "loading" | "resolved"`) and `isAppRoleResolved`
|
|
221
|
+
on `useLocalSetup()` let an app tell "not loaded yet" apart from "loaded, and
|
|
222
|
+
this account genuinely holds no member document" — the second is the correct
|
|
223
|
+
final answer for Seven365 staff-console accounts and residents, and is no
|
|
224
|
+
longer a reason to redirect. A failed request stays unresolved and retries on
|
|
225
|
+
the next navigation instead of denying for the rest of the session.
|
|
226
|
+
|
|
227
|
+
The `memberOnly` redirects are unchanged, and no permission gate was touched.
|
|
228
|
+
|
|
229
|
+
- eaa8500: Wire the three dead role permissions, and stop a refused roles list looking like an empty one
|
|
230
|
+
|
|
231
|
+
`RolePermissionMain` declared `canViewRole`, `canViewByRole` and `canUpdateRole`
|
|
232
|
+
and referenced none of them. Ten pages across the estate pass them in good faith,
|
|
233
|
+
so "See all roles", "See role details" and "Update role" were switches that did
|
|
234
|
+
nothing: the row click was ungated and `RolePermissionFormPreviewUpdate` had no
|
|
235
|
+
permission prop at all, showing Edit to everybody. `canViewRole` and
|
|
236
|
+
`canViewByRole` now default to `true`, which is exactly the behaviour the nine
|
|
237
|
+
pages that do not pass them have always had.
|
|
238
|
+
|
|
239
|
+
The roles table also rendered "No data available" for three different situations
|
|
240
|
+
-- you may not see roles, the request failed, and this organisation has none. It
|
|
241
|
+
now says which. And both role forms read `error.response._data.message`
|
|
242
|
+
unguarded, so a network or timeout failure threw inside its own catch and showed
|
|
243
|
+
the user nothing; the message the preview dialog does show was also duplicated in
|
|
244
|
+
the template and printed twice.
|
|
245
|
+
|
|
246
|
+
- 159aa53: ServiceProviderMain now derives its add/invite/view gates from a real permission catalogue entry when the caller omits them
|
|
247
|
+
|
|
248
|
+
`canAddServiceProvider`, `canInviteServiceProvider` and `canViewServiceProviders` all
|
|
249
|
+
declared `default: true`, and no app bound any of the three -- there was no permission
|
|
250
|
+
catalogue key anywhere in the package for an app to bind them to, so every signed-in user
|
|
251
|
+
in every consumer could add, invite and view service providers regardless of role.
|
|
252
|
+
|
|
253
|
+
Added a `service-provider` family (`add-service-provider`, `invite-service-provider`,
|
|
254
|
+
`see-all-service-providers`) to `useCommonPermission`'s catalogue and a new
|
|
255
|
+
`useServiceProviderPermission` composable that reads it, the same shape as the existing
|
|
256
|
+
`useBulletinBoardPermission` / `useSettingsPermission` gates. `ServiceProviderMain` now
|
|
257
|
+
reads that composable as the fallback when a prop is unbound
|
|
258
|
+
(`props.canAddServiceProvider ?? derived.value`, etc.) -- an app that already passes a
|
|
259
|
+
prop explicitly still wins.
|
|
260
|
+
|
|
261
|
+
This closes the gap in-package. It does not add server-side enforcement:
|
|
262
|
+
`service-provider.route.ts` only requires an authenticated session today, the same as
|
|
263
|
+
`bulletin-board.route.ts` -- this catalogue key is the same frontend-only enforcement
|
|
264
|
+
layer the rest of this family already relies on, not a new pattern.
|
|
265
|
+
|
|
3
266
|
## 4.0.2
|
|
4
267
|
|
|
5
268
|
### Patch Changes
|
|
@@ -17,11 +280,11 @@
|
|
|
17
280
|
`utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
|
|
18
281
|
own rule from two endpoints the console already calls, unprojected:
|
|
19
282
|
|
|
20
|
-
|
|
21
|
-
|
|
283
|
+
GET /api/members/user/:user/app/admin the Seven365 staff membership
|
|
284
|
+
GET /api/roles/id/:role that membership's role document
|
|
22
285
|
|
|
23
|
-
|
|
24
|
-
|
|
286
|
+
owner = member.type === "admin" && role.type === "admin" && role.default === true
|
|
287
|
+
staff = member.type === "admin" && role.type === "admin"
|
|
25
288
|
|
|
26
289
|
`role.default` is the marker because it is the only property of a platform
|
|
27
290
|
staff role no API caller can set - `role.controller.ts` validates create and
|
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The canonical permission vocabulary for the iService365 estate.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS FILE EXISTS
|
|
5
|
+
*
|
|
6
|
+
* `roles.permissions` is a flat array of `"resource:action"` strings and the API
|
|
7
|
+
* validates it as `Joi.array().items(Joi.string())` -- any string at all. The
|
|
8
|
+
* grantable vocabulary therefore lives in the front end, spread across this
|
|
9
|
+
* layer and eleven web apps, and nothing has ever compared the three sides. The
|
|
10
|
+
* result, measured on `origin/main`:
|
|
11
|
+
*
|
|
12
|
+
* - work orders are spelled FOUR ways in shipped code -- `work_orders`,
|
|
13
|
+
* `workOrder`, `work_order` and `work-orders` -- so a role granted
|
|
14
|
+
* "see all work orders" in one app's role editor is refused by another app's
|
|
15
|
+
* screen. `DashboardMain.vue` already unions three of them and says in its
|
|
16
|
+
* own comment that it cannot fix this from inside one component;
|
|
17
|
+
* - visitors are `visitor-mgmt` and `visitorManagement`;
|
|
18
|
+
* - `visitor:create`, `visitor:update` and `visitor:check-out` are enforced by
|
|
19
|
+
* the server on the HID door-reader endpoints and exist in no catalogue
|
|
20
|
+
* anywhere, so nobody can grant them;
|
|
21
|
+
* - `incident-reports:see-ncident-reports` is a live typo in the security
|
|
22
|
+
* mobile app -- a permanently false check that throws nothing.
|
|
23
|
+
*
|
|
24
|
+
* Every one of those was invisible until somebody noticed a blank screen.
|
|
25
|
+
*
|
|
26
|
+
* WHAT THIS FILE IS, AND IS NOT
|
|
27
|
+
*
|
|
28
|
+
* It is a NAMING source of truth: the canonical spelling of every permission the
|
|
29
|
+
* estate uses, typed, plus a map from each legacy spelling to its canonical
|
|
30
|
+
* form. Importing a permission from here makes a typo a compile error instead of
|
|
31
|
+
* a silently false check.
|
|
32
|
+
*
|
|
33
|
+
* It is NOT a grant list and it changes no gate. Nothing in this package is
|
|
34
|
+
* rewired by adding it; `useCleaningPermission()` is still the catalogue
|
|
35
|
+
* `hasPermission` looks actions up in, and every existing check still asks for
|
|
36
|
+
* exactly the string it asked for before. Adopting a canonical spelling in place
|
|
37
|
+
* of a legacy one is a per-app change with its own behaviour risk (a role
|
|
38
|
+
* holding the old string stops matching, a role holding the new one starts) and
|
|
39
|
+
* is deliberately NOT done here. See `LEGACY_PERMISSION_ALIASES`.
|
|
40
|
+
*
|
|
41
|
+
* HOW "CANONICAL" WAS DECIDED
|
|
42
|
+
*
|
|
43
|
+
* 1. If the server READS the string, the server's spelling wins -- that is
|
|
44
|
+
* what actually gates data. This settled `site-settings:*`,
|
|
45
|
+
* `virtual-patrol:*`, `facility-mgmt:update-facility` (all of which the UI
|
|
46
|
+
* already agreed with) and `service-provider-mgmt` (where the UI was split
|
|
47
|
+
* and `core/utils/notification-category.util.ts:185` broke the tie).
|
|
48
|
+
* 2. Where the server reads none, the spelling used by the MOST apps wins --
|
|
49
|
+
* counting an app once, whether it checks the string in its own code or
|
|
50
|
+
* through a composable in this layer.
|
|
51
|
+
* 3. Where rule 2 ties, the estate's dominant convention wins: kebab-case
|
|
52
|
+
* resource, `-mgmt` suffix, `see-all-<plural>` for list actions. That is
|
|
53
|
+
* what the clear majority of the 279 shipped strings already look like.
|
|
54
|
+
*
|
|
55
|
+
* One family could not be settled by those rules and is deliberately left
|
|
56
|
+
* unresolved -- see `UNRESOLVED_SERVER_SPELLINGS`.
|
|
57
|
+
*
|
|
58
|
+
* KEEPING IT HONEST
|
|
59
|
+
*
|
|
60
|
+
* `test/permission-vocabulary.test.mjs` fails when this file and reality drift
|
|
61
|
+
* apart. Read its header for exactly what it does and does not catch.
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Canonical `resource -> actions`.
|
|
66
|
+
*
|
|
67
|
+
* Only canonical spellings appear here. A legacy spelling that some app still
|
|
68
|
+
* ships lives in `LEGACY_PERMISSION_ALIASES` and resolves to an entry below.
|
|
69
|
+
*
|
|
70
|
+
* Provenance: extracted from `iservice365-layer-common` plus all eleven
|
|
71
|
+
* `iservice365-web-app-*` repos at `origin/main`, structurally (every
|
|
72
|
+
* `hasPermission(...)` call site plus every permission-shaped literal), not by
|
|
73
|
+
* eyeballing.
|
|
74
|
+
*/
|
|
75
|
+
export const PERMISSIONS = {
|
|
76
|
+
// ---- shared administration: every app that has a role editor ----
|
|
77
|
+
members: [
|
|
78
|
+
"view-members",
|
|
79
|
+
"assign-member-role",
|
|
80
|
+
"suspend-member",
|
|
81
|
+
"activate-member",
|
|
82
|
+
"delete-member",
|
|
83
|
+
],
|
|
84
|
+
// `roles-and-permissions` is `-admin`'s spelling of the same five actions;
|
|
85
|
+
// eight apps say `roles`.
|
|
86
|
+
roles: [
|
|
87
|
+
"add-role",
|
|
88
|
+
"see-all-roles",
|
|
89
|
+
"see-role-details",
|
|
90
|
+
"update-role",
|
|
91
|
+
"delete-role",
|
|
92
|
+
],
|
|
93
|
+
invitations: ["create-invitation", "view-invitations", "cancel-invitation"],
|
|
94
|
+
|
|
95
|
+
// ---- work orders: the four-way split this catalogue exists to end ----
|
|
96
|
+
// `work_orders` is canonical on rule 2: four apps (`-landscape`,
|
|
97
|
+
// `-mechanical-electrical`, `-pool-mgmt`, `-security`) against two for
|
|
98
|
+
// `workOrder` and one for `work_order`. The server gates none of them; its
|
|
99
|
+
// only read is the notification filter at
|
|
100
|
+
// `core/utils/notification-category.util.ts:97`, which already accepts all
|
|
101
|
+
// four spellings defensively.
|
|
102
|
+
work_orders: [
|
|
103
|
+
"create-work-order",
|
|
104
|
+
"see-all-work-orders",
|
|
105
|
+
"see-work-order-details",
|
|
106
|
+
"update-work-order",
|
|
107
|
+
"delete-work-order",
|
|
108
|
+
],
|
|
109
|
+
|
|
110
|
+
feedbacks: [
|
|
111
|
+
"add-feedback",
|
|
112
|
+
"see-all-feedback",
|
|
113
|
+
"see-feedback-details",
|
|
114
|
+
"update-feedback",
|
|
115
|
+
"delete-feedback",
|
|
116
|
+
],
|
|
117
|
+
|
|
118
|
+
// `bulletin-board` is canonical on rule 2: this layer's
|
|
119
|
+
// `useBulletinBoardPermission` is what `-hygiene`, `-landscape`,
|
|
120
|
+
// `-mechanical-electrical`, `-pest-control` and `-pool-mgmt` render (five
|
|
121
|
+
// apps), against two spelling it `bulletin-board-mgmt`.
|
|
122
|
+
"bulletin-board": [
|
|
123
|
+
"add-bulletin-board",
|
|
124
|
+
"see-all-bulletin-boards",
|
|
125
|
+
"see-bulletin-board-details",
|
|
126
|
+
"update-bulletin-board",
|
|
127
|
+
"delete-bulletin-board",
|
|
128
|
+
],
|
|
129
|
+
// Videos are a separate resource, not a spelling of the board above.
|
|
130
|
+
"bulletin-videos-mgmt": [
|
|
131
|
+
"add-bulletin-video",
|
|
132
|
+
"see-all-bulletin-video",
|
|
133
|
+
"see-bulletin-video-details",
|
|
134
|
+
"update-bulletin-video",
|
|
135
|
+
"delete-bulletin-video",
|
|
136
|
+
],
|
|
137
|
+
|
|
138
|
+
// ---- site settings: the server reads five of these, so it decides ----
|
|
139
|
+
"site-settings": [
|
|
140
|
+
"can-view-site-settings",
|
|
141
|
+
"can-manage-site-settings",
|
|
142
|
+
"manage-site-information",
|
|
143
|
+
"manage-cctv-camera",
|
|
144
|
+
"manage-anpr-camera",
|
|
145
|
+
"manage-delivery-companies",
|
|
146
|
+
"manage-resident-visitors",
|
|
147
|
+
"manage-work-order-settings",
|
|
148
|
+
"manage-billing-and-soa-configuration",
|
|
149
|
+
"manage-nfc-management",
|
|
150
|
+
"manage-nfc-patrol-settings",
|
|
151
|
+
"manage-entry-pass",
|
|
152
|
+
"manage-red-dot-settings",
|
|
153
|
+
],
|
|
154
|
+
|
|
155
|
+
// ---- module apps (cleaning, landscape, M&E, pest, pool) ----
|
|
156
|
+
"area-mgmt": [
|
|
157
|
+
"see-all-areas",
|
|
158
|
+
"add-area",
|
|
159
|
+
"update-area",
|
|
160
|
+
"see-area-details",
|
|
161
|
+
"import-areas",
|
|
162
|
+
"delete-area",
|
|
163
|
+
],
|
|
164
|
+
"unit-mgmt": [
|
|
165
|
+
"see-all-units",
|
|
166
|
+
"add-unit",
|
|
167
|
+
"update-unit",
|
|
168
|
+
"see-unit-details",
|
|
169
|
+
"import-units",
|
|
170
|
+
"delete-unit",
|
|
171
|
+
],
|
|
172
|
+
"supply-mgmt": [
|
|
173
|
+
"see-all-supplies",
|
|
174
|
+
"add-supply",
|
|
175
|
+
"update-supply",
|
|
176
|
+
"delete-supply",
|
|
177
|
+
"add-stock",
|
|
178
|
+
"view-stock",
|
|
179
|
+
"checkout-item",
|
|
180
|
+
],
|
|
181
|
+
"checkout-item-mgmt": [
|
|
182
|
+
"see-all-checkouts",
|
|
183
|
+
"create-checkout",
|
|
184
|
+
"update-checkout",
|
|
185
|
+
"approve-checkout",
|
|
186
|
+
"disapprove-checkout",
|
|
187
|
+
"view-checkout-details",
|
|
188
|
+
],
|
|
189
|
+
"schedule-task-mgmt": [
|
|
190
|
+
"see-all-schedule-tasks",
|
|
191
|
+
"add-schedule-task",
|
|
192
|
+
"update-schedule-task",
|
|
193
|
+
"see-schedule-task-details",
|
|
194
|
+
"delete-schedule-task",
|
|
195
|
+
],
|
|
196
|
+
"attendance-mgmt": [
|
|
197
|
+
"see-all-attendance",
|
|
198
|
+
"see-attendance-details",
|
|
199
|
+
"manage-attendance-settings",
|
|
200
|
+
],
|
|
201
|
+
"my-attendance": ["see-own-attendance", "check-in-out"],
|
|
202
|
+
inventory: ["view-inventory"],
|
|
203
|
+
toilet_locations: ["create-toilet-location"],
|
|
204
|
+
|
|
205
|
+
// ---- per-trade schedules ----
|
|
206
|
+
// These four carry the same seven actions on purpose:
|
|
207
|
+
// `useCleaningSchedulePermission` here, `useSchedulePermission` in
|
|
208
|
+
// `-landscape`, and `DashboardMain` all match them by SUFFIX
|
|
209
|
+
// (`permission.endsWith(`-schedule-mgmt:${action}`)`), so the trade prefix is
|
|
210
|
+
// data, not vocabulary. A new trade needs no code change -- and, because the
|
|
211
|
+
// match is a suffix, no test in this package can see a new prefix until it is
|
|
212
|
+
// listed here. This list was built by grepping the four apps for the key, not
|
|
213
|
+
// from the literal sweep, which cannot see a resource that only ever exists as
|
|
214
|
+
// a catalogue object key.
|
|
215
|
+
// Written out four times rather than shared: `as const` needs a literal to
|
|
216
|
+
// keep each tuple's element types, which is what makes a typo a build error.
|
|
217
|
+
"cleaning-schedule-mgmt": [
|
|
218
|
+
"see-all-schedules",
|
|
219
|
+
"see-schedule-details",
|
|
220
|
+
"download-schedule",
|
|
221
|
+
"manage-schedule-tasks",
|
|
222
|
+
"generate-checklist",
|
|
223
|
+
"view-history",
|
|
224
|
+
"add-remarks",
|
|
225
|
+
],
|
|
226
|
+
"landscape-schedule-mgmt": [
|
|
227
|
+
"see-all-schedules",
|
|
228
|
+
"see-schedule-details",
|
|
229
|
+
"download-schedule",
|
|
230
|
+
"manage-schedule-tasks",
|
|
231
|
+
"generate-checklist",
|
|
232
|
+
"view-history",
|
|
233
|
+
"add-remarks",
|
|
234
|
+
],
|
|
235
|
+
"pest-schedule-mgmt": [
|
|
236
|
+
"see-all-schedules",
|
|
237
|
+
"see-schedule-details",
|
|
238
|
+
"download-schedule",
|
|
239
|
+
"manage-schedule-tasks",
|
|
240
|
+
"generate-checklist",
|
|
241
|
+
"view-history",
|
|
242
|
+
"add-remarks",
|
|
243
|
+
],
|
|
244
|
+
"pool-schedule-mgmt": [
|
|
245
|
+
"see-all-schedules",
|
|
246
|
+
"see-schedule-details",
|
|
247
|
+
"download-schedule",
|
|
248
|
+
"manage-schedule-tasks",
|
|
249
|
+
"generate-checklist",
|
|
250
|
+
"view-history",
|
|
251
|
+
"add-remarks",
|
|
252
|
+
],
|
|
253
|
+
|
|
254
|
+
// ---- property management ----
|
|
255
|
+
"people-mgmt": [
|
|
256
|
+
"see-all-people",
|
|
257
|
+
"add-people",
|
|
258
|
+
"update-people",
|
|
259
|
+
"see-people-details",
|
|
260
|
+
"see-people-nric",
|
|
261
|
+
"delete-people",
|
|
262
|
+
],
|
|
263
|
+
"building-mgmt": [
|
|
264
|
+
"see-all-buildings",
|
|
265
|
+
"add-building",
|
|
266
|
+
"update-building",
|
|
267
|
+
"see-building-details",
|
|
268
|
+
"delete-building",
|
|
269
|
+
],
|
|
270
|
+
// `-property-management`'s only spelling; no kebab counterpart ships anywhere,
|
|
271
|
+
// so rule 2 leaves it as-is rather than inventing one nobody uses.
|
|
272
|
+
buildingUnitManagement: [
|
|
273
|
+
"see-all-building-units",
|
|
274
|
+
"add-building-unit",
|
|
275
|
+
"update-building-unit",
|
|
276
|
+
"see-building-unit-details",
|
|
277
|
+
"delete-building-unit",
|
|
278
|
+
],
|
|
279
|
+
"document-mgmt": [
|
|
280
|
+
"add-document",
|
|
281
|
+
"add-folder",
|
|
282
|
+
"update-document",
|
|
283
|
+
"delete-document",
|
|
284
|
+
],
|
|
285
|
+
"emergency-contact": [
|
|
286
|
+
"see-all",
|
|
287
|
+
"see-details",
|
|
288
|
+
"add-emergency-contact",
|
|
289
|
+
"update-emergency-contact",
|
|
290
|
+
"delete-emergency-contact",
|
|
291
|
+
"add-emergency-contact-category",
|
|
292
|
+
"delete-emergency-contact-category",
|
|
293
|
+
],
|
|
294
|
+
"event-mgmt": [
|
|
295
|
+
"see-all-event",
|
|
296
|
+
"add-event",
|
|
297
|
+
"update-event",
|
|
298
|
+
"see-event-details",
|
|
299
|
+
"delete-event",
|
|
300
|
+
],
|
|
301
|
+
"facility-mgmt": [
|
|
302
|
+
"see-all-facility",
|
|
303
|
+
"add-facility",
|
|
304
|
+
"update-facility",
|
|
305
|
+
"see-facility-details",
|
|
306
|
+
"delete-facility",
|
|
307
|
+
],
|
|
308
|
+
"facility-booking-mgmt": [
|
|
309
|
+
"see-all-facility-booking",
|
|
310
|
+
"add-facility-booking",
|
|
311
|
+
"update-facility-booking",
|
|
312
|
+
"see-facility-booking-details",
|
|
313
|
+
"delete-facility-booking",
|
|
314
|
+
],
|
|
315
|
+
"entry-pass-mgmt": [
|
|
316
|
+
"view-entry-pass-settings",
|
|
317
|
+
"add-entry-pass-settings",
|
|
318
|
+
"update-entry-pass-settings",
|
|
319
|
+
],
|
|
320
|
+
"online-form-configuration": [
|
|
321
|
+
"add-online-form-configuration",
|
|
322
|
+
"update-online-form-configuration",
|
|
323
|
+
"delete-online-form-configuration",
|
|
324
|
+
],
|
|
325
|
+
"nfc-patrol": [
|
|
326
|
+
"see-all-nfc-patrol",
|
|
327
|
+
"add-nfc-patrol",
|
|
328
|
+
"update-nfc-patrol",
|
|
329
|
+
"delete-nfc-patrol",
|
|
330
|
+
],
|
|
331
|
+
"billing-mgmt": [
|
|
332
|
+
"see-all-billing-summary",
|
|
333
|
+
"see-all-billing-items",
|
|
334
|
+
"see-billing-items-details",
|
|
335
|
+
"add-billing-items",
|
|
336
|
+
"update-billing-items",
|
|
337
|
+
"delete-billing-items",
|
|
338
|
+
"add-billing-configuration",
|
|
339
|
+
"update-billing-configuration",
|
|
340
|
+
"see-billing-configuration-details",
|
|
341
|
+
],
|
|
342
|
+
"SOA-mgmt": ["see-all-soa", "see-soa-details", "add-soa", "download-soa"],
|
|
343
|
+
// `service-provider-mgmt` is canonical on rule 1: the server reads the
|
|
344
|
+
// resource at `core/utils/notification-category.util.ts:185`. The UI is split
|
|
345
|
+
// one-app-one-layer-composable, so without the server read this would have
|
|
346
|
+
// been a coin toss.
|
|
347
|
+
"service-provider-mgmt": [
|
|
348
|
+
"see-all-service-providers",
|
|
349
|
+
"add-service-provider",
|
|
350
|
+
"invite-service-provider",
|
|
351
|
+
],
|
|
352
|
+
"access-card-mgmt": [
|
|
353
|
+
"see-all-card",
|
|
354
|
+
"see-all-qr-tagging",
|
|
355
|
+
"view",
|
|
356
|
+
"add-access-card",
|
|
357
|
+
"update-access-card",
|
|
358
|
+
"replace-access-card",
|
|
359
|
+
"delete-access-card",
|
|
360
|
+
"assign-access-card",
|
|
361
|
+
"manage-hid",
|
|
362
|
+
],
|
|
363
|
+
|
|
364
|
+
// ---- security ----
|
|
365
|
+
"visitor-mgmt": [
|
|
366
|
+
"see-all-visitor",
|
|
367
|
+
"add-visitor",
|
|
368
|
+
"update-visitor",
|
|
369
|
+
"see-visitor-details",
|
|
370
|
+
"delete-visitor",
|
|
371
|
+
],
|
|
372
|
+
"vehicle-mgmt": [
|
|
373
|
+
"see-all-vehicles",
|
|
374
|
+
"add-vehicle",
|
|
375
|
+
"update-vehicle",
|
|
376
|
+
"see-vehicle-details",
|
|
377
|
+
"approve-vehicle",
|
|
378
|
+
"delete-vehicle",
|
|
379
|
+
],
|
|
380
|
+
"incident-reports": [
|
|
381
|
+
"see-incident-reports",
|
|
382
|
+
"see-incident-report-details",
|
|
383
|
+
"add-incident-report",
|
|
384
|
+
"update-incident-report",
|
|
385
|
+
"review-incident-report",
|
|
386
|
+
"delete-incident-report",
|
|
387
|
+
],
|
|
388
|
+
// The server reads five of these (`core/utils/camera-view.util.ts:64,77`), so
|
|
389
|
+
// rule 1 fixes the whole family. Note `see-all-virtual-patrol-question` is
|
|
390
|
+
// singular and `see-all-virtual-patrols` has no `-route`/`-log` suffix: both
|
|
391
|
+
// are verbatim production spelling and must not be "corrected".
|
|
392
|
+
"virtual-patrol": [
|
|
393
|
+
"see-all-virtual-patrol-route",
|
|
394
|
+
"see-all-virtual-patrol-logs",
|
|
395
|
+
"see-all-virtual-patrol-question",
|
|
396
|
+
"see-all-virtual-patrols",
|
|
397
|
+
"see-virtual-patrol-route-details",
|
|
398
|
+
"see-virtual-patrol-log-details",
|
|
399
|
+
"see-virtual-patrol-question-details",
|
|
400
|
+
"add-virtual-patrol-route",
|
|
401
|
+
"add-virtual-patrol-log",
|
|
402
|
+
"add-virtual-patrol-question",
|
|
403
|
+
"update-virtual-patrol-route",
|
|
404
|
+
"update-virtual-patrol-log",
|
|
405
|
+
"update-virtual-patrol-question",
|
|
406
|
+
"start-virtual-patrol-route",
|
|
407
|
+
"delete-virtual-patrol-route",
|
|
408
|
+
"delete-virtual-patrol-log",
|
|
409
|
+
"delete-virtual-patrol-question",
|
|
410
|
+
],
|
|
411
|
+
"daily-occurrence-books": [
|
|
412
|
+
"see-all-daily-occurrence",
|
|
413
|
+
"add-daily-occurrence",
|
|
414
|
+
"update-daily-occurrence",
|
|
415
|
+
"see-daily-occurrence-details",
|
|
416
|
+
"delete-daily-occurrence",
|
|
417
|
+
],
|
|
418
|
+
"daily-occurrence-entries": [
|
|
419
|
+
"see-all-daily-occurrence-entries",
|
|
420
|
+
"add-daily-occurrence-entries",
|
|
421
|
+
"update-daily-occurrence-entries",
|
|
422
|
+
"see-daily-occurrence-entries-details",
|
|
423
|
+
"delete-daily-occurrence-entries",
|
|
424
|
+
],
|
|
425
|
+
"manpower-monitoring": [
|
|
426
|
+
"see-all-manpower-monitoring-sites",
|
|
427
|
+
"see-manpower-monitoring-sites-detail",
|
|
428
|
+
"see-all-manpower-monitoring-attendance",
|
|
429
|
+
"download-manpower-monitoring-attendance",
|
|
430
|
+
"see-all-manpower-monitoring-settings",
|
|
431
|
+
"save-manpower-monitoring-general-settings",
|
|
432
|
+
"save-manpower-monitoring-designation-settings",
|
|
433
|
+
"delete-manpower-sites",
|
|
434
|
+
],
|
|
435
|
+
"robot-mgmt": [
|
|
436
|
+
"see-all-robots",
|
|
437
|
+
"add-robot",
|
|
438
|
+
"update-robot",
|
|
439
|
+
"see-robot-details",
|
|
440
|
+
"delete-robot",
|
|
441
|
+
],
|
|
442
|
+
|
|
443
|
+
// ---- organisation console (`-org`) ----
|
|
444
|
+
customers: [
|
|
445
|
+
"see-all-customers",
|
|
446
|
+
"add-customer",
|
|
447
|
+
"see-customer-details",
|
|
448
|
+
"delete-customer",
|
|
449
|
+
],
|
|
450
|
+
sites: ["see-all-sites", "add-site", "see-site-details", "delete-site"],
|
|
451
|
+
subscriptions: ["view-subscription-details", "management-subscription"],
|
|
452
|
+
} as const;
|
|
453
|
+
|
|
454
|
+
/** Every canonical resource name. */
|
|
455
|
+
export type TPermissionResource = keyof typeof PERMISSIONS;
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Every canonical `"resource:action"` string, as a union type.
|
|
459
|
+
*
|
|
460
|
+
* This is the typo guard: a misspelt permission is not assignable to
|
|
461
|
+
* `TPermissionString`, so it fails `yarn build` instead of quietly answering
|
|
462
|
+
* `false` forever the way `incident-reports:see-ncident-reports` does today.
|
|
463
|
+
*/
|
|
464
|
+
export type TPermissionString = {
|
|
465
|
+
[R in TPermissionResource]: `${R}:${(typeof PERMISSIONS)[R][number]}`;
|
|
466
|
+
}[TPermissionResource];
|
|
467
|
+
|
|
468
|
+
/** The wildcard every gate in the estate short-circuits on. */
|
|
469
|
+
export const PERMISSION_WILDCARD = "*" as const;
|
|
470
|
+
|
|
471
|
+
/** Every canonical string, flattened. Frozen so a consumer cannot mutate it. */
|
|
472
|
+
export const PERMISSION_STRINGS: readonly TPermissionString[] = Object.freeze(
|
|
473
|
+
Object.entries(PERMISSIONS).flatMap(([resource, actions]) =>
|
|
474
|
+
(actions as readonly string[]).map((action) => `${resource}:${action}`),
|
|
475
|
+
) as TPermissionString[],
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Build a canonical permission string with the compiler checking both halves.
|
|
480
|
+
*
|
|
481
|
+
* `permission("work_orders", "see-all-work-orders")` compiles;
|
|
482
|
+
* `permission("work_orders", "see-all-work-order")` does not.
|
|
483
|
+
*/
|
|
484
|
+
export function permission<
|
|
485
|
+
R extends TPermissionResource,
|
|
486
|
+
A extends (typeof PERMISSIONS)[R][number],
|
|
487
|
+
>(resource: R, action: A): `${R}:${A}` {
|
|
488
|
+
return `${resource}:${action}` as `${R}:${A}`;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Legacy spelling -> canonical spelling.
|
|
493
|
+
*
|
|
494
|
+
* Every key is a string SHIPPED TODAY by something that still runs, so a role
|
|
495
|
+
* out there may be holding it. Nothing here is renamed by this package; the map
|
|
496
|
+
* exists so a consumer can accept both spellings during a migration and so the
|
|
497
|
+
* drift test can tell "an old spelling we know about" from "a new string nobody
|
|
498
|
+
* catalogued".
|
|
499
|
+
*
|
|
500
|
+
* Each entry names the consumer that still needs it. An entry can be deleted
|
|
501
|
+
* once that consumer ships the canonical string AND the roles carrying the old
|
|
502
|
+
* one have been migrated -- deleting it earlier strands whoever holds the old
|
|
503
|
+
* string.
|
|
504
|
+
*/
|
|
505
|
+
export const LEGACY_PERMISSION_ALIASES: Readonly<
|
|
506
|
+
Record<string, TPermissionString>
|
|
507
|
+
> = Object.freeze({
|
|
508
|
+
// --- work orders (four spellings, one resource) ---
|
|
509
|
+
// Needed by: `-property-management`, `-pest-control` (own catalogues) and
|
|
510
|
+
// `DashboardMain.vue`'s union in this package.
|
|
511
|
+
"workOrder:create-work-order": "work_orders:create-work-order",
|
|
512
|
+
"workOrder:see-all-work-orders": "work_orders:see-all-work-orders",
|
|
513
|
+
"workOrder:see-work-order-details": "work_orders:see-work-order-details",
|
|
514
|
+
"workOrder:update-work-order": "work_orders:update-work-order",
|
|
515
|
+
"workOrder:delete-work-order": "work_orders:delete-work-order",
|
|
516
|
+
// Needed by: `-hygiene`, and by `useCleaningPermission()` in this package,
|
|
517
|
+
// whose catalogue key is `work_order`.
|
|
518
|
+
"work_order:create-work-order": "work_orders:create-work-order",
|
|
519
|
+
"work_order:see-all-work-orders": "work_orders:see-all-work-orders",
|
|
520
|
+
"work_order:see-work-order-details": "work_orders:see-work-order-details",
|
|
521
|
+
"work_order:update-work-order": "work_orders:update-work-order",
|
|
522
|
+
"work_order:delete-work-order": "work_orders:delete-work-order",
|
|
523
|
+
// Needed by: the security mobile app (`isecure365-mobile-app`), and accepted
|
|
524
|
+
// defensively by `core/utils/notification-category.util.ts:97`. No web app
|
|
525
|
+
// ships this spelling.
|
|
526
|
+
"work-orders:create-work-order": "work_orders:create-work-order",
|
|
527
|
+
"work-orders:see-all-work-orders": "work_orders:see-all-work-orders",
|
|
528
|
+
"work-orders:update-work-order": "work_orders:update-work-order",
|
|
529
|
+
"work-orders:delete-work-order": "work_orders:delete-work-order",
|
|
530
|
+
// Needed by: `-security`, which says `add-` where every other app says
|
|
531
|
+
// `create-`.
|
|
532
|
+
"work_orders:add-work-order": "work_orders:create-work-order",
|
|
533
|
+
"work-orders:add-work-order": "work_orders:create-work-order",
|
|
534
|
+
|
|
535
|
+
// --- visitors ---
|
|
536
|
+
// Needed by: `-property-management`, and by `DashboardMain.vue`'s union here.
|
|
537
|
+
"visitorManagement:see-all-visitor": "visitor-mgmt:see-all-visitor",
|
|
538
|
+
"visitorManagement:add-visitor": "visitor-mgmt:add-visitor",
|
|
539
|
+
"visitorManagement:update-visitor": "visitor-mgmt:update-visitor",
|
|
540
|
+
"visitorManagement:see-visitor-details": "visitor-mgmt:see-visitor-details",
|
|
541
|
+
"visitorManagement:delete-visitor": "visitor-mgmt:delete-visitor",
|
|
542
|
+
|
|
543
|
+
// --- bulletin board ---
|
|
544
|
+
// Needed by: `-property-management` and `-security`, which both suffix the
|
|
545
|
+
// resource `-mgmt` while the five module apps do not.
|
|
546
|
+
"bulletin-board-mgmt:add-bulletin-board": "bulletin-board:add-bulletin-board",
|
|
547
|
+
"bulletin-board-mgmt:see-all-bulletin-boards":
|
|
548
|
+
"bulletin-board:see-all-bulletin-boards",
|
|
549
|
+
"bulletin-board-mgmt:see-bulletin-board-details":
|
|
550
|
+
"bulletin-board:see-bulletin-board-details",
|
|
551
|
+
"bulletin-board-mgmt:update-bulletin-board":
|
|
552
|
+
"bulletin-board:update-bulletin-board",
|
|
553
|
+
"bulletin-board-mgmt:delete-bulletin-board":
|
|
554
|
+
"bulletin-board:delete-bulletin-board",
|
|
555
|
+
// Needed by: `-property-management` and the security mobile app, which say
|
|
556
|
+
// "board" singular where everyone else says "boards".
|
|
557
|
+
"bulletin-board-mgmt:see-all-bulletin-board":
|
|
558
|
+
"bulletin-board:see-all-bulletin-boards",
|
|
559
|
+
|
|
560
|
+
// --- facility bookings ---
|
|
561
|
+
// Needed by: `DashboardMain.vue`'s module-mode widget map in this package,
|
|
562
|
+
// the only place that drops the `-mgmt`.
|
|
563
|
+
"facility-booking:see-all-facility-booking":
|
|
564
|
+
"facility-booking-mgmt:see-all-facility-booking",
|
|
565
|
+
|
|
566
|
+
// --- feedback ---
|
|
567
|
+
// Needed by: `DashboardMain.vue`'s union here (both of these), and by
|
|
568
|
+
// `-security`, which pluralises the action.
|
|
569
|
+
"feedback:see-all-feedback": "feedbacks:see-all-feedback",
|
|
570
|
+
"feedbacks:see-all-feedbacks": "feedbacks:see-all-feedback",
|
|
571
|
+
|
|
572
|
+
// --- incident reports ---
|
|
573
|
+
// Needed by: `DashboardMain.vue`'s union here. No app ships the `-all-` form.
|
|
574
|
+
"incident-reports:see-all-incident-reports":
|
|
575
|
+
"incident-reports:see-incident-reports",
|
|
576
|
+
|
|
577
|
+
// --- buildings ---
|
|
578
|
+
// Needed by: `-property-management`. `-security` already spells it kebab.
|
|
579
|
+
"buildingManagement:see-all-buildings": "building-mgmt:see-all-buildings",
|
|
580
|
+
"buildingManagement:add-building": "building-mgmt:add-building",
|
|
581
|
+
"buildingManagement:update-building": "building-mgmt:update-building",
|
|
582
|
+
"buildingManagement:see-building-details":
|
|
583
|
+
"building-mgmt:see-building-details",
|
|
584
|
+
"buildingManagement:delete-building": "building-mgmt:delete-building",
|
|
585
|
+
|
|
586
|
+
// --- roles ---
|
|
587
|
+
// Needed by: `-admin`, which is retired by redirect but still deployed.
|
|
588
|
+
"roles-and-permissions:add-role": "roles:add-role",
|
|
589
|
+
"roles-and-permissions:see-all-roles": "roles:see-all-roles",
|
|
590
|
+
"roles-and-permissions:see-role-details": "roles:see-role-details",
|
|
591
|
+
"roles-and-permissions:update-role": "roles:update-role",
|
|
592
|
+
"roles-and-permissions:delete-role": "roles:delete-role",
|
|
593
|
+
|
|
594
|
+
// --- service providers ---
|
|
595
|
+
// Needed by: `useServiceProviderPermission` in this package, which drops the
|
|
596
|
+
// `-mgmt` the server's notification filter expects.
|
|
597
|
+
"service-provider:see-all-service-providers":
|
|
598
|
+
"service-provider-mgmt:see-all-service-providers",
|
|
599
|
+
"service-provider:add-service-provider":
|
|
600
|
+
"service-provider-mgmt:add-service-provider",
|
|
601
|
+
"service-provider:invite-service-provider":
|
|
602
|
+
"service-provider-mgmt:invite-service-provider",
|
|
603
|
+
// Needed by: `-property-management`, which says `view-` where this layer says
|
|
604
|
+
// `see-all-`.
|
|
605
|
+
"service-provider-mgmt:view-service-providers":
|
|
606
|
+
"service-provider-mgmt:see-all-service-providers",
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Strings the SERVER enforces that no UI checks and no role editor can grant.
|
|
611
|
+
*
|
|
612
|
+
* `core/src/utils/hid-access.util.ts:36-38` gates the HID Amico visitor
|
|
613
|
+
* endpoints on `visitor:create`, `visitor:update` and `visitor:check-out`. The
|
|
614
|
+
* whole estate spells visitor rights `visitor-mgmt:add-visitor`,
|
|
615
|
+
* `:update-visitor` and so on; a bare `visitor` resource appears in no catalogue
|
|
616
|
+
* in any repo, so no role can hold these and the branch is reachable only
|
|
617
|
+
* through `*` or through the `access-card-mgmt` strings the same list unions in.
|
|
618
|
+
*
|
|
619
|
+
* They are NOT aliased to `visitor-mgmt:*` here, because doing so would be a
|
|
620
|
+
* decision with a real consequence in both directions:
|
|
621
|
+
*
|
|
622
|
+
* - teaching the server to read `visitor-mgmt:add-visitor` would take a check
|
|
623
|
+
* that is dead today and make it LIVE for every role already granted visitor
|
|
624
|
+
* management -- widening who may write a credential to a door reader;
|
|
625
|
+
* - adding `visitor:create` to the role editors would create a second visitor
|
|
626
|
+
* vocabulary next to the one already in use.
|
|
627
|
+
*
|
|
628
|
+
* Which of the two is right is a backend-lead and owner call, not this file's.
|
|
629
|
+
* Recorded rather than guessed, and pinned by the drift test so the list cannot
|
|
630
|
+
* grow without somebody noticing.
|
|
631
|
+
*
|
|
632
|
+
* (The estate's previous example of this, `vehicleManagement:approve-vehicle`,
|
|
633
|
+
* was fixed in `core` before this catalogue was written: `vehicle.service.ts:133`
|
|
634
|
+
* on `origin/main` now reads `vehicle-mgmt:approve-vehicle`, which every
|
|
635
|
+
* catalogue can grant. Notes written before 2026-08-23 still describe the old
|
|
636
|
+
* spelling.)
|
|
637
|
+
*/
|
|
638
|
+
export const SERVER_ONLY_UNGRANTABLE: readonly string[] = Object.freeze([
|
|
639
|
+
"visitor:create",
|
|
640
|
+
"visitor:update",
|
|
641
|
+
"visitor:check-out",
|
|
642
|
+
]);
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Canonical strings that are CHECKED -- by this package and by the server -- and
|
|
646
|
+
* that no role editor can GRANT.
|
|
647
|
+
*
|
|
648
|
+
* `NavigationDrawer.vue:172-180` accepts eight `access-card-mgmt` strings for
|
|
649
|
+
* the HID menu and `core/src/utils/hid-access.util.ts:19-32` enforces all nine;
|
|
650
|
+
* only six of them appear in any app's catalogue. The menu still opens, because
|
|
651
|
+
* the gate is an OR and the other six are grantable -- but a role built out of
|
|
652
|
+
* only these three is refused at both ends while looking correctly configured in
|
|
653
|
+
* the role editor. Frozen here so the list cannot grow without somebody noticing.
|
|
654
|
+
*/
|
|
655
|
+
export const CHECKED_BUT_UNGRANTABLE: readonly TPermissionString[] =
|
|
656
|
+
Object.freeze([
|
|
657
|
+
"access-card-mgmt:view",
|
|
658
|
+
"access-card-mgmt:assign-access-card",
|
|
659
|
+
"access-card-mgmt:manage-hid",
|
|
660
|
+
]);
|
|
661
|
+
|
|
662
|
+
const CANONICAL = new Set<string>(PERMISSION_STRINGS);
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Resolve any shipped spelling to its canonical form.
|
|
666
|
+
*
|
|
667
|
+
* Returns `undefined` for a string this catalogue has never heard of -- which is
|
|
668
|
+
* the signal the drift test looks for.
|
|
669
|
+
*/
|
|
670
|
+
export function canonicalPermission(
|
|
671
|
+
value: string,
|
|
672
|
+
): TPermissionString | undefined {
|
|
673
|
+
if (CANONICAL.has(value)) return value as TPermissionString;
|
|
674
|
+
return LEGACY_PERMISSION_ALIASES[value];
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/** Is this string one the catalogue knows, canonical or legacy? */
|
|
678
|
+
export function isKnownPermission(value: string): boolean {
|
|
679
|
+
return canonicalPermission(value) !== undefined;
|
|
680
|
+
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@7365admin1/layer-common",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "4.0.
|
|
5
|
+
"version": "4.0.4-staging.237",
|
|
6
6
|
"author": "7365admin1",
|
|
7
7
|
"main": "./nuxt.config.ts",
|
|
8
8
|
"//files": "What a consumer extending this layer actually loads. Without this npm ships the whole working tree - the changesets, the CI workflows, the render harness in tools/ and any scratch directory that happened to exist at publish time. Nuxt resolves a layer by directory, so every runtime directory below has to stay listed; adding a new top-level runtime directory means adding it here too.",
|