@convex-dev/better-auth
Version:
A Better Auth component for Convex.
190 lines • 7.13 kB
JavaScript
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { Component, useCallback, useEffect, useMemo, useState } from "react";
import { Authenticated, ConvexProviderWithAuth, useConvexAuth, useQuery, } from "convex/react";
/**
* A wrapper React component which provides a {@link react.ConvexReactClient}
* authenticated with Better Auth.
*
* @public
*/
export function ConvexBetterAuthProvider({ children, client, authClient, initialToken, }) {
const useBetterAuth = useUseAuthFromBetterAuth(authClient, initialToken);
useEffect(() => {
(async () => {
const url = new URL(window.location?.href);
const token = url.searchParams.get("ott");
if (token) {
const authClientWithCrossDomain = authClient;
url.searchParams.delete("ott");
const result = await authClientWithCrossDomain.crossDomain.oneTimeToken.verify({
token,
});
const session = result.data?.session;
if (session) {
await authClient.getSession({
fetchOptions: {
headers: {
Authorization: `Bearer ${session.token}`,
},
},
});
authClientWithCrossDomain.updateSession();
}
window.history.replaceState({}, "", url);
}
})();
}, [authClient]);
return (_jsx(ConvexProviderWithAuth, { client: client, useAuth: useBetterAuth, children: _jsx(_Fragment, { children: children }) }));
}
let initialTokenUsed = false;
function useUseAuthFromBetterAuth(authClient, initialToken) {
const [cachedToken, setCachedToken] = useState(initialTokenUsed ? (initialToken ?? null) : null);
useEffect(() => {
if (!initialTokenUsed) {
initialTokenUsed = true;
}
}, []);
return useMemo(() => function useAuthFromBetterAuth() {
const { data: session, isPending: isSessionPending } = authClient.useSession();
const sessionId = session?.session?.id;
useEffect(() => {
if (!session && !isSessionPending && cachedToken) {
setCachedToken(null);
}
}, [session, isSessionPending]);
const fetchAccessToken = useCallback(async ({ forceRefreshToken = false, } = {}) => {
if (cachedToken && !forceRefreshToken) {
return cachedToken;
}
try {
const { data } = await authClient.convex.token();
const token = data?.token || null;
setCachedToken(token);
return token;
}
catch {
setCachedToken(null);
return null;
}
},
// Build a new fetchAccessToken to trigger setAuth() whenever the
// session changes.
// eslint-disable-next-line react-hooks/exhaustive-deps
[sessionId]);
return useMemo(() => ({
isLoading: isSessionPending,
isAuthenticated: session !== null,
fetchAccessToken,
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[isSessionPending, sessionId, fetchAccessToken]);
}, [authClient]);
}
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = {};
}
static defaultProps = {
renderFallback: () => null,
};
static getDerivedStateFromError(error) {
return { error };
}
async componentDidCatch(error) {
if (this.props.isAuthError(error)) {
await this.props.onUnauth();
}
}
render() {
if (this.state.error && this.props.isAuthError(this.state.error)) {
return this.props.renderFallback?.();
}
return this.props.children;
}
}
// Subscribe to the session validated user to keep this check reactive to
// actual user auth state at the provider level (rather than just jwt validity state).
const UserSubscription = ({ getAuthUserFn, }) => {
useQuery(getAuthUserFn);
return null;
};
/**
* _Experimental_
*
* A wrapper React component which provides error handling for auth related errors.
* This is typically used to redirect the user to the login page when they are
* unauthenticated, and does so reactively based on the getAuthUserFn query.
*
* @example
* ```ts
* // convex/auth.ts
* export const { getAuthUser } = authComponent.clientApi();
*
* // auth-client.tsx
* import { AuthBoundary } from "@convex-dev/react";
* import { api } from '../../convex/_generated/api'
* import { isAuthError } from '../lib/utils'
*
* export const ClientAuthBoundary = ({ children }: PropsWithChildren) => {
* return (
* <AuthBoundary
* onUnauth={() => redirect("/sign-in")}
* authClient={authClient}
* getAuthUserFn={api.auth.getAuthUser}
* isAuthError={isAuthError}
* >
* <>{children}</>
* </AuthBoundary>
* )
* ```
* @param props.children - Children to render.
* @param props.onUnauth - Function to call when the user is
* unauthenticated. Typically a redirect to the login page.
* @param props.authClient - Better Auth authClient to use.
* @param props.renderFallback - Fallback component to render when the user is
* unauthenticated. Defaults to null. Generally not rendered as error handling
* is typically a redirect.
* @param props.getAuthUserFn - Reference to a Convex query that returns user.
* The component provides a query for this via `export const { getAuthUser } = authComponent.clientApi()`.
* @param props.isAuthError - Function to check if the error is auth related.
*/
export const AuthBoundary = ({ children,
/**
* The function to call when the user is unauthenticated. Typically a redirect
* to the login page.
*/
onUnauth,
/**
* The Better Auth authClient to use.
*/
authClient,
/**
* The fallback to render when the user is unauthenticated. Defaults to null.
* Generally not rendered as error handling is typically a redirect.
*/
renderFallback,
/**
* The function to call to get the auth user.
*/
getAuthUserFn,
/**
* The function to call to check if the error is auth related.
*/
isAuthError, }) => {
const { isAuthenticated, isLoading } = useConvexAuth();
const handleUnauth = useCallback(async () => {
// Auth request that will clear cookies if session is invalid
await authClient.getSession();
await onUnauth();
}, [onUnauth]);
useEffect(() => {
void (async () => {
if (!isLoading && !isAuthenticated) {
await handleUnauth();
}
})();
}, [isLoading, isAuthenticated]);
return (_jsxs(ErrorBoundary, { onUnauth: handleUnauth, isAuthError: isAuthError, renderFallback: renderFallback, children: [_jsx(Authenticated, { children: _jsx(UserSubscription, { getAuthUserFn: getAuthUserFn }) }), children] }));
};
//# sourceMappingURL=index.js.map