mirror of
https://git.meshkee.com/Meshkee/dashboards.git
synced 2026-08-11 22:30:58 +04:30
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:
co-authored by
Cursor
parent
7374dd9978
commit
cce28a4d7c
@@ -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 />
|
||||
|
||||
@@ -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)}`
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user