@fluxbase/sdk-react 0.1.0-rc.1 → 2026.1.1-rc.2
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-ADMIN.md +8 -8
- package/README.md +27 -14
- package/dist/index.d.mts +638 -83
- package/dist/index.d.ts +638 -83
- package/dist/index.js +552 -175
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +540 -172
- package/dist/index.mjs.map +1 -1
- package/examples/AdminDashboard.tsx +3 -3
- package/examples/README.md +1 -1
- package/package.json +3 -3
- package/src/index.ts +57 -18
- package/src/use-admin-auth.ts +62 -51
- package/src/use-auth.ts +66 -49
- package/src/use-captcha.ts +250 -0
- package/src/{use-api-keys.ts → use-client-keys.ts} +43 -32
- package/src/use-graphql.ts +392 -0
- package/src/use-realtime.ts +58 -44
- package/src/use-saml.ts +221 -0
- package/src/use-storage.ts +325 -82
- package/src/use-users.ts +11 -4
- package/tsconfig.tsbuildinfo +1 -1
- package/typedoc.json +2 -4
- package/CHANGELOG.md +0 -67
- package/src/use-rpc.ts +0 -109
package/src/use-admin-auth.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback } from
|
|
2
|
-
import { useFluxbaseClient } from
|
|
3
|
-
import type { AdminAuthResponse } from
|
|
1
|
+
import { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import { useFluxbaseClient } from "./context";
|
|
3
|
+
import type { AdminAuthResponse, DataResponse } from "@fluxbase/sdk";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Simplified admin user type returned by authentication
|
|
7
7
|
*/
|
|
8
8
|
export interface AdminUser {
|
|
9
|
-
id: string
|
|
10
|
-
email: string
|
|
11
|
-
role: string
|
|
9
|
+
id: string;
|
|
10
|
+
email: string;
|
|
11
|
+
role: string;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export interface UseAdminAuthOptions {
|
|
@@ -16,44 +16,44 @@ export interface UseAdminAuthOptions {
|
|
|
16
16
|
* Automatically check authentication status on mount
|
|
17
17
|
* @default true
|
|
18
18
|
*/
|
|
19
|
-
autoCheck?: boolean
|
|
19
|
+
autoCheck?: boolean;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export interface UseAdminAuthReturn {
|
|
23
23
|
/**
|
|
24
24
|
* Current admin user if authenticated
|
|
25
25
|
*/
|
|
26
|
-
user: AdminUser | null
|
|
26
|
+
user: AdminUser | null;
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Whether the admin is authenticated
|
|
30
30
|
*/
|
|
31
|
-
isAuthenticated: boolean
|
|
31
|
+
isAuthenticated: boolean;
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* Whether the authentication check is in progress
|
|
35
35
|
*/
|
|
36
|
-
isLoading: boolean
|
|
36
|
+
isLoading: boolean;
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* Any error that occurred during authentication
|
|
40
40
|
*/
|
|
41
|
-
error: Error | null
|
|
41
|
+
error: Error | null;
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* Login as admin
|
|
45
45
|
*/
|
|
46
|
-
login: (email: string, password: string) => Promise<AdminAuthResponse
|
|
46
|
+
login: (email: string, password: string) => Promise<AdminAuthResponse>;
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* Logout admin
|
|
50
50
|
*/
|
|
51
|
-
logout: () => Promise<void
|
|
51
|
+
logout: () => Promise<void>;
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
54
|
* Refresh admin user info
|
|
55
55
|
*/
|
|
56
|
-
refresh: () => Promise<void
|
|
56
|
+
refresh: () => Promise<void>;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/**
|
|
@@ -78,30 +78,35 @@ export interface UseAdminAuthReturn {
|
|
|
78
78
|
* }
|
|
79
79
|
* ```
|
|
80
80
|
*/
|
|
81
|
-
export function useAdminAuth(
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
export function useAdminAuth(
|
|
82
|
+
options: UseAdminAuthOptions = {},
|
|
83
|
+
): UseAdminAuthReturn {
|
|
84
|
+
const { autoCheck = true } = options;
|
|
85
|
+
const client = useFluxbaseClient();
|
|
84
86
|
|
|
85
|
-
const [user, setUser] = useState<AdminUser | null>(null)
|
|
86
|
-
const [isLoading, setIsLoading] = useState(autoCheck)
|
|
87
|
-
const [error, setError] = useState<Error | null>(null)
|
|
87
|
+
const [user, setUser] = useState<AdminUser | null>(null);
|
|
88
|
+
const [isLoading, setIsLoading] = useState(autoCheck);
|
|
89
|
+
const [error, setError] = useState<Error | null>(null);
|
|
88
90
|
|
|
89
91
|
/**
|
|
90
92
|
* Check current authentication status
|
|
91
93
|
*/
|
|
92
94
|
const checkAuth = useCallback(async () => {
|
|
93
95
|
try {
|
|
94
|
-
setIsLoading(true)
|
|
95
|
-
setError(null)
|
|
96
|
-
const {
|
|
97
|
-
|
|
96
|
+
setIsLoading(true);
|
|
97
|
+
setError(null);
|
|
98
|
+
const { data, error: apiError } = await client.admin.me();
|
|
99
|
+
if (apiError) {
|
|
100
|
+
throw apiError;
|
|
101
|
+
}
|
|
102
|
+
setUser(data!.user);
|
|
98
103
|
} catch (err) {
|
|
99
|
-
setUser(null)
|
|
100
|
-
setError(err as Error)
|
|
104
|
+
setUser(null);
|
|
105
|
+
setError(err as Error);
|
|
101
106
|
} finally {
|
|
102
|
-
setIsLoading(false)
|
|
107
|
+
setIsLoading(false);
|
|
103
108
|
}
|
|
104
|
-
}, [client])
|
|
109
|
+
}, [client]);
|
|
105
110
|
|
|
106
111
|
/**
|
|
107
112
|
* Login as admin
|
|
@@ -109,52 +114,58 @@ export function useAdminAuth(options: UseAdminAuthOptions = {}): UseAdminAuthRet
|
|
|
109
114
|
const login = useCallback(
|
|
110
115
|
async (email: string, password: string): Promise<AdminAuthResponse> => {
|
|
111
116
|
try {
|
|
112
|
-
setIsLoading(true)
|
|
113
|
-
setError(null)
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
+
setIsLoading(true);
|
|
118
|
+
setError(null);
|
|
119
|
+
const { data, error: apiError } = await client.admin.login({
|
|
120
|
+
email,
|
|
121
|
+
password,
|
|
122
|
+
});
|
|
123
|
+
if (apiError) {
|
|
124
|
+
throw apiError;
|
|
125
|
+
}
|
|
126
|
+
setUser(data!.user);
|
|
127
|
+
return data!;
|
|
117
128
|
} catch (err) {
|
|
118
|
-
setError(err as Error)
|
|
119
|
-
throw err
|
|
129
|
+
setError(err as Error);
|
|
130
|
+
throw err;
|
|
120
131
|
} finally {
|
|
121
|
-
setIsLoading(false)
|
|
132
|
+
setIsLoading(false);
|
|
122
133
|
}
|
|
123
134
|
},
|
|
124
|
-
[client]
|
|
125
|
-
)
|
|
135
|
+
[client],
|
|
136
|
+
);
|
|
126
137
|
|
|
127
138
|
/**
|
|
128
139
|
* Logout admin
|
|
129
140
|
*/
|
|
130
141
|
const logout = useCallback(async (): Promise<void> => {
|
|
131
142
|
try {
|
|
132
|
-
setIsLoading(true)
|
|
133
|
-
setError(null)
|
|
143
|
+
setIsLoading(true);
|
|
144
|
+
setError(null);
|
|
134
145
|
// Clear user state
|
|
135
|
-
setUser(null)
|
|
146
|
+
setUser(null);
|
|
136
147
|
// Note: Add logout endpoint call here when available
|
|
137
148
|
} catch (err) {
|
|
138
|
-
setError(err as Error)
|
|
139
|
-
throw err
|
|
149
|
+
setError(err as Error);
|
|
150
|
+
throw err;
|
|
140
151
|
} finally {
|
|
141
|
-
setIsLoading(false)
|
|
152
|
+
setIsLoading(false);
|
|
142
153
|
}
|
|
143
|
-
}, [])
|
|
154
|
+
}, []);
|
|
144
155
|
|
|
145
156
|
/**
|
|
146
157
|
* Refresh admin user info
|
|
147
158
|
*/
|
|
148
159
|
const refresh = useCallback(async (): Promise<void> => {
|
|
149
|
-
await checkAuth()
|
|
150
|
-
}, [checkAuth])
|
|
160
|
+
await checkAuth();
|
|
161
|
+
}, [checkAuth]);
|
|
151
162
|
|
|
152
163
|
// Auto-check authentication on mount
|
|
153
164
|
useEffect(() => {
|
|
154
165
|
if (autoCheck) {
|
|
155
|
-
checkAuth()
|
|
166
|
+
checkAuth();
|
|
156
167
|
}
|
|
157
|
-
}, [autoCheck, checkAuth])
|
|
168
|
+
}, [autoCheck, checkAuth]);
|
|
158
169
|
|
|
159
170
|
return {
|
|
160
171
|
user,
|
|
@@ -163,6 +174,6 @@ export function useAdminAuth(options: UseAdminAuthOptions = {}): UseAdminAuthRet
|
|
|
163
174
|
error,
|
|
164
175
|
login,
|
|
165
176
|
logout,
|
|
166
|
-
refresh
|
|
167
|
-
}
|
|
177
|
+
refresh,
|
|
178
|
+
};
|
|
168
179
|
}
|
package/src/use-auth.ts
CHANGED
|
@@ -2,132 +2,149 @@
|
|
|
2
2
|
* Authentication hooks for Fluxbase SDK
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { useMutation, useQuery, useQueryClient } from
|
|
6
|
-
import { useFluxbaseClient } from
|
|
7
|
-
import type {
|
|
5
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
6
|
+
import { useFluxbaseClient } from "./context";
|
|
7
|
+
import type {
|
|
8
|
+
SignInCredentials,
|
|
9
|
+
SignUpCredentials,
|
|
10
|
+
User,
|
|
11
|
+
AuthSession,
|
|
12
|
+
} from "@fluxbase/sdk";
|
|
8
13
|
|
|
9
14
|
/**
|
|
10
15
|
* Hook to get the current user
|
|
11
16
|
*/
|
|
12
17
|
export function useUser() {
|
|
13
|
-
const client = useFluxbaseClient()
|
|
18
|
+
const client = useFluxbaseClient();
|
|
14
19
|
|
|
15
20
|
return useQuery({
|
|
16
|
-
queryKey: [
|
|
21
|
+
queryKey: ["fluxbase", "auth", "user"],
|
|
17
22
|
queryFn: async () => {
|
|
18
|
-
const
|
|
19
|
-
if (!session) {
|
|
20
|
-
return null
|
|
23
|
+
const { data } = await client.auth.getSession();
|
|
24
|
+
if (!data?.session) {
|
|
25
|
+
return null;
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
try {
|
|
24
|
-
|
|
29
|
+
const result = await client.auth.getCurrentUser();
|
|
30
|
+
return result.data?.user ?? null;
|
|
25
31
|
} catch {
|
|
26
|
-
return null
|
|
32
|
+
return null;
|
|
27
33
|
}
|
|
28
34
|
},
|
|
29
35
|
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
30
|
-
})
|
|
36
|
+
});
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
/**
|
|
34
40
|
* Hook to get the current session
|
|
35
41
|
*/
|
|
36
42
|
export function useSession() {
|
|
37
|
-
const client = useFluxbaseClient()
|
|
43
|
+
const client = useFluxbaseClient();
|
|
38
44
|
|
|
39
45
|
return useQuery<AuthSession | null>({
|
|
40
|
-
queryKey: [
|
|
41
|
-
queryFn: () =>
|
|
46
|
+
queryKey: ["fluxbase", "auth", "session"],
|
|
47
|
+
queryFn: async () => {
|
|
48
|
+
const { data } = await client.auth.getSession();
|
|
49
|
+
return data?.session ?? null;
|
|
50
|
+
},
|
|
42
51
|
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
43
|
-
})
|
|
52
|
+
});
|
|
44
53
|
}
|
|
45
54
|
|
|
46
55
|
/**
|
|
47
56
|
* Hook for signing in
|
|
48
57
|
*/
|
|
49
58
|
export function useSignIn() {
|
|
50
|
-
const client = useFluxbaseClient()
|
|
51
|
-
const queryClient = useQueryClient()
|
|
59
|
+
const client = useFluxbaseClient();
|
|
60
|
+
const queryClient = useQueryClient();
|
|
52
61
|
|
|
53
62
|
return useMutation({
|
|
54
63
|
mutationFn: async (credentials: SignInCredentials) => {
|
|
55
|
-
return await client.auth.signIn(credentials)
|
|
64
|
+
return await client.auth.signIn(credentials);
|
|
56
65
|
},
|
|
57
66
|
onSuccess: (session) => {
|
|
58
|
-
queryClient.setQueryData([
|
|
67
|
+
queryClient.setQueryData(["fluxbase", "auth", "session"], session);
|
|
59
68
|
// Only set user if this is a complete auth session (not 2FA required)
|
|
60
|
-
if (
|
|
61
|
-
queryClient.setQueryData([
|
|
69
|
+
if ("user" in session) {
|
|
70
|
+
queryClient.setQueryData(["fluxbase", "auth", "user"], session.user);
|
|
62
71
|
}
|
|
63
72
|
},
|
|
64
|
-
})
|
|
73
|
+
});
|
|
65
74
|
}
|
|
66
75
|
|
|
67
76
|
/**
|
|
68
77
|
* Hook for signing up
|
|
69
78
|
*/
|
|
70
79
|
export function useSignUp() {
|
|
71
|
-
const client = useFluxbaseClient()
|
|
72
|
-
const queryClient = useQueryClient()
|
|
80
|
+
const client = useFluxbaseClient();
|
|
81
|
+
const queryClient = useQueryClient();
|
|
73
82
|
|
|
74
83
|
return useMutation({
|
|
75
84
|
mutationFn: async (credentials: SignUpCredentials) => {
|
|
76
|
-
return await client.auth.signUp(credentials)
|
|
85
|
+
return await client.auth.signUp(credentials);
|
|
77
86
|
},
|
|
78
|
-
onSuccess: (
|
|
79
|
-
|
|
80
|
-
|
|
87
|
+
onSuccess: (response) => {
|
|
88
|
+
if (response.data) {
|
|
89
|
+
queryClient.setQueryData(
|
|
90
|
+
["fluxbase", "auth", "session"],
|
|
91
|
+
response.data.session,
|
|
92
|
+
);
|
|
93
|
+
queryClient.setQueryData(
|
|
94
|
+
["fluxbase", "auth", "user"],
|
|
95
|
+
response.data.user,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
81
98
|
},
|
|
82
|
-
})
|
|
99
|
+
});
|
|
83
100
|
}
|
|
84
101
|
|
|
85
102
|
/**
|
|
86
103
|
* Hook for signing out
|
|
87
104
|
*/
|
|
88
105
|
export function useSignOut() {
|
|
89
|
-
const client = useFluxbaseClient()
|
|
90
|
-
const queryClient = useQueryClient()
|
|
106
|
+
const client = useFluxbaseClient();
|
|
107
|
+
const queryClient = useQueryClient();
|
|
91
108
|
|
|
92
109
|
return useMutation({
|
|
93
110
|
mutationFn: async () => {
|
|
94
|
-
await client.auth.signOut()
|
|
111
|
+
await client.auth.signOut();
|
|
95
112
|
},
|
|
96
113
|
onSuccess: () => {
|
|
97
|
-
queryClient.setQueryData([
|
|
98
|
-
queryClient.setQueryData([
|
|
99
|
-
queryClient.invalidateQueries({ queryKey: [
|
|
114
|
+
queryClient.setQueryData(["fluxbase", "auth", "session"], null);
|
|
115
|
+
queryClient.setQueryData(["fluxbase", "auth", "user"], null);
|
|
116
|
+
queryClient.invalidateQueries({ queryKey: ["fluxbase"] });
|
|
100
117
|
},
|
|
101
|
-
})
|
|
118
|
+
});
|
|
102
119
|
}
|
|
103
120
|
|
|
104
121
|
/**
|
|
105
122
|
* Hook for updating the current user
|
|
106
123
|
*/
|
|
107
124
|
export function useUpdateUser() {
|
|
108
|
-
const client = useFluxbaseClient()
|
|
109
|
-
const queryClient = useQueryClient()
|
|
125
|
+
const client = useFluxbaseClient();
|
|
126
|
+
const queryClient = useQueryClient();
|
|
110
127
|
|
|
111
128
|
return useMutation({
|
|
112
|
-
mutationFn: async (data: Partial<Pick<User,
|
|
113
|
-
return await client.auth.updateUser(data)
|
|
129
|
+
mutationFn: async (data: Partial<Pick<User, "email" | "metadata">>) => {
|
|
130
|
+
return await client.auth.updateUser(data);
|
|
114
131
|
},
|
|
115
132
|
onSuccess: (user) => {
|
|
116
|
-
queryClient.setQueryData([
|
|
133
|
+
queryClient.setQueryData(["fluxbase", "auth", "user"], user);
|
|
117
134
|
},
|
|
118
|
-
})
|
|
135
|
+
});
|
|
119
136
|
}
|
|
120
137
|
|
|
121
138
|
/**
|
|
122
139
|
* Combined auth hook with all auth state and methods
|
|
123
140
|
*/
|
|
124
141
|
export function useAuth() {
|
|
125
|
-
const { data: user, isLoading: isLoadingUser } = useUser()
|
|
126
|
-
const { data: session, isLoading: isLoadingSession } = useSession()
|
|
127
|
-
const signIn = useSignIn()
|
|
128
|
-
const signUp = useSignUp()
|
|
129
|
-
const signOut = useSignOut()
|
|
130
|
-
const updateUser = useUpdateUser()
|
|
142
|
+
const { data: user, isLoading: isLoadingUser } = useUser();
|
|
143
|
+
const { data: session, isLoading: isLoadingSession } = useSession();
|
|
144
|
+
const signIn = useSignIn();
|
|
145
|
+
const signUp = useSignUp();
|
|
146
|
+
const signOut = useSignOut();
|
|
147
|
+
const updateUser = useUpdateUser();
|
|
131
148
|
|
|
132
149
|
return {
|
|
133
150
|
user,
|
|
@@ -142,5 +159,5 @@ export function useAuth() {
|
|
|
142
159
|
isSigningUp: signUp.isPending,
|
|
143
160
|
isSigningOut: signOut.isPending,
|
|
144
161
|
isUpdating: updateUser.isPending,
|
|
145
|
-
}
|
|
162
|
+
};
|
|
146
163
|
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CAPTCHA hooks for Fluxbase SDK
|
|
3
|
+
*
|
|
4
|
+
* Provides hooks to:
|
|
5
|
+
* - Fetch CAPTCHA configuration from the server
|
|
6
|
+
* - Manage CAPTCHA widget state
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useQuery } from "@tanstack/react-query";
|
|
10
|
+
import { useFluxbaseClient } from "./context";
|
|
11
|
+
import type { CaptchaConfig, CaptchaProvider } from "@fluxbase/sdk";
|
|
12
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Hook to get the CAPTCHA configuration from the server
|
|
16
|
+
* Use this to determine which CAPTCHA provider to load
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* function AuthPage() {
|
|
21
|
+
* const { data: captchaConfig, isLoading } = useCaptchaConfig();
|
|
22
|
+
*
|
|
23
|
+
* if (isLoading) return <Loading />;
|
|
24
|
+
*
|
|
25
|
+
* return captchaConfig?.enabled ? (
|
|
26
|
+
* <CaptchaWidget provider={captchaConfig.provider} siteKey={captchaConfig.site_key} />
|
|
27
|
+
* ) : null;
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function useCaptchaConfig() {
|
|
32
|
+
const client = useFluxbaseClient();
|
|
33
|
+
|
|
34
|
+
return useQuery<CaptchaConfig>({
|
|
35
|
+
queryKey: ["fluxbase", "auth", "captcha", "config"],
|
|
36
|
+
queryFn: async () => {
|
|
37
|
+
const { data, error } = await client.auth.getCaptchaConfig();
|
|
38
|
+
if (error) {
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
return data!;
|
|
42
|
+
},
|
|
43
|
+
staleTime: 1000 * 60 * 60, // Cache for 1 hour (config rarely changes)
|
|
44
|
+
gcTime: 1000 * 60 * 60 * 24, // Keep in cache for 24 hours
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* CAPTCHA widget state for managing token generation
|
|
50
|
+
*/
|
|
51
|
+
export interface CaptchaState {
|
|
52
|
+
/** Current CAPTCHA token (null until solved) */
|
|
53
|
+
token: string | null;
|
|
54
|
+
/** Whether the CAPTCHA widget is ready */
|
|
55
|
+
isReady: boolean;
|
|
56
|
+
/** Whether a token is being generated */
|
|
57
|
+
isLoading: boolean;
|
|
58
|
+
/** Any error that occurred */
|
|
59
|
+
error: Error | null;
|
|
60
|
+
/** Reset the CAPTCHA widget */
|
|
61
|
+
reset: () => void;
|
|
62
|
+
/** Execute/trigger the CAPTCHA (for invisible CAPTCHA like reCAPTCHA v3) */
|
|
63
|
+
execute: () => Promise<string>;
|
|
64
|
+
/** Callback to be called when CAPTCHA is verified */
|
|
65
|
+
onVerify: (token: string) => void;
|
|
66
|
+
/** Callback to be called when CAPTCHA expires */
|
|
67
|
+
onExpire: () => void;
|
|
68
|
+
/** Callback to be called when CAPTCHA errors */
|
|
69
|
+
onError: (error: Error) => void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Hook to manage CAPTCHA widget state
|
|
74
|
+
*
|
|
75
|
+
* This hook provides a standardized interface for managing CAPTCHA tokens
|
|
76
|
+
* across different providers (hCaptcha, reCAPTCHA v3, Turnstile, Cap).
|
|
77
|
+
*
|
|
78
|
+
* Supported providers:
|
|
79
|
+
* - hcaptcha: Privacy-focused visual challenge
|
|
80
|
+
* - recaptcha_v3: Google's invisible risk-based CAPTCHA
|
|
81
|
+
* - turnstile: Cloudflare's invisible CAPTCHA
|
|
82
|
+
* - cap: Self-hosted proof-of-work CAPTCHA (https://capjs.js.org/)
|
|
83
|
+
*
|
|
84
|
+
* @param provider - The CAPTCHA provider type
|
|
85
|
+
* @returns CAPTCHA state and callbacks
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```tsx
|
|
89
|
+
* function LoginForm() {
|
|
90
|
+
* const captcha = useCaptcha('hcaptcha');
|
|
91
|
+
*
|
|
92
|
+
* const handleSubmit = async (e: FormEvent) => {
|
|
93
|
+
* e.preventDefault();
|
|
94
|
+
*
|
|
95
|
+
* // Get CAPTCHA token
|
|
96
|
+
* const captchaToken = captcha.token || await captcha.execute();
|
|
97
|
+
*
|
|
98
|
+
* // Sign in with CAPTCHA token
|
|
99
|
+
* await signIn({
|
|
100
|
+
* email,
|
|
101
|
+
* password,
|
|
102
|
+
* captchaToken
|
|
103
|
+
* });
|
|
104
|
+
* };
|
|
105
|
+
*
|
|
106
|
+
* return (
|
|
107
|
+
* <form onSubmit={handleSubmit}>
|
|
108
|
+
* <input name="email" />
|
|
109
|
+
* <input name="password" type="password" />
|
|
110
|
+
*
|
|
111
|
+
* <HCaptcha
|
|
112
|
+
* sitekey={siteKey}
|
|
113
|
+
* onVerify={captcha.onVerify}
|
|
114
|
+
* onExpire={captcha.onExpire}
|
|
115
|
+
* onError={captcha.onError}
|
|
116
|
+
* />
|
|
117
|
+
*
|
|
118
|
+
* <button type="submit" disabled={!captcha.isReady}>
|
|
119
|
+
* Sign In
|
|
120
|
+
* </button>
|
|
121
|
+
* </form>
|
|
122
|
+
* );
|
|
123
|
+
* }
|
|
124
|
+
* ```
|
|
125
|
+
*
|
|
126
|
+
* @example Cap provider
|
|
127
|
+
* ```tsx
|
|
128
|
+
* function LoginForm() {
|
|
129
|
+
* const { data: config } = useCaptchaConfig();
|
|
130
|
+
* const captcha = useCaptcha(config?.provider);
|
|
131
|
+
*
|
|
132
|
+
* // For Cap, load the widget from cap_server_url
|
|
133
|
+
* // <script src={`${config.cap_server_url}/widget.js`} />
|
|
134
|
+
* // <cap-widget data-cap-url={config.cap_server_url} />
|
|
135
|
+
* }
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
export function useCaptcha(provider?: CaptchaProvider): CaptchaState {
|
|
139
|
+
const [token, setToken] = useState<string | null>(null);
|
|
140
|
+
const [isReady, setIsReady] = useState(false);
|
|
141
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
142
|
+
const [error, setError] = useState<Error | null>(null);
|
|
143
|
+
|
|
144
|
+
// Promise resolver for execute() method
|
|
145
|
+
const executeResolverRef = useRef<((token: string) => void) | null>(null);
|
|
146
|
+
const executeRejecterRef = useRef<((error: Error) => void) | null>(null);
|
|
147
|
+
|
|
148
|
+
// Callback when CAPTCHA is verified
|
|
149
|
+
const onVerify = useCallback((newToken: string) => {
|
|
150
|
+
setToken(newToken);
|
|
151
|
+
setIsLoading(false);
|
|
152
|
+
setError(null);
|
|
153
|
+
setIsReady(true);
|
|
154
|
+
|
|
155
|
+
// Resolve the execute() promise if waiting
|
|
156
|
+
if (executeResolverRef.current) {
|
|
157
|
+
executeResolverRef.current(newToken);
|
|
158
|
+
executeResolverRef.current = null;
|
|
159
|
+
executeRejecterRef.current = null;
|
|
160
|
+
}
|
|
161
|
+
}, []);
|
|
162
|
+
|
|
163
|
+
// Callback when CAPTCHA expires
|
|
164
|
+
const onExpire = useCallback(() => {
|
|
165
|
+
setToken(null);
|
|
166
|
+
setIsReady(true);
|
|
167
|
+
}, []);
|
|
168
|
+
|
|
169
|
+
// Callback when CAPTCHA errors
|
|
170
|
+
const onError = useCallback((err: Error) => {
|
|
171
|
+
setError(err);
|
|
172
|
+
setIsLoading(false);
|
|
173
|
+
setToken(null);
|
|
174
|
+
|
|
175
|
+
// Reject the execute() promise if waiting
|
|
176
|
+
if (executeRejecterRef.current) {
|
|
177
|
+
executeRejecterRef.current(err);
|
|
178
|
+
executeResolverRef.current = null;
|
|
179
|
+
executeRejecterRef.current = null;
|
|
180
|
+
}
|
|
181
|
+
}, []);
|
|
182
|
+
|
|
183
|
+
// Reset the CAPTCHA
|
|
184
|
+
const reset = useCallback(() => {
|
|
185
|
+
setToken(null);
|
|
186
|
+
setError(null);
|
|
187
|
+
setIsLoading(false);
|
|
188
|
+
}, []);
|
|
189
|
+
|
|
190
|
+
// Execute/trigger the CAPTCHA (for invisible CAPTCHA)
|
|
191
|
+
const execute = useCallback(async (): Promise<string> => {
|
|
192
|
+
// If we already have a token, return it
|
|
193
|
+
if (token) {
|
|
194
|
+
return token;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// If CAPTCHA is not configured, return empty string
|
|
198
|
+
if (!provider) {
|
|
199
|
+
return "";
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
setIsLoading(true);
|
|
203
|
+
setError(null);
|
|
204
|
+
|
|
205
|
+
// Return a promise that will be resolved by onVerify
|
|
206
|
+
return new Promise<string>((resolve, reject) => {
|
|
207
|
+
executeResolverRef.current = resolve;
|
|
208
|
+
executeRejecterRef.current = reject;
|
|
209
|
+
|
|
210
|
+
// For invisible CAPTCHAs, the widget should call onVerify when done
|
|
211
|
+
// The actual execution is handled by the CAPTCHA widget component
|
|
212
|
+
});
|
|
213
|
+
}, [token, provider]);
|
|
214
|
+
|
|
215
|
+
// Mark as ready when provider is set
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
if (provider) {
|
|
218
|
+
setIsReady(true);
|
|
219
|
+
}
|
|
220
|
+
}, [provider]);
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
token,
|
|
224
|
+
isReady,
|
|
225
|
+
isLoading,
|
|
226
|
+
error,
|
|
227
|
+
reset,
|
|
228
|
+
execute,
|
|
229
|
+
onVerify,
|
|
230
|
+
onExpire,
|
|
231
|
+
onError,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Check if CAPTCHA is required for a specific endpoint
|
|
237
|
+
*
|
|
238
|
+
* @param config - CAPTCHA configuration from useCaptchaConfig
|
|
239
|
+
* @param endpoint - The endpoint to check (e.g., 'signup', 'login', 'password_reset')
|
|
240
|
+
* @returns Whether CAPTCHA is required for this endpoint
|
|
241
|
+
*/
|
|
242
|
+
export function isCaptchaRequiredForEndpoint(
|
|
243
|
+
config: CaptchaConfig | undefined,
|
|
244
|
+
endpoint: string
|
|
245
|
+
): boolean {
|
|
246
|
+
if (!config?.enabled) {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
return config.endpoints?.includes(endpoint) ?? false;
|
|
250
|
+
}
|