@tonycodes/auth-react 1.0.1 → 1.1.1
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/dist/AuthProvider.d.ts +2 -2
- package/dist/AuthProvider.d.ts.map +1 -1
- package/dist/AuthProvider.js +113 -23
- package/dist/AuthProvider.js.map +1 -1
- package/dist/SignInForm.d.ts +5 -8
- package/dist/SignInForm.d.ts.map +1 -1
- package/dist/SignInForm.js +21 -27
- package/dist/SignInForm.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/style.css +1 -1
- package/dist/types.d.ts +17 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/useAuth.d.ts +2 -2
- package/dist/useAuth.d.ts.map +1 -1
- package/dist/useProviders.d.ts +7 -3
- package/dist/useProviders.d.ts.map +1 -1
- package/dist/useProviders.js +77 -21
- package/dist/useProviders.js.map +1 -1
- package/dist/validateConfig.d.ts +14 -0
- package/dist/validateConfig.d.ts.map +1 -0
- package/dist/validateConfig.js +71 -0
- package/dist/validateConfig.js.map +1 -0
- package/package.json +2 -25
- package/src/AuthCallback.tsx +128 -0
- package/src/AuthProvider.tsx +331 -0
- package/src/OrganizationSwitcher.tsx +75 -0
- package/src/SignInForm.tsx +202 -0
- package/src/components.tsx +40 -0
- package/src/index.ts +9 -0
- package/src/style.css +1 -0
- package/src/types.ts +66 -0
- package/src/useAuth.ts +19 -0
- package/src/useProviders.ts +136 -0
- package/src/validateConfig.ts +94 -0
- package/tailwind.config.js +7 -0
- package/tsconfig.json +18 -0
- package/LICENSE +0 -21
package/dist/useProviders.js
CHANGED
|
@@ -1,47 +1,103 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
2
|
import { useAuthConfig } from './useAuth.js';
|
|
3
|
+
const CACHE_TTL = 60000; // 60 seconds
|
|
4
|
+
const cache = new Map();
|
|
5
|
+
function getCacheKey(authUrl, clientId) {
|
|
6
|
+
return `${authUrl}::${clientId}`;
|
|
7
|
+
}
|
|
8
|
+
function getCachedProviders(key) {
|
|
9
|
+
const entry = cache.get(key);
|
|
10
|
+
if (!entry)
|
|
11
|
+
return null;
|
|
12
|
+
return entry;
|
|
13
|
+
}
|
|
14
|
+
function isStale(entry) {
|
|
15
|
+
return Date.now() - entry.fetchedAt > CACHE_TTL;
|
|
16
|
+
}
|
|
17
|
+
async function fetchProviders(authUrl, clientId) {
|
|
18
|
+
const url = new URL(`${authUrl}/providers`);
|
|
19
|
+
url.searchParams.set('client_id', clientId);
|
|
20
|
+
const res = await fetch(url.toString());
|
|
21
|
+
if (!res.ok) {
|
|
22
|
+
throw new Error('Failed to fetch providers');
|
|
23
|
+
}
|
|
24
|
+
const data = await res.json();
|
|
25
|
+
return data.providers || [];
|
|
26
|
+
}
|
|
27
|
+
// ─── Hook ────────────────────────────────────────────────────────────────
|
|
3
28
|
/**
|
|
4
|
-
* Hook to fetch available SSO providers
|
|
5
|
-
*
|
|
29
|
+
* Hook to fetch available SSO providers from the auth service.
|
|
30
|
+
* Uses module-level cache with 60s TTL and stale-while-revalidate.
|
|
31
|
+
* Clears cache on window.focus for admin changes to take effect.
|
|
6
32
|
*/
|
|
7
33
|
export function useProviders() {
|
|
8
34
|
const config = useAuthConfig();
|
|
9
|
-
const
|
|
10
|
-
|
|
35
|
+
const cacheKey = getCacheKey(config.authUrl, config.clientId);
|
|
36
|
+
// Initialize from cache if available
|
|
37
|
+
const cached = getCachedProviders(cacheKey);
|
|
38
|
+
const [providers, setProviders] = useState(cached?.providers || []);
|
|
39
|
+
const [isLoading, setIsLoading] = useState(!cached);
|
|
11
40
|
const [error, setError] = useState(null);
|
|
41
|
+
const [refreshCounter, setRefreshCounter] = useState(0);
|
|
42
|
+
const refresh = useCallback(() => {
|
|
43
|
+
cache.delete(cacheKey);
|
|
44
|
+
setRefreshCounter((c) => c + 1);
|
|
45
|
+
}, [cacheKey]);
|
|
12
46
|
useEffect(() => {
|
|
13
47
|
let mounted = true;
|
|
14
|
-
async function
|
|
15
|
-
|
|
48
|
+
async function doFetch(showLoading) {
|
|
49
|
+
if (showLoading)
|
|
16
50
|
setIsLoading(true);
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
const res = await fetch(url.toString());
|
|
21
|
-
if (!res.ok) {
|
|
22
|
-
throw new Error(`Failed to fetch providers: ${res.status}`);
|
|
23
|
-
}
|
|
24
|
-
const data = await res.json();
|
|
51
|
+
try {
|
|
52
|
+
const result = await fetchProviders(config.authUrl, config.clientId);
|
|
53
|
+
cache.set(cacheKey, { providers: result, fetchedAt: Date.now() });
|
|
25
54
|
if (mounted) {
|
|
26
|
-
setProviders(
|
|
55
|
+
setProviders(result);
|
|
56
|
+
setError(null);
|
|
27
57
|
}
|
|
28
58
|
}
|
|
29
59
|
catch (err) {
|
|
30
60
|
if (mounted) {
|
|
31
61
|
setError(err instanceof Error ? err.message : 'Failed to fetch providers');
|
|
62
|
+
// Keep stale data if we have it
|
|
63
|
+
if (!cached)
|
|
64
|
+
setProviders([]);
|
|
32
65
|
}
|
|
33
66
|
}
|
|
34
67
|
finally {
|
|
35
|
-
if (mounted)
|
|
68
|
+
if (mounted)
|
|
36
69
|
setIsLoading(false);
|
|
37
|
-
}
|
|
38
70
|
}
|
|
39
71
|
}
|
|
40
|
-
|
|
72
|
+
const entry = getCachedProviders(cacheKey);
|
|
73
|
+
if (!entry) {
|
|
74
|
+
// No cache — fetch with loading state
|
|
75
|
+
doFetch(true);
|
|
76
|
+
}
|
|
77
|
+
else if (isStale(entry)) {
|
|
78
|
+
// Stale cache — show cached data, refresh in background
|
|
79
|
+
setProviders(entry.providers);
|
|
80
|
+
setIsLoading(false);
|
|
81
|
+
doFetch(false);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
// Fresh cache — use it directly
|
|
85
|
+
setProviders(entry.providers);
|
|
86
|
+
setIsLoading(false);
|
|
87
|
+
}
|
|
88
|
+
// Refetch on window focus (admin changes take effect when tab refocused)
|
|
89
|
+
function handleFocus() {
|
|
90
|
+
const current = getCachedProviders(cacheKey);
|
|
91
|
+
if (!current || isStale(current)) {
|
|
92
|
+
doFetch(false);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
window.addEventListener('focus', handleFocus);
|
|
41
96
|
return () => {
|
|
42
97
|
mounted = false;
|
|
98
|
+
window.removeEventListener('focus', handleFocus);
|
|
43
99
|
};
|
|
44
|
-
}, [config.authUrl, config.clientId]);
|
|
45
|
-
return { providers, isLoading, error };
|
|
100
|
+
}, [config.authUrl, config.clientId, cacheKey, refreshCounter]);
|
|
101
|
+
return { providers, isLoading, error, refresh };
|
|
46
102
|
}
|
|
47
103
|
//# sourceMappingURL=useProviders.js.map
|
package/dist/useProviders.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useProviders.js","sourceRoot":"","sources":["../src/useProviders.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"useProviders.js","sourceRoot":"","sources":["../src/useProviders.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AuthConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Configuration validation errors for better developer experience.
|
|
4
|
+
*/
|
|
5
|
+
export declare class AuthConfigError extends Error {
|
|
6
|
+
constructor(message: string);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Validates AuthConfig and throws descriptive errors if invalid.
|
|
10
|
+
* Called at initialization time to fail fast with helpful messages.
|
|
11
|
+
* Only clientId is required — authUrl and appUrl can be auto-discovered.
|
|
12
|
+
*/
|
|
13
|
+
export declare function validateConfig(config: AuthConfig): void;
|
|
14
|
+
//# sourceMappingURL=validateConfig.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validateConfig.d.ts","sourceRoot":"","sources":["../src/validateConfig.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C;;GAEG;AACH,qBAAa,eAAgB,SAAQ,KAAK;gBAC5B,OAAO,EAAE,MAAM;CAI5B;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CA4EvD"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration validation errors for better developer experience.
|
|
3
|
+
*/
|
|
4
|
+
export class AuthConfigError extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'AuthConfigError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Validates AuthConfig and throws descriptive errors if invalid.
|
|
12
|
+
* Called at initialization time to fail fast with helpful messages.
|
|
13
|
+
* Only clientId is required — authUrl and appUrl can be auto-discovered.
|
|
14
|
+
*/
|
|
15
|
+
export function validateConfig(config) {
|
|
16
|
+
if (!config) {
|
|
17
|
+
throw new AuthConfigError('AuthProvider requires a config prop. ' +
|
|
18
|
+
'At minimum, provide { clientId: "your-app-id" }.');
|
|
19
|
+
}
|
|
20
|
+
// Required field
|
|
21
|
+
if (!config.clientId) {
|
|
22
|
+
throw new AuthConfigError('Missing required config: clientId. ' +
|
|
23
|
+
'This is the client ID registered with the auth service.');
|
|
24
|
+
}
|
|
25
|
+
// URL format validation (only if provided)
|
|
26
|
+
if (config.authUrl) {
|
|
27
|
+
try {
|
|
28
|
+
new URL(config.authUrl);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
throw new AuthConfigError(`Invalid authUrl: "${config.authUrl}" is not a valid URL. ` +
|
|
32
|
+
'It should include the protocol (e.g., "https://auth.tony.codes").');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (config.appUrl) {
|
|
36
|
+
try {
|
|
37
|
+
new URL(config.appUrl);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
throw new AuthConfigError(`Invalid appUrl: "${config.appUrl}" is not a valid URL. ` +
|
|
41
|
+
'It should include the protocol (e.g., "https://myapp.tony.codes").');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (config.apiUrl) {
|
|
45
|
+
try {
|
|
46
|
+
new URL(config.apiUrl);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
throw new AuthConfigError(`Invalid apiUrl: "${config.apiUrl}" is not a valid URL. ` +
|
|
50
|
+
'It should include the protocol (e.g., "https://api.myapp.tony.codes").');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Common misconfiguration warnings (logged but don't throw)
|
|
54
|
+
if (typeof window !== 'undefined') {
|
|
55
|
+
const isProduction = !window.location.hostname.includes('test') &&
|
|
56
|
+
!window.location.hostname.includes('localhost');
|
|
57
|
+
if (isProduction && config.authUrl?.includes('localhost')) {
|
|
58
|
+
console.warn('[auth-react] Warning: authUrl contains "localhost" but app appears to be in production. ' +
|
|
59
|
+
'Make sure to update authUrl for production.');
|
|
60
|
+
}
|
|
61
|
+
if (config.authUrl?.endsWith('/')) {
|
|
62
|
+
console.warn('[auth-react] Warning: authUrl has a trailing slash. ' +
|
|
63
|
+
'This may cause double slashes in URLs.');
|
|
64
|
+
}
|
|
65
|
+
if (config.appUrl?.endsWith('/')) {
|
|
66
|
+
console.warn('[auth-react] Warning: appUrl has a trailing slash. ' +
|
|
67
|
+
'This may cause double slashes in URLs.');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=validateConfig.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validateConfig.js","sourceRoot":"","sources":["../src/validateConfig.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,OAAO,eAAgB,SAAQ,KAAK;IACxC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAChC,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,MAAkB;IAC/C,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,eAAe,CACvB,uCAAuC;YACrC,kDAAkD,CACrD,CAAC;IACJ,CAAC;IAED,iBAAiB;IACjB,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;QACrB,MAAM,IAAI,eAAe,CACvB,qCAAqC;YACnC,yDAAyD,CAC5D,CAAC;IACJ,CAAC;IAED,2CAA2C;IAC3C,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QACnB,IAAI,CAAC;YACH,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC1B,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,eAAe,CACvB,qBAAqB,MAAM,CAAC,OAAO,wBAAwB;gBACzD,mEAAmE,CACtE,CAAC;QACJ,CAAC;IACH,CAAC;IAED,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QAClB,IAAI,CAAC;YACH,IAAI,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACzB,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,eAAe,CACvB,oBAAoB,MAAM,CAAC,MAAM,wBAAwB;gBACvD,oEAAoE,CACvE,CAAC;QACJ,CAAC;IACH,CAAC;IAED,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QAClB,IAAI,CAAC;YACH,IAAI,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACzB,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,eAAe,CACvB,oBAAoB,MAAM,CAAC,MAAM,wBAAwB;gBACvD,wEAAwE,CAC3E,CAAC;QACJ,CAAC;IACH,CAAC;IAED,4DAA4D;IAC5D,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;QAClC,MAAM,YAAY,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC;YAC1C,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;QAErE,IAAI,YAAY,IAAI,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;YAC1D,OAAO,CAAC,IAAI,CACV,0FAA0F;gBACxF,6CAA6C,CAChD,CAAC;QACJ,CAAC;QAED,IAAI,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YAClC,OAAO,CAAC,IAAI,CACV,sDAAsD;gBACpD,wCAAwC,CAC3C,CAAC;QACJ,CAAC;QAED,IAAI,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YACjC,OAAO,CAAC,IAAI,CACV,qDAAqD;gBACnD,wCAAwC,CAC3C,CAAC;QACJ,CAAC;IACH,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tonycodes/auth-react",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "React SDK for Tony Auth service",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -12,32 +12,9 @@
|
|
|
12
12
|
},
|
|
13
13
|
"./style.css": "./dist/style.css"
|
|
14
14
|
},
|
|
15
|
-
"files": [
|
|
16
|
-
"dist"
|
|
17
|
-
],
|
|
18
15
|
"scripts": {
|
|
19
16
|
"build": "tsc && npx tailwindcss -i src/style.css -o dist/style.css --minify",
|
|
20
|
-
"dev": "tsc --watch"
|
|
21
|
-
"prepublishOnly": "npm run build"
|
|
22
|
-
},
|
|
23
|
-
"keywords": [
|
|
24
|
-
"react",
|
|
25
|
-
"auth",
|
|
26
|
-
"authentication",
|
|
27
|
-
"sdk"
|
|
28
|
-
],
|
|
29
|
-
"author": "Tony <tony@tony.codes>",
|
|
30
|
-
"license": "MIT",
|
|
31
|
-
"repository": {
|
|
32
|
-
"type": "git",
|
|
33
|
-
"url": "git+https://github.com/tonycodes/auth-react.git"
|
|
34
|
-
},
|
|
35
|
-
"homepage": "https://github.com/tonycodes/auth-react#readme",
|
|
36
|
-
"bugs": {
|
|
37
|
-
"url": "https://github.com/tonycodes/auth-react/issues"
|
|
38
|
-
},
|
|
39
|
-
"publishConfig": {
|
|
40
|
-
"access": "public"
|
|
17
|
+
"dev": "tsc --watch"
|
|
41
18
|
},
|
|
42
19
|
"peerDependencies": {
|
|
43
20
|
"react": ">=18.0.0",
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { useContext, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { AuthConfigContext } from './AuthProvider.js';
|
|
3
|
+
|
|
4
|
+
interface AuthCallbackProps {
|
|
5
|
+
/** API URL to exchange code (overrides config.apiUrl, defaults to current origin) */
|
|
6
|
+
apiUrl?: string;
|
|
7
|
+
/** Where to redirect after successful auth */
|
|
8
|
+
onSuccess?: (returnTo: string) => void;
|
|
9
|
+
/** Called on auth failure */
|
|
10
|
+
onError?: (error: string) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Component that handles the OAuth callback.
|
|
15
|
+
* Mount at /auth/callback route in your React router.
|
|
16
|
+
*
|
|
17
|
+
* When mounted, this component:
|
|
18
|
+
* 1. Extracts the authorization code from the URL
|
|
19
|
+
* 2. Calls /api/auth/callback on the backend to exchange the code for tokens
|
|
20
|
+
* 3. Redirects to the original page (from state)
|
|
21
|
+
*
|
|
22
|
+
* Your Express backend should mount callbackHandler() at /api/auth/callback.
|
|
23
|
+
*
|
|
24
|
+
* The API URL is resolved in this order:
|
|
25
|
+
* 1. `apiUrl` prop (explicit override)
|
|
26
|
+
* 2. `config.apiUrl` from AuthProvider
|
|
27
|
+
* 3. `config.appUrl` from AuthProvider
|
|
28
|
+
* 4. Current window origin (fallback)
|
|
29
|
+
*/
|
|
30
|
+
export function AuthCallback({ apiUrl: apiUrlProp, onSuccess, onError }: AuthCallbackProps) {
|
|
31
|
+
const config = useContext(AuthConfigContext);
|
|
32
|
+
const [error, setError] = useState<string | null>(null);
|
|
33
|
+
const exchangedRef = useRef(false);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
// Guard against React 18 strict mode double-firing
|
|
37
|
+
if (exchangedRef.current) return;
|
|
38
|
+
exchangedRef.current = true;
|
|
39
|
+
|
|
40
|
+
const params = new URLSearchParams(window.location.search);
|
|
41
|
+
const code = params.get('code');
|
|
42
|
+
const state = params.get('state');
|
|
43
|
+
const errorParam = params.get('error');
|
|
44
|
+
|
|
45
|
+
if (errorParam) {
|
|
46
|
+
setError(errorParam);
|
|
47
|
+
if (onError) {
|
|
48
|
+
onError(errorParam);
|
|
49
|
+
} else {
|
|
50
|
+
let redirectReturnTo = '/';
|
|
51
|
+
if (state) {
|
|
52
|
+
try {
|
|
53
|
+
const decoded = JSON.parse(atob(state));
|
|
54
|
+
redirectReturnTo = decoded.returnTo || '/';
|
|
55
|
+
} catch {
|
|
56
|
+
// Invalid state — default to /
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const loginUrl = new URL('/login', window.location.origin);
|
|
60
|
+
loginUrl.searchParams.set('error', errorParam);
|
|
61
|
+
if (redirectReturnTo !== '/') loginUrl.searchParams.set('returnTo', redirectReturnTo);
|
|
62
|
+
window.location.href = loginUrl.toString();
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!code) {
|
|
68
|
+
setError('Missing authorization code');
|
|
69
|
+
onError?.('Missing authorization code');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Resolve API URL: prop > config.apiUrl > config.appUrl > current origin
|
|
74
|
+
const baseUrl = apiUrlProp || config?.apiUrl || config?.appUrl || window.location.origin;
|
|
75
|
+
|
|
76
|
+
async function exchange() {
|
|
77
|
+
try {
|
|
78
|
+
const res = await fetch(`${baseUrl}/api/auth/callback?code=${encodeURIComponent(code!)}`, {
|
|
79
|
+
credentials: 'include',
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (!res.ok) {
|
|
83
|
+
const data = await res.json();
|
|
84
|
+
throw new Error(data.error || 'Authentication failed');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Decode state to get returnTo path
|
|
88
|
+
let returnTo = '/';
|
|
89
|
+
if (state) {
|
|
90
|
+
try {
|
|
91
|
+
const decoded = JSON.parse(atob(state));
|
|
92
|
+
returnTo = decoded.returnTo || '/';
|
|
93
|
+
} catch {
|
|
94
|
+
// Invalid state — default to /
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (onSuccess) {
|
|
99
|
+
onSuccess(returnTo);
|
|
100
|
+
} else {
|
|
101
|
+
window.location.href = returnTo;
|
|
102
|
+
}
|
|
103
|
+
} catch (err) {
|
|
104
|
+
const message = err instanceof Error ? err.message : 'Authentication failed';
|
|
105
|
+
setError(message);
|
|
106
|
+
onError?.(message);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
exchange();
|
|
111
|
+
}, [apiUrlProp, config, onSuccess, onError]);
|
|
112
|
+
|
|
113
|
+
if (error) {
|
|
114
|
+
return (
|
|
115
|
+
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
|
116
|
+
<h2>Authentication Failed</h2>
|
|
117
|
+
<p>{error}</p>
|
|
118
|
+
<a href="/">Go Home</a>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
|
125
|
+
<p>Signing in...</p>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|