用户系统

This commit is contained in:
2026-04-15 00:21:43 +08:00
parent 97bbb3f306
commit 47c0863bab
23 changed files with 1640 additions and 56 deletions

View File

@@ -0,0 +1,598 @@
# 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 ← 顶部导航(含用户菜单 + 退出登录)
```