From 2a3c6df96516612639412a425fc41decca340fbf Mon Sep 17 00:00:00 2001 From: david Date: Sat, 15 Aug 2026 15:30:50 +0300 Subject: [PATCH] change frontend routing strategy to hash --- backend/app/models/board.py | 2 +- backend/app/models/card.py | 2 +- backend/app/routes/home.py | 19 ++++++++++++++++--- docker/deployment.md | 1 + frontend/src/components/BoardDetailLayout.tsx | 2 +- .../src/components/kanban/KanbanColumn.tsx | 2 +- frontend/src/hooks/useApi.ts | 14 +++++++++++--- frontend/src/main.tsx | 6 +++--- frontend/src/pages/BoardDetail.tsx | 4 +--- 9 files changed, 36 insertions(+), 16 deletions(-) create mode 100644 docker/deployment.md diff --git a/backend/app/models/board.py b/backend/app/models/board.py index a654cf7..796b042 100644 --- a/backend/app/models/board.py +++ b/backend/app/models/board.py @@ -28,7 +28,7 @@ class Board(db.Model, SoftDeleteMixin): ) # Timestamps - date_last_activity = db.Column(db.DateTime) + date_last_activity = db.Column(db.DateTime, default=lambda: datetime.now(UTC)) date_last_view = db.Column(db.DateTime) created_at = db.Column(db.DateTime, default=lambda: datetime.now(UTC)) updated_at = db.Column( diff --git a/backend/app/models/card.py b/backend/app/models/card.py index 6297d3e..f339538 100644 --- a/backend/app/models/card.py +++ b/backend/app/models/card.py @@ -38,7 +38,7 @@ class Card(db.Model, SoftDeleteMixin): ) # Timestamps - date_last_activity = db.Column(db.DateTime) + date_last_activity = db.Column(db.DateTime, default=lambda: datetime.now(UTC)) created_at = db.Column(db.DateTime, default=lambda: datetime.now(UTC)) updated_at = db.Column( db.DateTime, 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/components/BoardDetailLayout.tsx b/frontend/src/components/BoardDetailLayout.tsx index 3096ba4..ed62c49 100644 --- a/frontend/src/components/BoardDetailLayout.tsx +++ b/frontend/src/components/BoardDetailLayout.tsx @@ -7,7 +7,7 @@ export const BoardDetailLayout = ({ children }: { children: ReactNode }) => { return (
-
{children}
+
{children}
{id && (
diff --git a/frontend/src/components/kanban/KanbanColumn.tsx b/frontend/src/components/kanban/KanbanColumn.tsx index 5cce0c7..de51a17 100644 --- a/frontend/src/components/kanban/KanbanColumn.tsx +++ b/frontend/src/components/kanban/KanbanColumn.tsx @@ -68,7 +68,7 @@ export function KanbanColumn({ }; return ( -
+
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( - + - + ); diff --git a/frontend/src/pages/BoardDetail.tsx b/frontend/src/pages/BoardDetail.tsx index e986e6e..a2adb07 100644 --- a/frontend/src/pages/BoardDetail.tsx +++ b/frontend/src/pages/BoardDetail.tsx @@ -245,9 +245,8 @@ export function BoardDetail() {
-
-
+
-
); }