UNPKG

codalware-auth

Version:

Complete authentication system with enterprise security, attack protection, team workspaces, waitlist, billing, UI components, 2FA, and account recovery - production-ready in 5 minutes. Enhanced CLI with verification, rollback, and App Router scaffolding.

256 lines (227 loc) 7.72 kB
/** * React Hooks for Auth Provider Abstraction * * This module provides React hooks that work with both NextAuth and better-auth. * The hooks dynamically detect which provider is configured and use the appropriate implementation. */ "use client"; import { useEffect, useState } from 'react'; import type { AuthSession } from './types'; import * as nextAuthReact from 'next-auth/react'; import * as betterAuthReact from 'better-auth/react'; // Lightweight typed shapes for the provider modules to avoid explicit `any` usage type NextAuthReactModule = { getSession?: () => Promise<unknown>; useSession?: () => { data?: { user?: { id?: string; email?: string; name?: string; image?: string; role?: string; tenantId?: string; status?: string; } | null; expires?: string; } | null; status?: 'loading' | 'authenticated' | 'unauthenticated'; update?: (d?: unknown) => Promise<unknown>; isPending?: boolean; }; signIn?: (...args: unknown[]) => Promise<unknown> | unknown; signOut?: (...args: unknown[]) => Promise<unknown> | unknown; }; type BetterAuthReactModule = { getSession?: () => Promise<unknown>; useSession?: () => { data?: { user?: { id?: string; email?: string; name?: string; image?: string; role?: string; tenantId?: string; status?: string; } | null; session?: { expiresAt?: string } | null; } | null; isPending?: boolean; }; signIn?: { email: (opts: { email: string; password: string; callbackURL?: string }) => Promise<{ data?: unknown; error?: { message?: string } }>; social: (opts: { provider: string; callbackURL?: string }) => Promise<{ data?: unknown; error?: { message?: string } }>; }; signOut?: (opts?: { fetchOptions?: { onSuccess?: () => void } }) => Promise<unknown>; }; const nextAuth = (nextAuthReact as unknown) as NextAuthReactModule; const betterAuth = (betterAuthReact as unknown) as BetterAuthReactModule; // Unified session hook return type export interface UseSessionResult { data: AuthSession | null; status: 'loading' | 'authenticated' | 'unauthenticated'; update?: (data?: unknown) => Promise<AuthSession | null>; } // Unified signIn function type export type SignInOptions = { email?: string; password?: string; redirect?: boolean; callbackUrl?: string; [key: string]: unknown; }; export type SignInResult = { ok?: boolean; error?: string; url?: string; } | undefined; // Check which provider is configured const isUsingBetterAuth = () => { if (typeof process !== 'undefined' && process.env?.NEXT_PUBLIC_AUTH_PROVIDER === 'better-auth') { return true; } return false; }; /** * Universal useSession hook * Automatically uses the correct provider based on NEXT_PUBLIC_AUTH_PROVIDER */ export function useSession(): UseSessionResult { const [state, setState] = useState<UseSessionResult>({ data: null, status: 'loading' }); useEffect(() => { let mounted = true; type SessionLike = { user?: { id?: string; email?: string; name?: string; image?: string; role?: string; tenantId?: string; status?: string }; session?: { expiresAt?: string }; expires?: string; } | null | undefined; async function resolveSession() { try { if (isUsingBetterAuth() && betterAuth && betterAuth.getSession) { const s = await betterAuth.getSession(); if (!mounted) return; // Map better-auth session shape to AuthSession // We do a best-effort mapping; missing fields defaulted const data = s as SessionLike; setState({ data: data ? { user: { id: data.user?.id ?? '', email: data.user?.email ?? '', name: data.user?.name, image: data.user?.image, role: data.user?.role ?? 'USER', tenantId: data.user?.tenantId, status: data.user?.status ?? 'APPROVED', }, expires: data.session?.expiresAt ?? new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), } : null, status: data ? 'authenticated' : 'unauthenticated', }); return; } if (nextAuth && nextAuth.getSession) { const s = await nextAuth.getSession(); if (!mounted) return; const data = s as SessionLike; setState({ data: data ? { user: { id: data.user?.id ?? '', email: data.user?.email ?? '', name: data.user?.name, image: data.user?.image, role: data.user?.role ?? 'USER', tenantId: data.user?.tenantId, status: data.user?.status ?? 'APPROVED', }, expires: data.expires ?? new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), } : null, status: data ? 'authenticated' : 'unauthenticated', }); return; } setState({ data: null, status: 'unauthenticated' }); } catch { if (!mounted) return; setState({ data: null, status: 'unauthenticated' }); } } resolveSession(); return () => { mounted = false; }; }, []); return state; } /** * Get signIn function for the configured provider */ export function useSignIn() { if (isUsingBetterAuth()) { const betterSignIn = betterAuth?.signIn; if (betterSignIn) { return async (provider: string, options: SignInOptions = {}): Promise<SignInResult> => { if (provider === 'credentials') { const result = await betterSignIn.email({ email: options.email!, password: options.password!, callbackURL: options.callbackUrl, }); return { ok: !!result.data, error: result.error?.message, }; } else { // Social provider const result = await betterSignIn.social({ provider, callbackURL: options.callbackUrl, }); return { ok: !!result.data, error: result.error?.message, }; } }; } } // Fall back to NextAuth if (nextAuth && nextAuth.signIn) { return nextAuth.signIn; } // Return no-op if no provider available return async () => ({ ok: false, error: 'No auth provider configured' }); } /** * Get signOut function for the configured provider */ export function useSignOut() { if (isUsingBetterAuth()) { const betterSignOutLocal = betterAuth?.signOut; if (betterSignOutLocal) { return async (options: { redirect?: boolean; callbackUrl?: string } = {}) => { await betterSignOutLocal({ fetchOptions: { onSuccess: () => { if (options.redirect !== false) { window.location.href = options.callbackUrl || '/'; } }, }, }); }; } } // Fall back to NextAuth if (nextAuth && nextAuth.signOut) { return nextAuth.signOut; } // Return no-op if no provider available return async () => {}; }