@7365admin1/layer-common 4.118.8 → 4.118.10

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.118.10
4
+
5
+ ### Patch Changes
6
+
7
+ - 5fd7f77: Accept Terms & Privacy gate now runs on every page (global middleware), not only pages that declare 01-auth.
8
+
9
+ ## 4.118.9
10
+
11
+ ### Patch Changes
12
+
13
+ - 8ba5d5c: Incident report PDF: the Incident Information heading bar starts 2px inside a white edge, so no sliver of its ghost is painted at the foot of the previous page.
14
+
3
15
  ## 4.118.8
4
16
 
5
17
  ### Patch Changes
@@ -3176,11 +3188,11 @@ Monthly report: a month that had patrols opens the list of that month's patrol r
3176
3188
  `utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
3177
3189
  own rule from two endpoints the console already calls, unprojected:
3178
3190
 
3179
- GET /api/members/user/:user/app/admin the Seven365 staff membership
3180
- GET /api/roles/id/:role that membership's role document
3191
+ GET /api/members/user/:user/app/admin the Seven365 staff membership
3192
+ GET /api/roles/id/:role that membership's role document
3181
3193
 
3182
- owner = member.type === "admin" && role.type === "admin" && role.default === true
3183
- staff = member.type === "admin" && role.type === "admin"
3194
+ owner = member.type === "admin" && role.type === "admin" && role.default === true
3195
+ staff = member.type === "admin" && role.type === "admin"
3184
3196
 
3185
3197
  `role.default` is the marker because it is the only property of a platform
3186
3198
  staff role no API caller can set - `role.controller.ts` validates create and
@@ -337,7 +337,11 @@ const maskNRIC = (value: any) => {
337
337
  out at the foot of the previous page Chrome paints a clipped ghost of it
338
338
  there, and a stroke on that ghost showed as a thin line. */
339
339
  .ir-section {
340
+ /* A white top border with the colour clipped to the padding box: the ghost then
341
+ starts 2px lower than the page foot, so not even a 0.2pt sliver is painted. */
340
342
  border: 0;
343
+ border-top: 2px solid #fff;
344
+ background-clip: padding-box;
341
345
  font-weight: 400;
342
346
  }
343
347
  </style>
@@ -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.8",
5
+ "version": "4.118.10",
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
+ }