webapi_auth.go 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. package state
  2. import (
  3. "context"
  4. "database/sql"
  5. "errors"
  6. "fmt"
  7. "time"
  8. )
  9. // WebAPITokenStore manages authentication tokens for Web API sessions.
  10. type WebAPITokenStore struct {
  11. store *SQLiteUserStore
  12. }
  13. // NewWebAPITokenStore creates a new token store.
  14. func (s *SQLiteUserStore) NewWebAPITokenStore() *WebAPITokenStore {
  15. return &WebAPITokenStore{store: s}
  16. }
  17. // StoreToken saves an authentication token for a user.
  18. func (s *WebAPITokenStore) StoreToken(ctx context.Context, token string, screenName IdentScreenName, expiresAt time.Time) error {
  19. query := `
  20. INSERT INTO webapi_tokens (token, screen_name, expires_at, created_at)
  21. VALUES (?, ?, ?, ?)
  22. ON CONFLICT(token) DO UPDATE SET
  23. screen_name = excluded.screen_name,
  24. expires_at = excluded.expires_at
  25. `
  26. _, err := s.store.db.ExecContext(ctx, query, token, screenName.String(), expiresAt, time.Now())
  27. if err != nil {
  28. return fmt.Errorf("failed to store token: %w", err)
  29. }
  30. return nil
  31. }
  32. // ValidateToken checks if a token is valid and returns the associated screen name.
  33. func (s *WebAPITokenStore) ValidateToken(ctx context.Context, token string) (IdentScreenName, error) {
  34. var screenNameStr string
  35. var expiresAt time.Time
  36. query := `
  37. SELECT screen_name, expires_at
  38. FROM webapi_tokens
  39. WHERE token = ?
  40. `
  41. err := s.store.db.QueryRowContext(ctx, query, token).Scan(&screenNameStr, &expiresAt)
  42. if err != nil {
  43. if errors.Is(err, sql.ErrNoRows) {
  44. return NewIdentScreenName(""), errors.New("invalid token")
  45. }
  46. return NewIdentScreenName(""), fmt.Errorf("failed to validate token: %w", err)
  47. }
  48. // Check if token has expired
  49. if time.Now().After(expiresAt) {
  50. // Clean up expired token
  51. s.DeleteToken(ctx, token)
  52. return NewIdentScreenName(""), errors.New("token expired")
  53. }
  54. return NewIdentScreenName(screenNameStr), nil
  55. }
  56. // DeleteToken removes a token.
  57. func (s *WebAPITokenStore) DeleteToken(ctx context.Context, token string) error {
  58. query := `DELETE FROM webapi_tokens WHERE token = ?`
  59. _, err := s.store.db.ExecContext(ctx, query, token)
  60. if err != nil {
  61. return fmt.Errorf("failed to delete token: %w", err)
  62. }
  63. return nil
  64. }
  65. // CleanupExpiredTokens removes all expired tokens from the database.
  66. func (s *WebAPITokenStore) CleanupExpiredTokens(ctx context.Context) error {
  67. query := `DELETE FROM webapi_tokens WHERE expires_at < ?`
  68. _, err := s.store.db.ExecContext(ctx, query, time.Now())
  69. if err != nil {
  70. return fmt.Errorf("failed to cleanup expired tokens: %w", err)
  71. }
  72. return nil
  73. }
  74. // AuthenticateUser verifies username and password.
  75. // This implementation uses the existing user store for authentication.
  76. func (u *SQLiteUserStore) AuthenticateUser(ctx context.Context, username, password string) (*User, error) {
  77. // Convert username to IdentScreenName for lookup
  78. identSN := NewIdentScreenName(username)
  79. // Try to find the user
  80. user, err := u.User(ctx, identSN)
  81. if err != nil {
  82. return nil, fmt.Errorf("user not found: %w", err)
  83. }
  84. // In development mode with DISABLE_AUTH=true, accept any password
  85. // In production, this would verify the password hash
  86. // For now, we'll accept any non-empty password if the user exists
  87. if password == "" {
  88. return nil, errors.New("password required")
  89. }
  90. // TODO: In production, verify password hash here
  91. // For development with DISABLE_AUTH, we just check if user exists
  92. return user, nil
  93. }
  94. // FindUserByScreenName finds a user by their screen name.
  95. // This is just an alias for the User method to satisfy the UserManager interface.
  96. func (u *SQLiteUserStore) FindUserByScreenName(ctx context.Context, screenName IdentScreenName) (*User, error) {
  97. return u.User(ctx, screenName)
  98. }