Files
EPEEAIKit/docs/art-agent/USER-SYSTEM.md
2026-04-15 00:21:43 +08:00

599 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# EPEEKit 用户系统
本文档覆盖用户系统的技术架构、API 参考和日常使用指南。
---
## 目录
- [架构概览](#架构概览)
- [快速开始](#快速开始)
- [环境配置](#环境配置)
- [API 参考](#api-参考)
- [认证接口](#认证接口)
- [管理员接口](#管理员接口)
- [业务接口鉴权](#业务接口鉴权)
- [前端认证流程](#前端认证流程)
- [数据隔离机制](#数据隔离机制)
- [用户管理操作手册](#用户管理操作手册)
- [安全说明](#安全说明)
- [故障排除](#故障排除)
---
## 架构概览
```
┌─────────────────────────────────────────────────────────┐
│ 前端 (Next.js) │
│ │
│ AuthProvider → AuthGuard → AppProvider → 页面组件 │
│ │ │ │ │
│ localStorage /login 白名单 按 user_id 隔离存储 │
│ ├ epeekit-auth-token │
│ ├ epeekit-refresh-token │
│ └ epeekit-{userId}-sessions / tags / assets │
└──────────────────────┬──────────────────────────────────┘
│ Authorization: Bearer <token>
┌─────────────────────────────────────────────────────────┐
│ 后端 (FastAPI) │
│ │
│ /api/auth/* ← 登录、刷新、改密(无需 token
│ /api/admin/* ← 用户管理(需管理员 token
│ /api/chat ← 对话(需 tokenuser_id 注入 Mem0
│ /api/models ← 模型列表(需 token
│ /api/upload-* ← 上传(需 token
│ │
│ 存储: │
│ ├ data/epeekit.db ← SQLiteusers 表 │
│ └ data/qdrant/ ← Mem0 向量库,按 user_id 隔离 │
└─────────────────────────────────────────────────────────┘
```
### 技术选型
| 组件 | 技术 | 说明 |
|---|---|---|
| 数据库 | SQLite (SQLModel) | 零部署,单文件 `data/epeekit.db` |
| 密码哈希 | Argon2id (pwdlib) | 密码哈希竞赛冠军,抗 GPU/ASIC兼容旧 bcrypt 哈希 |
| 令牌 | JWT (python-jose) | 无状态认证HS256 签名 |
| 前端状态 | React Context | AuthProvider + AuthGuard |
### 令牌机制
- **Access Token**:有效期 30 分钟,每个 API 请求携带
- **Refresh Token**:有效期 7 天,用于在 access token 过期后无感刷新
- JWT Payload 结构:`{ sub: user_id, exp: timestamp, type: "access" | "refresh" }`
---
## 快速开始
### 1. 配置环境变量
`art-agent/backend/.env` 中添加:
```env
# 必填JWT 签名密钥(随机字符串,建议 32+ 位)
JWT_SECRET=你的随机密钥
# 可选:首次启动时的管理员密码(不设则随机生成并打印到控制台)
# ADMIN_DEFAULT_PASSWORD=changeme123
```
### 2. 安装依赖
```bash
cd art-agent/backend
pip install -r requirements.txt
```
### 3. 启动后端
```bash
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
```
首次启动时,控制台会输出:
```
==================================================
默认管理员账号已创建
用户名: admin
密码: <随机生成的密码>
请登录后尽快修改密码!
==================================================
```
**请立即记录这个密码。** 如果设置了 `ADMIN_DEFAULT_PASSWORD`,则使用该值,不会打印。
### 4. 登录
打开前端 `http://localhost:3000`,会自动跳转到登录页,使用 admin 账号登录。
### 5. 创建团队成员账号
登录后,使用管理员 API 创建其他用户(见 [用户管理操作手册](#用户管理操作手册))。
---
## 环境配置
### 后端环境变量
| 变量 | 必填 | 默认值 | 说明 |
|---|---|---|---|
| `JWT_SECRET` | 是 | (无) | JWT 签名密钥,未设置时启动报错 |
| `ADMIN_DEFAULT_PASSWORD` | 否 | (随机) | 首次启动创建管理员时的密码 |
### 数据文件
| 路径 | 说明 |
|---|---|
| `art-agent/backend/data/epeekit.db` | SQLite 数据库,包含 users 表 |
| `art-agent/backend/data/qdrant/` | Mem0 向量数据,按 user_id 隔离 |
> **备份提示**:定期备份 `data/` 目录即可保全用户数据和记忆数据。
---
## API 参考
所有接口基础路径为 `http://localhost:8000`
### 认证接口
#### POST /api/auth/login
用户登录,获取令牌。
**请求体** (JSON)
```json
{
"username": "admin",
"password": "your-password"
}
```
**成功响应** (200)
```json
{
"access_token": "eyJ...",
"refresh_token": "eyJ...",
"token_type": "bearer",
"user": {
"id": "a1b2c3...",
"username": "admin",
"display_name": "管理员",
"is_admin": true
}
}
```
**错误响应**
- `401` — 用户名或密码错误
- `403` — 账号已禁用
---
#### POST /api/auth/refresh
用 refresh token 换取新的 access token。
**请求体** (JSON)
```json
{
"refresh_token": "eyJ..."
}
```
**成功响应** (200)
```json
{
"access_token": "eyJ...",
"token_type": "bearer"
}
```
---
#### POST /api/auth/change-password
修改当前用户密码。**需要 Bearer Token。**
**请求体** (JSON)
```json
{
"old_password": "current-password",
"new_password": "new-password-min-6"
}
```
**成功响应** (200)
```json
{
"message": "密码已修改"
}
```
**错误响应**
- `400` — 旧密码错误 / 新密码少于 6 位
---
#### GET /api/auth/me
获取当前登录用户的信息。**需要 Bearer Token。**
**成功响应** (200)
```json
{
"id": "a1b2c3...",
"username": "admin",
"display_name": "管理员",
"is_admin": true
}
```
---
### 管理员接口
以下接口均需要**管理员**的 Bearer Token普通用户调用返回 `403`
#### POST /api/admin/users
创建新用户。
**请求体** (JSON)
```json
{
"username": "zhangsan",
"password": "123456",
"display_name": "张三",
"is_admin": false
}
```
**成功响应** (200)
```json
{
"id": "d4e5f6...",
"username": "zhangsan",
"display_name": "张三",
"is_admin": false,
"is_active": true,
"created_at": "2026-04-14T23:00:00"
}
```
**错误响应**
- `409` — 用户名已存在
- `400` — 密码少于 6 位
---
#### GET /api/admin/users
获取所有用户列表。
**成功响应** (200)
```json
[
{
"id": "a1b2c3...",
"username": "admin",
"display_name": "管理员",
"is_admin": true,
"is_active": true,
"created_at": "2026-04-14T22:00:00"
},
...
]
```
---
#### DELETE /api/admin/users/{user_id}
禁用指定用户(软删除,不物理删除)。
**成功响应** (200)
```json
{
"message": "用户 zhangsan 已禁用"
}
```
**错误响应**
- `404` — 用户不存在
- `400` — 不能禁用自己
---
### 业务接口鉴权
以下已有接口现在需要 Bearer Token
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | `/api/chat` | 对话user_id 自动从 token 注入) |
| POST | `/api/upload-ref-image` | 上传参考图 |
| GET | `/api/models` | 获取模型列表 |
请求示例:
```bash
curl -X POST http://localhost:8000/api/chat \
-H "Authorization: Bearer eyJ..." \
-F "messages=[{\"role\":\"user\",\"content\":\"画一个图标\"}]"
```
未携带或令牌无效时返回 `401`
---
## 前端认证流程
### 组件层级
```
<AuthProvider> ← 管理 token、user 状态
<AuthGuard> ← 路由保护,未登录跳 /login
<AppProvider> ← 业务数据(按用户隔离的 localStorage
{children} ← 页面内容
</AppProvider>
</AuthGuard>
</AuthProvider>
```
### 流程图
```
页面加载
├─ 有 token
│ ├─ 调用 GET /api/auth/me 验证
│ │ ├─ 200 → 认证通过,加载用户数据
│ │ ├─ 401 → 尝试 refresh token
│ │ │ ├─ 成功 → 获得新 access token认证通过
│ │ │ └─ 失败 → 清除 token跳转 /login
│ │ └─ 网络错误 → 保留本地缓存,允许离线查看
│ └─ token 不存在 → 跳转 /login
└─ /login 页面
└─ 提交用户名+密码
├─ 成功 → 保存 token跳转 /
└─ 失败 → 显示错误信息
```
### Token 存储
| localStorage Key | 内容 |
|---|---|
| `epeekit-auth-token` | JWT access token |
| `epeekit-refresh-token` | JWT refresh token |
| `epeekit-auth-user` | 用户信息 JSON (id, username, display_name, is_admin) |
### 401 自动处理
所有 API 请求(`fetch``XMLHttpRequest`)在收到 `401` 响应时,自动跳转到 `/login`
---
## 数据隔离机制
### 后端Mem0 记忆)
Mem0 的 `user_id` 参数从 JWT 中解析的真实用户 ID 传入:
```python
# 之前(所有人共享)
search_kwargs = {"user_id": "default_user", ...}
# 现在(按用户隔离)
search_kwargs = {"user_id": current_user.id, ...}
```
每个用户的对话记忆完全独立,互不可见。
### 前端localStorage
所有 localStorage 键名加上用户 ID 前缀:
```
之前: epeekit-sessions
现在: epeekit-{userId}-sessions
```
完整键名表:
| Key 模式 | 内容 |
|---|---|
| `epeekit-{userId}-sessions` | 对话会话列表 |
| `epeekit-{userId}-tags` | 自定义标签 |
| `epeekit-{userId}-assets` | 图片资源元数据 |
| `epeekit-{userId}-selected-image-model` | 模型选择偏好 |
登出时数据不清除,下次登录同一账号时自动恢复。切换用户时自动加载对应用户的数据。
---
## 用户管理操作手册
### 场景一:为新团队成员创建账号
1. 使用管理员账号登录获取 token
```bash
# 登录
curl -X POST http://localhost:8000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "your-admin-password"}'
# 记录返回的 access_token
```
2. 创建用户:
```bash
curl -X POST http://localhost:8000/api/admin/users \
-H "Authorization: Bearer <admin-access-token>" \
-H "Content-Type: application/json" \
-d '{
"username": "zhangsan",
"password": "initial-password",
"display_name": "张三"
}'
```
3. 将用户名和初始密码告知成员,建议登录后立即修改密码。
### 场景二:查看所有用户
```bash
curl http://localhost:8000/api/admin/users \
-H "Authorization: Bearer <admin-access-token>"
```
### 场景三:禁用一个用户
```bash
curl -X DELETE http://localhost:8000/api/admin/users/<user-id> \
-H "Authorization: Bearer <admin-access-token>"
```
被禁用的用户:
- 无法登录
- 已有的 token 在下次请求时被拒绝
- 数据保留(不物理删除)
### 场景四:用户修改自己的密码
```bash
curl -X POST http://localhost:8000/api/auth/change-password \
-H "Authorization: Bearer <user-access-token>" \
-H "Content-Type: application/json" \
-d '{
"old_password": "current-password",
"new_password": "new-secure-password"
}'
```
### 场景五:使用 FastAPI 交互式文档
后端启动后,访问 `http://localhost:8000/docs` 可以看到 Swagger UI
- 点击右上角 "Authorize" 按钮
- 输入用户名和密码进行认证
- 之后可以直接在页面上调试所有 API
---
## 安全说明
### 已实施的安全措施
- **Argon2id 密码哈希**:密码哈希竞赛冠军算法,抗 GPU/ASIC 攻击,即使数据库泄露也无法直接获取密码
- **JWT 签名**:令牌使用 HS256 签名,无法伪造
- **令牌过期**access token 30 分钟过期refresh token 7 天过期
- **不开放注册**:只有管理员可以创建账号
- **软删除**:禁用用户而非删除,保留审计轨迹
### 当前限制
- **CORS 全开** (`allow_origins=["*"]`):适合内部使用和开发阶段,生产部署时应改为具体域名
- **无速率限制**:登录接口没有防暴力破解的限流机制,内部网络可接受
- **SQLite 单写**:高并发写入时可能遇到锁,内部团队规模不会触及
- **无审计日志**:未记录登录/操作日志,可后续添加
### JWT_SECRET 安全
- 必须使用足够长的随机字符串(建议 32+ 字符)
- 不要提交到 Git`.env` 文件应在 `.gitignore` 中)
- 更换 JWT_SECRET 会使所有已发放的令牌失效,所有用户需要重新登录
---
## 故障排除
### 启动报错 "JWT_SECRET 环境变量未设置"
`.env` 中添加 `JWT_SECRET=<随机字符串>` 后重启。
### 忘记管理员密码
删除数据库文件后重启,会重新创建默认管理员:
```bash
rm art-agent/backend/data/epeekit.db
# 重启后端,新密码会打印到控制台
```
> 注意:这会丢失所有用户数据。如果只想重置管理员密码,可以用 Python 脚本直接更新:
>
> ```python
> from app.db import engine, User
> from app.auth import hash_password
> from sqlmodel import Session, select
>
> with Session(engine) as s:
> admin = s.exec(select(User).where(User.username == "admin")).first()
> admin.hashed_password = hash_password("new-password")
> s.add(admin)
> s.commit()
> ```
### 前端一直跳转到登录页
- 检查后端是否正常运行(`curl http://localhost:8000/health`
- 检查浏览器控制台是否有 CORS 或网络错误
- 清除浏览器 localStorage 中的 `epeekit-auth-*` 键后重试
### 登录后看不到之前的对话
用户系统上线后localStorage 的键名格式变为 `epeekit-{userId}-sessions`。之前无用户系统时的数据存在旧键名 `epeekit-sessions` 下。如需迁移:
1. 打开浏览器开发者工具 → Application → Local Storage
2. 找到 `epeekit-sessions` 的值并复制
3. 创建新键 `epeekit-<你的userId>-sessions`,粘贴值
4. 刷新页面
### 多人使用同一浏览器
每个用户登录后,对话数据按 user_id 隔离存储在 localStorage 中。切换账号时自动加载对应用户的数据,互不影响。退出登录不会清除数据,下次登录仍可恢复。
---
## 源码结构
```
art-agent/backend/
├── app/
│ ├── db.py ← SQLite 连接、User 模型、建表、默认管理员
│ ├── auth.py ← Argon2id 密码哈希、JWT 编解码、FastAPI 依赖
│ ├── api/
│ │ ├── auth.py ← 登录/刷新/改密/me 路由
│ │ ├── admin.py ← 创建/列表/禁用用户路由
│ │ └── chat.py ← 已有对话路由(已加鉴权)
│ ├── agent/
│ │ └── loop.py ← Agent Loopuser_id 透传 Mem0
│ └── main.py ← 应用入口、路由注册、启动初始化
└── data/
├── epeekit.db ← 用户数据库
└── qdrant/ ← Mem0 向量存储
art-agent/frontend/src/
├── lib/
│ ├── auth-context.tsx ← AuthProvidertoken 管理、login/logout
│ ├── auth-guard.tsx ← AuthGuard路由保护
│ ├── app-context.tsx ← AppProvider按用户加载业务数据
│ ├── api.ts ← API 封装(自动带 Authorization 头)
│ └── store.ts ← localStorage 封装(按 user_id 隔离键名)
├── app/
│ ├── layout.tsx ← AuthProvider → AuthGuard → AppProvider
│ └── login/
│ └── page.tsx ← 登录页面
└── components/layout/
└── top-nav.tsx ← 顶部导航(含用户菜单 + 退出登录)
```