diff --git a/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java b/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java index faf71c2f97..4db9f118ec 100644 --- a/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java @@ -57,12 +57,13 @@ public class RequestUriUtils { return true; } - // Admin portal SPA shell. Served publicly like the editor root so a direct - // nav / refresh to /portal loads the app (the JWT lives in localStorage, not - // a cookie, so the server can't authenticate the navigation itself). The + // Admin portal SPA shell (mounted at /processor — must match the frontend + // PORTAL_BASENAME). Served publicly like the editor root so a direct nav / + // refresh to /processor loads the app (the JWT lives in localStorage, not a + // cookie, so the server can't authenticate the navigation itself). The // portal gates access via its own auth gate + RequirePortalAccess, and its // data APIs stay protected, so serving the shell pre-auth is safe. - if (normalizedUri.equals("/portal") || normalizedUri.startsWith("/portal/")) { + if (normalizedUri.equals("/processor") || normalizedUri.startsWith("/processor/")) { return true; } diff --git a/app/common/src/test/java/stirling/software/common/util/RequestUriUtilsTest.java b/app/common/src/test/java/stirling/software/common/util/RequestUriUtilsTest.java index a13aeac82b..a6fefd1091 100644 --- a/app/common/src/test/java/stirling/software/common/util/RequestUriUtilsTest.java +++ b/app/common/src/test/java/stirling/software/common/util/RequestUriUtilsTest.java @@ -75,10 +75,10 @@ class RequestUriUtilsTest { @Test void testIsStaticResource_portalShell() { - // The admin portal SPA shell is served pre-auth so it's directly navigable. - assertTrue(RequestUriUtils.isStaticResource("/portal")); - assertTrue(RequestUriUtils.isStaticResource("/portal/users")); - assertTrue(RequestUriUtils.isStaticResource("/app", "/app/portal")); + // The admin portal SPA shell (/processor) is served pre-auth so it's directly navigable. + assertTrue(RequestUriUtils.isStaticResource("/processor")); + assertTrue(RequestUriUtils.isStaticResource("/processor/users")); + assertTrue(RequestUriUtils.isStaticResource("/app", "/app/processor")); } // --- isFrontendRoute tests --- diff --git a/frontend/editor/src/core/routes/portalBasename.ts b/frontend/editor/src/core/routes/portalBasename.ts index 70f05f6a27..c9ad3c44a7 100644 --- a/frontend/editor/src/core/routes/portalBasename.ts +++ b/frontend/editor/src/core/routes/portalBasename.ts @@ -4,4 +4,4 @@ * mount point without importing portal code — build flavors that ship no * portal (core, desktop, prototypes) must never resolve @portal. */ -export const PORTAL_BASENAME = "/portal"; +export const PORTAL_BASENAME = "/processor"; diff --git a/frontend/editor/src/portal/PortalApp.tsx b/frontend/editor/src/portal/PortalApp.tsx index d14892363e..d1acc012ed 100644 --- a/frontend/editor/src/portal/PortalApp.tsx +++ b/frontend/editor/src/portal/PortalApp.tsx @@ -17,11 +17,11 @@ function ThemedSuiProvider({ children }: { children: ReactNode }) { } /** - * The portal, mounted as a route-set under /portal/* inside the editor app (via - * the admin-route seam). It supplies its own providers and its own i18next + * The portal, mounted as a route-set under /processor/* inside the editor app + * (via the admin-route seam). It supplies its own providers and its own i18next * instance (the `portal` namespace), but NOT a router — the editor's * is the one and only router; the portal's routes are relative - * to the /portal mount (see ViewRouter). + * to the /processor mount (see ViewRouter). * * The provider stack itself is a per-flavor seam (see {@link PortalProviders}): * self-hosted mounts the account-link layer, SaaS does not. diff --git a/frontend/editor/src/portal/ViewRouter.tsx b/frontend/editor/src/portal/ViewRouter.tsx index c93d7b28bd..5e6af09e38 100644 --- a/frontend/editor/src/portal/ViewRouter.tsx +++ b/frontend/editor/src/portal/ViewRouter.tsx @@ -15,7 +15,7 @@ import { DeveloperDocs } from "@portal/views/DeveloperDocs"; import { Procurement } from "@portal/views/Procurement"; import { VIEW_PATHS, toPortalPath } from "@portal/contexts/ViewContext"; -// The portal mounts as a route-set under /portal/* in the editor app, so these +// The portal mounts as a route-set under /processor/* in the editor app, so these // child routes are relative to that base: strip the leading slash from the // logical VIEW_PATHS, and home is the index route. Redirects use toPortalPath // so they resolve to the portal, not the editor root. diff --git a/frontend/editor/src/portal/auth/editorUrl.ts b/frontend/editor/src/portal/auth/editorUrl.ts index 8f60292331..f5538f47ee 100644 --- a/frontend/editor/src/portal/auth/editorUrl.ts +++ b/frontend/editor/src/portal/auth/editorUrl.ts @@ -3,8 +3,8 @@ * bouncing non-admins out). * * Sourced from VITE_EDITOR_URL so it's configurable per deploy rather than - * hardcoded, falling back to "/" (the editor serves the portal at /portal on - * the same origin, so the root is the editor). For dev cross-app navigation to + * hardcoded, falling back to "/" (the editor serves the portal at /processor + * on the same origin, so the root is the editor). For dev cross-app navigation to * a separately-running editor, set VITE_EDITOR_URL in editor/.env.local. */ export const EDITOR_URL = import.meta.env.VITE_EDITOR_URL || "/"; diff --git a/frontend/editor/src/portal/contexts/ViewContext.tsx b/frontend/editor/src/portal/contexts/ViewContext.tsx index 0a9706ef7a..dd8d207944 100644 --- a/frontend/editor/src/portal/contexts/ViewContext.tsx +++ b/frontend/editor/src/portal/contexts/ViewContext.tsx @@ -61,7 +61,7 @@ export const VIEW_PATHS: Record = { */ export { PORTAL_BASENAME }; -/** Logical view path -> full app path (e.g. "/users" -> "/portal/users"). */ +/** Logical view path -> full app path (e.g. "/users" -> "/processor/users"). */ export function toPortalPath(viewPath: string): string { return `${PORTAL_BASENAME}${viewPath === "/" ? "" : viewPath}`; } diff --git a/frontend/editor/src/portal/views/PipelineBuilder.test.tsx b/frontend/editor/src/portal/views/PipelineBuilder.test.tsx index 97ee9362d4..3474874dad 100644 --- a/frontend/editor/src/portal/views/PipelineBuilder.test.tsx +++ b/frontend/editor/src/portal/views/PipelineBuilder.test.tsx @@ -104,9 +104,12 @@ function renderBuilder(initial: string) { return render( - } /> - } /> - pipelines list} /> + } /> + } /> + pipelines list} + /> , ); @@ -131,7 +134,7 @@ describe("PipelineBuilder", () => { }); it("builds a new pipeline: name it, add a tool, and save", async () => { - renderBuilder("/portal/pipelines/new"); + renderBuilder("/processor/pipelines/new"); // The name field is the only textbox before the picker opens. fireEvent.change(await screen.findByRole("textbox"), { @@ -157,7 +160,7 @@ describe("PipelineBuilder", () => { }); it("runs an existing pipeline and reports success", async () => { - renderBuilder("/portal/pipelines/plc-1"); + renderBuilder("/processor/pipelines/plc-1"); fireEvent.click(await screen.findByText("portal.pipelines.detail.run")); @@ -168,7 +171,7 @@ describe("PipelineBuilder", () => { }); it("blocks saving a step that needs an uploaded file", async () => { - renderBuilder("/portal/pipelines/new"); + renderBuilder("/processor/pipelines/new"); fireEvent.change(await screen.findByRole("textbox"), { target: { value: "Watermarked" }, @@ -187,7 +190,7 @@ describe("PipelineBuilder", () => { }); it("deletes an existing pipeline after confirmation", async () => { - renderBuilder("/portal/pipelines/plc-1"); + renderBuilder("/processor/pipelines/plc-1"); fireEvent.click(await screen.findByText("portal.pipelines.detail.delete")); fireEvent.click(await screen.findByText("portal.pipelines.delete.confirm")); @@ -197,7 +200,7 @@ describe("PipelineBuilder", () => { }); it("prompts to save or discard when leaving with unsaved edits", async () => { - renderBuilder("/portal/pipelines/new"); + renderBuilder("/processor/pipelines/new"); fireEvent.change(await screen.findByRole("textbox"), { target: { value: "Draft" }, @@ -212,7 +215,7 @@ describe("PipelineBuilder", () => { }); it("leaves immediately when there are no unsaved edits", async () => { - renderBuilder("/portal/pipelines/new"); + renderBuilder("/processor/pipelines/new"); await screen.findByRole("textbox"); fireEvent.click(screen.getByText("portal.pipelines.composer.cancel")); diff --git a/frontend/editor/src/portal/views/Pipelines.test.tsx b/frontend/editor/src/portal/views/Pipelines.test.tsx index 5bc20616bb..6e19469cdb 100644 --- a/frontend/editor/src/portal/views/Pipelines.test.tsx +++ b/frontend/editor/src/portal/views/Pipelines.test.tsx @@ -48,14 +48,17 @@ const RESPONSE: PipelinesOverviewResponse = { ], }; -function renderView(initial = "/portal/pipelines") { +function renderView(initial = "/processor/pipelines") { return render( - } /> - builder new} /> + } /> builder new} + /> + pipeline page} /> diff --git a/frontend/editor/src/proprietary/routes/adminRouteExtensions.tsx b/frontend/editor/src/proprietary/routes/adminRouteExtensions.tsx index 89676d2d29..afb79c0a7f 100644 --- a/frontend/editor/src/proprietary/routes/adminRouteExtensions.tsx +++ b/frontend/editor/src/proprietary/routes/adminRouteExtensions.tsx @@ -1,6 +1,7 @@ import { lazy } from "react"; import type { ReactElement } from "react"; import { Route } from "react-router-dom"; +import { PORTAL_BASENAME } from "@app/routes/portalBasename"; // The portal ships as a lazy chunk of the editor. It's included in dev (so it's // always available to work on) and in production builds made with @@ -24,12 +25,18 @@ const PortalApp = includePortal : null; /** - * The portal mounts as an admin-only route-set at /portal/*. Access is gated - * inside PortalApp (its own AuthProvider + AuthGate, plus server enforcement), - * so this just wires the lazy route into the editor's router when the portal is - * included in this build. + * The portal mounts as an admin-only route-set at PORTAL_BASENAME (/processor/*). + * Access is gated inside PortalApp (its own AuthProvider + AuthGate, plus server + * enforcement), so this just wires the lazy route into the editor's router when + * the portal is included in this build. */ export function getAdminRouteExtensions(): ReactElement[] { if (!PortalApp) return []; - return [} />]; + return [ + } + />, + ]; }