Skip to content

Commit 80f0a75

Browse files
author
root
committed
Fix frontend login route cleanup
1 parent 502db90 commit 80f0a75

15 files changed

Lines changed: 330 additions & 47 deletions

docs/PRD.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ At the product level, the current version delivers three core values:
7171
| Route | Page | Notes |
7272
| --- | --- | --- |
7373
| `/` | Welcome page | Default entry when no reachable workspace is available; also provides workspace open and settings entry |
74-
| `/auth` | Auth page | The actual frontend login route |
74+
| `/login` | Auth page | Frontend login route |
7575
| `/workspace` | Workspace page | Main workspace; desktop and mobile use different experiences |
7676
| `/settings` | Settings page | Desktop and mobile share the same capability set, with different navigation structures |
7777
| `*` | 404 page | Displays unmatched paths and provides a return-home action |
@@ -156,7 +156,7 @@ The main content is a centered welcome card. Mobile only adjusts styling, not th
156156
- The welcome page itself has no complex error state.
157157
- If the app has already resolved at least one workspace, the startup flow usually redirects the user to `/workspace`, so the welcome page is not a long-lived state.
158158

159-
## 4.2 Auth Page `/auth`
159+
## 4.2 Auth Page `/login`
160160

161161
### 4.2.1 Page Goal
162162

@@ -1568,14 +1568,15 @@ This chapter covers only two kinds of content:
15681568

15691569
### 8.1 Auth Route Implementation Note
15701570

1571-
The main product review chapters consistently describe the login page as `/auth`, because that is the actual mounted frontend login page.
1571+
The frontend login page is mounted at `/login`.
15721572

1573-
But one implementation fact still needs to be preserved:
1573+
Backend auth endpoints remain under `/auth/*`, such as:
15741574

1575-
- Server-side SPA navigation for unauthenticated users redirects to `/login`
1576-
- Frontend bootstrap actively navigates users to `/auth` when auth fails
1575+
- `/auth/status`
1576+
- `/auth/login`
1577+
- `/auth/logout`
15771578

1578-
So the product document uses `/auth` in the main body, while preserving `/login` here as an implementation fact on the server side.
1579+
`/auth` is not a frontend page route and should not be described as one in product-facing flow documentation.
15791580

15801581
### 8.2 Theme Persistence Boundary
15811582

docs/PRD.zh-CN.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ Coder Studio 是一款**面向自部署环境、以 Deploy once, coding everywhe
7474
| 路由 | 页面 | 说明 |
7575
| --- | --- | --- |
7676
| `/` | 欢迎页 | 当没有可进入的 workspace 时作为默认入口;也承接打开工作区与进入设置 |
77-
| `/auth` | 登录页 | 当前前端实际登录页 |
77+
| `/login` | 登录页 | 前端登录页路由 |
7878
| `/workspace` | 工作区页 | 主工作区;桌面与移动端为两套不同体验 |
7979
| `/settings` | 设置页 | 桌面与移动端共用同一页能力,但导航结构不同 |
8080
| `*` | 404 页面 | 展示未命中路径并提供返回首页操作 |
@@ -163,7 +163,7 @@ Coder Studio 是一款**面向自部署环境、以 Deploy once, coding everywhe
163163

164164
---
165165

166-
## 4.2 登录页 `/auth`
166+
## 4.2 登录页 `/login`
167167

168168
### 4.2.1 页面目标
169169

@@ -1592,14 +1592,15 @@ Supervisor 的编辑类交互由两种容器承载:
15921592

15931593
### 8.1 认证路由实现备注
15941594

1595-
产品评审正文统一把登录页写作 `/auth`,因为这是当前前端实际挂载的登录页面
1595+
前端登录页挂载在 `/login`
15961596

1597-
但实现上还存在一个需要保留的事实
1597+
后端鉴权接口仍然位于 `/auth/*`,例如
15981598

1599-
- 服务端未认证的 SPA 导航会重定向到 `/login`
1600-
- 前端 bootstrap 在鉴权失败时会主动把用户导航到 `/auth`
1599+
- `/auth/status`
1600+
- `/auth/login`
1601+
- `/auth/logout`
16011602

1602-
因此,产品文档正文以 `/auth` 页面描述登录流,附录保留 `/login` 的服务端实现事实
1603+
`/auth` 不是前端页面路由,产品流程文档不应再把它描述成登录页
16031604

16041605
### 8.2 Theme 持久化边界
16051606

