@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,313 @@
1
+ /**
2
+ * Tests for authentication hooks
3
+ */
4
+
5
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
6
+ import { renderHook, waitFor, act } from '@testing-library/react';
7
+ import {
8
+ useUser,
9
+ useSession,
10
+ useSignIn,
11
+ useSignUp,
12
+ useSignOut,
13
+ useUpdateUser,
14
+ useAuth,
15
+ } from './use-auth';
16
+ import { createMockClient, createWrapper, createTestQueryClient } from './test-utils';
17
+
18
+ describe('useUser', () => {
19
+ it('should return null when no session', async () => {
20
+ const client = createMockClient({
21
+ auth: {
22
+ getSession: vi.fn().mockResolvedValue({ data: { session: null }, error: null }),
23
+ },
24
+ } as any);
25
+
26
+ const { result } = renderHook(() => useUser(), {
27
+ wrapper: createWrapper(client),
28
+ });
29
+
30
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
31
+ expect(result.current.data).toBeNull();
32
+ });
33
+
34
+ it('should return user when session exists', async () => {
35
+ const mockUser = { id: '1', email: 'test@example.com' };
36
+ const client = createMockClient({
37
+ auth: {
38
+ getSession: vi.fn().mockResolvedValue({ data: { session: { access_token: 'token' } }, error: null }),
39
+ getCurrentUser: vi.fn().mockResolvedValue({ data: { user: mockUser }, error: null }),
40
+ },
41
+ } as any);
42
+
43
+ const { result } = renderHook(() => useUser(), {
44
+ wrapper: createWrapper(client),
45
+ });
46
+
47
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
48
+ expect(result.current.data).toEqual(mockUser);
49
+ });
50
+
51
+ it('should return null when getCurrentUser fails', async () => {
52
+ const client = createMockClient({
53
+ auth: {
54
+ getSession: vi.fn().mockResolvedValue({ data: { session: { access_token: 'token' } }, error: null }),
55
+ getCurrentUser: vi.fn().mockRejectedValue(new Error('Not authenticated')),
56
+ },
57
+ } as any);
58
+
59
+ const { result } = renderHook(() => useUser(), {
60
+ wrapper: createWrapper(client),
61
+ });
62
+
63
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
64
+ expect(result.current.data).toBeNull();
65
+ });
66
+ });
67
+
68
+ describe('useSession', () => {
69
+ it('should return null when no session', async () => {
70
+ const client = createMockClient({
71
+ auth: {
72
+ getSession: vi.fn().mockResolvedValue({ data: { session: null }, error: null }),
73
+ },
74
+ } as any);
75
+
76
+ const { result } = renderHook(() => useSession(), {
77
+ wrapper: createWrapper(client),
78
+ });
79
+
80
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
81
+ expect(result.current.data).toBeNull();
82
+ });
83
+
84
+ it('should return session when exists', async () => {
85
+ const mockSession = { access_token: 'token', refresh_token: 'refresh' };
86
+ const client = createMockClient({
87
+ auth: {
88
+ getSession: vi.fn().mockResolvedValue({ data: { session: mockSession }, error: null }),
89
+ },
90
+ } as any);
91
+
92
+ const { result } = renderHook(() => useSession(), {
93
+ wrapper: createWrapper(client),
94
+ });
95
+
96
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
97
+ expect(result.current.data).toEqual(mockSession);
98
+ });
99
+ });
100
+
101
+ describe('useSignIn', () => {
102
+ it('should call signIn and update cache on success', async () => {
103
+ const mockSession = { user: { id: '1', email: 'test@example.com' }, access_token: 'token' };
104
+ const signInMock = vi.fn().mockResolvedValue(mockSession);
105
+ const client = createMockClient({
106
+ auth: {
107
+ signIn: signInMock,
108
+ },
109
+ } as any);
110
+
111
+ const queryClient = createTestQueryClient();
112
+ const { result } = renderHook(() => useSignIn(), {
113
+ wrapper: createWrapper(client, queryClient),
114
+ });
115
+
116
+ await act(async () => {
117
+ await result.current.mutateAsync({ email: 'test@example.com', password: 'password' });
118
+ });
119
+
120
+ expect(signInMock).toHaveBeenCalledWith({ email: 'test@example.com', password: 'password' });
121
+ expect(queryClient.getQueryData(['fluxbase', 'auth', 'session'])).toEqual(mockSession);
122
+ });
123
+
124
+ it('should handle 2FA required response (no user)', async () => {
125
+ const mockResponse = { mfa_required: true };
126
+ const signInMock = vi.fn().mockResolvedValue(mockResponse);
127
+ const client = createMockClient({
128
+ auth: {
129
+ signIn: signInMock,
130
+ },
131
+ } as any);
132
+
133
+ const queryClient = createTestQueryClient();
134
+ const { result } = renderHook(() => useSignIn(), {
135
+ wrapper: createWrapper(client, queryClient),
136
+ });
137
+
138
+ await act(async () => {
139
+ await result.current.mutateAsync({ email: 'test@example.com', password: 'password' });
140
+ });
141
+
142
+ // User should not be set when 2FA is required
143
+ expect(queryClient.getQueryData(['fluxbase', 'auth', 'user'])).toBeUndefined();
144
+ });
145
+ });
146
+
147
+ describe('useSignUp', () => {
148
+ it('should call signUp and return response on success', async () => {
149
+ const mockResponse = {
150
+ data: {
151
+ user: { id: '1', email: 'test@example.com' },
152
+ session: { access_token: 'token' },
153
+ },
154
+ };
155
+ const signUpMock = vi.fn().mockResolvedValue(mockResponse);
156
+ const client = createMockClient({
157
+ auth: {
158
+ signUp: signUpMock,
159
+ },
160
+ } as any);
161
+
162
+ const { result } = renderHook(() => useSignUp(), {
163
+ wrapper: createWrapper(client),
164
+ });
165
+
166
+ let response;
167
+ await act(async () => {
168
+ response = await result.current.mutateAsync({ email: 'test@example.com', password: 'password' });
169
+ });
170
+
171
+ expect(signUpMock).toHaveBeenCalledWith({ email: 'test@example.com', password: 'password' });
172
+ expect(response).toEqual(mockResponse);
173
+ });
174
+
175
+ it('should handle signup without immediate session', async () => {
176
+ const mockResponse = { data: null };
177
+ const signUpMock = vi.fn().mockResolvedValue(mockResponse);
178
+ const client = createMockClient({
179
+ auth: {
180
+ signUp: signUpMock,
181
+ },
182
+ } as any);
183
+
184
+ const queryClient = createTestQueryClient();
185
+ const { result } = renderHook(() => useSignUp(), {
186
+ wrapper: createWrapper(client, queryClient),
187
+ });
188
+
189
+ await act(async () => {
190
+ await result.current.mutateAsync({ email: 'test@example.com', password: 'password' });
191
+ });
192
+
193
+ // Cache should not be updated when data is null
194
+ expect(queryClient.getQueryData(['fluxbase', 'auth', 'session'])).toBeUndefined();
195
+ });
196
+ });
197
+
198
+ describe('useSignOut', () => {
199
+ it('should call signOut successfully', async () => {
200
+ const signOutMock = vi.fn().mockResolvedValue(undefined);
201
+ const client = createMockClient({
202
+ auth: {
203
+ signOut: signOutMock,
204
+ },
205
+ } as any);
206
+
207
+ const { result } = renderHook(() => useSignOut(), {
208
+ wrapper: createWrapper(client),
209
+ });
210
+
211
+ await act(async () => {
212
+ await result.current.mutateAsync();
213
+ });
214
+
215
+ expect(signOutMock).toHaveBeenCalled();
216
+ });
217
+ });
218
+
219
+ describe('useUpdateUser', () => {
220
+ it('should call updateUser and update cache', async () => {
221
+ const updatedUser = { id: '1', email: 'new@example.com' };
222
+ const updateUserMock = vi.fn().mockResolvedValue(updatedUser);
223
+ const client = createMockClient({
224
+ auth: {
225
+ updateUser: updateUserMock,
226
+ },
227
+ } as any);
228
+
229
+ const queryClient = createTestQueryClient();
230
+ const { result } = renderHook(() => useUpdateUser(), {
231
+ wrapper: createWrapper(client, queryClient),
232
+ });
233
+
234
+ await act(async () => {
235
+ await result.current.mutateAsync({ email: 'new@example.com' });
236
+ });
237
+
238
+ expect(updateUserMock).toHaveBeenCalledWith({ email: 'new@example.com' });
239
+ expect(queryClient.getQueryData(['fluxbase', 'auth', 'user'])).toEqual(updatedUser);
240
+ });
241
+ });
242
+
243
+ describe('useAuth', () => {
244
+ it('should return combined auth state', async () => {
245
+ const mockUser = { id: '1', email: 'test@example.com' };
246
+ const mockSession = { access_token: 'token' };
247
+ const client = createMockClient({
248
+ auth: {
249
+ getSession: vi.fn().mockResolvedValue({ data: { session: mockSession }, error: null }),
250
+ getCurrentUser: vi.fn().mockResolvedValue({ data: { user: mockUser }, error: null }),
251
+ },
252
+ } as any);
253
+
254
+ const { result } = renderHook(() => useAuth(), {
255
+ wrapper: createWrapper(client),
256
+ });
257
+
258
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
259
+
260
+ expect(result.current.user).toEqual(mockUser);
261
+ expect(result.current.session).toEqual(mockSession);
262
+ expect(result.current.isAuthenticated).toBe(true);
263
+ expect(result.current.signIn).toBeDefined();
264
+ expect(result.current.signUp).toBeDefined();
265
+ expect(result.current.signOut).toBeDefined();
266
+ expect(result.current.updateUser).toBeDefined();
267
+ });
268
+
269
+ it('should show loading state initially', () => {
270
+ const client = createMockClient({
271
+ auth: {
272
+ getSession: vi.fn().mockImplementation(() => new Promise(() => {})), // Never resolves
273
+ },
274
+ } as any);
275
+
276
+ const { result } = renderHook(() => useAuth(), {
277
+ wrapper: createWrapper(client),
278
+ });
279
+
280
+ expect(result.current.isLoading).toBe(true);
281
+ });
282
+
283
+ it('should show unauthenticated state when no session', async () => {
284
+ const client = createMockClient({
285
+ auth: {
286
+ getSession: vi.fn().mockResolvedValue({ data: { session: null }, error: null }),
287
+ },
288
+ } as any);
289
+
290
+ const { result } = renderHook(() => useAuth(), {
291
+ wrapper: createWrapper(client),
292
+ });
293
+
294
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
295
+
296
+ expect(result.current.user).toBeNull();
297
+ expect(result.current.session).toBeNull();
298
+ expect(result.current.isAuthenticated).toBe(false);
299
+ });
300
+
301
+ it('should have pending states for mutations', () => {
302
+ const client = createMockClient();
303
+
304
+ const { result } = renderHook(() => useAuth(), {
305
+ wrapper: createWrapper(client),
306
+ });
307
+
308
+ expect(result.current.isSigningIn).toBe(false);
309
+ expect(result.current.isSigningUp).toBe(false);
310
+ expect(result.current.isSigningOut).toBe(false);
311
+ expect(result.current.isUpdating).toBe(false);
312
+ });
313
+ });
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Authentication hooks for Fluxbase SDK
3
+ */
4
+
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";
13
+
14
+ /**
15
+ * Hook to get the current user
16
+ */
17
+ export function useUser() {
18
+ const client = useFluxbaseClient();
19
+
20
+ return useQuery({
21
+ queryKey: ["fluxbase", "auth", "user"],
22
+ queryFn: async () => {
23
+ const { data } = await client.auth.getSession();
24
+ if (!data?.session) {
25
+ return null;
26
+ }
27
+
28
+ try {
29
+ const result = await client.auth.getCurrentUser();
30
+ return result.data?.user ?? null;
31
+ } catch {
32
+ return null;
33
+ }
34
+ },
35
+ staleTime: 1000 * 60 * 5, // 5 minutes
36
+ });
37
+ }
38
+
39
+ /**
40
+ * Hook to get the current session
41
+ */
42
+ export function useSession() {
43
+ const client = useFluxbaseClient();
44
+
45
+ return useQuery<AuthSession | null>({
46
+ queryKey: ["fluxbase", "auth", "session"],
47
+ queryFn: async () => {
48
+ const { data } = await client.auth.getSession();
49
+ return data?.session ?? null;
50
+ },
51
+ staleTime: 1000 * 60 * 5, // 5 minutes
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Hook for signing in
57
+ */
58
+ export function useSignIn() {
59
+ const client = useFluxbaseClient();
60
+ const queryClient = useQueryClient();
61
+
62
+ return useMutation({
63
+ mutationFn: async (credentials: SignInCredentials) => {
64
+ return await client.auth.signIn(credentials);
65
+ },
66
+ onSuccess: (session) => {
67
+ queryClient.setQueryData(["fluxbase", "auth", "session"], session);
68
+ // Only set user if this is a complete auth session (not 2FA required)
69
+ // Check for truthy user value, not just property existence
70
+ if (session && "user" in session && session.user) {
71
+ queryClient.setQueryData(["fluxbase", "auth", "user"], session.user);
72
+ }
73
+ },
74
+ });
75
+ }
76
+
77
+ /**
78
+ * Hook for signing up
79
+ */
80
+ export function useSignUp() {
81
+ const client = useFluxbaseClient();
82
+ const queryClient = useQueryClient();
83
+
84
+ return useMutation({
85
+ mutationFn: async (credentials: SignUpCredentials) => {
86
+ return await client.auth.signUp(credentials);
87
+ },
88
+ onSuccess: (response) => {
89
+ if (response.data) {
90
+ queryClient.setQueryData(
91
+ ["fluxbase", "auth", "session"],
92
+ response.data.session,
93
+ );
94
+ queryClient.setQueryData(
95
+ ["fluxbase", "auth", "user"],
96
+ response.data.user,
97
+ );
98
+ }
99
+ },
100
+ });
101
+ }
102
+
103
+ /**
104
+ * Hook for signing out
105
+ */
106
+ export function useSignOut() {
107
+ const client = useFluxbaseClient();
108
+ const queryClient = useQueryClient();
109
+
110
+ return useMutation({
111
+ mutationFn: async () => {
112
+ await client.auth.signOut();
113
+ },
114
+ onSuccess: () => {
115
+ queryClient.setQueryData(["fluxbase", "auth", "session"], null);
116
+ queryClient.setQueryData(["fluxbase", "auth", "user"], null);
117
+ queryClient.invalidateQueries({ queryKey: ["fluxbase"] });
118
+ },
119
+ });
120
+ }
121
+
122
+ /**
123
+ * Hook for updating the current user
124
+ */
125
+ export function useUpdateUser() {
126
+ const client = useFluxbaseClient();
127
+ const queryClient = useQueryClient();
128
+
129
+ return useMutation({
130
+ mutationFn: async (data: Partial<Pick<User, "email" | "metadata">>) => {
131
+ return await client.auth.updateUser(data);
132
+ },
133
+ onSuccess: (user) => {
134
+ queryClient.setQueryData(["fluxbase", "auth", "user"], user);
135
+ },
136
+ });
137
+ }
138
+
139
+ /**
140
+ * Combined auth hook with all auth state and methods
141
+ */
142
+ export function useAuth() {
143
+ const { data: user, isLoading: isLoadingUser } = useUser();
144
+ const { data: session, isLoading: isLoadingSession } = useSession();
145
+ const signIn = useSignIn();
146
+ const signUp = useSignUp();
147
+ const signOut = useSignOut();
148
+ const updateUser = useUpdateUser();
149
+
150
+ return {
151
+ user,
152
+ session,
153
+ isLoading: isLoadingUser || isLoadingSession,
154
+ isAuthenticated: !!session,
155
+ signIn: signIn.mutateAsync,
156
+ signUp: signUp.mutateAsync,
157
+ signOut: signOut.mutateAsync,
158
+ updateUser: updateUser.mutateAsync,
159
+ isSigningIn: signIn.isPending,
160
+ isSigningUp: signUp.isPending,
161
+ isSigningOut: signOut.isPending,
162
+ isUpdating: updateUser.isPending,
163
+ };
164
+ }