diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js
index e96c0cf1..ec19d772 100644
--- a/frontend/src/api/index.js
+++ b/frontend/src/api/index.js
@@ -1,5 +1,5 @@
import axios from 'axios'
-import { withBase } from './../utils/basePath'
+import { withBase, stripBase } from './../utils/basePath'
// BASE_URL ends in '/', so this is '/api' at root or '/ops/api' under a subpath
// mount. Keeps the SPA, its API, and IIS all on the same mount path.
@@ -35,8 +35,10 @@ api.interceptors.response.use(
// Preserve the destination so login returns the user to this page.
if (hadToken) {
const loginPath = withBase('/login')
- const here = window.location.pathname + window.location.search
- const target = here && here !== loginPath
+ // Router paths exclude the mount base; strip it or login's
+ // router.push double-prefixes under a subpath mount.
+ const here = stripBase(window.location.pathname) + window.location.search
+ const target = here && here !== '/login'
? loginPath + '?redirect=' + encodeURIComponent(here)
: loginPath
window.location.href = target
diff --git a/frontend/src/utils/basePath.js b/frontend/src/utils/basePath.js
index c8e6318a..b77cbbbb 100644
--- a/frontend/src/utils/basePath.js
+++ b/frontend/src/utils/basePath.js
@@ -11,3 +11,15 @@ export function withBase(path) {
if (/^([a-z]+:)?\/\//i.test(path) || path.startsWith('data:')) return path
return BASE_URL + String(path).replace(/^\//, '')
}
+
+// Inverse of withBase: turn a browser pathname (which includes the mount
+// base, e.g. '/ops/computers') into a router path ('/computers'). Router
+// navigation already applies the base; feeding it an un-stripped pathname
+// double-prefixes ('/ops/ops/...').
+export function stripBase(path) {
+ if (!path) return path
+ if (BASE_URL !== '/' && path.startsWith(BASE_URL)) {
+ return '/' + path.slice(BASE_URL.length)
+ }
+ return path
+}
diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue
index c1f2f6cb..a5c43be8 100644
--- a/frontend/src/views/Login.vue
+++ b/frontend/src/views/Login.vue
@@ -52,7 +52,7 @@ import { useRouter, useRoute } from 'vue-router'
import { useAuthStore } from '../stores/auth'
import { setupApi } from '../api'
import { getSiteLogo } from '../utils/siteSettings'
-import { withBase } from '../utils/basePath'
+import { withBase, stripBase } from '../utils/basePath'
const router = useRouter()
const route = useRoute()
@@ -63,7 +63,9 @@ const authStore = useAuthStore()
function postLoginTarget() {
const redirect = route.query.redirect
if (typeof redirect === 'string' && redirect.startsWith('/') && !redirect.startsWith('//')) {
- return redirect
+ // Tolerate a redirect that still carries the mount base (old bookmarks,
+ // pre-fix interceptor URLs): router paths must be base-free.
+ return stripBase(redirect)
}
return '/'
}
diff --git a/frontend/src/views/geenforce/ManifestEditor.vue b/frontend/src/views/geenforce/ManifestEditor.vue
index 05dd0dd4..d16f3e74 100644
--- a/frontend/src/views/geenforce/ManifestEditor.vue
+++ b/frontend/src/views/geenforce/ManifestEditor.vue
@@ -23,7 +23,7 @@
common manifest uses PC types to hit a subset; a per-type
manifest already only runs on its own type, so it shows just the niche
gate it needs (CMM version, a bay number, a subtype).
Full guide: the GE-ENFORCE page in the project wiki.
+Full guide: the GE-ENFORCE wiki page.