Brijesh's Git Server — argus-core @ 6efa8455a7ff8585142540f046a36c6adf2b30f5

Logging service

feat: move from postgres to cassandra
Brijesh Wawdhane ops@brijesh.dev
Sat, 14 Dec 2024 03:21:09 +0530
commit

6efa8455a7ff8585142540f046a36c6adf2b30f5

parent

5e54080a940f236c483faa37881d3b3e0bcdbf1d

M go.modgo.mod

@@ -5,18 +5,15 @@

require ( github.com/go-chi/chi/v5 v5.1.0 github.com/go-chi/cors v1.2.1 + github.com/gocql/gocql v1.7.0 github.com/golang-jwt/jwt/v5 v5.2.1 - github.com/google/uuid v1.6.0 - github.com/jackc/pgx/v5 v5.7.1 github.com/joho/godotenv v1.5.1 golang.org/x/crypto v0.27.0 ) require ( - github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/stretchr/testify v1.9.0 // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/text v0.18.0 // indirect + github.com/golang/snappy v0.0.3 // indirect + github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect + github.com/kr/pretty v0.3.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect )
M go.sumgo.sum

@@ -1,38 +1,38 @@

+github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= +github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/gocql/gocql v1.7.0 h1:O+7U7/1gSN7QTEAaMEsJc1Oq2QHXvCWoF3DFK9HDHus= +github.com/gocql/gocql v1.7.0/go.mod h1:vnlvXyFZeLBF0Wy+RS8hrOdbn0UWsWtdg07XJnFxZ+4= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= -github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= -github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= -github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
M internal/auth/auth.gointernal/auth/auth.go

@@ -5,8 +5,8 @@ "errors"

"fmt" "time" + "github.com/gocql/gocql" "github.com/golang-jwt/jwt/v5" - "github.com/google/uuid" "golang.org/x/crypto/bcrypt" "argus-core/internal/database"

@@ -36,17 +36,15 @@ ErrAPIKeyNotFound = errors.New("API key not found")

) type Service interface { - // User authentication Register(email, password string) (*database.User, error) Login(email, password string) (string, *database.User, error) // Returns JWT token and user ValidateToken(token string) (*database.User, error) - // API Key management - CreateAPIKey(userID uuid.UUID, name string, expiresAt *time.Time) (*database.APIKey, string, error) // Returns APIKey and the actual key + CreateAPIKey(userID gocql.UUID, name string, expiresAt *time.Time) (*database.APIKey, string, error) // Returns APIKey and the actual key ValidateAPIKey(key string) (*database.APIKey, error) - ListAPIKeys(userID uuid.UUID) ([]database.APIKey, error) - RevokeAPIKey(userID, keyID uuid.UUID) error - DeleteAPIKey(userID, keyID uuid.UUID) error + ListAPIKeys(userID gocql.UUID) ([]database.APIKey, error) + RevokeAPIKey(userID, keyID gocql.UUID) error + DeleteAPIKey(userID, keyID gocql.UUID) error } type service struct {

@@ -136,7 +134,7 @@ return nil, ErrInvalidToken

} // Parse user ID - userID, err := uuid.Parse(claims["sub"].(string)) + userID, err := gocql.ParseUUID(claims["sub"].(string)) if err != nil { return nil, ErrInvalidToken }

@@ -150,7 +148,7 @@

