import { useMatches } from '@remix-run/react'; import { useMemo } from 'react'; import type { User } from '~/models/user.server'; const DEFAULT_REDIRECT = '/'; /** * This should be used any time the redirect path is user-provided * (Like the query string on our login/signup pages). This avoids * open-redirect vulnerabilities. * @param {string} to The redirect destination * @param {string} defaultRedirect The redirect to use if the to is unsafe. */ export function safeRedirect( to: FormDataEntryValue | string | null | undefined, defaultRedirect: string = DEFAULT_REDIRECT ) { if (!to || typeof to !== 'string') { return defaultRedirect; } if (!to.startsWith('/') || to.startsWith('//')) { return defaultRedirect; } return to; } /** * This base hook is used in other hooks to quickly search for specific data * across all loader data using useMatches. * @param {string} id The route id * @returns {JSON|undefined} The router data or undefined if not found */ export function useMatchesData( id: string ): Record | undefined { const matchingRoutes = useMatches(); const route = useMemo( () => matchingRoutes.find((route) => route.id === id), [matchingRoutes, id] ); return route?.data; } function isUser(user: any): user is User { return user && typeof user === 'object' && typeof user.email === 'string'; } export function useOptionalUser(): User | undefined { const data = useMatchesData('root'); if (!data || !isUser(data.user)) { return undefined; } return data.user; } export function useUser(): User { const maybeUser = useOptionalUser(); if (!maybeUser) { throw new Error( 'No user found in root loader, but user is required by useUser. If user is optional, try useOptionalUser instead.' ); } return maybeUser; } export function validateEmail(email: unknown): email is string { return typeof email === 'string' && email.length > 3 && email.includes('@'); }