@7365admin1/layer-common 4.132.0 → 4.133.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 +10 -4
- package/components/RolePermissionFormCreate.vue +5 -1
- package/components/RolePermissionFormPreviewUpdate.vue +34 -4
- package/components/RolePermissionMain.vue +3 -1
- package/components/VehicleAddSelection.vue +5 -1
- package/components/VehicleManagement.vue +10 -4
- package/composables/useSecurityPermission.ts +19 -0
- package/constants/permissions.ts +7 -0
- package/package.json +1 -1
- package/utils/org-modules.ts +17 -1
- package/utils/security-catalogue.ts +59 -0
- package/utils/vehicle-actions.ts +10 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @iservice365/layer-common
|
|
2
2
|
|
|
3
|
+
## 4.133.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2cfd359: Security roles: new permission boxes - See Blocklist, Block / Unblock Vehicles, Invite Visitor / Contractor, See Offline Data (needs core with the same keys). A security role gets a hidden `catalogue:security-v2` marker the first time it is saved in the editor; roles never saved keep today's access (the new boxes are pre-ticked from the old keys when opened). The marker is not counted in permission totals. `VehicleManagement` / `VehicleAddSelection` take optional `canSeeBlocklist` / `canManageBlocklist` props (default true = unchanged). Helper: `utils/security-catalogue`.
|
|
8
|
+
|
|
3
9
|
## 4.132.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
|
@@ -3346,11 +3352,11 @@ Monthly report: a month that had patrols opens the list of that month's patrol r
|
|
|
3346
3352
|
`utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
|
|
3347
3353
|
own rule from two endpoints the console already calls, unprojected:
|
|
3348
3354
|
|
|
3349
|
-
|
|
3350
|
-
|
|
3355
|
+
GET /api/members/user/:user/app/admin the Seven365 staff membership
|
|
3356
|
+
GET /api/roles/id/:role that membership's role document
|
|
3351
3357
|
|
|
3352
|
-
|
|
3353
|
-
|
|
3358
|
+
owner = member.type === "admin" && role.type === "admin" && role.default === true
|
|
3359
|
+
staff = member.type === "admin" && role.type === "admin"
|
|
3354
3360
|
|
|
3355
3361
|
`role.default` is the marker because it is the only property of a platform
|
|
3356
3362
|
staff role no API caller can set - `role.controller.ts` validates create and
|
|
@@ -283,6 +283,7 @@ import useRole from "../composables/useRole";
|
|
|
283
283
|
import useUtils from "../composables/useUtils";
|
|
284
284
|
import useSite from "../composables/useSite";
|
|
285
285
|
import { readApiError } from "../utils/subscription-form";
|
|
286
|
+
import { withSecurityMarker } from "../utils/security-catalogue";
|
|
286
287
|
import { applyRolePreset, ROLE_PRESETS, type TRolePreset } from "../utils/inventory-presets";
|
|
287
288
|
|
|
288
289
|
/** Shown after "Couldn't save:" when the server gave no reason (network, timeout). */
|
|
@@ -533,7 +534,10 @@ async function submit() {
|
|
|
533
534
|
try {
|
|
534
535
|
await createRole({
|
|
535
536
|
name: name.value,
|
|
536
|
-
|
|
537
|
+
// Security catalogue only (it carries `offline-data`): stamp the v2 marker.
|
|
538
|
+
permissions: "offline-data" in (props.permissions || {})
|
|
539
|
+
? withSecurityMarker(permState.value.selected)
|
|
540
|
+
: permState.value.selected,
|
|
537
541
|
type: props.type,
|
|
538
542
|
org: props.org,
|
|
539
543
|
site: props.siteId,
|
|
@@ -272,6 +272,11 @@ import {
|
|
|
272
272
|
import useRole from "../composables/useRole";
|
|
273
273
|
import useUtils from "../composables/useUtils";
|
|
274
274
|
import { readApiError } from "../utils/subscription-form";
|
|
275
|
+
import {
|
|
276
|
+
countRealPermissions,
|
|
277
|
+
securityV2PreTicks,
|
|
278
|
+
withSecurityMarker,
|
|
279
|
+
} from "../utils/security-catalogue";
|
|
275
280
|
import {
|
|
276
281
|
holdsPermission,
|
|
277
282
|
withoutPermission,
|
|
@@ -348,8 +353,8 @@ const totalLabel = computed(() => {
|
|
|
348
353
|
const held = definedModel.value ?? [];
|
|
349
354
|
if (holdsWildcard.value) return "All";
|
|
350
355
|
return props.catalogueSize > 0
|
|
351
|
-
? `${held
|
|
352
|
-
: String(held
|
|
356
|
+
? `${countRealPermissions(held)} / ${props.catalogueSize}`
|
|
357
|
+
: String(countRealPermissions(held));
|
|
353
358
|
});
|
|
354
359
|
|
|
355
360
|
/**
|
|
@@ -638,6 +643,23 @@ const edit = defineModel<boolean>("edit", {
|
|
|
638
643
|
default: false,
|
|
639
644
|
});
|
|
640
645
|
|
|
646
|
+
/**
|
|
647
|
+
* Security catalogue only: the Security tree carries the `offline-data`
|
|
648
|
+
* module. Other apps' editors never match, so they are untouched.
|
|
649
|
+
*/
|
|
650
|
+
const isSecurityCatalogue = computed(
|
|
651
|
+
() => "offline-data" in (props.permissions as Record<string, unknown>)
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
// Opening an UNMARKED security role for edit: pre-tick the new boxes the role
|
|
655
|
+
// already effectively has (legacy fallback), so an unrelated save never strips
|
|
656
|
+
// access. Cancel restores the original list.
|
|
657
|
+
watch(edit, (on) => {
|
|
658
|
+
if (!on || !isSecurityCatalogue.value) return;
|
|
659
|
+
const add = securityV2PreTicks(definedModel.value ?? []);
|
|
660
|
+
if (add.length) definedModel.value = [...definedModel.value, ...add];
|
|
661
|
+
});
|
|
662
|
+
|
|
641
663
|
/**
|
|
642
664
|
* CANCEL THROWS THE DRAFT AWAY.
|
|
643
665
|
*
|
|
@@ -688,7 +710,10 @@ const errorMessages = computed(() => {
|
|
|
688
710
|
return equalValue;
|
|
689
711
|
}
|
|
690
712
|
|
|
691
|
-
|
|
713
|
+
// The marker is not a permission: a list holding only it is still empty.
|
|
714
|
+
const requiredValue = requireListRule(
|
|
715
|
+
(definedModel.value ?? []).filter((p) => countRealPermissions([p]) > 0)
|
|
716
|
+
);
|
|
692
717
|
if (requiredValue) {
|
|
693
718
|
return requiredValue;
|
|
694
719
|
}
|
|
@@ -706,7 +731,12 @@ async function submit() {
|
|
|
706
731
|
disable.value = true;
|
|
707
732
|
message.value = "";
|
|
708
733
|
try {
|
|
709
|
-
await updatePermissionById(
|
|
734
|
+
await updatePermissionById(
|
|
735
|
+
props.id,
|
|
736
|
+
isSecurityCatalogue.value
|
|
737
|
+
? withSecurityMarker(definedModel.value)
|
|
738
|
+
: definedModel.value
|
|
739
|
+
);
|
|
710
740
|
emit("success");
|
|
711
741
|
} catch (error: any) {
|
|
712
742
|
// `error.response` is undefined on a network or timeout failure, so the old
|
|
@@ -208,6 +208,7 @@
|
|
|
208
208
|
// Relative import: this layer's `utils/` is not auto-imported into consumers.
|
|
209
209
|
import { roleLabel, safeReturnTo, showSiteField, sitePickerRoleLabel } from "../utils/role";
|
|
210
210
|
import { holdsPermission } from "../utils/permission-spellings";
|
|
211
|
+
import { countRealPermissions } from "../utils/security-catalogue";
|
|
211
212
|
import {
|
|
212
213
|
catalogueForOrg,
|
|
213
214
|
catalogueForOwnerGrant,
|
|
@@ -570,7 +571,8 @@ function getPermissionCount(permissions: unknown): string {
|
|
|
570
571
|
if (held.includes("*")) return "All";
|
|
571
572
|
|
|
572
573
|
const offered = permissionCatalogueSize.value;
|
|
573
|
-
|
|
574
|
+
const count = countRealPermissions(held);
|
|
575
|
+
return offered > 0 ? `${count} / ${offered}` : String(count);
|
|
574
576
|
}
|
|
575
577
|
|
|
576
578
|
function updateRolePermissionsInList(
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
type TVehicleAddChoice = TVehicleType | "seasonpass" | "whitelist-bulk";
|
|
28
28
|
|
|
29
29
|
const prop = defineProps({
|
|
30
|
+
// Security catalogue v2: false hides the Blocklist choice. Default = today.
|
|
31
|
+
canManageBlocklist: { type: Boolean, default: true },
|
|
30
32
|
});
|
|
31
33
|
|
|
32
34
|
const emit = defineEmits(['cancel', 'select']);
|
|
@@ -37,7 +39,9 @@ const selection = computed<{label: string, value: TVehicleAddChoice}[]>(() => {
|
|
|
37
39
|
// Many whitelist vehicles at once, typed or pasted into a sheet.
|
|
38
40
|
{ label: "Whitelist (Bulk)", value: "whitelist-bulk" },
|
|
39
41
|
{ label: "Season Pass", value: "seasonpass" },
|
|
40
|
-
|
|
42
|
+
...(prop.canManageBlocklist
|
|
43
|
+
? [{ label: "Blocklist", value: "blocklist" as TVehicleAddChoice }]
|
|
44
|
+
: []),
|
|
41
45
|
];
|
|
42
46
|
})
|
|
43
47
|
|
|
@@ -137,7 +137,7 @@
|
|
|
137
137
|
<!-- <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" /> -->
|
|
138
138
|
|
|
139
139
|
<v-dialog v-model="dialog.showSelection" width="450" persistent>
|
|
140
|
-
<VehicleAddSelection @cancel="dialog.showSelection = false" @select="handleSelectVehicleStatus" />
|
|
140
|
+
<VehicleAddSelection :can-manage-blocklist="canManageBlocklist" @cancel="dialog.showSelection = false" @select="handleSelectVehicleStatus" />
|
|
141
141
|
</v-dialog>
|
|
142
142
|
|
|
143
143
|
<!-- Many whitelist vehicles at once. Wider than the one-vehicle form: it
|
|
@@ -495,6 +495,9 @@ const props = defineProps({
|
|
|
495
495
|
canDeleteVehicle: { type: Boolean, default: false },
|
|
496
496
|
canViewVehicleDetails: { type: Boolean, default: false },
|
|
497
497
|
canApproveVehicle: { type: Boolean, default: false },
|
|
498
|
+
// Security catalogue v2. Defaults keep today's behaviour for every other app.
|
|
499
|
+
canSeeBlocklist: { type: Boolean, default: true },
|
|
500
|
+
canManageBlocklist: { type: Boolean, default: undefined },
|
|
498
501
|
site: { type: String, required: true },
|
|
499
502
|
org: { type: String, required: true },
|
|
500
503
|
app: { type: String, required: true },
|
|
@@ -578,11 +581,13 @@ const historyHeaders = [
|
|
|
578
581
|
* resident / season pass that was blocked
|
|
579
582
|
*/
|
|
580
583
|
type TVehicleTab = "residents" | "season-passes" | "block-listed";
|
|
581
|
-
const vehicleTabs
|
|
584
|
+
const vehicleTabs = computed<{ name: string; value: TVehicleTab }[]>(() => [
|
|
582
585
|
{ name: "Residents", value: "residents" },
|
|
583
586
|
{ name: "Season Passes", value: "season-passes" },
|
|
584
|
-
|
|
585
|
-
]
|
|
587
|
+
...(props.canSeeBlocklist
|
|
588
|
+
? [{ name: "Block Listed", value: "block-listed" as TVehicleTab }]
|
|
589
|
+
: []),
|
|
590
|
+
])
|
|
586
591
|
const activeTab = ref<TVehicleTab>("residents");
|
|
587
592
|
|
|
588
593
|
|
|
@@ -1134,6 +1139,7 @@ const vehiclePermissions = computed(() => ({
|
|
|
1134
1139
|
canUpdateVehicle: props.canUpdateVehicle && !revoked.update,
|
|
1135
1140
|
canDeleteVehicle: props.canDeleteVehicle && !revoked.delete,
|
|
1136
1141
|
canApproveVehicle: props.canApproveVehicle && !revoked.approve,
|
|
1142
|
+
canManageBlocklist: props.canManageBlocklist,
|
|
1137
1143
|
}));
|
|
1138
1144
|
|
|
1139
1145
|
/**
|
|
@@ -115,6 +115,14 @@ export default function useSecurityPermission() {
|
|
|
115
115
|
},
|
|
116
116
|
|
|
117
117
|
"vehicle-mgmt": {
|
|
118
|
+
"see-blocklist": {
|
|
119
|
+
check: true,
|
|
120
|
+
description: "See Blocklist",
|
|
121
|
+
},
|
|
122
|
+
"manage-blocklist": {
|
|
123
|
+
check: true,
|
|
124
|
+
description: "Block / Unblock Vehicles",
|
|
125
|
+
},
|
|
118
126
|
"add-vehicle": {
|
|
119
127
|
check: true,
|
|
120
128
|
description: "Allows the user to add a new vehicle to the system.",
|
|
@@ -291,6 +299,10 @@ export default function useSecurityPermission() {
|
|
|
291
299
|
},
|
|
292
300
|
|
|
293
301
|
"visitor-mgmt": {
|
|
302
|
+
"invite-visitor": {
|
|
303
|
+
check: true,
|
|
304
|
+
description: "Invite Visitor / Contractor",
|
|
305
|
+
},
|
|
294
306
|
"add-visitor": {
|
|
295
307
|
check: true,
|
|
296
308
|
description: "Allows the user to add a new visitor to the system.",
|
|
@@ -814,6 +826,13 @@ export default function useSecurityPermission() {
|
|
|
814
826
|
|
|
815
827
|
},
|
|
816
828
|
|
|
829
|
+
"offline-data": {
|
|
830
|
+
"see-offline-data": {
|
|
831
|
+
check: true,
|
|
832
|
+
description: "See Offline Data",
|
|
833
|
+
},
|
|
834
|
+
},
|
|
835
|
+
|
|
817
836
|
};
|
|
818
837
|
|
|
819
838
|
return {
|
package/constants/permissions.ts
CHANGED
|
@@ -391,7 +391,12 @@ export const PERMISSIONS = {
|
|
|
391
391
|
],
|
|
392
392
|
|
|
393
393
|
// ---- security ----
|
|
394
|
+
// Security catalogue v2 (2026-10-20). `catalogue:security-v2` is the marker the
|
|
395
|
+
// role editor stamps on save; it grants nothing (see utils/security-catalogue.ts).
|
|
396
|
+
"offline-data": ["see-offline-data"],
|
|
397
|
+
catalogue: ["security-v2"],
|
|
394
398
|
"visitor-mgmt": [
|
|
399
|
+
"invite-visitor",
|
|
395
400
|
"see-all-visitor",
|
|
396
401
|
"add-visitor",
|
|
397
402
|
"update-visitor",
|
|
@@ -399,6 +404,8 @@ export const PERMISSIONS = {
|
|
|
399
404
|
"delete-visitor",
|
|
400
405
|
],
|
|
401
406
|
"vehicle-mgmt": [
|
|
407
|
+
"see-blocklist",
|
|
408
|
+
"manage-blocklist",
|
|
402
409
|
"see-all-vehicles",
|
|
403
410
|
"add-vehicle",
|
|
404
411
|
"update-vehicle",
|
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.
|
|
5
|
+
"version": "4.133.0",
|
|
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.",
|
package/utils/org-modules.ts
CHANGED
|
@@ -54,6 +54,7 @@ export const ALWAYS_ALLOWED_MODULES: readonly string[] = Object.freeze([
|
|
|
54
54
|
"roles",
|
|
55
55
|
"roles-and-permissions",
|
|
56
56
|
"site-settings",
|
|
57
|
+
"offline-data",
|
|
57
58
|
]);
|
|
58
59
|
|
|
59
60
|
/**
|
|
@@ -246,6 +247,21 @@ export function ownerGrant(
|
|
|
246
247
|
return chosen ? (permissions as unknown[]).map(String) : null;
|
|
247
248
|
}
|
|
248
249
|
|
|
250
|
+
/**
|
|
251
|
+
* Legacy grant keys that also carry a newer key split out of them. Mirrors
|
|
252
|
+
* core `PERMISSION_COVERS` (security catalogue v2 entries only).
|
|
253
|
+
*/
|
|
254
|
+
const GRANT_COVERS: Readonly<Record<string, readonly string[]>> = {
|
|
255
|
+
"vehicle-mgmt:see-all-vehicles": ["vehicle-mgmt:see-blocklist"],
|
|
256
|
+
"vehicle-mgmt:update-vehicle": ["vehicle-mgmt:manage-blocklist"],
|
|
257
|
+
"visitor-mgmt:add-visitor": ["visitor-mgmt:invite-visitor"],
|
|
258
|
+
"visitorManagement:add-visitor": ["visitor-mgmt:invite-visitor"],
|
|
259
|
+
"visitor:create": ["visitor-mgmt:invite-visitor"],
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const grantHolds = (grant: readonly string[], key: string) =>
|
|
263
|
+
grant.includes(key) || grant.some((g) => GRANT_COVERS[g]?.includes(key));
|
|
264
|
+
|
|
249
265
|
/**
|
|
250
266
|
* Narrow a `{ resource: { action } }` catalogue to that grant. Running the
|
|
251
267
|
* organisation (`ALWAYS_ALLOWED_MODULES`) is never narrowed, matching the
|
|
@@ -266,7 +282,7 @@ export function catalogueForOwnerGrant<T extends Record<string, unknown>>(
|
|
|
266
282
|
if (!actions || typeof actions !== "object") continue;
|
|
267
283
|
const kept = Object.fromEntries(
|
|
268
284
|
Object.entries(actions as Record<string, unknown>).filter(([action]) =>
|
|
269
|
-
grant
|
|
285
|
+
grantHolds(grant, `${resource}:${action}`),
|
|
270
286
|
),
|
|
271
287
|
);
|
|
272
288
|
if (Object.keys(kept).length) narrowed[resource] = kept;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { holdsPermission } from "./permission-spellings.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Security catalogue v2 marker. The role editor adds it on save (security roles
|
|
5
|
+
* only). It grants nothing -- it only says "this role was saved with the new
|
|
6
|
+
* boxes, so check them exactly". A role without it keeps today's behaviour.
|
|
7
|
+
* Mirrors core/API-core; keep the three in step.
|
|
8
|
+
*/
|
|
9
|
+
export const SECURITY_CATALOGUE_MARKER = "catalogue:security-v2";
|
|
10
|
+
|
|
11
|
+
/** new key -> legacy keys that stand in for it on an UNMARKED role. null = open to all. */
|
|
12
|
+
export const SECURITY_V2_FALLBACKS: Readonly<Record<string, readonly string[] | null>> = {
|
|
13
|
+
"vehicle-mgmt:see-blocklist": ["vehicle-mgmt:see-all-vehicles"],
|
|
14
|
+
"vehicle-mgmt:manage-blocklist": ["vehicle-mgmt:update-vehicle"],
|
|
15
|
+
"visitor-mgmt:invite-visitor": [
|
|
16
|
+
"visitor-mgmt:add-visitor",
|
|
17
|
+
"visitorManagement:add-visitor",
|
|
18
|
+
"visitor:create",
|
|
19
|
+
],
|
|
20
|
+
"offline-data:see-offline-data": null,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const isSecurityV2Marked = (perms?: readonly string[] | null) =>
|
|
24
|
+
!!perms?.includes(SECURITY_CATALOGUE_MARKER);
|
|
25
|
+
|
|
26
|
+
/** Real keys = anything that is not the marker or the wildcard. */
|
|
27
|
+
export const countRealPermissions = (perms?: readonly string[] | null) =>
|
|
28
|
+
(perms ?? []).filter((p) => p !== SECURITY_CATALOGUE_MARKER).length;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Effective grant for a v2 key. Empty list / "*" = allowed (same as
|
|
32
|
+
* usePermission). Unmarked role -> legacy fallback; marked role -> exact key.
|
|
33
|
+
*/
|
|
34
|
+
export function effectiveSecurityGrant(
|
|
35
|
+
perms: readonly string[] | undefined | null,
|
|
36
|
+
key: string,
|
|
37
|
+
): boolean {
|
|
38
|
+
if (!perms?.length || perms.includes("*")) return true;
|
|
39
|
+
if (isSecurityV2Marked(perms)) return holdsPermission(perms, key);
|
|
40
|
+
const legacy = SECURITY_V2_FALLBACKS[key];
|
|
41
|
+
if (legacy === undefined) return holdsPermission(perms, key);
|
|
42
|
+
if (legacy === null) return true;
|
|
43
|
+
return legacy.some((k) => holdsPermission(perms, k));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Marker to append on save: only when the list has a real key and no "*". */
|
|
47
|
+
export function withSecurityMarker(perms: readonly string[]): string[] {
|
|
48
|
+
const keys = perms.filter((p) => p !== SECURITY_CATALOGUE_MARKER);
|
|
49
|
+
if (!keys.length || keys.includes("*")) return keys;
|
|
50
|
+
return [...keys, SECURITY_CATALOGUE_MARKER];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** New keys to pre-tick when opening an UNMARKED role in the editor. */
|
|
54
|
+
export function securityV2PreTicks(perms: readonly string[]): string[] {
|
|
55
|
+
if (!perms.length || perms.includes("*") || isSecurityV2Marked(perms)) return [];
|
|
56
|
+
return Object.keys(SECURITY_V2_FALLBACKS).filter(
|
|
57
|
+
(k) => !perms.includes(k) && effectiveSecurityGrant(perms, k),
|
|
58
|
+
);
|
|
59
|
+
}
|
package/utils/vehicle-actions.ts
CHANGED
|
@@ -27,6 +27,8 @@ export type VehicleActionPermissions = {
|
|
|
27
27
|
canUpdateVehicle?: boolean;
|
|
28
28
|
canDeleteVehicle?: boolean;
|
|
29
29
|
canApproveVehicle?: boolean;
|
|
30
|
+
/** Block / Unblock. Omitted = allowed (today's behaviour). */
|
|
31
|
+
canManageBlocklist?: boolean;
|
|
30
32
|
};
|
|
31
33
|
|
|
32
34
|
export type VehicleAction = {
|
|
@@ -81,6 +83,12 @@ export function vehiclePlateActions(
|
|
|
81
83
|
const isRestorable = RESTORABLE.has(status);
|
|
82
84
|
|
|
83
85
|
const canUpdate = !!permissions.canUpdateVehicle;
|
|
86
|
+
// Server accepts manage-blocklist OR update-vehicle. When the caller passes the
|
|
87
|
+
// flag it decides alone; absent, block follows update (today's rule).
|
|
88
|
+
const canBlock =
|
|
89
|
+
permissions.canManageBlocklist === undefined
|
|
90
|
+
? canUpdate
|
|
91
|
+
: !!permissions.canManageBlocklist;
|
|
84
92
|
const actions: VehicleAction[] = [];
|
|
85
93
|
|
|
86
94
|
if (canUpdate && (isAllowed || isBlocked)) {
|
|
@@ -118,7 +126,7 @@ export function vehiclePlateActions(
|
|
|
118
126
|
// same thing. A deleted whitelist plate keeps Block - that turns it into a
|
|
119
127
|
// blocked plate, which Restore (back to allowed) does not.
|
|
120
128
|
const restoresToBlocked = isRestorable && type === "blocklist";
|
|
121
|
-
if (
|
|
129
|
+
if (canBlock && (isAllowed || isUnblocked || (isRestorable && !restoresToBlocked))) {
|
|
122
130
|
actions.push({
|
|
123
131
|
key: "block",
|
|
124
132
|
title: "Block",
|
|
@@ -127,7 +135,7 @@ export function vehiclePlateActions(
|
|
|
127
135
|
});
|
|
128
136
|
}
|
|
129
137
|
|
|
130
|
-
if (
|
|
138
|
+
if (canBlock && isBlocked) {
|
|
131
139
|
actions.push({
|
|
132
140
|
key: "unblock",
|
|
133
141
|
title: "Unblock",
|