@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,269 @@
1
+ /**
2
+ * Tests for SAML SSO hooks
3
+ */
4
+
5
+ import { describe, it, expect, vi } from 'vitest';
6
+ import { renderHook, waitFor, act } from '@testing-library/react';
7
+ import {
8
+ useSAMLProviders,
9
+ useGetSAMLLoginUrl,
10
+ useSignInWithSAML,
11
+ useHandleSAMLCallback,
12
+ useSAMLMetadataUrl,
13
+ } from './use-saml';
14
+ import { createMockClient, createWrapper, createTestQueryClient } from './test-utils';
15
+
16
+ describe('useSAMLProviders', () => {
17
+ it('should fetch SAML providers', async () => {
18
+ const mockProviders = [
19
+ { id: '1', name: 'okta', display_name: 'Okta' },
20
+ { id: '2', name: 'azure', display_name: 'Azure AD' },
21
+ ];
22
+ const getSAMLProvidersMock = vi.fn().mockResolvedValue({
23
+ data: { providers: mockProviders },
24
+ error: null,
25
+ });
26
+
27
+ const client = createMockClient({
28
+ auth: { getSAMLProviders: getSAMLProvidersMock },
29
+ } as any);
30
+
31
+ const { result } = renderHook(
32
+ () => useSAMLProviders(),
33
+ { wrapper: createWrapper(client) }
34
+ );
35
+
36
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
37
+ expect(result.current.data).toEqual(mockProviders);
38
+ });
39
+
40
+ it('should throw error on fetch failure', async () => {
41
+ const error = new Error('Failed to fetch');
42
+ const getSAMLProvidersMock = vi.fn().mockResolvedValue({ data: null, error });
43
+
44
+ const client = createMockClient({
45
+ auth: { getSAMLProviders: getSAMLProvidersMock },
46
+ } as any);
47
+
48
+ const { result } = renderHook(
49
+ () => useSAMLProviders(),
50
+ { wrapper: createWrapper(client) }
51
+ );
52
+
53
+ await waitFor(() => expect(result.current.isError).toBe(true));
54
+ expect(result.current.error).toBe(error);
55
+ });
56
+ });
57
+
58
+ describe('useGetSAMLLoginUrl', () => {
59
+ it('should get SAML login URL', async () => {
60
+ const mockUrl = 'https://idp.example.com/sso/saml?SAMLRequest=...';
61
+ const getSAMLLoginUrlMock = vi.fn().mockResolvedValue({
62
+ data: { url: mockUrl },
63
+ error: null,
64
+ });
65
+
66
+ const client = createMockClient({
67
+ auth: { getSAMLLoginUrl: getSAMLLoginUrlMock },
68
+ } as any);
69
+
70
+ const { result } = renderHook(
71
+ () => useGetSAMLLoginUrl(),
72
+ { wrapper: createWrapper(client) }
73
+ );
74
+
75
+ await act(async () => {
76
+ await result.current.mutateAsync({
77
+ provider: 'okta',
78
+ options: { redirectUrl: 'https://app.example.com/callback' },
79
+ });
80
+ });
81
+
82
+ expect(getSAMLLoginUrlMock).toHaveBeenCalledWith('okta', {
83
+ redirectUrl: 'https://app.example.com/callback',
84
+ });
85
+ });
86
+
87
+ it('should get SAML login URL without options', async () => {
88
+ const getSAMLLoginUrlMock = vi.fn().mockResolvedValue({
89
+ data: { url: 'https://idp.example.com/sso' },
90
+ error: null,
91
+ });
92
+
93
+ const client = createMockClient({
94
+ auth: { getSAMLLoginUrl: getSAMLLoginUrlMock },
95
+ } as any);
96
+
97
+ const { result } = renderHook(
98
+ () => useGetSAMLLoginUrl(),
99
+ { wrapper: createWrapper(client) }
100
+ );
101
+
102
+ await act(async () => {
103
+ await result.current.mutateAsync({ provider: 'okta' });
104
+ });
105
+
106
+ expect(getSAMLLoginUrlMock).toHaveBeenCalledWith('okta', undefined);
107
+ });
108
+ });
109
+
110
+ describe('useSignInWithSAML', () => {
111
+ it('should initiate SAML sign in', async () => {
112
+ const signInWithSAMLMock = vi.fn().mockResolvedValue({ data: null, error: null });
113
+
114
+ const client = createMockClient({
115
+ auth: { signInWithSAML: signInWithSAMLMock },
116
+ } as any);
117
+
118
+ const { result } = renderHook(
119
+ () => useSignInWithSAML(),
120
+ { wrapper: createWrapper(client) }
121
+ );
122
+
123
+ await act(async () => {
124
+ await result.current.mutateAsync({ provider: 'okta' });
125
+ });
126
+
127
+ expect(signInWithSAMLMock).toHaveBeenCalledWith('okta', undefined);
128
+ });
129
+
130
+ it('should pass options to sign in', async () => {
131
+ const signInWithSAMLMock = vi.fn().mockResolvedValue({ data: null, error: null });
132
+
133
+ const client = createMockClient({
134
+ auth: { signInWithSAML: signInWithSAMLMock },
135
+ } as any);
136
+
137
+ const { result } = renderHook(
138
+ () => useSignInWithSAML(),
139
+ { wrapper: createWrapper(client) }
140
+ );
141
+
142
+ await act(async () => {
143
+ await result.current.mutateAsync({
144
+ provider: 'okta',
145
+ options: { redirectUrl: 'https://app.example.com' },
146
+ });
147
+ });
148
+
149
+ expect(signInWithSAMLMock).toHaveBeenCalledWith('okta', {
150
+ redirectUrl: 'https://app.example.com',
151
+ });
152
+ });
153
+ });
154
+
155
+ describe('useHandleSAMLCallback', () => {
156
+ it('should handle SAML callback successfully', async () => {
157
+ const mockResult = {
158
+ data: {
159
+ user: { id: '1', email: 'user@example.com' },
160
+ session: { access_token: 'token' },
161
+ },
162
+ error: null,
163
+ };
164
+ const handleSAMLCallbackMock = vi.fn().mockResolvedValue(mockResult);
165
+
166
+ const client = createMockClient({
167
+ auth: { handleSAMLCallback: handleSAMLCallbackMock },
168
+ } as any);
169
+
170
+ const { result } = renderHook(
171
+ () => useHandleSAMLCallback(),
172
+ { wrapper: createWrapper(client) }
173
+ );
174
+
175
+ let response;
176
+ await act(async () => {
177
+ response = await result.current.mutateAsync({ samlResponse: 'base64-response' });
178
+ });
179
+
180
+ expect(handleSAMLCallbackMock).toHaveBeenCalledWith('base64-response', undefined);
181
+ expect(response).toEqual(mockResult);
182
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
183
+ });
184
+
185
+ it('should pass provider to callback handler', async () => {
186
+ const handleSAMLCallbackMock = vi.fn().mockResolvedValue({
187
+ data: { user: {}, session: {} },
188
+ error: null,
189
+ });
190
+
191
+ const client = createMockClient({
192
+ auth: { handleSAMLCallback: handleSAMLCallbackMock },
193
+ } as any);
194
+
195
+ const { result } = renderHook(
196
+ () => useHandleSAMLCallback(),
197
+ { wrapper: createWrapper(client) }
198
+ );
199
+
200
+ await act(async () => {
201
+ await result.current.mutateAsync({
202
+ samlResponse: 'base64-response',
203
+ provider: 'okta',
204
+ });
205
+ });
206
+
207
+ expect(handleSAMLCallbackMock).toHaveBeenCalledWith('base64-response', 'okta');
208
+ });
209
+
210
+ it('should not update cache when no data', async () => {
211
+ const handleSAMLCallbackMock = vi.fn().mockResolvedValue({
212
+ data: null,
213
+ error: null,
214
+ });
215
+
216
+ const client = createMockClient({
217
+ auth: { handleSAMLCallback: handleSAMLCallbackMock },
218
+ } as any);
219
+
220
+ const queryClient = createTestQueryClient();
221
+ const { result } = renderHook(
222
+ () => useHandleSAMLCallback(),
223
+ { wrapper: createWrapper(client, queryClient) }
224
+ );
225
+
226
+ await act(async () => {
227
+ await result.current.mutateAsync({ samlResponse: 'base64-response' });
228
+ });
229
+
230
+ // Cache should not be updated
231
+ expect(queryClient.getQueryData(['fluxbase', 'auth', 'session'])).toBeUndefined();
232
+ });
233
+ });
234
+
235
+ describe('useSAMLMetadataUrl', () => {
236
+ it('should return metadata URL generator function', () => {
237
+ const getSAMLMetadataUrlMock = vi.fn().mockReturnValue('https://api.example.com/saml/metadata/okta');
238
+
239
+ const client = createMockClient({
240
+ auth: { getSAMLMetadataUrl: getSAMLMetadataUrlMock },
241
+ } as any);
242
+
243
+ const { result } = renderHook(
244
+ () => useSAMLMetadataUrl(),
245
+ { wrapper: createWrapper(client) }
246
+ );
247
+
248
+ const url = result.current('okta');
249
+
250
+ expect(getSAMLMetadataUrlMock).toHaveBeenCalledWith('okta');
251
+ expect(url).toBe('https://api.example.com/saml/metadata/okta');
252
+ });
253
+
254
+ it('should work with different providers', () => {
255
+ const getSAMLMetadataUrlMock = vi.fn().mockImplementation((provider) => `https://api.example.com/saml/metadata/${provider}`);
256
+
257
+ const client = createMockClient({
258
+ auth: { getSAMLMetadataUrl: getSAMLMetadataUrlMock },
259
+ } as any);
260
+
261
+ const { result } = renderHook(
262
+ () => useSAMLMetadataUrl(),
263
+ { wrapper: createWrapper(client) }
264
+ );
265
+
266
+ expect(result.current('okta')).toBe('https://api.example.com/saml/metadata/okta');
267
+ expect(result.current('azure')).toBe('https://api.example.com/saml/metadata/azure');
268
+ });
269
+ });
@@ -0,0 +1,221 @@
1
+ /**
2
+ * SAML SSO hooks for Fluxbase React SDK
3
+ */
4
+
5
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
6
+ import { useFluxbaseClient } from "./context";
7
+ import type { SAMLLoginOptions, SAMLProvider } from "@fluxbase/sdk";
8
+
9
+ /**
10
+ * Hook to get available SAML SSO providers
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * function SAMLProviderList() {
15
+ * const { data: providers, isLoading } = useSAMLProviders()
16
+ *
17
+ * if (isLoading) return <div>Loading...</div>
18
+ *
19
+ * return (
20
+ * <div>
21
+ * {providers?.map(provider => (
22
+ * <button key={provider.id} onClick={() => signInWithSAML(provider.name)}>
23
+ * Sign in with {provider.name}
24
+ * </button>
25
+ * ))}
26
+ * </div>
27
+ * )
28
+ * }
29
+ * ```
30
+ */
31
+ export function useSAMLProviders() {
32
+ const client = useFluxbaseClient();
33
+
34
+ return useQuery<SAMLProvider[]>({
35
+ queryKey: ["fluxbase", "auth", "saml", "providers"],
36
+ queryFn: async () => {
37
+ const { data, error } = await client.auth.getSAMLProviders();
38
+ if (error) throw error;
39
+ return data.providers;
40
+ },
41
+ staleTime: 1000 * 60 * 5, // 5 minutes - providers don't change often
42
+ });
43
+ }
44
+
45
+ /**
46
+ * Hook to get SAML login URL for a provider
47
+ *
48
+ * This hook returns a function to get the login URL for a specific provider.
49
+ * Use this when you need more control over the redirect behavior.
50
+ *
51
+ * @example
52
+ * ```tsx
53
+ * function SAMLLoginButton({ provider }: { provider: string }) {
54
+ * const getSAMLLoginUrl = useGetSAMLLoginUrl()
55
+ *
56
+ * const handleClick = async () => {
57
+ * const { data, error } = await getSAMLLoginUrl.mutateAsync({
58
+ * provider,
59
+ * options: { redirectUrl: window.location.href }
60
+ * })
61
+ * if (!error) {
62
+ * window.location.href = data.url
63
+ * }
64
+ * }
65
+ *
66
+ * return <button onClick={handleClick}>Login with {provider}</button>
67
+ * }
68
+ * ```
69
+ */
70
+ export function useGetSAMLLoginUrl() {
71
+ const client = useFluxbaseClient();
72
+
73
+ return useMutation({
74
+ mutationFn: async ({
75
+ provider,
76
+ options,
77
+ }: {
78
+ provider: string;
79
+ options?: SAMLLoginOptions;
80
+ }) => {
81
+ return await client.auth.getSAMLLoginUrl(provider, options);
82
+ },
83
+ });
84
+ }
85
+
86
+ /**
87
+ * Hook to initiate SAML login (redirects to IdP)
88
+ *
89
+ * This hook returns a mutation that when called, redirects the user to the
90
+ * SAML Identity Provider for authentication.
91
+ *
92
+ * @example
93
+ * ```tsx
94
+ * function SAMLLoginButton() {
95
+ * const signInWithSAML = useSignInWithSAML()
96
+ *
97
+ * return (
98
+ * <button
99
+ * onClick={() => signInWithSAML.mutate({ provider: 'okta' })}
100
+ * disabled={signInWithSAML.isPending}
101
+ * >
102
+ * {signInWithSAML.isPending ? 'Redirecting...' : 'Sign in with Okta'}
103
+ * </button>
104
+ * )
105
+ * }
106
+ * ```
107
+ */
108
+ export function useSignInWithSAML() {
109
+ const client = useFluxbaseClient();
110
+
111
+ return useMutation({
112
+ mutationFn: async ({
113
+ provider,
114
+ options,
115
+ }: {
116
+ provider: string;
117
+ options?: SAMLLoginOptions;
118
+ }) => {
119
+ return await client.auth.signInWithSAML(provider, options);
120
+ },
121
+ });
122
+ }
123
+
124
+ /**
125
+ * Hook to handle SAML callback after IdP authentication
126
+ *
127
+ * Use this in your SAML callback page to complete the authentication flow.
128
+ *
129
+ * @example
130
+ * ```tsx
131
+ * function SAMLCallbackPage() {
132
+ * const handleCallback = useHandleSAMLCallback()
133
+ * const navigate = useNavigate()
134
+ *
135
+ * useEffect(() => {
136
+ * const params = new URLSearchParams(window.location.search)
137
+ * const samlResponse = params.get('SAMLResponse')
138
+ *
139
+ * if (samlResponse) {
140
+ * handleCallback.mutate(
141
+ * { samlResponse },
142
+ * {
143
+ * onSuccess: () => navigate('/dashboard'),
144
+ * onError: (error) => console.error('SAML login failed:', error)
145
+ * }
146
+ * )
147
+ * }
148
+ * }, [])
149
+ *
150
+ * if (handleCallback.isPending) {
151
+ * return <div>Completing sign in...</div>
152
+ * }
153
+ *
154
+ * if (handleCallback.isError) {
155
+ * return <div>Authentication failed: {handleCallback.error.message}</div>
156
+ * }
157
+ *
158
+ * return null
159
+ * }
160
+ * ```
161
+ */
162
+ export function useHandleSAMLCallback() {
163
+ const client = useFluxbaseClient();
164
+ const queryClient = useQueryClient();
165
+
166
+ return useMutation({
167
+ mutationFn: async ({
168
+ samlResponse,
169
+ provider,
170
+ }: {
171
+ samlResponse: string;
172
+ provider?: string;
173
+ }) => {
174
+ return await client.auth.handleSAMLCallback(samlResponse, provider);
175
+ },
176
+ onSuccess: (result) => {
177
+ if (result.data) {
178
+ // Update auth state in React Query cache
179
+ queryClient.setQueryData(
180
+ ["fluxbase", "auth", "session"],
181
+ result.data.session,
182
+ );
183
+ queryClient.setQueryData(
184
+ ["fluxbase", "auth", "user"],
185
+ result.data.user,
186
+ );
187
+ // Invalidate any dependent queries
188
+ queryClient.invalidateQueries({ queryKey: ["fluxbase"] });
189
+ }
190
+ },
191
+ });
192
+ }
193
+
194
+ /**
195
+ * Hook to get SAML Service Provider metadata URL
196
+ *
197
+ * Returns a function that generates the SP metadata URL for a given provider.
198
+ * Use this URL when configuring your SAML IdP.
199
+ *
200
+ * @example
201
+ * ```tsx
202
+ * function SAMLSetupInfo({ provider }: { provider: string }) {
203
+ * const getSAMLMetadataUrl = useSAMLMetadataUrl()
204
+ * const metadataUrl = getSAMLMetadataUrl(provider)
205
+ *
206
+ * return (
207
+ * <div>
208
+ * <p>SP Metadata URL:</p>
209
+ * <code>{metadataUrl}</code>
210
+ * </div>
211
+ * )
212
+ * }
213
+ * ```
214
+ */
215
+ export function useSAMLMetadataUrl() {
216
+ const client = useFluxbaseClient();
217
+
218
+ return (provider: string): string => {
219
+ return client.auth.getSAMLMetadataUrl(provider);
220
+ };
221
+ }