@7365admin1/layer-common 4.118.9 → 4.119.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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 4.119.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 51b87b6: Security Ops: new `security-ops` resource (`start-site-visit`, `see-all-site-visits`) in the security role catalogue, and `site-settings:manage-geofence` added to the site-settings vocabulary so a Master Admin role can be given the Geofencing panel. The security catalogue cherry-picks site-settings entries rather than spreading them, so `manage-geofence` is listed there explicitly too, and `useSettingsPermission` exposes `canManageGeofence` (also folded into `canManageSiteSettings`).
8
+
9
+ ## 4.118.10
10
+
11
+ ### Patch Changes
12
+
13
+ - 5fd7f77: Accept Terms & Privacy gate now runs on every page (global middleware), not only pages that declare 01-auth.
14
+
3
15
  ## 4.118.9
4
16
 
5
17
  ### Patch Changes
@@ -3182,11 +3194,11 @@ Monthly report: a month that had patrols opens the list of that month's patrol r
3182
3194
  `utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
3183
3195
  own rule from two endpoints the console already calls, unprojected:
3184
3196
 
3185
- GET /api/members/user/:user/app/admin the Seven365 staff membership
3186
- GET /api/roles/id/:role that membership's role document
3197
+ GET /api/members/user/:user/app/admin the Seven365 staff membership
3198
+ GET /api/roles/id/:role that membership's role document
3187
3199
 
3188
- owner = member.type === "admin" && role.type === "admin" && role.default === true
3189
- staff = member.type === "admin" && role.type === "admin"
3200
+ owner = member.type === "admin" && role.type === "admin" && role.default === true
3201
+ staff = member.type === "admin" && role.type === "admin"
3190
3202
 
3191
3203
  `role.default` is the marker because it is the only property of a platform
3192
3204
  staff role no API caller can set - `role.controller.ts` validates create and
@@ -280,6 +280,16 @@ export function useCommonPermissions() {
280
280
  check: true,
281
281
  description: "Allows the user to manage work order settings.",
282
282
  },
283
+ // Security Ops site visits. The boundary is one half; the guardhouse
284
+ // code an Ops Executive scans to open a visit is the other, and it is the
285
+ // proof the whole module rests on - so this is the right that decides who
286
+ // may re-register it. In the COMMON catalogue because site settings are
287
+ // shared across verticals, not only security.
288
+ "manage-geofence": {
289
+ check: true,
290
+ description:
291
+ "Allows the user to set a site's boundary and register the guardhouse check-in code used for Security Ops site visits.",
292
+ },
283
293
  "manage-billing-and-soa-configuration": {
284
294
  check: true,
285
295
  description:
@@ -735,6 +735,38 @@ export default function useSecurityPermission() {
735
735
  description: "Allows the user to delete routes.",
736
736
  },
737
737
  },
738
+ /**
739
+ * NEW. Security Ops - the site visit and its checklists.
740
+ *
741
+ * NO ROLE HOLDS THESE YET. The module is new, so every one of these strings
742
+ * has a count of zero today. API-core's `siteVisit` gate is therefore
743
+ * COUNTED, not enforcing (`SITE_VISIT_PERMISSION_MODE`, default `log`): it
744
+ * records who WOULD be refused and refuses nobody, the same way the NFC
745
+ * patrol and incident report gates shipped. Provision roles against that
746
+ * count before anyone sets it to `enforce`.
747
+ *
748
+ * THREE PLACES, BY HAND. These spellings have to match API-core
749
+ * (`SITE_VISIT_PERMISSIONS` in `site-authz.util.ts`) and the RN app
750
+ * (`permission.catalog.ts`), which does NOT import this file - it keeps a
751
+ * literal copy and compares strings exactly. `roles-picker-covers-mobile-gates`
752
+ * in web-app-security is the test that holds the three together.
753
+ *
754
+ * The geofence right is NOT here: it is a site setting, so it lives in
755
+ * `useCommonPermission.ts` as `site-settings:manage-geofence` beside the
756
+ * other panels.
757
+ */
758
+ "security-ops": {
759
+ "start-site-visit": {
760
+ check: true,
761
+ description:
762
+ "Allows the user to check in at a site, fill in the visit checklists and check out.",
763
+ },
764
+ "see-all-site-visits": {
765
+ check: true,
766
+ description:
767
+ "Allows the user to view site visits across the sites they can reach, including other executives' visits.",
768
+ },
769
+ },
738
770
  // NEW. Seeing the CCTV wall, as distinct from setting cameras up
739
771
  // (`site-settings:manage-cctv-camera`).
740
772
  "cctv-mgmt": {
@@ -768,6 +800,12 @@ export default function useSecurityPermission() {
768
800
  "manage-nfc-management": siteSettingsPermissions["manage-nfc-management"],
769
801
  "manage-nfc-patrol-settings":
770
802
  siteSettingsPermissions["manage-nfc-patrol-settings"],
803
+ // Security Ops geofencing. This block CHERRY-PICKS from
804
+ // `siteSettingsPermissions` rather than spreading it, so a new entry in
805
+ // the common catalogue does not reach the security role editor on its own
806
+ // - without this line no administrator on a security site could grant the
807
+ // right, and the Geofencing panel would be invisible to everyone.
808
+ "manage-geofence": siteSettingsPermissions["manage-geofence"],
771
809
  "manage-red-dot-settings": {
772
810
  check: true,
773
811
  description:
@@ -129,6 +129,20 @@ export function useSettingsPermission() {
129
129
  "update-vehicle": { check: true, description: "Allows the user to update vehicle details." },
130
130
  },
131
131
  };
132
+ /**
133
+ * The Geofencing panel - Security Ops site visits.
134
+ *
135
+ * MUST ALSO BE IN `canManageSiteSettings` BELOW. That computed is an OR of
136
+ * every panel gate and it is what decides whether the Settings page renders at
137
+ * all, so a role holding ONLY this right would otherwise be given a page it
138
+ * cannot open. Adding a panel is always two lines, never one.
139
+ */
140
+ const canManageGeofence = computed(() => {
141
+ if (!userAppRole.value) return false;
142
+ if (userAppRole.value.permissions.includes("*")) return true;
143
+ return hasPermission(userAppRole.value, permissions, "site-settings", "manage-geofence");
144
+ });
145
+
132
146
  const canManageVehicleQr = computed(() => {
133
147
  if (!userAppRole.value) return false;
134
148
  if (userAppRole.value.permissions.includes("*")) return true;
@@ -136,7 +150,7 @@ export function useSettingsPermission() {
136
150
  });
137
151
 
138
152
  const canManageSiteSettings = computed(() => {
139
- return canManageSiteInformation.value || canManageANPRCamera.value || canManageCCTVCamera.value || canManageDeliveryCompanies.value || canManageResidentVisitors.value || canManageWorkOrderSettings.value || canManageBillingAndSOAConfiguration.value || canManageNFCManagement.value || canManageNFCPatrolSettings.value || canManageEntryPass.value || canManageRedDotSettings.value
153
+ return canManageSiteInformation.value || canManageANPRCamera.value || canManageCCTVCamera.value || canManageDeliveryCompanies.value || canManageResidentVisitors.value || canManageWorkOrderSettings.value || canManageBillingAndSOAConfiguration.value || canManageNFCManagement.value || canManageNFCPatrolSettings.value || canManageEntryPass.value || canManageRedDotSettings.value || canManageGeofence.value
140
154
  })
141
155
 
142
156
  return {
@@ -151,6 +165,7 @@ export function useSettingsPermission() {
151
165
  canManageNFCPatrolSettings,
152
166
  canManageEntryPass,
153
167
  canManageRedDotSettings,
168
+ canManageGeofence,
154
169
  canManageVehicleQr,
155
170
  canManageSiteSettings
156
171
 
@@ -150,6 +150,9 @@ export const PERMISSIONS = {
150
150
  "manage-nfc-patrol-settings",
151
151
  "manage-entry-pass",
152
152
  "manage-red-dot-settings",
153
+ // Security Ops: the site boundary and the guardhouse check-in code. The
154
+ // server reads this one too - API-core `GEOFENCE_MANAGE_PERMISSIONS`.
155
+ "manage-geofence",
153
156
  ],
154
157
 
155
158
  // ---- module apps (cleaning, landscape, M&E, pest, pool) ----
@@ -1,5 +1,3 @@
1
- import { termsGateSkips } from "../utils/terms-gate";
2
-
3
1
  export default defineNuxtRouteMiddleware(async (route) => {
4
2
  const { cookieConfig } = useRuntimeConfig().public;
5
3
 
@@ -14,13 +12,5 @@ export default defineNuxtRouteMiddleware(async (route) => {
14
12
  // Redirect to login page if no access token
15
13
  return navigateTo({ name: "index" });
16
14
  }
17
-
18
- // Terms and Privacy Policy: ask once per account per published version.
19
- // Fails open; print/PDF/preview/kiosk/public (plain layout) pages are skipped.
20
- if (import.meta.client && !termsGateSkips(route, navigator.userAgent)) {
21
- const { check } = useTermsGate();
22
- if (await check()) {
23
- return navigateTo({ path: "/accept-terms", query: { redirect: route.fullPath } });
24
- }
25
- }
15
+ // Terms and Privacy gate: see terms-gate.global.ts (runs on every page).
26
16
  });
@@ -0,0 +1,19 @@
1
+ import { termsGateRedirect } from "../utils/terms-gate";
2
+
3
+ /**
4
+ * "Accept Terms and Privacy Policy" gate for EVERY page in every app that
5
+ * extends layer-common. It was in the named `01-auth` middleware, so pages that
6
+ * did not declare it were never gated. Client-only, only with a `sid`, fails
7
+ * open, and reuses useTermsGate's cached answer (no extra call per navigation).
8
+ */
9
+ export default defineNuxtRouteMiddleware(async (to) => {
10
+ const { cookieConfig } = useRuntimeConfig().public;
11
+ const redirect = await termsGateRedirect({
12
+ client: import.meta.client,
13
+ sid: useCookie("sid", cookieConfig).value,
14
+ to,
15
+ userAgent: import.meta.client ? navigator.userAgent : "",
16
+ check: () => useTermsGate().check(),
17
+ });
18
+ if (redirect) return navigateTo(redirect);
19
+ });
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.118.9",
5
+ "version": "4.119.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.",
@@ -19,6 +19,17 @@ export function shouldPrompt(reply: TermsGateReply | null | undefined): boolean
19
19
 
20
20
  const NEVER_GATED_LAYOUTS = ["plain", "plain-dark"];
21
21
 
22
+ // Pre-login and public pages. Most are also `plain` layout; listed by path as a
23
+ // second net so a public page can never loop through the gate.
24
+ const PUBLIC_PATHS = [
25
+ "/sign-in", "/sign-up", "/sign-up-invite", "/forgot-password", "/reset-password",
26
+ "/verify", "/logout", "/public-view", "/terms-and-conditions", "/privacy-policy",
27
+ ];
28
+
29
+ function isPublicPath(path = ""): boolean {
30
+ return PUBLIC_PATHS.some((p) => path === p || path.startsWith(p + "/"));
31
+ }
32
+
22
33
  /**
23
34
  * Pages that must never be interrupted: print/PDF/preview/kiosk/public pages
24
35
  * (they use the plain layouts), the accept page itself, an explicit opt-out,
@@ -29,6 +40,7 @@ export function termsGateSkips(
29
40
  userAgent = "",
30
41
  ): boolean {
31
42
  if (to.path === "/accept-terms") return true;
43
+ if (isPublicPath(to.path)) return true;
32
44
  const meta = to.meta ?? {};
33
45
  if (meta.skipTermsGate) return true;
34
46
  if (typeof meta.layout === "string" && NEVER_GATED_LAYOUTS.includes(meta.layout)) return true;
@@ -43,3 +55,25 @@ export function safeRedirect(value: unknown): string {
43
55
  }
44
56
  return path;
45
57
  }
58
+
59
+ /**
60
+ * The global gate's decision: the path to redirect to, or null to let the page
61
+ * through. `check` (useTermsGate().check) already fails open and is cached, so
62
+ * this adds no API call per navigation.
63
+ */
64
+ export async function termsGateRedirect(opts: {
65
+ client: boolean;
66
+ sid: unknown;
67
+ to: { path?: string; fullPath?: string; meta?: Record<string, unknown> };
68
+ userAgent?: string;
69
+ check: () => Promise<boolean>;
70
+ }): Promise<{ path: string; query: { redirect: string } } | null> {
71
+ if (!opts.client || !opts.sid || termsGateSkips(opts.to, opts.userAgent)) return null;
72
+ let must = false;
73
+ try {
74
+ must = await opts.check();
75
+ } catch {
76
+ return null;
77
+ }
78
+ return must ? { path: "/accept-terms", query: { redirect: opts.to.fullPath ?? opts.to.path ?? "/" } } : null;
79
+ }