Skip to content

Commit 119eb1f

Browse files
authored
Portal: move the admin route from /portal to /processor (Stirling-Tools#6933)
## What this changes Moves the admin portal's browser route from **`/portal`** to **`/processor`**, to match the "Processor" product name (the in-app app switcher already says "processor"). - `PORTAL_BASENAME` `"/portal"` → `"/processor"` — the single source of truth for all portal paths on the frontend, so every `toPortalPath(...)` link and redirect follows automatically. - `adminRouteExtensions` now mounts at `` `${PORTAL_BASENAME}/*` `` instead of a hardcoded `"/portal/*"`, so the route can't drift from the constant. - **Backend:** `RequestUriUtils.isStaticResource` now treats `/processor` (not `/portal`) as the SPA shell, so a direct navigation or hard refresh to `/processor` serves the app instead of 404ing. (This is the only backend URL reference — there's no Spring Security matcher for it.) - Updated the two portal route tests and the doc comments that named the old path. **Not changed:** the `@portal/*` import alias — that's the code layer / flavor-layering path, not the URL. Renaming it would be a much larger, unrelated churn. The portal isn't publicly launched yet, so there are no existing links to preserve — no redirect from the old path is included. ## Testing - `task frontend:typecheck:all`, full test suite (1,211), lint, format — green - `./gradlew :common:test --tests "*RequestUriUtilsTest"` — green
1 parent d3638d7 commit 119eb1f

10 files changed

Lines changed: 48 additions & 34 deletions

File tree

app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,13 @@ public static boolean isStaticResource(String contextPath, String requestURI) {
5757
return true;
5858
}
5959

60-
// Admin portal SPA shell. Served publicly like the editor root so a direct
61-
// nav / refresh to /portal loads the app (the JWT lives in localStorage, not
62-
// a cookie, so the server can't authenticate the navigation itself). The
60+
// Admin portal SPA shell (mounted at /processor — must match the frontend
61+
// PORTAL_BASENAME). Served publicly like the editor root so a direct nav /
62+
// refresh to /processor loads the app (the JWT lives in localStorage, not a
63+
// cookie, so the server can't authenticate the navigation itself). The
6364
// portal gates access via its own auth gate + RequirePortalAccess, and its
6465
// data APIs stay protected, so serving the shell pre-auth is safe.
65-
if (normalizedUri.equals("/portal") || normalizedUri.startsWith("/portal/")) {
66+
if (normalizedUri.equals("/processor") || normalizedUri.startsWith("/processor/")) {
6667
return true;
6768
}
6869

app/common/src/test/java/stirling/software/common/util/RequestUriUtilsTest.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,10 @@ void testIsStaticResource_mobileScannerPath() {
7575

7676
@Test
7777
void testIsStaticResource_portalShell() {
78-
// The admin portal SPA shell is served pre-auth so it's directly navigable.
79-
assertTrue(RequestUriUtils.isStaticResource("/portal"));
80-
assertTrue(RequestUriUtils.isStaticResource("/portal/users"));
81-
assertTrue(RequestUriUtils.isStaticResource("/app", "/app/portal"));
78+
// The admin portal SPA shell (/processor) is served pre-auth so it's directly navigable.
79+
assertTrue(RequestUriUtils.isStaticResource("/processor"));
80+
assertTrue(RequestUriUtils.isStaticResource("/processor/users"));
81+
assertTrue(RequestUriUtils.isStaticResource("/app", "/app/processor"));
8282
}
8383

8484
// --- isFrontendRoute tests ---

frontend/editor/src/core/routes/portalBasename.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
* mount point without importing portal code — build flavors that ship no
55
* portal (core, desktop, prototypes) must never resolve @portal.
66
*/
7-
export const PORTAL_BASENAME = "/portal";
7+
export const PORTAL_BASENAME = "/processor";

frontend/editor/src/portal/PortalApp.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ function ThemedSuiProvider({ children }: { children: ReactNode }) {
1717
}
1818

1919
/**
20-
* The portal, mounted as a route-set under /portal/* inside the editor app (via
21-
* the admin-route seam). It supplies its own providers and its own i18next
20+
* The portal, mounted as a route-set under /processor/* inside the editor app
21+
* (via the admin-route seam). It supplies its own providers and its own i18next
2222
* instance (the `portal` namespace), but NOT a router — the editor's
2323
* <BrowserRouter> is the one and only router; the portal's routes are relative
24-
* to the /portal mount (see ViewRouter).
24+
* to the /processor mount (see ViewRouter).
2525
*
2626
* The provider stack itself is a per-flavor seam (see {@link PortalProviders}):
2727
* self-hosted mounts the account-link layer, SaaS does not.

frontend/editor/src/portal/ViewRouter.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { DeveloperDocs } from "@portal/views/DeveloperDocs";
1515
import { Procurement } from "@portal/views/Procurement";
1616
import { VIEW_PATHS, toPortalPath } from "@portal/contexts/ViewContext";
1717

18-
// The portal mounts as a route-set under /portal/* in the editor app, so these
18+
// The portal mounts as a route-set under /processor/* in the editor app, so these
1919
// child routes are relative to that base: strip the leading slash from the
2020
// logical VIEW_PATHS, and home is the index route. Redirects use toPortalPath
2121
// so they resolve to the portal, not the editor root.

frontend/editor/src/portal/auth/editorUrl.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
* bouncing non-admins out).
44
*
55
* Sourced from VITE_EDITOR_URL so it's configurable per deploy rather than
6-
* hardcoded, falling back to "/" (the editor serves the portal at /portal on
7-
* the same origin, so the root is the editor). For dev cross-app navigation to
6+
* hardcoded, falling back to "/" (the editor serves the portal at /processor
7+
* on the same origin, so the root is the editor). For dev cross-app navigation to
88
* a separately-running editor, set VITE_EDITOR_URL in editor/.env.local.
99
*/
1010
export const EDITOR_URL = import.meta.env.VITE_EDITOR_URL || "/";

frontend/editor/src/portal/contexts/ViewContext.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export const VIEW_PATHS: Record<ViewId, string> = {
6161
*/
6262
export { PORTAL_BASENAME };
6363

64-
/** Logical view path -> full app path (e.g. "/users" -> "/portal/users"). */
64+
/** Logical view path -> full app path (e.g. "/users" -> "/processor/users"). */
6565
export function toPortalPath(viewPath: string): string {
6666
return `${PORTAL_BASENAME}${viewPath === "/" ? "" : viewPath}`;
6767
}

frontend/editor/src/portal/views/PipelineBuilder.test.tsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,12 @@ function renderBuilder(initial: string) {
104104
return render(
105105
<MemoryRouter initialEntries={[initial]}>
106106
<Routes>
107-
<Route path="/portal/pipelines/new" element={<PipelineBuilder />} />
108-
<Route path="/portal/pipelines/:id" element={<PipelineBuilder />} />
109-
<Route path="/portal/pipelines" element={<div>pipelines list</div>} />
107+
<Route path="/processor/pipelines/new" element={<PipelineBuilder />} />
108+
<Route path="/processor/pipelines/:id" element={<PipelineBuilder />} />
109+
<Route
110+
path="/processor/pipelines"
111+
element={<div>pipelines list</div>}
112+
/>
110113
</Routes>
111114
</MemoryRouter>,
112115
);
@@ -131,7 +134,7 @@ describe("PipelineBuilder", () => {
131134
});
132135

133136
it("builds a new pipeline: name it, add a tool, and save", async () => {
134-
renderBuilder("/portal/pipelines/new");
137+
renderBuilder("/processor/pipelines/new");
135138

136139
// The name field is the only textbox before the picker opens.
137140
fireEvent.change(await screen.findByRole("textbox"), {
@@ -157,7 +160,7 @@ describe("PipelineBuilder", () => {
157160
});
158161

159162
it("runs an existing pipeline and reports success", async () => {
160-
renderBuilder("/portal/pipelines/plc-1");
163+
renderBuilder("/processor/pipelines/plc-1");
161164

162165
fireEvent.click(await screen.findByText("portal.pipelines.detail.run"));
163166

@@ -168,7 +171,7 @@ describe("PipelineBuilder", () => {
168171
});
169172

170173
it("blocks saving a step that needs an uploaded file", async () => {
171-
renderBuilder("/portal/pipelines/new");
174+
renderBuilder("/processor/pipelines/new");
172175

173176
fireEvent.change(await screen.findByRole("textbox"), {
174177
target: { value: "Watermarked" },
@@ -187,7 +190,7 @@ describe("PipelineBuilder", () => {
187190
});
188191

189192
it("deletes an existing pipeline after confirmation", async () => {
190-
renderBuilder("/portal/pipelines/plc-1");
193+
renderBuilder("/processor/pipelines/plc-1");
191194

192195
fireEvent.click(await screen.findByText("portal.pipelines.detail.delete"));
193196
fireEvent.click(await screen.findByText("portal.pipelines.delete.confirm"));
@@ -197,7 +200,7 @@ describe("PipelineBuilder", () => {
197200
});
198201

199202
it("prompts to save or discard when leaving with unsaved edits", async () => {
200-
renderBuilder("/portal/pipelines/new");
203+
renderBuilder("/processor/pipelines/new");
201204

202205
fireEvent.change(await screen.findByRole("textbox"), {
203206
target: { value: "Draft" },
@@ -212,7 +215,7 @@ describe("PipelineBuilder", () => {
212215
});
213216

214217
it("leaves immediately when there are no unsaved edits", async () => {
215-
renderBuilder("/portal/pipelines/new");
218+
renderBuilder("/processor/pipelines/new");
216219

217220
await screen.findByRole("textbox");
218221
fireEvent.click(screen.getByText("portal.pipelines.composer.cancel"));

frontend/editor/src/portal/views/Pipelines.test.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,17 @@ const RESPONSE: PipelinesOverviewResponse = {
4848
],
4949
};
5050

51-
function renderView(initial = "/portal/pipelines") {
51+
function renderView(initial = "/processor/pipelines") {
5252
return render(
5353
<MemoryRouter initialEntries={[initial]}>
5454
<Routes>
55-
<Route path="/portal/pipelines" element={<Pipelines />} />
56-
<Route path="/portal/pipelines/new" element={<div>builder new</div>} />
55+
<Route path="/processor/pipelines" element={<Pipelines />} />
5756
<Route
58-
path="/portal/pipelines/:id"
57+
path="/processor/pipelines/new"
58+
element={<div>builder new</div>}
59+
/>
60+
<Route
61+
path="/processor/pipelines/:id"
5962
element={<div>pipeline page</div>}
6063
/>
6164
</Routes>

frontend/editor/src/proprietary/routes/adminRouteExtensions.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { lazy } from "react";
22
import type { ReactElement } from "react";
33
import { Route } from "react-router-dom";
4+
import { PORTAL_BASENAME } from "@app/routes/portalBasename";
45

56
// The portal ships as a lazy chunk of the editor. It's included in dev (so it's
67
// always available to work on) and in production builds made with
@@ -24,12 +25,18 @@ const PortalApp = includePortal
2425
: null;
2526

2627
/**
27-
* The portal mounts as an admin-only route-set at /portal/*. Access is gated
28-
* inside PortalApp (its own AuthProvider + AuthGate, plus server enforcement),
29-
* so this just wires the lazy route into the editor's router when the portal is
30-
* included in this build.
28+
* The portal mounts as an admin-only route-set at PORTAL_BASENAME (/processor/*).
29+
* Access is gated inside PortalApp (its own AuthProvider + AuthGate, plus server
30+
* enforcement), so this just wires the lazy route into the editor's router when
31+
* the portal is included in this build.
3132
*/
3233
export function getAdminRouteExtensions(): ReactElement[] {
3334
if (!PortalApp) return [];
34-
return [<Route key="portal" path="/portal/*" element={<PortalApp />} />];
35+
return [
36+
<Route
37+
key="portal"
38+
path={`${PORTAL_BASENAME}/*`}
39+
element={<PortalApp />}
40+
/>,
41+
];
3542
}

0 commit comments

Comments
 (0)