# 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 ▼ ┌─────────────────────────────────────────────────────────┐ │ 后端 (FastAPI) │ │ │ │ /api/auth/* ← 登录、刷新、改密(无需 token) │ │ /api/admin/* ← 用户管理(需管理员 token) │ │ /api/chat ← 对话(需 token,user_id 注入 Mem0) │ │ /api/models ← 模型列表(需 token) │ │ /api/upload-* ← 上传(需 token) │ │ │ │ 存储: │ │ ├ data/epeekit.db ← SQLite,users 表 │ │ └ 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`。 --- ## 前端认证流程 ### 组件层级 ``` ← 管理 token、user 状态 ← 路由保护,未登录跳 /login ← 业务数据(按用户隔离的 localStorage) {children} ← 页面内容 ``` ### 流程图 ``` 页面加载 │ ├─ 有 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 " \ -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 " ``` ### 场景三:禁用一个用户 ```bash curl -X DELETE http://localhost:8000/api/admin/users/ \ -H "Authorization: Bearer " ``` 被禁用的用户: - 无法登录 - 已有的 token 在下次请求时被拒绝 - 数据保留(不物理删除) ### 场景四:用户修改自己的密码 ```bash curl -X POST http://localhost:8000/api/auth/change-password \ -H "Authorization: Bearer " \ -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 Loop(user_id 透传 Mem0) │ └── main.py ← 应用入口、路由注册、启动初始化 └── data/ ├── epeekit.db ← 用户数据库 └── qdrant/ ← Mem0 向量存储 art-agent/frontend/src/ ├── lib/ │ ├── auth-context.tsx ← AuthProvider:token 管理、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 ← 顶部导航(含用户菜单 + 退出登录) ```