From fea17e6bac1547d12d81da949d40c0278d7ed123 Mon Sep 17 00:00:00 2001 From: Nikolay Tatarinov Date: Wed, 25 Feb 2026 00:32:59 +0300 Subject: [PATCH] Refactor routing and documentation for SPA deployment - Updated the application to serve the SPA from the root path instead of /map/, enhancing accessibility. - Modified redirect logic to ensure backward compatibility with old /map/* URLs. - Adjusted documentation across multiple files to reflect the new routing structure and API endpoints. - Improved handling of OAuth redirects and session management in the backend. - Updated frontend configuration to align with the new base URL settings. --- CONTRIBUTING.md | 2 +- docs/api.md | 4 +- docs/architecture.md | 16 +++---- docs/deployment.md | 2 +- docs/development.md | 4 +- frontend-nuxt/components/MapView.vue | 12 +++-- frontend-nuxt/nuxt.config.ts | 2 +- internal/app/app.go | 70 +++++++++++++++++++--------- internal/app/client.go | 2 +- internal/app/handlers_redirects.go | 26 +---------- internal/app/oauth.go | 2 +- 11 files changed, 73 insertions(+), 69 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8f89a8b..1122d8b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ Clone the repository and run the project locally (see [Development](docs/develop ## Code layout - **Backend:** Entry point is `cmd/hnh-map/main.go`. All application logic lives in `internal/app/` (package `app`): `app.go`, `auth.go`, `api.go`, `handlers_redirects.go`, `client.go`, `client_grid.go`, `client_positions.go`, `client_markers.go`, `admin_*.go`, `map.go`, `tile.go`, `topic.go`, `migrations.go`. -- **Frontend:** Nuxt 3 app in `frontend-nuxt/` (pages, components, composables, layouts, server, plugins, `public/gfx`). It is served by the Go backend under `/map/` with baseURL `/map/`. +- **Frontend:** Nuxt 3 app in `frontend-nuxt/` (pages, components, composables, layouts, server, plugins, `public/gfx`). It is served by the Go backend at root `/` with baseURL `/`. ## Formatting and tests diff --git a/docs/api.md b/docs/api.md index d812a58..b827e1b 100644 --- a/docs/api.md +++ b/docs/api.md @@ -12,8 +12,8 @@ API доступно по префиксу `/map/api/`. Для запросов, ### OAuth - **GET /map/api/oauth/providers** — список настроенных OAuth-провайдеров. Ответ: `["google", ...]`. -- **GET /map/api/oauth/{provider}/login** — редирект на страницу авторизации провайдера. Query: `redirect` — путь для редиректа после успешного входа (например `/map/profile`). -- **GET /map/api/oauth/{provider}/callback** — callback от провайдера (вызывается автоматически). Обменивает `code` на токены, создаёт или находит пользователя, создаёт сессию и редиректит на `/map/profile` или `redirect` из state. +- **GET /map/api/oauth/{provider}/login** — редирект на страницу авторизации провайдера. Query: `redirect` — путь для редиректа после успешного входа (например `/profile`). +- **GET /map/api/oauth/{provider}/callback** — callback от провайдера (вызывается автоматически). Обменивает `code` на токены, создаёт или находит пользователя, создаёт сессию и редиректит на `/profile` или `redirect` из state. ## Кабинет diff --git a/docs/architecture.md b/docs/architecture.md index a5910dc..eeee84c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -2,16 +2,16 @@ ## Обзор -hnh-map — сервер автомаппера для HnH: Go-бэкенд с хранилищем bbolt, сессиями и Nuxt 3 SPA по пути `/map/`. Данные гридов и тайлов хранятся в каталоге `grids/` и в БД. +hnh-map — сервер автомаппера для HnH: Go-бэкенд с хранилищем bbolt, сессиями и Nuxt 3 SPA по корню `/`. API, SSE и тайлы — по `/map/api/`, `/map/updates`, `/map/grids/`. Данные гридов и тайлов хранятся в каталоге `grids/` и в БД. ``` ┌─────────────┐ HTTP/SSE ┌──────────────────────────────────────┐ │ Браузер │ ◄────────────────► │ Go-сервер (cmd/hnh-map) │ -│ (Nuxt SPA │ /map/, /map/api │ • bbolt (users, sessions, grids, │ -│ по /map/) │ /map/updates │ markers, tiles, maps, config) │ -│ │ /map/grids/ │ • Статика фронта (frontend/) │ -└─────────────┘ │ • internal/app — вся логика │ - └──────────────────────────────────────┘ +│ (Nuxt SPA │ /, /login, │ • bbolt (users, sessions, grids, │ +│ по /) │ /map/api, │ markers, tiles, maps, config) │ +│ │ /map/updates, │ • Статика фронта (frontend/) │ +│ │ /map/grids/ │ • internal/app — вся логика │ +└─────────────┘ └──────────────────────────────────────┘ ``` ## Структура бэкенда @@ -19,10 +19,10 @@ hnh-map — сервер автомаппера для HnH: Go-бэкенд с - **cmd/hnh-map/main.go** — единственная точка входа (`package main`): парсинг флагов (`-grids`, `-port`) и переменных окружения (`HNHMAP_PORT`), открытие bbolt, запуск миграций, создание `App`, регистрация маршрутов, запуск HTTP-сервера. Пути к `frontend/` и `public/` задаются из рабочей директории при старте. - **internal/app/** — пакет `app` с типом `App` и всей логикой: - - **app.go** — структура `App`, общие типы (`Character`, `Session`, `Coord`, `Position`, `Marker`, `User`, `MapInfo`, `GridData` и т.д.), регистрация маршрутов (`RegisterRoutes`), `serveMapFrontend`, `CleanChars`. + - **app.go** — структура `App`, общие типы (`Character`, `Session`, `Coord`, `Position`, `Marker`, `User`, `MapInfo`, `GridData` и т.д.), регистрация маршрутов (`RegisterRoutes`), `serveSPARoot`, `CleanChars`. - **auth.go** — сессии и авторизация: `getSession`, `deleteSession`, `saveSession`, `getUser`, `getPage`, `createSession`, `setupRequired`, `requireAdmin`. - **api.go** — HTTP API: авторизация (login, me, logout, setup), кабинет (tokens, password), админ (users, settings, maps, wipe, rebuildZooms, export, merge), роутер `/map/api/...`. - - **handlers_redirects.go** — редиректы: `/` → `/map/profile` или `/map/setup`, `/login` → `/map/login`, `/logout` → `/map/login`, `/admin` → `/map/admin`. + - **handlers_redirects.go** — редирект `/logout` → `/login` (после удаления сессии). - **client.go** — роутер клиента маппера (`/client/{token}/...`), `locate`. - **client_grid.go** — `gridUpdate`, `gridUpload`, `updateZoomLevel`. - **client_positions.go** — `updatePositions`. diff --git a/docs/deployment.md b/docs/deployment.md index 0fb7d97..5d018e7 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -35,7 +35,7 @@ docker run -v /srv/hnh-map:/map -p 8080:8080 \ ## Reverse proxy -Разместите сервис за nginx, Traefik, Caddy и т.п. на нужном домене. Проксируйте весь трафик на порт 8080 контейнера (или тот порт, на котором слушает приложение). Отдельная настройка для `/map` не обязательна: приложение само отдаёт SPA и API по путям `/map/`, `/map/api/`, `/map/updates`, `/map/grids/`. +Разместите сервис за nginx, Traefik, Caddy и т.п. на нужном домене. Проксируйте весь трафик на порт 8080 контейнера (или тот порт, на котором слушает приложение). Приложение отдаёт SPA по корню `/` (/, /login, /profile, /admin и т.д.), API — по `/map/api/`, SSE — `/map/updates`, тайлы — `/map/grids/`. ## Обновление и бэкапы diff --git a/docs/development.md b/docs/development.md index 179f07f..97e3b57 100644 --- a/docs/development.md +++ b/docs/development.md @@ -27,7 +27,7 @@ npm install npm run dev ``` -В dev-режиме приложение доступно по адресу с baseURL `/map/` (например `http://localhost:3000/map/`). Бэкенд должен быть доступен; при необходимости настройте прокси в `nuxt.config.ts` (например на `http://localhost:8080`). +В dev-режиме приложение доступно по корню (например `http://localhost:3000/`). Бэкенд должен быть доступен; при необходимости настройте прокси в `nuxt.config.ts` (например на `http://localhost:8080`). ### Docker Compose (разработка) @@ -38,7 +38,7 @@ docker compose -f docker-compose.dev.yml up - Фронт: порт **3000** (Nuxt dev-сервер). - Бэкенд: порт **3080** (чтобы не конфликтовать с другими сервисами на 8080). -Откройте http://localhost:3000/map/. Запросы к `/map/api`, `/map/updates`, `/map/grids` проксируются на бэкенд (host `backend`, порт 3080). +Откройте http://localhost:3000/. Запросы к `/map/api`, `/map/updates`, `/map/grids` проксируются на бэкенд (host `backend`, порт 3080). ### Сборка образа и prod-композ diff --git a/frontend-nuxt/components/MapView.vue b/frontend-nuxt/components/MapView.vue index e2c7c06..7c2777f 100644 --- a/frontend-nuxt/components/MapView.vue +++ b/frontend-nuxt/components/MapView.vue @@ -355,8 +355,11 @@ onMounted(async () => { props.mapId != null && props.mapId >= 1 ? props.mapId : mapsList.length > 0 ? (mapsList[0]?.ID ?? 0) : 0 mapid = initialMapId - const tileBase = (useRuntimeConfig().app.baseURL as string) ?? '/' - const tileUrl = tileBase.endsWith('/') ? `${tileBase}grids/{map}/{z}/{x}_{y}.png?{cache}` : `${tileBase}/grids/{map}/{z}/{x}_{y}.png?{cache}` + // Tiles are served at /map/grids/ (backend path, not SPA baseURL) + const runtimeConfig = useRuntimeConfig() + const apiBase = (runtimeConfig.public.apiBase as string) ?? '/map/api' + const backendBase = apiBase.replace(/\/api\/?$/, '') || '/map' + const tileUrl = `${backendBase}/grids/{map}/{z}/{x}_{y}.png?{cache}` layer = new SmartTileLayer(tileUrl, { minZoom: 1, maxZoom: 6, @@ -416,9 +419,8 @@ onMounted(async () => { } }) - // Use same origin as page so SSE connects to correct host/port (e.g. 3080 not 3088) - const base = (useRuntimeConfig().app.baseURL as string) ?? '/' - const updatesPath = base.endsWith('/') ? `${base}updates` : `${base}/updates` + // SSE is at /map/updates (backend path, not SPA baseURL). Same origin so it connects to correct host/port. + const updatesPath = `${backendBase}/updates` const updatesUrl = import.meta.client ? `${window.location.origin}${updatesPath}` : updatesPath source = new EventSource(updatesUrl) source.onmessage = (event: MessageEvent) => { diff --git a/frontend-nuxt/nuxt.config.ts b/frontend-nuxt/nuxt.config.ts index 97d9918..2437c70 100644 --- a/frontend-nuxt/nuxt.config.ts +++ b/frontend-nuxt/nuxt.config.ts @@ -6,7 +6,7 @@ export default defineNuxtConfig({ devtools: { enabled: true }, app: { - baseURL: '/map/', + baseURL: '/', pageTransition: { name: 'page', mode: 'out-in' }, head: { title: 'HnH Map', diff --git a/internal/app/app.go b/internal/app/app.go index 948faa9..69e0a59 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "path/filepath" + "strings" "sync" "time" @@ -139,41 +140,69 @@ type Page struct { Title string `json:"title"` } -// serveMapFrontend serves the map SPA: static files from frontend, fallback to index.html for client-side routes. -func (a *App) serveMapFrontend(rw http.ResponseWriter, req *http.Request) { +// serveSPARoot serves the map SPA from root: static files from frontend, fallback to index.html for client-side routes. +// Handles redirects from old /map/* URLs for backward compatibility. +func (a *App) serveSPARoot(rw http.ResponseWriter, req *http.Request) { path := req.URL.Path - if len(path) <= len("/map/") { - path = "" - } else { - path = path[len("/map/"):] + + // Redirect old /map/* URLs to flat routes + if path == "/map" || path == "/map/" { + http.Redirect(rw, req, "/", http.StatusFound) + return } - root := a.frontendRoot - if path == "" { - path = "index.html" + if strings.HasPrefix(path, "/map/") { + rest := path[len("/map/"):] + switch { + case rest == "login": + http.Redirect(rw, req, "/login", http.StatusFound) + return + case rest == "profile": + http.Redirect(rw, req, "/profile", http.StatusFound) + return + case rest == "admin" || strings.HasPrefix(rest, "admin/"): + http.Redirect(rw, req, "/"+rest, http.StatusFound) + return + case rest == "setup": + http.Redirect(rw, req, "/setup", http.StatusFound) + return + case strings.HasPrefix(rest, "character/"): + http.Redirect(rw, req, "/"+rest, http.StatusFound) + return + case strings.HasPrefix(rest, "grid/"): + http.Redirect(rw, req, "/"+rest, http.StatusFound) + return + } } - path = filepath.Clean(path) - if path == "." || path == ".." || (len(path) >= 2 && path[:2] == "..") { + + // File serving: path relative to frontend root (with baseURL /, files are at root) + filePath := strings.TrimPrefix(path, "/") + if filePath == "" { + filePath = "index.html" + } + filePath = filepath.Clean(filePath) + if filePath == "." || filePath == ".." || strings.HasPrefix(filePath, "..") { http.NotFound(rw, req) return } - tryPaths := []string{filepath.Join("map", path), path} + // Try both root and map/ for backward compatibility with old builds + tryPaths := []string{filePath, filepath.Join("map", filePath)} var f http.File for _, p := range tryPaths { var err error - f, err = http.Dir(root).Open(p) + f, err = http.Dir(a.frontendRoot).Open(p) if err == nil { - path = p + filePath = p break } } if f == nil { - http.ServeFile(rw, req, filepath.Join(root, "index.html")) + http.ServeFile(rw, req, filepath.Join(a.frontendRoot, "index.html")) return } defer f.Close() stat, err := f.Stat() if err != nil || stat.IsDir() { - http.ServeFile(rw, req, filepath.Join(root, "index.html")) + http.ServeFile(rw, req, filepath.Join(a.frontendRoot, "index.html")) return } http.ServeContent(rw, req, stat.Name(), stat.ModTime(), f) @@ -195,15 +224,12 @@ func (a *App) CleanChars() { // RegisterRoutes registers all HTTP handlers for the app. func (a *App) RegisterRoutes() { http.HandleFunc("/client/", a.client) - - http.HandleFunc("/login", a.redirectLogin) http.HandleFunc("/logout", a.redirectLogout) - http.HandleFunc("/admin", a.redirectAdmin) - http.HandleFunc("/admin/", a.redirectAdmin) - http.HandleFunc("/", a.redirectRoot) http.HandleFunc("/map/api/", a.apiRouter) http.HandleFunc("/map/updates", a.watchGridUpdates) http.HandleFunc("/map/grids/", a.gridTile) - http.HandleFunc("/map/", a.serveMapFrontend) + + // SPA catch-all: must be last + http.HandleFunc("/", a.serveSPARoot) } diff --git a/internal/app/client.go b/internal/app/client.go index 4f8395f..8e90eac 100644 --- a/internal/app/client.go +++ b/internal/app/client.go @@ -69,7 +69,7 @@ func (a *App) client(rw http.ResponseWriter, req *http.Request) { case "markerUpdate": a.uploadMarkers(rw, req) case "": - http.Redirect(rw, req, "/map/", 302) + http.Redirect(rw, req, "/", 302) case "checkVersion": if req.FormValue("version") == VERSION { rw.WriteHeader(200) diff --git a/internal/app/handlers_redirects.go b/internal/app/handlers_redirects.go index 98f6b1e..9351a93 100644 --- a/internal/app/handlers_redirects.go +++ b/internal/app/handlers_redirects.go @@ -4,26 +4,6 @@ import ( "net/http" ) -func (a *App) redirectRoot(rw http.ResponseWriter, req *http.Request) { - if req.URL.Path != "/" { - http.NotFound(rw, req) - return - } - if a.setupRequired() { - http.Redirect(rw, req, "/map/setup", http.StatusFound) - return - } - http.Redirect(rw, req, "/map/profile", http.StatusFound) -} - -func (a *App) redirectLogin(rw http.ResponseWriter, req *http.Request) { - if req.URL.Path != "/login" { - http.NotFound(rw, req) - return - } - http.Redirect(rw, req, "/map/login", http.StatusFound) -} - func (a *App) redirectLogout(rw http.ResponseWriter, req *http.Request) { if req.URL.Path != "/logout" { http.NotFound(rw, req) @@ -33,9 +13,5 @@ func (a *App) redirectLogout(rw http.ResponseWriter, req *http.Request) { if s != nil { a.deleteSession(s) } - http.Redirect(rw, req, "/map/login", http.StatusFound) -} - -func (a *App) redirectAdmin(rw http.ResponseWriter, req *http.Request) { - http.Redirect(rw, req, "/map/admin", http.StatusFound) + http.Redirect(rw, req, "/login", http.StatusFound) } diff --git a/internal/app/oauth.go b/internal/app/oauth.go index 2cc2d8f..077a61e 100644 --- a/internal/app/oauth.go +++ b/internal/app/oauth.go @@ -186,7 +186,7 @@ func (a *App) oauthCallback(rw http.ResponseWriter, req *http.Request, provider Secure: req.TLS != nil, SameSite: http.SameSiteLaxMode, }) - redirectTo := "/map/profile" + redirectTo := "/profile" if st.RedirectURI != "" { if u, err := url.Parse(st.RedirectURI); err == nil && u.Path != "" && !strings.HasPrefix(u.Path, "//") { redirectTo = u.Path