fix production login issue
Build & Push / Pipeline Tests (push) Successful in 1m53s
Test / Type Check (all packages) (push) Successful in 56s
Build & Push / Build & Push Docker Image (push) Successful in 4m19s
Test / API Unit Tests (push) Successful in 1m17s
Test / Homepage Unit Tests (push) Successful in 49s
Test / Carplace Unit Tests (push) Successful in 46s
Test / Admin Unit Tests (push) Successful in 42s
Test / Dashboard Unit Tests (push) Successful in 44s
Test / API Integration Tests (push) Successful in 1m7s
Build & Push / Pipeline Tests (push) Successful in 1m53s
Test / Type Check (all packages) (push) Successful in 56s
Build & Push / Build & Push Docker Image (push) Successful in 4m19s
Test / API Unit Tests (push) Successful in 1m17s
Test / Homepage Unit Tests (push) Successful in 49s
Test / Carplace Unit Tests (push) Successful in 46s
Test / Admin Unit Tests (push) Successful in 42s
Test / Dashboard Unit Tests (push) Successful in 44s
Test / API Integration Tests (push) Successful in 1m7s
This commit is contained in:
@@ -5,7 +5,7 @@ import { Bell, Search, Settings } from 'lucide-react'
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { io } from 'socket.io-client'
|
||||
import { EMPLOYEE_PROFILE_KEY, apiFetch, resolveApiOrigin } from '@/lib/api'
|
||||
import { EMPLOYEE_PROFILE_KEY, apiFetch, resolveRealtimeSocketTarget } from '@/lib/api'
|
||||
import { useDashboardI18n } from '@/components/I18nProvider'
|
||||
import { toDashboardAppPath } from '@/lib/dashboardPaths'
|
||||
|
||||
@@ -76,11 +76,12 @@ export default function TopBar() {
|
||||
useEffect(() => {
|
||||
if (!socketEnabled) return
|
||||
|
||||
const socketOrigin = resolveApiOrigin()
|
||||
if (!socketOrigin) return
|
||||
const socketTarget = resolveRealtimeSocketTarget()
|
||||
if (!socketTarget) return
|
||||
|
||||
const socket = io(socketOrigin, {
|
||||
const socket = io(socketTarget.origin, {
|
||||
autoConnect: false,
|
||||
path: socketTarget.path,
|
||||
withCredentials: true,
|
||||
reconnectionAttempts: 3,
|
||||
timeout: 5000,
|
||||
|
||||
@@ -167,6 +167,34 @@ describe('dashboard apiFetch', () => {
|
||||
expect(api.resolveApiOrigin()).toBe('http://localhost:4000')
|
||||
})
|
||||
|
||||
it('routes proxied realtime connections through the dashboard socket path', async () => {
|
||||
installBrowser()
|
||||
setBrowserHostname('rentaldrivego.ma')
|
||||
;(globalThis.window as any).location.origin = 'https://rentaldrivego.ma'
|
||||
process.env.NEXT_PUBLIC_API_URL = 'https://api.rentaldrivego.ma/api/v1'
|
||||
|
||||
const api = await import('./api')
|
||||
|
||||
expect(api.resolveRealtimeSocketTarget()).toEqual({
|
||||
origin: 'https://rentaldrivego.ma',
|
||||
path: '/dashboard/socket.io',
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps direct realtime connections on the default socket path for same-host API bases', async () => {
|
||||
installBrowser()
|
||||
setBrowserHostname('rentaldrivego.ma')
|
||||
;(globalThis.window as any).location.origin = 'https://rentaldrivego.ma'
|
||||
process.env.NEXT_PUBLIC_API_URL = 'https://rentaldrivego.ma/api/v1'
|
||||
|
||||
const api = await import('./api')
|
||||
|
||||
expect(api.resolveRealtimeSocketTarget()).toEqual({
|
||||
origin: 'https://rentaldrivego.ma',
|
||||
path: '/socket.io',
|
||||
})
|
||||
})
|
||||
|
||||
it('does not force JSON content type for FormData payloads', async () => {
|
||||
installBrowser()
|
||||
const fetchMock = vi.fn(async () => ({ ok: true, json: async () => ({ data: { uploaded: true } }) }))
|
||||
|
||||
@@ -44,6 +44,20 @@ export function resolveApiOrigin(): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveRealtimeSocketTarget(): { origin: string; path: string } | null {
|
||||
if (typeof window === 'undefined') return null
|
||||
|
||||
const apiBase = resolveApiBase()
|
||||
const isDashboardProxy = apiBase === DASHBOARD_PROXY_API_BASE || apiBase.startsWith(`${DASHBOARD_PROXY_API_BASE}/`)
|
||||
const origin = isDashboardProxy ? window.location.origin : resolveApiOrigin()
|
||||
if (!origin) return null
|
||||
|
||||
return {
|
||||
origin,
|
||||
path: isDashboardProxy ? '/dashboard/socket.io' : '/socket.io',
|
||||
}
|
||||
}
|
||||
|
||||
export const API_BASE = resolveApiBase()
|
||||
|
||||
export const EMPLOYEE_PROFILE_KEY = 'employee_profile'
|
||||
|
||||
@@ -39,4 +39,16 @@ describe('dashboard next config', () => {
|
||||
]),
|
||||
)
|
||||
})
|
||||
|
||||
it('allows non-production websocket connections for local Next runtimes', async () => {
|
||||
const nextConfig = require('../next.config.js')
|
||||
|
||||
const headers = await nextConfig.headers()
|
||||
const csp = headers
|
||||
.flatMap((entry: { headers: Array<{ key: string; value: string }> }) => entry.headers)
|
||||
.find((header: { key: string }) => header.key === 'Content-Security-Policy')
|
||||
|
||||
expect(csp?.value).toContain('connect-src')
|
||||
expect(csp?.value).toContain('ws:')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -76,6 +76,9 @@ function buildSecurityHeaders({ assetSources = [], connectSources = collectBrows
|
||||
const imgSrc = [...new Set(["'self'", 'data:', 'blob:', 'https:', ...assetOrigins, ...connectOrigins])]
|
||||
const fontSrc = ["'self'", 'data:', ...assetOrigins]
|
||||
const connectSrc = [...new Set(["'self'", 'https:', 'wss:', ...assetOrigins, ...connectOrigins, ...websocketOrigins])]
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
connectSrc.push('ws:')
|
||||
}
|
||||
|
||||
return [
|
||||
{ key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains' },
|
||||
|
||||
@@ -128,6 +128,14 @@ services:
|
||||
- traefik.http.routers.api.tls.certresolver=letsencrypt
|
||||
- traefik.http.services.api.loadbalancer.server.port=4000
|
||||
- traefik.http.routers.api.middlewares=rdg-security-headers@docker
|
||||
- traefik.http.routers.dashboard-socket.rule=(Host(`${PUBLIC_SITE_DOMAIN}`) || Host(`www.${PUBLIC_SITE_DOMAIN}`)) && PathPrefix(`/dashboard/socket.io`) && !HeaderRegexp(`x-middleware-subrequest`, `.+`)
|
||||
- traefik.http.routers.dashboard-socket.entrypoints=websecure
|
||||
- traefik.http.routers.dashboard-socket.tls=true
|
||||
- traefik.http.routers.dashboard-socket.tls.certresolver=letsencrypt
|
||||
- traefik.http.routers.dashboard-socket.service=api
|
||||
- traefik.http.routers.dashboard-socket.priority=30
|
||||
- traefik.http.routers.dashboard-socket.middlewares=dashboard-socket-strip@docker,rdg-security-headers@docker
|
||||
- traefik.http.middlewares.dashboard-socket-strip.stripprefix.prefixes=/dashboard
|
||||
- traefik.http.middlewares.rdg-security-headers.headers.stsSeconds=31536000
|
||||
- traefik.http.middlewares.rdg-security-headers.headers.stsIncludeSubdomains=true
|
||||
- traefik.http.middlewares.rdg-security-headers.headers.contentTypeNosniff=true
|
||||
|
||||
@@ -128,6 +128,14 @@ services:
|
||||
- traefik.http.routers.api.tls.certresolver=letsencrypt
|
||||
- traefik.http.services.api.loadbalancer.server.port=4000
|
||||
- traefik.http.routers.api.middlewares=rdg-security-headers@docker
|
||||
- traefik.http.routers.dashboard-socket.rule=(Host(`${PUBLIC_SITE_DOMAIN}`) || Host(`www.${PUBLIC_SITE_DOMAIN}`)) && PathPrefix(`/dashboard/socket.io`) && !HeaderRegexp(`x-middleware-subrequest`, `.+`)
|
||||
- traefik.http.routers.dashboard-socket.entrypoints=websecure
|
||||
- traefik.http.routers.dashboard-socket.tls=true
|
||||
- traefik.http.routers.dashboard-socket.tls.certresolver=letsencrypt
|
||||
- traefik.http.routers.dashboard-socket.service=api
|
||||
- traefik.http.routers.dashboard-socket.priority=30
|
||||
- traefik.http.routers.dashboard-socket.middlewares=dashboard-socket-strip@docker,rdg-security-headers@docker
|
||||
- traefik.http.middlewares.dashboard-socket-strip.stripprefix.prefixes=/dashboard
|
||||
- traefik.http.middlewares.rdg-security-headers.headers.stsSeconds=31536000
|
||||
- traefik.http.middlewares.rdg-security-headers.headers.stsIncludeSubdomains=true
|
||||
- traefik.http.middlewares.rdg-security-headers.headers.contentTypeNosniff=true
|
||||
|
||||
Reference in New Issue
Block a user