diff --git a/apps/customer/src/components/GuestRoute.tsx b/apps/customer/src/components/GuestRoute.tsx index 4f03622..5657295 100644 --- a/apps/customer/src/components/GuestRoute.tsx +++ b/apps/customer/src/components/GuestRoute.tsx @@ -1,13 +1,7 @@ import { Navigate, Outlet, useLocation } from 'react-router-dom' import { RouteLoader } from '@meshkee/dashboard-ui' import { useAuth } from '../context/AuthContext' - -function safeRedirectPath(value: string | null) { - if (!value || !value.startsWith('/') || value.startsWith('//')) { - return '/' - } - return value -} +import { safeRedirectPath } from '../lib/redirect' export function GuestRoute() { const { user, isLoading } = useAuth() diff --git a/apps/customer/src/components/ProtectedRoute.tsx b/apps/customer/src/components/ProtectedRoute.tsx index a258da5..2711817 100644 --- a/apps/customer/src/components/ProtectedRoute.tsx +++ b/apps/customer/src/components/ProtectedRoute.tsx @@ -1,16 +1,19 @@ -import { Navigate, Outlet } from 'react-router-dom' +import { Navigate, Outlet, useLocation } from 'react-router-dom' import { RouteLoader } from '@meshkee/dashboard-ui' import { useAuth } from '../context/AuthContext' +import { loginPathWithRedirect } from '../lib/redirect' export function ProtectedRoute() { const { user, isLoading } = useAuth() + const location = useLocation() if (isLoading) { return } if (!user) { - return + const returnTo = `${location.pathname}${location.search}${location.hash}` + return } return diff --git a/apps/customer/src/lib/redirect.ts b/apps/customer/src/lib/redirect.ts new file mode 100644 index 0000000..5074945 --- /dev/null +++ b/apps/customer/src/lib/redirect.ts @@ -0,0 +1,16 @@ +/** + * Same-origin relative path only. Blocks open redirects (`//…`, absolute URLs). + */ +export function safeRedirectPath(value: string | null | undefined, fallback = '/'): string { + if (!value) return fallback + if (!value.startsWith('/') || value.startsWith('//')) return fallback + if (value.includes('://')) return fallback + return value +} + +/** Build `/login?redirect=…` (or another login path) from a return path. */ +export function loginPathWithRedirect(returnTo: string, loginPath = '/login'): string { + const safe = safeRedirectPath(returnTo, '') + if (!safe) return loginPath + return `${loginPath}?redirect=${encodeURIComponent(safe)}` +} diff --git a/apps/customer/src/pages/LoginPage.tsx b/apps/customer/src/pages/LoginPage.tsx index d644729..7d2644c 100644 --- a/apps/customer/src/pages/LoginPage.tsx +++ b/apps/customer/src/pages/LoginPage.tsx @@ -17,19 +17,13 @@ import { } from '../services/authService' import { LanguageSelect, PasswordStrengthMeter, isPasswordStrong } from '@meshkee/dashboard-ui' import { useT } from '../i18n/useT' +import { safeRedirectPath } from '../lib/redirect' import meshkeeLogo from '../assets/meshkee-logo.png' import styles from './LoginPage.module.css' type AuthView = 'login' | 'signup' | 'forgot' | 'otp' type SmsStep = 'phone' | 'code' -function safeRedirectPath(value: string | null) { - if (!value || !value.startsWith('/') || value.startsWith('//')) { - return '/' - } - return value -} - export function LoginPage() { const navigate = useNavigate() const [searchParams] = useSearchParams() diff --git a/apps/customer/src/pages/checkout/CheckoutFlow.tsx b/apps/customer/src/pages/checkout/CheckoutFlow.tsx index dca6a5e..80ef1b8 100644 --- a/apps/customer/src/pages/checkout/CheckoutFlow.tsx +++ b/apps/customer/src/pages/checkout/CheckoutFlow.tsx @@ -2,6 +2,7 @@ import { Navigate, Outlet, useLocation } from 'react-router-dom' import { RouteLoader } from '@meshkee/dashboard-ui' import { useAuth } from '../../context/AuthContext' import { CheckoutStepper, type CheckoutStepId } from '../../components/checkout/CheckoutStepper' +import { loginPathWithRedirect, safeRedirectPath } from '../../lib/redirect' /** Cart and checkout login are public. Delivery, payment, and success require auth. */ const PUBLIC_STEPS = new Set(['cart', 'login']) @@ -14,10 +15,6 @@ function stepFromPath(pathname: string): CheckoutStepId { return 'cart' } -function loginRedirectPath(returnTo: string) { - return `/checkout/login?redirect=${encodeURIComponent(returnTo)}` -} - export function CheckoutFlow() { const { user, isLoading } = useAuth() const location = useLocation() @@ -31,7 +28,7 @@ export function CheckoutFlow() { if (!isAuthenticated && !PUBLIC_STEPS.has(step)) { return ( @@ -40,12 +37,9 @@ export function CheckoutFlow() { if (isAuthenticated && step === 'login') { const params = new URLSearchParams(location.search) - const fromQuery = params.get('redirect') + const fromQuery = safeRedirectPath(params.get('redirect'), '') const fromState = (location.state as { from?: string } | null)?.from - const destination = - fromQuery && fromQuery.startsWith('/') && !fromQuery.startsWith('//') - ? fromQuery - : fromState ?? '/checkout/delivery' + const destination = fromQuery || fromState || '/checkout/delivery' return } diff --git a/apps/customer/src/pages/checkout/CheckoutLoginStep.tsx b/apps/customer/src/pages/checkout/CheckoutLoginStep.tsx index 0a77d17..06dc5f2 100644 --- a/apps/customer/src/pages/checkout/CheckoutLoginStep.tsx +++ b/apps/customer/src/pages/checkout/CheckoutLoginStep.tsx @@ -4,6 +4,7 @@ import { Eye, EyeOff, Lock, Smartphone } from 'lucide-react' import { useAuth } from '../../context/AuthContext' import { ApiError } from '../../lib/api' import { toE164CellNumber } from '../../lib/cellNumber' +import { safeRedirectPath } from '../../lib/redirect' import { syncGuestCartToServer } from '../../services/syncGuestCart' import styles from './CheckoutSteps.module.css' @@ -19,14 +20,13 @@ export function CheckoutLoginStep() { const [isSubmitting, setIsSubmitting] = useState(false) function resolveRedirectPath() { - const fromQuery = searchParams.get('redirect') - if (fromQuery && fromQuery.startsWith('/') && !fromQuery.startsWith('//')) { - return fromQuery - } - const fromState = (location.state as { from?: string } | null)?.from - if (fromState && fromState.startsWith('/') && !fromState.startsWith('//')) { - return fromState - } + const fromQuery = safeRedirectPath(searchParams.get('redirect'), '') + if (fromQuery) return fromQuery + const fromState = safeRedirectPath( + (location.state as { from?: string } | null)?.from, + '', + ) + if (fromState) return fromState return '/checkout/delivery' }