docs/superpowers/plans/2026-05-01-mobile-friendly-phase-0.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -139,13 +139,13 @@ function useWorkspaceBootstrap() {
139139

140140
const authRequired = authEnabled === true;
141141
if (authRequired && !authenticated) {
142-
if (location.pathname !== '/auth') {
143-
navigate('/auth', { replace: true });
142+
if (location.pathname !== '/login') {
143+
navigate('/login', { replace: true });
144144
}
145145
return;
146146
}
147147

148-
if (location.pathname === '/auth') {
148+
if (location.pathname === '/login') {
149149
navigate('/', { replace: true });
150150
return;
151151
}
@@ -234,7 +234,7 @@ export function DesktopShell() {
234234
const location = useLocation();
235235
const authRequired = authEnabled === true;
236236
const authUnknown = authEnabled === null;
237-
const shouldShowLogin = authRequired && !authenticated && location.pathname === '/auth';
237+
const shouldShowLogin = authRequired && !authenticated && location.pathname === '/login';
238238
const shouldShowGlobalConfigDriftBanner =
239239
!shouldShowLogin && !authUnknown && !location.pathname.startsWith('/settings');
240240

@@ -265,7 +265,7 @@ export function DesktopShell() {
265265
) : (
266266
<Routes>
267267
<Route path="/" element={<WelcomePage />} />
268-
<Route path="/auth" element={<LoginPage />} />
268+
<Route path="/login" element={<LoginPage />} />
269269
<Route path="/workspace" element={<WorkspacePage />} />
270270
<Route path="/settings" element={<SettingsPage />} />
271271
</Routes>
@@ -371,8 +371,8 @@ describe('DesktopShell auth gating', () => {
371371
expect(screen.getByText('WorkspacePage')).toBeInTheDocument();
372372
});
373373

374-
it('renders the explicit /auth route when auth is enabled and user is unauthenticated', () => {
375-
window.history.replaceState({}, '', '/auth');
374+
it('renders the explicit /login route when auth is enabled and user is unauthenticated', () => {
375+
window.history.replaceState({}, '', '/login');
376376
const store = createStore();
377377
store.set(connectionStatusAtom, 'connected');
378378
store.set(authEnabledAtom, true);
@@ -548,7 +548,7 @@ pnpm dev:web
548548

549549
Open the dev URL the script prints. Verify:
550550
- App loads without console errors
551-
- Routes (`/`, `/workspace`, `/settings`, `/auth`) render the same as before
551+
- Routes (`/`, `/workspace`, `/settings`, `/login`) render the same as before
552552

553553
Stop the dev server.
554554

@@ -1273,7 +1273,7 @@ In a wide browser window:
12731273
- `/` shows Welcome (or redirects to `/workspace`)
12741274
- `/workspace` renders the workspace shell with all panels visible
12751275
- `/settings` opens settings normally
1276-
- `/auth` (when auth required) renders login
1276+
- `/login` (when auth required) renders login
12771277
- Connection banner appears when WS reconnects
12781278
- Command palette opens via shortcut
12791279
- No console errors

docs/superpowers/plans/2026-05-01-mobile-friendly-phase-1.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ Use the shared bootstrap hook and keep non-workspace routes on the existing page
136136
```tsx
137137
<Routes>
138138
<Route path="/" element={<WelcomePage />} />
139-
<Route path="/auth" element={<LoginPage />} />
139+
<Route path="/login" element={<LoginPage />} />
140140
<Route path="/settings" element={<SettingsPage />} />
141141
<Route path="/workspace" element={<MobileWorkspaceScaffold />} />
142142
</Routes>

docs/superpowers/specs/2026-04-13-coder-studio-design.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1751,13 +1751,11 @@ app.register(authPlugin, {
17511751
```
17521752

17531753
Plugin 内部:
1754-
- 非白名单路径(`/api/auth/*``/healthz``/internal/hooks/*`)以外都要有效 cookie
1754+
- 非白名单路径(`/auth/*``/healthz`)以外都要有效 cookie
17551755
- WS 连接前 upgrade 阶段也校验 cookie
17561756
- 登录失败 → 记录到 `auth_failures` → 命中阈值 → 写入 `auth_blocked_ips`
17571757
- IP 黑名单提前短路 (`ipBlocker` middleware)
17581758

1759-
内部 hooks endpoint `/internal/hooks/:event` **不经过** auth,但必须带查询参数 token 且只接受 `127.0.0.1` 连接(server 明确 bind 时也是如此)。
1760-
17611759
### 4.13 服务器生命周期与信号处理
17621760

17631761
**启动流程**已在 §4.0 / §4.1 描述。本节聚焦关闭和异常退出。
@@ -1980,7 +1978,7 @@ main.tsx
19801978
| `/workspace/:id` | `<WorkspacePage workspaceId={id}>` |
19811979
| `/settings` | `<SettingsPage>` |
19821980
| `/settings/:section` | `<SettingsPage section={section}>` |
1983-
| `/auth` | `<AuthPage>` (Phase 2) |
1981+
| `/login` | `<AuthPage>` (Phase 2) |
19841982

19851983
### 5.2 Jotai Atom 组织
19861984

docs/superpowers/specs/2026-04-30-mobile-friendly-design.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ drawer 永远盖过 sheet,sheet 永远盖过 chat。
391391

392392
### 9.3 认证 / 会话
393393

394-
- token 过期:拦截 ws 重连失败,自动 navigate 到 `/auth`,登录后回原 path
394+
- token 过期:拦截 ws 重连失败,自动 navigate 到 `/login`,登录后回原 path
395395
- 移动端 auth 与桌面共用 atoms,shell 切换无感
396396

397397
### 9.4 应用前后台切换(移动专属)

docs/superpowers/specs/2026-05-01-mobile-friendly-phase-4a-design.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ This phase exists because Phase 3 already established the mobile workspace shell
1616
## 2. In Scope
1717

1818
- Mobile full-screen treatment for `/` via `WelcomePage`
19-
- Mobile full-screen treatment for `/auth` via `LoginPage`
19+
- Mobile full-screen treatment for `/login` via `LoginPage`
2020
- Mobile full-screen treatment for `/settings` via `SettingsPage`
2121
- Mobile-only `Settings` navigation stack inside the existing `/settings` route
2222
- Mobile-safe return behavior from `Settings`
@@ -43,7 +43,7 @@ This phase also does not change:
4343

4444
## 4. Design Constraints
4545

46-
- Route paths remain unchanged: `/`, `/auth`, `/workspace`, `/settings`
46+
- Route paths remain unchanged: `/`, `/login`, `/workspace`, `/settings`
4747
- `MobileShell` remains responsible for shell selection, not for introducing mobile-only routes
4848
- Desktop `SettingsPage` keeps the current sidebar + content layout
4949
- Mobile `SettingsPage` must meet the spec requirement of `category -> detail`
@@ -57,7 +57,7 @@ This phase also does not change:
5757
The existing route model stays intact:
5858

5959
- `/` -> `WelcomePage`
60-
- `/auth` -> `LoginPage`
60+
- `/login` -> `LoginPage`
6161
- `/workspace` -> `WorkspacePage` on desktop, `MobileWorkspaceScaffold` on mobile
6262
- `/settings` -> `SettingsPage`
6363

@@ -280,4 +280,3 @@ Phase 4A is complete when all of the following are true:
280280
- On mobile, leaving the settings root uses history-first navigation with workspace/home fallback
281281
- On desktop, `Settings` still renders the existing sidebar/content layout
282282
- No Phase 3 mobile workspace behavior regresses
283-

packages/server/src/app-routing.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,22 @@ describe("app routing", () => {
141141
expect(response.body).toContain('<div id="root">shell</div>');
142142
});
143143

144+
it("serves index.html for the /login frontend route", async () => {
145+
const instance = await createApp();
146+
147+
const response = await instance.inject({
148+
method: "GET",
149+
url: "/login",
150+
headers: {
151+
accept: "text/html",
152+
},
153+
});
154+
155+
expect(response.statusCode).toBe(200);
156+
expect(response.headers["content-type"]).toContain("text/html");
157+
expect(response.body).toContain('<div id="root">shell</div>');
158+
});
159+
144160
it("serves the built entrypoint when requesting /index.html directly", async () => {
145161
const instance = await createApp();
146162

packages/server/src/auth/plugin.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,4 +295,57 @@ describe("auth login protection", () => {
295295
error: "Authentication required",
296296
});
297297
});
298+
299+
it("does not treat the legacy /auth frontend path as a login redirect target", async () => {
300+
const response = await app.inject({
301+
method: "GET",
302+
url: "/auth",
303+
headers: {
304+
accept: "text/html",
305+
},
306+
});
307+
308+
expect(response.statusCode).toBe(401);
309+
expect(response.json()).toMatchObject({
310+
ok: false,
311+
error: "Authentication required",
312+
});
313+
});
314+
315+
it("does not treat unknown /auth/* paths as frontend navigations", async () => {
316+
const response = await app.inject({
317+
method: "GET",
318+
url: "/auth/unknown",
319+
headers: {
320+
accept: "text/html",
321+
},
322+
});
323+
324+
expect(response.statusCode).toBe(401);
325+
expect(response.json()).toMatchObject({
326+
ok: false,
327+
error: "Authentication required",
328+
});
329+
});
330+
331+
it("does not fall through to the SPA for GET /auth/login or GET /auth/logout", async () => {
332+
const loginResponse = await app.inject({
333+
method: "GET",
334+
url: "/auth/login",
335+
headers: {
336+
accept: "text/html",
337+
},
338+
});
339+
340+
const logoutResponse = await app.inject({
341+
method: "GET",
342+
url: "/auth/logout",
343+
headers: {
344+
accept: "text/html",
345+
},
346+
});
347+
348+
expect(loginResponse.statusCode).toBe(404);
349+
expect(logoutResponse.statusCode).toBe(404);
350+
});
298351
});

packages/server/src/web-ui-routing.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { FastifyRequest } from "fastify";
22

3-
const RESERVED_PREFIXES = ["/api/", "/internal/", "/assets/"];
4-
const RESERVED_EXACT_PATHS = new Set(["/api", "/assets", "/login", "/healthz", "/internal", "/ws"]);
3+
const RESERVED_PREFIXES = ["/api/", "/auth/", "/internal/", "/assets/"];
4+
const RESERVED_EXACT_PATHS = new Set(["/api", "/auth", "/assets", "/healthz", "/internal", "/ws"]);
55
const ROOT_PUBLIC_FILE_PATHS = new Set(["/favicon.ico", "/index.html", "/task-complete.wav"]);
66

77
export function getRequestPathname(url: string): string {

0 commit comments

Comments
 (0)