@nsxbet/admin-sdk 0.8.0 → 0.9.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.
Files changed (47) hide show
  1. package/README.md +16 -0
  2. package/dist/auth/client/bff.js +19 -14
  3. package/dist/auth/client/in-memory.js +11 -14
  4. package/dist/auth/client/interface.d.ts +5 -0
  5. package/dist/auth/client/permission-match.d.ts +1 -0
  6. package/dist/auth/client/permission-match.js +13 -0
  7. package/dist/auth/client/rbac-resolution.d.ts +29 -0
  8. package/dist/auth/client/rbac-resolution.js +22 -0
  9. package/dist/auth/client/resolved-role-cache.d.ts +11 -0
  10. package/dist/auth/client/resolved-role-cache.js +54 -0
  11. package/dist/components/AuthProvider.d.ts +3 -1
  12. package/dist/components/AuthProvider.js +95 -23
  13. package/dist/i18n/locales/en-US.json +66 -1
  14. package/dist/i18n/locales/es.json +66 -1
  15. package/dist/i18n/locales/pt-BR.json +66 -1
  16. package/dist/i18n/locales/ro.json +66 -1
  17. package/dist/registry/types/manifest.d.ts +3 -1
  18. package/dist/sdk-version.js +1 -1
  19. package/dist/shell/AdminShell.js +13 -8
  20. package/dist/shell/components/HomePage.js +1 -1
  21. package/dist/shell/components/LeftNav.js +46 -4
  22. package/dist/shell/components/MainContent.js +25 -0
  23. package/dist/shell/components/RegistryPage.js +3 -3
  24. package/dist/shell/components/access-control/AccessControlAuditPage.d.ts +1 -0
  25. package/dist/shell/components/access-control/AccessControlAuditPage.js +135 -0
  26. package/dist/shell/components/access-control/AccessControlGroupDetailPage.d.ts +1 -0
  27. package/dist/shell/components/access-control/AccessControlGroupDetailPage.js +224 -0
  28. package/dist/shell/components/access-control/AccessControlGroupsPage.d.ts +1 -0
  29. package/dist/shell/components/access-control/AccessControlGroupsPage.js +183 -0
  30. package/dist/shell/components/access-control/AccessControlLayout.d.ts +8 -0
  31. package/dist/shell/components/access-control/AccessControlLayout.js +23 -0
  32. package/dist/shell/components/access-control/AccessControlMemberPicker.d.ts +10 -0
  33. package/dist/shell/components/access-control/AccessControlMemberPicker.js +44 -0
  34. package/dist/shell/components/access-control/AccessControlPermissionPicker.d.ts +8 -0
  35. package/dist/shell/components/access-control/AccessControlPermissionPicker.js +38 -0
  36. package/dist/shell/components/access-control/AccessControlUserPage.d.ts +1 -0
  37. package/dist/shell/components/access-control/AccessControlUserPage.js +42 -0
  38. package/dist/shell/components/access-control/AccessControlUsersListPage.d.ts +1 -0
  39. package/dist/shell/components/access-control/AccessControlUsersListPage.js +111 -0
  40. package/dist/shell/components/access-control/api.d.ts +111 -0
  41. package/dist/shell/components/access-control/api.js +119 -0
  42. package/dist/shell/components/access-control/index.d.ts +8 -0
  43. package/dist/shell/components/access-control/index.js +8 -0
  44. package/dist/shell/components/index.d.ts +1 -0
  45. package/dist/shell/components/index.js +1 -0
  46. package/dist/vite/plugins.js +1 -1
  47. package/package.json +1 -1
package/README.md CHANGED
@@ -1141,6 +1141,22 @@ When the shell loads modules dynamically from URLs, it validates each URL agains
1141
1141
 
1142
1142
  Production auth should go through **admin-bff**: the BFF sets an HttpOnly `access_token` cookie and exposes **`GET /me`** for identity. The shell does not read the JWT in the browser.
1143
1143
 
