PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.15
Yatra – Travel Booking & Tour Operator Software v3.0.15
3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 2.0.10 All 83 releases
yatra / resources / js / hooks / useCapabilities.ts

useCapabilities.ts in Yatra – Travel Booking & Tour Operator Software 3.0.15, at resources/js/hooks/useCapabilities.ts

153 lines 5.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { useMemo, useSyncExternalStore } from "react";
2
3 /**
4 * Capability awareness — reads from window.yatraAdmin.userCaps which is
5 * server-injected at first paint by the Team module's LocalizedData
6 * service. Falls back to "everything allowed" when:
7 *
8 * - The Team module isn't installed (no team slot in localized data) —
9 * in which case the server already enforced WP-native cap checks.
10 * UI-side hiding is a UX nicety, not the security boundary.
11 * - The current user has `isWpAdmin = true` — WP administrators always
12 * pass every cap check on the server, so the UI should mirror that.
13 *
14 * Mutability:
15 * When the user's role or scopes change (e.g. an Owner updates their
16 * own profile in a long-lived tab), the Team page invalidates the
17 * "team-current-user-caps" React Query and writes the fresh caps back
18 * into window.yatraAdmin.userCaps via `setUserCaps()`. Subscribers
19 * re-render via useSyncExternalStore.
20 *
21 * @since 3.5.0
22 */
23
24 /**
25 * The shape of window.yatraAdmin that THIS module cares about. The
26 * full definition lives in types/global.d.ts; we narrow to the
27 * Team-relevant subset here so the rest of the module reads cleanly.
28 */
29 type YatraAdminCapSlice = Pick<
30 NonNullable<Window["yatraAdmin"]>,
31 "teamEnabled" | "isWpAdmin" | "userCaps" | "userScopes"
32 >;
33
34 const subscribers = new Set<() => void>();
35
36 function getSnapshot(): YatraAdminCapSlice {
37 const w = window.yatraAdmin;
38 if (!w) return {} as YatraAdminCapSlice;
39 return {
40 teamEnabled: w.teamEnabled,
41 isWpAdmin: w.isWpAdmin,
42 userCaps: w.userCaps,
43 userScopes: w.userScopes,
44 };
45 }
46
47 function subscribe(cb: () => void): () => void {
48 subscribers.add(cb);
49 return () => subscribers.delete(cb);
50 }
51
52 /** Notify all subscribers — call after mutating window.yatraAdmin. */
53 function emit(): void {
54 subscribers.forEach((cb) => cb());
55 }
56
57 /**
58 * Replace the cached cap list. Call from mutation handlers after a
59 * /team/users/{id} update so the UI updates without a page reload.
60 */
61 export function setUserCaps(caps: string[]): void {
62 if (window.yatraAdmin) {
63 window.yatraAdmin.userCaps = [...caps];
64 emit();
65 }
66 }
67
68 /**
69 * Replace the cached scope state. Same triggering pattern as setUserCaps.
70 */
71 export function setUserScopes(scopes: YatraAdminCapSlice["userScopes"]): void {
72 if (window.yatraAdmin) {
73 window.yatraAdmin.userScopes = scopes;
74 emit();
75 }
76 }
77
78 /**
79 * Returns true if the current user passes a Yatra capability check.
80 *
81 * Important contract:
82 * This is a UI gate, not a security boundary. The server's REST
83 * permission_callback is the actual access check — useCanCap mirrors
84 * it so the UI doesn't surface controls that would 403. If the two
85 * ever disagree, the server wins.
86 */
87 export function useCanCap(cap: string): boolean {
88 // The store snapshot is recreated each call (object literal), so we
89 // can't use Object.is identity. Lift the relevant primitives so
90 // useSyncExternalStore sees stable references.
91 const caps = useSyncExternalStore(
92 subscribe,
93 () => getSnapshot().userCaps,
94 () => getSnapshot().userCaps,
95 );
96 const teamEnabled = useSyncExternalStore(
97 subscribe,
98 () => getSnapshot().teamEnabled,
99 () => getSnapshot().teamEnabled,
100 );
101 const isWpAdmin = useSyncExternalStore(
102 subscribe,
103 () => getSnapshot().isWpAdmin,
104 () => getSnapshot().isWpAdmin,
105 );
106 return useMemo(
107 () => evaluateCap({ userCaps: caps, teamEnabled, isWpAdmin }, cap),
108 [caps, teamEnabled, isWpAdmin, cap],
109 );
110 }
111
112 /**
113 * Same logic, exposed as a plain function for non-hook call sites
114 * (e.g. inside event handlers that build menu items at render time).
115 */
116 export function canCap(cap: string): boolean {
117 return evaluateCap(getSnapshot(), cap);
118 }
119
120 /**
121 * Filter a list of items to those whose `cap` field the current user
122 * has. Sidebar menu items, table action items, etc. use this.
123 */
124 export function filterByCap<T extends { cap?: string }>(items: T[]): T[] {
125 const snapshot = getSnapshot();
126 return items.filter((item) => !item.cap || evaluateCap(snapshot, item.cap));
127 }
128
129 function evaluateCap(snapshot: YatraAdminCapSlice, cap: string): boolean {
130 if (!cap) return true;
131
132 // WP admin always passes (mirrors the admin fallback in
133 // Capabilities::filterUserHasCap). Site owner can never be locked out.
134 if (snapshot.isWpAdmin === true) return true;
135
136 // No userCaps array in the localized data → we have no information
137 // to filter on. This happens on:
138 // - non-Pro installs (no team module ships userCaps)
139 // - Pro installs where the Team module isn't enabled
140 // - The TeamModule failed to boot for some reason
141 // In all those cases, the SERVER-SIDE cap check is still the
142 // security boundary — REST routes still enforce permission_callback,
143 // page handlers still enforce manage_options. UI default-allow here
144 // is just so we don't hide things the user can actually access.
145 const caps = snapshot.userCaps;
146 if (!Array.isArray(caps)) return true;
147
148 // We have the user's caps from the server. Show only what's in the
149 // list. This is the team-module-enabled path — every Yatra cap the
150 // user has been granted server-side is included by LocalizedData.
151 return caps.includes(cap);
152 }
153