sqlstore.go 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. package auth
  2. import (
  3. "context"
  4. "database/sql"
  5. "errors"
  6. "fmt"
  7. "time"
  8. )
  9. // SQLStore 是基于 database/sql 的 Store 实现,手写 SQL(architecture.md 5.1)。
  10. type SQLStore struct {
  11. db *sql.DB
  12. }
  13. // NewSQLStore 构造 SQLStore。
  14. func NewSQLStore(db *sql.DB) *SQLStore {
  15. return &SQLStore{db: db}
  16. }
  17. // DB 暴露底层连接池,供 main 装配迁移检查与测试使用。
  18. func (s *SQLStore) DB() *sql.DB { return s.db }
  19. // FindSessionByHash 按 token_hash 查未过期的会话(auth.md 6.1)。
  20. func (s *SQLStore) FindSessionByHash(ctx context.Context, tokenHash string) (Session, error) {
  21. const q = `
  22. SELECT id, user_id, token_hash, expires_at, last_used_at, user_agent, created_at
  23. FROM sessions
  24. WHERE token_hash = $1 AND expires_at > now()`
  25. var sess Session
  26. err := s.db.QueryRowContext(ctx, q, tokenHash).Scan(
  27. &sess.ID, &sess.UserID, &sess.TokenHash,
  28. &sess.ExpiresAt, &sess.LastUsedAt, &sess.UserAgent, &sess.CreatedAt,
  29. )
  30. if errors.Is(err, sql.ErrNoRows) {
  31. return Session{}, ErrNotFound
  32. }
  33. if err != nil {
  34. return Session{}, fmt.Errorf("查询会话失败: %w", err)
  35. }
  36. return sess, nil
  37. }
  38. // TouchSession 更新 last_used_at,不延长 expires_at(auth.md 第三节规则 5)。
  39. func (s *SQLStore) TouchSession(ctx context.Context, id string, at time.Time) error {
  40. const q = `UPDATE sessions SET last_used_at = $2 WHERE id = $1`
  41. if _, err := s.db.ExecContext(ctx, q, id, at); err != nil {
  42. return fmt.Errorf("更新会话时间失败: %w", err)
  43. }
  44. return nil
  45. }
  46. // DeleteSession 撤销会话:硬删除(auth.md 第三节规则 7)。
  47. func (s *SQLStore) DeleteSession(ctx context.Context, id string) error {
  48. const q = `DELETE FROM sessions WHERE id = $1`
  49. if _, err := s.db.ExecContext(ctx, q, id); err != nil {
  50. return fmt.Errorf("删除会话失败: %w", err)
  51. }
  52. return nil
  53. }
  54. // FindTokenByHash 按 token_hash 查 Token,包含已撤销 / 已过期的记录,由调用方判定。
  55. func (s *SQLStore) FindTokenByHash(ctx context.Context, tokenHash string) (APIToken, error) {
  56. const q = `
  57. SELECT id, user_id, name, token_hash, token_prefix, scope,
  58. expires_at, last_used_at, revoked_at, created_at
  59. FROM tokens
  60. WHERE token_hash = $1`
  61. var t APIToken
  62. err := s.db.QueryRowContext(ctx, q, tokenHash).Scan(
  63. &t.ID, &t.UserID, &t.Name, &t.TokenHash, &t.TokenPrefix, &t.Scope,
  64. &t.ExpiresAt, &t.LastUsedAt, &t.RevokedAt, &t.CreatedAt,
  65. )
  66. if errors.Is(err, sql.ErrNoRows) {
  67. return APIToken{}, ErrNotFound
  68. }
  69. if err != nil {
  70. return APIToken{}, fmt.Errorf("查询 Token 失败: %w", err)
  71. }
  72. return t, nil
  73. }
  74. // TouchToken 更新 last_used_at。
  75. func (s *SQLStore) TouchToken(ctx context.Context, id string, at time.Time) error {
  76. const q = `UPDATE tokens SET last_used_at = $2 WHERE id = $1`
  77. if _, err := s.db.ExecContext(ctx, q, id, at); err != nil {
  78. return fmt.Errorf("更新 Token 时间失败: %w", err)
  79. }
  80. return nil
  81. }
  82. // GetUser 按 id 取用户(不含密码哈希与 TOTP secret,够 /api/me 使用)。
  83. func (s *SQLStore) GetUser(ctx context.Context, id string) (User, error) {
  84. const q = `SELECT id, username, password_hash, coalesce(totp_secret, ''), totp_enabled
  85. FROM users WHERE id = $1`
  86. var u User
  87. err := s.db.QueryRowContext(ctx, q, id).Scan(
  88. &u.ID, &u.Username, &u.PasswordHash, &u.TotpSecret, &u.TotpEnabled)
  89. if errors.Is(err, sql.ErrNoRows) {
  90. return User{}, ErrNotFound
  91. }
  92. if err != nil {
  93. return User{}, fmt.Errorf("查询用户失败: %w", err)
  94. }
  95. return u, nil
  96. }
  97. // 编译期断言。
  98. var _ Store = (*SQLStore)(nil)
苏ICP备2026082406号
苏公网安备 待审核