FSD M1 · 认证与用户
← 总则 | 附录 → | 上游:PRD §4 M1 实现落点:
backend/internal/domain/auth/(service.go/model.go/repo.go)+cmd/server/auth_handlers.go(handlers)+cmd/server/routes.go:34-51(挂载)。事实基准=Go 代码;冻结版qmtv/server/index.js:73-230仅差异对拍参照。
M1 认证与用户
9 端点全量(基础信封见总则 §2.2:成功 {"ok":true,"data":…} / 失败 <码> {"error":"文案"})。本域零扣费、零 SSE、零素材上传;台账/通知为写入副作用(register/改邮箱两处)。
F1.01 注册
对应 PRD:§4 M1 · POST /api/auth/register(限流 5/h)| 状态:实现 | 实现落点:auth/service.go:68-140、auth_handlers.go:173-188 | UI:
/register(RegisterView)| 场景挂钩:无(e2e 未覆盖注册链,人工清单 T0 具名账号由 seed 产出)
1 功能定义
创建用户账号并签发首个会话(access + refresh 双令牌):初始积分发放、邀请码建分销关系、欢迎通知三件副作用一次完成。
2 触发条件与前置状态
- 未登录访问
/register提交表单 →POST /api/auth/register(公开面,WindowRateLimit("register", 5, time.Hour)——同 IP 每小时 5 次,超出 429)。 - 前置:邮箱未被注册;降级模式(无 DSN)下 503「数据库未配置」。
3 输入与校验
| 字段 | 来源 | 必填 | 校验 |
|---|---|---|---|
| JSON body | 是 | 非空(邮箱和密码必填)→ 严格正则 ^[^@\s]+@[^@\s]+\.[^@\s]+$(F7 统一版;冻结 register 原损坏正则被取代,PRD 差异已登记)→ 唯一性(邮箱已被注册)。三关任一失败 400 | |
| password | JSON body | 是 | 非空 + len ≥ 8(密码至少 8 位)400 |
| nickname | JSON body | 否 | 缺省取邮箱 @ 前缀 |
| invite_code | JSON body | 否 | 命中 referral_code 建分销关系;不命中静默跳过(不报错,冻结同) |
4 处理流程
5 输出与结果状态
200 {"ok":true,"data":{"token","refreshToken","user":PublicUser}}(AuthSession 统一会话体,model.go:78-82)。- user 投影字段集(冻结 publicUser 逐字节,model.go:33-44):
id/email/nickname/role/credits/org_id/workspace_version/referral_code/affiliate_enabled/created_at;org_id空串与referral_codeNULL 均→JSON null。
6 状态流转
无状态机。用户初始 status='active';workspace_version='classic'(后续 F1.09 可改)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| email/password 缺失 | 400 | 邮箱和密码必填 |
| 邮箱不过正则 | 400 | 邮箱格式不正确 |
| 密码 <8 位 | 400 | 密码至少 8 位 |
| 邮箱已注册 | 400 | 邮箱已被注册 |
| 限流超 5 次/时 | 429 | 限流信封(gateway.WindowRateLimit) |
| trial_credits 配置为 N(默认 200) | 实发 N 积分;欢迎通知文案跟随 N;台账描述硬编码「注册赠送 200 积分(初始)」不跟随(冻结 index.js:92 同 quirk 保真)→ DIF-28-1 | — |
| body 非 JSON | 400 | 请求体格式错误(handlers 层,非哨兵) |
8 权限与数据规则
- 权限:公开面(限流替代鉴权);写入
users/referral_relations/credit_records/notifications四表。 - 数据规则:
users.emailUNIQUE;referral_codeUNIQUE(碰撞时 INSERT 报错与冻结一致,crypto/rand 36^6 空间);invited_by哨兵空串(冻结写 null,DDL NOT NULL 差异已登记);credits初始 = trial_credits settings 值(F6 Billing.TrialCredits——冻结硬编码 200 属刻意差异)。
F1.02 登录
对应 PRD:§4 M1 · POST /api/auth/login(限流 20/min)| 状态:实现 | 实现落点:auth/service.go:143-156、auth_handlers.go:195-208 | UI:
/login(LoginView——错密码 alert「邮箱或密码错误」,登录成功跳/)| 场景挂钩:E2E-AUTH-01(断言册 auth.spec.ts)
1 功能定义
邮箱+密码换取会话双令牌;不存在与密码错合并同一 401(不暴露账号存在性),停用账号 403。
2 触发条件与前置状态
- 未登录(或会话过期)提交登录表单 →
POST /api/auth/login(公开面,限流 20/min)。 - 前置:用户存在且
status='active'。
3 输入与校验
| 字段 | 来源 | 必填 | 校验 |
|---|---|---|---|
| JSON body | 是 | 不存在与密码错合并 401(冻结 index.js:100-106 语义) | |
| password | JSON body | 是 | bcrypt 校验失败 401 |
4 处理流程
5 输出与结果状态
同 F1.01(AuthSession)。JWT claims 含 rol(AdminMiddleware 零查库鉴权——角色变更需重登生效,PRD §3 登记);access TTL 72h。
6 状态流转
无状态机;refresh_tokens 表新增一行(revoked_at NULL / replaced_by 空)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 邮箱不存在 | 401 | 邮箱或密码错误(合并语义) |
| 密码错误 | 401 | 同上 |
| 账号停用(status≠active) | 403 | 账号已被停用(登录时才知道身份,区别于 F1.05 的 401) |
| 限流超 20 次/分 | 429 | 限流信封(e2e 扩量场景需留意,遗留⑤登记) |
8 权限与数据规则
- 权限:公开面;读
users、写refresh_tokens。 - 数据规则:refresh token 原文不落库(SHA-256
token_hash+expires_atLocalTime);每次登录新增一行(不清理旧行,过期行由ValidByHash谓词自然失效)。
F1.03 令牌刷新(F1 真轮换)
对应 PRD:§4 M1 · POST /api/auth/refresh | 状态:实现 | 实现落点:auth/service.go:159-179、auth_handlers.go:214-227 | UI:前端 api 层 401 自动触发(frontend/src/api.ts:36-44)| 场景挂钩:ci –with-env refresh 轮换断言(新旧 RT 不同/旧复刷 401)
1 功能定义
用 refreshToken 换新会话:旧令牌撤销、新令牌串链(replaced_by)——同一 refreshToken 只能用一次;与冻结版差异(冻结 refresh 恒 401 无新字段),属 PRD F1 刻意差异。
2 触发条件与前置状态
- 前端任何 API 401 → 自动 POST
/api/auth/refresh重试一次;或手动调用于POST /api/auth/refresh(公开面无限流)。 - 前置:refreshToken 未撤销、未过期、属主
status='active'。
3 输入与校验
| 字段 | 来源 | 必填 | 校验 |
|---|---|---|---|
| refreshToken | JSON body | 是 | SHA-256 后查 ValidByHash:不存在/已撤销/过期任一 → 401 invalid refresh token;属主停用 → 同 401(安全收敛) |
4 处理流程
5 输出与结果状态
200 {"ok":true,"data":AuthSession}——refreshToken 字段含新令牌(冻结版响应只有 {token,user},无此字段——前端 W9a 同期消费)。旧令牌自此 401。
6 状态流转
refresh_tokens 行级状态机:active(revoked_at NULL)→ rotated(revoked_at 置位+replaced_by 串链);无单向状态图面,串链即审计线索。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| body 空/字段空 | 401 | invalid refresh token |
| 已用过的旧令牌二刷 | 401 | 同上(轮换唯一性) |
| 过期令牌 | 401 | 同上 |
| 属主已停用 | 401 | 同上(非 403——不暴露身份) |
8 权限与数据规则
- 权限:公开面;读
refresh_tokens/users、写refresh_tokens(撤旧)。 - 数据规则:原文仅存在响应体,库中只存 SHA-256(泄库不可逆推);
replaced_by构成轮换链。
F1.04 登出
对应 PRD:§4 M1 · POST /api/auth/logout | 状态:实现 | 实现落点:auth/service.go:258-260、auth_handlers.go:240-248 | UI:顶栏退出按钮(LoginView 登出回
/login)| 场景挂钩:E2E-AUTH-01(admin 可达 /admin 并登出回 /login)
1 功能定义
撤销当前用户全部 refreshToken(F1 增强;冻结版无服务端动作,PRD v1.2 补登差异)——改密/登出后所有已发刷新令牌一并失效。
2 触发条件与前置状态
- 已登录点击退出 →
POST /api/auth/logout(Bearer)。 - 前置:Bearer 有效;降级模式 503。
3 输入与校验
无 body。身份取自 JWT claims user_id。
4 处理流程
POST /auth/logout → RevokeAllForUser(userID)(UPDATE refresh_tokens SET revoked_at=NOW() WHERE user_id=? AND revoked_at IS NULL)→ 200 {"ok":true,"data":true}。
5 输出与结果状态
200 {"ok":true,"data":true};该用户全部未撤销 refresh_tokens 行 revoked_at 置位。
6 状态流转
同 F1.03 行级状态:active → rotated(批量)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 无刷新令牌在册 | 仍 200(UPDATE 零行非错误) | — |
| 停用用户带有效 Bearer 调用 | 401 unauthorized(authRequired 层拦截,见 F1.05) | — |
8 权限与数据规则
- 权限:登录态(amw);只写
refresh_tokens自己的行(user_id 来自 claims,非入参——不可越权撤他人)。
F1.05 当前用户信息(me)
对应 PRD:§4 M1 · GET /api/auth/me | 状态:实现 | 实现落点:auth/service.go:182-191、auth_handlers.go:229-238 | UI:顶栏积分胶囊/角色菜单数据源| 场景挂钩:E2E-AUTH-01(间接:全站登录态面)
1 功能定义
返回当前登录用户公开投影(PublicUser);已登录态遇停用账号返回 401(P2 期首核查 A1 恢复冻结 util.js:53 全 authed 拦截语义,触发前端登出链路)。
2 触发条件与前置状态
- 前端挂载/刷新/积分变动后拉取 →
GET /api/auth/me(Bearer)。 - 另有 query token 兜底形态
GET /api/auth/me?token=(ci 冒烟 C3 裁决面——与 SSE/images 同族,仅此三处)。
3 输入与校验
无参数。身份取自 claims。中间件层已验签(JWTAuth.AuthMiddleware);service 层仅补停用校验。
4 处理流程
GET /auth/me → ByID(userID) → 停用? →401 : →u.Public() 投影 → 200 {"ok":true,"data":PublicUser}。
5 输出与结果状态
字段集同 F1.01 §5。credits 为当前实时余额(DECIMAL 读侧 float64)。
6 状态流转
无。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 用户已被停用(曾登录) | 401 unauthorized——区别于登录时 403「账号已被停用」;前端 401→refresh→仍停用→登出 | — |
| JWT 过期/伪造 | 401 unauthorized(中间件层,json 键亦 error) | — |
8 权限与数据规则
- 权限:登录态;只读自己一行(claims 绑定)。
F1.06 积分流水分页
对应 PRD:§4 M1 · GET /api/auth/credit-records | 状态:实现 | 实现落点:auth_handlers.go:309-324(委托 credits.Records)| UI:
/credits(CreditsView)/ 顶栏积分弹层| 场景挂钩:E2E-CRD-01
1 功能定义
当前用户全局积分台账分页读(每页 20,created_at DESC,id DESC)——所有域的扣费/退款/充值/佣金行共用此读面。
2 触发条件与前置状态
- 进入积分流水页/翻页 →
GET /api/auth/credit-records?page=N(Bearer)。
3 输入与校验
| 字段 | 来源 | 必填 | 校验 |
|---|---|---|---|
| page | query | 否 | 缺省 1;非数字或 ≤0 折 1(不报 400,handlers.go:313-318 quirk) |
4 处理流程
GET /auth/credit-records → page 归一 → credits.Records(userID, page) → 200 {"ok":true,"data":{"list":[…],"total":N,"page":N}}。
5 输出与结果状态
list行字段:台账行投影(type/desc/amount/balance_after/created_at/task_id 等,credits 域读侧投影)。total全量行数(非当前页);page回显归一后值。
6 状态流转
无(纯读)。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| page=abc / page=0 / page=-1 | 折 1 返回第一页 | — |
| 空台账 | list:[]、total:0 200 | — |
8 权限与数据规则
- 权限:登录态;WHERE user_id=claims 绑定(看不到他人流水)。
- 数据规则:排序
created_at DESC,id DESC(同秒稳定序);分页 size 固定 20。
F1.07 修改密码
对应 PRD:§4 M1 · POST /api/auth/change-password | 状态:实现 | 实现落点:auth/service.go:194-217、auth_handlers.go:255-267 | UI:
/account(AccountView 安全区)| 场景挂钩:无独立 e2e(人工清单 T7 掉线重登路径)
1 功能定义
验原密码后更新密码哈希,并撤销全部 refreshToken(F1:改密后所有已登录会话的刷新能力失效,access 令牌存续至 72h 自然过期)。
2 触发条件与前置状态
- 账号设置页提交改密表单 →
POST /api/auth/change-password(Bearer)。 - 前置:status=‘active’(停用 401,A1 拦截——本就查库零成本恢复冻结语义)。
3 输入与校验
| 字段 | 来源 | 必填 | 校验 |
|---|---|---|---|
| old_password | JSON body | 是 | bcrypt 验证失败 400 原密码错误 |
| new_password | JSON body | 是 | 空/len < 8 400 新密码至少 8 位 |
校验序:查用户→停用拦→验原密码→验新密码(十六续 A1 修序先例在 M12 admin 域;本域顺序冻结逐字)。
4 处理流程
POST /auth/change-password → 停用?→401 : 验旧密码→验新密码→UpdatePassword(新 bcrypt 哈希)→RevokeAllForUser→200 {"ok":true,"data":true}。
5 输出与结果状态
200 {"ok":true,"data":true};users.password_hash 更新;该用户全部 refresh_tokens 撤销(前端引导重登)。
6 状态流转
无用户级状态机;refresh_tokens 批量 active→rotated。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 原密码错 | 400 | 原密码错误 |
| 新密码 <8 位或空 | 400 | 新密码至少 8 位 |
| 停用态调用 | 401 | unauthorized |
8 权限与数据规则
- 权限:登录态;只改自己(claims 绑定)。
F1.08 修改邮箱
对应 PRD:§4 M1 · PUT /api/auth/email | 状态:实现 | 实现落点:auth/service.go:220-244、auth_handlers.go:274-287 | UI:
/account(AccountView 安全区)| 场景挂钩:无独立 e2e(FAQ「改密登出」同族人工面)
1 功能定义
需当前密码确认的邮箱变更;成功后发站内通知(冻结 index.js:127-136 文案)。登出不联动(改邮箱不清会话,与改密不同——冻结语义保真)。
2 触发条件与前置状态
- 账号设置页提交改邮箱表单 →
PUT /api/auth/email(Bearer)。 - 前置:新邮箱未被任何账号占用。
3 输入与校验
| 字段 | 来源 | 必填 | 校验 |
|---|---|---|---|
| password | JSON body | 是 | 当前密码验证失败 400 当前密码错误 |
| new_email | JSON body | 是 | 非空+严格正则(F7 统一版)400 新邮箱格式不正确 → 唯一性 400 该邮箱已被占用 |
4 处理流程
PUT /auth/email → 停用拦 → 验当前密码 → 验新邮箱格式 → 唯一性 → UpdateEmail → 通知「登录邮箱已变更/登录邮箱已变更为 X,请使用新邮箱登录。」→ 200 {"ok":true,"data":{"email":"<新邮箱>"}}。
5 输出与结果状态
data.email = 新邮箱;users.email 更新;notifications 新增一行(标题+文案冻结逐字)。
6 状态流转
无。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
| 当前密码错 | 400 | 当前密码错误 |
| 新邮箱格式非法 | 400 | 新邮箱格式不正确 |
| 新邮箱已被占用 | 400 | 该邮箱已被占用 |
| 停用态调用 | 401 | unauthorized |
8 权限与数据规则
- 权限:登录态;只改自己。
- 数据规则:
users.emailUNIQUE 兜底;通知文案与新邮箱值拼接(无模板)。
F1.09 工作台版本状态
对应 PRD:§4 M1 · PUT /api/auth/workspace-state | 状态:实现 | 实现落点:auth/service.go:247-255、auth_handlers.go:289-306 | UI:工作台切换入口(双工作台
shot._tbl通路的选择器)| 场景挂钩:无(M2 拆解域间接消费)
1 功能定义
保存用户工作台版本偏好(users.workspace_version);非 'legacy' 值一律折 'classic'(白名单外静默归一,冻结 index.js:215 逐字)。
2 触发条件与前置状态
- 前端切换工作台版本 →
PUT /api/auth/workspace-state(Bearer)。
3 输入与校验
| 字段 | 来源 | 必填 | 校验 |
|---|---|---|---|
| version | JSON body | 是 | body 字段名是 version(非 workspace_version——冻结 quirk);值 ≠legacy 一律落 classic,无 400 面 |
4 处理流程
PUT /auth/workspace-state → 归一(≠legacy→classic)→ UpdateWorkspaceVersion → 200 {"ok":true,"data":{"workspace_version":"<归一值>"}}。
5 输出与结果状态
data.workspace_version 为归一后值(传 bogus 得 "classic");users.workspace_version 持久化。
6 状态流转
classic ⇄ legacy 二值域,无第三态。
7 边界与异常
| 条件 | 系统行为 | 提示/码 |
|---|---|---|
传任意非法值(空串/bogus/null) | 静默落 classic,200 | — |
仅 legacy 字面 | 落 legacy | — |
8 权限与数据规则
- 权限:登录态;只改自己。
- 数据规则:M2 拆解域读此列决定 shots/director_shots 双表通路归属(
shot._tbl约定——见总则 §1 术语)。
M1 端点覆盖对账
| # | 端点 | 功能节 |
|---|---|---|
| 1 | POST /api/auth/register | F1.01 |
| 2 | POST /api/auth/login | F1.02 |
| 3 | POST /api/auth/refresh | F1.03 |
| 4 | POST /api/auth/logout | F1.04 |
| 5 | GET /api/auth/me | F1.05 |
| 6 | GET /api/auth/credit-records | F1.06 |
| 7 | POST /api/auth/change-password | F1.07 |
| 8 | PUT /api/auth/email | F1.08 |
| 9 | PUT /api/auth/workspace-state | F1.09 |
9/9 对账 PRD §4 M1(DIF 新发现:DIF-28-1 注册台账描述硬编码——见 99-附录 B)。