tokens.go 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. package auth
  2. import (
  3. "context"
  4. "database/sql"
  5. "errors"
  6. "fmt"
  7. "time"
  8. )
  9. // API Token 管理(api.md 第五节 / auth.md 第五节)。
  10. //
  11. // 规则要点:
  12. // - 明文只在创建时返回一次,之后只能看到 token_prefix;
  13. // - 撤销 = 写 revoked_at,不删除记录;
  14. // - Token 不能管理凭证类资源,由 handler 层的 RequireTotpVerified 拦截。
  15. // ErrTokenNotFound 表示 Token 不存在或不属于当前用户(404)。
  16. var ErrTokenNotFound = errors.New("Token 不存在")
  17. // ErrTokenAlreadyRevoked 表示 Token 已撤销(422,api.md 5.3)。
  18. var ErrTokenAlreadyRevoked = errors.New("Token 已撤销")
  19. // APITokenSummary 是 GET /api/tokens 的列表项(api.md 5.1)。
  20. // 不包含明文,也不包含 token_hash。
  21. type APITokenSummary struct {
  22. ID string `json:"id"`
  23. Name string `json:"name"`
  24. TokenPrefix string `json:"token_prefix"`
  25. Scope string `json:"scope"`
  26. ExpiresAt *time.Time `json:"expires_at"`
  27. LastUsedAt *time.Time `json:"last_used_at"`
  28. RevokedAt *time.Time `json:"revoked_at"`
  29. CreatedAt time.Time `json:"created_at"`
  30. }
  31. // CreatedAPIToken 是 POST /api/tokens 的响应(api.md 5.2),含一次性明文。
  32. type CreatedAPIToken struct {
  33. APITokenSummary
  34. Token string `json:"token"`
  35. }
  36. // ListAPITokens 返回当前用户的全部 Token(含已撤销),按创建时间倒序。
  37. func (s *Service) ListAPITokens(ctx context.Context, userID string) ([]APITokenSummary, error) {
  38. const q = `
  39. SELECT id, name, token_prefix, scope, expires_at, last_used_at, revoked_at, created_at
  40. FROM tokens
  41. WHERE user_id = $1
  42. ORDER BY created_at DESC`
  43. rows, err := s.db.QueryContext(ctx, q, userID)
  44. if err != nil {
  45. return nil, fmt.Errorf("查询 Token 列表失败: %w", err)
  46. }
  47. defer rows.Close()
  48. out := make([]APITokenSummary, 0, 4)
  49. for rows.Next() {
  50. var item APITokenSummary
  51. if err := rows.Scan(
  52. &item.ID, &item.Name, &item.TokenPrefix, &item.Scope,
  53. &item.ExpiresAt, &item.LastUsedAt, &item.RevokedAt, &item.CreatedAt,
  54. ); err != nil {
  55. return nil, fmt.Errorf("扫描 Token 失败: %w", err)
  56. }
  57. out = append(out, item)
  58. }
  59. if err := rows.Err(); err != nil {
  60. return nil, fmt.Errorf("读取 Token 列表失败: %w", err)
  61. }
  62. return out, nil
  63. }
  64. // CreateAPIToken 创建 Token 并返回一次性明文(api.md 5.2)。
  65. // expiresAt 为 nil 表示永不过期(auth.md 第五节规则 5)。
  66. func (s *Service) CreateAPIToken(ctx context.Context, userID, name, scope string, expiresAt *time.Time) (CreatedAPIToken, error) {
  67. if err := ValidateScope(scope); err != nil {
  68. return CreatedAPIToken{}, err
  69. }
  70. token, prefix, err := newAPIToken(scope)
  71. if err != nil {
  72. return CreatedAPIToken{}, err
  73. }
  74. const q = `
  75. INSERT INTO tokens (user_id, name, token_hash, token_prefix, scope, expires_at)
  76. VALUES ($1, $2, $3, $4, $5, $6)
  77. RETURNING id, created_at`
  78. var id string
  79. var createdAt time.Time
  80. if err := s.db.QueryRowContext(ctx, q,
  81. userID, name, HashToken(token), prefix, scope, expiresAt,
  82. ).Scan(&id, &createdAt); err != nil {
  83. return CreatedAPIToken{}, fmt.Errorf("创建 Token 失败: %w", err)
  84. }
  85. return CreatedAPIToken{
  86. APITokenSummary: APITokenSummary{
  87. ID: id,
  88. Name: name,
  89. TokenPrefix: prefix,
  90. Scope: scope,
  91. ExpiresAt: expiresAt,
  92. CreatedAt: createdAt,
  93. },
  94. Token: token,
  95. }, nil
  96. }
  97. // RevokeAPIToken 撤销 Token:写 revoked_at,不删记录(api.md 5.3)。
  98. // 返回 Token 名与前缀,供写审计事件(audit.md 第三节)。
  99. func (s *Service) RevokeAPIToken(ctx context.Context, userID, tokenID string) (APITokenSummary, error) {
  100. const findQ = `
  101. SELECT id, name, token_prefix, scope, expires_at, last_used_at, revoked_at, created_at
  102. FROM tokens WHERE id = $1 AND user_id = $2`
  103. var item APITokenSummary
  104. err := s.db.QueryRowContext(ctx, findQ, tokenID, userID).Scan(
  105. &item.ID, &item.Name, &item.TokenPrefix, &item.Scope,
  106. &item.ExpiresAt, &item.LastUsedAt, &item.RevokedAt, &item.CreatedAt)
  107. if errors.Is(err, sql.ErrNoRows) {
  108. return APITokenSummary{}, ErrTokenNotFound
  109. }
  110. if err != nil {
  111. return APITokenSummary{}, fmt.Errorf("查询 Token 失败: %w", err)
  112. }
  113. if item.RevokedAt != nil {
  114. return APITokenSummary{}, ErrTokenAlreadyRevoked
  115. }
  116. const q = `UPDATE tokens SET revoked_at = now() WHERE id = $1 AND user_id = $2 AND revoked_at IS NULL`
  117. if _, err := s.db.ExecContext(ctx, q, tokenID, userID); err != nil {
  118. return APITokenSummary{}, fmt.Errorf("撤销 Token 失败: %w", err)
  119. }
  120. revokedAt := s.now()
  121. item.RevokedAt = &revokedAt
  122. return item, nil
  123. }
  124. // ValidateScope 校验 scope 取值(auth.md 第五节规则 4)。
  125. func ValidateScope(scope string) error {
  126. switch scope {
  127. case ScopeRead, ScopeReadWrite:
  128. return nil
  129. default:
  130. return fmt.Errorf("scope 只能取 %s 或 %s", ScopeRead, ScopeReadWrite)
  131. }
  132. }
  133. // createTokenForTest 直接建一个可用 Token 并返回明文,仅供测试使用。
  134. func createTokenForTest(ctx context.Context, db *sql.DB, userID, scope string) (string, error) {
  135. token, prefix, err := newAPIToken(scope)
  136. if err != nil {
  137. return "", err
  138. }
  139. _, err = db.ExecContext(ctx,
  140. `INSERT INTO tokens (user_id, name, token_hash, token_prefix, scope)
  141. VALUES ($1, 'test', $2, $3, $4)`,
  142. userID, HashToken(token), prefix, scope)
  143. if err != nil {
  144. return "", err
  145. }
  146. return token, nil
  147. }
苏ICP备2026082406号
苏公网安备 待审核