Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/skills/use-existing-components/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

**在开发中,如非必要,禁止使用 `localStorage` 和 `sessionStorage`,避免用户篡改数据。**

- 敏感数据(如 token、用户信息)必须使用 **内存存储**(参考 `../../../src/infra/Http.ts` 中的 `TokenManager` 类),不持久化到浏览器存储
- 认证状态使用服务端 Session + HttpOnly `SESSIONID` Cookie;前端敏感数据(用户信息、权限标识)使用 **内存存储**(参考 `src/contexts/AuthContext.tsx`),不持久化到浏览器存储
- 确需持久化的非敏感配置(如主题偏好、布局宽度),可例外使用 `localStorage`(参考 `ThemeContext` 和 `LayoutWidthContext` 的实现)
- **禁止**将权限标识、用户角色、付费状态等安全相关数据放入 `localStorage`/`sessionStorage`,必须通过 API 实时校验

Expand Down
10 changes: 6 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
实现任何 UI 时,**必须优先使用 `src/components/` 下已封装的自定义组件**,禁止使用 antd 原生组件或手写原生 HTML 元素替代。完整组件清单和使用指南见 `.claude/skills/use-existing-components/SKILL.md`。

### 2. 数据存储:内存优先
- **敏感数据**(token、用户信息、权限标识)必须使用内存存储,**禁止**使用 `localStorage`/`sessionStorage`,防止用户篡改
- **认证状态**由服务端 Session + HttpOnly `SESSIONID` Cookie 管理,前端不读取、不保存 token
- **敏感数据**(用户信息、权限标识)必须使用内存存储,**禁止**使用 `localStorage`/`sessionStorage`,防止用户篡改
- **非敏感配置**(主题偏好、布局宽度)方可例外使用 `localStorage`
- 参考:`src/infra/Http.ts` 中的 `TokenManager`(内存存储)vs `ThemeContext`(localStorage 例外)
- 参考:`src/contexts/AuthContext.tsx` 的内存用户态 vs `ThemeContext`(localStorage 例外)

