@nsxbet/admin-sdk 0.5.0 → 0.6.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/CHECKLIST.md +40 -10
- package/README.md +337 -36
- package/dist/auth/client/gateway-token.d.ts +19 -0
- package/dist/auth/client/gateway-token.js +89 -0
- package/dist/auth/client/in-memory.d.ts +5 -1
- package/dist/auth/client/in-memory.js +75 -38
- package/dist/auth/client/index.d.ts +0 -1
- package/dist/auth/client/interface.d.ts +6 -3
- package/dist/auth/client/keycloak.d.ts +0 -1
- package/dist/auth/client/keycloak.js +6 -3
- package/dist/auth/components/UserSelector.d.ts +0 -1
- package/dist/auth/components/UserSelector.js +89 -7
- package/dist/auth/components/index.d.ts +0 -1
- package/dist/auth/index.d.ts +0 -1
- package/dist/components/AuthProvider.d.ts +0 -1
- package/dist/components/Timestamp.d.ts +7 -0
- package/dist/components/Timestamp.js +50 -0
- package/dist/hooks/useAuth.d.ts +0 -1
- package/dist/hooks/useAuth.js +1 -1
- package/dist/hooks/useFetch.d.ts +0 -1
- package/dist/hooks/useI18n.d.ts +0 -1
- package/dist/hooks/usePlatformAPI.d.ts +0 -1
- package/dist/hooks/useTelemetry.d.ts +0 -1
- package/dist/hooks/useTimestamp.d.ts +8 -0
- package/dist/hooks/useTimestamp.js +122 -0
- package/dist/i18n/config.d.ts +20 -2
- package/dist/i18n/config.js +48 -0
- package/dist/i18n/index.d.ts +2 -3
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/locales/en-US.json +95 -18
- package/dist/i18n/locales/es.json +95 -18
- package/dist/i18n/locales/pt-BR.json +95 -18
- package/dist/i18n/locales/ro.json +95 -18
- package/dist/index.d.ts +11 -7
- package/dist/index.js +5 -1
- package/dist/registry/AdminShellRegistry.d.ts +1 -2
- package/dist/registry/cache/cached-catalog.d.ts +11 -0
- package/dist/registry/cache/cached-catalog.js +42 -0
- package/dist/registry/cache/catalog-cache.d.ts +10 -0
- package/dist/registry/cache/catalog-cache.js +58 -0
- package/dist/registry/cache/index.d.ts +5 -0
- package/dist/registry/cache/index.js +3 -0
- package/dist/registry/cache/types.d.ts +20 -0
- package/dist/registry/cache/types.js +3 -0
- package/dist/registry/client/http.d.ts +0 -1
- package/dist/registry/client/http.js +13 -0
- package/dist/registry/client/in-memory.d.ts +0 -1
- package/dist/registry/client/in-memory.js +117 -12
- package/dist/registry/client/index.d.ts +0 -1
- package/dist/registry/client/interface.d.ts +21 -6
- package/dist/registry/index.d.ts +5 -2
- package/dist/registry/index.js +4 -0
- package/dist/registry/types/index.d.ts +2 -3
- package/dist/registry/types/manifest.d.ts +20 -24
- package/dist/registry/types/manifest.js +17 -18
- package/dist/registry/types/module.d.ts +43 -14
- package/dist/registry/useRegistryPolling.d.ts +15 -0
- package/dist/registry/useRegistryPolling.js +66 -0
- package/dist/router/DynamicModule.d.ts +6 -22
- package/dist/router/DynamicModule.js +25 -48
- package/dist/router/ModuleErrorBoundary.d.ts +39 -0
- package/dist/router/ModuleErrorBoundary.js +101 -0
- package/dist/router/index.d.ts +1 -1
- package/dist/router/url-allowlist.d.ts +22 -0
- package/dist/router/url-allowlist.js +65 -0
- package/dist/shell/AdminShell.d.ts +0 -1
- package/dist/shell/AdminShell.js +178 -43
- package/dist/shell/BackofficeShell.d.ts +0 -1
- package/dist/shell/BackofficeShell.js +59 -25
- package/dist/shell/components/CommandPalette.d.ts +0 -1
- package/dist/shell/components/CommandPalette.js +26 -50
- package/dist/shell/components/DevtoolsPanel.d.ts +11 -0
- package/dist/shell/components/DevtoolsPanel.js +145 -0
- package/dist/shell/components/HomePage.d.ts +0 -1
- package/dist/shell/components/HomePage.js +9 -4
- package/dist/shell/components/LeftNav.d.ts +0 -1
- package/dist/shell/components/LeftNav.js +91 -93
- package/dist/shell/components/MainContent.d.ts +3 -2
- package/dist/shell/components/MainContent.js +8 -23
- package/dist/shell/components/ModuleOverview.d.ts +0 -1
- package/dist/shell/components/ModuleOverview.js +4 -20
- package/dist/shell/components/ProfilePage.d.ts +0 -1
- package/dist/shell/components/ProfilePage.js +1 -1
- package/dist/shell/components/RegistryPage.d.ts +0 -1
- package/dist/shell/components/RegistryPage.js +154 -64
- package/dist/shell/components/RegistryStatusBanner.d.ts +6 -0
- package/dist/shell/components/RegistryStatusBanner.js +31 -0
- package/dist/shell/components/RegistryUnavailable.d.ts +4 -0
- package/dist/shell/components/RegistryUnavailable.js +7 -0
- package/dist/shell/components/SettingsPage.d.ts +0 -1
- package/dist/shell/components/StackedPanel.d.ts +15 -0
- package/dist/shell/components/StackedPanel.js +45 -0
- package/dist/shell/components/TopBar.d.ts +4 -2
- package/dist/shell/components/TopBar.js +9 -3
- package/dist/shell/components/UpdateBanner.d.ts +5 -0
- package/dist/shell/components/UpdateBanner.js +8 -0
- package/dist/shell/components/index.d.ts +4 -1
- package/dist/shell/components/index.js +2 -0
- package/dist/shell/components/theme-provider.d.ts +0 -1
- package/dist/shell/components/theme-provider.js +8 -5
- package/dist/shell/hooks/useCspViolations.d.ts +12 -0
- package/dist/shell/hooks/useCspViolations.js +34 -0
- package/dist/shell/index.d.ts +1 -2
- package/dist/shell/polling-config.d.ts +10 -0
- package/dist/shell/polling-config.js +26 -0
- package/dist/shell/search/fuzzy.d.ts +0 -1
- package/dist/shell/search/index.d.ts +0 -1
- package/dist/shell/telemetry.d.ts +0 -1
- package/dist/shell/types.d.ts +34 -18
- package/dist/tailwind/index.d.ts +0 -1
- package/dist/types/keycloak.d.ts +0 -1
- package/dist/types/platform.d.ts +12 -1
- package/dist/vite/AdminShellSharedDeps.d.ts +64 -0
- package/dist/vite/AdminShellSharedDeps.js +215 -0
- package/dist/vite/config.d.ts +10 -2
- package/dist/vite/config.js +13 -10
- package/dist/vite/i18n-plugin.d.ts +13 -0
- package/dist/vite/i18n-plugin.js +81 -0
- package/dist/vite/index.d.ts +2 -1
- package/dist/vite/index.js +2 -0
- package/dist/vite/plugins.d.ts +0 -1
- package/package.json +6 -2
- package/dist/auth/client/in-memory.d.ts.map +0 -1
- package/dist/auth/client/index.d.ts.map +0 -1
- package/dist/auth/client/interface.d.ts.map +0 -1
- package/dist/auth/client/keycloak.d.ts.map +0 -1
- package/dist/auth/components/UserSelector.d.ts.map +0 -1
- package/dist/auth/components/index.d.ts.map +0 -1
- package/dist/auth/index.d.ts.map +0 -1
- package/dist/components/AuthProvider.d.ts.map +0 -1
- package/dist/hooks/useAuth.d.ts.map +0 -1
- package/dist/hooks/useFetch.d.ts.map +0 -1
- package/dist/hooks/useI18n.d.ts.map +0 -1
- package/dist/hooks/usePlatformAPI.d.ts.map +0 -1
- package/dist/hooks/useTelemetry.d.ts.map +0 -1
- package/dist/i18n/config.d.ts.map +0 -1
- package/dist/i18n/index.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/registry/AdminShellRegistry.d.ts.map +0 -1
- package/dist/registry/client/http.d.ts.map +0 -1
- package/dist/registry/client/in-memory.d.ts.map +0 -1
- package/dist/registry/client/index.d.ts.map +0 -1
- package/dist/registry/client/interface.d.ts.map +0 -1
- package/dist/registry/index.d.ts.map +0 -1
- package/dist/registry/types/index.d.ts.map +0 -1
- package/dist/registry/types/manifest.d.ts.map +0 -1
- package/dist/registry/types/module.d.ts.map +0 -1
- package/dist/router/DynamicModule.d.ts.map +0 -1
- package/dist/router/index.d.ts.map +0 -1
- package/dist/shell/AdminShell.d.ts.map +0 -1
- package/dist/shell/BackofficeShell.d.ts.map +0 -1
- package/dist/shell/components/CommandPalette.d.ts.map +0 -1
- package/dist/shell/components/HomePage.d.ts.map +0 -1
- package/dist/shell/components/LeftNav.d.ts.map +0 -1
- package/dist/shell/components/MainContent.d.ts.map +0 -1
- package/dist/shell/components/ModuleOverview.d.ts.map +0 -1
- package/dist/shell/components/ProfilePage.d.ts.map +0 -1
- package/dist/shell/components/RegistryPage.d.ts.map +0 -1
- package/dist/shell/components/SettingsPage.d.ts.map +0 -1
- package/dist/shell/components/TopBar.d.ts.map +0 -1
- package/dist/shell/components/index.d.ts.map +0 -1
- package/dist/shell/components/theme-provider.d.ts.map +0 -1
- package/dist/shell/index.d.ts.map +0 -1
- package/dist/shell/search/fuzzy.d.ts.map +0 -1
- package/dist/shell/search/index.d.ts.map +0 -1
- package/dist/shell/telemetry.d.ts.map +0 -1
- package/dist/shell/types.d.ts.map +0 -1
- package/dist/tailwind/index.d.ts.map +0 -1
- package/dist/types/keycloak.d.ts.map +0 -1
- package/dist/types/platform.d.ts.map +0 -1
- package/dist/vite/config.d.ts.map +0 -1
- package/dist/vite/index.d.ts.map +0 -1
- package/dist/vite/plugins.d.ts.map +0 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export class GatewayTimeoutError extends Error {
|
|
2
|
+
constructor(gatewayUrl, timeoutMs) {
|
|
3
|
+
super(`Gateway at ${gatewayUrl} did not respond within ${timeoutMs}ms`);
|
|
4
|
+
Object.defineProperty(this, "gatewayUrl", {
|
|
5
|
+
enumerable: true,
|
|
6
|
+
configurable: true,
|
|
7
|
+
writable: true,
|
|
8
|
+
value: gatewayUrl
|
|
9
|
+
});
|
|
10
|
+
Object.defineProperty(this, "timeoutMs", {
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
writable: true,
|
|
14
|
+
value: timeoutMs
|
|
15
|
+
});
|
|
16
|
+
this.name = 'GatewayTimeoutError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export class GatewayFetchError extends Error {
|
|
20
|
+
constructor(gatewayUrl, statusCode, originalError) {
|
|
21
|
+
const detail = statusCode
|
|
22
|
+
? `returned HTTP ${statusCode}`
|
|
23
|
+
: 'is not reachable';
|
|
24
|
+
super(`Gateway at ${gatewayUrl} ${detail}`);
|
|
25
|
+
Object.defineProperty(this, "gatewayUrl", {
|
|
26
|
+
enumerable: true,
|
|
27
|
+
configurable: true,
|
|
28
|
+
writable: true,
|
|
29
|
+
value: gatewayUrl
|
|
30
|
+
});
|
|
31
|
+
Object.defineProperty(this, "statusCode", {
|
|
32
|
+
enumerable: true,
|
|
33
|
+
configurable: true,
|
|
34
|
+
writable: true,
|
|
35
|
+
value: statusCode
|
|
36
|
+
});
|
|
37
|
+
Object.defineProperty(this, "originalError", {
|
|
38
|
+
enumerable: true,
|
|
39
|
+
configurable: true,
|
|
40
|
+
writable: true,
|
|
41
|
+
value: originalError
|
|
42
|
+
});
|
|
43
|
+
this.name = 'GatewayFetchError';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function isValidResponse(data) {
|
|
47
|
+
if (typeof data !== 'object' || data === null)
|
|
48
|
+
return false;
|
|
49
|
+
const obj = data;
|
|
50
|
+
return typeof obj.token === 'string' && (typeof obj.expires === 'string' || typeof obj.expires === 'number');
|
|
51
|
+
}
|
|
52
|
+
export async function fetchGatewayToken(gatewayUrl, user, options) {
|
|
53
|
+
const timeoutMs = options?.timeoutMs ?? 5000;
|
|
54
|
+
const params = new URLSearchParams({
|
|
55
|
+
sub: user.id,
|
|
56
|
+
email: user.email,
|
|
57
|
+
roles: user.roles.join(','),
|
|
58
|
+
scopes: 'openid,profile,email',
|
|
59
|
+
});
|
|
60
|
+
const url = `${gatewayUrl}/auth/token?${params.toString()}`;
|
|
61
|
+
const controller = new AbortController();
|
|
62
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
63
|
+
try {
|
|
64
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
throw new GatewayFetchError(gatewayUrl, response.status);
|
|
67
|
+
}
|
|
68
|
+
const data = await response.json();
|
|
69
|
+
if (!isValidResponse(data)) {
|
|
70
|
+
throw new GatewayFetchError(gatewayUrl);
|
|
71
|
+
}
|
|
72
|
+
const expiresAt = typeof data.expires === 'number'
|
|
73
|
+
? data.expires * 1000
|
|
74
|
+
: new Date(data.expires).getTime();
|
|
75
|
+
return { token: data.token, expiresAt };
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error instanceof GatewayTimeoutError || error instanceof GatewayFetchError) {
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
82
|
+
throw new GatewayTimeoutError(gatewayUrl, timeoutMs);
|
|
83
|
+
}
|
|
84
|
+
throw new GatewayFetchError(gatewayUrl, undefined, error);
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
clearTimeout(timeoutId);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Users can be selected from a predefined list or created custom.
|
|
6
6
|
*/
|
|
7
7
|
import type { MockUser, InMemoryAuthClient } from './interface';
|
|
8
|
+
export { GatewayTimeoutError, GatewayFetchError } from './gateway-token';
|
|
8
9
|
/**
|
|
9
10
|
* Role configuration for creating mock users
|
|
10
11
|
*/
|
|
@@ -45,6 +46,10 @@ export interface InMemoryAuthClientOptions {
|
|
|
45
46
|
users: MockUser[];
|
|
46
47
|
/** localStorage key prefix (defaults to '@nsxbet/auth') */
|
|
47
48
|
storageKey?: string;
|
|
49
|
+
/** Admin gateway URL for fetching real signed JWTs. If not set, falls back to import.meta.env.VITE_ADMIN_GATEWAY_URL. If neither is set, BFF integration is disabled. */
|
|
50
|
+
gatewayUrl?: string | null;
|
|
51
|
+
/** Timeout in milliseconds for gateway token fetch requests (defaults to 5000) */
|
|
52
|
+
tokenTimeout?: number;
|
|
48
53
|
}
|
|
49
54
|
/**
|
|
50
55
|
* Create an in-memory auth client for development/testing
|
|
@@ -66,4 +71,3 @@ export declare function createInMemoryAuthClient(options: InMemoryAuthClientOpti
|
|
|
66
71
|
* Clear in-memory auth storage (useful for tests)
|
|
67
72
|
*/
|
|
68
73
|
export declare function clearInMemoryAuth(storageKey?: string): void;
|
|
69
|
-
//# sourceMappingURL=in-memory.d.ts.map
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Provides fake authentication for development and testing.
|
|
5
5
|
* Users can be selected from a predefined list or created custom.
|
|
6
6
|
*/
|
|
7
|
+
import { fetchGatewayToken } from './gateway-token';
|
|
8
|
+
export { GatewayTimeoutError, GatewayFetchError } from './gateway-token';
|
|
7
9
|
/**
|
|
8
10
|
* Create mock users from a role configuration
|
|
9
11
|
*
|
|
@@ -67,15 +69,19 @@ const DEFAULT_STORAGE_KEY = '@nsxbet/auth';
|
|
|
67
69
|
* ```
|
|
68
70
|
*/
|
|
69
71
|
export function createInMemoryAuthClient(options) {
|
|
70
|
-
const { users, storageKey = DEFAULT_STORAGE_KEY } = options;
|
|
71
|
-
// Use provided users
|
|
72
|
+
const { users, storageKey = DEFAULT_STORAGE_KEY, tokenTimeout = 5000 } = options;
|
|
72
73
|
const predefinedUsers = users;
|
|
74
|
+
// Resolve gatewayUrl: explicit option → env var → null (disabled)
|
|
75
|
+
const resolvedGatewayUrl = options.gatewayUrl !== undefined
|
|
76
|
+
? options.gatewayUrl ?? null
|
|
77
|
+
: (typeof import.meta !== 'undefined' && import.meta.env?.VITE_ADMIN_GATEWAY_URL) || null;
|
|
73
78
|
// State
|
|
74
79
|
let selectedUser = null;
|
|
80
|
+
let tokenCache = null;
|
|
81
|
+
let useMockFallback = false;
|
|
82
|
+
let backgroundRefreshInFlight = false;
|
|
75
83
|
const subscribers = new Set();
|
|
76
|
-
|
|
77
|
-
* Load state from localStorage
|
|
78
|
-
*/
|
|
84
|
+
const REFRESH_BUFFER_MS = 60000;
|
|
79
85
|
function loadStorage() {
|
|
80
86
|
try {
|
|
81
87
|
const data = localStorage.getItem(storageKey);
|
|
@@ -88,28 +94,16 @@ export function createInMemoryAuthClient(options) {
|
|
|
88
94
|
}
|
|
89
95
|
return { selectedUserId: null, customUsers: [] };
|
|
90
96
|
}
|
|
91
|
-
/**
|
|
92
|
-
* Save state to localStorage
|
|
93
|
-
*/
|
|
94
97
|
function saveStorage(data) {
|
|
95
98
|
localStorage.setItem(storageKey, JSON.stringify(data));
|
|
96
99
|
}
|
|
97
|
-
/**
|
|
98
|
-
* Get all available users (predefined + custom)
|
|
99
|
-
*/
|
|
100
100
|
function getAllUsers() {
|
|
101
101
|
const storage = loadStorage();
|
|
102
102
|
return [...predefinedUsers, ...storage.customUsers];
|
|
103
103
|
}
|
|
104
|
-
/**
|
|
105
|
-
* Find user by ID
|
|
106
|
-
*/
|
|
107
104
|
function findUser(userId) {
|
|
108
105
|
return getAllUsers().find((u) => u.id === userId);
|
|
109
106
|
}
|
|
110
|
-
/**
|
|
111
|
-
* Notify subscribers of state change
|
|
112
|
-
*/
|
|
113
107
|
function notifySubscribers() {
|
|
114
108
|
const state = {
|
|
115
109
|
isAuthenticated: selectedUser !== null,
|
|
@@ -117,23 +111,36 @@ export function createInMemoryAuthClient(options) {
|
|
|
117
111
|
id: selectedUser.id,
|
|
118
112
|
email: selectedUser.email,
|
|
119
113
|
displayName: selectedUser.displayName,
|
|
114
|
+
roles: selectedUser.roles,
|
|
120
115
|
} : null,
|
|
121
116
|
};
|
|
122
117
|
subscribers.forEach((callback) => callback(state));
|
|
123
118
|
}
|
|
124
|
-
/**
|
|
125
|
-
* Convert MockUser to User
|
|
126
|
-
*/
|
|
127
119
|
function toUser(mockUser) {
|
|
128
120
|
return {
|
|
129
121
|
id: mockUser.id,
|
|
130
122
|
email: mockUser.email,
|
|
131
123
|
displayName: mockUser.displayName,
|
|
124
|
+
roles: mockUser.roles,
|
|
132
125
|
};
|
|
133
126
|
}
|
|
134
|
-
|
|
127
|
+
function mockToken() {
|
|
128
|
+
return `mock-token-${selectedUser.id}-${Date.now()}`;
|
|
129
|
+
}
|
|
130
|
+
async function refreshToken() {
|
|
131
|
+
return fetchGatewayToken(resolvedGatewayUrl, selectedUser, { timeoutMs: tokenTimeout });
|
|
132
|
+
}
|
|
133
|
+
function selectUser(user, userId) {
|
|
134
|
+
selectedUser = user;
|
|
135
|
+
const storage = loadStorage();
|
|
136
|
+
storage.selectedUserId = userId;
|
|
137
|
+
saveStorage(storage);
|
|
138
|
+
}
|
|
135
139
|
const client = {
|
|
136
140
|
type: 'in-memory',
|
|
141
|
+
get gatewayUrl() {
|
|
142
|
+
return resolvedGatewayUrl;
|
|
143
|
+
},
|
|
137
144
|
async initialize() {
|
|
138
145
|
const storage = loadStorage();
|
|
139
146
|
if (storage.selectedUserId) {
|
|
@@ -155,21 +162,44 @@ export function createInMemoryAuthClient(options) {
|
|
|
155
162
|
if (!selectedUser) {
|
|
156
163
|
throw new Error('Not authenticated');
|
|
157
164
|
}
|
|
158
|
-
|
|
165
|
+
if (useMockFallback || !resolvedGatewayUrl) {
|
|
166
|
+
return mockToken();
|
|
167
|
+
}
|
|
168
|
+
if (tokenCache) {
|
|
169
|
+
const now = Date.now();
|
|
170
|
+
if (now < tokenCache.expiresAt) {
|
|
171
|
+
// Token still valid — trigger background refresh if nearing expiry
|
|
172
|
+
if (now >= tokenCache.expiresAt - REFRESH_BUFFER_MS && !backgroundRefreshInFlight) {
|
|
173
|
+
backgroundRefreshInFlight = true;
|
|
174
|
+
refreshToken()
|
|
175
|
+
.then((result) => { tokenCache = result; })
|
|
176
|
+
.catch(() => { console.warn('[InMemoryAuth] Background token refresh failed, will retry on next access'); })
|
|
177
|
+
.finally(() => { backgroundRefreshInFlight = false; });
|
|
178
|
+
}
|
|
179
|
+
return tokenCache.token;
|
|
180
|
+
}
|
|
181
|
+
// Token fully expired — try foreground fetch
|
|
182
|
+
try {
|
|
183
|
+
tokenCache = await refreshToken();
|
|
184
|
+
return tokenCache.token;
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
console.warn('[InMemoryAuth] Token expired and refresh failed, falling back to mock token');
|
|
188
|
+
return mockToken();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return mockToken();
|
|
159
192
|
},
|
|
160
193
|
hasPermission(permission) {
|
|
161
194
|
if (!selectedUser) {
|
|
162
195
|
return false;
|
|
163
196
|
}
|
|
164
|
-
// '*' means all permissions
|
|
165
197
|
if (selectedUser.roles.includes('*')) {
|
|
166
198
|
return true;
|
|
167
199
|
}
|
|
168
|
-
// Check exact match or wildcard
|
|
169
200
|
return selectedUser.roles.some((role) => {
|
|
170
201
|
if (role === permission)
|
|
171
202
|
return true;
|
|
172
|
-
// Support wildcard like 'admin.*'
|
|
173
203
|
if (role.endsWith('.*')) {
|
|
174
204
|
const prefix = role.slice(0, -2);
|
|
175
205
|
return permission.startsWith(prefix);
|
|
@@ -179,6 +209,8 @@ export function createInMemoryAuthClient(options) {
|
|
|
179
209
|
},
|
|
180
210
|
logout() {
|
|
181
211
|
selectedUser = null;
|
|
212
|
+
tokenCache = null;
|
|
213
|
+
useMockFallback = false;
|
|
182
214
|
const storage = loadStorage();
|
|
183
215
|
storage.selectedUserId = null;
|
|
184
216
|
saveStorage(storage);
|
|
@@ -193,34 +225,39 @@ export function createInMemoryAuthClient(options) {
|
|
|
193
225
|
getAvailableUsers() {
|
|
194
226
|
return getAllUsers();
|
|
195
227
|
},
|
|
196
|
-
login(userId) {
|
|
228
|
+
async login(userId, loginOptions) {
|
|
197
229
|
const user = findUser(userId);
|
|
198
230
|
if (!user) {
|
|
199
231
|
throw new Error(`User not found: ${userId}`);
|
|
200
232
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
233
|
+
tokenCache = null;
|
|
234
|
+
if (loginOptions?.fallbackToMock || !resolvedGatewayUrl) {
|
|
235
|
+
useMockFallback = true;
|
|
236
|
+
if (resolvedGatewayUrl && loginOptions?.fallbackToMock) {
|
|
237
|
+
console.warn('[InMemoryAuth] Continuing with mock token — gateway token fetch was skipped');
|
|
238
|
+
}
|
|
239
|
+
selectUser(user, userId);
|
|
240
|
+
notifySubscribers();
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
useMockFallback = false;
|
|
244
|
+
tokenCache = await fetchGatewayToken(resolvedGatewayUrl, user, { timeoutMs: tokenTimeout });
|
|
245
|
+
selectUser(user, userId);
|
|
205
246
|
notifySubscribers();
|
|
206
247
|
},
|
|
207
|
-
createCustomUser(userData) {
|
|
248
|
+
async createCustomUser(userData) {
|
|
208
249
|
const id = `custom-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
209
250
|
const newUser = { ...userData, id };
|
|
210
|
-
// Save to storage
|
|
211
251
|
const storage = loadStorage();
|
|
212
252
|
storage.customUsers.push(newUser);
|
|
213
253
|
saveStorage(storage);
|
|
214
|
-
|
|
215
|
-
client.login(id);
|
|
254
|
+
await client.login(id);
|
|
216
255
|
return newUser;
|
|
217
256
|
},
|
|
218
257
|
isCustomUser(userId) {
|
|
219
|
-
// Custom users have IDs starting with 'custom-'
|
|
220
258
|
return userId.startsWith('custom-');
|
|
221
259
|
},
|
|
222
260
|
deleteCustomUser(userId) {
|
|
223
|
-
// Cannot delete predefined users
|
|
224
261
|
if (!client.isCustomUser(userId)) {
|
|
225
262
|
return false;
|
|
226
263
|
}
|
|
@@ -229,12 +266,12 @@ export function createInMemoryAuthClient(options) {
|
|
|
229
266
|
if (index === -1) {
|
|
230
267
|
return false;
|
|
231
268
|
}
|
|
232
|
-
// Remove from storage
|
|
233
269
|
storage.customUsers.splice(index, 1);
|
|
234
|
-
// If deleted user was logged in, logout
|
|
235
270
|
if (storage.selectedUserId === userId) {
|
|
236
271
|
storage.selectedUserId = null;
|
|
237
272
|
selectedUser = null;
|
|
273
|
+
tokenCache = null;
|
|
274
|
+
useMockFallback = false;
|
|
238
275
|
}
|
|
239
276
|
saveStorage(storage);
|
|
240
277
|
notifySubscribers();
|
|
@@ -4,4 +4,3 @@
|
|
|
4
4
|
export type { AuthClient, InMemoryAuthClient, MockUser, AuthState, AuthStateCallback, } from './interface';
|
|
5
5
|
export { createInMemoryAuthClient, clearInMemoryAuth, createMockUsersFromRoles, type InMemoryAuthClientOptions, type MockUserRoles, } from './in-memory';
|
|
6
6
|
export { createKeycloakAuthClient, type KeycloakAuthClientOptions, } from './keycloak';
|
|
7
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -91,6 +91,8 @@ export interface AuthClient {
|
|
|
91
91
|
*/
|
|
92
92
|
export interface InMemoryAuthClient extends AuthClient {
|
|
93
93
|
readonly type: 'in-memory';
|
|
94
|
+
/** Resolved gateway URL, or null if BFF integration is disabled */
|
|
95
|
+
readonly gatewayUrl: string | null;
|
|
94
96
|
/**
|
|
95
97
|
* Get all available mock users
|
|
96
98
|
*/
|
|
@@ -102,14 +104,15 @@ export interface InMemoryAuthClient extends AuthClient {
|
|
|
102
104
|
/**
|
|
103
105
|
* Login as a specific user
|
|
104
106
|
*/
|
|
105
|
-
login(userId: string
|
|
107
|
+
login(userId: string, options?: {
|
|
108
|
+
fallbackToMock?: boolean;
|
|
109
|
+
}): Promise<void>;
|
|
106
110
|
/**
|
|
107
111
|
* Create and login as a custom user
|
|
108
112
|
*/
|
|
109
|
-
createCustomUser(user: Omit<MockUser, 'id'>): MockUser
|
|
113
|
+
createCustomUser(user: Omit<MockUser, 'id'>): Promise<MockUser>;
|
|
110
114
|
/**
|
|
111
115
|
* Delete a custom user (predefined users cannot be deleted)
|
|
112
116
|
*/
|
|
113
117
|
deleteCustomUser(userId: string): boolean;
|
|
114
118
|
}
|
|
115
|
-
//# sourceMappingURL=interface.d.ts.map
|
|
@@ -21,6 +21,7 @@ export function createKeycloakAuthClient(options) {
|
|
|
21
21
|
id: tokenParsed.sub || '',
|
|
22
22
|
email: tokenParsed.email || '',
|
|
23
23
|
displayName: tokenParsed.name || tokenParsed.preferred_username || '',
|
|
24
|
+
roles: tokenParsed.realm_access?.roles ?? [],
|
|
24
25
|
};
|
|
25
26
|
}
|
|
26
27
|
/**
|
|
@@ -101,10 +102,12 @@ export function createKeycloakAuthClient(options) {
|
|
|
101
102
|
}
|
|
102
103
|
},
|
|
103
104
|
hasPermission(permission) {
|
|
104
|
-
//
|
|
105
|
-
//
|
|
105
|
+
// Deny-by-default: return false until Keycloak is fully initialized.
|
|
106
|
+
// This prevents a window where all actions appear permitted during auth init.
|
|
107
|
+
// AuthProvider gates rendering behind isAuthenticated, so modules won't
|
|
108
|
+
// render permission-gated UI until auth completes.
|
|
106
109
|
if (!isInitialized || !keycloak) {
|
|
107
|
-
return
|
|
110
|
+
return false;
|
|
108
111
|
}
|
|
109
112
|
return keycloak.hasRealmRole(permission);
|
|
110
113
|
},
|
|
@@ -6,7 +6,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
6
6
|
* Uses Brasa Design System tokens for consistent styling.
|
|
7
7
|
*/
|
|
8
8
|
import { useState } from 'react';
|
|
9
|
-
import { Crown, Ban, Eye, User, Users, Sparkles, Wrench, Trash2, Plus, ChevronRight } from '@nsxbet/admin-ui';
|
|
9
|
+
import { Crown, Ban, Eye, User, Users, Sparkles, Wrench, Trash2, Plus, ChevronRight, Loader2, AlertCircle, Clock, ArrowLeft, RefreshCw, ShieldAlert, } from '@nsxbet/admin-ui';
|
|
10
|
+
import { GatewayTimeoutError } from '../client/in-memory';
|
|
10
11
|
/**
|
|
11
12
|
* Get user icon based on roles
|
|
12
13
|
*/
|
|
@@ -84,26 +85,107 @@ function CustomUserForm({ onSubmit, onCancel, }) {
|
|
|
84
85
|
const isValid = displayName.trim() && email.trim();
|
|
85
86
|
return (_jsxs("form", { onSubmit: handleSubmit, className: "p-4 rounded-xl border border-border bg-card", children: [_jsxs("h3", { className: "font-semibold text-foreground mb-4 flex items-center gap-2", children: [_jsx(Sparkles, { className: "h-5 w-5 text-amber-500" }), "Create Custom User"] }), _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-sm font-medium text-muted-foreground mb-1", children: "Display Name" }), _jsx("input", { type: "text", value: displayName, onChange: (e) => setDisplayName(e.target.value), placeholder: "John Doe", className: "w-full px-3 py-2 rounded-lg border border-border bg-background text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent" })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-sm font-medium text-muted-foreground mb-1", children: "Email" }), _jsx("input", { type: "email", value: email, onChange: (e) => setEmail(e.target.value), placeholder: "john@example.com", className: "w-full px-3 py-2 rounded-lg border border-border bg-background text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent" })] }), _jsxs("div", { children: [_jsxs("label", { className: "block text-sm font-medium text-muted-foreground mb-1", children: ["Roles ", _jsx("span", { className: "text-muted-foreground/60", children: "(comma-separated)" })] }), _jsx("input", { type: "text", value: rolesInput, onChange: (e) => setRolesInput(e.target.value), placeholder: "admin, admin.users.view, admin.tasks.edit", className: "w-full px-3 py-2 rounded-lg border border-border bg-background text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent" }), _jsxs("p", { className: "text-xs text-muted-foreground mt-1", children: ["Use ", _jsx("code", { className: "px-1 py-0.5 rounded bg-muted", children: "*" }), " for all permissions"] })] })] }), _jsxs("div", { className: "flex gap-2 mt-4", children: [_jsx("button", { type: "button", onClick: onCancel, className: "flex-1 px-4 py-2 rounded-lg border border-border text-foreground hover:bg-muted/50 transition-colors", children: "Cancel" }), _jsx("button", { type: "submit", disabled: !isValid, className: "flex-1 px-4 py-2 rounded-lg bg-primary text-primary-foreground font-medium hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors", children: "Create & Login" })] })] }));
|
|
86
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Loading screen shown while fetching a BFF token.
|
|
90
|
+
*/
|
|
91
|
+
function LoginLoadingScreen({ userName }) {
|
|
92
|
+
return (_jsx("div", { className: "min-h-screen bg-background flex items-center justify-center p-4", children: _jsxs("div", { className: "w-full max-w-md text-center", children: [_jsx("div", { className: "inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary/10 mb-6", children: _jsx(Loader2, { className: "h-8 w-8 text-primary animate-spin" }) }), _jsx("h2", { className: "text-xl font-semibold text-foreground mb-2", children: "Authenticating\u2026" }), _jsxs("p", { className: "text-muted-foreground", children: ["Fetching token for ", _jsx("span", { className: "font-medium text-foreground", children: userName })] })] }) }));
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Shared action bar for error / timeout screens.
|
|
96
|
+
*/
|
|
97
|
+
function LoginRecoveryActions({ onRetry, onFallback, onBack, retrying, }) {
|
|
98
|
+
return (_jsxs("div", { className: "space-y-3 mt-6 w-full", children: [_jsxs("button", { onClick: onRetry, disabled: retrying, className: "w-full px-4 py-2.5 rounded-lg bg-primary text-primary-foreground font-medium hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center justify-center gap-2", children: [retrying ? _jsx(Loader2, { className: "h-4 w-4 animate-spin" }) : _jsx(RefreshCw, { className: "h-4 w-4" }), "Retry"] }), _jsxs("button", { onClick: onFallback, disabled: retrying, className: "w-full px-4 py-2.5 rounded-lg border border-border text-foreground hover:bg-muted/50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center justify-center gap-2", children: [_jsx(ShieldAlert, { className: "h-4 w-4" }), "Continue with mock token"] }), _jsxs("button", { onClick: onBack, disabled: retrying, className: "w-full px-4 py-2 text-sm text-muted-foreground hover:text-foreground disabled:opacity-50 transition-colors flex items-center justify-center gap-1", children: [_jsx(ArrowLeft, { className: "h-3.5 w-3.5" }), "Back to user selection"] })] }));
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Error screen shown when the gateway returns an error or is unreachable.
|
|
102
|
+
*/
|
|
103
|
+
function LoginErrorScreen({ errorMessage, gatewayUrl, onRetry, onFallback, onBack, retrying, }) {
|
|
104
|
+
return (_jsx("div", { className: "min-h-screen bg-background flex items-center justify-center p-4", children: _jsxs("div", { className: "w-full max-w-md flex flex-col items-center text-center", children: [_jsx("div", { className: "inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-destructive/10 mb-6", children: _jsx(AlertCircle, { className: "h-8 w-8 text-destructive" }) }), _jsx("h2", { className: "text-xl font-semibold text-foreground mb-2", children: "Authentication Failed" }), _jsx("p", { className: "text-muted-foreground mb-1", children: errorMessage }), gatewayUrl && (_jsx("p", { className: "text-xs text-muted-foreground/70 font-mono break-all", children: gatewayUrl })), _jsx(LoginRecoveryActions, { onRetry: onRetry, onFallback: onFallback, onBack: onBack, retrying: retrying })] }) }));
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Timeout screen shown when the gateway doesn't respond in time.
|
|
108
|
+
*/
|
|
109
|
+
function LoginTimeoutScreen({ gatewayUrl, onRetry, onFallback, onBack, retrying, }) {
|
|
110
|
+
return (_jsx("div", { className: "min-h-screen bg-background flex items-center justify-center p-4", children: _jsxs("div", { className: "w-full max-w-md flex flex-col items-center text-center", children: [_jsx("div", { className: "inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-amber-500/10 mb-6", children: _jsx(Clock, { className: "h-8 w-8 text-amber-500" }) }), _jsx("h2", { className: "text-xl font-semibold text-foreground mb-2", children: "Gateway Timeout" }), _jsx("p", { className: "text-muted-foreground mb-1", children: "The gateway did not respond in time." }), gatewayUrl && (_jsx("p", { className: "text-xs text-muted-foreground/70 font-mono break-all", children: gatewayUrl })), _jsx(LoginRecoveryActions, { onRetry: onRetry, onFallback: onFallback, onBack: onBack, retrying: retrying })] }) }));
|
|
111
|
+
}
|
|
87
112
|
/**
|
|
88
113
|
* Main User Selector Component
|
|
89
114
|
*/
|
|
90
115
|
export function UserSelector({ authClient, onUserSelected }) {
|
|
91
116
|
const [showCustomForm, setShowCustomForm] = useState(false);
|
|
117
|
+
const [loginState, setLoginState] = useState({ status: 'idle' });
|
|
92
118
|
const [, forceUpdate] = useState(0);
|
|
93
119
|
const users = authClient.getAvailableUsers();
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
120
|
+
const findUserName = (userId) => users.find((u) => u.id === userId)?.displayName ?? userId;
|
|
121
|
+
const handleSelectUser = async (userId) => {
|
|
122
|
+
if (!authClient.gatewayUrl) {
|
|
123
|
+
await authClient.login(userId);
|
|
124
|
+
onUserSelected?.();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
setLoginState({ status: 'loading', userId });
|
|
128
|
+
try {
|
|
129
|
+
await authClient.login(userId);
|
|
130
|
+
onUserSelected?.();
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (error instanceof GatewayTimeoutError) {
|
|
134
|
+
setLoginState({ status: 'timeout', userId });
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
setLoginState({
|
|
138
|
+
status: 'error',
|
|
139
|
+
userId,
|
|
140
|
+
error: error instanceof Error ? error.message : 'An unexpected error occurred',
|
|
141
|
+
canRetry: true,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const handleRetry = () => {
|
|
147
|
+
if (loginState.status === 'error' || loginState.status === 'timeout') {
|
|
148
|
+
handleSelectUser(loginState.userId);
|
|
149
|
+
}
|
|
97
150
|
};
|
|
98
|
-
const
|
|
99
|
-
|
|
151
|
+
const handleFallbackToMock = async () => {
|
|
152
|
+
if (loginState.status === 'error' || loginState.status === 'timeout') {
|
|
153
|
+
const { userId } = loginState;
|
|
154
|
+
setLoginState({ status: 'loading', userId });
|
|
155
|
+
try {
|
|
156
|
+
await authClient.login(userId, { fallbackToMock: true });
|
|
157
|
+
onUserSelected?.();
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
setLoginState({
|
|
161
|
+
status: 'error',
|
|
162
|
+
userId,
|
|
163
|
+
error: error instanceof Error ? error.message : 'An unexpected error occurred',
|
|
164
|
+
canRetry: true,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
const handleBack = () => {
|
|
170
|
+
setLoginState({ status: 'idle' });
|
|
171
|
+
};
|
|
172
|
+
const handleCreateCustomUser = async (userData) => {
|
|
173
|
+
await authClient.createCustomUser(userData);
|
|
100
174
|
onUserSelected?.();
|
|
101
175
|
};
|
|
102
176
|
const handleDeleteUser = (userId) => {
|
|
103
177
|
authClient.deleteCustomUser(userId);
|
|
104
|
-
// Force re-render to update the list
|
|
105
178
|
forceUpdate((n) => n + 1);
|
|
106
179
|
};
|
|
180
|
+
if (loginState.status === 'loading') {
|
|
181
|
+
return _jsx(LoginLoadingScreen, { userName: findUserName(loginState.userId) });
|
|
182
|
+
}
|
|
183
|
+
if (loginState.status === 'error') {
|
|
184
|
+
return (_jsx(LoginErrorScreen, { errorMessage: loginState.error, gatewayUrl: authClient.gatewayUrl, onRetry: handleRetry, onFallback: handleFallbackToMock, onBack: handleBack, retrying: false }));
|
|
185
|
+
}
|
|
186
|
+
if (loginState.status === 'timeout') {
|
|
187
|
+
return (_jsx(LoginTimeoutScreen, { gatewayUrl: authClient.gatewayUrl, onRetry: handleRetry, onFallback: handleFallbackToMock, onBack: handleBack, retrying: false }));
|
|
188
|
+
}
|
|
107
189
|
return (_jsx("div", { className: "min-h-screen bg-background flex items-center justify-center p-4", children: _jsxs("div", { className: "w-full max-w-md", children: [_jsxs("div", { className: "text-center mb-8", children: [_jsx("div", { className: "inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary mb-4 shadow-lg shadow-primary/25", children: _jsx(Users, { className: "h-8 w-8 text-primary-foreground" }) }), _jsx("h1", { className: "text-2xl font-bold text-foreground mb-2", children: "Select User" }), _jsx("p", { className: "text-muted-foreground", children: "Choose a mock user to continue in development mode" })] }), _jsx("div", { className: "space-y-3 mb-6", children: users.map((user) => {
|
|
108
190
|
const isCustom = authClient.isCustomUser(user.id);
|
|
109
191
|
return (_jsx(UserCard, { user: user, isCustom: isCustom, onClick: () => handleSelectUser(user.id), onDelete: isCustom ? () => handleDeleteUser(user.id) : undefined }, user.id));
|
package/dist/auth/index.d.ts
CHANGED
|
@@ -4,4 +4,3 @@
|
|
|
4
4
|
export type { AuthClient, InMemoryAuthClient, MockUser, AuthState, AuthStateCallback, } from './client';
|
|
5
5
|
export { createInMemoryAuthClient, clearInMemoryAuth, createMockUsersFromRoles, createKeycloakAuthClient, type InMemoryAuthClientOptions, type MockUserRoles, type KeycloakAuthClientOptions, } from './client';
|
|
6
6
|
export { UserSelector } from './components';
|
|
7
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { TimestampFormat } from '../types/platform';
|
|
2
|
+
export interface TimestampProps {
|
|
3
|
+
value: Date | string;
|
|
4
|
+
format?: TimestampFormat;
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function Timestamp({ value, format, className }: TimestampProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@nsxbet/admin-ui';
|
|
4
|
+
import { useTimestamp } from '../hooks/useTimestamp';
|
|
5
|
+
export function Timestamp({ value, format = 'datetime', className }) {
|
|
6
|
+
const { mode, formatDate } = useTimestamp();
|
|
7
|
+
const date = useMemo(() => {
|
|
8
|
+
if (value instanceof Date)
|
|
9
|
+
return value;
|
|
10
|
+
const parsed = new Date(value);
|
|
11
|
+
return parsed;
|
|
12
|
+
}, [value]);
|
|
13
|
+
const isInvalid = isNaN(date.getTime());
|
|
14
|
+
const formatted = useMemo(() => {
|
|
15
|
+
if (isInvalid)
|
|
16
|
+
return 'Invalid date';
|
|
17
|
+
return formatDate(date, format);
|
|
18
|
+
}, [date, format, formatDate, isInvalid]);
|
|
19
|
+
const tooltipText = useMemo(() => {
|
|
20
|
+
if (isInvalid)
|
|
21
|
+
return '';
|
|
22
|
+
const oppositeMode = mode === 'utc' ? 'local' : 'utc';
|
|
23
|
+
const oppositeTz = oppositeMode === 'utc' ? 'UTC' : undefined;
|
|
24
|
+
const suffix = oppositeMode === 'utc' ? ' UTC' : '';
|
|
25
|
+
if (format === 'relative') {
|
|
26
|
+
const options = {
|
|
27
|
+
year: 'numeric',
|
|
28
|
+
month: 'short',
|
|
29
|
+
day: 'numeric',
|
|
30
|
+
hour: 'numeric',
|
|
31
|
+
minute: '2-digit',
|
|
32
|
+
second: '2-digit',
|
|
33
|
+
timeZone: oppositeTz,
|
|
34
|
+
};
|
|
35
|
+
return new Intl.DateTimeFormat('en-US', options).format(date) + suffix;
|
|
36
|
+
}
|
|
37
|
+
const options = {
|
|
38
|
+
year: 'numeric',
|
|
39
|
+
month: 'short',
|
|
40
|
+
day: 'numeric',
|
|
41
|
+
hour: 'numeric',
|
|
42
|
+
minute: '2-digit',
|
|
43
|
+
second: '2-digit',
|
|
44
|
+
timeZone: oppositeTz,
|
|
45
|
+
};
|
|
46
|
+
return new Intl.DateTimeFormat('en-US', options).format(date) + suffix;
|
|
47
|
+
}, [date, mode, format, isInvalid]);
|
|
48
|
+
const isoString = isInvalid ? '' : date.toISOString();
|
|
49
|
+
return (_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("time", { dateTime: isoString, className: className, children: formatted }) }), !isInvalid && (_jsx(TooltipContent, { children: _jsx("p", { children: tooltipText }) }))] }) }));
|
|
50
|
+
}
|
package/dist/hooks/useAuth.d.ts
CHANGED
package/dist/hooks/useAuth.js
CHANGED
|
@@ -25,7 +25,7 @@ export function useAuth() {
|
|
|
25
25
|
return {
|
|
26
26
|
getAccessToken: authContext.getAccessToken,
|
|
27
27
|
hasPermission: authContext.hasPermission,
|
|
28
|
-
getUser: () => authContext.user || { id: '', email: '', displayName: '' },
|
|
28
|
+
getUser: () => authContext.user || { id: '', email: '', displayName: '', roles: [] },
|
|
29
29
|
logout: authContext.logout,
|
|
30
30
|
};
|
|
31
31
|
}
|
package/dist/hooks/useFetch.d.ts
CHANGED
package/dist/hooks/useI18n.d.ts
CHANGED