@7365admin1/layer-common 4.118.9 → 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,11 @@
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
+
3
9
  ## 4.118.9
4
10
 
5
11
  ### Patch Changes
@@ -3182,11 +3188,11 @@ Monthly report: a month that had patrols opens the list of that month's patrol r
3182
3188
  `utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
3183
3189
  own rule from two endpoints the console already calls, unprojected:
3184
3190
 
3185
- GET /api/members/user/:user/app/admin the Seven365 staff membership
3186
- 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
3187
3193
 
3188
- owner = member.type === "admin" && role.type === "admin" && role.default === true
3189
- 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"
3190
3196
 
3191
3197
  `role.default` is the marker because it is the only property of a platform
3192
3198
  staff role no API caller can set - `role.controller.ts` validates create and
@@ -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.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
+ }