@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.
- package/README.md +18 -298
- package/dist/cjs/components/AuthCallback.js +90 -0
- package/dist/cjs/contexts/TideCloakContextProvider.js +534 -73
- package/dist/cjs/hooks/useAuthCallback.js +161 -0
- package/dist/cjs/index.js +81 -2
- package/dist/esm/components/AuthCallback.js +90 -0
- package/dist/esm/contexts/TideCloakContextProvider.js +534 -73
- package/dist/esm/hooks/useAuthCallback.js +161 -0
- package/dist/esm/index.js +81 -2
- package/dist/types/components/AuthCallback.d.ts +90 -0
- package/dist/types/components/AuthCallback.d.ts.map +1 -0
- package/dist/types/contexts/TideCloakContextProvider.d.ts +92 -6
- package/dist/types/contexts/TideCloakContextProvider.d.ts.map +1 -1
- package/dist/types/hooks/useAuthCallback.d.ts +80 -0
- package/dist/types/hooks/useAuthCallback.d.ts.map +1 -0
- package/dist/types/index.d.ts +63 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -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
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTideCloakContext } from './contexts/TideCloakContextProvider';
|
|
2
3
|
export { TideCloakContextProvider } from './contexts/TideCloakContextProvider';
|
|
3
|
-
|
|
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
|
+
}
|