@nimbleflux/fluxbase-sdk-react 2026.3.6-rc.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.
Files changed (42) hide show
  1. package/.nvmrc +1 -0
  2. package/README-ADMIN.md +1076 -0
  3. package/README.md +195 -0
  4. package/examples/AdminDashboard.tsx +513 -0
  5. package/examples/README.md +163 -0
  6. package/package.json +66 -0
  7. package/src/context.test.tsx +147 -0
  8. package/src/context.tsx +33 -0
  9. package/src/index.test.ts +255 -0
  10. package/src/index.ts +175 -0
  11. package/src/test-setup.ts +22 -0
  12. package/src/test-utils.tsx +215 -0
  13. package/src/use-admin-auth.test.ts +175 -0
  14. package/src/use-admin-auth.ts +187 -0
  15. package/src/use-admin-hooks.test.ts +457 -0
  16. package/src/use-admin-hooks.ts +309 -0
  17. package/src/use-auth-config.test.ts +145 -0
  18. package/src/use-auth-config.ts +101 -0
  19. package/src/use-auth.test.ts +313 -0
  20. package/src/use-auth.ts +164 -0
  21. package/src/use-captcha.test.ts +273 -0
  22. package/src/use-captcha.ts +250 -0
  23. package/src/use-client-keys.test.ts +286 -0
  24. package/src/use-client-keys.ts +185 -0
  25. package/src/use-graphql.test.ts +424 -0
  26. package/src/use-graphql.ts +392 -0
  27. package/src/use-query.test.ts +348 -0
  28. package/src/use-query.ts +211 -0
  29. package/src/use-realtime.test.ts +359 -0
  30. package/src/use-realtime.ts +180 -0
  31. package/src/use-saml.test.ts +269 -0
  32. package/src/use-saml.ts +221 -0
  33. package/src/use-storage.test.ts +549 -0
  34. package/src/use-storage.ts +508 -0
  35. package/src/use-table-export.ts +481 -0
  36. package/src/use-users.test.ts +264 -0
  37. package/src/use-users.ts +198 -0
  38. package/tsconfig.json +28 -0
  39. package/tsconfig.tsbuildinfo +1 -0
  40. package/tsup.config.ts +11 -0
  41. package/typedoc.json +33 -0
  42. package/vitest.config.ts +22 -0
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Tests for CAPTCHA hooks
3
+ */
4
+
5
+ import { describe, it, expect, vi } from 'vitest';
6
+ import { renderHook, waitFor, act } from '@testing-library/react';
7
+ import { useCaptchaConfig, useCaptcha, isCaptchaRequiredForEndpoint } from './use-captcha';
8
+ import { createMockClient, createWrapper } from './test-utils';
9
+
10
+ describe('useCaptchaConfig', () => {
11
+ it('should fetch CAPTCHA configuration', async () => {
12
+ const mockConfig = {
13
+ enabled: true,
14
+ provider: 'hcaptcha',
15
+ site_key: 'test-site-key',
16
+ endpoints: ['signup', 'login'],
17
+ };
18
+ const getCaptchaConfigMock = vi.fn().mockResolvedValue({ data: mockConfig, error: null });
19
+
20
+ const client = createMockClient({
21
+ auth: { getCaptchaConfig: getCaptchaConfigMock },
22
+ } as any);
23
+
24
+ const { result } = renderHook(
25
+ () => useCaptchaConfig(),
26
+ { wrapper: createWrapper(client) }
27
+ );
28
+
29
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
30
+ expect(result.current.data).toEqual(mockConfig);
31
+ });
32
+
33
+ it('should throw error on fetch failure', async () => {
34
+ const error = new Error('Failed to fetch');
35
+ const getCaptchaConfigMock = vi.fn().mockResolvedValue({ data: null, error });
36
+
37
+ const client = createMockClient({
38
+ auth: { getCaptchaConfig: getCaptchaConfigMock },
39
+ } as any);
40
+
41
+ const { result } = renderHook(
42
+ () => useCaptchaConfig(),
43
+ { wrapper: createWrapper(client) }
44
+ );
45
+
46
+ await waitFor(() => expect(result.current.isError).toBe(true));
47
+ expect(result.current.error).toBe(error);
48
+ });
49
+ });
50
+
51
+ describe('useCaptcha', () => {
52
+ it('should initialize with empty state', () => {
53
+ const client = createMockClient();
54
+
55
+ const { result } = renderHook(
56
+ () => useCaptcha('hcaptcha'),
57
+ { wrapper: createWrapper(client) }
58
+ );
59
+
60
+ expect(result.current.token).toBeNull();
61
+ expect(result.current.isLoading).toBe(false);
62
+ expect(result.current.error).toBeNull();
63
+ });
64
+
65
+ it('should become ready when provider is set', async () => {
66
+ const client = createMockClient();
67
+
68
+ const { result } = renderHook(
69
+ () => useCaptcha('hcaptcha'),
70
+ { wrapper: createWrapper(client) }
71
+ );
72
+
73
+ await waitFor(() => expect(result.current.isReady).toBe(true));
74
+ });
75
+
76
+ it('should handle onVerify callback', () => {
77
+ const client = createMockClient();
78
+
79
+ const { result } = renderHook(
80
+ () => useCaptcha('hcaptcha'),
81
+ { wrapper: createWrapper(client) }
82
+ );
83
+
84
+ act(() => {
85
+ result.current.onVerify('test-token');
86
+ });
87
+
88
+ expect(result.current.token).toBe('test-token');
89
+ expect(result.current.isLoading).toBe(false);
90
+ });
91
+
92
+ it('should handle onExpire callback', () => {
93
+ const client = createMockClient();
94
+
95
+ const { result } = renderHook(
96
+ () => useCaptcha('hcaptcha'),
97
+ { wrapper: createWrapper(client) }
98
+ );
99
+
100
+ // First verify
101
+ act(() => {
102
+ result.current.onVerify('test-token');
103
+ });
104
+
105
+ expect(result.current.token).toBe('test-token');
106
+
107
+ // Then expire
108
+ act(() => {
109
+ result.current.onExpire();
110
+ });
111
+
112
+ expect(result.current.token).toBeNull();
113
+ expect(result.current.isReady).toBe(true);
114
+ });
115
+
116
+ it('should handle onError callback', () => {
117
+ const client = createMockClient();
118
+
119
+ const { result } = renderHook(
120
+ () => useCaptcha('hcaptcha'),
121
+ { wrapper: createWrapper(client) }
122
+ );
123
+
124
+ const error = new Error('CAPTCHA error');
125
+ act(() => {
126
+ result.current.onError(error);
127
+ });
128
+
129
+ expect(result.current.error).toBe(error);
130
+ expect(result.current.token).toBeNull();
131
+ expect(result.current.isLoading).toBe(false);
132
+ });
133
+
134
+ it('should reset state', () => {
135
+ const client = createMockClient();
136
+
137
+ const { result } = renderHook(
138
+ () => useCaptcha('hcaptcha'),
139
+ { wrapper: createWrapper(client) }
140
+ );
141
+
142
+ // Set a token
143
+ act(() => {
144
+ result.current.onVerify('test-token');
145
+ });
146
+
147
+ expect(result.current.token).toBe('test-token');
148
+
149
+ // Reset
150
+ act(() => {
151
+ result.current.reset();
152
+ });
153
+
154
+ expect(result.current.token).toBeNull();
155
+ expect(result.current.error).toBeNull();
156
+ expect(result.current.isLoading).toBe(false);
157
+ });
158
+
159
+ it('should return existing token in execute', async () => {
160
+ const client = createMockClient();
161
+
162
+ const { result } = renderHook(
163
+ () => useCaptcha('hcaptcha'),
164
+ { wrapper: createWrapper(client) }
165
+ );
166
+
167
+ // Set a token
168
+ act(() => {
169
+ result.current.onVerify('existing-token');
170
+ });
171
+
172
+ // Execute should return existing token
173
+ let token;
174
+ await act(async () => {
175
+ token = await result.current.execute();
176
+ });
177
+
178
+ expect(token).toBe('existing-token');
179
+ });
180
+
181
+ it('should return empty string in execute when no provider', async () => {
182
+ const client = createMockClient();
183
+
184
+ const { result } = renderHook(
185
+ () => useCaptcha(undefined),
186
+ { wrapper: createWrapper(client) }
187
+ );
188
+
189
+ let token;
190
+ await act(async () => {
191
+ token = await result.current.execute();
192
+ });
193
+
194
+ expect(token).toBe('');
195
+ });
196
+
197
+ it('should set loading state during execute', async () => {
198
+ const client = createMockClient();
199
+
200
+ const { result } = renderHook(
201
+ () => useCaptcha('hcaptcha'),
202
+ { wrapper: createWrapper(client) }
203
+ );
204
+
205
+ // Start execute (returns a promise that needs onVerify to resolve)
206
+ let executePromise: Promise<string>;
207
+ act(() => {
208
+ executePromise = result.current.execute();
209
+ });
210
+
211
+ expect(result.current.isLoading).toBe(true);
212
+
213
+ // Resolve by calling onVerify
214
+ act(() => {
215
+ result.current.onVerify('new-token');
216
+ });
217
+
218
+ const token = await executePromise!;
219
+ expect(token).toBe('new-token');
220
+ expect(result.current.isLoading).toBe(false);
221
+ });
222
+
223
+ it('should reject execute on error', async () => {
224
+ const client = createMockClient();
225
+
226
+ const { result } = renderHook(
227
+ () => useCaptcha('hcaptcha'),
228
+ { wrapper: createWrapper(client) }
229
+ );
230
+
231
+ // Start execute
232
+ let executePromise: Promise<string>;
233
+ act(() => {
234
+ executePromise = result.current.execute();
235
+ });
236
+
237
+ // Trigger error
238
+ const error = new Error('CAPTCHA failed');
239
+ act(() => {
240
+ result.current.onError(error);
241
+ });
242
+
243
+ await expect(executePromise!).rejects.toThrow('CAPTCHA failed');
244
+ });
245
+ });
246
+
247
+ describe('isCaptchaRequiredForEndpoint', () => {
248
+ it('should return false when CAPTCHA is disabled', () => {
249
+ const config = { enabled: false, endpoints: ['signup', 'login'] };
250
+ expect(isCaptchaRequiredForEndpoint(config as any, 'signup')).toBe(false);
251
+ });
252
+
253
+ it('should return false when config is undefined', () => {
254
+ expect(isCaptchaRequiredForEndpoint(undefined, 'signup')).toBe(false);
255
+ });
256
+
257
+ it('should return true when endpoint is in list', () => {
258
+ const config = { enabled: true, endpoints: ['signup', 'login'] };
259
+ expect(isCaptchaRequiredForEndpoint(config as any, 'signup')).toBe(true);
260
+ expect(isCaptchaRequiredForEndpoint(config as any, 'login')).toBe(true);
261
+ });
262
+
263
+ it('should return false when endpoint is not in list', () => {
264
+ const config = { enabled: true, endpoints: ['signup'] };
265
+ expect(isCaptchaRequiredForEndpoint(config as any, 'login')).toBe(false);
266
+ expect(isCaptchaRequiredForEndpoint(config as any, 'password_reset')).toBe(false);
267
+ });
268
+
269
+ it('should return false when endpoints is undefined', () => {
270
+ const config = { enabled: true };
271
+ expect(isCaptchaRequiredForEndpoint(config as any, 'signup')).toBe(false);
272
+ });
273
+ });
@@ -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
+ }