@checkstack/auth-frontend 0.5.15 → 0.5.17
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/CHANGELOG.md +27 -0
- package/package.json +1 -1
- package/src/components/ChangePasswordPage.tsx +2 -2
- package/src/components/ForgotPasswordPage.tsx +2 -2
- package/src/components/LoginPage.tsx +3 -4
- package/src/components/OnboardingPage.tsx +2 -2
- package/src/components/RegisterPage.tsx +2 -2
- package/src/components/ResetPasswordPage.tsx +2 -2
- package/src/hooks/useAccessRules.ts +4 -5
- package/src/index.tsx +5 -6
- package/src/lib/SessionProvider.tsx +64 -0
- package/src/lib/auth-client.ts +9 -23
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# @checkstack/auth-frontend
|
|
2
2
|
|
|
3
|
+
## 0.5.17
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [23c80bc]
|
|
8
|
+
- @checkstack/ui@1.2.0
|
|
9
|
+
|
|
10
|
+
## 0.5.16
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- e01945b: Reduce excessive /api/auth/get-session requests
|
|
15
|
+
|
|
16
|
+
- Enable better-auth's `cookieCache` on the server (5-minute TTL) so repeated session
|
|
17
|
+
checks verify a signed cookie instead of querying the database. Compatible with
|
|
18
|
+
horizontal scaling since validation uses the shared `BETTER_AUTH_SECRET`.
|
|
19
|
+
|
|
20
|
+
- Introduce a `SessionProvider` React context that fetches the session exactly once
|
|
21
|
+
at the top of the component tree. All 7+ components that previously called
|
|
22
|
+
`useSession()` independently now read from this shared context — eliminating
|
|
23
|
+
duplicate HTTP requests on every page load.
|
|
24
|
+
|
|
25
|
+
- Remove the `useAuthClient()` hook which created per-component better-auth client
|
|
26
|
+
instances via `useMemo`, causing separate nanostore atoms and independent fetches.
|
|
27
|
+
All imperative usages (signIn, signUp, resetPassword, etc.) now use the singleton
|
|
28
|
+
`getAuthClientLazy()` instead.
|
|
29
|
+
|
|
3
30
|
## 0.5.15
|
|
4
31
|
|
|
5
32
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
AlertDescription,
|
|
20
20
|
Checkbox,
|
|
21
21
|
} from "@checkstack/ui";
|
|
22
|
-
import {
|
|
22
|
+
import { getAuthClientLazy } from "../lib/auth-client";
|
|
23
23
|
|
|
24
24
|
export const ChangePasswordPage = () => {
|
|
25
25
|
const navigate = useNavigate();
|
|
@@ -32,7 +32,7 @@ export const ChangePasswordPage = () => {
|
|
|
32
32
|
const [error, setError] = useState<string>();
|
|
33
33
|
const [success, setSuccess] = useState(false);
|
|
34
34
|
const [validationErrors, setValidationErrors] = useState<string[]>([]);
|
|
35
|
-
const authClient =
|
|
35
|
+
const authClient = getAuthClientLazy();
|
|
36
36
|
|
|
37
37
|
// Validate new password on change
|
|
38
38
|
useEffect(() => {
|
|
@@ -14,13 +14,13 @@ import {
|
|
|
14
14
|
CardContent,
|
|
15
15
|
CardFooter,
|
|
16
16
|
} from "@checkstack/ui";
|
|
17
|
-
import {
|
|
17
|
+
import { getAuthClientLazy } from "../lib/auth-client";
|
|
18
18
|
|
|
19
19
|
export const ForgotPasswordPage = () => {
|
|
20
20
|
const [email, setEmail] = useState("");
|
|
21
21
|
const [loading, setLoading] = useState(false);
|
|
22
22
|
const [submitted, setSubmitted] = useState(false);
|
|
23
|
-
const authClient =
|
|
23
|
+
const authClient = getAuthClientLazy();
|
|
24
24
|
|
|
25
25
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
26
26
|
e.preventDefault();
|
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
import { authApiRef, EnabledAuthStrategy } from "../api";
|
|
40
40
|
import { useEnabledStrategies } from "../hooks/useEnabledStrategies";
|
|
41
41
|
import { useAccessRules } from "../hooks/useAccessRules";
|
|
42
|
-
import {
|
|
42
|
+
import { getAuthClientLazy } from "../lib/auth-client";
|
|
43
43
|
import { SocialProviderButton } from "./SocialProviderButton";
|
|
44
44
|
import { useEffect } from "react";
|
|
45
45
|
|
|
@@ -417,7 +417,6 @@ export const LoginNavbarAction = () => {
|
|
|
417
417
|
const authApi = useApi(authApiRef);
|
|
418
418
|
const { data: session, isPending } = authApi.useSession();
|
|
419
419
|
const { accessRules, loading: accessRulesLoading } = useAccessRules();
|
|
420
|
-
const authClient = useAuthClient();
|
|
421
420
|
const [hasCredentialAccount, setHasCredentialAccount] =
|
|
422
421
|
useState<boolean>(false);
|
|
423
422
|
const [credentialLoading, setCredentialLoading] = useState(true);
|
|
@@ -427,7 +426,7 @@ export const LoginNavbarAction = () => {
|
|
|
427
426
|
setCredentialLoading(false);
|
|
428
427
|
return;
|
|
429
428
|
}
|
|
430
|
-
|
|
429
|
+
getAuthClientLazy().listAccounts().then((result) => {
|
|
431
430
|
if (result.data) {
|
|
432
431
|
const hasCredential = result.data.some(
|
|
433
432
|
(account) => account.providerId === "credential",
|
|
@@ -436,7 +435,7 @@ export const LoginNavbarAction = () => {
|
|
|
436
435
|
}
|
|
437
436
|
setCredentialLoading(false);
|
|
438
437
|
});
|
|
439
|
-
}, [session?.user
|
|
438
|
+
}, [session?.user]);
|
|
440
439
|
|
|
441
440
|
if (isPending || accessRulesLoading || credentialLoading) {
|
|
442
441
|
return <div className="w-20 h-9 bg-muted animate-pulse rounded-full" />;
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
AlertTitle,
|
|
21
21
|
AlertDescription,
|
|
22
22
|
} from "@checkstack/ui";
|
|
23
|
-
import {
|
|
23
|
+
import { getAuthClientLazy } from "../lib/auth-client";
|
|
24
24
|
|
|
25
25
|
export const OnboardingPage = () => {
|
|
26
26
|
const navigate = useNavigate();
|
|
@@ -36,7 +36,7 @@ export const OnboardingPage = () => {
|
|
|
36
36
|
const authClient = usePluginClient(AuthApi);
|
|
37
37
|
const completeOnboardingMutation =
|
|
38
38
|
authClient.completeOnboarding.useMutation();
|
|
39
|
-
const betterAuthClient =
|
|
39
|
+
const betterAuthClient = getAuthClientLazy();
|
|
40
40
|
|
|
41
41
|
// Check if onboarding is needed
|
|
42
42
|
const { data: onboardingStatus, isLoading: checkingStatus } =
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
} from "@checkstack/ui";
|
|
24
24
|
import { useEnabledStrategies } from "../hooks/useEnabledStrategies";
|
|
25
25
|
import { SocialProviderButton } from "./SocialProviderButton";
|
|
26
|
-
import {
|
|
26
|
+
import { getAuthClientLazy } from "../lib/auth-client";
|
|
27
27
|
|
|
28
28
|
export const RegisterPage = () => {
|
|
29
29
|
const [name, setName] = useState("");
|
|
@@ -35,7 +35,7 @@ export const RegisterPage = () => {
|
|
|
35
35
|
const authApi = useApi(authApiRef);
|
|
36
36
|
const authClient = usePluginClient(AuthApi);
|
|
37
37
|
const { strategies, loading: strategiesLoading } = useEnabledStrategies();
|
|
38
|
-
const authBetterClient =
|
|
38
|
+
const authBetterClient = getAuthClientLazy();
|
|
39
39
|
|
|
40
40
|
// Validate password on change
|
|
41
41
|
useEffect(() => {
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
AlertTitle,
|
|
20
20
|
AlertDescription,
|
|
21
21
|
} from "@checkstack/ui";
|
|
22
|
-
import {
|
|
22
|
+
import { getAuthClientLazy } from "../lib/auth-client";
|
|
23
23
|
|
|
24
24
|
export const ResetPasswordPage = () => {
|
|
25
25
|
const [searchParams] = useSearchParams();
|
|
@@ -32,7 +32,7 @@ export const ResetPasswordPage = () => {
|
|
|
32
32
|
const [error, setError] = useState<string>();
|
|
33
33
|
const [success, setSuccess] = useState(false);
|
|
34
34
|
const [validationErrors, setValidationErrors] = useState<string[]>([]);
|
|
35
|
-
const authClient =
|
|
35
|
+
const authClient = getAuthClientLazy();
|
|
36
36
|
|
|
37
37
|
// Validate password on change
|
|
38
38
|
useEffect(() => {
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import { usePluginClient } from "@checkstack/frontend-api";
|
|
1
|
+
import { useApi, usePluginClient } from "@checkstack/frontend-api";
|
|
2
2
|
import { AuthApi } from "@checkstack/auth-common";
|
|
3
|
-
import {
|
|
3
|
+
import { authApiRef } from "../api";
|
|
4
4
|
|
|
5
5
|
export const useAccessRules = () => {
|
|
6
|
-
const
|
|
6
|
+
const authApi = useApi(authApiRef);
|
|
7
7
|
const authClient = usePluginClient(AuthApi);
|
|
8
|
-
const { data: session, isPending: sessionPending } =
|
|
9
|
-
authBetterClient.useSession();
|
|
8
|
+
const { data: session, isPending: sessionPending } = authApi.useSession();
|
|
10
9
|
|
|
11
10
|
// Query: Fetch access rules (only when user is authenticated)
|
|
12
11
|
const { data, isLoading } = authClient.accessRules.useQuery(
|
package/src/index.tsx
CHANGED
|
@@ -24,6 +24,7 @@ import { ProfilePage } from "./components/ProfilePage";
|
|
|
24
24
|
import { authApiRef, AuthApi, AuthSession } from "./api";
|
|
25
25
|
import { getAuthClientLazy } from "./lib/auth-client";
|
|
26
26
|
import { AuthAccessApi } from "./lib/AuthAccessApi";
|
|
27
|
+
import { useSessionContext } from "./lib/SessionProvider";
|
|
27
28
|
|
|
28
29
|
import { useNavigate } from "react-router-dom";
|
|
29
30
|
import { Settings2, User } from "lucide-react";
|
|
@@ -107,12 +108,7 @@ class BetterAuthApi implements AuthApi {
|
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
useSession() {
|
|
110
|
-
|
|
111
|
-
return {
|
|
112
|
-
data: data as AuthSession | undefined,
|
|
113
|
-
isPending,
|
|
114
|
-
error: error as Error | undefined,
|
|
115
|
-
};
|
|
111
|
+
return useSessionContext();
|
|
116
112
|
}
|
|
117
113
|
}
|
|
118
114
|
|
|
@@ -120,6 +116,9 @@ class BetterAuthApi implements AuthApi {
|
|
|
120
116
|
export { TeamAccessEditor } from "./components/TeamAccessEditor";
|
|
121
117
|
export type { TeamAccessEditorProps } from "./components/TeamAccessEditor";
|
|
122
118
|
|
|
119
|
+
// Re-export SessionProvider for App.tsx to wrap the component tree
|
|
120
|
+
export { SessionProvider } from "./lib/SessionProvider";
|
|
121
|
+
|
|
123
122
|
export const authPlugin = createFrontendPlugin({
|
|
124
123
|
metadata: pluginMetadata,
|
|
125
124
|
apis: [
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React, { createContext, useContext } from "react";
|
|
2
|
+
import { getAuthClientLazy } from "./auth-client";
|
|
3
|
+
import type { AuthSession } from "../api";
|
|
4
|
+
|
|
5
|
+
// =============================================================================
|
|
6
|
+
// SESSION CONTEXT
|
|
7
|
+
// =============================================================================
|
|
8
|
+
|
|
9
|
+
interface SessionContextValue {
|
|
10
|
+
data: AuthSession | undefined;
|
|
11
|
+
isPending: boolean;
|
|
12
|
+
error: Error | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const SessionContext = createContext<SessionContextValue>({
|
|
16
|
+
data: undefined,
|
|
17
|
+
isPending: true,
|
|
18
|
+
error: undefined,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// PROVIDER
|
|
23
|
+
// =============================================================================
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* SessionProvider fetches the session exactly once at the top of the tree
|
|
27
|
+
* and distributes the result via React context.
|
|
28
|
+
*
|
|
29
|
+
* All consumers calling `authApi.useSession()` read from this context
|
|
30
|
+
* instead of each independently fetching `/api/auth/get-session`.
|
|
31
|
+
*
|
|
32
|
+
* Must be placed inside RuntimeConfigProvider (needs baseUrl for the client).
|
|
33
|
+
*/
|
|
34
|
+
export const SessionProvider: React.FC<{ children: React.ReactNode }> = ({
|
|
35
|
+
children,
|
|
36
|
+
}) => {
|
|
37
|
+
const { data, isPending, error } = getAuthClientLazy().useSession();
|
|
38
|
+
|
|
39
|
+
const value: SessionContextValue = {
|
|
40
|
+
data: data as AuthSession | undefined,
|
|
41
|
+
isPending,
|
|
42
|
+
error: error as Error | undefined,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<SessionContext.Provider value={value}>{children}</SessionContext.Provider>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// =============================================================================
|
|
51
|
+
// HOOK
|
|
52
|
+
// =============================================================================
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Read session data from the SessionProvider context.
|
|
56
|
+
*
|
|
57
|
+
* This does NOT trigger a fetch — it reads the value provided by the
|
|
58
|
+
* single SessionProvider at the top of the tree.
|
|
59
|
+
*
|
|
60
|
+
* @throws If called outside a SessionProvider
|
|
61
|
+
*/
|
|
62
|
+
export function useSessionContext(): SessionContextValue {
|
|
63
|
+
return useContext(SessionContext);
|
|
64
|
+
}
|
package/src/lib/auth-client.ts
CHANGED
|
@@ -1,35 +1,20 @@
|
|
|
1
|
-
import { useMemo } from "react";
|
|
2
1
|
import { createAuthClient } from "better-auth/react";
|
|
3
|
-
import {
|
|
4
|
-
useRuntimeConfig,
|
|
5
|
-
getCachedRuntimeConfig,
|
|
6
|
-
} from "@checkstack/frontend-api";
|
|
2
|
+
import { getCachedRuntimeConfig } from "@checkstack/frontend-api";
|
|
7
3
|
|
|
8
4
|
// Cache for lazy-initialized client
|
|
9
5
|
let cachedClient: ReturnType<typeof createAuthClient> | undefined;
|
|
10
6
|
let cachedBaseUrl: string | undefined;
|
|
11
7
|
|
|
12
8
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
|
|
16
|
-
export function useAuthClient() {
|
|
17
|
-
const { baseUrl } = useRuntimeConfig();
|
|
18
|
-
|
|
19
|
-
return useMemo(
|
|
20
|
-
() =>
|
|
21
|
-
createAuthClient({
|
|
22
|
-
baseURL: baseUrl,
|
|
23
|
-
basePath: "/api/auth",
|
|
24
|
-
}),
|
|
25
|
-
[baseUrl]
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Lazy-initialized auth client for class-based APIs.
|
|
9
|
+
* Lazy-initialized auth client for imperative (non-hook) APIs like
|
|
10
|
+
* `getAuthClientLazy().listAccounts()` or `getAuthClientLazy().signOut()`.
|
|
11
|
+
*
|
|
31
12
|
* Uses the cached runtime config from RuntimeConfigProvider.
|
|
32
13
|
*
|
|
14
|
+
* IMPORTANT: Do NOT call `.useSession()` on this client from components.
|
|
15
|
+
* Use `authApi.useSession()` (via `useApi(authApiRef)`) instead — it reads
|
|
16
|
+
* from the shared SessionProvider context and does not trigger extra fetches.
|
|
17
|
+
*
|
|
33
18
|
* Note: This should only be called AFTER RuntimeConfigProvider has loaded.
|
|
34
19
|
* Components rendered inside the provider tree are guaranteed to have config available.
|
|
35
20
|
*/
|
|
@@ -48,3 +33,4 @@ export function getAuthClientLazy(): ReturnType<typeof createAuthClient> {
|
|
|
48
33
|
|
|
49
34
|
return cachedClient;
|
|
50
35
|
}
|
|
36
|
+
|