diff --git a/backend/igny8_core/settings.py b/backend/igny8_core/settings.py index 9018e10f..7cda2608 100644 --- a/backend/igny8_core/settings.py +++ b/backend/igny8_core/settings.py @@ -23,9 +23,10 @@ ALLOWED_HOSTS = [ 'app.igny8.com', 'igny8.com', 'www.igny8.com', - '31.97.144.105', 'localhost', '127.0.0.1', + # Note: Do NOT add static IP addresses here - they change on container restart + # Use container names or domain names instead ] INSTALLED_APPS = [ diff --git a/docker-compose.app.yml b/docker-compose.app.yml index 5fd85d50..35490b85 100644 --- a/docker-compose.app.yml +++ b/docker-compose.app.yml @@ -93,7 +93,6 @@ services: - "0.0.0.0:8023:5174" # Marketing dev server port (internal: 5174, external: 8023) environment: VITE_BACKEND_URL: "https://api.igny8.com/api" - VITE_ALLOWED_HOSTS: "igny8.com,www.igny8.com,app.igny8.com,api.igny8.com,localhost,127.0.0.1,0.0.0.0,172.18.0.13" volumes: - /data/app/igny8/frontend:/app:rw networks: [igny8_net] diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index fb3eff4d..4eb655e0 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -140,6 +140,24 @@ export async function fetchAPI(endpoint: string, options?: RequestInit & { timeo // Read response body once (can only be consumed once) const text = await response.text(); + // Handle 403 Forbidden with authentication error - clear invalid tokens + if (response.status === 403) { + try { + const errorData = text ? JSON.parse(text) : null; + // Check if it's an authentication credentials error + if (errorData?.detail?.includes('Authentication credentials') || + errorData?.message?.includes('Authentication credentials') || + errorData?.error?.includes('Authentication credentials')) { + // Token is invalid - clear auth state and force re-login + const { logout } = useAuthStore.getState(); + logout(); + // Don't throw here - let the error handling below show the error + } + } catch (e) { + // If parsing fails, continue with normal error handling + } + } + // Handle 401 Unauthorized - try to refresh token if (response.status === 401) { const refreshToken = getRefreshToken(); @@ -197,8 +215,14 @@ export async function fetchAPI(endpoint: string, options?: RequestInit & { timeo } } } catch (refreshError) { - // Refresh failed, continue with original error handling + // Refresh failed, clear auth state and force re-login + const { logout } = useAuthStore.getState(); + logout(); } + } else { + // No refresh token available, clear auth state + const { logout } = useAuthStore.getState(); + logout(); } } diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 78728637..dfcf6712 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -179,12 +179,14 @@ export default defineConfig(({ mode, command }) => { usePolling: true, // Needed for file watching in Docker }, hmr: { - // Behind reverse proxy - use same origin strategy + // Behind reverse proxy - explicitly set host and port to public domain // Client connects via public domain on default HTTPS port (443) // Caddy automatically proxies WebSocket upgrades from 443 to dev port protocol: "wss", - // Don't specify host/port - Vite will use same origin as page - // This ensures client connects to wss://domain.com (port 443 implicit) + host: isMarketingDev ? "igny8.com" : "app.igny8.com", // Marketing uses igny8.com, main app uses app.igny8.com + clientPort: 443, // Explicitly set to 443 (HTTPS default) to prevent localhost fallback + // This ensures client connects to wss://domain.com:443 + // and prevents fallback to localhost:5173/5174 }, // Increase timeout for slow connections and dependency pre-bundling timeout: 120000, // 120 seconds (2 minutes) to match Caddy timeout