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'
}