diff --git a/backend/app/routes/home.py b/backend/app/routes/home.py index 04789f8..ae85b14 100644 --- a/backend/app/routes/home.py +++ b/backend/app/routes/home.py @@ -1,6 +1,19 @@ +"""Serves the built React SPA. + +With hash-based routing, client-side routes never reach the server +(e.g. /#/boards/123 arrives as GET /). The only paths ever requested are: + + 1. "/" -> the SPA entry point (index.html) + 2. "/" -> real files in the built static/ folder (JS, CSS, icons) + +Everything else is a scanner probe (e.g. /.env, /cgi-bin/status, +/xmlrpc.php) and correctly gets 404. There is deliberately NO catch-all +that returns index.html for arbitrary paths. +""" + import os -from flask import Blueprint +from flask import Blueprint, abort from flask import current_app as app from flask import send_from_directory @@ -13,7 +26,7 @@ def serve_root(): @home_bp.route("/") -def serve_spa(path): +def serve_static(path): if os.path.exists(os.path.join(app.static_folder, path)): return send_from_directory(app.static_folder, path) - return send_from_directory(app.static_folder, "index.html") + return abort(404) \ No newline at end of file diff --git a/docker/deployment.md b/docker/deployment.md new file mode 100644 index 0000000..eeca0ba --- /dev/null +++ b/docker/deployment.md @@ -0,0 +1 @@ +deployment is through the Dockerfile \ No newline at end of file diff --git a/frontend/src/hooks/useApi.ts b/frontend/src/hooks/useApi.ts index 0519254..1c737c0 100644 --- a/frontend/src/hooks/useApi.ts +++ b/frontend/src/hooks/useApi.ts @@ -32,6 +32,13 @@ api.interceptors.request.use( (error) => Promise.reject(error) ); +// Extract the current route path from the hash (e.g. "#/boards/123" -> "/boards/123"). +// With HashRouter the pathname is always "/", so the real route lives in the fragment. +const getRoutePath = () => { + const hash = window.location.hash; + return hash ? hash.replace(/^#/, '') : '/'; +}; + // Handle response errors api.interceptors.response.use( (response) => response, @@ -41,9 +48,10 @@ api.interceptors.response.use( localStorage.removeItem('token'); localStorage.removeItem('user'); - if (!['/login', '/register'].includes(window.location.pathname)) { - const currentPath = window.location.pathname; - window.location.href = `/login?redirect=${encodeURIComponent(currentPath)}`; + const currentPath = getRoutePath(); + if (!['/login', '/register'].includes(currentPath)) { + // Hash-router login URL: the whole route + query string lives inside the hash + window.location.href = `#/login?redirect=${encodeURIComponent(currentPath)}`; } } return Promise.reject(error); diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index a0bdab8..6f78e93 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,16 +1,16 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import { BrowserRouter } from 'react-router-dom'; +import { HashRouter } from 'react-router-dom'; import { AppProvider } from './context/AppContext'; import App from './App.tsx'; import './index.css'; ReactDOM.createRoot(document.getElementById('root')!).render( - + - + );