@tidecloak/react 0.13.11 → 0.13.14

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.
@@ -0,0 +1,161 @@
1
+ import { useState, useEffect, useCallback, useRef } from 'react';
2
+ import { IAMService } from '@tidecloak/js';
3
+ /**
4
+ * Parse OAuth callback data from URL and sessionStorage.
5
+ */
6
+ function parseCallback() {
7
+ const params = new URLSearchParams(window.location.search);
8
+ const code = params.get('code');
9
+ const state = params.get('state') || '';
10
+ const error = params.get('error');
11
+ const errorDescription = params.get('error_description');
12
+ const isCallback = !!(code || error);
13
+ // Parse state parameter (format: verifier__url_returnUrl or just returnUrl)
14
+ let verifier = sessionStorage.getItem('kc_pkce_verifier');
15
+ let returnUrl = sessionStorage.getItem('kc_return_url');
16
+ // Also try to extract return URL from state if it contains __url_
17
+ if (state && state.includes('__url_')) {
18
+ const urlStart = state.indexOf('__url_') + 6;
19
+ returnUrl = state.substring(urlStart) || returnUrl;
20
+ }
21
+ return {
22
+ code,
23
+ state,
24
+ verifier,
25
+ returnUrl,
26
+ error,
27
+ errorDescription,
28
+ isCallback,
29
+ };
30
+ }
31
+ /**
32
+ * Hook for handling OAuth callback pages in hybrid mode.
33
+ *
34
+ * Automatically detects if the current page is an OAuth callback and processes it.
35
+ *
36
+ * @example
37
+ * ```tsx
38
+ * function OAuthCallback() {
39
+ * const { isProcessing, isSuccess, error, returnUrl } = useAuthCallback({
40
+ * onSuccess: (url) => navigate(url || '/'),
41
+ * onError: (err) => console.error(err),
42
+ * });
43
+ *
44
+ * if (isProcessing) return <Spinner />;
45
+ * if (error) return <ErrorMessage error={error} />;
46
+ * return null;
47
+ * }
48
+ * ```
49
+ */
50
+ export function useAuthCallback(options = {}) {
51
+ const { autoProcess = true, onSuccess, onError, onMissingVerifierRedirectTo, } = options;
52
+ const [state, setState] = useState(() => {
53
+ const parsed = parseCallback();
54
+ return {
55
+ isCallback: parsed.isCallback,
56
+ isProcessing: false,
57
+ isSuccess: false,
58
+ error: null,
59
+ returnUrl: parsed.returnUrl,
60
+ code: parsed.code,
61
+ idpError: parsed.error,
62
+ idpErrorDescription: parsed.errorDescription,
63
+ };
64
+ });
65
+ // Track if we've already processed to prevent double-execution in StrictMode
66
+ const processedRef = useRef(false);
67
+ const onSuccessRef = useRef(onSuccess);
68
+ const onErrorRef = useRef(onError);
69
+ // Keep refs updated
70
+ useEffect(() => {
71
+ onSuccessRef.current = onSuccess;
72
+ onErrorRef.current = onError;
73
+ }, [onSuccess, onError]);
74
+ const processCallback = useCallback(async () => {
75
+ var _a, _b, _c, _d, _e, _f;
76
+ // Guard against double processing
77
+ if (processedRef.current)
78
+ return;
79
+ processedRef.current = true;
80
+ setState(s => ({ ...s, isProcessing: true }));
81
+ try {
82
+ const parsed = parseCallback();
83
+ // Check for IdP error first
84
+ if (parsed.error) {
85
+ const error = new Error(parsed.errorDescription || parsed.error);
86
+ setState(s => ({
87
+ ...s,
88
+ isProcessing: false,
89
+ error,
90
+ idpError: parsed.error,
91
+ idpErrorDescription: parsed.errorDescription,
92
+ }));
93
+ (_a = onErrorRef.current) === null || _a === void 0 ? void 0 : _a.call(onErrorRef, error);
94
+ return;
95
+ }
96
+ if (!parsed.code) {
97
+ setState(s => ({ ...s, isProcessing: false }));
98
+ return;
99
+ }
100
+ // Check for missing verifier (page was refreshed)
101
+ if (!parsed.verifier) {
102
+ if (onMissingVerifierRedirectTo) {
103
+ window.location.assign(onMissingVerifierRedirectTo);
104
+ return;
105
+ }
106
+ const error = new Error('Session expired. Please try logging in again.');
107
+ setState(s => ({ ...s, isProcessing: false, error }));
108
+ (_b = onErrorRef.current) === null || _b === void 0 ? void 0 : _b.call(onErrorRef, error);
109
+ return;
110
+ }
111
+ // Get config and let IAMService handle the callback
112
+ const config = IAMService.getConfig();
113
+ if (!config) {
114
+ const error = new Error('IAMService not configured. Call loadConfig() first.');
115
+ setState(s => ({ ...s, isProcessing: false, error }));
116
+ (_c = onErrorRef.current) === null || _c === void 0 ? void 0 : _c.call(onErrorRef, error);
117
+ return;
118
+ }
119
+ // Initialize IAMService which will handle the token exchange
120
+ const authenticated = await IAMService.initIAM(config);
121
+ if (authenticated) {
122
+ // Try to get return URL from sessionStorage since we can't access internal state
123
+ const returnUrl = sessionStorage.getItem('kc_return_url') || parsed.returnUrl;
124
+ setState(s => ({
125
+ ...s,
126
+ isProcessing: false,
127
+ isSuccess: true,
128
+ returnUrl,
129
+ }));
130
+ (_d = onSuccessRef.current) === null || _d === void 0 ? void 0 : _d.call(onSuccessRef, returnUrl);
131
+ }
132
+ else {
133
+ const error = new Error('Authentication failed');
134
+ setState(s => ({ ...s, isProcessing: false, error }));
135
+ (_e = onErrorRef.current) === null || _e === void 0 ? void 0 : _e.call(onErrorRef, error);
136
+ }
137
+ }
138
+ catch (err) {
139
+ const error = err instanceof Error ? err : new Error(String(err));
140
+ setState(s => ({ ...s, isProcessing: false, error }));
141
+ (_f = onErrorRef.current) === null || _f === void 0 ? void 0 : _f.call(onErrorRef, error);
142
+ }
143
+ }, [onMissingVerifierRedirectTo]);
144
+ // Auto-process on mount if enabled and this is a callback
145
+ useEffect(() => {
146
+ if (autoProcess && state.isCallback && !processedRef.current) {
147
+ processCallback();
148
+ }
149
+ }, [autoProcess, state.isCallback, processCallback]);
150
+ return {
151
+ ...state,
152
+ processCallback,
153
+ };
154
+ }
155
+ /**
156
+ * Parse OAuth callback data from URL without using IAMService.
157
+ * Useful for manual token exchange flows.
158
+ */
159
+ export function parseCallbackUrl() {
160
+ return parseCallback();
161
+ }
package/dist/cjs/index.js CHANGED
@@ -1,19 +1,98 @@
1
- import { useTideCloakContext, } from './contexts/TideCloakContextProvider';
1
+ import React from 'react';
2
+ import { useTideCloakContext } from './contexts/TideCloakContextProvider';
2
3
  export { TideCloakContextProvider } from './contexts/TideCloakContextProvider';
