UNPKG

@csrf-armor/nextjs

Version:

CSRF protection middleware for Next.js applications

255 lines (253 loc) 6.99 kB
import { csrfFetch, getCsrfToken, refreshCsrfToken } from "./client.js"; import { usePathname } from "next/navigation"; import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react"; import { jsx } from "react/jsx-runtime"; //#region src/client/react.tsx const CsrfContext = createContext(null); /** * React context provider for CSRF token management. * * Provides centralized CSRF token state management for React components, * with automatic token refresh on route changes and enhanced fetch capabilities. * * **Features:** * - Automatic token retrieval and state synchronization * - Route change detection with automatic token refresh * - Enhanced fetch function with built-in CSRF protection * - Browser history integration (back/forward navigation) * - Optimized re-renders with memoization * * **Best Practices:** * - Place near the root of your component tree * - Use with Next.js App Router for automatic route detection * - Combine with server-side CSRF middleware for complete protection * * @public * @param children - React components that will have access to CSRF context * @param config - Optional CSRF client configuration * * @example * ```tsx * // app/layout.tsx - Application-wide CSRF provider * import { CsrfProvider } from '@csrf-armor/nextjs/client'; * * export default function RootLayout({ * children, * }: { * children: React.ReactNode; * }) { * return ( * <html lang="en"> * <body> * <CsrfProvider config={{ * cookieName: 'csrf-token', * headerName: 'x-csrf-token' * }}> * {children} * </CsrfProvider> * </body> * </html> * ); * } * ``` * * @example * ```tsx * // components/ProtectedForm.tsx - Using CSRF in forms * import { useCsrf } from '@csrf-armor/nextjs/client'; * * export function ProtectedForm() { * const { csrfToken, csrfFetch } = useCsrf(); * * const handleSubmit = async (formData: FormData) => { * try { * const response = await csrfFetch('/api/submit', { * method: 'POST', * body: formData * }); * * if (response.ok) { * console.log('Form submitted successfully'); * } * } catch (error) { * console.error('Submission failed:', error); * } * }; * * return ( * <form action={handleSubmit}> * {csrfToken && ( * <input type="hidden" name="csrf_token" value={csrfToken} /> * )} * <button type="submit">Submit</button> * </form> * ); * } * ``` */ function CsrfProvider({ children, config }) { const [csrfToken, setCsrfToken] = useState(config?.initialToken ?? null); const pathname = usePathname(); const updateToken = useCallback(() => { const newToken = getCsrfToken(config); setCsrfToken((prev) => prev !== newToken ? newToken : prev); }, [config]); const refreshToken = useCallback(async () => { setTimeout(async () => { const newToken = await refreshCsrfToken(config); setCsrfToken((prev) => prev !== newToken ? newToken : prev); }, 50); }, [config]); useEffect(() => { updateToken(); }, [updateToken]); useEffect(() => { updateToken(); const handlePopState = () => { refreshToken(); }; window.addEventListener("popstate", handlePopState); return () => { window.removeEventListener("popstate", handlePopState); }; }, [pathname]); const secureFetch = useCallback(async (input, init) => { const response = await csrfFetch(input, init, config); const headerName = config?.headerName ?? "x-csrf-token"; const newToken = response.headers.get(headerName); if (newToken && newToken !== csrfToken) setCsrfToken(newToken); return response; }, [config, csrfToken]); const value = useMemo(() => ({ csrfToken, updateToken, csrfFetch: secureFetch }), [ csrfToken, updateToken, secureFetch ]); return /* @__PURE__ */ jsx(CsrfContext.Provider, { value, children }); } /** * React hook for accessing CSRF token and utilities. * * Provides access to the current CSRF token and related utilities from the * nearest CsrfProvider. Must be used within a component tree wrapped by CsrfProvider. * * **Return Value:** * - `csrfToken`: Current token string (null if not available) * - `updateToken`: Function to manually refresh token from storage * - `csrfFetch`: Enhanced fetch with automatic CSRF headers * * @public * @returns CSRF context value with token and utilities * @throws Error if used outside of CsrfProvider * * @example * ```tsx * import { useCsrf } from '@csrf-armor/nextjs/client'; * * function MyComponent() { * const { csrfToken, csrfFetch, updateToken } = useCsrf(); * * const handleSubmit = async (data: FormData) => { * try { * const response = await csrfFetch('/api/submit', { * method: 'POST', * body: data * }); * console.log('Success:', await response.json()); * } catch (error) { * console.error('Failed:', error); * // Optionally refresh token and retry * updateToken(); * } * }; * * return ( * <div> * <p>Current token: {csrfToken || 'Not available'}</p> * <button onClick={() => updateToken()}> * Refresh Token * </button> * </div> * ); * } * ``` * * @example * ```tsx * // Using with forms and hidden inputs * function ContactForm() { * const { csrfToken } = useCsrf(); * * return ( * <form method="post" action="/api/contact"> * {csrfToken && ( * <input type="hidden" name="csrf_token" value={csrfToken} /> * )} * <input name="email" type="email" required /> * <textarea name="message" required /> * <button type="submit">Send Message</button> * </form> * ); * } * ``` * * @example * ```tsx * // Error handling with automatic retry * function DataUploader() { * const { csrfFetch, updateToken } = useCsrf(); * * const uploadFile = async (file: File) => { * const formData = new FormData(); * formData.append('file', file); * * try { * const response = await csrfFetch('/api/upload', { * method: 'POST', * body: formData * }); * * if (!response.ok) { * throw new Error(`HTTP ${response.status}`); * } * * return await response.json(); * } catch (error) { * if (error.message.includes('403')) { * // CSRF token might be stale, refresh and retry once * updateToken(); * await new Promise(resolve => setTimeout(resolve, 100)); * return uploadFile(file); // Recursive retry * } * throw error; * } * }; * * return ( * <input * type="file" * onChange={(e) => { * if (e.target.files?.[0]) { * uploadFile(e.target.files[0]); * } * }} * /> * ); * } * ``` */ function useCsrf() { const context = useContext(CsrfContext); if (!context) throw new Error("useCsrf must be used within a CsrfProvider"); return context; } //#endregion export { CsrfProvider, useCsrf }; //# sourceMappingURL=react.js.map