sessions.go 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. package auth
  2. import (
  3. "context"
  4. "database/sql"
  5. "errors"
  6. "fmt"
  7. "time"
  8. )
  9. // 会话管理(api.md 第三节)。
  10. //
  11. // 会话列表只对会话身份开放,Token 调用返回 403 —— 这条在 handler 层用
  12. // RequireSession 控制;Service 本身不区分调用方身份。
  13. // ErrSessionNotFound 见 service.go。
  14. // SessionSummary 是 GET /api/sessions 的列表项(api.md 3.1)。
  15. type SessionSummary struct {
  16. ID string `json:"id"`
  17. UserAgent string `json:"user_agent"`
  18. CreatedAt time.Time `json:"created_at"`
  19. LastUsedAt time.Time `json:"last_used_at"`
  20. IsCurrent bool `json:"is_current"`
  21. }
  22. // ListSessions 返回当前用户的全部会话,按最近使用倒序。
  23. // currentSessionID 用于标记 is_current(api.md 3.1)。
  24. func (s *Service) ListSessions(ctx context.Context, userID, currentSessionID string) ([]SessionSummary, error) {
  25. const q = `
  26. SELECT id, user_agent, created_at, last_used_at
  27. FROM sessions
  28. WHERE user_id = $1 AND expires_at > now()
  29. ORDER BY last_used_at DESC`
  30. rows, err := s.db.QueryContext(ctx, q, userID)
  31. if err != nil {
  32. return nil, fmt.Errorf("查询会话列表失败: %w", err)
  33. }
  34. defer rows.Close()
  35. out := make([]SessionSummary, 0, 4)
  36. for rows.Next() {
  37. var item SessionSummary
  38. if err := rows.Scan(&item.ID, &item.UserAgent, &item.CreatedAt, &item.LastUsedAt); err != nil {
  39. return nil, fmt.Errorf("扫描会话失败: %w", err)
  40. }
  41. item.IsCurrent = item.ID == currentSessionID
  42. out = append(out, item)
  43. }
  44. if err := rows.Err(); err != nil {
  45. return nil, fmt.Errorf("读取会话列表失败: %w", err)
  46. }
  47. return out, nil
  48. }
  49. // RevokeSession 撤销指定会话(api.md 3.2)。
  50. // 不允许撤销当前会话:api.md 3.2 要求返回 422,引导走登出接口。
  51. func (s *Service) RevokeSession(ctx context.Context, userID, sessionID, currentSessionID string) error {
  52. if sessionID == currentSessionID {
  53. return ErrCannotRevokeCurrentSession
  54. }
  55. const q = `DELETE FROM sessions WHERE id = $1 AND user_id = $2`
  56. res, err := s.db.ExecContext(ctx, q, sessionID, userID)
  57. if err != nil {
  58. return fmt.Errorf("删除会话失败: %w", err)
  59. }
  60. n, err := res.RowsAffected()
  61. if err != nil {
  62. return fmt.Errorf("读取删除结果失败: %w", err)
  63. }
  64. if n == 0 {
  65. // 不存在或不属于当前用户,一律 404(不泄露存在性)
  66. return ErrSessionNotFound
  67. }
  68. return nil
  69. }
  70. // FindSessionOwner 返回会话所属用户与创建时间,供撤销时写审计事件。
  71. // 会话不存在(或不属于该用户)时返回 ErrSessionNotFound。
  72. func (s *Service) FindSessionOwner(ctx context.Context, userID, sessionID string) (time.Time, error) {
  73. const q = `SELECT created_at FROM sessions WHERE id = $1 AND user_id = $2`
  74. var createdAt time.Time
  75. err := s.db.QueryRowContext(ctx, q, sessionID, userID).Scan(&createdAt)
  76. if errors.Is(err, sql.ErrNoRows) {
  77. return time.Time{}, ErrSessionNotFound
  78. }
  79. if err != nil {
  80. return time.Time{}, fmt.Errorf("查询会话失败: %w", err)
  81. }
  82. return createdAt, nil
  83. }
苏ICP备2026082406号
苏公网安备 待审核