@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,215 @@
1
+ /**
2
+ * Test utilities for Fluxbase React SDK
3
+ */
4
+
5
+ import React, { ReactElement } from "react";
6
+ import { render, RenderOptions, RenderResult } from "@testing-library/react";
7
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
8
+ import { FluxbaseProvider } from "./context";
9
+ import type { FluxbaseClient } from "@nimbleflux/fluxbase-sdk";
10
+ import { vi } from "vitest";
11
+
12
+ /**
13
+ * Create a mock FluxbaseClient for testing
14
+ */
15
+ export function createMockClient(
16
+ overrides: Partial<FluxbaseClient> = {},
17
+ ): FluxbaseClient {
18
+ return {
19
+ auth: {
20
+ getSession: vi.fn().mockResolvedValue({ data: null, error: null }),
21
+ getCurrentUser: vi.fn().mockResolvedValue({ data: null, error: null }),
22
+ signIn: vi.fn().mockResolvedValue({ user: null, session: null }),
23
+ signUp: vi.fn().mockResolvedValue({ data: null, error: null }),
24
+ signOut: vi.fn().mockResolvedValue(undefined),
25
+ updateUser: vi
26
+ .fn()
27
+ .mockResolvedValue({ id: "1", email: "test@example.com" }),
28
+ getAuthConfig: vi.fn().mockResolvedValue({ data: {}, error: null }),
29
+ getCaptchaConfig: vi.fn().mockResolvedValue({ data: {}, error: null }),
30
+ getSAMLProviders: vi
31
+ .fn()
32
+ .mockResolvedValue({ data: { providers: [] }, error: null }),
33
+ getSAMLLoginUrl: vi
34
+ .fn()
35
+ .mockResolvedValue({ data: { url: "" }, error: null }),
36
+ signInWithSAML: vi.fn().mockResolvedValue({ data: null, error: null }),
37
+ handleSAMLCallback: vi
38
+ .fn()
39
+ .mockResolvedValue({ data: null, error: null }),
40
+ getSAMLMetadataUrl: vi
41
+ .fn()
42
+ .mockReturnValue("http://localhost/saml/metadata"),
43
+ ...overrides.auth,
44
+ },
45
+ from: vi.fn().mockReturnValue({
46
+ select: vi.fn().mockReturnThis(),
47
+ insert: vi.fn().mockResolvedValue({ data: null, error: null }),
48
+ update: vi.fn().mockResolvedValue({ data: null, error: null }),
49
+ upsert: vi.fn().mockResolvedValue({ data: null, error: null }),
50
+ delete: vi.fn().mockResolvedValue({ data: null, error: null }),
51
+ execute: vi.fn().mockResolvedValue({ data: [], error: null }),
52
+ eq: vi.fn().mockReturnThis(),
53
+ }),
54
+ storage: {
55
+ from: vi.fn().mockReturnValue({
56
+ list: vi.fn().mockResolvedValue({ data: [], error: null }),
57
+ upload: vi
58
+ .fn()
59
+ .mockResolvedValue({ data: { path: "test.txt" }, error: null }),
60
+ download: vi.fn().mockResolvedValue({ data: new Blob(), error: null }),
61
+ remove: vi.fn().mockResolvedValue({ data: null, error: null }),
62
+ getPublicUrl: vi
63
+ .fn()
64
+ .mockReturnValue({ data: { publicUrl: "http://localhost/file" } }),
65
+ getTransformUrl: vi
66
+ .fn()
67
+ .mockReturnValue("http://localhost/transform/file"),
68
+ createSignedUrl: vi
69
+ .fn()
70
+ .mockResolvedValue({
71
+ data: { signedUrl: "http://localhost/signed" },
72
+ error: null,
73
+ }),
74
+ move: vi
75
+ .fn()
76
+ .mockResolvedValue({ data: { path: "new.txt" }, error: null }),
77
+ copy: vi
78
+ .fn()
79
+ .mockResolvedValue({ data: { path: "copy.txt" }, error: null }),
80
+ }),
81
+ listBuckets: vi.fn().mockResolvedValue({ data: [], error: null }),
82
+ createBucket: vi.fn().mockResolvedValue({ error: null }),
83
+ deleteBucket: vi.fn().mockResolvedValue({ error: null }),
84
+ ...overrides.storage,
85
+ },
86
+ realtime: {
87
+ channel: vi.fn().mockReturnValue({
88
+ on: vi.fn().mockReturnThis(),
89
+ subscribe: vi.fn().mockReturnThis(),
90
+ unsubscribe: vi.fn(),
91
+ }),
92
+ ...overrides.realtime,
93
+ },
94
+ graphql: {
95
+ execute: vi.fn().mockResolvedValue({ data: null, errors: null }),
96
+ query: vi.fn().mockResolvedValue({ data: null, errors: null }),
97
+ mutation: vi.fn().mockResolvedValue({ data: null, errors: null }),
98
+ introspect: vi
99
+ .fn()
100
+ .mockResolvedValue({ data: { __schema: {} }, errors: null }),
101
+ ...overrides.graphql,
102
+ },
103
+ admin: {
104
+ me: vi.fn().mockResolvedValue({ data: null, error: null }),
105
+ login: vi.fn().mockResolvedValue({ data: null, error: null }),
106
+ listUsers: vi
107
+ .fn()
108
+ .mockResolvedValue({ data: { users: [], total: 0 }, error: null }),
109
+ inviteUser: vi.fn().mockResolvedValue({ data: null, error: null }),
110
+ updateUserRole: vi.fn().mockResolvedValue({ data: null, error: null }),
111
+ deleteUser: vi.fn().mockResolvedValue({ data: null, error: null }),
112
+ resetUserPassword: vi
113
+ .fn()
114
+ .mockResolvedValue({
115
+ data: { message: "Password reset" },
116
+ error: null,
117
+ }),
118
+ settings: {
119
+ app: {
120
+ get: vi.fn().mockResolvedValue({}),
121
+ update: vi.fn().mockResolvedValue({}),
122
+ },
123
+ system: {
124
+ list: vi.fn().mockResolvedValue({ settings: [] }),
125
+ update: vi.fn().mockResolvedValue({}),
126
+ delete: vi.fn().mockResolvedValue({}),
127
+ },
128
+ },
129
+ management: {
130
+ clientKeys: {
131
+ list: vi.fn().mockResolvedValue({ client_keys: [] }),
132
+ create: vi.fn().mockResolvedValue({ key: "new-key", client_key: {} }),
133
+ update: vi.fn().mockResolvedValue({}),
134
+ revoke: vi.fn().mockResolvedValue({}),
135
+ delete: vi.fn().mockResolvedValue({}),
136
+ },
137
+ webhooks: {
138
+ list: vi.fn().mockResolvedValue({ webhooks: [] }),
139
+ create: vi.fn().mockResolvedValue({}),
140
+ update: vi.fn().mockResolvedValue({}),
141
+ delete: vi.fn().mockResolvedValue({}),
142
+ test: vi.fn().mockResolvedValue({}),
143
+ },
144
+ },
145
+ ...overrides.admin,
146
+ },
147
+ ...overrides,
148
+ } as unknown as FluxbaseClient;
149
+ }
150
+
151
+ /**
152
+ * Create a fresh QueryClient for testing
153
+ */
154
+ export function createTestQueryClient(): QueryClient {
155
+ return new QueryClient({
156
+ defaultOptions: {
157
+ queries: {
158
+ retry: false,
159
+ gcTime: 0,
160
+ },
161
+ mutations: {
162
+ retry: false,
163
+ },
164
+ },
165
+ });
166
+ }
167
+
168
+ interface WrapperProps {
169
+ children: React.ReactNode;
170
+ }
171
+
172
+ /**
173
+ * Create a wrapper component with all providers
174
+ */
175
+ export function createWrapper(
176
+ client: FluxbaseClient,
177
+ queryClient?: QueryClient,
178
+ ) {
179
+ const qc = queryClient || createTestQueryClient();
180
+
181
+ return function Wrapper({ children }: WrapperProps) {
182
+ return (
183
+ <QueryClientProvider client={qc}>
184
+ <FluxbaseProvider client={client}>{children}</FluxbaseProvider>
185
+ </QueryClientProvider>
186
+ );
187
+ };
188
+ }
189
+
190
+ /**
191
+ * Custom render function that includes all providers
192
+ */
193
+ export function renderWithProviders(
194
+ ui: ReactElement,
195
+ options?: Omit<RenderOptions, "wrapper"> & {
196
+ client?: FluxbaseClient;
197
+ queryClient?: QueryClient;
198
+ },
199
+ ): RenderResult & { client: FluxbaseClient; queryClient: QueryClient } {
200
+ const {
201
+ client = createMockClient(),
202
+ queryClient,
203
+ ...renderOptions
204
+ } = options || {};
205
+ const wrapper = createWrapper(client, queryClient);
206
+
207
+ return {
208
+ ...render(ui, { wrapper, ...renderOptions }),
209
+ client,
210
+ queryClient: queryClient || createTestQueryClient(),
211
+ };
212
+ }
213
+
214
+ // Re-export testing library utilities
215
+ export * from "@testing-library/react";
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Tests for admin authentication hook
3
+ */
4
+
5
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
6
+ import { renderHook, waitFor, act } from '@testing-library/react';
7
+ import { useAdminAuth } from './use-admin-auth';
8
+ import { createMockClient, createWrapper } from './test-utils';
9
+
10
+ describe('useAdminAuth', () => {
11
+ it('should check auth status on mount when autoCheck is true', async () => {
12
+ const mockUser = { id: '1', email: 'admin@example.com', role: 'admin' };
13
+ const meMock = vi.fn().mockResolvedValue({ data: { user: mockUser }, error: null });
14
+
15
+ const client = createMockClient({
16
+ admin: { me: meMock },
17
+ } as any);
18
+
19
+ const { result } = renderHook(
20
+ () => useAdminAuth({ autoCheck: true }),
21
+ { wrapper: createWrapper(client) }
22
+ );
23
+
24
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
25
+ expect(result.current.user).toEqual(mockUser);
26
+ expect(result.current.isAuthenticated).toBe(true);
27
+ expect(meMock).toHaveBeenCalled();
28
+ });
29
+
30
+ it('should not check auth status when autoCheck is false', async () => {
31
+ const meMock = vi.fn();
32
+
33
+ const client = createMockClient({
34
+ admin: { me: meMock },
35
+ } as any);
36
+
37
+ const { result } = renderHook(
38
+ () => useAdminAuth({ autoCheck: false }),
39
+ { wrapper: createWrapper(client) }
40
+ );
41
+
42
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
43
+ expect(meMock).not.toHaveBeenCalled();
44
+ expect(result.current.isAuthenticated).toBe(false);
45
+ });
46
+
47
+ it('should handle auth check error', async () => {
48
+ const error = new Error('Not authenticated');
49
+ const meMock = vi.fn().mockResolvedValue({ data: null, error });
50
+
51
+ const client = createMockClient({
52
+ admin: { me: meMock },
53
+ } as any);
54
+
55
+ const { result } = renderHook(
56
+ () => useAdminAuth({ autoCheck: true }),
57
+ { wrapper: createWrapper(client) }
58
+ );
59
+
60
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
61
+ expect(result.current.user).toBeNull();
62
+ expect(result.current.isAuthenticated).toBe(false);
63
+ expect(result.current.error).toBe(error);
64
+ });
65
+
66
+ it('should login successfully', async () => {
67
+ const mockUser = { id: '1', email: 'admin@example.com', role: 'admin' };
68
+ const loginMock = vi.fn().mockResolvedValue({
69
+ data: { user: mockUser, token: 'token' },
70
+ error: null,
71
+ });
72
+
73
+ const client = createMockClient({
74
+ admin: { login: loginMock, me: vi.fn().mockResolvedValue({ data: null, error: null }) },
75
+ } as any);
76
+
77
+ const { result } = renderHook(
78
+ () => useAdminAuth({ autoCheck: false }),
79
+ { wrapper: createWrapper(client) }
80
+ );
81
+
82
+ await act(async () => {
83
+ await result.current.login('admin@example.com', 'password');
84
+ });
85
+
86
+ expect(loginMock).toHaveBeenCalledWith({ email: 'admin@example.com', password: 'password' });
87
+ expect(result.current.user).toEqual(mockUser);
88
+ expect(result.current.isAuthenticated).toBe(true);
89
+ });
90
+
91
+ it('should handle login error', async () => {
92
+ const error = new Error('Invalid credentials');
93
+ const loginMock = vi.fn().mockResolvedValue({ data: null, error });
94
+
95
+ const client = createMockClient({
96
+ admin: { login: loginMock, me: vi.fn().mockResolvedValue({ data: null, error: null }) },
97
+ } as any);
98
+
99
+ const { result } = renderHook(
100
+ () => useAdminAuth({ autoCheck: false }),
101
+ { wrapper: createWrapper(client) }
102
+ );
103
+
104
+ await expect(act(async () => {
105
+ await result.current.login('admin@example.com', 'wrong-password');
106
+ })).rejects.toThrow();
107
+
108
+ // User should remain null after failed login
109
+ expect(result.current.user).toBeNull();
110
+ expect(result.current.isAuthenticated).toBe(false);
111
+ });
112
+
113
+ it('should logout', async () => {
114
+ const mockUser = { id: '1', email: 'admin@example.com', role: 'admin' };
115
+ const meMock = vi.fn().mockResolvedValue({ data: { user: mockUser }, error: null });
116
+
117
+ const client = createMockClient({
118
+ admin: { me: meMock },
119
+ } as any);
120
+
121
+ const { result } = renderHook(
122
+ () => useAdminAuth({ autoCheck: true }),
123
+ { wrapper: createWrapper(client) }
124
+ );
125
+
126
+ await waitFor(() => expect(result.current.isAuthenticated).toBe(true));
127
+
128
+ await act(async () => {
129
+ await result.current.logout();
130
+ });
131
+
132
+ expect(result.current.user).toBeNull();
133
+ expect(result.current.isAuthenticated).toBe(false);
134
+ });
135
+
136
+ it('should refresh user info', async () => {
137
+ const mockUser = { id: '1', email: 'admin@example.com', role: 'admin' };
138
+ const meMock = vi.fn().mockResolvedValue({ data: { user: mockUser }, error: null });
139
+
140
+ const client = createMockClient({
141
+ admin: { me: meMock },
142
+ } as any);
143
+
144
+ const { result } = renderHook(
145
+ () => useAdminAuth({ autoCheck: false }),
146
+ { wrapper: createWrapper(client) }
147
+ );
148
+
149
+ await act(async () => {
150
+ await result.current.refresh();
151
+ });
152
+
153
+ expect(meMock).toHaveBeenCalledTimes(1);
154
+ expect(result.current.user).toEqual(mockUser);
155
+ });
156
+
157
+ it('should show loading state during operations', async () => {
158
+ const meMock = vi.fn().mockImplementation(() => new Promise((resolve) => {
159
+ setTimeout(() => resolve({ data: { user: {} }, error: null }), 100);
160
+ }));
161
+
162
+ const client = createMockClient({
163
+ admin: { me: meMock },
164
+ } as any);
165
+
166
+ const { result } = renderHook(
167
+ () => useAdminAuth({ autoCheck: true }),
168
+ { wrapper: createWrapper(client) }
169
+ );
170
+
171
+ expect(result.current.isLoading).toBe(true);
172
+
173
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
174
+ });
175
+ });
@@ -0,0 +1,187 @@
1
+ import { useState, useEffect, useCallback } from "react";
2
+ import { useFluxbaseClient } from "./context";
3
+ import type { AdminAuthResponse, DataResponse } from "@fluxbase/sdk";
4
+
5
+ /**
6
+ * Simplified admin user type returned by authentication
7
+ */
8
+ export interface AdminUser {
9
+ id: string;
10
+ email: string;
11
+ role: string;
12
+ }
13
+
14
+ export interface UseAdminAuthOptions {
15
+ /**
16
+ * Automatically check authentication status on mount
17
+ * @default true
18
+ */
19
+ autoCheck?: boolean;
20
+ }
21
+
22
+ export interface UseAdminAuthReturn {
23
+ /**
24
+ * Current admin user if authenticated
25
+ */
26
+ user: AdminUser | null;
27
+
28
+ /**
29
+ * Whether the admin is authenticated
30
+ */
31
+ isAuthenticated: boolean;
32
+
33
+ /**
34
+ * Whether the authentication check is in progress
35
+ */
36
+ isLoading: boolean;
37
+
38
+ /**
39
+ * Any error that occurred during authentication
40
+ */
41
+ error: Error | null;
42
+
43
+ /**
44
+ * Login as admin
45
+ */
46
+ login: (email: string, password: string) => Promise<AdminAuthResponse>;
47
+
48
+ /**
49
+ * Logout admin
50
+ */
51
+ logout: () => Promise<void>;
52
+
53
+ /**
54
+ * Refresh admin user info
55
+ */
56
+ refresh: () => Promise<void>;
57
+ }
58
+
59
+ /**
60
+ * Hook for admin authentication
61
+ *
62
+ * Manages admin login state, authentication checks, and user info.
63
+ *
64
+ * @example
65
+ * ```tsx
66
+ * function AdminLogin() {
67
+ * const { user, isAuthenticated, isLoading, login, logout } = useAdminAuth()
68
+ *
69
+ * const handleLogin = async (e: React.FormEvent) => {
70
+ * e.preventDefault()
71
+ * await login(email, password)
72
+ * }
73
+ *
74
+ * if (isLoading) return <div>Loading...</div>
75
+ * if (isAuthenticated) return <div>Welcome {user?.email}</div>
76
+ *
77
+ * return <form onSubmit={handleLogin}>...</form>
78
+ * }
79
+ * ```
80
+ */
81
+ export function useAdminAuth(
82
+ options: UseAdminAuthOptions = {},
83
+ ): UseAdminAuthReturn {
84
+ const { autoCheck = true } = options;
85
+ const client = useFluxbaseClient();
86
+
87
+ const [user, setUser] = useState<AdminUser | null>(null);
88
+ const [isLoading, setIsLoading] = useState(autoCheck);
89
+ const [error, setError] = useState<Error | null>(null);
90
+
91
+ /**
92
+ * Check current authentication status
93
+ */
94
+ const checkAuth = useCallback(async () => {
95
+ try {
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);
103
+ } catch (err) {
104
+ setUser(null);
105
+ setError(err as Error);
106
+ } finally {
107
+ setIsLoading(false);
108
+ }
109
+ }, [client]);
110
+
111
+ /**
112
+ * Login as admin
113
+ */
114
+ const login = useCallback(
115
+ async (email: string, password: string): Promise<AdminAuthResponse> => {
116
+ try {
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!;
128
+ } catch (err) {
129
+ setError(err as Error);
130
+ throw err;
131
+ } finally {
132
+ setIsLoading(false);
133
+ }
134
+ },
135
+ [client],
136
+ );
137
+
138
+ /**
139
+ * Logout admin
140
+ *
141
+ * WARNING: Currently only clears local state. The server-side session/token
142
+ * remains valid until it expires. This should call a logout endpoint to
143
+ * invalidate the session on the server for proper security.
144
+ */
145
+ const logout = useCallback(async (): Promise<void> => {
146
+ try {
147
+ setIsLoading(true);
148
+ setError(null);
149
+
150
+ // TODO: Call server-side logout endpoint when available
151
+ // This is a security concern - the token remains valid on the server
152
+ // await client.admin.logout();
153
+
154
+ // Clear local user state
155
+ setUser(null);
156
+ } catch (err) {
157
+ setError(err as Error);
158
+ throw err;
159
+ } finally {
160
+ setIsLoading(false);
161
+ }
162
+ }, []);
163
+
164
+ /**
165
+ * Refresh admin user info
166
+ */
167
+ const refresh = useCallback(async (): Promise<void> => {
168
+ await checkAuth();
169
+ }, [checkAuth]);
170
+
171
+ // Auto-check authentication on mount
172
+ useEffect(() => {
173
+ if (autoCheck) {
174
+ checkAuth();
175
+ }
176
+ }, [autoCheck, checkAuth]);
177
+
178
+ return {
179
+ user,
180
+ isAuthenticated: user !== null,
181
+ isLoading,
182
+ error,
183
+ login,
184
+ logout,
185
+ refresh,
186
+ };
187
+ }