return user, nil } -func (s *service) CreateAPIKey(userID uuid.UUID, name string, expiresAt *time.Time) (*database.APIKey, string, error) { +func (s *service) CreateAPIKey(userID gocql.UUID, name string, expiresAt *time.Time) (*database.APIKey, string, error) { // Generate random API key apiKeyStr, err := generateAPIKey() if err != nil {

@@ -192,23 +190,17 @@ if !apiKey.IsActive {

return nil, ErrAPIKeyNotFound } - // Update last used timestamp - if err := s.db.UpdateAPIKeyLastUsed(apiKey.ID); err != nil { - // Log error but don't fail the request - // log.Printf("Failed to update API key last used: %v", err) - } - return apiKey, nil } -func (s *service) ListAPIKeys(userID uuid.UUID) ([]database.APIKey, error) { +func (s *service) ListAPIKeys(userID gocql.UUID) ([]database.APIKey, error) { return s.db.ListAPIKeys(userID) } -func (s *service) RevokeAPIKey(userID, keyID uuid.UUID) error { +func (s *service) RevokeAPIKey(userID, keyID gocql.UUID) error { return s.db.RevokeAPIKey(userID, keyID) } -func (s *service) DeleteAPIKey(userID, keyID uuid.UUID) error { +func (s *service) DeleteAPIKey(userID, keyID gocql.UUID) error { return s.db.DeleteAPIKey(userID, keyID) }
M internal/auth/utils.gointernal/auth/utils.go

@@ -9,13 +9,11 @@ "fmt"

) const ( - // APIKeyPrefix is the prefix for all API keys APIKeyPrefix = "argus" - // APIKeyBytes is the number of random bytes to generate for the API key - APIKeyBytes = 32 + APIKeyBytes = 32 ) -// generateAPIKey generates a new API key with format: argus_<random-string> +// generateAPIKey generates a new API key with format: argus_<random-string> base64 encoded // The random string is base64 encoded and URL safe func generateAPIKey() (string, error) { // Generate random bytes
M internal/database/database.gointernal/database/database.go

@@ -1,31 +1,26 @@

package database import ( - "context" - "database/sql" "fmt" "log" "os" - "strconv" "time" - "github.com/google/uuid" - _ "github.com/jackc/pgx/v5/stdlib" + "github.com/gocql/gocql" _ "github.com/joho/godotenv/autoload" ) type User struct { - ID uuid.UUID `json:"id"` - Email string `json:"email"` - PasswordHash string `json:"-"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID gocql.UUID `json:"id"` + Email string `json:"email"` + PasswordHash string `json:"-"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } -// APIKey represents an API key in the database type APIKey struct { - ID uuid.UUID `json:"id"` - UserID uuid.UUID `json:"user_id"` + ID gocql.UUID `json:"id"` + UserID gocql.UUID `json:"user_id"` Name string `json:"name"` KeyHash string `json:"-"` CreatedAt time.Time `json:"created_at"`

@@ -34,314 +29,240 @@ ExpiresAt *time.Time `json:"expires_at,omitempty"`

IsActive bool `json:"is_active"` } -// Service represents a service that interacts with a database. type Service interface { - // Health returns a map of health status information. Health() map[string]string - - // Close terminates the database connection. Close() error - // User-related queries CreateUser(email, passwordHash string) (*User, error) GetUserByEmail(email string) (*User, error) - GetUserByID(id uuid.UUID) (*User, error) + GetUserByID(id gocql.UUID) (*User, error) - // API Key-related queries - CreateAPIKey(userID uuid.UUID, name, keyHash string, expiresAt *time.Time) (*APIKey, error) + CreateAPIKey(userID gocql.UUID, name, keyHash string, expiresAt *time.Time) (*APIKey, error) GetAPIKeyByHash(keyHash string) (*APIKey, error) - ListAPIKeys(userID uuid.UUID) ([]APIKey, error) - UpdateAPIKeyLastUsed(keyID uuid.UUID) error - RevokeAPIKey(userID, keyID uuid.UUID) error - DeleteAPIKey(userID, keyID uuid.UUID) error + ListAPIKeys(userID gocql.UUID) ([]APIKey, error) + UpdateAPIKeyLastUsed(keyID gocql.UUID) error + RevokeAPIKey(userID, keyID gocql.UUID) error + DeleteAPIKey(userID, keyID gocql.UUID) error } type service struct { - db *sql.DB + session *gocql.Session } -var ( - database = os.Getenv("DB_DATABASE") - password = os.Getenv("DB_PASSWORD") - username = os.Getenv("DB_USERNAME") - port = os.Getenv("DB_PORT") - host = os.Getenv("DB_HOST") - schema = os.Getenv("DB_SCHEMA") - dbInstance *service -) +func New() Service { + // Load environment variables + cassandraHost := os.Getenv("CASSANDRA_HOST") + cassandraUsername := os.Getenv("CASSANDRA_USERNAME") + cassandraPassword := os.Getenv("CASSANDRA_PASSWORD") + cassandraCaPath := os.Getenv("CASSANDRA_CA_PATH") + cassandraKeyspace := os.Getenv("CASSANDRA_KEYSPACE") -func New() Service { - // Reuse Connection - if dbInstance != nil { - return dbInstance + cluster := gocql.NewCluster(cassandraHost) + cluster.Port = 9142 + cluster.Authenticator = gocql.PasswordAuthenticator{ + Username: cassandraUsername, + Password: cassandraPassword, + } + cluster.SslOpts = &gocql.SslOptions{ + CaPath: cassandraCaPath, + EnableHostVerification: false, } - connStr := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable&search_path=%s", username, password, host, port, database, schema) - db, err := sql.Open("pgx", connStr) + cluster.Consistency = gocql.LocalQuorum + cluster.Keyspace = cassandraKeyspace + + session, err := cluster.CreateSession() if err != nil { - log.Fatal(err) - } - dbInstance = &service{ - db: db, + log.Fatal("Failed to create Cassandra session:", err) } - return dbInstance + + return &service{session: session} } -// Health checks the health of the database connection by pinging the database. -// It returns a map with keys indicating various health statistics. func (s *service) Health() map[string]string { - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) - defer cancel() - stats := make(map[string]string) - // Ping the database - err := s.db.PingContext(ctx) - if err != nil { + if err := s.session.Query("SELECT release_version FROM system.local").Exec(); err != nil { stats["status"] = "down" - stats["error"] = fmt.Sprintf("db down: %v", err) - log.Fatalf("db down: %v", err) // Log the error and terminate the program + stats["error"] = fmt.Sprintf("Cassandra down: %v", err) return stats } - // Database is up, add more statistics stats["status"] = "up" - stats["message"] = "It's healthy" - - // Get database stats (like open connections, in use, idle, etc.) - dbStats := s.db.Stats() - stats["open_connections"] = strconv.Itoa(dbStats.OpenConnections) - stats["in_use"] = strconv.Itoa(dbStats.InUse) - stats["idle"] = strconv.Itoa(dbStats.Idle) - stats["wait_count"] = strconv.FormatInt(dbStats.WaitCount, 10) - stats["wait_duration"] = dbStats.WaitDuration.String() - stats["max_idle_closed"] = strconv.FormatInt(dbStats.MaxIdleClosed, 10) - stats["max_lifetime_closed"] = strconv.FormatInt(dbStats.MaxLifetimeClosed, 10) - - // Evaluate stats to provide a health message - if dbStats.OpenConnections > 40 { // Assuming 50 is the max for this example - stats["message"] = "The database is experiencing heavy load." - } - - if dbStats.WaitCount > 1000 { - stats["message"] = "The database has a high number of wait events, indicating potential bottlenecks." - } - - if dbStats.MaxIdleClosed > int64(dbStats.OpenConnections)/2 { - stats["message"] = "Many idle connections are being closed, consider revising the connection pool settings." - } - - if dbStats.MaxLifetimeClosed > int64(dbStats.OpenConnections)/2 { - stats["message"] = "Many connections are being closed due to max lifetime, consider increasing max lifetime or revising the connection usage pattern." - } - + stats["message"] = "Cassandra is healthy" return stats } -// Close closes the database connection. -// It logs a message indicating the disconnection from the specific database. -// If the connection is successfully closed, it returns nil. -// If an error occurs while closing the connection, it returns the error. func (s *service) Close() error { - log.Printf("Disconnected from database: %s", database) - return s.db.Close() + s.session.Close() + return nil } -// auth queries - +// User operations func (s *service) CreateUser(email, passwordHash string) (*User, error) { - var user User - err := s.db.QueryRow(` - INSERT INTO users (email, password_hash) - VALUES ($1, $2) - RETURNING id, email, password_hash, created_at, updated_at - `, email, passwordHash).Scan( - &user.ID, - &user.Email, - &user.PasswordHash, - &user.CreatedAt, - &user.UpdatedAt, - ) - if err != nil { + user := &User{ + ID: gocql.TimeUUID(), + Email: email, + PasswordHash: passwordHash, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + if err := s.session.Query(` + INSERT INTO users (id, email, password_hash, created_at, updated_at) + VALUES (?, ?, ?, ?, ?)`, + user.ID, user.Email, user.PasswordHash, user.CreatedAt, user.UpdatedAt, + ).Exec(); err != nil { return nil, fmt.Errorf("error creating user: %w", err) } - return &user, nil + + return user, nil } func (s *service) GetUserByEmail(email string) (*User, error) { var user User - err := s.db.QueryRow(` - SELECT id, email, password_hash, created_at, updated_at - FROM users - WHERE email = $1 - `, email).Scan( - &user.ID, - &user.Email, - &user.PasswordHash, - &user.CreatedAt, - &user.UpdatedAt, - ) - if err != nil { - if err == sql.ErrNoRows { - return nil, fmt.Errorf("user not found") - } - return nil, fmt.Errorf("error getting user: %w", err) + if err := s.session.Query(` + SELECT id, email, password_hash, created_at, updated_at + FROM users WHERE email = ? ALLOW FILTERING`, + email, + ).Scan(&user.ID, &user.Email, &user.PasswordHash, &user.CreatedAt, &user.UpdatedAt); err != nil { + return nil, fmt.Errorf("user not found: %w", err) } return &user, nil } -func (s *service) GetUserByID(id uuid.UUID) (*User, error) { +func (s *service) GetUserByID(id gocql.UUID) (*User, error) { var user User - err := s.db.QueryRow(` - SELECT id, email, password_hash, created_at, updated_at - FROM users - WHERE id = $1 - `, id).Scan( - &user.ID, - &user.Email, - &user.PasswordHash, - &user.CreatedAt, - &user.UpdatedAt, - ) - if err != nil { - if err == sql.ErrNoRows { - return nil, fmt.Errorf("user not found") - } - return nil, fmt.Errorf("error getting user: %w", err) + if err := s.session.Query(` + SELECT id, email, password_hash, created_at, updated_at + FROM users WHERE id = ?`, + id, + ).Scan(&user.ID, &user.Email, &user.PasswordHash, &user.CreatedAt, &user.UpdatedAt); err != nil { + return nil, fmt.Errorf("user not found: %w", err) } return &user, nil } // API Key-related query implementations -func (s *service) CreateAPIKey(userID uuid.UUID, name, keyHash string, expiresAt *time.Time) (*APIKey, error) { - var apiKey APIKey - err := s.db.QueryRow(` - INSERT INTO api_keys (user_id, name, key_hash, expires_at) - VALUES ($1, $2, $3, $4) - RETURNING id, user_id, name, key_hash, created_at, last_used_at, expires_at, is_active - `, userID, name, keyHash, expiresAt).Scan( - &apiKey.ID, - &apiKey.UserID, - &apiKey.Name, - &apiKey.KeyHash, - &apiKey.CreatedAt, - &apiKey.LastUsedAt, - &apiKey.ExpiresAt, - &apiKey.IsActive, - ) - if err != nil { +func (s *service) CreateAPIKey(userID gocql.UUID, name, keyHash string, expiresAt *time.Time) (*APIKey, error) { + apiKey := &APIKey{ + ID: gocql.TimeUUID(), + UserID: userID, + Name: name, + KeyHash: keyHash, + CreatedAt: time.Now(), + ExpiresAt: expiresAt, + IsActive: true, + } + + if err := s.session.Query(` + INSERT INTO api_keys (id, user_id, name, key_hash, created_at, expires_at, is_active) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + apiKey.ID, apiKey.UserID, apiKey.Name, apiKey.KeyHash, + apiKey.CreatedAt, apiKey.ExpiresAt, apiKey.IsActive, + ).Exec(); err != nil { return nil, fmt.Errorf("error creating API key: %w", err) } - return &apiKey, nil + + return apiKey, nil } func (s *service) GetAPIKeyByHash(keyHash string) (*APIKey, error) { var apiKey APIKey - err := s.db.QueryRow(` - SELECT id, user_id, name, key_hash, created_at, last_used_at, expires_at, is_active - FROM api_keys - WHERE key_hash = $1 - `, keyHash).Scan( - &apiKey.ID, - &apiKey.UserID, - &apiKey.Name, - &apiKey.KeyHash, - &apiKey.CreatedAt, - &apiKey.LastUsedAt, - &apiKey.ExpiresAt, - &apiKey.IsActive, - ) - if err != nil { - if err == sql.ErrNoRows { - return nil, fmt.Errorf("API key not found") - } - return nil, fmt.Errorf("error getting API key: %w", err) + if err := s.session.Query(` + SELECT id, user_id, name, key_hash, created_at, last_used_at, expires_at, is_active + FROM api_keys WHERE key_hash = ? ALLOW FILTERING`, + keyHash, + ).Scan( + &apiKey.ID, &apiKey.UserID, &apiKey.Name, &apiKey.KeyHash, + &apiKey.CreatedAt, &apiKey.LastUsedAt, &apiKey.ExpiresAt, &apiKey.IsActive, + ); err != nil { + return nil, fmt.Errorf("API key not found: %w", err) } return &apiKey, nil } -func (s *service) ListAPIKeys(userID uuid.UUID) ([]APIKey, error) { - rows, err := s.db.Query(` - SELECT id, user_id, name, key_hash, created_at, last_used_at, expires_at, is_active - FROM api_keys - WHERE user_id = $1 - ORDER BY created_at DESC - `, userID) - if err != nil { - return nil, fmt.Errorf("error listing API keys: %w", err) - } - defer rows.Close() +func (s *service) ListAPIKeys(userID gocql.UUID) ([]APIKey, error) { + iter := s.session.Query(` + SELECT id, user_id, name, key_hash, created_at, last_used_at, expires_at, is_active + FROM api_keys WHERE user_id = ? ALLOW FILTERING`, + userID, + ).Iter() var apiKeys []APIKey - for rows.Next() { - var apiKey APIKey - err := rows.Scan( - &apiKey.ID, - &apiKey.UserID, - &apiKey.Name, - &apiKey.KeyHash, - &apiKey.CreatedAt, - &apiKey.LastUsedAt, - &apiKey.ExpiresAt, - &apiKey.IsActive, - ) - if err != nil { - return nil, fmt.Errorf("error scanning API key: %w", err) - } + var apiKey APIKey + + for iter.Scan( + &apiKey.ID, &apiKey.UserID, &apiKey.Name, &apiKey.KeyHash, + &apiKey.CreatedAt, &apiKey.LastUsedAt, &apiKey.ExpiresAt, &apiKey.IsActive, + ) { apiKeys = append(apiKeys, apiKey) } + + if err := iter.Close(); err != nil { + return nil, fmt.Errorf("error listing API keys: %w", err) + } + return apiKeys, nil } -func (s *service) UpdateAPIKeyLastUsed(keyID uuid.UUID) error { - _, err := s.db.Exec(` - UPDATE api_keys - SET last_used_at = CURRENT_TIMESTAMP - WHERE id = $1 - `, keyID) - if err != nil { +func (s *service) UpdateAPIKeyLastUsed(keyID gocql.UUID) error { + now := time.Now() + if err := s.session.Query(` + UPDATE api_keys SET last_used_at = ? WHERE id = ?`, + now, keyID, + ).Exec(); err != nil { return fmt.Errorf("error updating API key last used: %w", err) } return nil } -func (s *service) RevokeAPIKey(userID, keyID uuid.UUID) error { - result, err := s.db.Exec(` - UPDATE api_keys - SET is_active = false - WHERE id = $1 AND user_id = $2 - `, keyID, userID) - if err != nil { - return fmt.Errorf("error revoking API key: %w", err) +func (s *service) RevokeAPIKey(userID, keyID gocql.UUID) error { + // First verify the API key belongs to the user + var count int + if err := s.session.Query(` + SELECT COUNT(*) FROM api_keys + WHERE id = ? AND user_id = ? ALLOW FILTERING`, + keyID, userID, + ).Scan(&count); err != nil { + return fmt.Errorf("error verifying API key ownership: %w", err) } - rowsAffected, err := result.RowsAffected() - if err != nil { - return fmt.Errorf("error getting rows affected: %w", err) + if count == 0 { + return fmt.Errorf("API key not found or not owned by user") } - if rowsAffected == 0 { - return fmt.Errorf("API key not found or not owned by user") + // Update the is_active status + if err := s.session.Query(` + UPDATE api_keys SET is_active = ? WHERE id = ?`, + false, keyID, + ).Exec(); err != nil { + return fmt.Errorf("error revoking API key: %w", err) } return nil } -func (s *service) DeleteAPIKey(userID, keyID uuid.UUID) error { - result, err := s.db.Exec(` - DELETE FROM api_keys - WHERE id = $1 AND user_id = $2 - `, keyID, userID) - if err != nil { - return fmt.Errorf("error deleting API key: %w", err) +func (s *service) DeleteAPIKey(userID, keyID gocql.UUID) error { + // First verify the API key belongs to the user + var count int + if err := s.session.Query(` + SELECT COUNT(*) FROM api_keys + WHERE id = ? AND user_id = ? ALLOW FILTERING`, + keyID, userID, + ).Scan(&count); err != nil { + return fmt.Errorf("error verifying API key ownership: %w", err) } - rowsAffected, err := result.RowsAffected() - if err != nil { - return fmt.Errorf("error getting rows affected: %w", err) + if count == 0 { + return fmt.Errorf("API key not found or not owned by user") } - if rowsAffected == 0 { - return fmt.Errorf("API key not found or not owned by user") + // Delete the API key + if err := s.session.Query(` + DELETE FROM api_keys WHERE id = ?`, + keyID, + ).Exec(); err != nil { + return fmt.Errorf("error deleting API key: %w", err) } return nil
M internal/server/routes.gointernal/server/routes.go

@@ -9,7 +9,7 @@

"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" - "github.com/google/uuid" + "github.com/gocql/gocql" "argus-core/internal/auth" )

