|
1 | 1 | "use client"; |
2 | 2 |
|
3 | | -import { useState, useEffect, FormEvent } from "react"; |
4 | | -import { apiUrl } from "@/lib/api"; |
5 | | -import { getToken, setToken } from "@/lib/auth"; |
| 3 | +import { useState, useEffect, useCallback, FormEvent } from "react"; |
| 4 | +import { apiUrl, API_URL } from "@/lib/api"; |
| 5 | +import { getToken, setToken, clearToken } from "@/lib/auth"; |
6 | 6 |
|
7 | 7 | export default function AuthGate({ children }: { children: React.ReactNode }) { |
8 | 8 | const [checking, setChecking] = useState(true); |
9 | 9 | const [authRequired, setAuthRequired] = useState(false); |
10 | 10 | const [authenticated, setAuthenticated] = useState(false); |
| 11 | + const [connectionError, setConnectionError] = useState(false); |
11 | 12 | const [passphrase, setPassphrase] = useState(""); |
12 | 13 | const [showPassphrase, setShowPassphrase] = useState(false); |
13 | 14 | const [error, setError] = useState(""); |
14 | 15 | const [submitting, setSubmitting] = useState(false); |
15 | 16 |
|
16 | | - useEffect(() => { |
17 | | - fetch(apiUrl("/api/auth/status")) |
18 | | - .then((res) => res.json()) |
| 17 | + const checkAuth = useCallback(() => { |
| 18 | + setChecking(true); |
| 19 | + setConnectionError(false); |
| 20 | + |
| 21 | + const url = apiUrl("/api/auth/status"); |
| 22 | + console.log(`[AuthGate] Checking auth status at ${url}`); |
| 23 | + |
| 24 | + fetch(url) |
| 25 | + .then((res) => { |
| 26 | + if (!res.ok) { |
| 27 | + console.error(`[AuthGate] Auth status returned ${res.status}`); |
| 28 | + throw new Error(`Backend returned ${res.status}`); |
| 29 | + } |
| 30 | + return res.json(); |
| 31 | + }) |
19 | 32 | .then((data) => { |
| 33 | + console.log(`[AuthGate] Auth enabled: ${data.auth_enabled}`); |
20 | 34 | if (!data.auth_enabled) { |
21 | 35 | setAuthenticated(true); |
| 36 | + setChecking(false); |
22 | 37 | } else { |
23 | 38 | setAuthRequired(true); |
24 | 39 | // Check if we have a cached token that still works |
25 | 40 | const token = getToken(); |
26 | 41 | if (token) { |
27 | | - fetch(apiUrl("/api/health"), { |
| 42 | + fetch(apiUrl("/api/auth/verify"), { |
28 | 43 | headers: { Authorization: `Bearer ${token}` }, |
29 | 44 | }).then((res) => { |
30 | 45 | if (res.ok) { |
| 46 | + console.log("[AuthGate] Cached token is valid"); |
31 | 47 | setAuthenticated(true); |
| 48 | + } else { |
| 49 | + console.log("[AuthGate] Cached token is invalid, clearing"); |
| 50 | + clearToken(); |
32 | 51 | } |
33 | 52 | setChecking(false); |
34 | | - }).catch(() => setChecking(false)); |
| 53 | + }).catch(() => { |
| 54 | + console.error("[AuthGate] Failed to validate cached token"); |
| 55 | + setChecking(false); |
| 56 | + }); |
35 | 57 | } else { |
36 | 58 | setChecking(false); |
37 | 59 | } |
38 | 60 | } |
39 | 61 | }) |
40 | | - .catch(() => { |
41 | | - // Can't reach backend — assume auth is required so we don't bypass it |
42 | | - setAuthRequired(true); |
43 | | - }) |
44 | | - .finally(() => { |
45 | | - if (!authRequired) setChecking(false); |
| 62 | + .catch((err) => { |
| 63 | + console.error(`[AuthGate] Cannot connect to backend at ${API_URL}:`, err.message); |
| 64 | + setConnectionError(true); |
| 65 | + setChecking(false); |
46 | 66 | }); |
47 | 67 | }, []); |
48 | 68 |
|
| 69 | + useEffect(() => { |
| 70 | + checkAuth(); |
| 71 | + }, [checkAuth]); |
| 72 | + |
49 | 73 | const handleSubmit = async (e: FormEvent) => { |
50 | 74 | e.preventDefault(); |
51 | 75 | setError(""); |
@@ -83,6 +107,50 @@ export default function AuthGate({ children }: { children: React.ReactNode }) { |
83 | 107 | ); |
84 | 108 | } |
85 | 109 |
|
| 110 | + if (connectionError) { |
| 111 | + return ( |
| 112 | + <div className="min-h-screen bg-f1-dark flex items-center justify-center px-4"> |
| 113 | + <div className="w-full max-w-md"> |
| 114 | + <div className="text-center mb-8"> |
| 115 | + <img src="/logo.png" alt="F1 Replay" className="w-16 h-16 rounded-lg mx-auto mb-4" /> |
| 116 | + <h1 className="text-xl font-bold text-white">F1 Replay Timing</h1> |
| 117 | + </div> |
| 118 | + |
| 119 | + <div className="bg-f1-card border border-f1-border rounded-xl p-6"> |
| 120 | + <h2 className="text-sm font-bold text-red-400 mb-3">Cannot connect to backend</h2> |
| 121 | + <p className="text-sm text-f1-muted mb-3"> |
| 122 | + The frontend failed to reach the API server at: |
| 123 | + </p> |
| 124 | + <code className="block text-xs text-white bg-f1-dark border border-f1-border rounded px-3 py-2 mb-4 break-all"> |
| 125 | + {API_URL} |
| 126 | + </code> |
| 127 | + <div className="text-xs text-f1-muted space-y-2"> |
| 128 | + <p>Common causes:</p> |
| 129 | + <ul className="list-disc list-inside space-y-1 ml-1"> |
| 130 | + <li>The backend container is still starting up</li> |
| 131 | + <li> |
| 132 | + <code className="text-white">NEXT_PUBLIC_API_URL</code> in your |
| 133 | + docker-compose.yml is set to a URL that isn't reachable from |
| 134 | + your browser |
| 135 | + </li> |
| 136 | + <li> |
| 137 | + If behind a reverse proxy, this URL must be the address your browser |
| 138 | + uses to reach the backend, not an internal Docker address |
| 139 | + </li> |
| 140 | + </ul> |
| 141 | + </div> |
| 142 | + <button |
| 143 | + onClick={checkAuth} |
| 144 | + className="w-full mt-5 px-4 py-2 bg-f1-red text-white text-sm font-bold rounded hover:bg-red-700 transition-colors" |
| 145 | + > |
| 146 | + Retry |
| 147 | + </button> |
| 148 | + </div> |
| 149 | + </div> |
| 150 | + </div> |
| 151 | + ); |
| 152 | + } |
| 153 | + |
86 | 154 | if (authenticated) { |
87 | 155 | return <>{children}</>; |
88 | 156 | } |
|
0 commit comments