Skip to content
File

Blob: src/client/lib/auth-client.ts

typescript49 lines
1import { oauthProviderClient } from "@better-auth/oauth-provider/client";
2import { adminClient } from "better-auth/client/plugins";
3import { createAuthClient } from "better-auth/react";
4import type { Session, User } from "better-auth/types";
5import { useEffect, useState } from "react";
6 
7import { hasAdminRole } from "@/shared/role";
8 
9export const authClient = createAuthClient({
10 baseURL: typeof window !== "undefined" ? window.location.origin : undefined,
11 plugins: [adminClient(), oauthProviderClient()],
12});
13 
14export const { useSession } = authClient;
15 
16export interface SessionUser extends User {
17 role?: string | null;
18 preferredUsername?: string | null;
19}
20 
21export interface SessionState {
22 data: { session: Session; user: SessionUser } | null;
23 error: { message?: string } | null;
24 isPending: boolean;
25 refetch: () => void;
26}
27 
28export const isAdminSession = (session: SessionState): boolean =>
29 Boolean(session.data && hasAdminRole(session.data.user.role));
30 
31export const useSessionWithInitialPending = (): SessionState & { isInitialPending: boolean } => {
32 const session = useSession();
33 const resolved = Boolean(session.data) || !session.isPending;
34 const [hasResolved, setHasResolved] = useState(resolved);
35 
36 useEffect(() => {
37 if (!resolved || hasResolved) return;
38 // This is a one-way lifecycle latch, not derived UI state. It prevents
39 // later session refetches from re-entering the initial loading chrome.
40 // eslint-disable-next-line react-hooks/set-state-in-effect
41 setHasResolved(true);
42 }, [resolved, hasResolved]);
43 
44 return {
45 ...session,
46 isInitialPending: !hasResolved && !resolved && session.isPending && !session.data,
47 };
48};