@@ -72,7 +72,6 @@ }

user, err := s.auth.Register(req.Email, req.Password) if err != nil { - log.Println("Failed to register user:", err) s.respondWithError(w, http.StatusInternalServerError, "Failed to register user") return }

@@ -93,7 +92,6 @@ s.respondWithError(w, http.StatusUnauthorized, "Invalid credentials")

return } - // Return both token and user info in the response body s.respondWithJSON(w, http.StatusOK, map[string]interface{}{ "token": token, "user": user,

@@ -102,9 +100,8 @@ }

func (s *Server) handleGetCurrentUser(w http.ResponseWriter, r *http.Request) { // Get user ID from context (set by authMiddleware) - userID := r.Context().Value("userID").(uuid.UUID) + userID := r.Context().Value("userID").(gocql.UUID) - // Get user from database user, err := s.db.GetUserByID(userID) if err != nil { s.respondWithError(w, http.StatusInternalServerError, "Failed to get user details")

@@ -121,7 +118,7 @@ s.respondWithError(w, http.StatusBadRequest, "Invalid request payload")

return } - userID := r.Context().Value("userID").(uuid.UUID) + userID := r.Context().Value("userID").(gocql.UUID) apiKey, keyString, err := s.auth.CreateAPIKey(userID, req.Name, req.ExpiresAt) if err != nil { s.respondWithError(w, http.StatusInternalServerError, "Failed to create API key")

@@ -135,7 +132,7 @@ })

} func (s *Server) handleListAPIKeys(w http.ResponseWriter, r *http.Request) { - userID := r.Context().Value("userID").(uuid.UUID) + userID := r.Context().Value("userID").(gocql.UUID) apiKeys, err := s.auth.ListAPIKeys(userID) if err != nil { s.respondWithError(w, http.StatusInternalServerError, "Failed to list API keys")

@@ -146,8 +143,8 @@ s.respondWithJSON(w, http.StatusOK, apiKeys)

} func (s *Server) handleRevokeAPIKey(w http.ResponseWriter, r *http.Request) { - userID := r.Context().Value("userID").(uuid.UUID) - keyID, err := uuid.Parse(chi.URLParam(r, "keyID")) + userID := r.Context().Value("userID").(gocql.UUID) + keyID, err := gocql.ParseUUID(chi.URLParam(r, "keyID")) if err != nil { s.respondWithError(w, http.StatusBadRequest, "Invalid key ID") return

@@ -162,8 +159,8 @@ w.WriteHeader(http.StatusNoContent)

} func (s *Server) handleDeleteAPIKey(w http.ResponseWriter, r *http.Request) { - userID := r.Context().Value("userID").(uuid.UUID) - keyID, err := uuid.Parse(chi.URLParam(r, "keyID")) + userID := r.Context().Value("userID").(gocql.UUID) + keyID, err := gocql.ParseUUID(chi.URLParam(r, "keyID")) if err != nil { s.respondWithError(w, http.StatusBadRequest, "Invalid key ID") return

@@ -179,21 +176,13 @@ }

func (s *Server) authMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Get token from Authorization header authHeader := r.Header.Get("Authorization") if authHeader == "" { s.respondWithError(w, http.StatusUnauthorized, "No authorization header") return } - // Remove "Bearer " prefix if present - token := authHeader - if len(authHeader) > 7 && authHeader[:7] == "Bearer " { - token = authHeader[7:] - } - - // Validate token - user, err := s.auth.ValidateToken(token) + user, err := s.auth.ValidateToken(authHeader) if err != nil { s.respondWithError(w, http.StatusUnauthorized, "Invalid token") return
A sf-class2-root.crt

@@ -0,0 +1,24 @@

+-----BEGIN CERTIFICATE----- +MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzEl +MCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMp +U3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQw +NjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBoMQswCQYDVQQGEwJVUzElMCMGA1UE +ChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZp +ZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqGSIb3 +DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf +8MOh2tTYbitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN ++lq2cwQlZut3f+dZxkqZJRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0 +X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVmepsZGD3/cVE8MC5fvj13c7JdBmzDI1aa +K4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSNF4Azbl5KXZnJHoe0nRrA +1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HFMIHCMB0G +A1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fR +zt0fhvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0 +YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBD +bGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8w +DQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGsafPzWdqbAYcaT1epoXkJKtv3 +L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLMPUxA2IGvd56D +eruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl +xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynp +VSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY +WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q= +-----END CERTIFICATE-----