@rebasepro/auth 0.0.1-canary.eae7889 → 0.1.0
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/dist/api.d.ts +21 -3
- package/dist/components/RebaseLoginView.d.ts +22 -0
- package/dist/index.es.js +127 -78
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +127 -78
- package/dist/index.umd.js.map +1 -1
- package/dist/types.d.ts +8 -2
- package/package.json +4 -4
- package/src/api.ts +49 -9
- package/src/components/AdminViews.tsx +19 -5
- package/src/components/RebaseLoginView.tsx +70 -39
- package/src/hooks/useBackendUserManagement.ts +53 -8
- package/src/hooks/useRebaseAuthController.ts +11 -3
- package/src/types.ts +5 -2
package/dist/types.d.ts
CHANGED
|
@@ -4,8 +4,14 @@ import { AuthController, Role, User } from "@rebasepro/types";
|
|
|
4
4
|
* with additional methods for email/password and Google login
|
|
5
5
|
*/
|
|
6
6
|
export type RebaseAuthController = AuthController & {
|
|
7
|
-
/** Login with Google
|
|
8
|
-
googleLogin:
|
|
7
|
+
/** Login with Google — accepts legacy (token, tokenType) or code-flow payload */
|
|
8
|
+
googleLogin: {
|
|
9
|
+
(token: string, tokenType?: "idToken" | "accessToken"): Promise<void>;
|
|
10
|
+
(payload: {
|
|
11
|
+
code: string;
|
|
12
|
+
redirectUri: string;
|
|
13
|
+
}): Promise<void>;
|
|
14
|
+
};
|
|
9
15
|
/** Generic OAuth login — works with any provider. Posts payload to /auth/{providerId}. */
|
|
10
16
|
oauthLogin: (providerId: string, payload: Record<string, unknown>) => Promise<void>;
|
|
11
17
|
/** Login with email and password */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/auth",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.1.0",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
12
12
|
"source": "src/index.ts",
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@rebasepro/
|
|
15
|
-
"@rebasepro/
|
|
16
|
-
"@rebasepro/
|
|
14
|
+
"@rebasepro/core": "0.1.0",
|
|
15
|
+
"@rebasepro/types": "0.1.0",
|
|
16
|
+
"@rebasepro/ui": "0.1.0"
|
|
17
17
|
},
|
|
18
18
|
"peerDependencies": {
|
|
19
19
|
"react": ">=19.0.0",
|
package/src/api.ts
CHANGED
|
@@ -103,13 +103,33 @@ password })
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
/**
|
|
106
|
-
*
|
|
106
|
+
* Google login payload — one of the three supported flows.
|
|
107
107
|
*/
|
|
108
|
-
export
|
|
108
|
+
export type GoogleLoginPayload =
|
|
109
|
+
| { idToken: string }
|
|
110
|
+
| { accessToken: string }
|
|
111
|
+
| { code: string; redirectUri: string };
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Login with Google.
|
|
115
|
+
*
|
|
116
|
+
* Overload 1 (legacy): `googleLogin("token", "idToken" | "accessToken")`
|
|
117
|
+
* Overload 2 (code flow): `googleLogin({ code, redirectUri })`
|
|
118
|
+
*/
|
|
119
|
+
export async function googleLogin(payload: GoogleLoginPayload): Promise<AuthResponse>;
|
|
120
|
+
export async function googleLogin(token: string, tokenType?: "idToken" | "accessToken"): Promise<AuthResponse>;
|
|
121
|
+
export async function googleLogin(
|
|
122
|
+
tokenOrPayload: string | GoogleLoginPayload,
|
|
123
|
+
tokenType: "idToken" | "accessToken" = "idToken"
|
|
124
|
+
): Promise<AuthResponse> {
|
|
125
|
+
const body = typeof tokenOrPayload === "string"
|
|
126
|
+
? { [tokenType]: tokenOrPayload }
|
|
127
|
+
: tokenOrPayload;
|
|
128
|
+
|
|
109
129
|
const response = await fetchWithHandling(`${baseApiUrl}/api/auth/google`, {
|
|
110
130
|
method: "POST",
|
|
111
131
|
headers: { "Content-Type": "application/json" },
|
|
112
|
-
body: JSON.stringify(
|
|
132
|
+
body: JSON.stringify(body)
|
|
113
133
|
});
|
|
114
134
|
|
|
115
135
|
return handleResponse<AuthResponse>(response);
|
|
@@ -349,17 +369,37 @@ export interface AuthConfigResponse {
|
|
|
349
369
|
enabledProviders?: string[];
|
|
350
370
|
}
|
|
351
371
|
|
|
372
|
+
/**
|
|
373
|
+
* Inflight promise for `fetchAuthConfig` — ensures concurrent callers
|
|
374
|
+
* (e.g. React StrictMode double-mount) reuse the same network request.
|
|
375
|
+
*/
|
|
376
|
+
let authConfigInflight: Promise<AuthConfigResponse> | null = null;
|
|
377
|
+
|
|
352
378
|
/**
|
|
353
379
|
* Fetch auth configuration / status from the backend
|
|
354
|
-
* This is an unauthenticated endpoint used to detect bootstrap mode
|
|
380
|
+
* This is an unauthenticated endpoint used to detect bootstrap mode.
|
|
381
|
+
*
|
|
382
|
+
* Concurrent calls are deduplicated: only one network request is made
|
|
383
|
+
* and all callers share the same promise.
|
|
355
384
|
*/
|
|
356
385
|
export async function fetchAuthConfig(): Promise<AuthConfigResponse> {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
});
|
|
386
|
+
if (authConfigInflight) {
|
|
387
|
+
return authConfigInflight;
|
|
388
|
+
}
|
|
361
389
|
|
|
362
|
-
|
|
390
|
+
authConfigInflight = (async () => {
|
|
391
|
+
const response = await fetchWithHandling(`${baseApiUrl}/api/auth/config`, {
|
|
392
|
+
method: "GET",
|
|
393
|
+
headers: { "Content-Type": "application/json" }
|
|
394
|
+
});
|
|
395
|
+
return handleResponse<AuthConfigResponse>(response);
|
|
396
|
+
})();
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
return await authConfigInflight;
|
|
400
|
+
} finally {
|
|
401
|
+
authConfigInflight = null;
|
|
402
|
+
}
|
|
363
403
|
}
|
|
364
404
|
|
|
365
405
|
export { AuthApiError };
|
|
@@ -26,14 +26,12 @@ export function createUserManagementAdminViews({ userManagement, apiUrl, getAuth
|
|
|
26
26
|
{
|
|
27
27
|
slug: "dev/users",
|
|
28
28
|
name: "CMS Users",
|
|
29
|
-
group: "Admin",
|
|
30
29
|
icon: "face",
|
|
31
30
|
view: <UsersView userManagement={userManagement} apiUrl={apiUrl} getAuthToken={getAuthToken}/>
|
|
32
31
|
},
|
|
33
32
|
{
|
|
34
33
|
slug: "dev/roles",
|
|
35
34
|
name: "Roles",
|
|
36
|
-
group: "Admin",
|
|
37
35
|
icon: "gpp_good",
|
|
38
36
|
view: <RolesView userManagement={userManagement} collections={collections}/>
|
|
39
37
|
}
|
|
@@ -71,6 +69,7 @@ export function UsersView({ userManagement, apiUrl, getAuthToken }: {
|
|
|
71
69
|
getAuthToken: () => Promise<string>;
|
|
72
70
|
}) {
|
|
73
71
|
const { users, roles, saveUser, deleteUser, loading } = userManagement;
|
|
72
|
+
const usersError = 'usersError' in userManagement ? (userManagement as { usersError?: Error }).usersError : undefined;
|
|
74
73
|
const snackbarController = useSnackbarController();
|
|
75
74
|
const { user: loggedInUser } = useAuthController();
|
|
76
75
|
|
|
@@ -219,8 +218,15 @@ message: error instanceof Error ? error.message : "Error deleting user" });
|
|
|
219
218
|
<TableCell colspan={4}>
|
|
220
219
|
<CenteredView className="flex flex-col gap-4 my-8 items-center">
|
|
221
220
|
<Typography variant="label">
|
|
222
|
-
|
|
221
|
+
{usersError
|
|
222
|
+
? "You don't have permission to view users"
|
|
223
|
+
: "There are no users yet"}
|
|
223
224
|
</Typography>
|
|
225
|
+
{usersError && (
|
|
226
|
+
<Typography variant="caption" className="text-surface-500">
|
|
227
|
+
Contact an administrator if you need access to this section.
|
|
228
|
+
</Typography>
|
|
229
|
+
)}
|
|
224
230
|
</CenteredView>
|
|
225
231
|
</TableCell>
|
|
226
232
|
</TableRow>
|
|
@@ -405,6 +411,7 @@ height: "100%" }}>
|
|
|
405
411
|
// ============================================
|
|
406
412
|
export function RolesView({ userManagement, collections = [] }: { userManagement: UserManagement, collections?: EntityCollection[] }) {
|
|
407
413
|
const { roles, saveRole, deleteRole, loading, allowDefaultRolesCreation } = userManagement;
|
|
414
|
+
const rolesError = 'rolesError' in userManagement ? (userManagement as { rolesError?: Error }).rolesError : undefined;
|
|
408
415
|
const snackbarController = useSnackbarController();
|
|
409
416
|
|
|
410
417
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
@@ -516,9 +523,16 @@ isAdmin: false }
|
|
|
516
523
|
<TableCell colspan={4}>
|
|
517
524
|
<CenteredView className="flex flex-col gap-4 my-8 items-center">
|
|
518
525
|
<Typography variant="label">
|
|
519
|
-
|
|
526
|
+
{rolesError
|
|
527
|
+
? "You don't have permission to view roles"
|
|
528
|
+
: "You don\u0026apos;t have any roles yet."}
|
|
520
529
|
</Typography>
|
|
521
|
-
{
|
|
530
|
+
{rolesError && (
|
|
531
|
+
<Typography variant="caption" className="text-surface-500">
|
|
532
|
+
Contact an administrator if you need access to this section.
|
|
533
|
+
</Typography>
|
|
534
|
+
)}
|
|
535
|
+
{!rolesError && allowDefaultRolesCreation && (
|
|
522
536
|
<Button onClick={createDefaultRoles}>
|
|
523
537
|
Create default roles
|
|
524
538
|
</Button>
|
|
@@ -141,7 +141,7 @@ export function RebaseLoginView({
|
|
|
141
141
|
return (
|
|
142
142
|
<div
|
|
143
143
|
className={cls(
|
|
144
|
-
"relative flex items-center justify-center h-screen w-screen p-4 transition-opacity duration-500 bg-white dark:bg-surface-
|
|
144
|
+
"relative flex items-center justify-center h-screen w-screen p-4 transition-opacity duration-500 bg-white dark:bg-surface-900",
|
|
145
145
|
fadeIn ? "opacity-100" : "opacity-0"
|
|
146
146
|
)}>
|
|
147
147
|
|
|
@@ -296,6 +296,37 @@ function LoginButton({
|
|
|
296
296
|
);
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
+
const GoogleIcon = () => (
|
|
300
|
+
<svg viewBox="0 0 24 24" width="20" height="20">
|
|
301
|
+
<path fill="#4285F4"
|
|
302
|
+
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
|
303
|
+
<path fill="#34A853"
|
|
304
|
+
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
|
305
|
+
<path fill="#FBBC05"
|
|
306
|
+
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
|
307
|
+
<path fill="#EA4335"
|
|
308
|
+
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
|
309
|
+
</svg>
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
/** Google Identity Services SDK — injected by the GIS <script> tag. */
|
|
313
|
+
declare global {
|
|
314
|
+
interface Window {
|
|
315
|
+
google?: {
|
|
316
|
+
accounts: {
|
|
317
|
+
oauth2: {
|
|
318
|
+
initCodeClient(config: {
|
|
319
|
+
client_id: string;
|
|
320
|
+
scope: string;
|
|
321
|
+
ux_mode: "popup" | "redirect";
|
|
322
|
+
callback: (response: { code?: string; error?: string }) => void;
|
|
323
|
+
}): { requestCode(): void };
|
|
324
|
+
};
|
|
325
|
+
};
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
299
330
|
function GoogleLoginButton({
|
|
300
331
|
disabled,
|
|
301
332
|
googleClientId,
|
|
@@ -305,52 +336,50 @@ function GoogleLoginButton({
|
|
|
305
336
|
googleClientId: string,
|
|
306
337
|
authController: RebaseAuthController
|
|
307
338
|
}) {
|
|
308
|
-
const
|
|
309
|
-
try {
|
|
310
|
-
const google = (window as unknown as { google?: { accounts: { id: { initialize: (config: { client_id: string; callback: (response: { credential: string }) => void }) => void; prompt: () => void } } } }).google;
|
|
311
|
-
if (!google) {
|
|
312
|
-
console.error("Google Sign-In not loaded");
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
339
|
+
const codeClientRef = useRef<{ requestCode(): void } | null>(null);
|
|
315
340
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
341
|
+
useEffect(() => {
|
|
342
|
+
const google = window.google;
|
|
343
|
+
if (!google || codeClientRef.current) return;
|
|
344
|
+
|
|
345
|
+
codeClientRef.current = google.accounts.oauth2.initCodeClient({
|
|
346
|
+
client_id: googleClientId,
|
|
347
|
+
scope: "openid email profile",
|
|
348
|
+
ux_mode: "popup",
|
|
349
|
+
callback: async (response: { code?: string; error?: string }) => {
|
|
350
|
+
if (response.error || !response.code) {
|
|
351
|
+
console.error("Google login error:", response.error);
|
|
352
|
+
return;
|
|
324
353
|
}
|
|
325
|
-
|
|
354
|
+
try {
|
|
355
|
+
// Send the authorization code to the backend.
|
|
356
|
+
// redirectUri "postmessage" is required when using popup ux_mode.
|
|
357
|
+
await authController.googleLogin({
|
|
358
|
+
code: response.code,
|
|
359
|
+
redirectUri: "postmessage"
|
|
360
|
+
});
|
|
361
|
+
} catch (err: unknown) {
|
|
362
|
+
console.error("Google login error:", err);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
}, [googleClientId, authController]);
|
|
326
367
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
console.error("Google
|
|
368
|
+
const handleClick = () => {
|
|
369
|
+
if (!codeClientRef.current) {
|
|
370
|
+
console.error("Google Sign-In not loaded");
|
|
371
|
+
return;
|
|
330
372
|
}
|
|
373
|
+
codeClientRef.current.requestCode();
|
|
331
374
|
};
|
|
332
375
|
|
|
333
376
|
return (
|
|
334
|
-
<
|
|
377
|
+
<LoginButton
|
|
335
378
|
disabled={disabled}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
<div className="flex items-center justify-center w-full gap-3 py-1">
|
|
341
|
-
<svg viewBox="0 0 24 24" width="20" height="20">
|
|
342
|
-
<path fill="#4285F4"
|
|
343
|
-
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
|
344
|
-
<path fill="#34A853"
|
|
345
|
-
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
|
346
|
-
<path fill="#FBBC05"
|
|
347
|
-
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
|
348
|
-
<path fill="#EA4335"
|
|
349
|
-
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
|
350
|
-
</svg>
|
|
351
|
-
<Typography variant="button">Continue with Google</Typography>
|
|
352
|
-
</div>
|
|
353
|
-
</Button>
|
|
379
|
+
text="Sign in with Google"
|
|
380
|
+
icon={<GoogleIcon/>}
|
|
381
|
+
onClick={handleClick}
|
|
382
|
+
/>
|
|
354
383
|
);
|
|
355
384
|
}
|
|
356
385
|
|
|
@@ -518,6 +547,7 @@ function LoginForm({
|
|
|
518
547
|
<LoadingButton
|
|
519
548
|
type="submit"
|
|
520
549
|
variant="filled"
|
|
550
|
+
color="primary"
|
|
521
551
|
className="w-full mt-1"
|
|
522
552
|
size="large"
|
|
523
553
|
loading={authController.authLoading}
|
|
@@ -681,6 +711,7 @@ function ForgotPasswordForm({
|
|
|
681
711
|
<LoadingButton
|
|
682
712
|
type="submit"
|
|
683
713
|
variant="filled"
|
|
714
|
+
color="primary"
|
|
684
715
|
className="w-full"
|
|
685
716
|
size="large"
|
|
686
717
|
loading={authController.authLoading}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
import { Role, User } from "@rebasepro/types";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -135,6 +135,14 @@ export function useBackendUserManagement(config: BackendUserManagementConfig): U
|
|
|
135
135
|
const [usersError, setUsersError] = useState<Error | undefined>();
|
|
136
136
|
const [rolesError, setRolesError] = useState<Error | undefined>();
|
|
137
137
|
|
|
138
|
+
// Tracks the UID for which roles+users were last successfully loaded.
|
|
139
|
+
// Prevents redundant refetches on React StrictMode double-mounts.
|
|
140
|
+
const lastLoadedUidRef = useRef<string | null>(null);
|
|
141
|
+
|
|
142
|
+
// Ref to hold the latest apiRequest so the initial-load effect doesn't
|
|
143
|
+
// re-trigger every time the callback identity changes.
|
|
144
|
+
const apiRequestRef = useRef<typeof apiRequest | null>(null);
|
|
145
|
+
|
|
138
146
|
/**
|
|
139
147
|
* Make authenticated API request
|
|
140
148
|
*/
|
|
@@ -226,6 +234,9 @@ export function useBackendUserManagement(config: BackendUserManagementConfig): U
|
|
|
226
234
|
throw lastError;
|
|
227
235
|
}, [apiUrl, getAuthToken]);
|
|
228
236
|
|
|
237
|
+
// Keep the ref in sync after every render.
|
|
238
|
+
apiRequestRef.current = apiRequest;
|
|
239
|
+
|
|
229
240
|
/**
|
|
230
241
|
* Load roles from API
|
|
231
242
|
*/
|
|
@@ -260,7 +271,11 @@ export function useBackendUserManagement(config: BackendUserManagementConfig): U
|
|
|
260
271
|
|
|
261
272
|
/**
|
|
262
273
|
* Initial data load - only when user is logged in
|
|
263
|
-
* Load roles first, then admin users
|
|
274
|
+
* Load roles first, then admin users.
|
|
275
|
+
*
|
|
276
|
+
* Dependencies are intentionally limited to `currentUser?.uid` so the
|
|
277
|
+
* effect does NOT re-run when callback identities change. The latest
|
|
278
|
+
* `apiRequest` is read via `apiRequestRef`.
|
|
264
279
|
*/
|
|
265
280
|
useEffect(() => {
|
|
266
281
|
// Don't load if no user is logged in
|
|
@@ -269,27 +284,56 @@ export function useBackendUserManagement(config: BackendUserManagementConfig): U
|
|
|
269
284
|
return;
|
|
270
285
|
}
|
|
271
286
|
|
|
287
|
+
// Skip refetch if we already loaded data for this same UID
|
|
288
|
+
// (e.g. React StrictMode unmounts and re-mounts with the same user).
|
|
289
|
+
if (lastLoadedUidRef.current === currentUser.uid) {
|
|
290
|
+
setLoading(false);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
272
294
|
const abortController = new AbortController();
|
|
273
295
|
|
|
274
296
|
const load = async () => {
|
|
275
297
|
setLoading(true);
|
|
298
|
+
const request = apiRequestRef.current!;
|
|
299
|
+
|
|
276
300
|
// Load roles first
|
|
277
301
|
try {
|
|
278
|
-
const data = await
|
|
302
|
+
const data = await request("/roles", "GET", undefined, 6, abortController.signal);
|
|
279
303
|
setRoles(data.roles.map(convertRole));
|
|
280
304
|
setRolesError(undefined);
|
|
281
305
|
} catch (error: unknown) {
|
|
282
|
-
if (error instanceof Error && error.name
|
|
283
|
-
|
|
284
|
-
|
|
306
|
+
if (error instanceof Error && error.name === "AbortError") return;
|
|
307
|
+
console.error("Failed to load roles:", error);
|
|
308
|
+
setRolesError(error instanceof Error ? error : new Error(String(error)));
|
|
309
|
+
|
|
310
|
+
// If the error is a permission issue (e.g. 403), skip loading
|
|
311
|
+
// users — they will fail with the same error and we'd show a
|
|
312
|
+
// duplicate snackbar / error message.
|
|
313
|
+
const status = (error as { status?: number }).status;
|
|
314
|
+
if (status === 403 || status === 401) {
|
|
315
|
+
setUsersError(error instanceof Error ? error : new Error(String(error)));
|
|
316
|
+
setLoading(false);
|
|
317
|
+
return;
|
|
285
318
|
}
|
|
286
319
|
}
|
|
320
|
+
|
|
287
321
|
// Then load all users if not aborted
|
|
288
322
|
if (!abortController.signal.aborted) {
|
|
289
|
-
|
|
323
|
+
try {
|
|
324
|
+
const data = await request("/users", "GET", undefined, 6, abortController.signal);
|
|
325
|
+
const allUsers: User[] = data.users.map((u: ApiUser) => convertUser(u));
|
|
326
|
+
setUsers(allUsers);
|
|
327
|
+
setUsersError(undefined);
|
|
328
|
+
} catch (error: unknown) {
|
|
329
|
+
if (error instanceof Error && error.name === "AbortError") return;
|
|
330
|
+
console.error("Failed to load users:", error);
|
|
331
|
+
setUsersError(error instanceof Error ? error : new Error(String(error)));
|
|
332
|
+
}
|
|
290
333
|
}
|
|
291
334
|
|
|
292
335
|
if (!abortController.signal.aborted) {
|
|
336
|
+
lastLoadedUidRef.current = currentUser.uid;
|
|
293
337
|
setLoading(false);
|
|
294
338
|
}
|
|
295
339
|
};
|
|
@@ -298,7 +342,8 @@ export function useBackendUserManagement(config: BackendUserManagementConfig): U
|
|
|
298
342
|
return () => {
|
|
299
343
|
abortController.abort();
|
|
300
344
|
};
|
|
301
|
-
|
|
345
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
346
|
+
}, [currentUser?.uid]);
|
|
302
347
|
|
|
303
348
|
/**
|
|
304
349
|
* Search users with server-side pagination.
|
|
@@ -306,6 +306,7 @@ export function useRebaseAuthController(
|
|
|
306
306
|
|
|
307
307
|
// Handle successful authentication
|
|
308
308
|
const handleAuthSuccess = useCallback(async (userInfo: UserInfo, tokens: AuthTokens) => {
|
|
309
|
+
console.log("[Auth] handleAuthSuccess called, user:", userInfo.email, "uid:", userInfo.uid);
|
|
309
310
|
tokensRef.current = tokens;
|
|
310
311
|
let convertedUser = convertToUser(userInfo);
|
|
311
312
|
|
|
@@ -321,11 +322,13 @@ roles: customRoles.map(r => r.id) };
|
|
|
321
322
|
// Save to localStorage for persistence
|
|
322
323
|
saveAuthToStorage(tokens, userInfo);
|
|
323
324
|
|
|
325
|
+
console.log("[Auth] Calling setUser, roles:", convertedUser.roles);
|
|
324
326
|
setUser(convertedUser);
|
|
325
327
|
setAuthError(null);
|
|
326
328
|
setAuthProviderError(null);
|
|
327
329
|
setLoginSkipped(false);
|
|
328
330
|
scheduleTokenRefresh(tokens);
|
|
331
|
+
console.log("[Auth] handleAuthSuccess completed");
|
|
329
332
|
}, [scheduleTokenRefresh, defineRolesFor]);
|
|
330
333
|
|
|
331
334
|
// Email/password login
|
|
@@ -360,13 +363,18 @@ roles: customRoles.map(r => r.id) };
|
|
|
360
363
|
}
|
|
361
364
|
}, [handleAuthSuccess]);
|
|
362
365
|
|
|
363
|
-
// Google login
|
|
364
|
-
const googleLogin = useCallback(async (
|
|
366
|
+
// Google login — supports legacy (token, tokenType) and code-flow payload
|
|
367
|
+
const googleLogin = useCallback(async (
|
|
368
|
+
tokenOrPayload: string | { code: string; redirectUri: string },
|
|
369
|
+
tokenType?: "idToken" | "accessToken"
|
|
370
|
+
) => {
|
|
365
371
|
setAuthLoading(true);
|
|
366
372
|
setAuthProviderError(null);
|
|
367
373
|
|
|
368
374
|
try {
|
|
369
|
-
const response =
|
|
375
|
+
const response = typeof tokenOrPayload === "string"
|
|
376
|
+
? await authApi.googleLogin(tokenOrPayload, tokenType ?? "idToken")
|
|
377
|
+
: await authApi.googleLogin(tokenOrPayload);
|
|
370
378
|
await handleAuthSuccess(response.user, response.tokens);
|
|
371
379
|
} catch (error: unknown) {
|
|
372
380
|
setAuthProviderError(error as Error);
|
package/src/types.ts
CHANGED
|
@@ -5,8 +5,11 @@ import { AuthController, Role, User } from "@rebasepro/types";
|
|
|
5
5
|
* with additional methods for email/password and Google login
|
|
6
6
|
*/
|
|
7
7
|
export type RebaseAuthController = AuthController & {
|
|
8
|
-
/** Login with Google
|
|
9
|
-
googleLogin:
|
|
8
|
+
/** Login with Google — accepts legacy (token, tokenType) or code-flow payload */
|
|
9
|
+
googleLogin: {
|
|
10
|
+
(token: string, tokenType?: "idToken" | "accessToken"): Promise<void>;
|
|
11
|
+
(payload: { code: string; redirectUri: string }): Promise<void>;
|
|
12
|
+
};
|
|
10
13
|
/** Generic OAuth login — works with any provider. Posts payload to /auth/{providerId}. */
|
|
11
14
|
oauthLogin: (providerId: string, payload: Record<string, unknown>) => Promise<void>;
|
|
12
15
|
/** Login with email and password */
|