### 3. 组件开发流程
```
Expand Down Expand Up @@ -40,8 +41,9 @@ npm run format # Prettier 格式化
## 项目架构要点

- **Provider 嵌套顺序**:BrowserRouter → Theme → Auth → LayoutWidth → SiteSettings → Message → Confirm
- **Token 认证**:从 Cookie 提取 S_TOKEN,存于内存 TokenManager,HTTP 拦截器自动附加 Bearer header
- **WebSocket**:`notificationWS` 单例,生命周期绑定 AuthContext(登录连/登出断),支持自动重连
- **Session 认证**:服务端设置 HttpOnly `SESSIONID`,Axios 统一 `withCredentials: true`,不再附加 Bearer header
- **登录方式**:密码 + TOTP、Passkey(WebAuthn)、邮箱验证码恢复;登录后统一通过 `/user/info` 恢复用户态
- **WebSocket**:`notificationWS` 单例,生命周期绑定 AuthContext(登录连/登出断),握手依赖同源 Cookie,支持自动重连
- **路由**:`/auth` 和 `/admin/*` 免维护模式守卫,其余路由受 `MaintenanceGuard` 保护
- **样式**:CSS Modules + CSS 自定义属性(`data-theme` 驱动明暗主题,`data-width-mode` 驱动页面宽度)
- **HTTP**:`src/infra/Http.ts` 封装,拦截器做了完整的中文业务状态码映射
7 changes: 4 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ BrowserRouter → ThemeProvider → AuthProvider → LayoutWidthProvider → Sit

### 认证与 HTTP 层

- Token 使用 **内存存储**(`TokenManager` 类,见 `src/infra/Http.ts`),不使用 localStorage/Cookie 直接读写,避免 XSS 风险
- Token 来源:登录时从响应的 `Set-Cookie` header 中提取 `S_TOKEN`,或从 `document.cookie` 回退读取
- 所有 API 请求通过 `src/infra/Http.ts` 的 `http` 实例发送,拦截器自动附加 `Authorization: Bearer <token>` header
- 认证使用 **服务端 Session + HttpOnly `SESSIONID` Cookie**;前端不读取、不保存 token
- 所有 API 请求通过 `src/infra/Http.ts` 的 `http` 实例发送,统一 `withCredentials: true`,不再附加 `Authorization` header
- 页面启动时通过 `/user/info` 恢复用户态;匿名 1101 需要跳过全局会话失效跳转
- 支持密码 + TOTP、Passkey(WebAuthn)、邮箱验证码恢复登录
- 后端返回统一结构 `{ code, data, message/msg, success }`,响应拦截器做了完整的业务状态码映射(400-504 均有中文错误提示)
- API Service 层位于 `src/services/`,每个 service 文件封装一类业务接口

Expand Down
15 changes: 7 additions & 8 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,9 @@ Vite loads environment files according to the current mode, including `.env.deve

| Variable | Purpose |
| -------------------------- | --------------------------------------------------------------------------------- |
| `VITE_API_BASE_URL` | Direct API URL, or the target URL for the Vite development proxy |
| `VITE_WS_URL` | Base URL of the WebSocket service |
| `VITE_USE_PROXY` | When set to the string `"true"`, HTTP requests use the same-origin `/api/v1` path |
| `VITE_REQUIRE_CREDENTIALS` | When set to the string `"true"`, enables Axios `withCredentials` |
| `VITE_API_BASE_URL` | Direct API URL, or the target URL for the Vite development proxy |
| `VITE_WS_URL` | Base URL of the WebSocket service |
| `VITE_USE_PROXY` | When set to the string `"true"`, HTTP requests use the same-origin `/api/v1` path |

### Request Modes

Expand Down Expand Up @@ -192,10 +191,10 @@ The `/rd` layout additionally mounts `RdOrgProvider` for the current organizatio

### Authentication and Storage

- The application restores the token from the `S_TOKEN` Cookie at startup.
- During runtime, the token is managed by `AuthContext` and the in-memory `TokenManager`.
- The Axios request interceptor adds the Bearer token automatically.
- Sensitive state such as tokens, user data, roles, and permissions must not be written to `localStorage` or `sessionStorage`.
- Authentication uses a server-side session with an HttpOnly `SESSIONID` Cookie; the frontend never reads or stores tokens.
- At startup the app restores the user state through `/user/info`, and Axios sends Cookie credentials on every request.
- Supported sign-in methods include password + TOTP, Passkey (WebAuthn), and email-code account recovery.
- Sensitive state such as user data, roles, and permissions must not be written to `localStorage` or `sessionStorage`.
- Non-sensitive data such as themes, layout preferences, public configuration caches, and content drafts may be persisted when appropriate.
- Frontend permission checks only control the UI; the backend must enforce actual permissions.

Expand Down
15 changes: 7 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,9 @@ npm run dev

| 变量 | 作用 |
| -------------------------- | --------------------------------------------------------- |
| `VITE_API_BASE_URL` | API 直连地址,或 Vite 开发代理的目标地址 |
| `VITE_WS_URL` | WebSocket 服务基础地址 |
| `VITE_USE_PROXY` | 为字符串 `"true"` 时,HTTP 请求使用同源 `/api/v1` |
| `VITE_REQUIRE_CREDENTIALS` | 为字符串 `"true"` 时,为 Axios 请求启用 `withCredentials` |
| `VITE_API_BASE_URL` | API 直连地址,或 Vite 开发代理的目标地址 |
| `VITE_WS_URL` | WebSocket 服务基础地址 |
| `VITE_USE_PROXY` | 为字符串 `"true"` 时,HTTP 请求使用同源 `/api/v1` |

### 请求模式

Expand Down Expand Up @@ -192,10 +191,10 @@ BrowserRouter

### 认证与存储

- 页面启动时从 `S_TOKEN` Cookie 恢复 Token。
- 运行期间由 `AuthContext` 和内存 `TokenManager` 管理 Token。
- Axios 请求拦截器自动附加 Bearer Token。
- Token、用户信息、角色和权限等敏感状态不得写入 `localStorage` 或 `sessionStorage`。
- 认证使用服务端 Session + HttpOnly `SESSIONID` Cookie;前端不读取、不保存 token。
- 页面启动时通过 `/user/info` 恢复用户态,Axios 统一携带 Cookie 凭据。
- 支持密码 + TOTP、Passkey(WebAuthn)、邮箱验证码恢复登录。
- 用户信息、角色和权限等敏感状态不得写入 `localStorage` 或 `sessionStorage`。
- 主题、布局、公开配置缓存和内容草稿等非敏感数据可以按需持久化。
- 所有前端权限判断只用于界面控制,后端仍必须执行实际权限校验。

Expand Down
4 changes: 2 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 接口与数据协议(前端新增界面)

本目录收录本次新增的 5 个用户界面所需的接口与数据协议。这些界面当前均使用 `src` 内的本地 mock 数据渲染,下列协议用于指导后端对接时提供一致的契约。
本目录收录用户界面相关的接口与数据协议。部分界面仍使用 `src` 内的本地 mock 数据渲染;账户安全已接入服务端会话、登录设备与 Passkey 接口。

涉及界面:

Expand All @@ -17,7 +17,7 @@
所有接口基于 `../src/infra/Http.ts` 中的 `HttpClient`,遵循以下约定:

- **Base URL**:`VITE_USE_PROXY=true` 时为 `/api/v1`,否则为 `VITE_API_BASE_URL`(如 `http://8.148.82.38:8078`)。
- **鉴权**:请求拦截器自动附加 `Authorization: Bearer <token>`,token 来自内存态 `TokenManager`(取自 `S_TOKEN` Cookie),**不**使用 localStorage。
- **鉴权**:服务端 Session + HttpOnly `SESSIONID` Cookie;Axios 统一 `withCredentials: true`,前端不读取、不保存 token。
- **响应包络(HttpResponse)**:
```ts
interface HttpResponse<T = any> {
Expand Down
149 changes: 90 additions & 59 deletions docs/account-security.md
Original file line number Diff line number Diff line change
@@ -1,84 +1,115 @@
# 账户安全(Account Security)

入口:个人中心(`/personal?tab=security`)→ 账户安全 Tab。
对应组件:`src/pages/user/SecurityTab.tsx`(外壳 `src/pages/user/AccountSecurity.tsx` 已废弃,仅保留 Tab)。

## 数据模型

```ts
interface Device {
id: string;
name: string; // 设备描述,如 "Chrome · macOS"
location: string; // 登录地点,如 "杭州"
lastActive: string; // 最近活跃,如 "刚刚" / "2 小时前"
current: boolean; // 是否为当前设备
mobile: boolean; // 是否移动端
}
对应组件:`src/pages/user/SecurityTab.tsx`。

interface SecuritySettings {
twoFactor: boolean; // 两步验证开关
loginAlert: boolean; // 异地登录提醒开关
}
```
## 认证约定

## 接口
- 认证使用服务端 Session + HttpOnly `SESSIONID` Cookie。
- 前端不读取、不保存 token;所有请求由 `HttpClient` 统一携带 Cookie。
- 会话失效时后端返回 `errno=1101`,前端清理用户态并跳转 `/auth`。

### GET /api/v1/user/security/settings
获取安全开关状态。
## 登录会话

**响应 data**
```json
{ "twoFactor": false, "loginAlert": true }
```
### GET /api/v1/user/device/list

### PUT /api/v1/user/security/2fa
开启 / 关闭两步验证。
获取当前用户的登录会话列表。响应 `data`:

**请求体**
```json
{ "enabled": true }
{
"total": 1,
"list": [
{
"id": "<session hash>",
"device_id": "<session hash>",
"device_name": "Chrome · macOS",
"platform": "macOS",
"ip": "203.0.113.10",
"is_current": true,
"login_time": 1790000000,
"last_active_time": 1790000000
}
]
}
```

**响应 data**
```json
{ "twoFactor": true }
```
### POST /api/v1/user/device/kick

### PUT /api/v1/user/security/login-alert
开启 / 关闭异地登录提醒。
强制下线指定会话。表单字段:

**请求体**
```json
{ "enabled": false }
```
| 字段 | 必填 | 说明 |
| ----------- | ---- | ------------------------------- |
| `device_id` | 是 | 列表中的会话 hash,不是旧设备表 ID |

当前设备不显示下线按钮;后端仍会校验会话归属。

## Passkey(WebAuthn)

### POST /api/v1/auth/passkey/list

列出当前用户的 Passkey。响应 `data`:

**响应 data**
```json
{ "loginAlert": false }
{
"total": 1,
"list": [
{
"id": 1,
"name": "MacBook Touch ID",
"alg": -7,
"aaguid": "...",
"transports": "internal,hybrid",
"backup_eligible": 1,
"backup_state": 0,
"created_at": 1790000000,
"last_used_at": 1790000000
}
]
}
```

### GET /api/v1/user/devices
获取当前账号登录设备列表。
### POST /api/v1/auth/passkey/register/begin

表单字段 `name` 可选;返回 WebAuthn `PublicKeyCredentialCreationOptions`。

### POST /api/v1/auth/passkey/register/finish

请求体为 `PublicKeyCredential.toJSON()`,并附带 `name`:

**响应 data**
```json
[
{
"id": "1",
"name": "Chrome · macOS",
"location": "杭州",
"lastActive": "刚刚",
"current": true,
"mobile": false
}
]
{
"id": "...",
"rawId": "...",
"type": "public-key",
"response": {
"clientDataJSON": "...",
"attestationObject": "...",
"transports": ["internal", "hybrid"]
},
"name": "MacBook Touch ID"
}
```

### DELETE /api/v1/user/devices/:id
强制下线指定设备(对应前端 `revoke` 二次确认)。
### POST /api/v1/auth/passkey/delete

表单字段 `id`(Passkey 记录 ID)软删除当前用户的凭据。

## 两步验证(2FA)

**响应**:成功返回空 `data`;`errno` 非 0 表示失败(如设备不存在)。
后端已有接口:

| 接口 | 说明 |
| --------------------------- | -------------------------------------- |
| `POST /user/2fa/enable` | 生成 TOTP secret 和 `otp_uri` |
| `POST /user/2fa/confirm` | 校验动态码并返回一次性恢复码 |
| `POST /user/2fa/disable` | 校验动态码后关闭 2FA |
| `POST /user/2fa/verify` | 登录第二步验证 |
| `POST /user/2fa/recovery` | 使用一次性恢复码完成登录 |

> 当前 `/user/info` 尚未返回 `totp_enabled`,安全页暂不提供 2FA 开关;登录页已支持 2FA 验证步骤。

## 备注
- 修改密码不在本界面(平台采用内存态 Token + HttpOnly Cookie,密码修改由认证服务单独处理)。
- 所有写操作需携带有效 `Bearer` token,否则响应 `errno=1101` 触发重新登录。

- 修改密码由认证服务单独处理;修改成功后后端会撤销全部旧会话。
- 邮箱验证码恢复登录会撤销全部旧会话,并签发新的 `SESSIONID`。
- 所有写操作依赖同源 Cookie 中的有效会话;前端权限判断仅用于界面控制。
3 changes: 3 additions & 0 deletions docs/auth-websocket-login-incident.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# 登录与 WebSocket 认证故障复盘

> 历史文档:本文记录的是旧 token 认证时期的问题。当前前端已改为服务端 Session + HttpOnly `SESSIONID` Cookie,
> WebSocket 握手依赖同源 Cookie,不再使用 `TokenManager` / `refresh_token` / `S_TOKEN_TIME`。

## 故障概述

生产环境曾出现登录后立即失效、页面跳转到 `/auth`,以及浏览器控制台持续输出以下错误的问题:
Expand Down
2 changes: 1 addition & 1 deletion docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ <h3>管理后台</h3>
<div class="feature-card">
<div class="icon">🔐</div>
<h3>系统能力</h3>
<p>内存态 Token 认证、WebSocket 实时通知、维护模式守卫、明暗主题、页面宽度切换、分片上传。</p>
<p>服务端会话 + Passkey 认证、WebSocket 实时通知、维护模式守卫、明暗主题、页面宽度切换、分片上传。</p>
</div>
</div>
</div>
Expand Down
43 changes: 0 additions & 43 deletions src/auth/CookieHelper.ts

This file was deleted.

Loading
Loading