import * as React from 'react' import { type AuthClientConfig, ClientSessionError, fetchData, now, parseUrl, useOnline, type SessionContextValue, type SessionProviderProps, type GetSessionParams, type UseSessionOptions, type LiteralUnion, type SignInOptions, type SignInAuthorizationParams, type SignInResponse, type ClientSafeProvider, type SignOutParams, type SignOutResponse, } from './client' import type { LoggerInstance, Session } from '@auth/core/types' import { useContext, useEffect, useMemo } from 'react' import type { BuiltInProviderType, RedirectableProviderType } from '@auth/core/providers' const logger: LoggerInstance = { debug: console.debug, error: console.error, warn: console.warn, } class AuthConfigManager { private static instance: AuthConfigManager | null = null private config: AuthClientConfig private constructor() { this.config = this.createDefaultConfig() } private createDefaultConfig(): AuthClientConfig { return { baseUrl: typeof window !== 'undefined' ? parseUrl(window.location.origin).origin : '', basePath: typeof window !== 'undefined' ? parseUrl(window.location.origin).path : '/api/auth', credentials: 'same-origin', lastSync: 0, session: null, fetchSession: async () => void 0, } } static getInstance(): AuthConfigManager { if (!AuthConfigManager.instance) { AuthConfigManager.instance = new AuthConfigManager() } return AuthConfigManager.instance } setConfig(userConfig: Partial): void { this.config = { ...this.config, ...userConfig } } getConfig(): AuthClientConfig { return this.config } initializeConfig(hasInitialSession: boolean): void { this.config.lastSync = hasInitialSession ? now() : 0 } } export const authConfigManager = AuthConfigManager.getInstance() export const SessionContext = React.createContext(undefined) function useInitializeSession(hasInitialSession: boolean, initialSession: Session | null) { const authConfig = authConfigManager.getConfig() const [session, setSession] = React.useState(initialSession) const [loading, setLoading] = React.useState(!hasInitialSession) useEffect(() => { authConfig.fetchSession = async ({ event } = {}) => { try { const isStorageEvent = event === 'storage' if (isStorageEvent || !authConfig.session) { authConfig.lastSync = now() authConfig.session = await getSession() setSession(authConfig.session) return } if (!event || !authConfig.session || now() < authConfig.lastSync) { return } authConfig.lastSync = now() authConfig.session = await getSession() setSession(authConfig.session) } catch (error) { logger.error(new ClientSessionError((error as Error).message, error as any)) } finally { setLoading(false) } } authConfig.fetchSession() return () => { authConfig.lastSync = 0 authConfig.session = null authConfig.fetchSession = async () => void 0 } }, []) return { session, setSession, loading, setLoading } } function useVisibilityChangeEventListener( authConfig: AuthClientConfig, refetchOnWindowFocus: boolean ) { useEffect(() => { const abortController = new AbortController() const handleVisibilityChange = () => { if (refetchOnWindowFocus && document.visibilityState === 'visible') { authConfig.fetchSession({ event: 'visibilitychange' }) } } document.addEventListener('visibilitychange', handleVisibilityChange, { signal: abortController.signal, }) return () => abortController.abort() }, [refetchOnWindowFocus]) } function useRefetchInterval( authConfig: AuthClientConfig, refetchInterval?: number, shouldRefetch?: boolean ) { useEffect(() => { if (refetchInterval && shouldRefetch) { const intervalId = setInterval(() => { if (authConfig.session) { authConfig.fetchSession({ event: 'poll' }) } }, refetchInterval * 1000) return () => clearInterval(intervalId) } }, [refetchInterval, shouldRefetch]) } export async function getSession(params?: GetSessionParams) { const { baseUrl, basePath, credentials } = authConfigManager.getConfig() const session = await fetchData( 'session', { baseUrl, basePath, credentials, }, logger, params ) return session } export async function getCsrfToken() { const { baseUrl, basePath, credentials } = authConfigManager.getConfig() const response = await fetchData<{ csrfToken: string }>( 'csrf', { baseUrl, basePath, credentials, }, logger ) return response?.csrfToken ?? '' } export function SessionProvider(props: SessionProviderProps) { if (!SessionContext) { throw new Error('React Context is unavailable in Server Components') } const { children, refetchInterval, refetchWhenOffline = true } = props const authConfig = authConfigManager.getConfig() const hasInitialSession = !!props.session authConfigManager.initializeConfig(hasInitialSession) const { session, setSession, loading, setLoading } = useInitializeSession( hasInitialSession, props.session ?? null ) useVisibilityChangeEventListener(authConfig, props.refetchOnWindowFocus ?? true) const isOnline = useOnline() const shouldRefetch = refetchWhenOffline || isOnline useRefetchInterval(authConfig, refetchInterval, shouldRefetch) const contextValue: SessionContextValue = useMemo( () => ({ data: session, status: loading ? 'loading' : session ? 'authenticated' : 'unauthenticated', update: async (data) => { if (loading || !session) { return } setLoading(true) const updatedSession = await fetchData( 'session', authConfig, logger, data ? { body: { csrfToken: await getCsrfToken(), data } } : undefined ) setLoading(false) if (updatedSession) { setSession(updatedSession) } return updatedSession }, } as SessionContextValue), [session, loading, setSession] ) return {children} } export function useSession( options?: UseSessionOptions ): SessionContextValue { if (!SessionContext) { throw new Error('React Context is unavailable in Server Components') } const config = authConfigManager.getConfig() const session = useContext(SessionContext) const { required, onUnauthenticated } = options ?? {} const requiredAndNotLoading = required && session?.status === 'unauthenticated' useEffect(() => { if (requiredAndNotLoading) { const url = `${config.baseUrl}${config.basePath}/signin?${new URLSearchParams({ error: 'SessionRequired', callbackUrl: window.location.href, })}` if (onUnauthenticated) { onUnauthenticated() } else { window.location.href = url } } }, [requiredAndNotLoading, onUnauthenticated]) if (requiredAndNotLoading) { return { data: session?.data, update: session?.update, status: 'loading', } } return session as SessionContextValue } type ProvidersType = Record, ClientSafeProvider> export async function getProviders() { return fetchData('providers', authConfigManager.getConfig(), logger) } export async function signIn

