Preserve post-login redirect for customer dashboard deep links.

Unauthenticated visits to protected pages now land on /login?redirect=…, and a shared helper keeps redirect targets same-origin only.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-10 09:23:45 +03:30
co-authored by Cursor
parent 7374dd9978
commit cce28a4d7c
6 changed files with 35 additions and 34 deletions
+1 -7
View File
@@ -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()
@@ -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 <RouteLoader />
}
if (!user) {
return <Navigate to="/login" replace />
const returnTo = `${location.pathname}${location.search}${location.hash}`
return <Navigate to={loginPathWithRedirect(returnTo)} replace />
}
return <Outlet />
+16
View File
@@ -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)}`
}
+1 -7
View File
@@ -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()
@@ -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<CheckoutStepId>(['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 (
<Navigate
to={loginRedirectPath(location.pathname)}
to={loginPathWithRedirect(location.pathname, '/checkout/login')}
replace
state={{ from: location.pathname }}
/>
@@ -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 <Navigate to={destination} replace />
}
@@ -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'
}