fix the errors in dashboard
This commit is contained in:
@@ -324,7 +324,10 @@ export default function OffersPage() {
|
||||
|
||||
function load() {
|
||||
apiFetch<Offer[]>('/offers')
|
||||
.then((data) => setOffers(data ?? []))
|
||||
.then((data) => {
|
||||
setOffers(Array.isArray(data) ? data : [])
|
||||
setError(null)
|
||||
})
|
||||
.catch((err) => setError(err.message))
|
||||
}
|
||||
|
||||
@@ -334,7 +337,7 @@ export default function OffersPage() {
|
||||
setLoadingFleet(true)
|
||||
try {
|
||||
const data = await apiFetch<FleetVehicle[]>('/vehicles?pageSize=200')
|
||||
setFleet(data ?? [])
|
||||
setFleet(Array.isArray(data) ? data : [])
|
||||
} catch {
|
||||
// non-fatal; vehicle picker just stays empty
|
||||
} finally {
|
||||
@@ -363,7 +366,7 @@ export default function OffersPage() {
|
||||
const detail = await apiFetch<Offer>(`/offers/${offer.id}`)
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
vehicleIds: detail.vehicles?.map((v) => v.vehicleId) ?? [],
|
||||
vehicleIds: Array.isArray(detail?.vehicles) ? detail.vehicles.map((v) => v.vehicleId) : [],
|
||||
}))
|
||||
} catch {
|
||||
// leave vehicleIds as empty if fetch fails
|
||||
|
||||
@@ -117,11 +117,19 @@ export default function SignInPageClient({ embedded = false }: { embedded?: bool
|
||||
changed = true
|
||||
}
|
||||
|
||||
// useSearchParams() can return empty params during hydration without a Suspense boundary.
|
||||
// Always preserve embedded=1 when the component was server-rendered as embedded so
|
||||
// the router.replace doesn't strip it and trigger an unintended redirect.
|
||||
if (embedded && params.get('embedded') !== '1') {
|
||||
params.set('embedded', '1')
|
||||
changed = true
|
||||
}
|
||||
|
||||
if (!changed) return
|
||||
|
||||
const nextQuery = params.toString()
|
||||
router.replace(nextQuery ? `${pathname}?${nextQuery}` : pathname, { scroll: false })
|
||||
}, [language, pathname, router, searchParams, theme])
|
||||
}, [embedded, language, pathname, router, searchParams, theme])
|
||||
|
||||
useEffect(() => {
|
||||
const currentPath = window.location.pathname + (window.location.search || '')
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import { Suspense } from 'react'
|
||||
import { headers } from 'next/headers'
|
||||
import { redirect } from 'next/navigation'
|
||||
import SignInPageClient from './SignInPageClient'
|
||||
|
||||
const MARKETPLACE_URL = process.env.NEXT_PUBLIC_MARKETPLACE_URL ?? 'http://localhost:3000'
|
||||
|
||||
function isInternalHost(host: string | null): boolean {
|
||||
if (!host) return false
|
||||
const hostname = host.split(':')[0]?.toLowerCase()
|
||||
return ['host.docker.internal', 'dashboard', 'admin', 'api', 'marketplace'].includes(hostname)
|
||||
}
|
||||
|
||||
export default async function SignInPage({
|
||||
searchParams,
|
||||
}: {
|
||||
@@ -13,6 +22,7 @@ export default async function SignInPage({
|
||||
if (!embedded) {
|
||||
const requestHeaders = headers()
|
||||
const host = requestHeaders.get('host')
|
||||
const forwardedHost = requestHeaders.get('x-forwarded-host')
|
||||
const proto = requestHeaders.get('x-forwarded-proto') ?? 'http'
|
||||
const query = new URLSearchParams()
|
||||
|
||||
@@ -25,12 +35,22 @@ export default async function SignInPage({
|
||||
}
|
||||
}
|
||||
|
||||
const destination = host
|
||||
? `${proto}://${host}/sign-in${query.size ? `?${query.toString()}` : ''}`
|
||||
: `${process.env.NEXT_PUBLIC_MARKETPLACE_URL ?? 'http://localhost:3000'}/sign-in${query.size ? `?${query.toString()}` : ''}`
|
||||
const marketplaceOrigin = new URL(MARKETPLACE_URL)
|
||||
const publicHost = forwardedHost && !isInternalHost(forwardedHost)
|
||||
? forwardedHost
|
||||
: host && !isInternalHost(host)
|
||||
? host
|
||||
: null
|
||||
|
||||
redirect(destination)
|
||||
if (!publicHost || publicHost === marketplaceOrigin.host) {
|
||||
const destination = `${MARKETPLACE_URL}/sign-in${query.size ? `?${query.toString()}` : ''}`
|
||||
redirect(destination)
|
||||
}
|
||||
}
|
||||
|
||||
return <SignInPageClient embedded={embedded} />
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<SignInPageClient embedded={embedded} />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,12 @@ type FooterItem = {
|
||||
href?: string
|
||||
}
|
||||
|
||||
const appPrivacyHref = {
|
||||
en: `${marketplaceUrl}/app-privacy-en`,
|
||||
fr: `${marketplaceUrl}/app-privacy-fr`,
|
||||
ar: `${marketplaceUrl}/app-privacy-ar`,
|
||||
} as const
|
||||
|
||||
const localeOptions: Array<{ value: DashboardLanguage; label: string; flag: string }> = [
|
||||
{ value: 'en', label: 'Global (English)', flag: '🇺🇸' },
|
||||
{ value: 'ar', label: 'North Africa (Arabic)', flag: '🇲🇦' },
|
||||
@@ -34,13 +40,13 @@ function getFooterContent(language: DashboardLanguage): {
|
||||
{ label: 'Conditions d’utilisation', href: `${marketplaceUrl}/footer/terms-of-service` },
|
||||
{ label: 'Sécurité', href: `${marketplaceUrl}/footer/security` },
|
||||
{ label: 'Conformité', href: `${marketplaceUrl}/footer/compliance` },
|
||||
{ label: 'Politique de confidentialité', href: `${marketplaceUrl}/footer/privacy-policy` },
|
||||
{ label: 'Politique de confidentialité', href: appPrivacyHref.fr },
|
||||
{ label: 'Politique relative aux cookies', href: `${marketplaceUrl}/footer/cookie-policy` },
|
||||
],
|
||||
secondary: [
|
||||
{ label: 'Newsletter', href: `${marketplaceUrl}/footer/newsletter` },
|
||||
{ label: 'Contacter les ventes', href: `${marketplaceUrl}/footer/contact-sales` },
|
||||
{ label: 'Nos bureaux', href: `${marketplaceUrl}/footer/our-offices` },
|
||||
{ label: 'Conditions générales', href: `${marketplaceUrl}/footer/general-conditions` },
|
||||
],
|
||||
localeLabel: 'Europe (French)',
|
||||
rightsLabel: 'Tous droits réservés.',
|
||||
@@ -52,13 +58,13 @@ function getFooterContent(language: DashboardLanguage): {
|
||||
{ label: 'شروط الاستخدام', href: `${marketplaceUrl}/footer/terms-of-service` },
|
||||
{ label: 'الأمان', href: `${marketplaceUrl}/footer/security` },
|
||||
{ label: 'الامتثال', href: `${marketplaceUrl}/footer/compliance` },
|
||||
{ label: 'سياسة الخصوصية', href: `${marketplaceUrl}/footer/privacy-policy` },
|
||||
{ label: 'سياسة الخصوصية', href: appPrivacyHref.ar },
|
||||
{ label: 'سياسة ملفات تعريف الارتباط', href: `${marketplaceUrl}/footer/cookie-policy` },
|
||||
],
|
||||
secondary: [
|
||||
{ label: 'النشرة الإخبارية', href: `${marketplaceUrl}/footer/newsletter` },
|
||||
{ label: 'تواصل مع المبيعات', href: `${marketplaceUrl}/footer/contact-sales` },
|
||||
{ label: 'مكاتبنا', href: `${marketplaceUrl}/footer/our-offices` },
|
||||
{ label: 'الشروط العامة', href: `${marketplaceUrl}/footer/general-conditions` },
|
||||
],
|
||||
localeLabel: 'North Africa (Arabic)',
|
||||
rightsLabel: 'جميع الحقوق محفوظة.',
|
||||
@@ -71,13 +77,13 @@ function getFooterContent(language: DashboardLanguage): {
|
||||
{ label: 'Terms of Service', href: `${marketplaceUrl}/footer/terms-of-service` },
|
||||
{ label: 'Security', href: `${marketplaceUrl}/footer/security` },
|
||||
{ label: 'Compliance', href: `${marketplaceUrl}/footer/compliance` },
|
||||
{ label: 'Privacy Policy', href: `${marketplaceUrl}/footer/privacy-policy` },
|
||||
{ label: 'Privacy Policy', href: appPrivacyHref.en },
|
||||
{ label: 'Cookie Policy', href: `${marketplaceUrl}/footer/cookie-policy` },
|
||||
],
|
||||
secondary: [
|
||||
{ label: 'Newsletter', href: `${marketplaceUrl}/footer/newsletter` },
|
||||
{ label: 'Contact Sales', href: `${marketplaceUrl}/footer/contact-sales` },
|
||||
{ label: 'Our Offices', href: `${marketplaceUrl}/footer/our-offices` },
|
||||
{ label: 'General Conditions', href: `${marketplaceUrl}/footer/general-conditions` },
|
||||
],
|
||||
localeLabel: 'Global (English)',
|
||||
rightsLabel: 'All rights reserved.',
|
||||
|
||||
@@ -1,18 +1,41 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import type { NextRequest } from 'next/server'
|
||||
|
||||
const PUBLIC_DASHBOARD_PREFIXES = ['/dashboard/sign-in', '/dashboard/forgot-password']
|
||||
const MARKETPLACE_URL = process.env.NEXT_PUBLIC_MARKETPLACE_URL ?? 'http://localhost:3000'
|
||||
|
||||
function isInternalHost(host: string | null): boolean {
|
||||
if (!host) return false
|
||||
const hostname = host.split(':')[0]?.toLowerCase()
|
||||
return ['host.docker.internal', 'dashboard', 'admin', 'api', 'marketplace'].includes(hostname)
|
||||
}
|
||||
|
||||
function isProtectedRoute(req: NextRequest) {
|
||||
return req.nextUrl.pathname === '/dashboard' || req.nextUrl.pathname.startsWith('/dashboard/')
|
||||
const { pathname } = req.nextUrl
|
||||
if (PUBLIC_DASHBOARD_PREFIXES.some((p) => pathname === p || pathname.startsWith(p + '/'))) return false
|
||||
return pathname === '/dashboard' || pathname.startsWith('/dashboard/')
|
||||
}
|
||||
|
||||
function localJwtMiddleware(req: NextRequest): NextResponse {
|
||||
if (!isProtectedRoute(req)) return NextResponse.next()
|
||||
|
||||
// Check for employee_token cookie (set on login) OR allow if coming from sign-in
|
||||
const token = req.cookies.get('employee_token')?.value
|
||||
if (!token) {
|
||||
const forwardedHost = req.headers.get('x-forwarded-host')
|
||||
const forwardedProto = req.headers.get('x-forwarded-proto')
|
||||
const marketplaceOrigin = new URL(MARKETPLACE_URL)
|
||||
|
||||
const signInUrl = req.nextUrl.clone()
|
||||
signInUrl.pathname = '/sign-in'
|
||||
if (forwardedHost && !isInternalHost(forwardedHost)) {
|
||||
signInUrl.host = forwardedHost
|
||||
signInUrl.protocol = (forwardedProto ?? 'http') + ':'
|
||||
} else if (isInternalHost(signInUrl.host)) {
|
||||
signInUrl.host = marketplaceOrigin.host
|
||||
signInUrl.protocol = marketplaceOrigin.protocol
|
||||
}
|
||||
|
||||
const isMarketplaceHost = signInUrl.host === marketplaceOrigin.host
|
||||
signInUrl.pathname = isMarketplaceHost ? '/sign-in' : '/dashboard/sign-in'
|
||||
signInUrl.searchParams.set('redirect', req.nextUrl.pathname)
|
||||
return NextResponse.redirect(signInUrl)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user