@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,309 @@
1
+ /**
2
+ * Admin Settings and Management Hooks
3
+ *
4
+ * Hooks for managing application settings, system settings, and webhooks.
5
+ */
6
+
7
+ import { useState, useEffect, useCallback } from 'react'
8
+ import { useFluxbaseClient } from './context'
9
+ import type {
10
+ AppSettings,
11
+ UpdateAppSettingsRequest,
12
+ SystemSetting,
13
+ UpdateSystemSettingRequest,
14
+ Webhook,
15
+ CreateWebhookRequest,
16
+ UpdateWebhookRequest
17
+ } from '@fluxbase/sdk'
18
+
19
+ // ============================================================================
20
+ // useAppSettings Hook
21
+ // ============================================================================
22
+
23
+ export interface UseAppSettingsOptions {
24
+ autoFetch?: boolean
25
+ }
26
+
27
+ export interface UseAppSettingsReturn {
28
+ settings: AppSettings | null
29
+ isLoading: boolean
30
+ error: Error | null
31
+ refetch: () => Promise<void>
32
+ updateSettings: (update: UpdateAppSettingsRequest) => Promise<void>
33
+ }
34
+
35
+ /**
36
+ * Hook for managing application settings
37
+ *
38
+ * @example
39
+ * ```tsx
40
+ * function SettingsPanel() {
41
+ * const { settings, isLoading, updateSettings } = useAppSettings({ autoFetch: true })
42
+ *
43
+ * const handleToggleFeature = async (feature: string, enabled: boolean) => {
44
+ * await updateSettings({
45
+ * features: { ...settings?.features, [feature]: enabled }
46
+ * })
47
+ * }
48
+ *
49
+ * return <div>...</div>
50
+ * }
51
+ * ```
52
+ */
53
+ export function useAppSettings(options: UseAppSettingsOptions = {}): UseAppSettingsReturn {
54
+ const { autoFetch = true } = options
55
+ const client = useFluxbaseClient()
56
+
57
+ const [settings, setSettings] = useState<AppSettings | null>(null)
58
+ const [isLoading, setIsLoading] = useState(autoFetch)
59
+ const [error, setError] = useState<Error | null>(null)
60
+
61
+ const fetchSettings = useCallback(async () => {
62
+ try {
63
+ setIsLoading(true)
64
+ setError(null)
65
+ const appSettings = await client.admin.settings.app.get()
66
+ setSettings(appSettings)
67
+ } catch (err) {
68
+ setError(err as Error)
69
+ } finally {
70
+ setIsLoading(false)
71
+ }
72
+ }, [client])
73
+
74
+ const updateSettings = useCallback(
75
+ async (update: UpdateAppSettingsRequest): Promise<void> => {
76
+ await client.admin.settings.app.update(update)
77
+ await fetchSettings()
78
+ },
79
+ [client, fetchSettings]
80
+ )
81
+
82
+ useEffect(() => {
83
+ if (autoFetch) {
84
+ fetchSettings()
85
+ }
86
+ }, [autoFetch, fetchSettings])
87
+
88
+ return {
89
+ settings,
90
+ isLoading,
91
+ error,
92
+ refetch: fetchSettings,
93
+ updateSettings
94
+ }
95
+ }
96
+
97
+ // ============================================================================
98
+ // useSystemSettings Hook
99
+ // ============================================================================
100
+
101
+ export interface UseSystemSettingsOptions {
102
+ autoFetch?: boolean
103
+ }
104
+
105
+ export interface UseSystemSettingsReturn {
106
+ settings: SystemSetting[]
107
+ isLoading: boolean
108
+ error: Error | null
109
+ refetch: () => Promise<void>
110
+ getSetting: (key: string) => SystemSetting | undefined
111
+ updateSetting: (key: string, update: UpdateSystemSettingRequest) => Promise<void>
112
+ deleteSetting: (key: string) => Promise<void>
113
+ }
114
+
115
+ /**
116
+ * Hook for managing system settings (key-value storage)
117
+ *
118
+ * @example
119
+ * ```tsx
120
+ * function SystemSettings() {
121
+ * const { settings, isLoading, updateSetting } = useSystemSettings({ autoFetch: true })
122
+ *
123
+ * const handleUpdateSetting = async (key: string, value: any) => {
124
+ * await updateSetting(key, { value })
125
+ * }
126
+ *
127
+ * return <div>...</div>
128
+ * }
129
+ * ```
130
+ */
131
+ export function useSystemSettings(options: UseSystemSettingsOptions = {}): UseSystemSettingsReturn {
132
+ const { autoFetch = true } = options
133
+ const client = useFluxbaseClient()
134
+
135
+ const [settings, setSettings] = useState<SystemSetting[]>([])
136
+ const [isLoading, setIsLoading] = useState(autoFetch)
137
+ const [error, setError] = useState<Error | null>(null)
138
+
139
+ const fetchSettings = useCallback(async () => {
140
+ try {
141
+ setIsLoading(true)
142
+ setError(null)
143
+ const response = await client.admin.settings.system.list()
144
+ setSettings(response.settings)
145
+ } catch (err) {
146
+ setError(err as Error)
147
+ } finally {
148
+ setIsLoading(false)
149
+ }
150
+ }, [client])
151
+
152
+ const getSetting = useCallback(
153
+ (key: string): SystemSetting | undefined => {
154
+ return settings.find((s) => s.key === key)
155
+ },
156
+ [settings]
157
+ )
158
+
159
+ const updateSetting = useCallback(
160
+ async (key: string, update: UpdateSystemSettingRequest): Promise<void> => {
161
+ await client.admin.settings.system.update(key, update)
162
+ await fetchSettings()
163
+ },
164
+ [client, fetchSettings]
165
+ )
166
+
167
+ const deleteSetting = useCallback(
168
+ async (key: string): Promise<void> => {
169
+ await client.admin.settings.system.delete(key)
170
+ await fetchSettings()
171
+ },
172
+ [client, fetchSettings]
173
+ )
174
+
175
+ useEffect(() => {
176
+ if (autoFetch) {
177
+ fetchSettings()
178
+ }
179
+ }, [autoFetch, fetchSettings])
180
+
181
+ return {
182
+ settings,
183
+ isLoading,
184
+ error,
185
+ refetch: fetchSettings,
186
+ getSetting,
187
+ updateSetting,
188
+ deleteSetting
189
+ }
190
+ }
191
+
192
+ // ============================================================================
193
+ // useWebhooks Hook
194
+ // ============================================================================
195
+
196
+ export interface UseWebhooksOptions {
197
+ autoFetch?: boolean
198
+ refetchInterval?: number
199
+ }
200
+
201
+ export interface UseWebhooksReturn {
202
+ webhooks: Webhook[]
203
+ isLoading: boolean
204
+ error: Error | null
205
+ refetch: () => Promise<void>
206
+ createWebhook: (webhook: CreateWebhookRequest) => Promise<Webhook>
207
+ updateWebhook: (id: string, update: UpdateWebhookRequest) => Promise<Webhook>
208
+ deleteWebhook: (id: string) => Promise<void>
209
+ testWebhook: (id: string) => Promise<void>
210
+ }
211
+
212
+ /**
213
+ * Hook for managing webhooks
214
+ *
215
+ * @example
216
+ * ```tsx
217
+ * function WebhooksManager() {
218
+ * const { webhooks, isLoading, createWebhook, deleteWebhook } = useWebhooks({
219
+ * autoFetch: true
220
+ * })
221
+ *
222
+ * const handleCreate = async () => {
223
+ * await createWebhook({
224
+ * url: 'https://example.com/webhook',
225
+ * events: ['user.created', 'user.updated'],
226
+ * enabled: true
227
+ * })
228
+ * }
229
+ *
230
+ * return <div>...</div>
231
+ * }
232
+ * ```
233
+ */
234
+ export function useWebhooks(options: UseWebhooksOptions = {}): UseWebhooksReturn {
235
+ const { autoFetch = true, refetchInterval = 0 } = options
236
+ const client = useFluxbaseClient()
237
+
238
+ const [webhooks, setWebhooks] = useState<Webhook[]>([])
239
+ const [isLoading, setIsLoading] = useState(autoFetch)
240
+ const [error, setError] = useState<Error | null>(null)
241
+
242
+ const fetchWebhooks = useCallback(async () => {
243
+ try {
244
+ setIsLoading(true)
245
+ setError(null)
246
+ const response = await client.admin.management.webhooks.list()
247
+ setWebhooks(response.webhooks)
248
+ } catch (err) {
249
+ setError(err as Error)
250
+ } finally {
251
+ setIsLoading(false)
252
+ }
253
+ }, [client])
254
+
255
+ const createWebhook = useCallback(
256
+ async (webhook: CreateWebhookRequest): Promise<Webhook> => {
257
+ const created = await client.admin.management.webhooks.create(webhook)
258
+ await fetchWebhooks()
259
+ return created
260
+ },
261
+ [client, fetchWebhooks]
262
+ )
263
+
264
+ const updateWebhook = useCallback(
265
+ async (id: string, update: UpdateWebhookRequest): Promise<Webhook> => {
266
+ const updated = await client.admin.management.webhooks.update(id, update)
267
+ await fetchWebhooks()
268
+ return updated
269
+ },
270
+ [client, fetchWebhooks]
271
+ )
272
+
273
+ const deleteWebhook = useCallback(
274
+ async (id: string): Promise<void> => {
275
+ await client.admin.management.webhooks.delete(id)
276
+ await fetchWebhooks()
277
+ },
278
+ [client, fetchWebhooks]
279
+ )
280
+
281
+ const testWebhook = useCallback(
282
+ async (id: string): Promise<void> => {
283
+ await client.admin.management.webhooks.test(id)
284
+ },
285
+ [client]
286
+ )
287
+
288
+ useEffect(() => {
289
+ if (autoFetch) {
290
+ fetchWebhooks()
291
+ }
292
+
293
+ if (refetchInterval > 0) {
294
+ const interval = setInterval(fetchWebhooks, refetchInterval)
295
+ return () => clearInterval(interval)
296
+ }
297
+ }, [autoFetch, refetchInterval, fetchWebhooks])
298
+
299
+ return {
300
+ webhooks,
301
+ isLoading,
302
+ error,
303
+ refetch: fetchWebhooks,
304
+ createWebhook,
305
+ updateWebhook,
306
+ deleteWebhook,
307
+ testWebhook
308
+ }
309
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Tests for auth configuration hook
3
+ */
4
+
5
+ import { describe, it, expect, vi } from 'vitest';
6
+ import { renderHook, waitFor } from '@testing-library/react';
7
+ import { useAuthConfig } from './use-auth-config';
8
+ import { createMockClient, createWrapper } from './test-utils';
9
+
10
+ describe('useAuthConfig', () => {
11
+ it('should fetch auth configuration', async () => {
12
+ const mockConfig = {
13
+ signup_enabled: true,
14
+ email_verification_required: true,
15
+ magic_link_enabled: true,
16
+ mfa_enabled: true,
17
+ password_min_length: 8,
18
+ password_require_uppercase: true,
19
+ password_require_lowercase: true,
20
+ password_require_number: true,
21
+ password_require_special: false,
22
+ oauth_providers: [
23
+ { provider: 'google', display_name: 'Google', authorize_url: 'https://google.com/oauth' },
24
+ ],
25
+ saml_providers: [],
26
+ captcha: { enabled: false },
27
+ };
28
+
29
+ const getAuthConfigMock = vi.fn().mockResolvedValue({ data: mockConfig, error: null });
30
+
31
+ const client = createMockClient({
32
+ auth: { getAuthConfig: getAuthConfigMock },
33
+ } as any);
34
+
35
+ const { result } = renderHook(
36
+ () => useAuthConfig(),
37
+ { wrapper: createWrapper(client) }
38
+ );
39
+
40
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
41
+ expect(result.current.data).toEqual(mockConfig);
42
+ expect(getAuthConfigMock).toHaveBeenCalled();
43
+ });
44
+
45
+ it('should throw error on fetch failure', async () => {
46
+ const error = new Error('Failed to fetch config');
47
+ const getAuthConfigMock = vi.fn().mockResolvedValue({ data: null, error });
48
+
49
+ const client = createMockClient({
50
+ auth: { getAuthConfig: getAuthConfigMock },
51
+ } as any);
52
+
53
+ const { result } = renderHook(
54
+ () => useAuthConfig(),
55
+ { wrapper: createWrapper(client) }
56
+ );
57
+
58
+ await waitFor(() => expect(result.current.isError).toBe(true));
59
+ expect(result.current.error).toBe(error);
60
+ });
61
+
62
+ it('should have appropriate stale time', async () => {
63
+ const mockConfig = { signup_enabled: true };
64
+ const getAuthConfigMock = vi.fn().mockResolvedValue({ data: mockConfig, error: null });
65
+
66
+ const client = createMockClient({
67
+ auth: { getAuthConfig: getAuthConfigMock },
68
+ } as any);
69
+
70
+ const { result } = renderHook(
71
+ () => useAuthConfig(),
72
+ { wrapper: createWrapper(client) }
73
+ );
74
+
75
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
76
+
77
+ // The config should be cached with a stale time of 5 minutes
78
+ // Verify the data is available
79
+ expect(result.current.data).toEqual(mockConfig);
80
+ });
81
+
82
+ it('should return signup enabled status', async () => {
83
+ const mockConfig = { signup_enabled: false };
84
+ const getAuthConfigMock = vi.fn().mockResolvedValue({ data: mockConfig, error: null });
85
+
86
+ const client = createMockClient({
87
+ auth: { getAuthConfig: getAuthConfigMock },
88
+ } as any);
89
+
90
+ const { result } = renderHook(
91
+ () => useAuthConfig(),
92
+ { wrapper: createWrapper(client) }
93
+ );
94
+
95
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
96
+ expect(result.current.data?.signup_enabled).toBe(false);
97
+ });
98
+
99
+ it('should return OAuth providers', async () => {
100
+ const mockConfig = {
101
+ oauth_providers: [
102
+ { provider: 'google', display_name: 'Google', authorize_url: 'https://google.com' },
103
+ { provider: 'github', display_name: 'GitHub', authorize_url: 'https://github.com' },
104
+ ],
105
+ };
106
+ const getAuthConfigMock = vi.fn().mockResolvedValue({ data: mockConfig, error: null });
107
+
108
+ const client = createMockClient({
109
+ auth: { getAuthConfig: getAuthConfigMock },
110
+ } as any);
111
+
112
+ const { result } = renderHook(
113
+ () => useAuthConfig(),
114
+ { wrapper: createWrapper(client) }
115
+ );
116
+
117
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
118
+ expect(result.current.data?.oauth_providers).toHaveLength(2);
119
+ });
120
+
121
+ it('should return password requirements', async () => {
122
+ const mockConfig = {
123
+ password_min_length: 12,
124
+ password_require_uppercase: true,
125
+ password_require_lowercase: true,
126
+ password_require_number: true,
127
+ password_require_special: true,
128
+ };
129
+ const getAuthConfigMock = vi.fn().mockResolvedValue({ data: mockConfig, error: null });
130
+
131
+ const client = createMockClient({
132
+ auth: { getAuthConfig: getAuthConfigMock },
133
+ } as any);
134
+
135
+ const { result } = renderHook(
136
+ () => useAuthConfig(),
137
+ { wrapper: createWrapper(client) }
138
+ );
139
+
140
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
141
+ expect(result.current.data?.password_min_length).toBe(12);
142
+ expect(result.current.data?.password_require_uppercase).toBe(true);
143
+ expect(result.current.data?.password_require_special).toBe(true);
144
+ });
145
+ });
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Auth configuration hooks for Fluxbase SDK
3
+ *
4
+ * Provides hooks to fetch comprehensive authentication configuration
5
+ * from the server including signup status, OAuth/SAML providers,
6
+ * password requirements, and CAPTCHA settings.
7
+ */
8
+
9
+ import { useQuery } from "@tanstack/react-query";
10
+ import { useFluxbaseClient } from "./context";
11
+ import type { AuthConfig } from "@fluxbase/sdk";
12
+
13
+ /**
14
+ * Hook to get the complete authentication configuration from the server
15
+ *
16
+ * Returns all public auth settings in a single request including:
17
+ * - Signup enabled status
18
+ * - Email verification requirements
19
+ * - Magic link availability
20
+ * - MFA availability
21
+ * - Password requirements (length, complexity)
22
+ * - Available OAuth providers (Google, GitHub, etc.)
23
+ * - Available SAML providers (enterprise SSO)
24
+ * - CAPTCHA configuration
25
+ *
26
+ * Use this to conditionally render UI elements based on server configuration,
27
+ * such as hiding signup forms when signup is disabled or displaying available
28
+ * OAuth provider buttons.
29
+ *
30
+ * @returns Query result with authentication configuration
31
+ *
32
+ * @example
33
+ * ```tsx
34
+ * function AuthPage() {
35
+ * const { data: config, isLoading } = useAuthConfig();
36
+ *
37
+ * if (isLoading) return <Loading />;
38
+ *
39
+ * return (
40
+ * <div>
41
+ * {config?.signup_enabled && (
42
+ * <SignupForm passwordMinLength={config.password_min_length} />
43
+ * )}
44
+ *
45
+ * {config?.oauth_providers.map(provider => (
46
+ * <OAuthButton
47
+ * key={provider.provider}
48
+ * provider={provider.provider}
49
+ * displayName={provider.display_name}
50
+ * authorizeUrl={provider.authorize_url}
51
+ * />
52
+ * ))}
53
+ *
54
+ * {config?.saml_providers.map(provider => (
55
+ * <SAMLButton
56
+ * key={provider.provider}
57
+ * provider={provider.provider}
58
+ * displayName={provider.display_name}
59
+ * />
60
+ * ))}
61
+ * </div>
62
+ * );
63
+ * }
64
+ * ```
65
+ *
66
+ * @example Showing password requirements
67
+ * ```tsx
68
+ * function PasswordInput() {
69
+ * const { data: config } = useAuthConfig();
70
+ *
71
+ * return (
72
+ * <div>
73
+ * <input type="password" minLength={config?.password_min_length} />
74
+ * <ul>
75
+ * <li>Minimum {config?.password_min_length || 8} characters</li>
76
+ * {config?.password_require_uppercase && <li>One uppercase letter</li>}
77
+ * {config?.password_require_lowercase && <li>One lowercase letter</li>}
78
+ * {config?.password_require_number && <li>One number</li>}
79
+ * {config?.password_require_special && <li>One special character</li>}
80
+ * </ul>
81
+ * </div>
82
+ * );
83
+ * }
84
+ * ```
85
+ */
86
+ export function useAuthConfig() {
87
+ const client = useFluxbaseClient();
88
+
89
+ return useQuery<AuthConfig>({
90
+ queryKey: ["fluxbase", "auth", "config"],
91
+ queryFn: async () => {
92
+ const { data, error } = await client.auth.getAuthConfig();
93
+ if (error) {
94
+ throw error;
95
+ }
96
+ return data!;
97
+ },
98
+ staleTime: 1000 * 60 * 5, // Cache for 5 minutes (config changes infrequently)
99
+ gcTime: 1000 * 60 * 60, // Keep in cache for 1 hour
100
+ });
101
+ }