@famgia/omnify-client-sso-react 1.0.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/README.md ADDED
@@ -0,0 +1,264 @@
1
+ # @omnify/sso-react
2
+
3
+ React components and hooks for Omnify SSO integration.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @omnify/sso-react
9
+ # or
10
+ yarn add @omnify/sso-react
11
+ # or
12
+ pnpm add @omnify/sso-react
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ### 1. Wrap your app with SsoProvider
18
+
19
+ ```tsx
20
+ // app/layout.tsx or _app.tsx
21
+ import { SsoProvider } from '@omnify/sso-react';
22
+
23
+ const ssoConfig = {
24
+ apiUrl: process.env.NEXT_PUBLIC_API_URL!,
25
+ consoleUrl: process.env.NEXT_PUBLIC_SSO_CONSOLE_URL!,
26
+ serviceSlug: process.env.NEXT_PUBLIC_SSO_SERVICE_SLUG!,
27
+ };
28
+
29
+ export default function RootLayout({ children }) {
30
+ return (
31
+ <SsoProvider config={ssoConfig}>
32
+ {children}
33
+ </SsoProvider>
34
+ );
35
+ }
36
+ ```
37
+
38
+ ### 2. Create callback page
39
+
40
+ ```tsx
41
+ // app/sso/callback/page.tsx
42
+ import { SsoCallback } from '@omnify/sso-react';
43
+
44
+ export default function CallbackPage() {
45
+ return (
46
+ <SsoCallback
47
+ redirectTo="/dashboard"
48
+ onSuccess={(user) => console.log('Logged in:', user)}
49
+ onError={(error) => console.error('Login failed:', error)}
50
+ />
51
+ );
52
+ }
53
+ ```
54
+
55
+ ### 3. Use hooks in your components
56
+
57
+ ```tsx
58
+ import { useAuth, useOrganization, useSso } from '@omnify/sso-react';
59
+
60
+ function Header() {
61
+ const { user, isAuthenticated, login, logout } = useAuth();
62
+ const { currentOrg } = useOrganization();
63
+
64
+ if (!isAuthenticated) {
65
+ return <button onClick={() => login()}>Login</button>;
66
+ }
67
+
68
+ return (
69
+ <div>
70
+ <span>Welcome, {user?.name}</span>
71
+ <span>Org: {currentOrg?.name}</span>
72
+ <button onClick={() => logout()}>Logout</button>
73
+ </div>
74
+ );
75
+ }
76
+ ```
77
+
78
+ ## Components
79
+
80
+ ### SsoProvider
81
+
82
+ Wraps your app and provides SSO context.
83
+
84
+ ```tsx
85
+ <SsoProvider
86
+ config={{
87
+ apiUrl: 'https://api.yourservice.com',
88
+ consoleUrl: 'https://auth.console.com',
89
+ serviceSlug: 'your-service',
90
+ storage: 'localStorage', // or 'sessionStorage'
91
+ storageKey: 'sso_selected_org',
92
+ }}
93
+ onAuthChange={(isAuthenticated, user) => {
94
+ console.log('Auth changed:', isAuthenticated, user);
95
+ }}
96
+ >
97
+ {children}
98
+ </SsoProvider>
99
+ ```
100
+
101
+ ### SsoCallback
102
+
103
+ Handles the SSO callback and token exchange.
104
+
105
+ ```tsx
106
+ <SsoCallback
107
+ redirectTo="/dashboard"
108
+ onSuccess={(user, organizations) => {}}
109
+ onError={(error) => {}}
110
+ loadingComponent={<CustomLoader />}
111
+ errorComponent={(error) => <CustomError error={error} />}
112
+ />
113
+ ```
114
+
115
+ ### OrganizationSwitcher
116
+
117
+ Dropdown for switching between organizations. Only renders if user has multiple orgs.
118
+
119
+ ```tsx
120
+ // Basic
121
+ <OrganizationSwitcher />
122
+
123
+ // Custom rendering
124
+ <OrganizationSwitcher
125
+ renderTrigger={(currentOrg, isOpen) => (
126
+ <button>{currentOrg?.name} {isOpen ? '▲' : '▼'}</button>
127
+ )}
128
+ renderOption={(org, isSelected) => (
129
+ <div className={isSelected ? 'selected' : ''}>{org.name}</div>
130
+ )}
131
+ onChange={(org) => console.log('Switched to:', org.name)}
132
+ />
133
+ ```
134
+
135
+ ### ProtectedRoute
136
+
137
+ Wraps content that requires authentication.
138
+
139
+ ```tsx
140
+ // Basic
141
+ <ProtectedRoute>
142
+ <Dashboard />
143
+ </ProtectedRoute>
144
+
145
+ // With role requirement
146
+ <ProtectedRoute requiredRole="admin">
147
+ <AdminPanel />
148
+ </ProtectedRoute>
149
+
150
+ // Custom fallbacks
151
+ <ProtectedRoute
152
+ fallback={<Spinner />}
153
+ loginFallback={<CustomLoginPage />}
154
+ onAccessDenied={(reason) => console.log(reason)}
155
+ >
156
+ <ProtectedContent />
157
+ </ProtectedRoute>
158
+ ```
159
+
160
+ ## Hooks
161
+
162
+ ### useAuth
163
+
164
+ ```tsx
165
+ const {
166
+ user, // SsoUser | null
167
+ isLoading, // boolean
168
+ isAuthenticated,// boolean
169
+ login, // (redirectTo?: string) => void
170
+ logout, // () => Promise<void>
171
+ globalLogout, // (redirectTo?: string) => void
172
+ refreshUser, // () => Promise<void>
173
+ } = useAuth();
174
+ ```
175
+
176
+ ### useOrganization
177
+
178
+ ```tsx
179
+ const {
180
+ organizations, // SsoOrganization[]
181
+ currentOrg, // SsoOrganization | null
182
+ hasMultipleOrgs, // boolean
183
+ switchOrg, // (orgSlug: string) => void
184
+ currentRole, // string | null
185
+ hasRole, // (role: string) => boolean
186
+ } = useOrganization();
187
+ ```
188
+
189
+ ### useSso
190
+
191
+ Combined hook with all functionality.
192
+
193
+ ```tsx
194
+ const {
195
+ // Auth
196
+ user, isLoading, isAuthenticated, login, logout, globalLogout, refreshUser,
197
+ // Organization
198
+ organizations, currentOrg, hasMultipleOrgs, switchOrg,
199
+ // Utilities
200
+ getHeaders, config,
201
+ } = useSso();
202
+ ```
203
+
204
+ ## Making API Requests
205
+
206
+ Use `getHeaders()` to include the organization header in your requests:
207
+
208
+ ```tsx
209
+ const { getHeaders } = useSso();
210
+
211
+ // With fetch
212
+ const response = await fetch('/api/data', {
213
+ headers: {
214
+ ...getHeaders(),
215
+ 'Content-Type': 'application/json',
216
+ },
217
+ credentials: 'include',
218
+ });
219
+
220
+ // With axios
221
+ const response = await axios.get('/api/data', {
222
+ headers: getHeaders(),
223
+ withCredentials: true,
224
+ });
225
+ ```
226
+
227
+ ## Types
228
+
229
+ ```tsx
230
+ interface SsoUser {
231
+ id: number;
232
+ consoleUserId: number;
233
+ email: string;
234
+ name: string;
235
+ }
236
+
237
+ interface SsoOrganization {
238
+ id: number;
239
+ slug: string;
240
+ name: string;
241
+ orgRole: string;
242
+ serviceRole: string | null;
243
+ }
244
+
245
+ interface SsoConfig {
246
+ apiUrl: string;
247
+ consoleUrl: string;
248
+ serviceSlug: string;
249
+ storage?: 'localStorage' | 'sessionStorage';
250
+ storageKey?: string;
251
+ }
252
+ ```
253
+
254
+ ## Environment Variables
255
+
256
+ ```env
257
+ NEXT_PUBLIC_API_URL=https://api.yourservice.com
258
+ NEXT_PUBLIC_SSO_CONSOLE_URL=https://auth.console.com
259
+ NEXT_PUBLIC_SSO_SERVICE_SLUG=your-service
260
+ ```
261
+
262
+ ## License
263
+
264
+ MIT
@@ -0,0 +1,366 @@
1
+ import * as react from 'react';
2
+ import * as react_jsx_runtime from 'react/jsx-runtime';
3
+
4
+ /**
5
+ * User information from SSO
6
+ */
7
+ interface SsoUser {
8
+ id: number;
9
+ consoleUserId: number;
10
+ email: string;
11
+ name: string;
12
+ }
13
+ /**
14
+ * Organization with user's access information
15
+ */
16
+ interface SsoOrganization {
17
+ id: number;
18
+ slug: string;
19
+ name: string;
20
+ orgRole: string;
21
+ serviceRole: string | null;
22
+ }
23
+ /**
24
+ * SSO Provider configuration
25
+ */
26
+ interface SsoConfig {
27
+ /** Service backend API URL */
28
+ apiUrl: string;
29
+ /** Console URL for SSO redirects */
30
+ consoleUrl: string;
31
+ /** Service slug registered in Console */
32
+ serviceSlug: string;
33
+ /** Storage type for selected org */
34
+ storage?: 'localStorage' | 'sessionStorage';
35
+ /** Key name for storing selected org */
36
+ storageKey?: string;
37
+ }
38
+ /**
39
+ * SSO Context value
40
+ */
41
+ interface SsoContextValue {
42
+ /** Current authenticated user */
43
+ user: SsoUser | null;
44
+ /** List of organizations user has access to */
45
+ organizations: SsoOrganization[];
46
+ /** Currently selected organization */
47
+ currentOrg: SsoOrganization | null;
48
+ /** Loading state */
49
+ isLoading: boolean;
50
+ /** Authentication state */
51
+ isAuthenticated: boolean;
52
+ /** Configuration */
53
+ config: SsoConfig;
54
+ /** Redirect to Console login */
55
+ login: (redirectTo?: string) => void;
56
+ /** Logout from service */
57
+ logout: () => Promise<void>;
58
+ /** Global logout (logout from Console too) */
59
+ globalLogout: (redirectTo?: string) => void;
60
+ /** Switch to different organization */
61
+ switchOrg: (orgSlug: string) => void;
62
+ /** Refresh user data */
63
+ refreshUser: () => Promise<void>;
64
+ /** Get headers for API requests */
65
+ getHeaders: () => Record<string, string>;
66
+ }
67
+ /**
68
+ * SSO Callback response from backend
69
+ */
70
+ interface SsoCallbackResponse {
71
+ user: {
72
+ id: number;
73
+ console_user_id: number;
74
+ email: string;
75
+ name: string;
76
+ };
77
+ organizations: Array<{
78
+ organization_id: number;
79
+ organization_slug: string;
80
+ organization_name: string;
81
+ org_role: string;
82
+ service_role: string | null;
83
+ }>;
84
+ token?: string;
85
+ }
86
+ /**
87
+ * Props for SsoProvider
88
+ */
89
+ interface SsoProviderProps {
90
+ children: React.ReactNode;
91
+ config: SsoConfig;
92
+ /** Called when auth state changes */
93
+ onAuthChange?: (isAuthenticated: boolean, user: SsoUser | null) => void;
94
+ }
95
+ /**
96
+ * Props for SsoCallback component
97
+ */
98
+ interface SsoCallbackProps {
99
+ /** Called on successful login */
100
+ onSuccess?: (user: SsoUser, organizations: SsoOrganization[]) => void;
101
+ /** Called on error */
102
+ onError?: (error: Error) => void;
103
+ /** Redirect path after login */
104
+ redirectTo?: string;
105
+ /** Loading component */
106
+ loadingComponent?: React.ReactNode;
107
+ /** Error component */
108
+ errorComponent?: (error: Error) => React.ReactNode;
109
+ }
110
+ /**
111
+ * Props for OrganizationSwitcher component
112
+ */
113
+ interface OrganizationSwitcherProps {
114
+ className?: string;
115
+ /** Custom trigger render */
116
+ renderTrigger?: (currentOrg: SsoOrganization | null, isOpen: boolean) => React.ReactNode;
117
+ /** Custom option render */
118
+ renderOption?: (org: SsoOrganization, isSelected: boolean) => React.ReactNode;
119
+ /** Called when org changes */
120
+ onChange?: (org: SsoOrganization) => void;
121
+ }
122
+ /**
123
+ * Props for ProtectedRoute component
124
+ */
125
+ interface ProtectedRouteProps {
126
+ children: React.ReactNode;
127
+ /** Component to show when loading */
128
+ fallback?: React.ReactNode;
129
+ /** Component to show when not authenticated */
130
+ loginFallback?: React.ReactNode;
131
+ /** Required role to access */
132
+ requiredRole?: string;
133
+ /** Required permission to access */
134
+ requiredPermission?: string;
135
+ /** Called when access is denied */
136
+ onAccessDenied?: (reason: 'unauthenticated' | 'insufficient_role' | 'missing_permission') => void;
137
+ }
138
+
139
+ /**
140
+ * SSO Context
141
+ */
142
+ declare const SsoContext: react.Context<SsoContextValue | null>;
143
+
144
+ /**
145
+ * SSO Provider component
146
+ */
147
+ declare function SsoProvider({ children, config, onAuthChange }: SsoProviderProps): react_jsx_runtime.JSX.Element;
148
+
149
+ /**
150
+ * Hook for authentication actions and state
151
+ */
152
+ interface UseAuthReturn {
153
+ /** Current user or null */
154
+ user: SsoUser | null;
155
+ /** Whether auth is being loaded */
156
+ isLoading: boolean;
157
+ /** Whether user is authenticated */
158
+ isAuthenticated: boolean;
159
+ /** Redirect to login */
160
+ login: (redirectTo?: string) => void;
161
+ /** Logout from service only */
162
+ logout: () => Promise<void>;
163
+ /** Logout from service and Console */
164
+ globalLogout: (redirectTo?: string) => void;
165
+ /** Refresh user data */
166
+ refreshUser: () => Promise<void>;
167
+ }
168
+ /**
169
+ * Hook for authentication
170
+ *
171
+ * @example
172
+ * ```tsx
173
+ * function LoginButton() {
174
+ * const { isAuthenticated, login, logout, user } = useAuth();
175
+ *
176
+ * if (isAuthenticated) {
177
+ * return (
178
+ * <div>
179
+ * <span>Hello, {user?.name}</span>
180
+ * <button onClick={() => logout()}>Logout</button>
181
+ * </div>
182
+ * );
183
+ * }
184
+ *
185
+ * return <button onClick={() => login()}>Login</button>;
186
+ * }
187
+ * ```
188
+ */
189
+ declare function useAuth(): UseAuthReturn;
190
+
191
+ /**
192
+ * Hook return type for organization management
193
+ */
194
+ interface UseOrganizationReturn {
195
+ /** List of organizations user has access to */
196
+ organizations: SsoOrganization[];
197
+ /** Currently selected organization */
198
+ currentOrg: SsoOrganization | null;
199
+ /** Whether user has multiple organizations */
200
+ hasMultipleOrgs: boolean;
201
+ /** Switch to a different organization */
202
+ switchOrg: (orgSlug: string) => void;
203
+ /** Get current org's role */
204
+ currentRole: string | null;
205
+ /** Check if user has at least the given role in current org */
206
+ hasRole: (role: string) => boolean;
207
+ }
208
+ /**
209
+ * Hook for organization management
210
+ *
211
+ * @example
212
+ * ```tsx
213
+ * function OrgInfo() {
214
+ * const { currentOrg, organizations, switchOrg, hasRole } = useOrganization();
215
+ *
216
+ * return (
217
+ * <div>
218
+ * <p>Current: {currentOrg?.name}</p>
219
+ * {hasRole('admin') && <AdminPanel />}
220
+ * <select onChange={(e) => switchOrg(e.target.value)}>
221
+ * {organizations.map((org) => (
222
+ * <option key={org.slug} value={org.slug}>{org.name}</option>
223
+ * ))}
224
+ * </select>
225
+ * </div>
226
+ * );
227
+ * }
228
+ * ```
229
+ */
230
+ declare function useOrganization(): UseOrganizationReturn;
231
+
232
+ /**
233
+ * Combined SSO hook return type
234
+ */
235
+ interface UseSsoReturn {
236
+ user: SsoUser | null;
237
+ isLoading: boolean;
238
+ isAuthenticated: boolean;
239
+ login: (redirectTo?: string) => void;
240
+ logout: () => Promise<void>;
241
+ globalLogout: (redirectTo?: string) => void;
242
+ refreshUser: () => Promise<void>;
243
+ organizations: SsoOrganization[];
244
+ currentOrg: SsoOrganization | null;
245
+ hasMultipleOrgs: boolean;
246
+ switchOrg: (orgSlug: string) => void;
247
+ getHeaders: () => Record<string, string>;
248
+ config: SsoConfig;
249
+ }
250
+ /**
251
+ * Combined hook for all SSO functionality
252
+ *
253
+ * @example
254
+ * ```tsx
255
+ * function MyComponent() {
256
+ * const {
257
+ * user,
258
+ * isAuthenticated,
259
+ * currentOrg,
260
+ * getHeaders,
261
+ * login,
262
+ * logout,
263
+ * } = useSso();
264
+ *
265
+ * const fetchData = async () => {
266
+ * const response = await fetch('/api/data', {
267
+ * headers: getHeaders(),
268
+ * });
269
+ * // ...
270
+ * };
271
+ *
272
+ * if (!isAuthenticated) {
273
+ * return <button onClick={() => login()}>Login</button>;
274
+ * }
275
+ *
276
+ * return (
277
+ * <div>
278
+ * <p>Welcome, {user?.name}</p>
279
+ * <p>Organization: {currentOrg?.name}</p>
280
+ * <button onClick={() => logout()}>Logout</button>
281
+ * </div>
282
+ * );
283
+ * }
284
+ * ```
285
+ */
286
+ declare function useSso(): UseSsoReturn;
287
+
288
+ /**
289
+ * SSO Callback component
290
+ *
291
+ * Place this component at your callback route (e.g., /sso/callback)
292
+ * It handles the SSO code exchange and redirects after successful login.
293
+ *
294
+ * @example
295
+ * ```tsx
296
+ * // pages/sso/callback.tsx or app/sso/callback/page.tsx
297
+ * export default function CallbackPage() {
298
+ * return (
299
+ * <SsoCallback
300
+ * redirectTo="/dashboard"
301
+ * onSuccess={(user, orgs) => console.log('Logged in:', user)}
302
+ * onError={(error) => console.error('Login failed:', error)}
303
+ * />
304
+ * );
305
+ * }
306
+ * ```
307
+ */
308
+ declare function SsoCallback({ onSuccess, onError, redirectTo, loadingComponent, errorComponent, }: SsoCallbackProps): react_jsx_runtime.JSX.Element | null;
309
+
310
+ /**
311
+ * Organization Switcher component using Ant Design
312
+ *
313
+ * A dropdown component for switching between organizations.
314
+ * Only renders if user has access to multiple organizations.
315
+ *
316
+ * @example
317
+ * ```tsx
318
+ * // Basic usage
319
+ * <OrganizationSwitcher />
320
+ *
321
+ * // With custom styling
322
+ * <OrganizationSwitcher className="my-switcher" />
323
+ *
324
+ * // With custom render
325
+ * <OrganizationSwitcher
326
+ * renderTrigger={(org, isOpen) => (
327
+ * <Button>{org?.name} {isOpen ? '▲' : '▼'}</Button>
328
+ * )}
329
+ * renderOption={(org, isSelected) => (
330
+ * <div className={isSelected ? 'selected' : ''}>{org.name}</div>
331
+ * )}
332
+ * />
333
+ * ```
334
+ */
335
+ declare function OrganizationSwitcher({ className, renderTrigger, renderOption, onChange, }: OrganizationSwitcherProps): react_jsx_runtime.JSX.Element | null;
336
+
337
+ /**
338
+ * Protected Route component
339
+ *
340
+ * Wraps content that requires authentication and optionally specific roles/permissions.
341
+ *
342
+ * @example
343
+ * ```tsx
344
+ * // Basic protection
345
+ * <ProtectedRoute>
346
+ * <Dashboard />
347
+ * </ProtectedRoute>
348
+ *
349
+ * // With role requirement
350
+ * <ProtectedRoute requiredRole="admin">
351
+ * <AdminPanel />
352
+ * </ProtectedRoute>
353
+ *
354
+ * // With custom fallbacks
355
+ * <ProtectedRoute
356
+ * fallback={<Spinner />}
357
+ * loginFallback={<CustomLoginPage />}
358
+ * onAccessDenied={(reason) => console.log(reason)}
359
+ * >
360
+ * <ProtectedContent />
361
+ * </ProtectedRoute>
362
+ * ```
363
+ */
364
+ declare function ProtectedRoute({ children, fallback, loginFallback, requiredRole, requiredPermission, onAccessDenied, }: ProtectedRouteProps): react_jsx_runtime.JSX.Element;
365
+
366
+ export { OrganizationSwitcher, type OrganizationSwitcherProps, ProtectedRoute, type ProtectedRouteProps, SsoCallback, type SsoCallbackProps, type SsoCallbackResponse, type SsoConfig, SsoContext, type SsoContextValue, type SsoOrganization, SsoProvider, type SsoProviderProps, type SsoUser, type UseAuthReturn, type UseOrganizationReturn, type UseSsoReturn, useAuth, useOrganization, useSso };