1144
+ When `AdminShell` receives an `apiUrl`, the SDK now treats **RBAC-resolved effective roles** from the Admin API as the long-term permission source:
1145
+
1146
+ - `AuthProvider` calls `GET /api/access-control/users/me/resolved-roles` after successful authentication
1147
+ - the current subject's resolved roles are cached in localStorage for up to five minutes
1148
+ - temporary RBAC resolution failures may fall back to that same-subject cached role set
1149
+ - logout clears the cache immediately
1150
+ - once the cache expires, permission checks fail closed until RBAC resolution succeeds again
1151
+
1152
+ This means `hasPermission()` reflects local group membership and hybrid catalog resolution rather than only raw upstream roles from `/me` or JWT payloads.
1153
+
1154
+ The SDK shell also exposes a native `Access Control` route subtree under `/_access-control/*` for groups, user inspection, and audit history. Those pages are gated by exact platform permissions such as:
1155
+
1156
+ - `admin.platform.access-control.groups.manage`
1157
+ - `admin.platform.access-control.users.view`
1158
+ - `admin.platform.access-control.audit.view`
1159
+
1144
1160
  ### AdminShell
1145
1161
 
1146
1162
  ```tsx
@@ -6,6 +6,7 @@
6
6
  * to build the user, and persists the token in localStorage. Falls back to
7
7
  * `GET /me` (cookie-based) when no token is present.
8
8
  */
9
+ import { matchesPermission } from './permission-match';
9
10
  const TOKEN_STORAGE_KEY = 'admin-bff-token';
10
11
  const EXPIRES_STORAGE_KEY = 'admin-bff-token-expires';
11
12
  const LOGGED_OUT_FLAG_KEY = 'admin-bff-logged-out';
@@ -70,19 +71,6 @@ function jwtToUser(payload) {
70
71
  roles: [...groups, ...roles],
71
72
  };
72
73
  }
73
- function matchesPermission(userRoles, permission) {
74
- if (userRoles.includes('*'))
75
- return true;
76
- return userRoles.some((role) => {
77
- if (role === permission)
78
- return true;
79
- if (role.endsWith('.*')) {
80
- const prefix = role.slice(0, -2);
81
- return permission.startsWith(prefix);
82
- }
83
- return false;
84
- });
85
- }
86
74
  /** Read token + expires from URL query params, then clean the URL. */
87
75
  function consumeTokenFromUrl() {
88
76
  if (typeof window === 'undefined')
@@ -148,6 +136,8 @@ export function createBffAuthClient(options = {}) {
148
136
  const bffBaseUrl = normalizeBaseUrl(options.bffBaseUrl ?? resolveDefaultBffBaseUrl());
149
137
  let currentUser = null;
150
138
  let accessToken = null;
139
+ let rawRoles = [];
140
+ let resolvedRoles = null;
151
141
  let initialized = false;
152
142
  const subscribers = new Set();
153
143
  function notifySubscribers() {
@@ -181,6 +171,8 @@ export function createBffAuthClient(options = {}) {
181
171
  }
182
172
  const payload = decodeJwtPayload(token);
183
173
  currentUser = jwtToUser(payload);
174
+ rawRoles = currentUser.roles;
175
+ resolvedRoles = null;
184
176
  accessToken = token;
185
177
  storeToken(token, expires);
186
178
  return true;
@@ -222,6 +214,8 @@ export function createBffAuthClient(options = {}) {
222
214
  initialized = true;
223
215
  if (result.ok) {
224
216
  currentUser = result.user;
217
+ rawRoles = result.user.roles;
218
+ resolvedRoles = null;
225
219
  notifySubscribers();
226
220
  return true;
227
221
  }
@@ -250,11 +244,22 @@ export function createBffAuthClient(options = {}) {
250
244
  if (!initialized || !currentUser) {
251
245
  return false;
252
246
  }
253
- return matchesPermission(currentUser.roles, permission);
247
+ return matchesPermission(resolvedRoles ?? currentUser.roles, permission);
248
+ },
249
+ setResolvedRoles(roles) {
250
+ resolvedRoles = roles ? [...roles] : null;
251
+ if (currentUser) {
252
+ currentUser = {
253
+ ...currentUser,
254
+ roles: resolvedRoles ?? rawRoles,
255
+ };
256
+ }
254
257
  },
255
258
  logout() {
256
259
  currentUser = null;
257
260
  accessToken = null;
261
+ rawRoles = [];
262
+ resolvedRoles = null;
258
263
  clearStoredToken();
259
264
  setLoggedOutFlag();
260
265
  notifySubscribers();
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { fetchGatewayToken } from './gateway-token';
8
8
  import { env } from '../../env';
9
+ import { matchesPermission } from './permission-match';
9
10
  export { GatewayTimeoutError, GatewayFetchError } from './gateway-token';
10
11
  /**
11
12
  * Create mock users from a role configuration
@@ -80,6 +81,7 @@ export function createInMemoryAuthClient(options) {
80
81
  let tokenCache = null;
81
82
  let useMockFallback = false;
82
83
  let backgroundRefreshInFlight = false;
84
+ let resolvedRoles = null;
83
85
  const subscribers = new Set();
84
86
  const REFRESH_BUFFER_MS = 60000;
85
87
  function loadStorage() {
@@ -111,7 +113,7 @@ export function createInMemoryAuthClient(options) {
111
113
  id: selectedUser.id,
112
114
  email: selectedUser.email,
113
115
  displayName: selectedUser.displayName,
114
- roles: selectedUser.roles,
116
+ roles: resolvedRoles ?? selectedUser.roles,
115
117
  } : null,
116
118
  };
117
119
  subscribers.forEach((callback) => callback(state));
@@ -121,7 +123,7 @@ export function createInMemoryAuthClient(options) {
121
123
  id: mockUser.id,
122
124
  email: mockUser.email,
123
125
  displayName: mockUser.displayName,
124
- roles: mockUser.roles,
126
+ roles: resolvedRoles ?? mockUser.roles,
125
127
  };
126
128
  }
127
129
  function mockToken() {
@@ -147,6 +149,7 @@ export function createInMemoryAuthClient(options) {
147
149
  const user = findUser(storage.selectedUserId);
148
150
  if (user) {
149
151
  selectedUser = user;
152
+ resolvedRoles = null;
150
153
  return true;
151
154
  }
152
155
  }
@@ -194,23 +197,16 @@ export function createInMemoryAuthClient(options) {
194
197
  if (!selectedUser) {
195
198
  return false;
196
199
  }
197
- if (selectedUser.roles.includes('*')) {
198
- return true;
199
- }
200
- return selectedUser.roles.some((role) => {
201
- if (role === permission)
202
- return true;
203
- if (role.endsWith('.*')) {
204
- const prefix = role.slice(0, -2);
205
- return permission.startsWith(prefix);
206
- }
207
- return false;
208
- });
200
+ return matchesPermission(resolvedRoles ?? selectedUser.roles, permission);
201
+ },
202
+ setResolvedRoles(roles) {
203
+ resolvedRoles = roles ? [...roles] : null;
209
204
  },
210
205
  logout() {
211
206
  selectedUser = null;
212
207
  tokenCache = null;
213
208
  useMockFallback = false;
209
+ resolvedRoles = null;
214
210
  const storage = loadStorage();
215
211
  storage.selectedUserId = null;
216
212
  saveStorage(storage);
@@ -231,6 +227,7 @@ export function createInMemoryAuthClient(options) {
231
227
  throw new Error(`User not found: ${userId}`);
232
228
  }
233
229
  tokenCache = null;
230
+ resolvedRoles = null;
234
231
  if (loginOptions?.fallbackToMock || !resolvedGatewayUrl) {
235
232
  useMockFallback = true;
236
233
  if (resolvedGatewayUrl && loginOptions?.fallbackToMock) {
@@ -76,6 +76,11 @@ export interface AuthClient {
76
76
  * Check if user has a specific permission/role
77
77
  */
78
78
  hasPermission(permission: string): boolean;
79
+ /**
80
+ * Override raw auth roles with RBAC-resolved roles when available.
81
+ * Passing `null` clears the override and falls back to raw roles.
82
+ */
83
+ setResolvedRoles?(roles: string[] | null): void;
79
84
  /**
80
85
  * Log out the current user
81
86
  */
@@ -0,0 +1 @@
1
+ export declare function matchesPermission(userRoles: string[], permission: string): boolean;
@@ -0,0 +1,13 @@
1
+ export function matchesPermission(userRoles, permission) {
2
+ if (userRoles.includes('*'))
3
+ return true;
4
+ return userRoles.some((role) => {
5
+ if (role === permission)
6
+ return true;
7
+ if (role.endsWith('.*')) {
8
+ const prefix = role.slice(0, -2);
9
+ return permission.startsWith(prefix);
10
+ }
11
+ return false;
12
+ });
13
+ }
@@ -0,0 +1,29 @@
1
+ export interface ResolvedRoleUser {
2
+ id: number;
3
+ subject: string;
4
+ email: string;
5
+ displayName: string;
6
+ }
7
+ export interface ResolvedRoleGroup {
8
+ id: string;
9
+ displayName: string;
10
+ permissions: string[];
11
+ }
12
+ export interface ResolvedRoleAttribution {
13
+ permission: string;
14
+ groups: Array<{
15
+ id: string;
16
+ displayName: string;
17
+ }>;
18
+ }
19
+ export interface ResolvedRolesResponse {
20
+ user: ResolvedRoleUser;
21
+ roles: string[];
22
+ groups: ResolvedRoleGroup[];
23
+ attribution: ResolvedRoleAttribution[];
24
+ }
25
+ export interface FetchResolvedRolesOptions {
26
+ apiUrl: string;
27
+ getAccessToken: () => Promise<string | null>;
28
+ }
29
+ export declare function fetchCurrentUserResolvedRoles(options: FetchResolvedRolesOptions): Promise<ResolvedRolesResponse>;
@@ -0,0 +1,22 @@
1
+ function normalizeApiUrl(apiUrl) {
2
+ const normalized = apiUrl.replace(/\/+$/, '');
3
+ return normalized.endsWith('/api') ? normalized : `${normalized}/api`;
4
+ }
5
+ export async function fetchCurrentUserResolvedRoles(options) {
6
+ const token = await options.getAccessToken();
7
+ const headers = new Headers();
8
+ const response = await fetch(`${normalizeApiUrl(options.apiUrl)}/access-control/users/me/resolved-roles`, token === null
9
+ ? {
10
+ headers,
11
+ credentials: 'include',
12
+ }
13
+ : {
14
+ headers: {
15
+ Authorization: `Bearer ${token}`,
16
+ },
17
+ });
18
+ if (!response.ok) {
19
+ throw new Error(`RBAC resolution failed: ${response.status}`);
20
+ }
21
+ return response.json();
22
+ }
@@ -0,0 +1,11 @@
1
+ export declare const RESOLVED_ROLE_CACHE_STORAGE_KEY = "adminPlatform.resolvedRoles";
2
+ export declare const RESOLVED_ROLE_CACHE_TTL_MS: number;
3
+ export interface ResolvedRoleCacheEntry {
4
+ subject: string;
5
+ roles: string[];
6
+ cachedAt: number;
7
+ }
8
+ export declare function readResolvedRoleCache(): ResolvedRoleCacheEntry | null;
9
+ export declare function writeResolvedRoleCache(subject: string, roles: string[]): void;
10
+ export declare function clearResolvedRoleCache(): void;
11
+ export declare function readUsableResolvedRolesFromCache(subject: string, now?: number, ttlMs?: number): string[] | null;
@@ -0,0 +1,54 @@
1
+ export const RESOLVED_ROLE_CACHE_STORAGE_KEY = 'adminPlatform.resolvedRoles';
2
+ export const RESOLVED_ROLE_CACHE_TTL_MS = 5 * 60 * 1000;
3
+ export function readResolvedRoleCache() {
4
+ try {
5
+ const raw = localStorage.getItem(RESOLVED_ROLE_CACHE_STORAGE_KEY);
6
+ if (!raw)
7
+ return null;
8
+ const parsed = JSON.parse(raw);
9
+ if (typeof parsed.subject !== 'string' ||
10
+ !Array.isArray(parsed.roles) ||
11
+ typeof parsed.cachedAt !== 'number') {
12
+ return null;
13
+ }
14
+ return {
15
+ subject: parsed.subject,
16
+ roles: parsed.roles.filter((role) => typeof role === 'string'),
17
+ cachedAt: parsed.cachedAt,
18
+ };
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ export function writeResolvedRoleCache(subject, roles) {
25
+ try {
26
+ const entry = {
27
+ subject,
28
+ roles,
29
+ cachedAt: Date.now(),
30
+ };
31
+ localStorage.setItem(RESOLVED_ROLE_CACHE_STORAGE_KEY, JSON.stringify(entry));
32
+ }
33
+ catch {
34
+ // Ignore storage failures and continue without cache.
35
+ }
36
+ }
37
+ export function clearResolvedRoleCache() {
38
+ try {
39
+ localStorage.removeItem(RESOLVED_ROLE_CACHE_STORAGE_KEY);
40
+ }
41
+ catch {
42
+ // Ignore storage failures.
43
+ }
44
+ }
45
+ export function readUsableResolvedRolesFromCache(subject, now = Date.now(), ttlMs = RESOLVED_ROLE_CACHE_TTL_MS) {
46
+ const entry = readResolvedRoleCache();
47
+ if (!entry)
48
+ return null;
49
+ if (entry.subject !== subject)
50
+ return null;
51
+ if (entry.cachedAt + ttlMs < now)
52
+ return null;
53
+ return entry.roles;
54
+ }
@@ -31,11 +31,13 @@ interface AuthProviderProps {
31
31
  children: React.ReactNode;
32
32
  /** The auth client to use */
33
33
  authClient: AuthClient;
34
+ /** Base URL for the admin API used for RBAC resolution */
35
+ apiUrl?: string;
34
36
  }
35
37
  /**
36
38
  * AuthProvider manages authentication state and renders appropriate UI
37
39
  */
38
- export declare function AuthProvider({ children, authClient }: AuthProviderProps): import("react/jsx-runtime").JSX.Element;
40
+ export declare function AuthProvider({ children, authClient, apiUrl }: AuthProviderProps): import("react/jsx-runtime").JSX.Element;
39
41
  /**
40
42
  * Hook to access auth context
41
43
  */
@@ -5,8 +5,11 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
5
5
  * Works with both in-memory (mock) and BFF cookie auth clients.
6
6
  * Shows user selection screen when in-memory client has no selected user.
7
7
  */
8
- import { createContext, useContext, useEffect, useState, useCallback, useMemo } from 'react';
8
+ import { createContext, useContext, useEffect, useState, useCallback, useMemo, useRef } from 'react';
9
9
  import { useCallbackRef } from '../hooks/useCallbackRef';
10
+ import { matchesPermission } from '../auth/client/permission-match';
11
+ import { fetchCurrentUserResolvedRoles } from '../auth/client/rbac-resolution';
12
+ import { clearResolvedRoleCache, readUsableResolvedRolesFromCache, writeResolvedRoleCache, } from '../auth/client/resolved-role-cache';
10
13
  import { isLoggedOutFlag } from '../auth/client/bff';
11
14
  import { UserSelector } from '../auth/components/UserSelector';
12
15
  import { LoginPage } from '../auth/components/LoginPage';
@@ -20,7 +23,7 @@ function LoadingScreen() {
20
23
  /**
21
24
  * AuthProvider manages authentication state and renders appropriate UI
22
25
  */
23
- export function AuthProvider({ children, authClient }) {
26
+ export function AuthProvider({ children, authClient, apiUrl }) {
24
27
  const [authState, setAuthState] = useState({
25
28
  isAuthenticated: false,
26
29
  user: null,
@@ -29,6 +32,53 @@ export function AuthProvider({ children, authClient }) {
29
32
  const [isInitializing, setIsInitializing] = useState(!bffLoggedOut);
30
33
  const [needsUserSelection, setNeedsUserSelection] = useState(false);
31
34
  const [needsLogin, setNeedsLogin] = useState(bffLoggedOut);
35
+ const effectiveRolesRef = useRef([]);
36
+ effectiveRolesRef.current = authState.user?.roles ?? [];
37
+ const applyResolvedRoles = useCallback(async (state) => {
38
+ if (!state.isAuthenticated || !state.user) {
39
+ authClient.setResolvedRoles?.(null);
40
+ return state;
41
+ }
42
+ if (!apiUrl) {
43
+ return state;
44
+ }
45
+ try {
46
+ const resolved = await fetchCurrentUserResolvedRoles({
47
+ apiUrl,
48
+ getAccessToken: () => authClient.getAccessToken(),
49
+ });
50
+ writeResolvedRoleCache(state.user.id, resolved.roles);
51
+ authClient.setResolvedRoles?.(resolved.roles);
52
+ return {
53
+ ...state,
54
+ user: {
55
+ ...state.user,
56
+ roles: resolved.roles,
57
+ },
58
+ };
59
+ }
60
+ catch {
61
+ const cachedRoles = readUsableResolvedRolesFromCache(state.user.id);
62
+ if (cachedRoles) {
63
+ authClient.setResolvedRoles?.(cachedRoles);
64
+ return {
65
+ ...state,
66
+ user: {
67
+ ...state.user,
68
+ roles: cachedRoles,
69
+ },
70
+ };
71
+ }
72
+ authClient.setResolvedRoles?.([]);
73
+ return {
74
+ ...state,
75
+ user: {
76
+ ...state.user,
77
+ roles: [],
78
+ },
79
+ };
80
+ }
81
+ }, [apiUrl, authClient]);
32
82
  // Initialize auth client
33
83
  useEffect(() => {
34
84
  let mounted = true;
@@ -38,18 +88,25 @@ export function AuthProvider({ children, authClient }) {
38
88
  if (!mounted)
39
89
  return;
40
90
  if (isAuthenticated) {
41
- setAuthState({
91
+ const resolvedState = await applyResolvedRoles({
42
92
  isAuthenticated: true,
43
93
  user: authClient.getUser(),
44
94
  });
95
+ if (!mounted)
96
+ return;
97
+ setAuthState(resolvedState);
45
98
  setNeedsUserSelection(false);
46
99
  setNeedsLogin(false);
47
100
  }
48
101
  else if (authClient.type === 'in-memory') {
102
+ clearResolvedRoleCache();
103
+ authClient.setResolvedRoles?.(null);
49
104
  setNeedsUserSelection(true);
50
105
  setNeedsLogin(false);
51
106
  }
52
107
  else if (authClient.type === 'bff') {
108
+ clearResolvedRoleCache();
109
+ authClient.setResolvedRoles?.(null);
53
110
  setNeedsLogin(true);
54
111
  setNeedsUserSelection(false);
55
112
  }
@@ -69,38 +126,53 @@ export function AuthProvider({ children, authClient }) {
69
126
  initialize();
70
127
  // Subscribe to auth state changes
71
128
  const unsubscribe = authClient.subscribe((state) => {
72
- if (mounted) {
73
- setAuthState(state);
74
- if (!state.isAuthenticated && authClient.type === 'in-memory') {
75
- setNeedsUserSelection(true);
76
- setNeedsLogin(false);
77
- }
78
- else if (!state.isAuthenticated && authClient.type === 'bff') {
79
- setNeedsLogin(true);
80
- setNeedsUserSelection(false);
81
- }
82
- else {
83
- setNeedsUserSelection(false);
84
- setNeedsLogin(false);
129
+ if (!mounted)
130
+ return;
131
+ const syncState = async () => {
132
+ if (!state.isAuthenticated) {
133
+ clearResolvedRoleCache();
134
+ authClient.setResolvedRoles?.(null);
135
+ setAuthState(state);
136
+ if (authClient.type === 'in-memory') {
137
+ setNeedsUserSelection(true);
138
+ setNeedsLogin(false);
139
+ }
140
+ else if (authClient.type === 'bff') {
141
+ setNeedsLogin(true);
142
+ setNeedsUserSelection(false);
143
+ }
144
+ return;
85
145
  }
86
- }
146
+ const resolvedState = await applyResolvedRoles(state);
147
+ if (!mounted)
148
+ return;
149
+ setAuthState(resolvedState);
150
+ setNeedsUserSelection(false);
151
+ setNeedsLogin(false);
152
+ };
153
+ syncState();
87
154
  });
88
155
  return () => {
89
156
  mounted = false;
90
157
  unsubscribe();
91
158
  };
92
- }, [authClient]);
159
+ }, [authClient, applyResolvedRoles]);
93
160
  // Handle user selection
94
- const handleUserSelected = useCallback(() => {
95
- setAuthState({
161
+ const handleUserSelected = useCallback(async () => {
162
+ const resolvedState = await applyResolvedRoles({
96
163
  isAuthenticated: authClient.isAuthenticated(),
97
164
  user: authClient.getUser(),
98
165
  });
166
+ setAuthState(resolvedState);
99
167
  setNeedsUserSelection(false);
100
- }, [authClient]);
168
+ }, [authClient, applyResolvedRoles]);
101
169
  const getAccessToken = useCallbackRef(() => authClient.getAccessToken());
102
- const hasPermission = useCallbackRef((permission) => authClient.hasPermission(permission));
103
- const logout = useCallbackRef(() => authClient.logout());
170
+ const hasPermission = useCallbackRef((permission) => matchesPermission(effectiveRolesRef.current, permission));
171
+ const logout = useCallbackRef(() => {
172
+ clearResolvedRoleCache();
173
+ authClient.setResolvedRoles?.(null);
174
+ authClient.logout();
175
+ });
104
176
  const contextValue = useMemo(() => ({
105
177
  isAuthenticated: authState.isAuthenticated,
106
178
  user: authState.user,
@@ -10,6 +10,7 @@
10
10
  "edit": "Edit",
11
11
  "create": "Create",
12
12
  "search": "Search",
13
+ "retry": "Retry",
13
14
  "noResults": "No results found",
14
15
  "close": "Close",
15
16
  "back": "Back",
@@ -42,6 +43,7 @@
42
43
  "settings": "Settings",
43
44
  "profile": "Profile",
44
45
  "registry": "Registry",
46
+ "accessControl": "Access Control",
45
47
  "logout": "Logout",
46
48
  "pinned": "Pinned",
47
49
  "platform": "Platform",
@@ -219,10 +221,73 @@
219
221
  "loginButton": "Login with Corporate SSO",
220
222
  "secureAuth": "Secure Authentication"
221
223
  },
224
+ "accessControlPage": {
225
+ "title": "Access Control",
226
+ "description": "Manage local authorization, inspection, and audit workflows.",
227
+ "groups": "Groups",
228
+ "users": "Users",
229
+ "audit": "Audit",
230
+ "statusAll": "All",
231
+ "statusActive": "Active",
232
+ "statusInactive": "Inactive",
233
+ "groupsTitle": "Groups",
234
+ "groupsDescription": "Browse and manage RBAC groups.",
235
+ "noGroupsTitle": "No groups found",
236
+ "noGroupsDescription": "No groups matched the current filter.",
237
+ "protectedGroup": "Protected",
238
+ "viewGroup": "View Group",
239
+ "deactivate": "Deactivate",
240
+ "reactivate": "Reactivate",
241
+ "confirmDeactivate": "Are you sure you want to deactivate \"{{group}}\"? Members of this group will lose all permissions granted through it.",
242
+ "confirmReactivate": "Are you sure you want to reactivate \"{{group}}\"? Members will regain all permissions assigned to this group.",
243
+ "promptCreateGroup": "Enter a display name for the new group",
244
+ "promptEditGroup": "Update the group display name",
245
+ "groupDetailTitle": "Group Detail",
246
+ "groupDetailDescription": "Review memberships and permissions for group {{groupId}}.",
247
+ "membersTab": "Members",
248
+ "permissionsTab": "Permissions",
249
+ "noMembers": "This group has no members yet.",
250
+ "noPermissions": "This group has no assigned permissions yet.",
251
+ "selfRemoveTitle": "Remove yourself from this group?",
252
+ "selfRemoveDescription": "You are about to remove yourself from the \"{{group}}\" group. This may revoke your access to manage this group and other platform areas.",
253
+ "selfRemovePrompt": "Type CONFIRM to proceed.",
254
+ "selfRemoveConfirmButton": "Remove myself",
255
+ "userTitle": "User Access",
256
+ "userDescription": "Inspect resolved access for user {{userId}}.",
257
+ "userRolesTitle": "Effective Roles",
258
+ "userGroupsTitle": "Contributing Groups",
259
+ "noRoles": "This user has no effective roles.",
260
+ "auditTitle": "Audit History",
261
+ "auditDescription": "Browse read-only Access Control audit events.",
262
+ "filterEventType": "Event type",
263
+ "filterActor": "Actor user id",
264
+ "filterTargetType": "Target type",
265
+ "filterTargetId": "Target id",
266
+ "noAuditTitle": "No audit events found",
267
+ "noAuditDescription": "No audit events matched the current filters.",
268
+ "noAccess": "You do not have permission to access the Access Control area.",
269
+ "columnName": "Name",
270
+ "columnProtected": "Protected",
271
+ "columnEmail": "Email",
272
+ "columnSubject": "Subject",
273
+ "columnTimestamp": "Timestamp",
274
+ "columnEvent": "Event",
275
+ "columnActor": "Actor",
276
+ "columnTarget": "Target",
277
+ "columnSummary": "Summary",
278
+ "inspectUser": "Inspect",
279
+ "usersTitle": "Users",
280
+ "noUsersTitle": "No users found",
281
+ "noUsersDescription": "No known local users matched the search."
282
+ },
222
283
  "breadcrumbs": {
223
284
  "home": "Home",
224
285
  "registry": "Registry",
225
286
  "settings": "Settings",
226
- "profile": "Profile"
287
+ "profile": "Profile",
288
+ "accessControl": "Access Control",
289
+ "accessControlGroups": "Groups",
290
+ "accessControlUsers": "Users",
291
+ "accessControlAudit": "Audit"
227
292
  }
228
293
  }