@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.
- package/.nvmrc +1 -0
- package/README-ADMIN.md +1076 -0
- package/README.md +195 -0
- package/examples/AdminDashboard.tsx +513 -0
- package/examples/README.md +163 -0
- package/package.json +66 -0
- package/src/context.test.tsx +147 -0
- package/src/context.tsx +33 -0
- package/src/index.test.ts +255 -0
- package/src/index.ts +175 -0
- package/src/test-setup.ts +22 -0
- package/src/test-utils.tsx +215 -0
- package/src/use-admin-auth.test.ts +175 -0
- package/src/use-admin-auth.ts +187 -0
- package/src/use-admin-hooks.test.ts +457 -0
- package/src/use-admin-hooks.ts +309 -0
- package/src/use-auth-config.test.ts +145 -0
- package/src/use-auth-config.ts +101 -0
- package/src/use-auth.test.ts +313 -0
- package/src/use-auth.ts +164 -0
- package/src/use-captcha.test.ts +273 -0
- package/src/use-captcha.ts +250 -0
- package/src/use-client-keys.test.ts +286 -0
- package/src/use-client-keys.ts +185 -0
- package/src/use-graphql.test.ts +424 -0
- package/src/use-graphql.ts +392 -0
- package/src/use-query.test.ts +348 -0
- package/src/use-query.ts +211 -0
- package/src/use-realtime.test.ts +359 -0
- package/src/use-realtime.ts +180 -0
- package/src/use-saml.test.ts +269 -0
- package/src/use-saml.ts +221 -0
- package/src/use-storage.test.ts +549 -0
- package/src/use-storage.ts +508 -0
- package/src/use-table-export.ts +481 -0
- package/src/use-users.test.ts +264 -0
- package/src/use-users.ts +198 -0
- package/tsconfig.json +28 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsup.config.ts +11 -0
- package/typedoc.json +33 -0
- 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
|
+
}
|