/** * React hooks for Django-CFG encryption. */ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { createDecryptionClient, decryptObject } from '../decryption'; import { deriveKeyFromConfig } from '../key-derivation'; import { isEncryptedField } from '../types'; import type { DecryptionConfig, EncryptedField } from '../types'; /** * Hook state for decryption operations. */ interface UseDecryptState { /** Decrypted data */ data: T | undefined; /** Loading state */ isLoading: boolean; /** Error if decryption failed */ error: Error | undefined; /** Whether data has been decrypted */ isDecrypted: boolean; } /** * Hook to decrypt data on mount or when dependencies change. * * @param encryptedData - Data potentially containing encrypted fields * @param config - Decryption configuration * @returns Decrypted data state * * @example * ```typescript * function ProductPrice({ product }: { product: Product }) { * const { data, isLoading, error } = useDecrypt(product, { * secretKey: process.env.NEXT_PUBLIC_DECRYPT_KEY!, * userId: user.id * }); * * if (isLoading) return ; * if (error) return ; * return {data.price}; * } * ``` */ export function useDecrypt( encryptedData: unknown, config: DecryptionConfig ): UseDecryptState { const [state, setState] = useState>({ data: undefined, isLoading: true, error: undefined, isDecrypted: false, }); // Cache the key to avoid re-deriving on every render const keyRef = useRef(null); const configRef = useRef(config); // Check if config changed const configChanged = configRef.current.secretKey !== config.secretKey || configRef.current.userId !== config.userId || configRef.current.sessionId !== config.sessionId; if (configChanged) { configRef.current = config; keyRef.current = null; } useEffect(() => { let cancelled = false; async function decrypt() { try { setState((s) => ({ ...s, isLoading: true, error: undefined })); // Get or derive key if (!keyRef.current) { keyRef.current = await deriveKeyFromConfig(config); } const decrypted = await decryptObject(encryptedData, keyRef.current); if (!cancelled) { setState({ data: decrypted, isLoading: false, error: undefined, isDecrypted: true, }); } } catch (err) { if (!cancelled) { setState({ data: undefined, isLoading: false, error: err instanceof Error ? err : new Error('Decryption failed'), isDecrypted: false, }); } } } decrypt(); return () => { cancelled = true; }; }, [encryptedData, config.secretKey, config.userId, config.sessionId]); return state; } /** * Hook to create a memoized decryption client. * * @param config - Decryption configuration * @returns Decryption client or undefined while loading * * @example * ```typescript * function App() { * const crypto = useDecryptionClient({ * secretKey: process.env.NEXT_PUBLIC_DECRYPT_KEY! * }); * * const handleFetch = async () => { * const response = await fetch('/api/products/?encrypt=true'); * const data = await response.json(); * const decrypted = await crypto?.decryptObject(data); * }; * } * ``` */ export function useDecryptionClient(config: DecryptionConfig) { const [client, setClient] = useState > | null>(null); useEffect(() => { createDecryptionClient(config).then(setClient); }, [config.secretKey, config.userId, config.sessionId]); return client; } /** * Hook for lazy decryption with manual trigger. * * @param config - Decryption configuration * @returns Decrypt function and state * * @example * ```typescript * function LazyProduct({ product }: { product: Product }) { * const { decrypt, data, isLoading } = useLazyDecrypt({ * secretKey: process.env.NEXT_PUBLIC_DECRYPT_KEY! * }); * * return ( *
* * {isLoading && } * {data && {data.price}} *
* ); * } * ``` */ export function useLazyDecrypt(config: DecryptionConfig) { const [state, setState] = useState>({ data: undefined, isLoading: false, error: undefined, isDecrypted: false, }); const keyRef = useRef(null); const decrypt = useCallback( async (encryptedData: unknown): Promise => { try { setState((s) => ({ ...s, isLoading: true, error: undefined })); if (!keyRef.current) { keyRef.current = await deriveKeyFromConfig(config); } const decrypted = await decryptObject(encryptedData, keyRef.current); setState({ data: decrypted, isLoading: false, error: undefined, isDecrypted: true, }); return decrypted; } catch (err) { const error = err instanceof Error ? err : new Error('Decryption failed'); setState({ data: undefined, isLoading: false, error, isDecrypted: false, }); return undefined; } }, [config.secretKey, config.userId, config.sessionId] ); const reset = useCallback(() => { setState({ data: undefined, isLoading: false, error: undefined, isDecrypted: false, }); }, []); return { ...state, decrypt, reset }; } /** * Hook to check if a value needs decryption. * * @param value - Value to check * @returns Whether the value is encrypted */ export function useIsEncrypted(value: unknown): boolean { return useMemo(() => isEncryptedField(value), [value]); }