@tonycodes/auth-react 1.1.0 → 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.js +1 -1
- package/dist/AuthProvider.js.map +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- 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/AuthProvider.js
CHANGED
|
@@ -264,7 +264,7 @@ export function AuthProvider({ config, children }) {
|
|
|
264
264
|
return refreshToken();
|
|
265
265
|
}, [accessToken, refreshToken]);
|
|
266
266
|
const value = {
|
|
267
|
-
isAuthenticated: !!accessToken && !!organization,
|
|
267
|
+
isAuthenticated: !!accessToken && (config.requireOrg === false || !!organization),
|
|
268
268
|
isLoading: isLoading || !resolved,
|
|
269
269
|
user,
|
|
270
270
|
organization,
|
package/dist/AuthProvider.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuthProvider.js","sourceRoot":"","sources":["../src/AuthProvider.tsx"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"AuthProvider.js","sourceRoot":"","sources":["../src/AuthProvider.tsx"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -12,6 +12,11 @@ export interface AuthConfig {
|
|
|
12
12
|
* Auto-discovered from server or defaults to appUrl.
|
|
13
13
|
*/
|
|
14
14
|
apiUrl?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Whether to require an organization for the user to be considered authenticated.
|
|
17
|
+
* Defaults to true. Set to false for single-user apps without org context.
|
|
18
|
+
*/
|
|
19
|
+
requireOrg?: boolean;
|
|
15
20
|
}
|
|
16
21
|
/** Resolved config with all URLs populated (after discovery) */
|
|
17
22
|
export interface ResolvedAuthConfig {
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACzB,6CAA6C;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,4DAA4D;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6FAA6F;IAC7F,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACzB,6CAA6C;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,4DAA4D;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6FAA6F;IAC7F,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,gEAAgE;AAChE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,SAAS;IACxB,eAAe,EAAE,OAAO,CAAC;IACzB,SAAS,EAAE,OAAO,CAAC;IACnB,IAAI,EAAE,QAAQ,GAAG,IAAI,CAAC;IACtB,YAAY,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACtC,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC1D,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,OAAO,CAAC;IACtB,eAAe,EAAE,OAAO,CAAC;IACzB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,cAAc,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7C,KAAK,EAAE,CAAC,QAAQ,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,kBAAkB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,aAAa,EAAE,gBAAgB,EAAE,CAAC;IAClC,YAAY,EAAE,OAAO,CAAC;IACtB,WAAW,EAAE,OAAO,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,aAAa,EAAE,OAAO,CAAC;CACxB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tonycodes/auth-react",
|
|
3
|
-
"version": "1.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
|
+
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { createContext, useCallback, useEffect, useRef, useState, type ReactNode } from 'react';
|
|
2
|
+
import type { AuthConfig, ResolvedAuthConfig, AuthUser, AuthOrganization, AuthState } from './types.js';
|
|
3
|
+
import { validateConfig } from './validateConfig.js';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_AUTH_URL = 'https://auth.tony.codes';
|
|
6
|
+
|
|
7
|
+
interface JWTPayload {
|
|
8
|
+
sub: string;
|
|
9
|
+
email: string;
|
|
10
|
+
name: string | null;
|
|
11
|
+
avatarUrl: string | null;
|
|
12
|
+
org: { id: string; name: string; slug: string; role: string } | null;
|
|
13
|
+
isSuperAdmin: boolean;
|
|
14
|
+
exp: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function decodeJWT(token: string): JWTPayload {
|
|
18
|
+
try {
|
|
19
|
+
const base64 = token.split('.')[1];
|
|
20
|
+
if (!base64) throw new Error('Invalid token structure');
|
|
21
|
+
const json = atob(base64.replace(/-/g, '+').replace(/_/g, '/'));
|
|
22
|
+
return JSON.parse(json);
|
|
23
|
+
} catch {
|
|
24
|
+
throw new Error('Failed to decode access token — the token may be malformed');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const AuthContext = createContext<AuthState | null>(null);
|
|
29
|
+
export const AuthConfigContext = createContext<ResolvedAuthConfig | null>(null);
|
|
30
|
+
|
|
31
|
+
interface AuthProviderProps {
|
|
32
|
+
config: AuthConfig;
|
|
33
|
+
children: ReactNode;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve config by discovering missing URLs from the auth service.
|
|
38
|
+
* 1. If appUrl provided explicitly → use it, skip discovery
|
|
39
|
+
* 2. Otherwise → fetch from /api/client-apps/:clientId/config
|
|
40
|
+
* 3. Fallback → use window.location.origin
|
|
41
|
+
*/
|
|
42
|
+
async function resolveConfig(config: AuthConfig): Promise<ResolvedAuthConfig> {
|
|
43
|
+
const authUrl = config.authUrl || DEFAULT_AUTH_URL;
|
|
44
|
+
let appUrl = config.appUrl;
|
|
45
|
+
let apiUrl = config.apiUrl;
|
|
46
|
+
|
|
47
|
+
// If appUrl is already provided, skip discovery
|
|
48
|
+
if (!appUrl) {
|
|
49
|
+
try {
|
|
50
|
+
const res = await fetch(`${authUrl}/api/client-apps/${config.clientId}/config`);
|
|
51
|
+
if (res.ok) {
|
|
52
|
+
const data = await res.json();
|
|
53
|
+
appUrl = data.appUrl || undefined;
|
|
54
|
+
if (!apiUrl) apiUrl = data.apiUrl || undefined;
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// Discovery failed — use fallback
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Fallback to window.location.origin
|
|
62
|
+
if (!appUrl && typeof window !== 'undefined') {
|
|
63
|
+
appUrl = window.location.origin;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!appUrl) {
|
|
67
|
+
appUrl = authUrl; // Last resort
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
clientId: config.clientId,
|
|
72
|
+
authUrl,
|
|
73
|
+
appUrl,
|
|
74
|
+
apiUrl: apiUrl || appUrl,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function AuthProvider({ config, children }: AuthProviderProps) {
|
|
79
|
+
// Validate config on initialization (throws if invalid)
|
|
80
|
+
validateConfig(config);
|
|
81
|
+
|
|
82
|
+
const [resolved, setResolved] = useState<ResolvedAuthConfig | null>(() => {
|
|
83
|
+
// If all URLs are provided, resolve synchronously
|
|
84
|
+
const authUrl = config.authUrl || DEFAULT_AUTH_URL;
|
|
85
|
+
if (config.appUrl) {
|
|
86
|
+
return {
|
|
87
|
+
clientId: config.clientId,
|
|
88
|
+
authUrl,
|
|
89
|
+
appUrl: config.appUrl,
|
|
90
|
+
apiUrl: config.apiUrl || config.appUrl,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const [accessToken, setAccessToken] = useState<string | null>(null);
|
|
97
|
+
const [user, setUser] = useState<AuthUser | null>(null);
|
|
98
|
+
const [organization, setOrganization] = useState<AuthOrganization | null>(null);
|
|
99
|
+
const [organizations, setOrganizations] = useState<AuthOrganization[]>([]);
|
|
100
|
+
const [orgRole, setOrgRole] = useState<string>('member');
|
|
101
|
+
const [isSuperAdmin, setIsSuperAdmin] = useState(false);
|
|
102
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
103
|
+
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
|
104
|
+
const refreshTimerRef = useRef<ReturnType<typeof setTimeout>>();
|
|
105
|
+
const refreshLockRef = useRef(false);
|
|
106
|
+
|
|
107
|
+
// Discover config if needed
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (resolved) return; // Already resolved synchronously
|
|
110
|
+
let mounted = true;
|
|
111
|
+
resolveConfig(config).then((r) => {
|
|
112
|
+
if (mounted) setResolved(r);
|
|
113
|
+
});
|
|
114
|
+
return () => { mounted = false; };
|
|
115
|
+
}, [config, resolved]);
|
|
116
|
+
|
|
117
|
+
const updateFromToken = useCallback((token: string) => {
|
|
118
|
+
const payload = decodeJWT(token);
|
|
119
|
+
|
|
120
|
+
setUser({
|
|
121
|
+
id: payload.sub,
|
|
122
|
+
email: payload.email,
|
|
123
|
+
name: payload.name || 'User',
|
|
124
|
+
role: payload.org?.role === 'owner' || payload.org?.role === 'admin' ? 'admin' : 'member',
|
|
125
|
+
imageUrl: payload.avatarUrl,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (payload.org) {
|
|
129
|
+
setOrganization({
|
|
130
|
+
id: payload.org.id,
|
|
131
|
+
name: payload.org.name,
|
|
132
|
+
slug: payload.org.slug,
|
|
133
|
+
imageUrl: null,
|
|
134
|
+
});
|
|
135
|
+
setOrgRole(payload.org.role);
|
|
136
|
+
} else {
|
|
137
|
+
setOrganization(null);
|
|
138
|
+
setOrgRole('member');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
setIsSuperAdmin(payload.isSuperAdmin);
|
|
142
|
+
setAccessToken(token);
|
|
143
|
+
|
|
144
|
+
return payload;
|
|
145
|
+
}, []);
|
|
146
|
+
|
|
147
|
+
const refreshToken = useCallback(async (): Promise<string | null> => {
|
|
148
|
+
if (!resolved) return null;
|
|
149
|
+
if (refreshLockRef.current) return null;
|
|
150
|
+
refreshLockRef.current = true;
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const res = await fetch(`${resolved.apiUrl}/auth/refresh`, {
|
|
154
|
+
method: 'POST',
|
|
155
|
+
credentials: 'include',
|
|
156
|
+
headers: { 'Content-Type': 'application/json' },
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (!res.ok) {
|
|
160
|
+
setAccessToken(null);
|
|
161
|
+
setUser(null);
|
|
162
|
+
setOrganization(null);
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const data = await res.json();
|
|
167
|
+
const payload = updateFromToken(data.access_token);
|
|
168
|
+
|
|
169
|
+
// Schedule next refresh 1 minute before expiry
|
|
170
|
+
const expiresIn = payload.exp * 1000 - Date.now();
|
|
171
|
+
const refreshIn = Math.max(expiresIn - 60_000, 10_000);
|
|
172
|
+
if (refreshTimerRef.current) clearTimeout(refreshTimerRef.current);
|
|
173
|
+
refreshTimerRef.current = setTimeout(() => {
|
|
174
|
+
refreshToken();
|
|
175
|
+
}, refreshIn);
|
|
176
|
+
|
|
177
|
+
return data.access_token;
|
|
178
|
+
} catch {
|
|
179
|
+
return null;
|
|
180
|
+
} finally {
|
|
181
|
+
refreshLockRef.current = false;
|
|
182
|
+
}
|
|
183
|
+
}, [resolved, updateFromToken]);
|
|
184
|
+
|
|
185
|
+
// Fetch user organizations list
|
|
186
|
+
const fetchOrganizations = useCallback(async (token: string) => {
|
|
187
|
+
if (!resolved) return;
|
|
188
|
+
try {
|
|
189
|
+
const res = await fetch(`${resolved.authUrl}/api/organizations`, {
|
|
190
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
191
|
+
});
|
|
192
|
+
if (res.ok) {
|
|
193
|
+
const data = await res.json();
|
|
194
|
+
setOrganizations(data.organizations || []);
|
|
195
|
+
}
|
|
196
|
+
} catch {
|
|
197
|
+
// Silent failure — orgs list is supplementary
|
|
198
|
+
}
|
|
199
|
+
}, [resolved]);
|
|
200
|
+
|
|
201
|
+
// Initial auth check — try to refresh on mount (after config resolves)
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
if (!resolved) return;
|
|
204
|
+
let mounted = true;
|
|
205
|
+
|
|
206
|
+
async function init() {
|
|
207
|
+
const token = await refreshToken();
|
|
208
|
+
if (mounted) {
|
|
209
|
+
if (token) {
|
|
210
|
+
await fetchOrganizations(token);
|
|
211
|
+
}
|
|
212
|
+
setIsLoading(false);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
init();
|
|
217
|
+
|
|
218
|
+
return () => {
|
|
219
|
+
mounted = false;
|
|
220
|
+
if (refreshTimerRef.current) clearTimeout(refreshTimerRef.current);
|
|
221
|
+
};
|
|
222
|
+
}, [resolved, refreshToken, fetchOrganizations]);
|
|
223
|
+
|
|
224
|
+
const login = useCallback((provider?: string) => {
|
|
225
|
+
if (!resolved) return;
|
|
226
|
+
const redirectUri = `${resolved.appUrl}/auth/callback`;
|
|
227
|
+
const state = btoa(JSON.stringify({ returnTo: window.location.pathname }));
|
|
228
|
+
const params = new URLSearchParams({
|
|
229
|
+
client_id: resolved.clientId,
|
|
230
|
+
redirect_uri: redirectUri,
|
|
231
|
+
state,
|
|
232
|
+
});
|
|
233
|
+
if (provider) params.set('provider', provider);
|
|
234
|
+
window.location.href = `${resolved.authUrl}/authorize?${params}`;
|
|
235
|
+
}, [resolved]);
|
|
236
|
+
|
|
237
|
+
const logout = useCallback(async () => {
|
|
238
|
+
if (!resolved) return;
|
|
239
|
+
setIsLoggingOut(true);
|
|
240
|
+
try {
|
|
241
|
+
await fetch(`${resolved.apiUrl}/auth/logout`, {
|
|
242
|
+
method: 'POST',
|
|
243
|
+
credentials: 'include',
|
|
244
|
+
});
|
|
245
|
+
} catch {
|
|
246
|
+
// Best-effort
|
|
247
|
+
}
|
|
248
|
+
if (refreshTimerRef.current) clearTimeout(refreshTimerRef.current);
|
|
249
|
+
setAccessToken(null);
|
|
250
|
+
setUser(null);
|
|
251
|
+
setOrganization(null);
|
|
252
|
+
setOrganizations([]);
|
|
253
|
+
setIsLoggingOut(false);
|
|
254
|
+
}, [resolved]);
|
|
255
|
+
|
|
256
|
+
const switchOrganization = useCallback(
|
|
257
|
+
async (orgId: string) => {
|
|
258
|
+
if (!resolved) return;
|
|
259
|
+
try {
|
|
260
|
+
const res = await fetch(`${resolved.apiUrl}/auth/switch-org`, {
|
|
261
|
+
method: 'POST',
|
|
262
|
+
credentials: 'include',
|
|
263
|
+
headers: { 'Content-Type': 'application/json' },
|
|
264
|
+
body: JSON.stringify({ org_id: orgId }),
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
if (res.ok) {
|
|
268
|
+
const data = await res.json();
|
|
269
|
+
updateFromToken(data.access_token);
|
|
270
|
+
}
|
|
271
|
+
} catch {
|
|
272
|
+
// Silent failure
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
[resolved, updateFromToken],
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
const isAdmin = orgRole === 'admin' || orgRole === 'owner';
|
|
279
|
+
const isOwner = orgRole === 'owner';
|
|
280
|
+
|
|
281
|
+
const getAccessToken = useCallback(async (): Promise<string | null> => {
|
|
282
|
+
if (accessToken) {
|
|
283
|
+
try {
|
|
284
|
+
const payload = decodeJWT(accessToken);
|
|
285
|
+
if (payload.exp * 1000 - Date.now() > 60_000) {
|
|
286
|
+
return accessToken;
|
|
287
|
+
}
|
|
288
|
+
} catch {
|
|
289
|
+
// Fall through to refresh
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return refreshToken();
|
|
293
|
+
}, [accessToken, refreshToken]);
|
|
294
|
+
|
|
295
|
+
const value: AuthState = {
|
|
296
|
+
isAuthenticated: !!accessToken && (config.requireOrg === false || !!organization),
|
|
297
|
+
isLoading: isLoading || !resolved,
|
|
298
|
+
user,
|
|
299
|
+
organization,
|
|
300
|
+
tenant: organization ? { id: organization.id, name: organization.name, slug: organization.slug } : null,
|
|
301
|
+
isAdmin,
|
|
302
|
+
isOwner,
|
|
303
|
+
orgRole,
|
|
304
|
+
isSuperAdmin,
|
|
305
|
+
isPlatformAdmin: isSuperAdmin,
|
|
306
|
+
accessToken,
|
|
307
|
+
getAccessToken,
|
|
308
|
+
login,
|
|
309
|
+
logout,
|
|
310
|
+
switchOrganization,
|
|
311
|
+
organizations,
|
|
312
|
+
isLoggingOut,
|
|
313
|
+
isLoggingIn: false,
|
|
314
|
+
loginError: null,
|
|
315
|
+
impersonating: false,
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// Use resolved config for context, or a placeholder during discovery
|
|
319
|
+
const configValue: ResolvedAuthConfig = resolved || {
|
|
320
|
+
clientId: config.clientId,
|
|
321
|
+
authUrl: config.authUrl || DEFAULT_AUTH_URL,
|
|
322
|
+
appUrl: config.appUrl || '',
|
|
323
|
+
apiUrl: config.apiUrl || config.appUrl || '',
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
return (
|
|
327
|
+
<AuthConfigContext.Provider value={configValue}>
|
|
328
|
+
<AuthContext.Provider value={value}>{children}</AuthContext.Provider>
|
|
329
|
+
</AuthConfigContext.Provider>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { useState, useRef, useEffect } from 'react';
|
|
2
|
+
import { useAuth } from './useAuth.js';
|
|
3
|
+
|
|
4
|
+
interface OrganizationSwitcherProps {
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function OrganizationSwitcher({ className = '' }: OrganizationSwitcherProps) {
|
|
9
|
+
const { organization, organizations, switchOrganization } = useAuth();
|
|
10
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
11
|
+
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
12
|
+
|
|
13
|
+
// Close dropdown when clicking outside
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
function handleClickOutside(event: MouseEvent) {
|
|
16
|
+
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
|
17
|
+
setIsOpen(false);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
21
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
if (organizations.length <= 1) {
|
|
25
|
+
// Single org — just display name
|
|
26
|
+
return (
|
|
27
|
+
<div className={className}>
|
|
28
|
+
<span>{organization?.name || 'No organization'}</span>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div ref={dropdownRef} className={`relative ${className}`}>
|
|
35
|
+
<button
|
|
36
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
37
|
+
className="flex items-center gap-2 px-3 py-2 text-sm border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
|
38
|
+
>
|
|
39
|
+
<span className="font-medium">{organization?.name || 'Select org'}</span>
|
|
40
|
+
<svg
|
|
41
|
+
className={`w-4 h-4 transition-transform ${isOpen ? 'rotate-180' : ''}`}
|
|
42
|
+
fill="none"
|
|
43
|
+
viewBox="0 0 24 24"
|
|
44
|
+
stroke="currentColor"
|
|
45
|
+
>
|
|
46
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
47
|
+
</svg>
|
|
48
|
+
</button>
|
|
49
|
+
|
|
50
|
+
{isOpen && (
|
|
51
|
+
<div className="absolute top-full left-0 mt-1 w-64 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50">
|
|
52
|
+
<div className="py-1">
|
|
53
|
+
{organizations.map((org) => (
|
|
54
|
+
<button
|
|
55
|
+
key={org.id}
|
|
56
|
+
onClick={() => {
|
|
57
|
+
switchOrganization(org.id);
|
|
58
|
+
setIsOpen(false);
|
|
59
|
+
}}
|
|
60
|
+
className={`w-full text-left px-4 py-2 text-sm hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors ${
|
|
61
|
+
org.id === organization?.id
|
|
62
|
+
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400'
|
|
63
|
+
: 'text-gray-700 dark:text-gray-300'
|
|
64
|
+
}`}
|
|
65
|
+
>
|
|
66
|
+
<div className="font-medium">{org.name}</div>
|
|
67
|
+
<div className="text-xs text-gray-400">{org.slug}</div>
|
|
68
|
+
</button>
|
|
69
|
+
))}
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { useAuthConfig } from './useAuth.js';
|
|
3
|
+
import { useProviders, type SSOProvider } from './useProviders.js';
|
|
4
|
+
|
|
5
|
+
type Mode = 'signin' | 'signup';
|
|
6
|
+
|
|
7
|
+
const ERROR_MESSAGES: Record<string, string> = {
|
|
8
|
+
account_not_found: 'No account found with that login. Sign up to create one.',
|
|
9
|
+
oauth_failed: 'Something went wrong during sign in. Please try again.',
|
|
10
|
+
missing_code: 'Authorization failed. Please try again.',
|
|
11
|
+
invalid_state: 'Session expired. Please try again.',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const GITHUB_ICON = (
|
|
15
|
+
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
|
16
|
+
<path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844a9.59 9.59 0 0 1 2.504.337c1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.02 10.02 0 0 0 22 12.017C22 6.484 17.522 2 12 2z" />
|
|
17
|
+
</svg>
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const GOOGLE_ICON = (
|
|
21
|
+
<svg viewBox="0 0 24 24" width="20" height="20">
|
|
22
|
+
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" />
|
|
23
|
+
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
|
|
24
|
+
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
|
|
25
|
+
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
|
|
26
|
+
</svg>
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const APPLE_ICON = (
|
|
30
|
+
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
|
31
|
+
<path d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z" />
|
|
32
|
+
</svg>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const BITBUCKET_ICON = (
|
|
36
|
+
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
|
37
|
+
<path d="M.778 1.213a.768.768 0 0 0-.768.892l3.263 19.81c.084.5.515.868 1.022.873H19.95a.772.772 0 0 0 .77-.646l3.27-20.03a.768.768 0 0 0-.768-.891L.778 1.213zM14.52 15.53H9.522L8.17 8.466h7.561l-1.211 7.064z" />
|
|
38
|
+
</svg>
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
interface SignInFormProps {
|
|
42
|
+
/** List of provider IDs to show (for manual control). Overrides autoFetch when provided. */
|
|
43
|
+
providers?: SSOProvider[];
|
|
44
|
+
/** Whether to automatically fetch providers from the auth service. Defaults to true. */
|
|
45
|
+
autoFetch?: boolean;
|
|
46
|
+
className?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function SignInForm({ providers: propProviders, autoFetch = true, className }: SignInFormProps) {
|
|
50
|
+
const config = useAuthConfig();
|
|
51
|
+
const { providers: fetchedProviders, isLoading: providersLoading } = useProviders();
|
|
52
|
+
const [activeTab, setActiveTab] = useState<Mode>('signin');
|
|
53
|
+
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
54
|
+
const [returnTo, setReturnTo] = useState('/');
|
|
55
|
+
|
|
56
|
+
// Use prop providers if explicitly provided, otherwise use fetched providers
|
|
57
|
+
const enabledProviders: SSOProvider[] = propProviders
|
|
58
|
+
? propProviders
|
|
59
|
+
: autoFetch
|
|
60
|
+
? fetchedProviders.map((p) => p.id)
|
|
61
|
+
: ['github']; // fallback default
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
const params = new URLSearchParams(window.location.search);
|
|
65
|
+
const errorParam = params.get('error');
|
|
66
|
+
const tabParam = params.get('tab') as Mode | null;
|
|
67
|
+
const returnToParam = params.get('returnTo');
|
|
68
|
+
|
|
69
|
+
if (returnToParam) setReturnTo(returnToParam);
|
|
70
|
+
|
|
71
|
+
if (errorParam) {
|
|
72
|
+
setErrorMessage(ERROR_MESSAGES[errorParam] || `Authentication error: ${errorParam}`);
|
|
73
|
+
if (errorParam === 'account_not_found') {
|
|
74
|
+
setActiveTab('signup');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (tabParam === 'signin' || tabParam === 'signup') {
|
|
79
|
+
setActiveTab(tabParam);
|
|
80
|
+
}
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
function handleOAuth(provider: string) {
|
|
84
|
+
const redirectUri = `${config.appUrl}/auth/callback`;
|
|
85
|
+
const state = btoa(JSON.stringify({ returnTo }));
|
|
86
|
+
const params = new URLSearchParams({
|
|
87
|
+
client_id: config.clientId,
|
|
88
|
+
redirect_uri: redirectUri,
|
|
89
|
+
state,
|
|
90
|
+
provider,
|
|
91
|
+
mode: activeTab,
|
|
92
|
+
});
|
|
93
|
+
window.location.href = `${config.authUrl}/authorize?${params}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function switchTab(tab: Mode) {
|
|
97
|
+
setActiveTab(tab);
|
|
98
|
+
setErrorMessage(null);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function tabClass(tab: Mode): string {
|
|
102
|
+
const isActive = activeTab === tab;
|
|
103
|
+
return [
|
|
104
|
+
'flex-1 py-2 text-sm font-medium rounded-lg text-center cursor-pointer transition-colors',
|
|
105
|
+
isActive ? 'bg-white text-zinc-900 shadow-sm' : 'text-zinc-500 hover:text-zinc-700',
|
|
106
|
+
].join(' ');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const buttonText = (provider: string) => {
|
|
110
|
+
const providerNames: Record<string, string> = {
|
|
111
|
+
github: 'GitHub',
|
|
112
|
+
google: 'Google',
|
|
113
|
+
apple: 'Apple',
|
|
114
|
+
bitbucket: 'Bitbucket',
|
|
115
|
+
};
|
|
116
|
+
const name = providerNames[provider] || provider;
|
|
117
|
+
return activeTab === 'signin' ? `Sign in with ${name}` : `Sign up with ${name}`;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<div className={className || ''}>
|
|
122
|
+
{/* Tab toggle */}
|
|
123
|
+
<div className="flex gap-1 p-1 bg-zinc-100 rounded-xl mb-6">
|
|
124
|
+
<button type="button" className={tabClass('signin')} onClick={() => switchTab('signin')}>
|
|
125
|
+
Sign In
|
|
126
|
+
</button>
|
|
127
|
+
<button type="button" className={tabClass('signup')} onClick={() => switchTab('signup')}>
|
|
128
|
+
Sign Up
|
|
129
|
+
</button>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
{/* Error message */}
|
|
133
|
+
{errorMessage && (
|
|
134
|
+
<div className="mb-4 p-3 rounded-lg bg-red-50 border border-red-200 text-red-700 text-sm">
|
|
135
|
+
{errorMessage}
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
|
|
139
|
+
{/* Loading state */}
|
|
140
|
+
{autoFetch && !propProviders && providersLoading ? (
|
|
141
|
+
<div className="flex items-center justify-center py-4">
|
|
142
|
+
<div className="h-5 w-5 animate-spin rounded-full border-2 border-zinc-300 border-t-zinc-600" />
|
|
143
|
+
</div>
|
|
144
|
+
) : (
|
|
145
|
+
<div className="space-y-3">
|
|
146
|
+
{/* GitHub */}
|
|
147
|
+
{enabledProviders.includes('github') && (
|
|
148
|
+
<button
|
|
149
|
+
type="button"
|
|
150
|
+
className="w-full flex items-center justify-center gap-3 px-6 py-3 bg-zinc-900 text-white font-medium rounded-xl hover:bg-zinc-800 transition-colors cursor-pointer"
|
|
151
|
+
onClick={() => handleOAuth('github')}
|
|
152
|
+
>
|
|
153
|
+
{GITHUB_ICON}
|
|
154
|
+
<span>{buttonText('github')}</span>
|
|
155
|
+
</button>
|
|
156
|
+
)}
|
|
157
|
+
|
|
158
|
+
{/* Google */}
|
|
159
|
+
{enabledProviders.includes('google') && (
|
|
160
|
+
<button
|
|
161
|
+
type="button"
|
|
162
|
+
className="w-full flex items-center justify-center gap-3 px-6 py-3 bg-white text-zinc-900 font-medium rounded-xl border border-zinc-300 hover:bg-zinc-50 transition-colors cursor-pointer"
|
|
163
|
+
onClick={() => handleOAuth('google')}
|
|
164
|
+
>
|
|
165
|
+
{GOOGLE_ICON}
|
|
166
|
+
<span>{buttonText('google')}</span>
|
|
167
|
+
</button>
|
|
168
|
+
)}
|
|
169
|
+
|
|
170
|
+
{/* Apple */}
|
|
171
|
+
{enabledProviders.includes('apple') && (
|
|
172
|
+
<button
|
|
173
|
+
type="button"
|
|
174
|
+
className="w-full flex items-center justify-center gap-3 px-6 py-3 bg-black text-white font-medium rounded-xl hover:bg-zinc-900 transition-colors cursor-pointer"
|
|
175
|
+
onClick={() => handleOAuth('apple')}
|
|
176
|
+
>
|
|
177
|
+
{APPLE_ICON}
|
|
178
|
+
<span>{buttonText('apple')}</span>
|
|
179
|
+
</button>
|
|
180
|
+
)}
|
|
181
|
+
|
|
182
|
+
{/* Bitbucket */}
|
|
183
|
+
{enabledProviders.includes('bitbucket') && (
|
|
184
|
+
<button
|
|
185
|
+
type="button"
|
|
186
|
+
className="w-full flex items-center justify-center gap-3 px-6 py-3 bg-blue-600 text-white font-medium rounded-xl hover:bg-blue-700 transition-colors cursor-pointer"
|
|
187
|
+
onClick={() => handleOAuth('bitbucket')}
|
|
188
|
+
>
|
|
189
|
+
{BITBUCKET_ICON}
|
|
190
|
+
<span>{buttonText('bitbucket')}</span>
|
|
191
|
+
</button>
|
|
192
|
+
)}
|
|
193
|
+
</div>
|
|
194
|
+
)}
|
|
195
|
+
|
|
196
|
+
{/* Terms */}
|
|
197
|
+
<p className="mt-6 text-center text-xs text-zinc-400">
|
|
198
|
+
By continuing, you agree to our terms of service.
|
|
199
|
+
</p>
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { useAuth } from './useAuth.js';
|
|
3
|
+
|
|
4
|
+
interface AuthGateProps {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Renders children only when user is authenticated
|
|
10
|
+
*/
|
|
11
|
+
export function SignedIn({ children }: AuthGateProps) {
|
|
12
|
+
const { isAuthenticated, isLoading } = useAuth();
|
|
13
|
+
if (isLoading || !isAuthenticated) return null;
|
|
14
|
+
return <>{children}</>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Renders children only when user is NOT authenticated
|
|
19
|
+
*/
|
|
20
|
+
export function SignedOut({ children }: AuthGateProps) {
|
|
21
|
+
const { isAuthenticated, isLoading } = useAuth();
|
|
22
|
+
if (isLoading || isAuthenticated) return null;
|
|
23
|
+
return <>{children}</>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Redirects to auth service sign-in when user is not authenticated
|
|
28
|
+
*/
|
|
29
|
+
export function RedirectToSignIn() {
|
|
30
|
+
const { isAuthenticated, isLoading, login } = useAuth();
|
|
31
|
+
|
|
32
|
+
if (isLoading) return null;
|
|
33
|
+
|
|
34
|
+
if (!isAuthenticated) {
|
|
35
|
+
login();
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return null;
|
|
40
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { AuthProvider } from './AuthProvider.js';
|
|
2
|
+
export { useAuth, useAuthConfig } from './useAuth.js';
|
|
3
|
+
export { useProviders, type SSOProvider, type ProviderInfo, type UseProvidersResult } from './useProviders.js';
|
|
4
|
+
export { SignedIn, SignedOut, RedirectToSignIn } from './components.js';
|
|
5
|
+
export { OrganizationSwitcher } from './OrganizationSwitcher.js';
|
|
6
|
+
export { AuthCallback } from './AuthCallback.js';
|
|
7
|
+
export { SignInForm } from './SignInForm.js';
|
|
8
|
+
export { AuthConfigError } from './validateConfig.js';
|
|
9
|
+
export type { AuthConfig, ResolvedAuthConfig, AuthUser, AuthOrganization, AuthState } from './types.js';
|
package/src/style.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@tailwind utilities;
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export interface AuthConfig {
|
|
2
|
+
/** Client ID registered with auth service */
|
|
3
|
+
clientId: string;
|
|
4
|
+
/** Auth service URL. Defaults to https://auth.tony.codes */
|
|
5
|
+
authUrl?: string;
|
|
6
|
+
/** This app's base URL. Auto-discovered from server or defaults to window.location.origin */
|
|
7
|
+
appUrl?: string;
|
|
8
|
+
/**
|
|
9
|
+
* API base URL for proxied auth endpoints (callback, refresh, logout).
|
|
10
|
+
* Required if your API runs on a different subdomain than your frontend
|
|
11
|
+
* (e.g., api.myapp.test vs myapp.test).
|
|
12
|
+
* Auto-discovered from server or defaults to appUrl.
|
|
13
|
+
*/
|
|
14
|
+
apiUrl?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Whether to require an organization for the user to be considered authenticated.
|
|
17
|
+
* Defaults to true. Set to false for single-user apps without org context.
|
|
18
|
+
*/
|
|
19
|
+
requireOrg?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Resolved config with all URLs populated (after discovery) */
|
|
23
|
+
export interface ResolvedAuthConfig {
|
|
24
|
+
clientId: string;
|
|
25
|
+
authUrl: string;
|
|
26
|
+
appUrl: string;
|
|
27
|
+
apiUrl: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface AuthUser {
|
|
31
|
+
id: string;
|
|
32
|
+
email: string;
|
|
33
|
+
name: string;
|
|
34
|
+
role: string;
|
|
35
|
+
imageUrl: string | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface AuthOrganization {
|
|
39
|
+
id: string;
|
|
40
|
+
name: string;
|
|
41
|
+
slug: string;
|
|
42
|
+
imageUrl: string | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface AuthState {
|
|
46
|
+
isAuthenticated: boolean;
|
|
47
|
+
isLoading: boolean;
|
|
48
|
+
user: AuthUser | null;
|
|
49
|
+
organization: AuthOrganization | null;
|
|
50
|
+
tenant: { id: string; name: string; slug: string } | null; // legacy alias
|
|
51
|
+
isAdmin: boolean;
|
|
52
|
+
isOwner: boolean;
|
|
53
|
+
orgRole: string;
|
|
54
|
+
isSuperAdmin: boolean;
|
|
55
|
+
isPlatformAdmin: boolean;
|
|
56
|
+
accessToken: string | null;
|
|
57
|
+
getAccessToken: () => Promise<string | null>;
|
|
58
|
+
login: (provider?: string) => void;
|
|
59
|
+
logout: () => Promise<void>;
|
|
60
|
+
switchOrganization: (orgId: string) => Promise<void>;
|
|
61
|
+
organizations: AuthOrganization[];
|
|
62
|
+
isLoggingOut: boolean;
|
|
63
|
+
isLoggingIn: boolean;
|
|
64
|
+
loginError: string | null;
|
|
65
|
+
impersonating: boolean;
|
|
66
|
+
}
|
package/src/useAuth.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { AuthContext, AuthConfigContext } from './AuthProvider.js';
|
|
3
|
+
import type { ResolvedAuthConfig, AuthState } from './types.js';
|
|
4
|
+
|
|
5
|
+
export function useAuth(): AuthState {
|
|
6
|
+
const context = useContext(AuthContext);
|
|
7
|
+
if (!context) {
|
|
8
|
+
throw new Error('useAuth must be used within an AuthProvider');
|
|
9
|
+
}
|
|
10
|
+
return context;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function useAuthConfig(): ResolvedAuthConfig {
|
|
14
|
+
const config = useContext(AuthConfigContext);
|
|
15
|
+
if (!config) {
|
|
16
|
+
throw new Error('useAuthConfig must be used within an AuthProvider');
|
|
17
|
+
}
|
|
18
|
+
return config;
|
|
19
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import { useAuthConfig } from './useAuth.js';
|
|
3
|
+
|
|
4
|
+
export type SSOProvider = 'github' | 'google' | 'apple' | 'bitbucket';
|
|
5
|
+
|
|
6
|
+
export interface ProviderInfo {
|
|
7
|
+
id: SSOProvider;
|
|
8
|
+
name: string;
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface UseProvidersResult {
|
|
13
|
+
providers: ProviderInfo[];
|
|
14
|
+
isLoading: boolean;
|
|
15
|
+
error: string | null;
|
|
16
|
+
/** Force refetch, bypassing cache */
|
|
17
|
+
refresh: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ─── Module-level cache ──────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
interface CacheEntry {
|
|
23
|
+
providers: ProviderInfo[];
|
|
24
|
+
fetchedAt: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const CACHE_TTL = 60_000; // 60 seconds
|
|
28
|
+
const cache = new Map<string, CacheEntry>();
|
|
29
|
+
|
|
30
|
+
function getCacheKey(authUrl: string, clientId: string): string {
|
|
31
|
+
return `${authUrl}::${clientId}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getCachedProviders(key: string): CacheEntry | null {
|
|
35
|
+
const entry = cache.get(key);
|
|
36
|
+
if (!entry) return null;
|
|
37
|
+
return entry;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isStale(entry: CacheEntry): boolean {
|
|
41
|
+
return Date.now() - entry.fetchedAt > CACHE_TTL;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function fetchProviders(authUrl: string, clientId: string): Promise<ProviderInfo[]> {
|
|
45
|
+
const url = new URL(`${authUrl}/providers`);
|
|
46
|
+
url.searchParams.set('client_id', clientId);
|
|
47
|
+
|
|
48
|
+
const res = await fetch(url.toString());
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
throw new Error('Failed to fetch providers');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const data = await res.json();
|
|
54
|
+
return data.providers || [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── Hook ────────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Hook to fetch available SSO providers from the auth service.
|
|
61
|
+
* Uses module-level cache with 60s TTL and stale-while-revalidate.
|
|
62
|
+
* Clears cache on window.focus for admin changes to take effect.
|
|
63
|
+
*/
|
|
64
|
+
export function useProviders(): UseProvidersResult {
|
|
65
|
+
const config = useAuthConfig();
|
|
66
|
+
const cacheKey = getCacheKey(config.authUrl, config.clientId);
|
|
67
|
+
|
|
68
|
+
// Initialize from cache if available
|
|
69
|
+
const cached = getCachedProviders(cacheKey);
|
|
70
|
+
const [providers, setProviders] = useState<ProviderInfo[]>(cached?.providers || []);
|
|
71
|
+
const [isLoading, setIsLoading] = useState(!cached);
|
|
72
|
+
const [error, setError] = useState<string | null>(null);
|
|
73
|
+
const [refreshCounter, setRefreshCounter] = useState(0);
|
|
74
|
+
|
|
75
|
+
const refresh = useCallback(() => {
|
|
76
|
+
cache.delete(cacheKey);
|
|
77
|
+
setRefreshCounter((c: number) => c + 1);
|
|
78
|
+
}, [cacheKey]);
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
let mounted = true;
|
|
82
|
+
|
|
83
|
+
async function doFetch(showLoading: boolean) {
|
|
84
|
+
if (showLoading) setIsLoading(true);
|
|
85
|
+
try {
|
|
86
|
+
const result = await fetchProviders(config.authUrl, config.clientId);
|
|
87
|
+
cache.set(cacheKey, { providers: result, fetchedAt: Date.now() });
|
|
88
|
+
if (mounted) {
|
|
89
|
+
setProviders(result);
|
|
90
|
+
setError(null);
|
|
91
|
+
}
|
|
92
|
+
} catch (err) {
|
|
93
|
+
if (mounted) {
|
|
94
|
+
setError(err instanceof Error ? err.message : 'Failed to fetch providers');
|
|
95
|
+
// Keep stale data if we have it
|
|
96
|
+
if (!cached) setProviders([]);
|
|
97
|
+
}
|
|
98
|
+
} finally {
|
|
99
|
+
if (mounted) setIsLoading(false);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const entry = getCachedProviders(cacheKey);
|
|
104
|
+
|
|
105
|
+
if (!entry) {
|
|
106
|
+
// No cache — fetch with loading state
|
|
107
|
+
doFetch(true);
|
|
108
|
+
} else if (isStale(entry)) {
|
|
109
|
+
// Stale cache — show cached data, refresh in background
|
|
110
|
+
setProviders(entry.providers);
|
|
111
|
+
setIsLoading(false);
|
|
112
|
+
doFetch(false);
|
|
113
|
+
} else {
|
|
114
|
+
// Fresh cache — use it directly
|
|
115
|
+
setProviders(entry.providers);
|
|
116
|
+
setIsLoading(false);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Refetch on window focus (admin changes take effect when tab refocused)
|
|
120
|
+
function handleFocus() {
|
|
121
|
+
const current = getCachedProviders(cacheKey);
|
|
122
|
+
if (!current || isStale(current)) {
|
|
123
|
+
doFetch(false);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
window.addEventListener('focus', handleFocus);
|
|
128
|
+
|
|
129
|
+
return () => {
|
|
130
|
+
mounted = false;
|
|
131
|
+
window.removeEventListener('focus', handleFocus);
|
|
132
|
+
};
|
|
133
|
+
}, [config.authUrl, config.clientId, cacheKey, refreshCounter]);
|
|
134
|
+
|
|
135
|
+
return { providers, isLoading, error, refresh };
|
|
136
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { AuthConfig } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Configuration validation errors for better developer experience.
|
|
5
|
+
*/
|
|
6
|
+
export class AuthConfigError extends Error {
|
|
7
|
+
constructor(message: string) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'AuthConfigError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Validates AuthConfig and throws descriptive errors if invalid.
|
|
15
|
+
* Called at initialization time to fail fast with helpful messages.
|
|
16
|
+
* Only clientId is required — authUrl and appUrl can be auto-discovered.
|
|
17
|
+
*/
|
|
18
|
+
export function validateConfig(config: AuthConfig): void {
|
|
19
|
+
if (!config) {
|
|
20
|
+
throw new AuthConfigError(
|
|
21
|
+
'AuthProvider requires a config prop. ' +
|
|
22
|
+
'At minimum, provide { clientId: "your-app-id" }.',
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Required field
|
|
27
|
+
if (!config.clientId) {
|
|
28
|
+
throw new AuthConfigError(
|
|
29
|
+
'Missing required config: clientId. ' +
|
|
30
|
+
'This is the client ID registered with the auth service.',
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// URL format validation (only if provided)
|
|
35
|
+
if (config.authUrl) {
|
|
36
|
+
try {
|
|
37
|
+
new URL(config.authUrl);
|
|
38
|
+
} catch {
|
|
39
|
+
throw new AuthConfigError(
|
|
40
|
+
`Invalid authUrl: "${config.authUrl}" is not a valid URL. ` +
|
|
41
|
+
'It should include the protocol (e.g., "https://auth.tony.codes").',
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (config.appUrl) {
|
|
47
|
+
try {
|
|
48
|
+
new URL(config.appUrl);
|
|
49
|
+
} catch {
|
|
50
|
+
throw new AuthConfigError(
|
|
51
|
+
`Invalid appUrl: "${config.appUrl}" is not a valid URL. ` +
|
|
52
|
+
'It should include the protocol (e.g., "https://myapp.tony.codes").',
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (config.apiUrl) {
|
|
58
|
+
try {
|
|
59
|
+
new URL(config.apiUrl);
|
|
60
|
+
} catch {
|
|
61
|
+
throw new AuthConfigError(
|
|
62
|
+
`Invalid apiUrl: "${config.apiUrl}" is not a valid URL. ` +
|
|
63
|
+
'It should include the protocol (e.g., "https://api.myapp.tony.codes").',
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Common misconfiguration warnings (logged but don't throw)
|
|
69
|
+
if (typeof window !== 'undefined') {
|
|
70
|
+
const isProduction = !window.location.hostname.includes('test') &&
|
|
71
|
+
!window.location.hostname.includes('localhost');
|
|
72
|
+
|
|
73
|
+
if (isProduction && config.authUrl?.includes('localhost')) {
|
|
74
|
+
console.warn(
|
|
75
|
+
'[auth-react] Warning: authUrl contains "localhost" but app appears to be in production. ' +
|
|
76
|
+
'Make sure to update authUrl for production.',
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (config.authUrl?.endsWith('/')) {
|
|
81
|
+
console.warn(
|
|
82
|
+
'[auth-react] Warning: authUrl has a trailing slash. ' +
|
|
83
|
+
'This may cause double slashes in URLs.',
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (config.appUrl?.endsWith('/')) {
|
|
88
|
+
console.warn(
|
|
89
|
+
'[auth-react] Warning: appUrl has a trailing slash. ' +
|
|
90
|
+
'This may cause double slashes in URLs.',
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"jsx": "react-jsx",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"outDir": "dist",
|
|
10
|
+
"rootDir": "src",
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationMap": true,
|
|
14
|
+
"sourceMap": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src"],
|
|
17
|
+
"exclude": ["node_modules", "dist"]
|
|
18
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2024 Tony
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|