UNPKG

@frank-auth/react

Version:

Flexible and customizable React UI components for Frank Authentication

498 lines (433 loc) 14.5 kB
'use client' /** * @frank-auth/react - useAuth Hook * * Main authentication hook that provides access to authentication state, * methods, and organization context. This is the primary hook for most * authentication operations. */ import {useCallback, useMemo} from 'react'; import type { Organization, PasswordResetConfirmRequest, PasswordResetConfirmResponse, PasswordResetResponse, ResendVerificationRequest, ResendVerificationResponse, Session, User, ValidateTokenInputBody, ValidateTokenResponse, VerificationRequest, VerificationResponse, } from '@frank-auth/client'; import {useAuth as useAuthProvider} from '../provider/auth-provider'; import {useConfig} from '../provider/config-provider'; import type { AuthError, OrganizationMembership, SDKState, SetActiveParams, SignInParams, SignInResult, SignUpParams, SignUpResult, UpdateUserParams, } from '../provider/types'; import type {PasswordResetRequest} from "@frank-auth/sdk"; // ============================================================================ // Auth Hook Interface // ============================================================================ export interface UseAuthReturn { sdk: SDKState // Authentication state isLoaded: boolean; isLoading: boolean; isSignedIn: boolean; user: User | null; session: Session | null; // Organization context organization: Organization | null; organizationMemberships: OrganizationMembership[]; activeOrganization: Organization | null; // Error handling error: AuthError | null; // Authentication methods signIn: (params: SignInParams) => Promise<SignInResult>; signUp: (params: SignUpParams) => Promise<SignUpResult>; signOut: () => Promise<void>; resendVerification: (request: ResendVerificationRequest) => Promise<ResendVerificationResponse> verifyIdentity: (type: "email" | "phone", request: VerificationRequest) => Promise<VerificationResponse> // Session management createSession: (token: string) => Promise<Session>; setActive: (params: SetActiveParams) => Promise<void>; // Organization management setActiveOrganization: (organizationId: string) => Promise<void>; switchOrganization: (organizationId: string) => Promise<void>; // User management updateUser: (params: UpdateUserParams) => Promise<User>; deleteUser: () => Promise<void>; // Utility methods reload: () => Promise<void>; // Recovery resetPassword: (params: PasswordResetConfirmRequest) => Promise<PasswordResetConfirmResponse> requestPasswordReset: (request: PasswordResetRequest) => Promise<PasswordResetResponse> extractEmailFromUrl: (url?: string) => (string | null) extractTokenFromUrl: (url?: string) => (string | null) validateToken: (request: ValidateTokenInputBody) => Promise<ValidateTokenResponse> // Convenience properties userId: string | null; userEmail: string | null; userName: string | null; organizationId: string | null; organizationName: string | null; userType: string | null; // Permission helpers hasOrganization: boolean; isOrganizationMember: boolean; isOrganizationAdmin: boolean; // Status helpers isAuthenticated: boolean; requiresVerification: boolean; requiresMFA: boolean; } // ============================================================================ // Main useAuth Hook // ============================================================================ /** * Main authentication hook providing access to all authentication functionality * * @example Basic usage * ```tsx * import { useAuth } from '@frank-auth/react'; * * function MyComponent() { * const { user, signIn, signOut, isLoaded } = useAuth(); * * if (!isLoaded) return <div>Loading...</div>; * * if (!user) { * return <button onClick={() => signIn({ strategy: 'password', identifier: 'user@example.com', password: 'password' })}> * Sign In * </button>; * } * * return ( * <div> * <p>Welcome, {user.firstName}!</p> * <button onClick={signOut}>Sign Out</button> * </div> * ); * } * ``` * * @example Organization management * ```tsx * function OrganizationSwitcher() { * const { organizationMemberships, activeOrganization, switchOrganization } = useAuth(); * * return ( * <select * value={activeOrganization?.id || ''} * onChange={(e) => switchOrganization(e.target.value)} * > * {organizationMemberships.map((membership) => ( * <option key={membership.organization.id} value={membership.organization.id}> * {membership.organization.name} * </option> * ))} * </select> * ); * } * ``` */ export function useAuth(): UseAuthReturn { const authContext = useAuthProvider(); const { userType, features } = useConfig(); // Convenience properties const userId = useMemo(() => authContext.user?.id || null, [authContext.user]); const userEmail = useMemo(() => authContext.user?.primaryEmailAddress || null, [authContext.user]); const userName = useMemo(() => { if (!authContext.user) return null; return authContext.user.username || `${authContext.user.firstName || ''} ${authContext.user.lastName || ''}`.trim() || authContext.user.primaryEmailAddress || null; }, [authContext.user]); const organizationId = useMemo(() => authContext.activeOrganization?.id || null, [authContext.activeOrganization] ); const organizationName = useMemo(() => authContext.activeOrganization?.name || null, [authContext.activeOrganization] ); // Permission helpers const hasOrganization = useMemo(() => !!authContext.activeOrganization, [authContext.activeOrganization] ); const isOrganizationMember = useMemo(() => { if (!authContext.activeOrganization || !authContext.user) return false; return authContext.organizationMemberships.some( membership => membership.organization.id === authContext.activeOrganization?.id ); }, [authContext.activeOrganization, authContext.user, authContext.organizationMemberships]); const isOrganizationAdmin = useMemo(() => { if (!authContext.activeOrganization || !authContext.user) return false; const membership = authContext.organizationMemberships.find( membership => membership.organization.id === authContext.activeOrganization?.id ); return membership?.role === 'admin' || membership?.role === 'owner'; }, [authContext.activeOrganization, authContext.user, authContext.organizationMemberships]); // Status helpers const isAuthenticated = useMemo(() => authContext.isLoaded && authContext.isSignedIn, [authContext.isLoaded, authContext.isSignedIn] ); const requiresVerification = useMemo(() => { if (!authContext.user) return false; return !authContext.user.emailVerified || (features.mfa && !authContext.user.mfaEnabled); }, [authContext.user, features.mfa]); const requiresMFA = useMemo(() => { if (!authContext.user) return false; return features.mfa && !authContext.user.mfaEnabled; }, [authContext.user, features.mfa]); // Enhanced sign in with validation const signIn = useCallback(async (params: SignInParams): Promise<SignInResult> => { // Validate required features if (params.strategy === 'oauth' && !features.oauth) { throw new Error('OAuth authentication is not enabled for this organization'); } if (params.strategy === 'passkey' && !features.passkeys) { throw new Error('Passkey authentication is not enabled for this organization'); } if (params.strategy === 'sso' && !features.sso) { throw new Error('SSO authentication is not enabled for this organization'); } if (params.strategy === 'magic_link' && !features.magicLink) { throw new Error('Magic link authentication is not enabled for this organization'); } return authContext.signIn(params); }, [authContext.signIn, features]); // Enhanced sign up with validation const signUp = useCallback(async (params: SignUpParams): Promise<SignUpResult> => { // Validate sign up is enabled if (!features.signUp) { throw new Error('User registration is not enabled for this organization'); } return authContext.signUp(params); }, [authContext.signUp, features]); // Extract token from URL const extractEmailFromUrl = useCallback((url?: string): string | null => { const urlToCheck = url || window.location.href; try { const urlObj = new URL(urlToCheck); return urlObj.searchParams.get('email'); } catch { return null; } }, []); // Extract token from URL const extractTokenFromUrl = useCallback((url?: string): string | null => { const urlToCheck = url || window.location.href; try { const urlObj = new URL(urlToCheck); return urlObj.searchParams.get('token'); } catch { return null; } }, []); return { sdk: authContext.sdk, // Core authentication state isLoaded: authContext.isLoaded, isLoading: authContext.isLoading, isSignedIn: authContext.isSignedIn, user: authContext.user, session: authContext.session, // Organization context organization: authContext.organization, organizationMemberships: authContext.organizationMemberships, activeOrganization: authContext.activeOrganization, // Error state error: authContext.error, // Authentication methods signIn, signUp, signOut: authContext.signOut, resendVerification: authContext.resendVerification, verifyIdentity: authContext.verifyIdentity, // Recovery methods requestPasswordReset: authContext.requestPasswordReset, resetPassword: authContext.resetPassword, validateToken: authContext.validateToken, extractEmailFromUrl, extractTokenFromUrl, // Session management createSession: authContext.createSession, setActive: authContext.setActive, // Organization management setActiveOrganization: authContext.setActiveOrganization, switchOrganization: authContext.switchOrganization, // User management updateUser: authContext.updateUser, deleteUser: authContext.deleteUser, // Utility methods reload: authContext.reload, // Convenience properties userId, userEmail, userName, organizationId, organizationName, userType, // Permission helpers hasOrganization, isOrganizationMember, isOrganizationAdmin, // Status helpers isAuthenticated, requiresVerification, requiresMFA, }; } // ============================================================================ // Specialized Auth Hooks // ============================================================================ /** * Hook for authentication state only (no methods) * Useful for components that only need to display auth state */ export function useAuthState() { const { isLoaded, isLoading, isSignedIn, user, session, organization, activeOrganization, error, userId, userEmail, userName, organizationId, organizationName, userType, hasOrganization, isOrganizationMember, isOrganizationAdmin, isAuthenticated, requiresVerification, requiresMFA, } = useAuth(); return { isLoaded, isLoading, isSignedIn, user, session, organization, activeOrganization, error, userId, userEmail, userName, organizationId, organizationName, userType, hasOrganization, isOrganizationMember, isOrganizationAdmin, isAuthenticated, requiresVerification, requiresMFA, }; } /** * Hook for authentication methods only * Useful for forms and action components */ export function useAuthActions() { const { signIn, signUp, signOut, createSession, setActive, setActiveOrganization, switchOrganization, updateUser, deleteUser, reload, } = useAuth(); return { signIn, signUp, signOut, createSession, setActive, setActiveOrganization, switchOrganization, updateUser, deleteUser, reload, }; } /** * Hook for organization-specific authentication data * Useful for multi-tenant applications */ export function useAuthOrganization() { const { organization, organizationMemberships, activeOrganization, organizationId, organizationName, hasOrganization, isOrganizationMember, isOrganizationAdmin, setActiveOrganization, switchOrganization, } = useAuth(); return { organization, organizationMemberships, activeOrganization, organizationId, organizationName, hasOrganization, isOrganizationMember, isOrganizationAdmin, setActiveOrganization, switchOrganization, }; } /** * Authentication status hook with loading states * Useful for conditional rendering based on auth status */ export function useAuthStatus() { const { isLoaded, isLoading, isSignedIn, isAuthenticated, requiresVerification, requiresMFA, error, } = useAuth(); return { isLoaded, isLoading, isSignedIn, isAuthenticated, requiresVerification, requiresMFA, hasError: !!error, error, status: isLoading ? 'loading' : isSignedIn ? 'signed-in' : 'signed-out', }; }