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

16 KiB
Raw Permalink Blame History

EPEEKit 用户系统

本文档覆盖用户系统的技术架构、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 中添加:

# 必填JWT 签名密钥(随机字符串,建议 32+ 位)
JWT_SECRET=你的随机密钥

# 可选:首次启动时的管理员密码(不设则随机生成并打印到控制台)
# ADMIN_DEFAULT_PASSWORD=changeme123

2. 安装依赖

cd art-agent/backend
pip install -r requirements.txt

3. 启动后端

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)

{
  "username": "admin",
  "password": "your-password"
}

成功响应 (200)

{
  "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)

{
  "refresh_token": "eyJ..."
}

成功响应 (200)

{
  "access_token": "eyJ...",
  "token_type": "bearer"
}

POST /api/auth/change-password

修改当前用户密码。需要 Bearer Token。

请求体 (JSON)

{
  "old_password": "current-password",
  "new_password": "new-password-min-6"
}

成功响应 (200)

{
  "message": "密码已修改"
}

错误响应

  • 400 — 旧密码错误 / 新密码少于 6 位

GET /api/auth/me

获取当前登录用户的信息。需要 Bearer Token。

成功响应 (200)

{
  "id": "a1b2c3...",
  "username": "admin",
  "display_name": "管理员",
  "is_admin": true
}

管理员接口

以下接口均需要管理员的 Bearer Token普通用户调用返回 403

POST /api/admin/users

创建新用户。

请求体 (JSON)

{
  "username": "zhangsan",
  "password": "123456",
  "display_name": "张三",
  "is_admin": false
}

成功响应 (200)

{
  "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)

[
  {
    "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)

{
  "message": "用户 zhangsan 已禁用"
}

错误响应

  • 404 — 用户不存在
  • 400 — 不能禁用自己

业务接口鉴权

以下已有接口现在需要 Bearer Token

方法 路径 说明
POST /api/chat 对话user_id 自动从 token 注入)
POST /api/upload-ref-image 上传参考图
GET /api/models 获取模型列表

请求示例:

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 请求(fetchXMLHttpRequest)在收到 401 响应时,自动跳转到 /login


数据隔离机制

后端Mem0 记忆)

Mem0 的 user_id 参数从 JWT 中解析的真实用户 ID 传入:

# 之前(所有人共享)
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
# 登录
curl -X POST http://localhost:8000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username": "admin", "password": "your-admin-password"}'

# 记录返回的 access_token
  1. 创建用户:
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": "张三"
  }'
  1. 将用户名和初始密码告知成员,建议登录后立即修改密码。

场景二:查看所有用户

curl http://localhost:8000/api/admin/users \
  -H "Authorization: Bearer <admin-access-token>"

场景三:禁用一个用户

curl -X DELETE http://localhost:8000/api/admin/users/<user-id> \
  -H "Authorization: Bearer <admin-access-token>"

被禁用的用户:

  • 无法登录
  • 已有的 token 在下次请求时被拒绝
  • 数据保留(不物理删除)

场景四:用户修改自己的密码

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=<随机字符串> 后重启。

忘记管理员密码

删除数据库文件后重启,会重新创建默认管理员:

rm art-agent/backend/data/epeekit.db
# 重启后端,新密码会打印到控制台

注意:这会丢失所有用户数据。如果只想重置管理员密码,可以用 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         ← 顶部导航(含用户菜单 + 退出登录)