feat(server): web 用户中心后端(3/3) — 用户 TOTP 2FA + 登录二段式

- 用户 TOTP(auth/totp_user.go,复用 internal/totp + AES-256-GCM 加密存密钥):
  POST /v1/me/totp/setup(生成密钥+otpauth_uri)、/verify(校验码→启用)、
  /disable(校验码→清空)。仅在 USER_TOTP_ENC_KEY(32B/64hex) 配置时挂载。
- 登录二段式:Login 在 totp_enabled 时不发 token,改发短期 pending token(Redis
  5min)+ 返回 {totp_required, pending_token};POST /v1/auth/login/totp 消费
  pending + 校验码 → 发 token。非 TOTP 用户仍走扁平 TokenPair,app 不受影响。
- User 结构 + GetUserByEmail 补 totp_enabled。
- 单测覆盖 AES seal/open 往返 + 篡改/错误密钥检测 + pending token 唯一性。
- 全量 server 23 包测试通过。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-17 08:45:50 +08:00
parent 8047d17b48
commit 0782cf651b
6 changed files with 416 additions and 13 deletions
+9 -8
View File
@@ -13,12 +13,13 @@ import (
// User is the subset of the users row the auth module needs.
type User struct {
ID int64
UUID string
Email string
PwHash string
DpUUID string
Status string // "active" | "banned"
ID int64
UUID string
Email string
PwHash string
DpUUID string
Status string // "active" | "banned"
TOTPEnabled bool // two-factor enabled → login requires a second TOTP step
}
// Sentinel store errors. Service maps these to API errors.
@@ -112,8 +113,8 @@ func (s *SQLStore) CreateUserWithTrial(ctx context.Context, email, pwHash string
func (s *SQLStore) GetUserByEmail(ctx context.Context, email string) (*User, error) {
var u User
err := s.db.QueryRowContext(ctx,
`SELECT id, uuid, email, pw_hash, dp_uuid, status FROM users WHERE email = ?`,
email).Scan(&u.ID, &u.UUID, &u.Email, &u.PwHash, &u.DpUUID, &u.Status)
`SELECT id, uuid, email, pw_hash, dp_uuid, status, totp_enabled FROM users WHERE email = ?`,
email).Scan(&u.ID, &u.UUID, &u.Email, &u.PwHash, &u.DpUUID, &u.Status, &u.TOTPEnabled)
if err == sql.ErrNoRows {
return nil, ErrNotFound
}