( provider?: LiteralUnion< P extends RedirectableProviderType ? P | BuiltInProviderType : BuiltInProviderType >, options: SignInOptions = {}, authorizationParams: SignInAuthorizationParams = {} ): Promise

{ const { callbackUrl = window.location.href, redirect = true, ...opts } = options const config = authConfigManager.getConfig() const href = `${config.baseUrl}${config.basePath}` const providers = await getProviders() if (!providers) { window.location.href = `${href}/error` return } if (!provider || !(provider in providers)) { window.location.href = `${href}/signin?${new URLSearchParams({ callbackUrl })}` return } const isCredentials = providers[provider].type === 'credentials' const isEmail = providers[provider].type === 'email' const signInUrl = `${href}/${isCredentials ? 'callback' : 'signin'}/${provider}` const csrfToken = await getCsrfToken() const res = await fetch(`${signInUrl}?${new URLSearchParams(authorizationParams)}`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Auth-Return-Redirect': '1', }, body: new URLSearchParams({ ...opts, csrfToken, callbackUrl }), credentials: config.credentials, }) const data = (await res.json()) as { url: string } if (redirect) { const url = data.url ?? callbackUrl window.location.href = url if (url.includes('#')) { window.location.reload() } return } const error = new URL(data.url).searchParams.get('error') if (res.ok) { await config.fetchSession?.({ event: 'storage' }) } return { error, status: res.status, ok: res.ok, url: error ? null : data.url, } as P extends RedirectableProviderType ? SignInResponse : undefined } export async function signOut( options?: SignOutParams ): Promise { const { callbackUrl = window.location.href, redirect = true } = options ?? {} const config = authConfigManager.getConfig() const csrfToken = await getCsrfToken() const res = await fetch(`${config.baseUrl}${config.basePath}/signout`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Auth-Return-Redirect': '1', }, body: new URLSearchParams({ csrfToken, callbackUrl }), credentials: config.credentials, }) const data = (await res.json()) as { url: string } if (redirect) { const url = data.url ?? callbackUrl window.location.href = url if (url.includes('#')) { window.location.reload() } return undefined as R extends true ? undefined : SignOutResponse } await config.fetchSession?.({ event: 'storage' }) return data as R extends true ? undefined : SignOutResponse }