package auth import ( "context" "database/sql" "encoding/json" "fmt" "log" "time" _ "github.com/mattn/go-sqlite3" ) // DatabaseAuthStore implements authentication using SQLite database type DatabaseAuthStore struct { db *sql.DB } // APIKeyRecord represents an API key in the database type APIKeyRecord struct { ID int `json:"id"` UserID string `json:"user_id"` KeyHash string `json:"key_hash"` Admin bool `json:"admin"` Roles string `json:"roles"` // JSON array Permissions string `json:"permissions"` // JSON object CreatedAt time.Time `json:"created_at"` ExpiresAt *time.Time `json:"expires_at,omitempty"` RevokedAt *time.Time `json:"revoked_at,omitempty"` } // NewDatabaseAuthStore creates a new database-backed auth store func NewDatabaseAuthStore(dbPath string) (*DatabaseAuthStore, error) { db, err := sql.Open("sqlite3", dbPath) if err != nil { return nil, fmt.Errorf("failed to open database: %w", err) } store := &DatabaseAuthStore{db: db} if err := store.init(); err != nil { return nil, fmt.Errorf("failed to initialize database: %w", err) } return store, nil } // init creates the necessary database tables func (s *DatabaseAuthStore) init() error { query := ` CREATE TABLE IF NOT EXISTS api_keys ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL UNIQUE, key_hash TEXT NOT NULL UNIQUE, admin BOOLEAN NOT NULL DEFAULT FALSE, roles TEXT NOT NULL DEFAULT '[]', permissions TEXT NOT NULL DEFAULT '{}', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, expires_at DATETIME, revoked_at DATETIME, CHECK (json_valid(roles)), CHECK (json_valid(permissions)) ); CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash); CREATE INDEX IF NOT EXISTS idx_api_keys_user ON api_keys(user_id); CREATE INDEX IF NOT EXISTS idx_api_keys_active ON api_keys(revoked_at, COALESCE(expires_at, '9999-12-31')); ` _, err := s.db.Exec(query) return err } // ValidateAPIKey checks if an API key is valid and returns user info func (s *DatabaseAuthStore) ValidateAPIKey(ctx context.Context, key string) (*User, error) { keyHash := HashAPIKey(key) query := ` SELECT user_id, admin, roles, permissions, expires_at, revoked_at FROM api_keys WHERE key_hash = ? AND (revoked_at IS NULL OR revoked_at > ?) AND (expires_at IS NULL OR expires_at > ?) ` var userID string var admin bool var rolesJSON, permissionsJSON string var expiresAt, revokedAt sql.NullTime now := time.Now() err := s.db.QueryRowContext(ctx, query, keyHash, now, now).Scan( &userID, &admin, &rolesJSON, &permissionsJSON, &expiresAt, &revokedAt, ) if err != nil { if err == sql.ErrNoRows { return nil, fmt.Errorf("invalid API key") } return nil, fmt.Errorf("database error: %w", err) } // Parse roles var roles []string if err := json.Unmarshal([]byte(rolesJSON), &roles); err != nil { log.Printf("Failed to parse roles for user %s: %v", userID, err) roles = []string{} } // Parse permissions var permissions map[string]bool if err := json.Unmarshal([]byte(permissionsJSON), &permissions); err != nil { log.Printf("Failed to parse permissions for user %s: %v", userID, err) permissions = make(map[string]bool) } // Admin gets all permissions if admin { permissions["*"] = true } return &User{ Name: userID, Admin: admin, Roles: roles, Permissions: permissions, }, nil } // CreateAPIKey creates a new API key in the database func (s *DatabaseAuthStore) CreateAPIKey(ctx context.Context, userID string, keyHash string, admin bool, roles []string, permissions map[string]bool, expiresAt *time.Time) error { rolesJSON, err := json.Marshal(roles) if err != nil { return fmt.Errorf("failed to marshal roles: %w", err) } permissionsJSON, err := json.Marshal(permissions) if err != nil { return fmt.Errorf("failed to marshal permissions: %w", err) } query := ` INSERT INTO api_keys (user_id, key_hash, admin, roles, permissions, expires_at) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(user_id) DO UPDATE SET key_hash = excluded.key_hash, admin = excluded.admin, roles = excluded.roles, permissions = excluded.permissions, expires_at = excluded.expires_at, revoked_at = NULL ` _, err = s.db.ExecContext(ctx, query, userID, keyHash, admin, rolesJSON, permissionsJSON, expiresAt) return err } // RevokeAPIKey revokes an API key func (s *DatabaseAuthStore) RevokeAPIKey(ctx context.Context, userID string) error { query := `UPDATE api_keys SET revoked_at = CURRENT_TIMESTAMP WHERE user_id = ?` _, err := s.db.ExecContext(ctx, query, userID) return err } // ListUsers returns all active users func (s *DatabaseAuthStore) ListUsers(ctx context.Context) ([]APIKeyRecord, error) { query := ` SELECT id, user_id, key_hash, admin, roles, permissions, created_at, expires_at, revoked_at FROM api_keys WHERE revoked_at IS NULL ORDER BY created_at DESC ` rows, err := s.db.QueryContext(ctx, query) if err != nil { return nil, fmt.Errorf("failed to query users: %w", err) } defer rows.Close() var users []APIKeyRecord for rows.Next() { var user APIKeyRecord err := rows.Scan( &user.ID, &user.UserID, &user.KeyHash, &user.Admin, &user.Roles, &user.Permissions, &user.CreatedAt, &user.ExpiresAt, &user.RevokedAt, ) if err != nil { return nil, fmt.Errorf("failed to scan user: %w", err) } users = append(users, user) } return users, nil } // CleanupExpiredKeys removes expired and revoked keys func (s *DatabaseAuthStore) CleanupExpiredKeys(ctx context.Context) error { query := ` DELETE FROM api_keys WHERE (revoked_at IS NOT NULL AND revoked_at < datetime('now', '-30 days')) OR (expires_at IS NOT NULL AND expires_at < datetime('now', '-7 days')) ` _, err := s.db.ExecContext(ctx, query) return err } // Close closes the database connection func (s *DatabaseAuthStore) Close() error { return s.db.Close() }