| 209 | } |
| 210 | |
| 211 | export default function App() { |
| 212 | const { token, setAuth, user } = useAuthStore(); |
| 213 | const [loading, setLoading] = useState(true); |
| 214 | |
| 215 | useEffect(() => { |
| 216 | // Initialize theme on app mount (ensures login page gets correct theme) |
| 217 | const savedTheme = localStorage.getItem('theme') || 'light'; |
| 218 | document.documentElement.setAttribute('data-theme', savedTheme); |
| 219 | |
| 220 | // Cross-domain tenant switch: the backend appends ?token=<jwt> to the redirect URL |
| 221 | // so the new domain receives a fresh scoped token. Consume it here (before any other |
| 222 | // auth logic) so it always takes precedence over a stale token in localStorage. |
| 223 | // |
| 224 | // IMPORTANT: Only apply this on paths that do NOT use ?token= for their own purposes. |
| 225 | // /reset-password and /verify-email both receive a one-time token for their own flow — |
| 226 | // consuming it here as a session JWT would call /auth/me, fail, log out the user, |
| 227 | // and redirect them to /login instead of showing the correct page. |
| 228 | const urlParams = new URLSearchParams(window.location.search); |
| 229 | const urlToken = urlParams.get('token'); |
| 230 | const currentPath = window.location.pathname; |
| 231 | const pathsWithOwnToken = ['/reset-password', '/verify-email']; |
| 232 | let effectiveToken = token; |
| 233 | |
| 234 | if (urlToken && !pathsWithOwnToken.includes(currentPath)) { |
| 235 | // Persist the new token and update the zustand store's in-memory value |
| 236 | localStorage.setItem('token', urlToken); |
| 237 | useAuthStore.setState({ token: urlToken, user: null }); |
| 238 | effectiveToken = urlToken; |
| 239 | |
| 240 | // Remove token from URL to prevent it from leaking into browser history |
| 241 | // and to avoid re-applying it on a manual page refresh. |
| 242 | urlParams.delete('token'); |
| 243 | const cleanSearch = urlParams.toString(); |
| 244 | const cleanUrl = window.location.pathname |
| 245 | + (cleanSearch ? `?${cleanSearch}` : '') |
| 246 | + window.location.hash; |
| 247 | window.history.replaceState({}, '', cleanUrl); |
| 248 | } |
| 249 | |
| 250 | |
| 251 | if (effectiveToken && !user) { |
| 252 | authApi.me() |
| 253 | .then((u) => setAuth(u, effectiveToken!)) |
| 254 | .catch(() => useAuthStore.getState().logout()) |
| 255 | .finally(() => setLoading(false)); |
| 256 | } else { |
| 257 | setLoading(false); |
| 258 | } |
| 259 | }, []); |
| 260 | |
| 261 | |
| 262 | if (loading) { |
| 263 | return ( |
| 264 | <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100vh', color: 'var(--text-tertiary)' }}> |
| 265 | 加载中... |
| 266 | </div> |
| 267 | ); |
| 268 | } |