diff --git a/server/internal/db/db.go b/server/internal/db/db.go index a0c21eb..f097d1d 100644 --- a/server/internal/db/db.go +++ b/server/internal/db/db.go @@ -3,7 +3,9 @@ package db import ( "database/sql" + "fmt" + "novamd/internal/logging" "novamd/internal/models" "novamd/internal/secrets" @@ -77,6 +79,7 @@ type Database interface { Migrate() error } +// Verify that the database implements the required interfaces var ( // Main Database interface _ Database = (*database)(nil) @@ -92,6 +95,15 @@ var ( _ WorkspaceWriter = (*database)(nil) ) +var logger logging.Logger + +func getLogger() logging.Logger { + if logger == nil { + logger = logging.WithGroup("db") + } + return logger +} + // database represents the database connection type database struct { *sql.DB @@ -100,44 +112,84 @@ type database struct { // Init initializes the database connection func Init(dbPath string, secretsService secrets.Service) (Database, error) { + log := getLogger() + log.Info("initializing database", "path", dbPath) + db, err := sql.Open("sqlite3", dbPath) if err != nil { - return nil, err + log.Error("failed to open database", "error", err) + return nil, fmt.Errorf("failed to open database: %w", err) } if err := db.Ping(); err != nil { - return nil, err + log.Error("failed to ping database", "error", err) + return nil, fmt.Errorf("failed to ping database: %w", err) } + log.Debug("database ping successful") // Enable foreign keys for this connection if _, err := db.Exec("PRAGMA foreign_keys = ON"); err != nil { - return nil, err + log.Error("failed to enable foreign keys", "error", err) + return nil, fmt.Errorf("failed to enable foreign keys: %w", err) } + log.Debug("foreign keys enabled") database := &database{ DB: db, secretsService: secretsService, } + log.Info("database initialized successfully") return database, nil } // Close closes the database connection func (db *database) Close() error { - return db.DB.Close() + log := getLogger() + log.Info("closing database connection") + + if err := db.DB.Close(); err != nil { + log.Error("failed to close database", "error", err) + return fmt.Errorf("failed to close database: %w", err) + } + + log.Info("database connection closed successfully") + return nil } // Helper methods for token encryption/decryption func (db *database) encryptToken(token string) (string, error) { + log := getLogger() + if token == "" { + log.Debug("skipping encryption for empty token") return "", nil } - return db.secretsService.Encrypt(token) + + encrypted, err := db.secretsService.Encrypt(token) + if err != nil { + log.Error("failed to encrypt token", "error", err) + return "", fmt.Errorf("failed to encrypt token: %w", err) + } + + log.Debug("token encrypted successfully") + return encrypted, nil } func (db *database) decryptToken(token string) (string, error) { + log := getLogger() + if token == "" { + log.Debug("skipping decryption for empty token") return "", nil } - return db.secretsService.Decrypt(token) + + decrypted, err := db.secretsService.Decrypt(token) + if err != nil { + log.Error("failed to decrypt token", "error", err) + return "", fmt.Errorf("failed to decrypt token: %w", err) + } + + log.Debug("token decrypted successfully") + return decrypted, nil } diff --git a/server/internal/db/migrations.go b/server/internal/db/migrations.go index a5d1586..755bf2d 100644 --- a/server/internal/db/migrations.go +++ b/server/internal/db/migrations.go @@ -2,7 +2,6 @@ package db import ( "fmt" - "log" ) // Migration represents a database migration @@ -79,56 +78,86 @@ var migrations = []Migration{ // Migrate applies all database migrations func (db *database) Migrate() error { + log := getLogger().WithGroup("migrations") + log.Info("starting database migration") + // Create migrations table if it doesn't exist + log.Debug("ensuring migrations table exists") _, err := db.Exec(`CREATE TABLE IF NOT EXISTS migrations ( - version INTEGER PRIMARY KEY - )`) + version INTEGER PRIMARY KEY + )`) if err != nil { - return err + log.Error("failed to create migrations table", "error", err) + return fmt.Errorf("failed to create migrations table: %w", err) } // Get current version + log.Debug("checking current migration version") var currentVersion int err = db.QueryRow("SELECT COALESCE(MAX(version), 0) FROM migrations").Scan(¤tVersion) if err != nil { - return err + log.Error("failed to get current migration version", "error", err) + return fmt.Errorf("failed to get current migration version: %w", err) } + log.Info("current database version", "version", currentVersion) // Apply new migrations for _, migration := range migrations { if migration.Version > currentVersion { - log.Printf("Applying migration %d", migration.Version) + log := log.With("migration_version", migration.Version) + log.Info("applying migration") tx, err := db.Begin() if err != nil { - return err + log.Error("failed to begin transaction", "error", err) + return fmt.Errorf("failed to begin transaction for migration %d: %w", migration.Version, err) } + // Execute migration SQL + log.Debug("executing migration SQL") _, err = tx.Exec(migration.SQL) if err != nil { + log.Error("migration failed", "error", err) if rbErr := tx.Rollback(); rbErr != nil { - return fmt.Errorf("migration %d failed: %v, rollback failed: %v", migration.Version, err, rbErr) + log.Error("rollback failed after migration error", + "migration_error", err, + "rollback_error", rbErr) + return fmt.Errorf("migration %d failed: %v, rollback failed: %v", + migration.Version, err, rbErr) } - return fmt.Errorf("migration %d failed: %v", migration.Version, err) + log.Debug("successfully rolled back failed migration") + return fmt.Errorf("migration %d failed: %w", migration.Version, err) } + // Update migrations table + log.Debug("updating migrations version") _, err = tx.Exec("INSERT INTO migrations (version) VALUES (?)", migration.Version) if err != nil { + log.Error("failed to update migration version", "error", err) if rbErr := tx.Rollback(); rbErr != nil { - return fmt.Errorf("failed to update migration version: %v, rollback failed: %v", err, rbErr) + log.Error("rollback failed after version update error", + "update_error", err, + "rollback_error", rbErr) + return fmt.Errorf("failed to update migration version: %v, rollback failed: %v", + err, rbErr) } - return fmt.Errorf("failed to update migration version: %v", err) + log.Debug("successfully rolled back failed version update") + return fmt.Errorf("failed to update migration version: %w", err) } + // Commit transaction + log.Debug("committing migration") err = tx.Commit() if err != nil { - return fmt.Errorf("failed to commit migration %d: %v", migration.Version, err) + log.Error("failed to commit migration", "error", err) + return fmt.Errorf("failed to commit migration %d: %w", migration.Version, err) } currentVersion = migration.Version + log.Info("migration applied successfully", "new_version", currentVersion) } } - log.Printf("Database is at version %d", currentVersion) + log.Info("database migration completed", "final_version", currentVersion) return nil } diff --git a/server/internal/db/sessions.go b/server/internal/db/sessions.go index 79b9231..4bc66b3 100644 --- a/server/internal/db/sessions.go +++ b/server/internal/db/sessions.go @@ -10,19 +10,36 @@ import ( // CreateSession inserts a new session record into the database func (db *database) CreateSession(session *models.Session) error { + log := getLogger().WithGroup("sessions") + log.Debug("creating new session", + "session_id", session.ID, + "user_id", session.UserID, + "expires_at", session.ExpiresAt) + _, err := db.Exec(` - INSERT INTO sessions (id, user_id, refresh_token, expires_at, created_at) - VALUES (?, ?, ?, ?, ?)`, + INSERT INTO sessions (id, user_id, refresh_token, expires_at, created_at) + VALUES (?, ?, ?, ?, ?)`, session.ID, session.UserID, session.RefreshToken, session.ExpiresAt, session.CreatedAt, ) if err != nil { + log.Error("failed to store session", + "error", err, + "session_id", session.ID, + "user_id", session.UserID) return fmt.Errorf("failed to store session: %w", err) } + + log.Info("session created successfully", + "session_id", session.ID, + "user_id", session.UserID) return nil } // GetSessionByRefreshToken retrieves a session by its refresh token func (db *database) GetSessionByRefreshToken(refreshToken string) (*models.Session, error) { + log := getLogger().WithGroup("sessions") + log.Debug("fetching session by refresh token") + session := &models.Session{} err := db.QueryRow(` SELECT id, user_id, refresh_token, expires_at, created_at @@ -32,59 +49,97 @@ func (db *database) GetSessionByRefreshToken(refreshToken string) (*models.Sessi ).Scan(&session.ID, &session.UserID, &session.RefreshToken, &session.ExpiresAt, &session.CreatedAt) if err == sql.ErrNoRows { + log.Debug("session not found or expired") return nil, fmt.Errorf("session not found or expired") } if err != nil { + log.Error("failed to fetch session by refresh token", "error", err) return nil, fmt.Errorf("failed to fetch session: %w", err) } + log.Debug("session retrieved successfully", + "session_id", session.ID, + "user_id", session.UserID) return session, nil } // GetSessionByID retrieves a session by its ID func (db *database) GetSessionByID(sessionID string) (*models.Session, error) { + log := getLogger().WithGroup("sessions") + log.Debug("fetching session by ID", "session_id", sessionID) + session := &models.Session{} err := db.QueryRow(` - SELECT id, user_id, refresh_token, expires_at, created_at - FROM sessions - WHERE id = ? AND expires_at > ?`, + SELECT id, user_id, refresh_token, expires_at, created_at + FROM sessions + WHERE id = ? AND expires_at > ?`, sessionID, time.Now(), ).Scan(&session.ID, &session.UserID, &session.RefreshToken, &session.ExpiresAt, &session.CreatedAt) if err == sql.ErrNoRows { + log.Debug("session not found", "session_id", sessionID) return nil, fmt.Errorf("session not found") } if err != nil { + log.Error("failed to fetch session by ID", + "error", err, + "session_id", sessionID) return nil, fmt.Errorf("failed to fetch session: %w", err) } + log.Debug("session retrieved successfully", + "session_id", session.ID, + "user_id", session.UserID) return session, nil } // DeleteSession removes a session from the database func (db *database) DeleteSession(sessionID string) error { + log := getLogger().WithGroup("sessions") + log.Debug("deleting session", "session_id", sessionID) + result, err := db.Exec("DELETE FROM sessions WHERE id = ?", sessionID) if err != nil { + log.Error("failed to delete session", + "error", err, + "session_id", sessionID) return fmt.Errorf("failed to delete session: %w", err) } rowsAffected, err := result.RowsAffected() if err != nil { + log.Error("failed to get rows affected after session deletion", + "error", err, + "session_id", sessionID) return fmt.Errorf("failed to get rows affected: %w", err) } if rowsAffected == 0 { + log.Debug("no session found to delete", "session_id", sessionID) return fmt.Errorf("session not found") } + log.Info("session deleted successfully", "session_id", sessionID) return nil } // CleanExpiredSessions removes all expired sessions from the database func (db *database) CleanExpiredSessions() error { - _, err := db.Exec("DELETE FROM sessions WHERE expires_at <= ?", time.Now()) + log := getLogger().WithGroup("sessions") + log.Info("cleaning expired sessions") + + result, err := db.Exec("DELETE FROM sessions WHERE expires_at <= ?", time.Now()) if err != nil { + log.Error("failed to clean expired sessions", "error", err) return fmt.Errorf("failed to clean expired sessions: %w", err) } + + rowsAffected, err := result.RowsAffected() + if err != nil { + log.Error("failed to get count of cleaned sessions", "error", err) + return fmt.Errorf("failed to get rows affected: %w", err) + } + + log.Info("expired sessions cleaned successfully", "sessions_removed", rowsAffected) return nil } diff --git a/server/internal/db/system.go b/server/internal/db/system.go index f954b34..f1eadbf 100644 --- a/server/internal/db/system.go +++ b/server/internal/db/system.go @@ -2,6 +2,7 @@ package db import ( "crypto/rand" + "database/sql" "encoding/base64" "fmt" ) @@ -21,82 +22,137 @@ type UserStats struct { // EnsureJWTSecret makes sure a JWT signing secret exists in the database // If no secret exists, it generates and stores a new one func (db *database) EnsureJWTSecret() (string, error) { + log := getLogger().WithGroup("system") + log.Debug("ensuring JWT secret exists") + // First, try to get existing secret secret, err := db.GetSystemSetting(JWTSecretKey) if err == nil { + log.Debug("existing JWT secret found") return secret, nil } + log.Info("no existing JWT secret found, generating new secret") + // Generate new secret if none exists newSecret, err := generateRandomSecret(32) // 256 bits if err != nil { + log.Error("failed to generate JWT secret", "error", err) return "", fmt.Errorf("failed to generate JWT secret: %w", err) } // Store the new secret err = db.SetSystemSetting(JWTSecretKey, newSecret) if err != nil { + log.Error("failed to store JWT secret", "error", err) return "", fmt.Errorf("failed to store JWT secret: %w", err) } + log.Info("new JWT secret generated and stored successfully") return newSecret, nil } // GetSystemSetting retrieves a system setting by key func (db *database) GetSystemSetting(key string) (string, error) { + log := getLogger().WithGroup("system") + log.Debug("retrieving system setting", "key", key) + var value string err := db.QueryRow("SELECT value FROM system_settings WHERE key = ?", key).Scan(&value) if err != nil { + if err == sql.ErrNoRows { + log.Debug("system setting not found", "key", key) + } else { + log.Error("failed to retrieve system setting", + "error", err, + "key", key) + } return "", err } + + log.Debug("system setting retrieved successfully", "key", key) return value, nil } // SetSystemSetting stores or updates a system setting func (db *database) SetSystemSetting(key, value string) error { + log := getLogger().WithGroup("system") + log.Debug("storing system setting", "key", key) + _, err := db.Exec(` - INSERT INTO system_settings (key, value) - VALUES (?, ?) - ON CONFLICT(key) DO UPDATE SET value = ?`, + INSERT INTO system_settings (key, value) + VALUES (?, ?) + ON CONFLICT(key) DO UPDATE SET value = ?`, key, value, value) - return err + + if err != nil { + log.Error("failed to store system setting", + "error", err, + "key", key) + return fmt.Errorf("failed to store system setting: %w", err) + } + + log.Info("system setting stored successfully", "key", key) + return nil } // generateRandomSecret generates a cryptographically secure random string func generateRandomSecret(bytes int) (string, error) { + log := getLogger().WithGroup("system") + log.Debug("generating random secret", "bytes", bytes) + b := make([]byte, bytes) _, err := rand.Read(b) if err != nil { - return "", err + log.Error("failed to generate random bytes", + "error", err, + "bytes", bytes) + return "", fmt.Errorf("failed to generate random bytes: %w", err) } - return base64.StdEncoding.EncodeToString(b), nil + + secret := base64.StdEncoding.EncodeToString(b) + log.Debug("random secret generated successfully", "bytes", bytes) + return secret, nil } // GetSystemStats returns system-wide statistics func (db *database) GetSystemStats() (*UserStats, error) { + log := getLogger().WithGroup("system") + log.Debug("collecting system statistics") + stats := &UserStats{} // Get total users err := db.QueryRow("SELECT COUNT(*) FROM users").Scan(&stats.TotalUsers) if err != nil { - return nil, err + log.Error("failed to get total users count", "error", err) + return nil, fmt.Errorf("failed to get total users count: %w", err) } + log.Debug("got total users count", "count", stats.TotalUsers) // Get total workspaces err = db.QueryRow("SELECT COUNT(*) FROM workspaces").Scan(&stats.TotalWorkspaces) if err != nil { - return nil, err + log.Error("failed to get total workspaces count", "error", err) + return nil, fmt.Errorf("failed to get total workspaces count: %w", err) } + log.Debug("got total workspaces count", "count", stats.TotalWorkspaces) // Get active users (users with activity in last 30 days) err = db.QueryRow(` - SELECT COUNT(DISTINCT user_id) - FROM sessions - WHERE created_at > datetime('now', '-30 days')`). + SELECT COUNT(DISTINCT user_id) + FROM sessions + WHERE created_at > datetime('now', '-30 days')`). Scan(&stats.ActiveUsers) if err != nil { - return nil, err + log.Error("failed to get active users count", "error", err) + return nil, fmt.Errorf("failed to get active users count: %w", err) } + log.Debug("got active users count", "count", stats.ActiveUsers) + log.Info("system statistics collected successfully", + "total_users", stats.TotalUsers, + "total_workspaces", stats.TotalWorkspaces, + "active_users", stats.ActiveUsers) return stats, nil } diff --git a/server/internal/db/users.go b/server/internal/db/users.go index ecb2da1..15b19d1 100644 --- a/server/internal/db/users.go +++ b/server/internal/db/users.go @@ -2,75 +2,112 @@ package db import ( "database/sql" + "fmt" "novamd/internal/models" ) // CreateUser inserts a new user record into the database func (db *database) CreateUser(user *models.User) (*models.User, error) { + log := getLogger().WithGroup("users") + log.Info("creating new user", + "email", user.Email, + "role", user.Role) + tx, err := db.Begin() if err != nil { - return nil, err + log.Error("failed to begin transaction", "error", err) + return nil, fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() result, err := tx.Exec(` - INSERT INTO users (email, display_name, password_hash, role) - VALUES (?, ?, ?, ?)`, + INSERT INTO users (email, display_name, password_hash, role) + VALUES (?, ?, ?, ?)`, user.Email, user.DisplayName, user.PasswordHash, user.Role) if err != nil { - return nil, err + log.Error("failed to insert user", + "error", err, + "email", user.Email) + return nil, fmt.Errorf("failed to insert user: %w", err) } userID, err := result.LastInsertId() if err != nil { - return nil, err + log.Error("failed to get last insert ID", "error", err) + return nil, fmt.Errorf("failed to get last insert ID: %w", err) } user.ID = int(userID) // Retrieve the created_at timestamp err = tx.QueryRow("SELECT created_at FROM users WHERE id = ?", user.ID).Scan(&user.CreatedAt) if err != nil { - return nil, err + log.Error("failed to get created timestamp", + "error", err, + "user_id", user.ID) + return nil, fmt.Errorf("failed to get created timestamp: %w", err) } // Create default workspace with default settings + log.Debug("creating default workspace for user", "user_id", user.ID) defaultWorkspace := &models.Workspace{ UserID: user.ID, Name: "Main", } - defaultWorkspace.SetDefaultSettings() // Initialize default settings + defaultWorkspace.SetDefaultSettings() // Create workspace with settings err = db.createWorkspaceTx(tx, defaultWorkspace) if err != nil { - return nil, err + log.Error("failed to create default workspace", + "error", err, + "user_id", user.ID) + return nil, fmt.Errorf("failed to create default workspace: %w", err) } // Update user's last workspace ID + log.Debug("updating user's last workspace", + "user_id", user.ID, + "workspace_id", defaultWorkspace.ID) _, err = tx.Exec("UPDATE users SET last_workspace_id = ? WHERE id = ?", defaultWorkspace.ID, user.ID) if err != nil { - return nil, err + log.Error("failed to update last workspace ID", + "error", err, + "user_id", user.ID, + "workspace_id", defaultWorkspace.ID) + return nil, fmt.Errorf("failed to update last workspace ID: %w", err) } err = tx.Commit() if err != nil { - return nil, err + log.Error("failed to commit transaction", + "error", err, + "user_id", user.ID) + return nil, fmt.Errorf("failed to commit transaction: %w", err) } user.LastWorkspaceID = defaultWorkspace.ID + log.Info("user created successfully", + "user_id", user.ID, + "email", user.Email, + "workspace_id", defaultWorkspace.ID) return user, nil } // Helper function to create a workspace in a transaction func (db *database) createWorkspaceTx(tx *sql.Tx, workspace *models.Workspace) error { + log := getLogger().WithGroup("users") + log.Debug("creating workspace in transaction", + "user_id", workspace.UserID, + "name", workspace.Name) + result, err := tx.Exec(` - INSERT INTO workspaces ( - user_id, name, - theme, auto_save, show_hidden_files, - git_enabled, git_url, git_user, git_token, - git_auto_commit, git_commit_msg_template, - git_commit_name, git_commit_email - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + INSERT INTO workspaces ( + user_id, name, + theme, auto_save, show_hidden_files, + git_enabled, git_url, git_user, git_token, + git_auto_commit, git_commit_msg_template, + git_commit_name, git_commit_email + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, workspace.UserID, workspace.Name, workspace.Theme, workspace.AutoSave, workspace.ShowHiddenFiles, workspace.GitEnabled, workspace.GitURL, workspace.GitUser, workspace.GitToken, @@ -78,18 +115,29 @@ func (db *database) createWorkspaceTx(tx *sql.Tx, workspace *models.Workspace) e workspace.GitCommitName, workspace.GitCommitEmail, ) if err != nil { - return err + log.Error("failed to insert workspace", + "error", err, + "user_id", workspace.UserID) + return fmt.Errorf("failed to insert workspace: %w", err) } + id, err := result.LastInsertId() if err != nil { - return err + log.Error("failed to get workspace ID", "error", err) + return fmt.Errorf("failed to get workspace ID: %w", err) } workspace.ID = int(id) + + log.Debug("workspace created successfully", + "workspace_id", workspace.ID, + "user_id", workspace.UserID) return nil } -// GetUserByID retrieves a user by ID func (db *database) GetUserByID(id int) (*models.User, error) { + log := getLogger().WithGroup("users") + log.Debug("fetching user by ID", "user_id", id) + user := &models.User{} err := db.QueryRow(` SELECT @@ -97,16 +145,28 @@ func (db *database) GetUserByID(id int) (*models.User, error) { last_workspace_id FROM users WHERE id = ?`, id). - Scan(&user.ID, &user.Email, &user.DisplayName, &user.PasswordHash, &user.Role, &user.CreatedAt, - &user.LastWorkspaceID) - if err != nil { - return nil, err + Scan(&user.ID, &user.Email, &user.DisplayName, &user.PasswordHash, + &user.Role, &user.CreatedAt, &user.LastWorkspaceID) + + if err == sql.ErrNoRows { + log.Debug("user not found", "user_id", id) + return nil, fmt.Errorf("user not found") } + if err != nil { + log.Error("failed to fetch user", + "error", err, + "user_id", id) + return nil, fmt.Errorf("failed to fetch user: %w", err) + } + + log.Debug("user retrieved successfully", "user_id", id) return user, nil } -// GetUserByEmail retrieves a user by email func (db *database) GetUserByEmail(email string) (*models.User, error) { + log := getLogger().WithGroup("users") + log.Debug("fetching user by email", "email", email) + user := &models.User{} err := db.QueryRow(` SELECT @@ -114,35 +174,74 @@ func (db *database) GetUserByEmail(email string) (*models.User, error) { last_workspace_id FROM users WHERE email = ?`, email). - Scan(&user.ID, &user.Email, &user.DisplayName, &user.PasswordHash, &user.Role, &user.CreatedAt, - &user.LastWorkspaceID) + Scan(&user.ID, &user.Email, &user.DisplayName, &user.PasswordHash, + &user.Role, &user.CreatedAt, &user.LastWorkspaceID) + + if err == sql.ErrNoRows { + log.Debug("user not found", "email", email) + return nil, fmt.Errorf("user not found") + } if err != nil { - return nil, err + log.Error("failed to fetch user", + "error", err, + "email", email) + return nil, fmt.Errorf("failed to fetch user: %w", err) } + log.Debug("user retrieved successfully", "user_id", user.ID) return user, nil } -// UpdateUser updates a user's information func (db *database) UpdateUser(user *models.User) error { - _, err := db.Exec(` - UPDATE users - SET email = ?, display_name = ?, password_hash = ?, role = ?, last_workspace_id = ? - WHERE id = ?`, - user.Email, user.DisplayName, user.PasswordHash, user.Role, user.LastWorkspaceID, user.ID) - return err + log := getLogger().WithGroup("users") + log.Info("updating user", + "user_id", user.ID, + "email", user.Email) + + result, err := db.Exec(` + UPDATE users + SET email = ?, display_name = ?, password_hash = ?, role = ?, last_workspace_id = ? + WHERE id = ?`, + user.Email, user.DisplayName, user.PasswordHash, user.Role, + user.LastWorkspaceID, user.ID) + + if err != nil { + log.Error("failed to update user", + "error", err, + "user_id", user.ID) + return fmt.Errorf("failed to update user: %w", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + log.Error("failed to get rows affected", + "error", err, + "user_id", user.ID) + return fmt.Errorf("failed to get rows affected: %w", err) + } + + if rowsAffected == 0 { + log.Warn("no user found to update", "user_id", user.ID) + return fmt.Errorf("user not found") + } + + log.Info("user updated successfully", "user_id", user.ID) + return nil } -// GetAllUsers returns a list of all users in the system func (db *database) GetAllUsers() ([]*models.User, error) { + log := getLogger().WithGroup("users") + log.Debug("fetching all users") + rows, err := db.Query(` - SELECT - id, email, display_name, role, created_at, - last_workspace_id - FROM users - ORDER BY id ASC`) + SELECT + id, email, display_name, role, created_at, + last_workspace_id + FROM users + ORDER BY id ASC`) if err != nil { - return nil, err + log.Error("failed to query users", "error", err) + return nil, fmt.Errorf("failed to query users: %w", err) } defer rows.Close() @@ -154,61 +253,126 @@ func (db *database) GetAllUsers() ([]*models.User, error) { &user.CreatedAt, &user.LastWorkspaceID, ) if err != nil { - return nil, err + log.Error("failed to scan user row", "error", err) + return nil, fmt.Errorf("failed to scan user row: %w", err) } users = append(users, user) } + + log.Debug("users retrieved successfully", "count", len(users)) return users, nil } -// UpdateLastWorkspace updates the last workspace the user accessed func (db *database) UpdateLastWorkspace(userID int, workspaceName string) error { + log := getLogger().WithGroup("users") + log.Debug("updating last workspace", + "user_id", userID, + "workspace_name", workspaceName) + tx, err := db.Begin() if err != nil { - return err + log.Error("failed to begin transaction", "error", err) + return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() var workspaceID int - - err = tx.QueryRow("SELECT id FROM workspaces WHERE user_id = ? AND name = ?", userID, workspaceName).Scan(&workspaceID) + err = tx.QueryRow("SELECT id FROM workspaces WHERE user_id = ? AND name = ?", + userID, workspaceName).Scan(&workspaceID) if err != nil { - return err + log.Error("failed to find workspace", + "error", err, + "user_id", userID, + "workspace_name", workspaceName) + return fmt.Errorf("failed to find workspace: %w", err) } - _, err = tx.Exec("UPDATE users SET last_workspace_id = ? WHERE id = ?", workspaceID, userID) + _, err = tx.Exec("UPDATE users SET last_workspace_id = ? WHERE id = ?", + workspaceID, userID) if err != nil { - return err + log.Error("failed to update last workspace", + "error", err, + "user_id", userID, + "workspace_id", workspaceID) + return fmt.Errorf("failed to update last workspace: %w", err) } - return tx.Commit() + err = tx.Commit() + if err != nil { + log.Error("failed to commit transaction", "error", err) + return fmt.Errorf("failed to commit transaction: %w", err) + } + + log.Info("last workspace updated successfully", + "user_id", userID, + "workspace_id", workspaceID) + return nil } -// DeleteUser deletes a user and all their workspaces func (db *database) DeleteUser(id int) error { + log := getLogger().WithGroup("users") + log.Info("deleting user", "user_id", id) + tx, err := db.Begin() if err != nil { - return err + log.Error("failed to begin transaction", "error", err) + return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() // Delete all user's workspaces first - _, err = tx.Exec("DELETE FROM workspaces WHERE user_id = ?", id) + log.Debug("deleting user workspaces", "user_id", id) + result, err := tx.Exec("DELETE FROM workspaces WHERE user_id = ?", id) if err != nil { - return err + log.Error("failed to delete workspaces", + "error", err, + "user_id", id) + return fmt.Errorf("failed to delete workspaces: %w", err) + } + + workspacesDeleted, err := result.RowsAffected() + if err != nil { + log.Error("failed to get deleted workspaces count", "error", err) + return fmt.Errorf("failed to get deleted workspaces count: %w", err) } // Delete the user - _, err = tx.Exec("DELETE FROM users WHERE id = ?", id) + log.Debug("deleting user record", "user_id", id) + result, err = tx.Exec("DELETE FROM users WHERE id = ?", id) if err != nil { - return err + log.Error("failed to delete user", + "error", err, + "user_id", id) + return fmt.Errorf("failed to delete user: %w", err) } - return tx.Commit() + userDeleted, err := result.RowsAffected() + if err != nil { + log.Error("failed to get deleted user count", "error", err) + return fmt.Errorf("failed to get deleted user count: %w", err) + } + + if userDeleted == 0 { + log.Warn("no user found to delete", "user_id", id) + return fmt.Errorf("user not found") + } + + err = tx.Commit() + if err != nil { + log.Error("failed to commit transaction", "error", err) + return fmt.Errorf("failed to commit transaction: %w", err) + } + + log.Info("user deleted successfully", + "user_id", id, + "workspaces_deleted", workspacesDeleted) + return nil } -// GetLastWorkspaceName returns the name of the last workspace the user accessed func (db *database) GetLastWorkspaceName(userID int) (string, error) { + log := getLogger().WithGroup("users") + log.Debug("fetching last workspace name", "user_id", userID) + var workspaceName string err := db.QueryRow(` SELECT @@ -217,12 +381,36 @@ func (db *database) GetLastWorkspaceName(userID int) (string, error) { JOIN users u ON u.last_workspace_id = w.id WHERE u.id = ?`, userID). Scan(&workspaceName) - return workspaceName, err + + if err == sql.ErrNoRows { + log.Debug("no last workspace found", "user_id", userID) + return "", fmt.Errorf("no last workspace found") + } + if err != nil { + log.Error("failed to fetch last workspace name", + "error", err, + "user_id", userID) + return "", fmt.Errorf("failed to fetch last workspace name: %w", err) + } + + log.Debug("last workspace name retrieved", + "user_id", userID, + "workspace_name", workspaceName) + return workspaceName, nil } // CountAdminUsers returns the number of admin users in the system func (db *database) CountAdminUsers() (int, error) { + log := getLogger().WithGroup("users") + log.Debug("counting admin users") + var count int err := db.QueryRow("SELECT COUNT(*) FROM users WHERE role = 'admin'").Scan(&count) - return count, err + if err != nil { + log.Error("failed to count admin users", "error", err) + return 0, fmt.Errorf("failed to count admin users: %w", err) + } + + log.Debug("admin users counted successfully", "count", count) + return count, nil } diff --git a/server/internal/db/workspaces.go b/server/internal/db/workspaces.go index 667cf98..54b051d 100644 --- a/server/internal/db/workspaces.go +++ b/server/internal/db/workspaces.go @@ -8,88 +8,125 @@ import ( // CreateWorkspace inserts a new workspace record into the database func (db *database) CreateWorkspace(workspace *models.Workspace) error { + log := getLogger().WithGroup("workspaces") + log.Info("creating new workspace", + "user_id", workspace.UserID, + "name", workspace.Name, + "git_enabled", workspace.GitEnabled) + // Set default settings if not provided if workspace.Theme == "" { + log.Debug("setting default workspace settings") workspace.SetDefaultSettings() } // Encrypt token if present encryptedToken, err := db.encryptToken(workspace.GitToken) if err != nil { + log.Error("failed to encrypt git token", "error", err) return fmt.Errorf("failed to encrypt token: %w", err) } result, err := db.Exec(` - INSERT INTO workspaces ( - user_id, name, theme, auto_save, show_hidden_files, - git_enabled, git_url, git_user, git_token, - git_auto_commit, git_commit_msg_template, - git_commit_name, git_commit_email - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + INSERT INTO workspaces ( + user_id, name, theme, auto_save, show_hidden_files, + git_enabled, git_url, git_user, git_token, + git_auto_commit, git_commit_msg_template, + git_commit_name, git_commit_email + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, workspace.UserID, workspace.Name, workspace.Theme, workspace.AutoSave, workspace.ShowHiddenFiles, workspace.GitEnabled, workspace.GitURL, workspace.GitUser, encryptedToken, workspace.GitAutoCommit, workspace.GitCommitMsgTemplate, workspace.GitCommitName, workspace.GitCommitEmail, ) if err != nil { - return err + log.Error("failed to insert workspace", "error", err) + return fmt.Errorf("failed to insert workspace: %w", err) } id, err := result.LastInsertId() if err != nil { - return err + log.Error("failed to get workspace ID", "error", err) + return fmt.Errorf("failed to get workspace ID: %w", err) } workspace.ID = int(id) + + log.Info("workspace created successfully", + "workspace_id", workspace.ID, + "user_id", workspace.UserID) return nil } // GetWorkspaceByID retrieves a workspace by its ID func (db *database) GetWorkspaceByID(id int) (*models.Workspace, error) { + log := getLogger().WithGroup("workspaces") + log.Debug("fetching workspace by ID", "workspace_id", id) + workspace := &models.Workspace{} var encryptedToken string err := db.QueryRow(` - SELECT - id, user_id, name, created_at, - theme, auto_save, show_hidden_files, - git_enabled, git_url, git_user, git_token, - git_auto_commit, git_commit_msg_template, - git_commit_name, git_commit_email - FROM workspaces - WHERE id = ?`, + SELECT + id, user_id, name, created_at, + theme, auto_save, show_hidden_files, + git_enabled, git_url, git_user, git_token, + git_auto_commit, git_commit_msg_template, + git_commit_name, git_commit_email + FROM workspaces + WHERE id = ?`, id, ).Scan( &workspace.ID, &workspace.UserID, &workspace.Name, &workspace.CreatedAt, &workspace.Theme, &workspace.AutoSave, &workspace.ShowHiddenFiles, &workspace.GitEnabled, &workspace.GitURL, &workspace.GitUser, &encryptedToken, - &workspace.GitAutoCommit, &workspace.GitCommitMsgTemplate, &workspace.GitCommitName, &workspace.GitCommitEmail, + &workspace.GitAutoCommit, &workspace.GitCommitMsgTemplate, + &workspace.GitCommitName, &workspace.GitCommitEmail, ) + + if err == sql.ErrNoRows { + log.Debug("workspace not found", "workspace_id", id) + return nil, fmt.Errorf("workspace not found") + } if err != nil { - return nil, err + log.Error("failed to fetch workspace", + "error", err, + "workspace_id", id) + return nil, fmt.Errorf("failed to fetch workspace: %w", err) } // Decrypt token workspace.GitToken, err = db.decryptToken(encryptedToken) if err != nil { + log.Error("failed to decrypt git token", + "error", err, + "workspace_id", id) return nil, fmt.Errorf("failed to decrypt token: %w", err) } + log.Debug("workspace retrieved successfully", + "workspace_id", id, + "user_id", workspace.UserID) return workspace, nil } // GetWorkspaceByName retrieves a workspace by its name and user ID func (db *database) GetWorkspaceByName(userID int, workspaceName string) (*models.Workspace, error) { + log := getLogger().WithGroup("workspaces") + log.Debug("fetching workspace by name", + "user_id", userID, + "workspace_name", workspaceName) + workspace := &models.Workspace{} var encryptedToken string err := db.QueryRow(` - SELECT - id, user_id, name, created_at, - theme, auto_save, show_hidden_files, - git_enabled, git_url, git_user, git_token, - git_auto_commit, git_commit_msg_template, - git_commit_name, git_commit_email - FROM workspaces - WHERE user_id = ? AND name = ?`, + SELECT + id, user_id, name, created_at, + theme, auto_save, show_hidden_files, + git_enabled, git_url, git_user, git_token, + git_auto_commit, git_commit_msg_template, + git_commit_name, git_commit_email + FROM workspaces + WHERE user_id = ? AND name = ?`, userID, workspaceName, ).Scan( &workspace.ID, &workspace.UserID, &workspace.Name, &workspace.CreatedAt, @@ -98,43 +135,67 @@ func (db *database) GetWorkspaceByName(userID int, workspaceName string) (*model &workspace.GitAutoCommit, &workspace.GitCommitMsgTemplate, &workspace.GitCommitName, &workspace.GitCommitEmail, ) + + if err == sql.ErrNoRows { + log.Debug("workspace not found", + "user_id", userID, + "workspace_name", workspaceName) + return nil, fmt.Errorf("workspace not found") + } if err != nil { - return nil, err + log.Error("failed to fetch workspace", + "error", err, + "user_id", userID, + "workspace_name", workspaceName) + return nil, fmt.Errorf("failed to fetch workspace: %w", err) } // Decrypt token workspace.GitToken, err = db.decryptToken(encryptedToken) if err != nil { + log.Error("failed to decrypt git token", + "error", err, + "workspace_id", workspace.ID) return nil, fmt.Errorf("failed to decrypt token: %w", err) } + log.Debug("workspace retrieved successfully", + "workspace_id", workspace.ID, + "user_id", userID) return workspace, nil } // UpdateWorkspace updates a workspace record in the database func (db *database) UpdateWorkspace(workspace *models.Workspace) error { + log := getLogger().WithGroup("workspaces") + log.Info("updating workspace", + "workspace_id", workspace.ID, + "user_id", workspace.UserID, + "git_enabled", workspace.GitEnabled) + // Encrypt token before storing encryptedToken, err := db.encryptToken(workspace.GitToken) if err != nil { + log.Error("failed to encrypt git token", "error", err) return fmt.Errorf("failed to encrypt token: %w", err) } - _, err = db.Exec(` - UPDATE workspaces - SET - name = ?, - theme = ?, - auto_save = ?, - show_hidden_files = ?, - git_enabled = ?, - git_url = ?, - git_user = ?, - git_token = ?, - git_auto_commit = ?, - git_commit_msg_template = ?, - git_commit_name = ?, - git_commit_email = ? - WHERE id = ? AND user_id = ?`, + result, err := db.Exec(` + UPDATE workspaces + SET + name = ?, + theme = ?, + auto_save = ?, + show_hidden_files = ?, + git_enabled = ?, + git_url = ?, + git_user = ?, + git_token = ?, + git_auto_commit = ?, + git_commit_msg_template = ?, + git_commit_name = ?, + git_commit_email = ? + WHERE id = ? AND user_id = ?`, workspace.Name, workspace.Theme, workspace.AutoSave, @@ -150,24 +211,55 @@ func (db *database) UpdateWorkspace(workspace *models.Workspace) error { workspace.ID, workspace.UserID, ) - return err + if err != nil { + log.Error("failed to update workspace", + "error", err, + "workspace_id", workspace.ID) + return fmt.Errorf("failed to update workspace: %w", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + log.Error("failed to get rows affected", + "error", err, + "workspace_id", workspace.ID) + return fmt.Errorf("failed to get rows affected: %w", err) + } + + if rowsAffected == 0 { + log.Warn("no workspace found to update", + "workspace_id", workspace.ID, + "user_id", workspace.UserID) + return fmt.Errorf("workspace not found") + } + + log.Info("workspace updated successfully", + "workspace_id", workspace.ID, + "user_id", workspace.UserID) + return nil } // GetWorkspacesByUserID retrieves all workspaces for a user func (db *database) GetWorkspacesByUserID(userID int) ([]*models.Workspace, error) { + log := getLogger().WithGroup("workspaces") + log.Debug("fetching workspaces for user", "user_id", userID) + rows, err := db.Query(` - SELECT - id, user_id, name, created_at, - theme, auto_save, show_hidden_files, - git_enabled, git_url, git_user, git_token, - git_auto_commit, git_commit_msg_template, - git_commit_name, git_commit_email - FROM workspaces - WHERE user_id = ?`, + SELECT + id, user_id, name, created_at, + theme, auto_save, show_hidden_files, + git_enabled, git_url, git_user, git_token, + git_auto_commit, git_commit_msg_template, + git_commit_name, git_commit_email + FROM workspaces + WHERE user_id = ?`, userID, ) if err != nil { - return nil, err + log.Error("failed to query workspaces", + "error", err, + "user_id", userID) + return nil, fmt.Errorf("failed to query workspaces: %w", err) } defer rows.Close() @@ -183,38 +275,57 @@ func (db *database) GetWorkspacesByUserID(userID int) ([]*models.Workspace, erro &workspace.GitCommitName, &workspace.GitCommitEmail, ) if err != nil { - return nil, err + log.Error("failed to scan workspace row", "error", err) + return nil, fmt.Errorf("failed to scan workspace row: %w", err) } // Decrypt token workspace.GitToken, err = db.decryptToken(encryptedToken) if err != nil { + log.Error("failed to decrypt git token", + "error", err, + "workspace_id", workspace.ID) return nil, fmt.Errorf("failed to decrypt token: %w", err) } workspaces = append(workspaces, workspace) } + + if err = rows.Err(); err != nil { + log.Error("error iterating workspace rows", + "error", err, + "user_id", userID) + return nil, fmt.Errorf("error iterating workspace rows: %w", err) + } + + log.Debug("workspaces retrieved successfully", + "user_id", userID, + "count", len(workspaces)) return workspaces, nil } // UpdateWorkspaceSettings updates only the settings portion of a workspace -// This is useful when you don't want to modify the name or other core workspace properties func (db *database) UpdateWorkspaceSettings(workspace *models.Workspace) error { - _, err := db.Exec(` - UPDATE workspaces - SET - theme = ?, - auto_save = ?, - show_hidden_files = ?, - git_enabled = ?, - git_url = ?, - git_user = ?, - git_token = ?, - git_auto_commit = ?, - git_commit_msg_template = ?, - git_commit_name = ?, - git_commit_email = ? - WHERE id = ?`, + log := getLogger().WithGroup("workspaces") + log.Info("updating workspace settings", + "workspace_id", workspace.ID, + "git_enabled", workspace.GitEnabled) + + result, err := db.Exec(` + UPDATE workspaces + SET + theme = ?, + auto_save = ?, + show_hidden_files = ?, + git_enabled = ?, + git_url = ?, + git_user = ?, + git_token = ?, + git_auto_commit = ?, + git_commit_msg_template = ?, + git_commit_name = ?, + git_commit_email = ? + WHERE id = ?`, workspace.Theme, workspace.AutoSave, workspace.ShowHiddenFiles, @@ -228,59 +339,214 @@ func (db *database) UpdateWorkspaceSettings(workspace *models.Workspace) error { workspace.GitCommitEmail, workspace.ID, ) - return err + if err != nil { + log.Error("failed to update workspace settings", + "error", err, + "workspace_id", workspace.ID) + return fmt.Errorf("failed to update workspace settings: %w", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + log.Error("failed to get rows affected", + "error", err, + "workspace_id", workspace.ID) + return fmt.Errorf("failed to get rows affected: %w", err) + } + + if rowsAffected == 0 { + log.Warn("no workspace found to update settings", + "workspace_id", workspace.ID) + return fmt.Errorf("workspace not found") + } + + log.Info("workspace settings updated successfully", + "workspace_id", workspace.ID) + return nil } // DeleteWorkspace removes a workspace record from the database func (db *database) DeleteWorkspace(id int) error { - _, err := db.Exec("DELETE FROM workspaces WHERE id = ?", id) - return err + log := getLogger().WithGroup("workspaces") + log.Info("deleting workspace", "workspace_id", id) + + result, err := db.Exec("DELETE FROM workspaces WHERE id = ?", id) + if err != nil { + log.Error("failed to delete workspace", + "error", err, + "workspace_id", id) + return fmt.Errorf("failed to delete workspace: %w", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + log.Error("failed to get rows affected", + "error", err, + "workspace_id", id) + return fmt.Errorf("failed to get rows affected: %w", err) + } + + if rowsAffected == 0 { + log.Warn("no workspace found to delete", "workspace_id", id) + return fmt.Errorf("workspace not found") + } + + log.Info("workspace deleted successfully", "workspace_id", id) + return nil } // DeleteWorkspaceTx removes a workspace record from the database within a transaction func (db *database) DeleteWorkspaceTx(tx *sql.Tx, id int) error { - _, err := tx.Exec("DELETE FROM workspaces WHERE id = ?", id) - return err + log := getLogger().WithGroup("workspaces") + log.Debug("deleting workspace in transaction", "workspace_id", id) + + result, err := tx.Exec("DELETE FROM workspaces WHERE id = ?", id) + if err != nil { + log.Error("failed to delete workspace in transaction", + "error", err, + "workspace_id", id) + return fmt.Errorf("failed to delete workspace in transaction: %w", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + log.Error("failed to get rows affected in transaction", + "error", err, + "workspace_id", id) + return fmt.Errorf("failed to get rows affected in transaction: %w", err) + } + + if rowsAffected == 0 { + log.Warn("no workspace found to delete in transaction", + "workspace_id", id) + return fmt.Errorf("workspace not found") + } + + log.Debug("workspace deleted successfully in transaction", + "workspace_id", id) + return nil } -// UpdateLastWorkspaceTx sets the last workspace for a user in with a transaction +// UpdateLastWorkspaceTx sets the last workspace for a user in a transaction func (db *database) UpdateLastWorkspaceTx(tx *sql.Tx, userID, workspaceID int) error { - _, err := tx.Exec("UPDATE users SET last_workspace_id = ? WHERE id = ?", workspaceID, userID) - return err + log := getLogger().WithGroup("workspaces") + log.Debug("updating last workspace in transaction", + "user_id", userID, + "workspace_id", workspaceID) + + result, err := tx.Exec("UPDATE users SET last_workspace_id = ? WHERE id = ?", + workspaceID, userID) + if err != nil { + log.Error("failed to update last workspace in transaction", + "error", err, + "user_id", userID, + "workspace_id", workspaceID) + return fmt.Errorf("failed to update last workspace in transaction: %w", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + log.Error("failed to get rows affected in transaction", + "error", err, + "user_id", userID) + return fmt.Errorf("failed to get rows affected in transaction: %w", err) + } + + if rowsAffected == 0 { + log.Warn("no user found to update last workspace", + "user_id", userID) + return fmt.Errorf("user not found") + } + + log.Debug("last workspace updated successfully in transaction", + "user_id", userID, + "workspace_id", workspaceID) + return nil } // UpdateLastOpenedFile updates the last opened file path for a workspace func (db *database) UpdateLastOpenedFile(workspaceID int, filePath string) error { - _, err := db.Exec("UPDATE workspaces SET last_opened_file_path = ? WHERE id = ?", filePath, workspaceID) - return err + log := getLogger().WithGroup("workspaces") + log.Debug("updating last opened file", + "workspace_id", workspaceID, + "file_path", filePath) + + result, err := db.Exec("UPDATE workspaces SET last_opened_file_path = ? WHERE id = ?", + filePath, workspaceID) + if err != nil { + log.Error("failed to update last opened file", + "error", err, + "workspace_id", workspaceID) + return fmt.Errorf("failed to update last opened file: %w", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + log.Error("failed to get rows affected", + "error", err, + "workspace_id", workspaceID) + return fmt.Errorf("failed to get rows affected: %w", err) + } + + if rowsAffected == 0 { + log.Warn("no workspace found to update last opened file", + "workspace_id", workspaceID) + return fmt.Errorf("workspace not found") + } + + log.Debug("last opened file updated successfully", + "workspace_id", workspaceID) + return nil } // GetLastOpenedFile retrieves the last opened file path for a workspace func (db *database) GetLastOpenedFile(workspaceID int) (string, error) { + log := getLogger().WithGroup("workspaces") + log.Debug("fetching last opened file", "workspace_id", workspaceID) + var filePath sql.NullString - err := db.QueryRow("SELECT last_opened_file_path FROM workspaces WHERE id = ?", workspaceID).Scan(&filePath) - if err != nil { - return "", err + err := db.QueryRow("SELECT last_opened_file_path FROM workspaces WHERE id = ?", + workspaceID).Scan(&filePath) + + if err == sql.ErrNoRows { + log.Debug("workspace not found", "workspace_id", workspaceID) + return "", fmt.Errorf("workspace not found") } + if err != nil { + log.Error("failed to fetch last opened file", + "error", err, + "workspace_id", workspaceID) + return "", fmt.Errorf("failed to fetch last opened file: %w", err) + } + if !filePath.Valid { + log.Debug("no last opened file found", "workspace_id", workspaceID) return "", nil } + + log.Debug("last opened file retrieved successfully", + "workspace_id", workspaceID, + "file_path", filePath.String) return filePath.String, nil } // GetAllWorkspaces retrieves all workspaces in the database func (db *database) GetAllWorkspaces() ([]*models.Workspace, error) { + log := getLogger().WithGroup("workspaces") + log.Debug("fetching all workspaces") + rows, err := db.Query(` - SELECT - id, user_id, name, created_at, - theme, auto_save, show_hidden_files, - git_enabled, git_url, git_user, git_token, - git_auto_commit, git_commit_msg_template, - git_commit_name, git_commit_email - FROM workspaces`, + SELECT + id, user_id, name, created_at, + theme, auto_save, show_hidden_files, + git_enabled, git_url, git_user, git_token, + git_auto_commit, git_commit_msg_template, + git_commit_name, git_commit_email + FROM workspaces`, ) if err != nil { - return nil, err + log.Error("failed to query workspaces", "error", err) + return nil, fmt.Errorf("failed to query workspaces: %w", err) } defer rows.Close() @@ -296,16 +562,27 @@ func (db *database) GetAllWorkspaces() ([]*models.Workspace, error) { &workspace.GitCommitName, &workspace.GitCommitEmail, ) if err != nil { - return nil, err + log.Error("failed to scan workspace row", "error", err) + return nil, fmt.Errorf("failed to scan workspace row: %w", err) } // Decrypt token workspace.GitToken, err = db.decryptToken(encryptedToken) if err != nil { + log.Error("failed to decrypt git token", + "error", err, + "workspace_id", workspace.ID) return nil, fmt.Errorf("failed to decrypt token: %w", err) } workspaces = append(workspaces, workspace) } + + if err = rows.Err(); err != nil { + log.Error("error iterating workspace rows", "error", err) + return nil, fmt.Errorf("error iterating workspace rows: %w", err) + } + + log.Debug("all workspaces retrieved successfully", "count", len(workspaces)) return workspaces, nil }