3
- export { RequestEnclave } from "@tidecloak/js";
4
+ // Hybrid mode utilities
5
+ export { useAuthCallback, parseCallbackUrl } from './hooks/useAuthCallback';
6
+ export { AuthCallback, SimpleAuthCallback } from './components/AuthCallback';
7
+ export { RequestEnclave, AdminAPI } from "@tidecloak/js";
4
8
  /**
5
9
  * Hook to access authentication state and helpers.
10
+ * Must be used within a TideCloakContextProvider.
6
11
  */
7
12
  export const useTideCloak = useTideCloakContext;
13
+ /**
14
+ * Renders children only when user is authenticated.
15
+ */
8
16
  export function Authenticated({ children }) {
9
17
  const { authenticated, isInitializing } = useTideCloakContext();
10
18
  if (isInitializing)
11
19
  return null;
12
20
  return authenticated ? children : null;
13
21
  }
22
+ /**
23
+ * Renders children only when user is NOT authenticated.
24
+ */
14
25
  export function Unauthenticated({ children }) {
15
26
  const { authenticated, isInitializing } = useTideCloakContext();
16
27
  if (isInitializing)
17
28
  return null;
18
29
  return !authenticated ? children : null;
19
30
  }
31
+ /**
32
+ * Renders children only when user has the specified realm role.
33
+ */
34
+ export function HasRealmRole({ role, children, fallback = null }) {
35
+ const { authenticated, isInitializing, hasRealmRole } = useTideCloakContext();
36
+ if (isInitializing)
37
+ return null;
38
+ if (!authenticated)
39
+ return fallback;
40
+ return hasRealmRole(role) ? children : fallback;
41
+ }
42
+ /**
43
+ * Renders children only when user has the specified client/resource role.
44
+ */
45
+ export function HasClientRole({ role, resource, children, fallback = null }) {
46
+ const { authenticated, isInitializing, hasClientRole } = useTideCloakContext();
47
+ if (isInitializing)
48
+ return null;
49
+ if (!authenticated)
50
+ return fallback;
51
+ return hasClientRole(role, resource) ? children : fallback;
52
+ }
53
+ /**
54
+ * Renders children only when user is offline.
55
+ */
56
+ export function Offline({ children }) {
57
+ const { isOffline } = useTideCloakContext();
58
+ return isOffline ? children : null;
59
+ }
60
+ /**
61
+ * Renders children only when user is online.
62
+ */
63
+ export function Online({ children }) {
64
+ const { isOffline } = useTideCloakContext();
65
+ return !isOffline ? children : null;
66
+ }
67
+ /**
68
+ * Renders children when the user was offline at some point during the session.
69
+ * Useful for showing "sync needed" banners.
70
+ */
71
+ export function WasOffline({ children, onReset }) {
72
+ const { wasOffline, resetWasOffline } = useTideCloakContext();
73
+ const handleReset = React.useCallback(() => {
74
+ resetWasOffline();
75
+ onReset === null || onReset === void 0 ? void 0 : onReset();
76
+ }, [resetWasOffline, onReset]);
77
+ if (!wasOffline)
78
+ return null;
79
+ // If children is a function, pass the reset handler
80
+ if (typeof children === 'function') {
81
+ return children(handleReset);
82
+ }
83
+ return children;
84
+ }
85
+ /**
86
+ * Renders children when re-authentication is needed (e.g., after 401).
87
+ */
88
+ export function NeedsReauth({ children }) {
89
+ const { needsReauth } = useTideCloakContext();
90
+ return needsReauth ? children : null;
91
+ }
92
+ /**
93
+ * Renders loading state during login/logout operations.
94
+ */
95
+ export function AuthLoading({ children }) {
96
+ const { isLoading } = useTideCloakContext();
97
+ return isLoading ? children : null;
98
+ }
@@ -0,0 +1,90 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useAuthCallback } from '../hooks/useAuthCallback';
3
+ /**
4
+ * Component for handling OAuth callback pages in hybrid mode.
5
+ *
6
+ * Drop this component into your callback route and it handles everything:
7
+ * - Detecting if this is a callback page
8
+ * - Processing the authorization code
9
+ * - Exchanging tokens with your backend
10
+ * - Calling onSuccess/onError callbacks
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * // Simple usage with navigation
15
+ * function OAuthCallbackPage() {
16
+ * const navigate = useNavigate();
17
+ *
18
+ * return (
19
+ * <AuthCallback
20
+ * onSuccess={(returnUrl) => navigate(returnUrl || '/')}
21
+ * onError={(error) => console.error(error)}
22
+ * loadingComponent={<Spinner />}
23
+ * errorComponent={({ error }) => <Alert status="error">{error.message}</Alert>}
24
+ * />
25
+ * );
26
+ * }
27
+ *
28
+ * // Or in routes directly
29
+ * <Route path="/auth/callback" element={
30
+ * <AuthCallback
31
+ * onSuccess={(url) => window.location.assign(url || '/')}
32
+ * loadingComponent={<LoadingScreen />}
33
+ * />
34
+ * } />
35
+ * ```
36
+ */
37
+ export function AuthCallback({ loadingComponent = null, errorComponent, successComponent, children, ...options }) {
38
+ const { isCallback, isProcessing, isSuccess, error, returnUrl } = useAuthCallback(options);
39
+ // Not a callback page - render children or nothing
40
+ if (!isCallback) {
41
+ return children !== null && children !== void 0 ? children : null;
42
+ }
43
+ // Processing
44
+ if (isProcessing) {
45
+ return loadingComponent;
46
+ }
47
+ // Error
48
+ if (error) {
49
+ if (errorComponent) {
50
+ if (typeof errorComponent === 'function') {
51
+ const ErrorComponent = errorComponent;
52
+ return _jsx(ErrorComponent, { error: error });
53
+ }
54
+ return errorComponent;
55
+ }
56
+ return null;
57
+ }
58
+ // Success
59
+ if (isSuccess) {
60
+ if (successComponent) {
61
+ if (typeof successComponent === 'function') {
62
+ const SuccessComponent = successComponent;
63
+ return _jsx(SuccessComponent, { returnUrl: returnUrl });
64
+ }
65
+ return successComponent;
66
+ }
67
+ return null;
68
+ }
69
+ return null;
70
+ }
71
+ /**
72
+ * Simple callback page that auto-redirects on success.
73
+ * Use this when you just want the callback handled with minimal UI.
74
+ *
75
+ * @example
76
+ * ```tsx
77
+ * <Route path="/auth/callback" element={
78
+ * <SimpleAuthCallback
79
+ * defaultRedirect="/"
80
+ * loginPage="/login"
81
+ * loadingComponent={<FullPageSpinner />}
82
+ * />
83
+ * } />
84
+ * ```
85
+ */
86
+ export function SimpleAuthCallback({ defaultRedirect = '/', loginPage = '/login', loadingComponent, errorComponent, }) {
87
+ return (_jsx(AuthCallback, { onSuccess: (returnUrl) => {
88
+ window.location.assign(returnUrl || defaultRedirect);
89
+ }, onMissingVerifierRedirectTo: loginPage, loadingComponent: loadingComponent, errorComponent: errorComponent }));
90
+ }