@orsetra/shared-ui 1.0.24 → 1.0.26

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.
@@ -11,6 +11,7 @@ import { useIsMobile } from "../../hooks/use-mobile"
11
11
  import { Menu } from "lucide-react"
12
12
 
13
13
  interface LayoutContainerProps {
14
+ main_base_url: string
14
15
  children: React.ReactNode
15
16
  sidebarMenus: SidebarMenus
16
17
  user?: { profile?: { email?: string; preferred_username?: string } } | null
@@ -19,7 +20,7 @@ interface LayoutContainerProps {
19
20
  userMenuConfig?: UserMenuConfig
20
21
  }
21
22
 
22
- function LayoutContent({ children, sidebarMenus, user, onSignOut, mode = 'expanded', userMenuConfig }: LayoutContainerProps) {
23
+ function LayoutContent({ children, sidebarMenus, user, onSignOut, mode = 'expanded', userMenuConfig, main_base_url }: LayoutContainerProps) {
23
24
  const pathname = usePathname()
24
25
  const { setOpen } = useSidebar()
25
26
  const isMobile = useIsMobile()
@@ -69,6 +70,7 @@ function LayoutContent({ children, sidebarMenus, user, onSignOut, mode = 'expand
69
70
 
70
71
  {/* MainSidebar - always available, opens as overlay when isHidden */}
71
72
  <MainSidebar
73
+ main_base_url={main_base_url}
72
74
  isOpen={isMainSidebarOpen}
73
75
  onToggle={handleMainSidebarToggle}
74
76
  onMenuSelect={handleMenuSelect}
@@ -13,6 +13,7 @@ import Link from "next/link"
13
13
  export type SidebarMode = 'expanded' | 'minimized' | 'hidden'
14
14
 
15
15
  interface MainSidebarProps {
16
+ main_base_url: string
16
17
  isOpen: boolean
17
18
  onToggle: () => void
18
19
  onMenuSelect: (menu: string) => void
@@ -23,6 +24,7 @@ interface MainSidebarProps {
23
24
  }
24
25
 
25
26
  export function MainSidebar({
27
+ main_base_url,
26
28
  isOpen,
27
29
  onToggle,
28
30
  onMenuSelect,
@@ -49,7 +51,7 @@ export function MainSidebar({
49
51
  router.push(subMenus[0].href)
50
52
  } else {
51
53
  // Fallback: naviguer vers /{menuId} (ex: /assets-manager)
52
- router.push(`/${menuId}`)
54
+ router.push(`${main_base_url}/${menuId}`)
53
55
  }
54
56
 
55
57
  if (!isMinimized) {
@@ -2,6 +2,22 @@
2
2
 
3
3
  export type AuthHeadersProvider = () => Promise<Record<string, string>> | Record<string, string>;
4
4
 
5
+ export class ApiError extends Error {
6
+ status: number;
7
+ code?: number;
8
+ details?: unknown;
9
+ raw?: unknown;
10
+
11
+ constructor(message: string, params: { status: number; code?: number; details?: unknown; raw?: unknown }) {
12
+ super(message);
13
+ this.name = 'ApiError';
14
+ this.status = params.status;
15
+ this.code = params.code;
16
+ this.details = params.details;
17
+ this.raw = params.raw;
18
+ }
19
+ }
20
+
5
21
  class HttpClient {
6
22
  private baseUrl: string;
7
23
  private authHeadersProvider?: AuthHeadersProvider;
@@ -58,8 +74,9 @@ class HttpClient {
58
74
  if (!response.ok) {
59
75
  // Try to extract error message from API response
60
76
  let errorMessage = `Request failed: ${response.status}`;
77
+ let errorBody: any = undefined;
61
78
  try {
62
- const errorBody = await response.json();
79
+ errorBody = await response.json();
63
80
  // Zitadel API returns { code, message, details } on error
64
81
  if (errorBody?.message) {
65
82
  errorMessage = errorBody.message;
@@ -69,7 +86,32 @@ class HttpClient {
69
86
  } catch {
70
87
  // If we can't parse JSON, use the default message
71
88
  }
72
- throw new Error(errorMessage);
89
+
90
+ const apiError = new ApiError(errorMessage, {
91
+ status: response.status,
92
+ code: typeof errorBody?.code === 'number' ? errorBody.code : undefined,
93
+ details: errorBody?.details,
94
+ raw: errorBody,
95
+ });
96
+
97
+ // Zitadel step-up requirement
98
+ const isMfaRequired =
99
+ apiError.code === 7 ||
100
+ (typeof errorMessage === 'string' && errorMessage.toLowerCase().includes('mfa required'));
101
+
102
+ if (isMfaRequired && typeof window !== 'undefined') {
103
+ try {
104
+ window.dispatchEvent(
105
+ new CustomEvent('zitadel:mfa-required', {
106
+ detail: { url, status: apiError.status, code: apiError.code, error: apiError.raw },
107
+ })
108
+ );
109
+ } catch {
110
+ // ignore event dispatch issues
111
+ }
112
+ }
113
+
114
+ throw apiError;
73
115
  }
74
116
 
75
117
  if (response.status === 204) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orsetra/shared-ui",
3
- "version": "1.0.24",
3
+ "version": "1.0.26",
4
4
  "description": "Shared UI components for Orsetra platform",
5
5
  "main": "./index.ts",
6
6
  "types": "./index.ts",
@@ -92,4 +92,4 @@
92
92
  "next": "^16.0.7",
93
93
  "typescript": "^5"
94
94
  }
95
- }
95
+ }