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 { Navigate, Outlet, useLocation } from 'react-router-dom'
|
||||||
import { RouteLoader } from '@meshkee/dashboard-ui'
|
import { RouteLoader } from '@meshkee/dashboard-ui'
|
||||||
import { useAuth } from '../context/AuthContext'
|
import { useAuth } from '../context/AuthContext'
|
||||||
|
import { safeRedirectPath } from '../lib/redirect'
|
||||||
function safeRedirectPath(value: string | null) {
|
|
||||||
if (!value || !value.startsWith('/') || value.startsWith('//')) {
|
|
||||||
return '/'
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
export function GuestRoute() {
|
export function GuestRoute() {
|
||||||
const { user, isLoading } = useAuth()
|
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 { RouteLoader } from '@meshkee/dashboard-ui'
|
||||||
import { useAuth } from '../context/AuthContext'
|
import { useAuth } from '../context/AuthContext'
|
||||||
|
import { loginPathWithRedirect } from '../lib/redirect'
|
||||||
|
|
||||||
export function ProtectedRoute() {
|
export function ProtectedRoute() {
|
||||||
const { user, isLoading } = useAuth()
|
const { user, isLoading } = useAuth()
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <RouteLoader />
|
return <RouteLoader />
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return <Navigate to="/login" replace />
|
const returnTo = `${location.pathname}${location.search}${location.hash}`
|
||||||
|
return <Navigate to={loginPathWithRedirect(returnTo)} replace />
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Outlet />
|
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'
|
} from '../services/authService'
|
||||||
import { LanguageSelect, PasswordStrengthMeter, isPasswordStrong } from '@meshkee/dashboard-ui'
|
import { LanguageSelect, PasswordStrengthMeter, isPasswordStrong } from '@meshkee/dashboard-ui'
|
||||||
import { useT } from '../i18n/useT'
|
import { useT } from '../i18n/useT'
|
||||||
|
import { safeRedirectPath } from '../lib/redirect'
|
||||||
import meshkeeLogo from '../assets/meshkee-logo.png'
|
import meshkeeLogo from '../assets/meshkee-logo.png'
|
||||||
import styles from './LoginPage.module.css'
|
import styles from './LoginPage.module.css'
|
||||||
|
|
||||||
type AuthView = 'login' | 'signup' | 'forgot' | 'otp'
|
type AuthView = 'login' | 'signup' | 'forgot' | 'otp'
|
||||||
type SmsStep = 'phone' | 'code'
|
type SmsStep = 'phone' | 'code'
|
||||||
|
|
||||||
function safeRedirectPath(value: string | null) {
|
|
||||||
if (!value || !value.startsWith('/') || value.startsWith('//')) {
|
|
||||||
return '/'
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LoginPage() {
|
export function LoginPage() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [searchParams] = useSearchParams()
|
const [searchParams] = useSearchParams()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Navigate, Outlet, useLocation } from 'react-router-dom'
|
|||||||
import { RouteLoader } from '@meshkee/dashboard-ui'
|
import { RouteLoader } from '@meshkee/dashboard-ui'
|
||||||
import { useAuth } from '../../context/AuthContext'
|
import { useAuth } from '../../context/AuthContext'
|
||||||
import { CheckoutStepper, type CheckoutStepId } from '../../components/checkout/CheckoutStepper'
|
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. */
|
/** Cart and checkout login are public. Delivery, payment, and success require auth. */
|
||||||
const PUBLIC_STEPS = new Set<CheckoutStepId>(['cart', 'login'])
|
const PUBLIC_STEPS = new Set<CheckoutStepId>(['cart', 'login'])
|
||||||
@@ -14,10 +15,6 @@ function stepFromPath(pathname: string): CheckoutStepId {
|
|||||||
return 'cart'
|
return 'cart'
|
||||||
}
|
}
|
||||||
|
|
||||||
function loginRedirectPath(returnTo: string) {
|
|
||||||
return `/checkout/login?redirect=${encodeURIComponent(returnTo)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CheckoutFlow() {
|
export function CheckoutFlow() {
|
||||||
const { user, isLoading } = useAuth()
|
const { user, isLoading } = useAuth()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
@@ -31,7 +28,7 @@ export function CheckoutFlow() {
|
|||||||
if (!isAuthenticated && !PUBLIC_STEPS.has(step)) {
|
if (!isAuthenticated && !PUBLIC_STEPS.has(step)) {
|
||||||
return (
|
return (
|
||||||
<Navigate
|
<Navigate
|
||||||
to={loginRedirectPath(location.pathname)}
|
to={loginPathWithRedirect(location.pathname, '/checkout/login')}
|
||||||
replace
|
replace
|
||||||
state={{ from: location.pathname }}
|
state={{ from: location.pathname }}
|
||||||
/>
|
/>
|
||||||
@@ -40,12 +37,9 @@ export function CheckoutFlow() {
|
|||||||
|
|
||||||
if (isAuthenticated && step === 'login') {
|
if (isAuthenticated && step === 'login') {
|
||||||
const params = new URLSearchParams(location.search)
|
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 fromState = (location.state as { from?: string } | null)?.from
|
||||||
const destination =
|
const destination = fromQuery || fromState || '/checkout/delivery'
|
||||||
fromQuery && fromQuery.startsWith('/') && !fromQuery.startsWith('//')
|
|
||||||
? fromQuery
|
|
||||||
: fromState ?? '/checkout/delivery'
|
|
||||||
return <Navigate to={destination} replace />
|
return <Navigate to={destination} replace />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Eye, EyeOff, Lock, Smartphone } from 'lucide-react'
|
|||||||
import { useAuth } from '../../context/AuthContext'
|
import { useAuth } from '../../context/AuthContext'
|
||||||
import { ApiError } from '../../lib/api'
|
import { ApiError } from '../../lib/api'
|
||||||
import { toE164CellNumber } from '../../lib/cellNumber'
|
import { toE164CellNumber } from '../../lib/cellNumber'
|
||||||
|
import { safeRedirectPath } from '../../lib/redirect'
|
||||||
import { syncGuestCartToServer } from '../../services/syncGuestCart'
|
import { syncGuestCartToServer } from '../../services/syncGuestCart'
|
||||||
import styles from './CheckoutSteps.module.css'
|
import styles from './CheckoutSteps.module.css'
|
||||||
|
|
||||||
@@ -19,14 +20,13 @@ export function CheckoutLoginStep() {
|
|||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
|
||||||
function resolveRedirectPath() {
|
function resolveRedirectPath() {
|
||||||
const fromQuery = searchParams.get('redirect')
|
const fromQuery = safeRedirectPath(searchParams.get('redirect'), '')
|
||||||
if (fromQuery && fromQuery.startsWith('/') && !fromQuery.startsWith('//')) {
|
if (fromQuery) return fromQuery
|
||||||
return fromQuery
|
const fromState = safeRedirectPath(
|
||||||
}
|
(location.state as { from?: string } | null)?.from,
|
||||||
const fromState = (location.state as { from?: string } | null)?.from
|
'',
|
||||||
if (fromState && fromState.startsWith('/') && !fromState.startsWith('//')) {
|
)
|
||||||
return fromState
|
if (fromState) return fromState
|
||||||
}
|
|
||||||
return '/checkout/delivery'
|
return '/checkout/delivery'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user