From ea916c3ecc14733daa7f8ba8032bd70189a65fe7 Mon Sep 17 00:00:00 2001 From: LordMathis Date: Tue, 10 Dec 2024 22:16:50 +0100 Subject: [PATCH 01/22] Initial logging implementation --- .vscode/launch.json | 14 ++ server/internal/app/config.go | 30 ++++ server/internal/logging/logger.go | 260 ++++++++++++++++++++++++++++++ 3 files changed, 304 insertions(+) create mode 100644 .vscode/launch.json create mode 100644 server/internal/logging/logger.go diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..405bacd --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch NovaMD Server", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/server/cmd/server/main.go", + "cwd": "${workspaceFolder}", + "envFile": "${workspaceFolder}/server/.env" + } + ] +} diff --git a/server/internal/app/config.go b/server/internal/app/config.go index b9ff02a..f9b8e2c 100644 --- a/server/internal/app/config.go +++ b/server/internal/app/config.go @@ -2,6 +2,7 @@ package app import ( "fmt" + "novamd/internal/logging" "novamd/internal/secrets" "os" "strconv" @@ -25,6 +26,9 @@ type Config struct { RateLimitRequests int RateLimitWindow time.Duration IsDevelopment bool + LogDir string + LogLevel logging.LogLevel + ConsoleOutput bool } // DefaultConfig returns a new Config instance with default values @@ -37,6 +41,9 @@ func DefaultConfig() *Config { RateLimitRequests: 100, RateLimitWindow: time.Minute * 15, IsDevelopment: false, + LogDir: "./logs", + LogLevel: logging.INFO, + ConsoleOutput: true, } } @@ -108,6 +115,29 @@ func LoadConfig() (*Config, error) { } } + // Configure log directory + if logDir := os.Getenv("NOVAMD_LOG_DIR"); logDir != "" { + config.LogDir = logDir + } + + // Configure log level, if isDevelopment is set, default to debug + if logLevel := os.Getenv("NOVAMD_LOG_LEVEL"); logLevel != "" { + if parsed, err := logging.ParseLogLevel(logLevel); err == nil { + config.LogLevel = parsed + } + } else if config.IsDevelopment { + config.LogLevel = logging.DEBUG + } + + // Configure console output, if isDevelopment is set, default to true + if consoleOutput := os.Getenv("NOVAMD_CONSOLE_OUTPUT"); consoleOutput != "" { + if parsed, err := strconv.ParseBool(consoleOutput); err == nil { + config.ConsoleOutput = parsed + } + } else if config.IsDevelopment { + config.ConsoleOutput = true + } + // Validate all settings if err := config.validate(); err != nil { return nil, err diff --git a/server/internal/logging/logger.go b/server/internal/logging/logger.go new file mode 100644 index 0000000..b04dbbe --- /dev/null +++ b/server/internal/logging/logger.go @@ -0,0 +1,260 @@ +// Package logging provides a logging interface for the application. +package logging + +import ( + "fmt" + "io" + "os" + "path/filepath" + "time" +) + +// LogLevel defines the severity of a log message +type LogLevel int + +// Log levels +const ( + DEBUG LogLevel = iota + INFO + WARN + ERROR +) + +func (l LogLevel) String() string { + switch l { + case DEBUG: + return "DEBUG" + case INFO: + return "INFO" + case WARN: + return "WARN" + case ERROR: + return "ERROR" + default: + return "INFO" + } +} + +// ParseLogLevel converts a string to a LogLevel +func ParseLogLevel(level string) (LogLevel, error) { + switch level { + case "DEBUG": + return DEBUG, nil + case "INFO": + return INFO, nil + case "WARN": + return WARN, nil + case "ERROR": + return ERROR, nil + default: + return INFO, fmt.Errorf("invalid log level: %s", level) + } +} + +// Logger defines the interface for all logging operations +type Logger interface { + // Standard logging + Debug(msg string, fields map[string]interface{}) + Info(msg string, fields map[string]interface{}) + Warn(msg string, fields map[string]interface{}) + Error(err error, fields map[string]interface{}) + + // Audit logging (always at INFO level) + Audit(userID int, workspaceID int, action, resource string, details map[string]interface{}) + + // Security logging (with levels) + Security(level LogLevel, userID int, event string, details map[string]interface{}) + + // Close closes all outputs + Close() error +} + +type logger struct { + logDir string + minLevel LogLevel + consoleOut bool + appOutput io.Writer // Combined output for application logs + auditOutput io.Writer // Output for audit logs + secOutput io.Writer // Output for security logs +} + +type logEntry struct { + Timestamp time.Time + Level LogLevel + Message string + Fields map[string]interface{} +} + +// New creates a new Logger instance +func New(logDir string, minLevel LogLevel, consoleOut bool) (Logger, error) { + if err := os.MkdirAll(logDir, 0755); err != nil { + return nil, fmt.Errorf("failed to create log directory: %w", err) + } + + l := &logger{ + logDir: logDir, + minLevel: minLevel, + consoleOut: consoleOut, + } + + // Setup application log output + appFile, err := os.OpenFile( + filepath.Join(logDir, "app.log"), + os.O_APPEND|os.O_CREATE|os.O_WRONLY, + 0644, + ) + if err != nil { + return nil, fmt.Errorf("failed to open app log file: %w", err) + } + + // Setup audit log output + auditFile, err := os.OpenFile( + filepath.Join(logDir, "audit.log"), + os.O_APPEND|os.O_CREATE|os.O_WRONLY, + 0644, + ) + if err != nil { + return nil, fmt.Errorf("failed to open audit log file: %w", err) + } + + // Setup security log output + securityFile, err := os.OpenFile( + filepath.Join(logDir, "security.log"), + os.O_APPEND|os.O_CREATE|os.O_WRONLY, + 0644, + ) + if err != nil { + return nil, fmt.Errorf("failed to open security log file: %w", err) + } + + // Configure outputs + l.appOutput = appFile + if consoleOut { + l.appOutput = io.MultiWriter(appFile, os.Stdout) + } + + l.auditOutput = auditFile + l.secOutput = io.MultiWriter(securityFile, os.Stderr) // Security logs always go to stderr + + return l, nil +} + +// writeToOutput writes to the output and handles errors appropriately +func (l *logger) writeOutput(w io.Writer, format string, args ...interface{}) { + _, err := fmt.Fprintf(w, format, args...) + if err != nil { + // Log to stderr if writing fails + fmt.Fprintf(os.Stderr, "logging error: %v\n", err) + } +} + +func (l *logger) logApp(level LogLevel, msg string, fields map[string]interface{}) { + if level < l.minLevel { + return + } + + entry := logEntry{ + Timestamp: time.Now(), + Level: level, + Message: msg, + Fields: fields, + } + + l.writeOutput(l.appOutput, "[%s] [%s] %s%s\n", + entry.Timestamp.Format(time.RFC3339), + entry.Level, + entry.Message, + formatFields(entry.Fields), + ) +} + +// Debug logs a debug message +func (l *logger) Debug(msg string, fields map[string]interface{}) { + l.logApp(DEBUG, msg, fields) +} + +// Info logs an informational message +func (l *logger) Info(msg string, fields map[string]interface{}) { + l.logApp(INFO, msg, fields) +} + +// Warn logs a warning message +func (l *logger) Warn(msg string, fields map[string]interface{}) { + l.logApp(WARN, msg, fields) +} + +// Error logs an error message +func (l *logger) Error(err error, fields map[string]interface{}) { + if fields == nil { + fields = make(map[string]interface{}) + } + fields["error"] = err.Error() + l.logApp(ERROR, "error occurred", fields) +} + +// Audit logs an audit event +func (l *logger) Audit(userID int, workspaceID int, action, resource string, details map[string]interface{}) { + if details == nil { + details = make(map[string]interface{}) + } + details["user_id"] = userID + details["workspace_id"] = workspaceID + details["action"] = action + details["resource"] = resource + + l.writeOutput(l.auditOutput, "[%s] [AUDIT] %s on %s%s\n", + time.Now().Format(time.RFC3339), + action, + resource, + formatFields(details), + ) +} + +// Security logs a security event +func (l *logger) Security(level LogLevel, userID int, event string, details map[string]interface{}) { + if level < l.minLevel { + return + } + + l.writeOutput(l.secOutput, "[%s] [%s] [%d] [SECURITY] %s%s\n", + time.Now().Format(time.RFC3339), + level, + userID, + event, + formatFields(details), + ) +} + +func (l *logger) Close() error { + var errs []error + + if closer, ok := l.appOutput.(io.Closer); ok { + if err := closer.Close(); err != nil { + errs = append(errs, fmt.Errorf("failed to close app output: %w", err)) + } + } + + if closer, ok := l.auditOutput.(io.Closer); ok { + if err := closer.Close(); err != nil { + errs = append(errs, fmt.Errorf("failed to close audit output: %w", err)) + } + } + + if closer, ok := l.secOutput.(io.Closer); ok { + if err := closer.Close(); err != nil { + errs = append(errs, fmt.Errorf("failed to close security output: %w", err)) + } + } + + if len(errs) > 0 { + return fmt.Errorf("failed to close some outputs: %v", errs) + } + return nil +} + +func formatFields(fields map[string]interface{}) string { + if len(fields) == 0 { + return "" + } + return fmt.Sprintf(" - Fields: %v", fields) +} From 9d82b6426cbddb4f174dc07144906cead54f230e Mon Sep 17 00:00:00 2001 From: LordMathis Date: Thu, 12 Dec 2024 20:53:35 +0100 Subject: [PATCH 02/22] Use slog for logging --- server/internal/app/config.go | 4 +- server/internal/logging/logger.go | 355 ++++++++++++++---------------- 2 files changed, 163 insertions(+), 196 deletions(-) diff --git a/server/internal/app/config.go b/server/internal/app/config.go index f9b8e2c..61af395 100644 --- a/server/internal/app/config.go +++ b/server/internal/app/config.go @@ -43,7 +43,7 @@ func DefaultConfig() *Config { IsDevelopment: false, LogDir: "./logs", LogLevel: logging.INFO, - ConsoleOutput: true, + ConsoleOutput: false, } } @@ -134,8 +134,6 @@ func LoadConfig() (*Config, error) { if parsed, err := strconv.ParseBool(consoleOutput); err == nil { config.ConsoleOutput = parsed } - } else if config.IsDevelopment { - config.ConsoleOutput = true } // Validate all settings diff --git a/server/internal/logging/logger.go b/server/internal/logging/logger.go index b04dbbe..2135918 100644 --- a/server/internal/logging/logger.go +++ b/server/internal/logging/logger.go @@ -1,41 +1,100 @@ -// Package logging provides a logging interface for the application. +// Package logging provides a structured logging interface for the application. package logging import ( + "context" "fmt" "io" + "log/slog" "os" "path/filepath" "time" ) -// LogLevel defines the severity of a log message -type LogLevel int +// LogLevel represents the logging level +type LogLevel slog.Level // Log levels const ( - DEBUG LogLevel = iota - INFO - WARN - ERROR + DEBUG LogLevel = LogLevel(slog.LevelDebug) + INFO LogLevel = LogLevel(slog.LevelInfo) + WARN LogLevel = LogLevel(slog.LevelWarn) + ERROR LogLevel = LogLevel(slog.LevelError) ) -func (l LogLevel) String() string { - switch l { - case DEBUG: - return "DEBUG" - case INFO: - return "INFO" - case WARN: - return "WARN" - case ERROR: - return "ERROR" - default: - return "INFO" - } +// Logger defines the interface for logging operations +type Logger interface { + App() *slog.Logger // Returns logger for application logs + Audit() *slog.Logger // Returns logger for audit logs + Security() *slog.Logger // Returns logger for security logs + Close() error // Cleanup and close log files } -// ParseLogLevel converts a string to a LogLevel +// logger implements the Logger interface +type logger struct { + appLogger *slog.Logger + auditLogger *slog.Logger + securityLogger *slog.Logger + files []*os.File // Keep track of open file handles +} + +// Output represents a destination for logs +type Output struct { + Type OutputFormat + Writer io.Writer +} + +// OutputFormat represents the format of the log output +type OutputFormat int + +const ( + OutputTypeJSON OutputFormat = iota // OutputTypeJSON JSON format + OutputTypeText // OutputTypeText text format +) + +// createLogFile creates a log file with the given name in the log directory +func createLogFile(logDir, name string) (*os.File, error) { + if err := os.MkdirAll(logDir, 0755); err != nil { + return nil, fmt.Errorf("failed to create log directory: %w", err) + } + + filename := filepath.Join(logDir, fmt.Sprintf("%s.log", name)) + file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return nil, fmt.Errorf("failed to open log file: %w", err) + } + + return file, nil +} + +// createLogger creates a new slog.Handler for the given outputs and options +func createLogger(opts *slog.HandlerOptions, outputs []Output) slog.Handler { + if len(outputs) == 0 { + return slog.NewTextHandler(io.Discard, opts) + } + + if len(outputs) == 1 { + output := outputs[0] + if output.Type == OutputTypeJSON { + return slog.NewJSONHandler(output.Writer, opts) + } + return slog.NewTextHandler(output.Writer, opts) + } + + // Multiple outputs - create handlers for each + handlers := make([]slog.Handler, 0, len(outputs)) + for _, output := range outputs { + if output.Type == OutputTypeJSON { + handlers = append(handlers, slog.NewJSONHandler(output.Writer, opts)) + } else { + handlers = append(handlers, slog.NewTextHandler(output.Writer, opts)) + } + } + + return multiHandler(handlers) +} + +// ParseLogLevel parses a string into a LogLevel func ParseLogLevel(level string) (LogLevel, error) { switch level { case "DEBUG": @@ -51,210 +110,120 @@ func ParseLogLevel(level string) (LogLevel, error) { } } -// Logger defines the interface for all logging operations -type Logger interface { - // Standard logging - Debug(msg string, fields map[string]interface{}) - Info(msg string, fields map[string]interface{}) - Warn(msg string, fields map[string]interface{}) - Error(err error, fields map[string]interface{}) - - // Audit logging (always at INFO level) - Audit(userID int, workspaceID int, action, resource string, details map[string]interface{}) - - // Security logging (with levels) - Security(level LogLevel, userID int, event string, details map[string]interface{}) - - // Close closes all outputs - Close() error -} - -type logger struct { - logDir string - minLevel LogLevel - consoleOut bool - appOutput io.Writer // Combined output for application logs - auditOutput io.Writer // Output for audit logs - secOutput io.Writer // Output for security logs -} - -type logEntry struct { - Timestamp time.Time - Level LogLevel - Message string - Fields map[string]interface{} -} - // New creates a new Logger instance func New(logDir string, minLevel LogLevel, consoleOut bool) (Logger, error) { - if err := os.MkdirAll(logDir, 0755); err != nil { - return nil, fmt.Errorf("failed to create log directory: %w", err) - } - l := &logger{ - logDir: logDir, - minLevel: minLevel, - consoleOut: consoleOut, + files: make([]*os.File, 0, 3), } - // Setup application log output - appFile, err := os.OpenFile( - filepath.Join(logDir, "app.log"), - os.O_APPEND|os.O_CREATE|os.O_WRONLY, - 0644, - ) - if err != nil { - return nil, fmt.Errorf("failed to open app log file: %w", err) + // Define our log types and their filenames + logTypes := []struct { + name string + setLogger func(*slog.Logger) + }{ + {"app", func(lg *slog.Logger) { l.appLogger = lg }}, + {"audit", func(lg *slog.Logger) { l.auditLogger = lg }}, + {"security", func(lg *slog.Logger) { l.securityLogger = lg }}, } - // Setup audit log output - auditFile, err := os.OpenFile( - filepath.Join(logDir, "audit.log"), - os.O_APPEND|os.O_CREATE|os.O_WRONLY, - 0644, - ) - if err != nil { - return nil, fmt.Errorf("failed to open audit log file: %w", err) + // Setup handlers options + opts := &slog.HandlerOptions{ + Level: slog.Level(minLevel), + AddSource: slog.Level(minLevel) == slog.LevelDebug, + ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr { + if a.Key == slog.TimeKey { + return slog.Attr{ + Key: a.Key, + Value: slog.StringValue(time.Now().UTC().Format(time.RFC3339)), + } + } + return a + }, } - // Setup security log output - securityFile, err := os.OpenFile( - filepath.Join(logDir, "security.log"), - os.O_APPEND|os.O_CREATE|os.O_WRONLY, - 0644, - ) - if err != nil { - return nil, fmt.Errorf("failed to open security log file: %w", err) - } + // Create loggers for each type + for _, lt := range logTypes { + // Create file output + file, err := createLogFile(logDir, lt.name) + if err != nil { + if err := l.Close(); err != nil { + return nil, fmt.Errorf("failed to close logger: %w", err) + } + return nil, fmt.Errorf("failed to create %s log file: %w", lt.name, err) + } + l.files = append(l.files, file) - // Configure outputs - l.appOutput = appFile - if consoleOut { - l.appOutput = io.MultiWriter(appFile, os.Stdout) - } + // Prepare outputs + outputs := []Output{{Type: OutputTypeJSON, Writer: file}} + if consoleOut { + outputs = append(outputs, Output{Type: OutputTypeText, Writer: os.Stdout}) + } - l.auditOutput = auditFile - l.secOutput = io.MultiWriter(securityFile, os.Stderr) // Security logs always go to stderr + // Create and set logger + handler := createLogger(opts, outputs) + lt.setLogger(slog.New(handler)) + } return l, nil } -// writeToOutput writes to the output and handles errors appropriately -func (l *logger) writeOutput(w io.Writer, format string, args ...interface{}) { - _, err := fmt.Fprintf(w, format, args...) - if err != nil { - // Log to stderr if writing fails - fmt.Fprintf(os.Stderr, "logging error: %v\n", err) - } +func (l *logger) App() *slog.Logger { + return l.appLogger } -func (l *logger) logApp(level LogLevel, msg string, fields map[string]interface{}) { - if level < l.minLevel { - return - } - - entry := logEntry{ - Timestamp: time.Now(), - Level: level, - Message: msg, - Fields: fields, - } - - l.writeOutput(l.appOutput, "[%s] [%s] %s%s\n", - entry.Timestamp.Format(time.RFC3339), - entry.Level, - entry.Message, - formatFields(entry.Fields), - ) +func (l *logger) Audit() *slog.Logger { + return l.auditLogger } -// Debug logs a debug message -func (l *logger) Debug(msg string, fields map[string]interface{}) { - l.logApp(DEBUG, msg, fields) -} - -// Info logs an informational message -func (l *logger) Info(msg string, fields map[string]interface{}) { - l.logApp(INFO, msg, fields) -} - -// Warn logs a warning message -func (l *logger) Warn(msg string, fields map[string]interface{}) { - l.logApp(WARN, msg, fields) -} - -// Error logs an error message -func (l *logger) Error(err error, fields map[string]interface{}) { - if fields == nil { - fields = make(map[string]interface{}) - } - fields["error"] = err.Error() - l.logApp(ERROR, "error occurred", fields) -} - -// Audit logs an audit event -func (l *logger) Audit(userID int, workspaceID int, action, resource string, details map[string]interface{}) { - if details == nil { - details = make(map[string]interface{}) - } - details["user_id"] = userID - details["workspace_id"] = workspaceID - details["action"] = action - details["resource"] = resource - - l.writeOutput(l.auditOutput, "[%s] [AUDIT] %s on %s%s\n", - time.Now().Format(time.RFC3339), - action, - resource, - formatFields(details), - ) -} - -// Security logs a security event -func (l *logger) Security(level LogLevel, userID int, event string, details map[string]interface{}) { - if level < l.minLevel { - return - } - - l.writeOutput(l.secOutput, "[%s] [%s] [%d] [SECURITY] %s%s\n", - time.Now().Format(time.RFC3339), - level, - userID, - event, - formatFields(details), - ) +func (l *logger) Security() *slog.Logger { + return l.securityLogger } func (l *logger) Close() error { - var errs []error - - if closer, ok := l.appOutput.(io.Closer); ok { - if err := closer.Close(); err != nil { - errs = append(errs, fmt.Errorf("failed to close app output: %w", err)) + var lastErr error + for _, file := range l.files { + if file != nil { + if err := file.Close(); err != nil { + lastErr = err + } } } + return lastErr +} - if closer, ok := l.auditOutput.(io.Closer); ok { - if err := closer.Close(); err != nil { - errs = append(errs, fmt.Errorf("failed to close audit output: %w", err)) +// multiHandler implements slog.Handler for multiple outputs +type multiHandler []slog.Handler + +func (h multiHandler) Enabled(ctx context.Context, level slog.Level) bool { + for _, handler := range h { + if handler.Enabled(ctx, level) { + return true } } + return false +} - if closer, ok := l.secOutput.(io.Closer); ok { - if err := closer.Close(); err != nil { - errs = append(errs, fmt.Errorf("failed to close security output: %w", err)) +func (h multiHandler) Handle(ctx context.Context, r slog.Record) error { + for _, handler := range h { + if err := handler.Handle(ctx, r); err != nil { + return err } } - - if len(errs) > 0 { - return fmt.Errorf("failed to close some outputs: %v", errs) - } return nil } -func formatFields(fields map[string]interface{}) string { - if len(fields) == 0 { - return "" +func (h multiHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + handlers := make([]slog.Handler, len(h)) + for i, handler := range h { + handlers[i] = handler.WithAttrs(attrs) } - return fmt.Sprintf(" - Fields: %v", fields) + return multiHandler(handlers) +} + +func (h multiHandler) WithGroup(name string) slog.Handler { + handlers := make([]slog.Handler, len(h)) + for i, handler := range h { + handlers[i] = handler.WithGroup(name) + } + return multiHandler(handlers) } From 1ee8d947899717b3507a3153d18c43bc02cdfff2 Mon Sep 17 00:00:00 2001 From: LordMathis Date: Thu, 12 Dec 2024 21:06:15 +0100 Subject: [PATCH 03/22] Add logger to server --- server/internal/app/options.go | 9 +++++++++ server/internal/app/server.go | 6 ++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/server/internal/app/options.go b/server/internal/app/options.go index 21a8fa5..40c8bb7 100644 --- a/server/internal/app/options.go +++ b/server/internal/app/options.go @@ -3,6 +3,7 @@ package app import ( "novamd/internal/auth" "novamd/internal/db" + "novamd/internal/logging" "novamd/internal/storage" ) @@ -11,6 +12,7 @@ type Options struct { Config *Config Database db.Database Storage storage.Manager + Logger logging.Logger JWTManager auth.JWTManager SessionManager auth.SessionManager CookieService auth.CookieManager @@ -33,6 +35,12 @@ func DefaultOptions(cfg *Config) (*Options, error) { // Initialize storage storageManager := storage.NewService(cfg.WorkDir) + // Initialize logger + logger, err := logging.New(cfg.LogDir, cfg.LogLevel, cfg.ConsoleOutput) + if err != nil { + return nil, err + } + // Initialize auth services jwtManager, sessionService, cookieService, err := initAuth(cfg, database) if err != nil { @@ -48,6 +56,7 @@ func DefaultOptions(cfg *Config) (*Options, error) { Config: cfg, Database: database, Storage: storageManager, + Logger: logger, JWTManager: jwtManager, SessionManager: sessionService, CookieService: cookieService, diff --git a/server/internal/app/server.go b/server/internal/app/server.go index adb381e..d9166a1 100644 --- a/server/internal/app/server.go +++ b/server/internal/app/server.go @@ -1,7 +1,7 @@ package app import ( - "log" + "log/slog" "net/http" "github.com/go-chi/chi/v5" @@ -11,6 +11,7 @@ import ( type Server struct { router *chi.Mux options *Options + logger *slog.Logger } // NewServer creates a new server instance with the given options @@ -18,6 +19,7 @@ func NewServer(options *Options) *Server { return &Server{ router: setupRouter(*options), options: options, + logger: options.Logger.App(), } } @@ -25,7 +27,7 @@ func NewServer(options *Options) *Server { func (s *Server) Start() error { // Start server addr := ":" + s.options.Config.Port - log.Printf("Server starting on port %s", s.options.Config.Port) + s.logger.Info("Starting server", "address", addr) return http.ListenAndServe(addr, s.router) } From 71df436a936a1ac30049e98fc8eb0dd311a44394 Mon Sep 17 00:00:00 2001 From: LordMathis Date: Sat, 14 Dec 2024 23:59:28 +0100 Subject: [PATCH 04/22] Simplify logging --- server/internal/app/config.go | 24 +-- server/internal/app/options.go | 7 +- server/internal/app/server.go | 6 +- server/internal/logging/logger.go | 233 +++++------------------------- 4 files changed, 46 insertions(+), 224 deletions(-) diff --git a/server/internal/app/config.go b/server/internal/app/config.go index 61af395..be9936a 100644 --- a/server/internal/app/config.go +++ b/server/internal/app/config.go @@ -26,9 +26,7 @@ type Config struct { RateLimitRequests int RateLimitWindow time.Duration IsDevelopment bool - LogDir string LogLevel logging.LogLevel - ConsoleOutput bool } // DefaultConfig returns a new Config instance with default values @@ -41,9 +39,6 @@ func DefaultConfig() *Config { RateLimitRequests: 100, RateLimitWindow: time.Minute * 15, IsDevelopment: false, - LogDir: "./logs", - LogLevel: logging.INFO, - ConsoleOutput: false, } } @@ -115,25 +110,14 @@ func LoadConfig() (*Config, error) { } } - // Configure log directory - if logDir := os.Getenv("NOVAMD_LOG_DIR"); logDir != "" { - config.LogDir = logDir - } - // Configure log level, if isDevelopment is set, default to debug if logLevel := os.Getenv("NOVAMD_LOG_LEVEL"); logLevel != "" { - if parsed, err := logging.ParseLogLevel(logLevel); err == nil { - config.LogLevel = parsed - } + parsed := logging.ParseLogLevel(logLevel) + config.LogLevel = parsed } else if config.IsDevelopment { config.LogLevel = logging.DEBUG - } - - // Configure console output, if isDevelopment is set, default to true - if consoleOutput := os.Getenv("NOVAMD_CONSOLE_OUTPUT"); consoleOutput != "" { - if parsed, err := strconv.ParseBool(consoleOutput); err == nil { - config.ConsoleOutput = parsed - } + } else { + config.LogLevel = logging.INFO } // Validate all settings diff --git a/server/internal/app/options.go b/server/internal/app/options.go index 40c8bb7..6042dcb 100644 --- a/server/internal/app/options.go +++ b/server/internal/app/options.go @@ -12,7 +12,6 @@ type Options struct { Config *Config Database db.Database Storage storage.Manager - Logger logging.Logger JWTManager auth.JWTManager SessionManager auth.SessionManager CookieService auth.CookieManager @@ -36,10 +35,7 @@ func DefaultOptions(cfg *Config) (*Options, error) { storageManager := storage.NewService(cfg.WorkDir) // Initialize logger - logger, err := logging.New(cfg.LogDir, cfg.LogLevel, cfg.ConsoleOutput) - if err != nil { - return nil, err - } + logging.Setup(cfg.LogLevel) // Initialize auth services jwtManager, sessionService, cookieService, err := initAuth(cfg, database) @@ -56,7 +52,6 @@ func DefaultOptions(cfg *Config) (*Options, error) { Config: cfg, Database: database, Storage: storageManager, - Logger: logger, JWTManager: jwtManager, SessionManager: sessionService, CookieService: cookieService, diff --git a/server/internal/app/server.go b/server/internal/app/server.go index d9166a1..32f1575 100644 --- a/server/internal/app/server.go +++ b/server/internal/app/server.go @@ -1,8 +1,8 @@ package app import ( - "log/slog" "net/http" + "novamd/internal/logging" "github.com/go-chi/chi/v5" ) @@ -11,7 +11,6 @@ import ( type Server struct { router *chi.Mux options *Options - logger *slog.Logger } // NewServer creates a new server instance with the given options @@ -19,7 +18,6 @@ func NewServer(options *Options) *Server { return &Server{ router: setupRouter(*options), options: options, - logger: options.Logger.App(), } } @@ -27,7 +25,7 @@ func NewServer(options *Options) *Server { func (s *Server) Start() error { // Start server addr := ":" + s.options.Config.Port - s.logger.Info("Starting server", "address", addr) + logging.Info("Starting server", "address", addr) return http.ListenAndServe(addr, s.router) } diff --git a/server/internal/logging/logger.go b/server/internal/logging/logger.go index 2135918..75f9222 100644 --- a/server/internal/logging/logger.go +++ b/server/internal/logging/logger.go @@ -1,17 +1,15 @@ -// Package logging provides a structured logging interface for the application. +// Package logging provides a simple logging interface for the server. package logging import ( - "context" - "fmt" - "io" "log/slog" "os" - "path/filepath" - "time" ) -// LogLevel represents the logging level +// Logger is the global logger instance +var Logger *slog.Logger + +// LogLevel represents the log level type LogLevel slog.Level // Log levels @@ -22,208 +20,55 @@ const ( ERROR LogLevel = LogLevel(slog.LevelError) ) -// Logger defines the interface for logging operations -type Logger interface { - App() *slog.Logger // Returns logger for application logs - Audit() *slog.Logger // Returns logger for audit logs - Security() *slog.Logger // Returns logger for security logs - Close() error // Cleanup and close log files -} - -// logger implements the Logger interface -type logger struct { - appLogger *slog.Logger - auditLogger *slog.Logger - securityLogger *slog.Logger - files []*os.File // Keep track of open file handles -} - -// Output represents a destination for logs -type Output struct { - Type OutputFormat - Writer io.Writer -} - -// OutputFormat represents the format of the log output -type OutputFormat int - -const ( - OutputTypeJSON OutputFormat = iota // OutputTypeJSON JSON format - OutputTypeText // OutputTypeText text format -) - -// createLogFile creates a log file with the given name in the log directory -func createLogFile(logDir, name string) (*os.File, error) { - if err := os.MkdirAll(logDir, 0755); err != nil { - return nil, fmt.Errorf("failed to create log directory: %w", err) - } - - filename := filepath.Join(logDir, fmt.Sprintf("%s.log", name)) - file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - return nil, fmt.Errorf("failed to open log file: %w", err) - } - - return file, nil -} - -// createLogger creates a new slog.Handler for the given outputs and options -func createLogger(opts *slog.HandlerOptions, outputs []Output) slog.Handler { - if len(outputs) == 0 { - return slog.NewTextHandler(io.Discard, opts) - } - - if len(outputs) == 1 { - output := outputs[0] - if output.Type == OutputTypeJSON { - return slog.NewJSONHandler(output.Writer, opts) - } - return slog.NewTextHandler(output.Writer, opts) - } - - // Multiple outputs - create handlers for each - handlers := make([]slog.Handler, 0, len(outputs)) - for _, output := range outputs { - if output.Type == OutputTypeJSON { - handlers = append(handlers, slog.NewJSONHandler(output.Writer, opts)) - } else { - handlers = append(handlers, slog.NewTextHandler(output.Writer, opts)) - } - } - - return multiHandler(handlers) -} - -// ParseLogLevel parses a string into a LogLevel -func ParseLogLevel(level string) (LogLevel, error) { - switch level { - case "DEBUG": - return DEBUG, nil - case "INFO": - return INFO, nil - case "WARN": - return WARN, nil - case "ERROR": - return ERROR, nil - default: - return INFO, fmt.Errorf("invalid log level: %s", level) - } -} - -// New creates a new Logger instance -func New(logDir string, minLevel LogLevel, consoleOut bool) (Logger, error) { - l := &logger{ - files: make([]*os.File, 0, 3), - } - - // Define our log types and their filenames - logTypes := []struct { - name string - setLogger func(*slog.Logger) - }{ - {"app", func(lg *slog.Logger) { l.appLogger = lg }}, - {"audit", func(lg *slog.Logger) { l.auditLogger = lg }}, - {"security", func(lg *slog.Logger) { l.securityLogger = lg }}, - } - - // Setup handlers options +// Setup initializes the logger with the given minimum log level +func Setup(minLevel LogLevel) { opts := &slog.HandlerOptions{ - Level: slog.Level(minLevel), - AddSource: slog.Level(minLevel) == slog.LevelDebug, - ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr { - if a.Key == slog.TimeKey { - return slog.Attr{ - Key: a.Key, - Value: slog.StringValue(time.Now().UTC().Format(time.RFC3339)), - } - } - return a - }, + Level: slog.Level(minLevel), } - // Create loggers for each type - for _, lt := range logTypes { - // Create file output - file, err := createLogFile(logDir, lt.name) - if err != nil { - if err := l.Close(); err != nil { - return nil, fmt.Errorf("failed to close logger: %w", err) - } - return nil, fmt.Errorf("failed to create %s log file: %w", lt.name, err) - } - l.files = append(l.files, file) + Logger = slog.New(slog.NewTextHandler(os.Stdout, opts)) +} - // Prepare outputs - outputs := []Output{{Type: OutputTypeJSON, Writer: file}} - if consoleOut { - outputs = append(outputs, Output{Type: OutputTypeText, Writer: os.Stdout}) - } - - // Create and set logger - handler := createLogger(opts, outputs) - lt.setLogger(slog.New(handler)) +// ParseLogLevel converts a string to a LogLevel +func ParseLogLevel(level string) LogLevel { + switch level { + case "debug": + return DEBUG + case "warn": + return WARN + case "error": + return ERROR + default: + return INFO } - - return l, nil } -func (l *logger) App() *slog.Logger { - return l.appLogger +// Debug logs a debug message +func Debug(msg string, args ...any) { + Logger.Debug(msg, args...) } -func (l *logger) Audit() *slog.Logger { - return l.auditLogger +// Info logs an info message +func Info(msg string, args ...any) { + Logger.Info(msg, args...) } -func (l *logger) Security() *slog.Logger { - return l.securityLogger +// Warn logs a warning message +func Warn(msg string, args ...any) { + Logger.Warn(msg, args...) } -func (l *logger) Close() error { - var lastErr error - for _, file := range l.files { - if file != nil { - if err := file.Close(); err != nil { - lastErr = err - } - } - } - return lastErr +// Error logs an error message +func Error(msg string, args ...any) { + Logger.Error(msg, args...) } -// multiHandler implements slog.Handler for multiple outputs -type multiHandler []slog.Handler - -func (h multiHandler) Enabled(ctx context.Context, level slog.Level) bool { - for _, handler := range h { - if handler.Enabled(ctx, level) { - return true - } - } - return false +// WithGroup adds a group to the logger context +func WithGroup(name string) *slog.Logger { + return Logger.WithGroup(name) } -func (h multiHandler) Handle(ctx context.Context, r slog.Record) error { - for _, handler := range h { - if err := handler.Handle(ctx, r); err != nil { - return err - } - } - return nil -} - -func (h multiHandler) WithAttrs(attrs []slog.Attr) slog.Handler { - handlers := make([]slog.Handler, len(h)) - for i, handler := range h { - handlers[i] = handler.WithAttrs(attrs) - } - return multiHandler(handlers) -} - -func (h multiHandler) WithGroup(name string) slog.Handler { - handlers := make([]slog.Handler, len(h)) - for i, handler := range h { - handlers[i] = handler.WithGroup(name) - } - return multiHandler(handlers) +// With adds key-value pairs to the logger context +func With(args ...any) *slog.Logger { + return Logger.With(args...) } From d14eae4de4719b8240ad0d424050859c76d4a178 Mon Sep 17 00:00:00 2001 From: LordMathis Date: Sun, 15 Dec 2024 13:34:31 +0100 Subject: [PATCH 05/22] Add Logger interface --- server/internal/logging/logger.go | 62 ++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/server/internal/logging/logger.go b/server/internal/logging/logger.go index 75f9222..e05ea32 100644 --- a/server/internal/logging/logger.go +++ b/server/internal/logging/logger.go @@ -6,8 +6,23 @@ import ( "os" ) +// Logger represents the interface for logging operations +type Logger interface { + Debug(msg string, args ...any) + Info(msg string, args ...any) + Warn(msg string, args ...any) + Error(msg string, args ...any) + WithGroup(name string) Logger + With(args ...any) Logger +} + +// Implementation of the Logger interface using slog +type logger struct { + logger *slog.Logger +} + // Logger is the global logger instance -var Logger *slog.Logger +var defaultLogger Logger // LogLevel represents the log level type LogLevel slog.Level @@ -26,7 +41,9 @@ func Setup(minLevel LogLevel) { Level: slog.Level(minLevel), } - Logger = slog.New(slog.NewTextHandler(os.Stdout, opts)) + defaultLogger = &logger{ + logger: slog.New(slog.NewTextHandler(os.Stdout, opts)), + } } // ParseLogLevel converts a string to a LogLevel @@ -43,32 +60,57 @@ func ParseLogLevel(level string) LogLevel { } } +// Implementation of Logger interface methods +func (l *logger) Debug(msg string, args ...any) { + l.logger.Debug(msg, args...) +} + +func (l *logger) Info(msg string, args ...any) { + l.logger.Info(msg, args...) +} + +func (l *logger) Warn(msg string, args ...any) { + l.logger.Warn(msg, args...) +} + +func (l *logger) Error(msg string, args ...any) { + l.logger.Error(msg, args...) +} + +func (l *logger) WithGroup(name string) Logger { + return &logger{logger: l.logger.WithGroup(name)} +} + +func (l *logger) With(args ...any) Logger { + return &logger{logger: l.logger.With(args...)} +} + // Debug logs a debug message func Debug(msg string, args ...any) { - Logger.Debug(msg, args...) + defaultLogger.Debug(msg, args...) } // Info logs an info message func Info(msg string, args ...any) { - Logger.Info(msg, args...) + defaultLogger.Info(msg, args...) } // Warn logs a warning message func Warn(msg string, args ...any) { - Logger.Warn(msg, args...) + defaultLogger.Warn(msg, args...) } // Error logs an error message func Error(msg string, args ...any) { - Logger.Error(msg, args...) + defaultLogger.Error(msg, args...) } // WithGroup adds a group to the logger context -func WithGroup(name string) *slog.Logger { - return Logger.WithGroup(name) +func WithGroup(name string) Logger { + return defaultLogger.WithGroup(name) } // With adds key-value pairs to the logger context -func With(args ...any) *slog.Logger { - return Logger.With(args...) +func With(args ...any) Logger { + return defaultLogger.With(args...) } From ab00276f0d19a975f72422f647dae379a9c4f628 Mon Sep 17 00:00:00 2001 From: LordMathis Date: Sun, 15 Dec 2024 14:12:39 +0100 Subject: [PATCH 06/22] Add logging to db package --- server/internal/db/db.go | 64 ++++- server/internal/db/migrations.go | 55 +++- server/internal/db/sessions.go | 67 ++++- server/internal/db/system.go | 80 +++++- server/internal/db/users.go | 310 +++++++++++++++++---- server/internal/db/workspaces.go | 461 +++++++++++++++++++++++++------ 6 files changed, 847 insertions(+), 190 deletions(-) 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 } From a32e0957eddf4e734dc77fc20a32611bc9db1878 Mon Sep 17 00:00:00 2001 From: LordMathis Date: Sun, 15 Dec 2024 16:46:29 +0100 Subject: [PATCH 07/22] Add logging to app package --- server/internal/app/config.go | 37 ++++++++++++++++++++-- server/internal/app/init.go | 59 +++++++++++++++++++++++++++-------- server/internal/app/routes.go | 8 +++++ server/internal/app/server.go | 1 + 4 files changed, 90 insertions(+), 15 deletions(-) diff --git a/server/internal/app/config.go b/server/internal/app/config.go index be9936a..84646fb 100644 --- a/server/internal/app/config.go +++ b/server/internal/app/config.go @@ -58,37 +58,46 @@ func (c *Config) validate() error { // LoadConfig creates a new Config instance with values from environment variables func LoadConfig() (*Config, error) { + logging.Info("Loading configuration from environment variables") config := DefaultConfig() if env := os.Getenv("NOVAMD_ENV"); env != "" { + logging.Debug("Loading config for environment", "env", env) config.IsDevelopment = env == "development" } if dbPath := os.Getenv("NOVAMD_DB_PATH"); dbPath != "" { + logging.Debug("Loading config for database path", "path", dbPath) config.DBPath = dbPath } if workDir := os.Getenv("NOVAMD_WORKDIR"); workDir != "" { + logging.Debug("Loading config for work directory", "dir", workDir) config.WorkDir = workDir } if staticPath := os.Getenv("NOVAMD_STATIC_PATH"); staticPath != "" { + logging.Debug("Loading config for static path", "path", staticPath) config.StaticPath = staticPath } if port := os.Getenv("NOVAMD_PORT"); port != "" { + logging.Debug("Loading config for port", "port", port) config.Port = port } if rootURL := os.Getenv("NOVAMD_ROOT_URL"); rootURL != "" { + logging.Debug("Loading config for root URL", "url", rootURL) config.RootURL = rootURL } if domain := os.Getenv("NOVAMD_DOMAIN"); domain != "" { + logging.Debug("Loading config for domain", "domain", domain) config.Domain = domain } if corsOrigins := os.Getenv("NOVAMD_CORS_ORIGINS"); corsOrigins != "" { + logging.Debug("Loading config for CORS origins", "origins", corsOrigins) config.CORSOrigins = strings.Split(corsOrigins, ",") } @@ -97,15 +106,35 @@ func LoadConfig() (*Config, error) { config.EncryptionKey = os.Getenv("NOVAMD_ENCRYPTION_KEY") config.JWTSigningKey = os.Getenv("NOVAMD_JWT_SIGNING_KEY") + logging.Debug("Sensitive configuration loaded", + "adminEmailSet", config.AdminEmail != "", + "adminPasswordSet", config.AdminPassword != "", + "encryptionKeySet", config.EncryptionKey != "", + "jwtSigningKeySet", config.JWTSigningKey != "") + // Configure rate limiting if reqStr := os.Getenv("NOVAMD_RATE_LIMIT_REQUESTS"); reqStr != "" { - if parsed, err := strconv.Atoi(reqStr); err == nil { + parsed, err := strconv.Atoi(reqStr) + if err != nil { + logging.Warn("Invalid rate limit requests value, using default", + "value", reqStr, + "default", config.RateLimitRequests, + "error", err) + } else { + logging.Debug("Loading config for rate limit requests", "requests", parsed) config.RateLimitRequests = parsed } } if windowStr := os.Getenv("NOVAMD_RATE_LIMIT_WINDOW"); windowStr != "" { - if parsed, err := time.ParseDuration(windowStr); err == nil { + parsed, err := time.ParseDuration(windowStr) + if err != nil { + logging.Warn("Invalid rate limit window value, using default", + "value", windowStr, + "default", config.RateLimitWindow, + "error", err) + } else { + logging.Debug("Loading config for rate limit window", "window", parsed) config.RateLimitWindow = parsed } } @@ -113,10 +142,13 @@ func LoadConfig() (*Config, error) { // Configure log level, if isDevelopment is set, default to debug if logLevel := os.Getenv("NOVAMD_LOG_LEVEL"); logLevel != "" { parsed := logging.ParseLogLevel(logLevel) + logging.Debug("Loading config for log level", "level", parsed) config.LogLevel = parsed } else if config.IsDevelopment { + logging.Debug("Setting log level to debug for development") config.LogLevel = logging.DEBUG } else { + logging.Debug("Setting log level to info for production") config.LogLevel = logging.INFO } @@ -125,5 +157,6 @@ func LoadConfig() (*Config, error) { return nil, err } + logging.Info("Configuration loaded successfully") return config, nil } diff --git a/server/internal/app/init.go b/server/internal/app/init.go index 773f8c3..0ea17d0 100644 --- a/server/internal/app/init.go +++ b/server/internal/app/init.go @@ -4,13 +4,13 @@ package app import ( "database/sql" "fmt" - "log" "time" "golang.org/x/crypto/bcrypt" "novamd/internal/auth" "novamd/internal/db" + "novamd/internal/logging" "novamd/internal/models" "novamd/internal/secrets" "novamd/internal/storage" @@ -18,39 +18,53 @@ import ( // initSecretsService initializes the secrets service func initSecretsService(cfg *Config) (secrets.Service, error) { + logging.Debug("Initializing secrets service") secretsService, err := secrets.NewService(cfg.EncryptionKey) if err != nil { return nil, fmt.Errorf("failed to initialize secrets service: %w", err) } + logging.Debug("Secrets service initialized") return secretsService, nil } // initDatabase initializes and migrates the database func initDatabase(cfg *Config, secretsService secrets.Service) (db.Database, error) { + logging.Debug("Initializing database", "path", cfg.DBPath) + database, err := db.Init(cfg.DBPath, secretsService) if err != nil { return nil, fmt.Errorf("failed to initialize database: %w", err) } + logging.Debug("Running database migrations") if err := database.Migrate(); err != nil { return nil, fmt.Errorf("failed to apply database migrations: %w", err) } + logging.Debug("Database initialization complete") return database, nil } // initAuth initializes JWT and session services func initAuth(cfg *Config, database db.Database) (auth.JWTManager, auth.SessionManager, auth.CookieManager, error) { + logging.Debug("Initializing authentication services") + // Get or generate JWT signing key signingKey := cfg.JWTSigningKey if signingKey == "" { + logging.Debug("No JWT signing key provided, generating new key") var err error signingKey, err = database.EnsureJWTSecret() if err != nil { return nil, nil, nil, fmt.Errorf("failed to ensure JWT secret: %w", err) } + logging.Debug("JWT signing key generated") } + logging.Debug("Initializing JWT service", + "accessTokenExpiry", "15m", + "refreshTokenExpiry", "168h") + // Initialize JWT service jwtManager, err := auth.NewJWTService(auth.JWTConfig{ SigningKey: signingKey, @@ -62,36 +76,45 @@ func initAuth(cfg *Config, database db.Database) (auth.JWTManager, auth.SessionM } // Initialize session service + logging.Debug("Initializing session service") sessionManager := auth.NewSessionService(database, jwtManager) - // Cookie service + // Initialize cookie service + logging.Debug("Initializing cookie service", + "isDevelopment", cfg.IsDevelopment, + "domain", cfg.Domain) cookieService := auth.NewCookieService(cfg.IsDevelopment, cfg.Domain) + logging.Debug("Authentication services initialized") return jwtManager, sessionManager, cookieService, nil } // setupAdminUser creates the admin user if it doesn't exist func setupAdminUser(database db.Database, storageManager storage.Manager, cfg *Config) error { - adminEmail := cfg.AdminEmail - adminPassword := cfg.AdminPassword + logging.Debug("Checking for existing admin user", "email", cfg.AdminEmail) // Check if admin user exists - adminUser, err := database.GetUserByEmail(adminEmail) - if adminUser != nil { - return nil // Admin user already exists - } else if err != sql.ErrNoRows { - return err + adminUser, err := database.GetUserByEmail(cfg.AdminEmail) + if err != nil && err != sql.ErrNoRows { + return fmt.Errorf("failed to check for existing admin user: %w", err) } + if adminUser != nil { + logging.Debug("Admin user already exists", "userId", adminUser.ID) + return nil + } + + logging.Debug("Creating new admin user") + // Hash the password - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(adminPassword), bcrypt.DefaultCost) + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(cfg.AdminPassword), bcrypt.DefaultCost) if err != nil { - return fmt.Errorf("failed to hash password: %w", err) + return fmt.Errorf("failed to hash admin password: %w", err) } // Create admin user adminUser = &models.User{ - Email: adminEmail, + Email: cfg.AdminEmail, DisplayName: "Admin", PasswordHash: string(hashedPassword), Role: models.RoleAdmin, @@ -102,13 +125,23 @@ func setupAdminUser(database db.Database, storageManager storage.Manager, cfg *C return fmt.Errorf("failed to create admin user: %w", err) } + logging.Debug("Admin user created", + "userId", createdUser.ID, + "workspaceId", createdUser.LastWorkspaceID) + // Initialize workspace directory + logging.Debug("Initializing admin workspace directory", + "userId", createdUser.ID, + "workspaceId", createdUser.LastWorkspaceID) + err = storageManager.InitializeUserWorkspace(createdUser.ID, createdUser.LastWorkspaceID) if err != nil { return fmt.Errorf("failed to initialize admin workspace: %w", err) } - log.Printf("Created admin user with ID: %d and default workspace with ID: %d", createdUser.ID, createdUser.LastWorkspaceID) + logging.Info("Admin user setup completed", + "userId", createdUser.ID, + "workspaceId", createdUser.LastWorkspaceID) return nil } diff --git a/server/internal/app/routes.go b/server/internal/app/routes.go index 644dee6..3a20f9c 100644 --- a/server/internal/app/routes.go +++ b/server/internal/app/routes.go @@ -4,6 +4,7 @@ import ( "novamd/internal/auth" "novamd/internal/context" "novamd/internal/handlers" + "novamd/internal/logging" "time" "github.com/go-chi/chi/v5" @@ -19,6 +20,7 @@ import ( // setupRouter creates and configures the chi router with middleware and routes func setupRouter(o Options) *chi.Mux { + logging.Debug("Setting up router") r := chi.NewRouter() // Basic middleware @@ -29,6 +31,7 @@ func setupRouter(o Options) *chi.Mux { r.Use(middleware.Timeout(30 * time.Second)) // Security headers + logging.Debug("Setting up security headers") r.Use(secure.New(secure.Options{ SSLRedirect: false, SSLProxyHeaders: map[string]string{"X-Forwarded-Proto": "https"}, @@ -36,6 +39,7 @@ func setupRouter(o Options) *chi.Mux { }).Handler) // CORS if origins are configured + logging.Debug("Setting up CORS") if len(o.Config.CORSOrigins) > 0 { r.Use(cors.Handler(cors.Options{ AllowedOrigins: o.Config.CORSOrigins, @@ -48,6 +52,7 @@ func setupRouter(o Options) *chi.Mux { } // Initialize auth middleware and handler + logging.Debug("Setting up authentication middleware") authMiddleware := auth.NewMiddleware(o.JWTManager, o.SessionManager, o.CookieService) handler := &handlers.Handler{ DB: o.Database, @@ -55,12 +60,14 @@ func setupRouter(o Options) *chi.Mux { } if o.Config.IsDevelopment { + logging.Debug("Setting up Swagger docs") r.Get("/swagger/*", httpSwagger.Handler( httpSwagger.URL("/swagger/doc.json"), // The URL pointing to API definition )) } // API routes + logging.Debug("Setting up API routes") r.Route("/api/v1", func(r chi.Router) { // Rate limiting for API routes if o.Config.RateLimitRequests > 0 { @@ -147,6 +154,7 @@ func setupRouter(o Options) *chi.Mux { }) // Handle all other routes with static file server + logging.Debug("Setting up static file server") r.Get("/*", handlers.NewStaticHandler(o.Config.StaticPath).ServeHTTP) return r diff --git a/server/internal/app/server.go b/server/internal/app/server.go index 32f1575..ecfd4d9 100644 --- a/server/internal/app/server.go +++ b/server/internal/app/server.go @@ -31,6 +31,7 @@ func (s *Server) Start() error { // Close handles graceful shutdown of server dependencies func (s *Server) Close() error { + logging.Info("Shutting down server") return s.options.Database.Close() } From 8dd57bdf0b0a9eca0d099474aab8dcc644c5bef5 Mon Sep 17 00:00:00 2001 From: LordMathis Date: Sun, 15 Dec 2024 17:13:40 +0100 Subject: [PATCH 08/22] Use lowercase for log messages --- server/internal/app/config.go | 36 ++++++++++++++-------------- server/internal/app/init.go | 45 +++++++++++++++++++---------------- server/internal/app/routes.go | 14 +++++------ server/internal/app/server.go | 4 ++-- 4 files changed, 51 insertions(+), 48 deletions(-) diff --git a/server/internal/app/config.go b/server/internal/app/config.go index 84646fb..08a4815 100644 --- a/server/internal/app/config.go +++ b/server/internal/app/config.go @@ -58,46 +58,46 @@ func (c *Config) validate() error { // LoadConfig creates a new Config instance with values from environment variables func LoadConfig() (*Config, error) { - logging.Info("Loading configuration from environment variables") + logging.Info("loading configuration from environment variables") config := DefaultConfig() if env := os.Getenv("NOVAMD_ENV"); env != "" { - logging.Debug("Loading config for environment", "env", env) + logging.Debug("loading config for environment", "env", env) config.IsDevelopment = env == "development" } if dbPath := os.Getenv("NOVAMD_DB_PATH"); dbPath != "" { - logging.Debug("Loading config for database path", "path", dbPath) + logging.Debug("loading config for database path", "path", dbPath) config.DBPath = dbPath } if workDir := os.Getenv("NOVAMD_WORKDIR"); workDir != "" { - logging.Debug("Loading config for work directory", "dir", workDir) + logging.Debug("loading config for work directory", "dir", workDir) config.WorkDir = workDir } if staticPath := os.Getenv("NOVAMD_STATIC_PATH"); staticPath != "" { - logging.Debug("Loading config for static path", "path", staticPath) + logging.Debug("loading config for static path", "path", staticPath) config.StaticPath = staticPath } if port := os.Getenv("NOVAMD_PORT"); port != "" { - logging.Debug("Loading config for port", "port", port) + logging.Debug("loading config for port", "port", port) config.Port = port } if rootURL := os.Getenv("NOVAMD_ROOT_URL"); rootURL != "" { - logging.Debug("Loading config for root URL", "url", rootURL) + logging.Debug("loading config for root URL", "url", rootURL) config.RootURL = rootURL } if domain := os.Getenv("NOVAMD_DOMAIN"); domain != "" { - logging.Debug("Loading config for domain", "domain", domain) + logging.Debug("loading config for domain", "domain", domain) config.Domain = domain } if corsOrigins := os.Getenv("NOVAMD_CORS_ORIGINS"); corsOrigins != "" { - logging.Debug("Loading config for CORS origins", "origins", corsOrigins) + logging.Debug("loading config for CORS origins", "origins", corsOrigins) config.CORSOrigins = strings.Split(corsOrigins, ",") } @@ -106,7 +106,7 @@ func LoadConfig() (*Config, error) { config.EncryptionKey = os.Getenv("NOVAMD_ENCRYPTION_KEY") config.JWTSigningKey = os.Getenv("NOVAMD_JWT_SIGNING_KEY") - logging.Debug("Sensitive configuration loaded", + logging.Debug("sensitive configuration loaded", "adminEmailSet", config.AdminEmail != "", "adminPasswordSet", config.AdminPassword != "", "encryptionKeySet", config.EncryptionKey != "", @@ -116,12 +116,12 @@ func LoadConfig() (*Config, error) { if reqStr := os.Getenv("NOVAMD_RATE_LIMIT_REQUESTS"); reqStr != "" { parsed, err := strconv.Atoi(reqStr) if err != nil { - logging.Warn("Invalid rate limit requests value, using default", + logging.Warn("invalid rate limit requests value, using default", "value", reqStr, "default", config.RateLimitRequests, "error", err) } else { - logging.Debug("Loading config for rate limit requests", "requests", parsed) + logging.Debug("loading config for rate limit requests", "requests", parsed) config.RateLimitRequests = parsed } } @@ -129,12 +129,12 @@ func LoadConfig() (*Config, error) { if windowStr := os.Getenv("NOVAMD_RATE_LIMIT_WINDOW"); windowStr != "" { parsed, err := time.ParseDuration(windowStr) if err != nil { - logging.Warn("Invalid rate limit window value, using default", + logging.Warn("invalid rate limit window value, using default", "value", windowStr, "default", config.RateLimitWindow, "error", err) } else { - logging.Debug("Loading config for rate limit window", "window", parsed) + logging.Debug("loading config for rate limit window", "window", parsed) config.RateLimitWindow = parsed } } @@ -142,13 +142,13 @@ func LoadConfig() (*Config, error) { // Configure log level, if isDevelopment is set, default to debug if logLevel := os.Getenv("NOVAMD_LOG_LEVEL"); logLevel != "" { parsed := logging.ParseLogLevel(logLevel) - logging.Debug("Loading config for log level", "level", parsed) + logging.Debug("loading config for log level", "level", parsed) config.LogLevel = parsed } else if config.IsDevelopment { - logging.Debug("Setting log level to debug for development") + logging.Debug("setting log level to debug for development") config.LogLevel = logging.DEBUG } else { - logging.Debug("Setting log level to info for production") + logging.Debug("setting log level to info for production") config.LogLevel = logging.INFO } @@ -157,6 +157,6 @@ func LoadConfig() (*Config, error) { return nil, err } - logging.Info("Configuration loaded successfully") + logging.Info("configuration loaded successfully") return config, nil } diff --git a/server/internal/app/init.go b/server/internal/app/init.go index 0ea17d0..dd003f7 100644 --- a/server/internal/app/init.go +++ b/server/internal/app/init.go @@ -18,41 +18,44 @@ import ( // initSecretsService initializes the secrets service func initSecretsService(cfg *Config) (secrets.Service, error) { - logging.Debug("Initializing secrets service") + logging.Debug("initializing secrets service") secretsService, err := secrets.NewService(cfg.EncryptionKey) if err != nil { return nil, fmt.Errorf("failed to initialize secrets service: %w", err) } - logging.Debug("Secrets service initialized") + logging.Debug("secrets service initialized") return secretsService, nil } // initDatabase initializes and migrates the database func initDatabase(cfg *Config, secretsService secrets.Service) (db.Database, error) { - logging.Debug("Initializing database", "path", cfg.DBPath) + logging.Debug("initializing database", "path", cfg.DBPath) database, err := db.Init(cfg.DBPath, secretsService) if err != nil { return nil, fmt.Errorf("failed to initialize database: %w", err) } - logging.Debug("Running database migrations") + logging.Debug("running database migrations") if err := database.Migrate(); err != nil { return nil, fmt.Errorf("failed to apply database migrations: %w", err) } - logging.Debug("Database initialization complete") + logging.Debug("database initialization complete") return database, nil } // initAuth initializes JWT and session services func initAuth(cfg *Config, database db.Database) (auth.JWTManager, auth.SessionManager, auth.CookieManager, error) { - logging.Debug("Initializing authentication services") + logging.Debug("initializing authentication services") + + accessTokeExpiry := 15 * time.Minute + refreshTokenExpiry := 7 * 24 * time.Hour // Get or generate JWT signing key signingKey := cfg.JWTSigningKey if signingKey == "" { - logging.Debug("No JWT signing key provided, generating new key") + logging.Debug("no JWT signing key provided, generating new key") var err error signingKey, err = database.EnsureJWTSecret() if err != nil { @@ -61,37 +64,37 @@ func initAuth(cfg *Config, database db.Database) (auth.JWTManager, auth.SessionM logging.Debug("JWT signing key generated") } - logging.Debug("Initializing JWT service", - "accessTokenExpiry", "15m", - "refreshTokenExpiry", "168h") + logging.Debug("initializing JWT service", + "accessTokenExpiry", accessTokeExpiry.String(), + "refreshTokenExpiry", refreshTokenExpiry.String()) // Initialize JWT service jwtManager, err := auth.NewJWTService(auth.JWTConfig{ SigningKey: signingKey, - AccessTokenExpiry: 15 * time.Minute, - RefreshTokenExpiry: 7 * 24 * time.Hour, + AccessTokenExpiry: accessTokeExpiry, + RefreshTokenExpiry: refreshTokenExpiry, }) if err != nil { return nil, nil, nil, fmt.Errorf("failed to initialize JWT service: %w", err) } // Initialize session service - logging.Debug("Initializing session service") + logging.Debug("initializing session service") sessionManager := auth.NewSessionService(database, jwtManager) // Initialize cookie service - logging.Debug("Initializing cookie service", + logging.Debug("initializing cookie service", "isDevelopment", cfg.IsDevelopment, "domain", cfg.Domain) cookieService := auth.NewCookieService(cfg.IsDevelopment, cfg.Domain) - logging.Debug("Authentication services initialized") + logging.Debug("authentication services initialized") return jwtManager, sessionManager, cookieService, nil } // setupAdminUser creates the admin user if it doesn't exist func setupAdminUser(database db.Database, storageManager storage.Manager, cfg *Config) error { - logging.Debug("Checking for existing admin user", "email", cfg.AdminEmail) + logging.Debug("checking for existing admin user", "email", cfg.AdminEmail) // Check if admin user exists adminUser, err := database.GetUserByEmail(cfg.AdminEmail) @@ -100,11 +103,11 @@ func setupAdminUser(database db.Database, storageManager storage.Manager, cfg *C } if adminUser != nil { - logging.Debug("Admin user already exists", "userId", adminUser.ID) + logging.Debug("admin user already exists", "userId", adminUser.ID) return nil } - logging.Debug("Creating new admin user") + logging.Debug("creating new admin user") // Hash the password hashedPassword, err := bcrypt.GenerateFromPassword([]byte(cfg.AdminPassword), bcrypt.DefaultCost) @@ -125,12 +128,12 @@ func setupAdminUser(database db.Database, storageManager storage.Manager, cfg *C return fmt.Errorf("failed to create admin user: %w", err) } - logging.Debug("Admin user created", + logging.Debug("admin user created", "userId", createdUser.ID, "workspaceId", createdUser.LastWorkspaceID) // Initialize workspace directory - logging.Debug("Initializing admin workspace directory", + logging.Debug("initializing admin workspace directory", "userId", createdUser.ID, "workspaceId", createdUser.LastWorkspaceID) @@ -139,7 +142,7 @@ func setupAdminUser(database db.Database, storageManager storage.Manager, cfg *C return fmt.Errorf("failed to initialize admin workspace: %w", err) } - logging.Info("Admin user setup completed", + logging.Info("admin user setup completed", "userId", createdUser.ID, "workspaceId", createdUser.LastWorkspaceID) diff --git a/server/internal/app/routes.go b/server/internal/app/routes.go index 3a20f9c..b22e1f8 100644 --- a/server/internal/app/routes.go +++ b/server/internal/app/routes.go @@ -20,7 +20,7 @@ import ( // setupRouter creates and configures the chi router with middleware and routes func setupRouter(o Options) *chi.Mux { - logging.Debug("Setting up router") + logging.Debug("setting up router") r := chi.NewRouter() // Basic middleware @@ -31,7 +31,7 @@ func setupRouter(o Options) *chi.Mux { r.Use(middleware.Timeout(30 * time.Second)) // Security headers - logging.Debug("Setting up security headers") + logging.Debug("setting up security headers") r.Use(secure.New(secure.Options{ SSLRedirect: false, SSLProxyHeaders: map[string]string{"X-Forwarded-Proto": "https"}, @@ -39,7 +39,7 @@ func setupRouter(o Options) *chi.Mux { }).Handler) // CORS if origins are configured - logging.Debug("Setting up CORS") + logging.Debug("setting up CORS") if len(o.Config.CORSOrigins) > 0 { r.Use(cors.Handler(cors.Options{ AllowedOrigins: o.Config.CORSOrigins, @@ -52,7 +52,7 @@ func setupRouter(o Options) *chi.Mux { } // Initialize auth middleware and handler - logging.Debug("Setting up authentication middleware") + logging.Debug("setting up authentication middleware") authMiddleware := auth.NewMiddleware(o.JWTManager, o.SessionManager, o.CookieService) handler := &handlers.Handler{ DB: o.Database, @@ -60,14 +60,14 @@ func setupRouter(o Options) *chi.Mux { } if o.Config.IsDevelopment { - logging.Debug("Setting up Swagger docs") + logging.Debug("setting up Swagger docs") r.Get("/swagger/*", httpSwagger.Handler( httpSwagger.URL("/swagger/doc.json"), // The URL pointing to API definition )) } // API routes - logging.Debug("Setting up API routes") + logging.Debug("setting up API routes") r.Route("/api/v1", func(r chi.Router) { // Rate limiting for API routes if o.Config.RateLimitRequests > 0 { @@ -154,7 +154,7 @@ func setupRouter(o Options) *chi.Mux { }) // Handle all other routes with static file server - logging.Debug("Setting up static file server") + logging.Debug("setting up static file server") r.Get("/*", handlers.NewStaticHandler(o.Config.StaticPath).ServeHTTP) return r diff --git a/server/internal/app/server.go b/server/internal/app/server.go index ecfd4d9..d0958c4 100644 --- a/server/internal/app/server.go +++ b/server/internal/app/server.go @@ -25,13 +25,13 @@ func NewServer(options *Options) *Server { func (s *Server) Start() error { // Start server addr := ":" + s.options.Config.Port - logging.Info("Starting server", "address", addr) + logging.Info("starting server", "address", addr) return http.ListenAndServe(addr, s.router) } // Close handles graceful shutdown of server dependencies func (s *Server) Close() error { - logging.Info("Shutting down server") + logging.Info("shutting down server") return s.options.Database.Close() } From d6680d8e034dc3cd59424a6e230eabf34b767daa Mon Sep 17 00:00:00 2001 From: LordMathis Date: Sun, 15 Dec 2024 17:28:53 +0100 Subject: [PATCH 09/22] Update db logging --- server/internal/db/db.go | 6 --- server/internal/db/migrations.go | 12 ----- server/internal/db/sessions.go | 16 ------ server/internal/db/system.go | 24 --------- server/internal/db/users.go | 76 +++----------------------- server/internal/db/workspaces.go | 91 ++++---------------------------- 6 files changed, 15 insertions(+), 210 deletions(-) diff --git a/server/internal/db/db.go b/server/internal/db/db.go index f097d1d..d8ccdd1 100644 --- a/server/internal/db/db.go +++ b/server/internal/db/db.go @@ -117,19 +117,16 @@ func Init(dbPath string, secretsService secrets.Service) (Database, error) { db, err := sql.Open("sqlite3", dbPath) if err != nil { - log.Error("failed to open database", "error", err) return nil, fmt.Errorf("failed to open database: %w", err) } if err := db.Ping(); err != nil { - 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 { - 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") @@ -149,7 +146,6 @@ func (db *database) Close() error { 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) } @@ -168,7 +164,6 @@ func (db *database) encryptToken(token string) (string, error) { 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) } @@ -186,7 +181,6 @@ func (db *database) decryptToken(token string) (string, error) { 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) } diff --git a/server/internal/db/migrations.go b/server/internal/db/migrations.go index 755bf2d..dfa0100 100644 --- a/server/internal/db/migrations.go +++ b/server/internal/db/migrations.go @@ -87,7 +87,6 @@ func (db *database) Migrate() error { version INTEGER PRIMARY KEY )`) if err != nil { - log.Error("failed to create migrations table", "error", err) return fmt.Errorf("failed to create migrations table: %w", err) } @@ -96,7 +95,6 @@ func (db *database) Migrate() error { var currentVersion int err = db.QueryRow("SELECT COALESCE(MAX(version), 0) FROM migrations").Scan(¤tVersion) if err != nil { - 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) @@ -109,7 +107,6 @@ func (db *database) Migrate() error { tx, err := db.Begin() if err != nil { - log.Error("failed to begin transaction", "error", err) return fmt.Errorf("failed to begin transaction for migration %d: %w", migration.Version, err) } @@ -117,11 +114,7 @@ func (db *database) Migrate() error { 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 { - 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) } @@ -133,11 +126,7 @@ func (db *database) Migrate() error { 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 { - 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) } @@ -149,7 +138,6 @@ func (db *database) Migrate() error { log.Debug("committing migration") err = tx.Commit() if err != nil { - log.Error("failed to commit migration", "error", err) return fmt.Errorf("failed to commit migration %d: %w", migration.Version, err) } diff --git a/server/internal/db/sessions.go b/server/internal/db/sessions.go index 4bc66b3..ce5ba16 100644 --- a/server/internal/db/sessions.go +++ b/server/internal/db/sessions.go @@ -22,10 +22,6 @@ func (db *database) CreateSession(session *models.Session) error { 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) } @@ -53,7 +49,6 @@ func (db *database) GetSessionByRefreshToken(refreshToken string) (*models.Sessi 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) } @@ -81,9 +76,6 @@ func (db *database) GetSessionByID(sessionID string) (*models.Session, error) { 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) } @@ -100,17 +92,11 @@ func (db *database) DeleteSession(sessionID string) error { 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) } @@ -130,13 +116,11 @@ func (db *database) CleanExpiredSessions() error { 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) } diff --git a/server/internal/db/system.go b/server/internal/db/system.go index f1eadbf..536d072 100644 --- a/server/internal/db/system.go +++ b/server/internal/db/system.go @@ -2,7 +2,6 @@ package db import ( "crypto/rand" - "database/sql" "encoding/base64" "fmt" ) @@ -37,14 +36,12 @@ func (db *database) EnsureJWTSecret() (string, error) { // 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) } @@ -60,13 +57,6 @@ func (db *database) GetSystemSetting(key string) (string, error) { 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 } @@ -86,9 +76,6 @@ func (db *database) SetSystemSetting(key, value string) error { key, value, value) if err != nil { - log.Error("failed to store system setting", - "error", err, - "key", key) return fmt.Errorf("failed to store system setting: %w", err) } @@ -104,9 +91,6 @@ func generateRandomSecret(bytes int) (string, error) { b := make([]byte, bytes) _, err := rand.Read(b) if err != nil { - log.Error("failed to generate random bytes", - "error", err, - "bytes", bytes) return "", fmt.Errorf("failed to generate random bytes: %w", err) } @@ -125,7 +109,6 @@ func (db *database) GetSystemStats() (*UserStats, error) { // Get total users err := db.QueryRow("SELECT COUNT(*) FROM users").Scan(&stats.TotalUsers) if err != nil { - 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) @@ -133,7 +116,6 @@ func (db *database) GetSystemStats() (*UserStats, error) { // Get total workspaces err = db.QueryRow("SELECT COUNT(*) FROM workspaces").Scan(&stats.TotalWorkspaces) if err != nil { - 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) @@ -145,14 +127,8 @@ func (db *database) GetSystemStats() (*UserStats, error) { WHERE created_at > datetime('now', '-30 days')`). Scan(&stats.ActiveUsers) if err != nil { - 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 15b19d1..5f23fe8 100644 --- a/server/internal/db/users.go +++ b/server/internal/db/users.go @@ -9,13 +9,12 @@ import ( // 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", + log.Debug("creating new user", "email", user.Email, "role", user.Role) tx, err := db.Begin() if err != nil { - log.Error("failed to begin transaction", "error", err) return nil, fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() @@ -25,15 +24,11 @@ func (db *database) CreateUser(user *models.User) (*models.User, error) { VALUES (?, ?, ?, ?)`, user.Email, user.DisplayName, user.PasswordHash, user.Role) if err != nil { - 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 { - 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) @@ -41,9 +36,6 @@ func (db *database) CreateUser(user *models.User) (*models.User, error) { // Retrieve the created_at timestamp err = tx.QueryRow("SELECT created_at FROM users WHERE id = ?", user.ID).Scan(&user.CreatedAt) if err != nil { - log.Error("failed to get created timestamp", - "error", err, - "user_id", user.ID) return nil, fmt.Errorf("failed to get created timestamp: %w", err) } @@ -58,9 +50,6 @@ func (db *database) CreateUser(user *models.User) (*models.User, error) { // Create workspace with settings err = db.createWorkspaceTx(tx, defaultWorkspace) if err != nil { - log.Error("failed to create default workspace", - "error", err, - "user_id", user.ID) return nil, fmt.Errorf("failed to create default workspace: %w", err) } @@ -70,23 +59,16 @@ func (db *database) CreateUser(user *models.User) (*models.User, error) { "workspace_id", defaultWorkspace.ID) _, err = tx.Exec("UPDATE users SET last_workspace_id = ? WHERE id = ?", defaultWorkspace.ID, user.ID) if err != nil { - 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 { - 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", + log.Info("user created", "user_id", user.ID, "email", user.Email, "workspace_id", defaultWorkspace.ID) @@ -115,15 +97,11 @@ func (db *database) createWorkspaceTx(tx *sql.Tx, workspace *models.Workspace) e workspace.GitCommitName, workspace.GitCommitEmail, ) if err != nil { - 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 { - log.Error("failed to get workspace ID", "error", err) return fmt.Errorf("failed to get workspace ID: %w", err) } workspace.ID = int(id) @@ -153,9 +131,6 @@ func (db *database) GetUserByID(id int) (*models.User, error) { 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) } @@ -182,9 +157,6 @@ func (db *database) GetUserByEmail(email string) (*models.User, error) { return nil, fmt.Errorf("user not found") } if err != nil { - log.Error("failed to fetch user", - "error", err, - "email", email) return nil, fmt.Errorf("failed to fetch user: %w", err) } @@ -194,10 +166,6 @@ func (db *database) GetUserByEmail(email string) (*models.User, error) { func (db *database) UpdateUser(user *models.User) error { 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 = ? @@ -206,17 +174,11 @@ func (db *database) UpdateUser(user *models.User) error { 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) } @@ -225,7 +187,7 @@ func (db *database) UpdateUser(user *models.User) error { return fmt.Errorf("user not found") } - log.Info("user updated successfully", "user_id", user.ID) + log.Info("user updated", "user_id", user.ID) return nil } @@ -240,7 +202,6 @@ func (db *database) GetAllUsers() ([]*models.User, error) { FROM users ORDER BY id ASC`) if err != nil { - log.Error("failed to query users", "error", err) return nil, fmt.Errorf("failed to query users: %w", err) } defer rows.Close() @@ -253,7 +214,6 @@ func (db *database) GetAllUsers() ([]*models.User, error) { &user.CreatedAt, &user.LastWorkspaceID, ) if err != nil { - log.Error("failed to scan user row", "error", err) return nil, fmt.Errorf("failed to scan user row: %w", err) } users = append(users, user) @@ -271,7 +231,6 @@ func (db *database) UpdateLastWorkspace(userID int, workspaceName string) error tx, err := db.Begin() if err != nil { - log.Error("failed to begin transaction", "error", err) return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() @@ -280,30 +239,21 @@ func (db *database) UpdateLastWorkspace(userID int, workspaceName string) error err = tx.QueryRow("SELECT id FROM workspaces WHERE user_id = ? AND name = ?", userID, workspaceName).Scan(&workspaceID) if err != nil { - 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) if err != nil { - 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) } 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", + log.Info("last workspace updated", "user_id", userID, "workspace_id", workspaceID) return nil @@ -311,11 +261,10 @@ func (db *database) UpdateLastWorkspace(userID int, workspaceName string) error func (db *database) DeleteUser(id int) error { log := getLogger().WithGroup("users") - log.Info("deleting user", "user_id", id) + log.Debug("deleting user", "user_id", id) tx, err := db.Begin() if err != nil { - log.Error("failed to begin transaction", "error", err) return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() @@ -324,15 +273,11 @@ func (db *database) DeleteUser(id int) error { log.Debug("deleting user workspaces", "user_id", id) result, err := tx.Exec("DELETE FROM workspaces WHERE user_id = ?", id) if err != nil { - 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) } @@ -340,15 +285,11 @@ func (db *database) DeleteUser(id int) error { log.Debug("deleting user record", "user_id", id) result, err = tx.Exec("DELETE FROM users WHERE id = ?", id) if err != nil { - log.Error("failed to delete user", - "error", err, - "user_id", id) return fmt.Errorf("failed to delete user: %w", err) } 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) } @@ -359,11 +300,10 @@ func (db *database) DeleteUser(id int) error { 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", + log.Info("user deleted", "user_id", id, "workspaces_deleted", workspacesDeleted) return nil @@ -387,9 +327,6 @@ func (db *database) GetLastWorkspaceName(userID int) (string, error) { 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) } @@ -407,7 +344,6 @@ func (db *database) CountAdminUsers() (int, error) { var count int err := db.QueryRow("SELECT COUNT(*) FROM users WHERE role = 'admin'").Scan(&count) if err != nil { - log.Error("failed to count admin users", "error", err) return 0, fmt.Errorf("failed to count admin users: %w", err) } diff --git a/server/internal/db/workspaces.go b/server/internal/db/workspaces.go index 54b051d..58616f0 100644 --- a/server/internal/db/workspaces.go +++ b/server/internal/db/workspaces.go @@ -9,7 +9,7 @@ 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", + log.Debug("creating new workspace", "user_id", workspace.UserID, "name", workspace.Name, "git_enabled", workspace.GitEnabled) @@ -23,7 +23,6 @@ func (db *database) CreateWorkspace(workspace *models.Workspace) error { // 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) } @@ -39,18 +38,16 @@ func (db *database) CreateWorkspace(workspace *models.Workspace) error { workspace.GitAutoCommit, workspace.GitCommitMsgTemplate, workspace.GitCommitName, workspace.GitCommitEmail, ) if err != nil { - log.Error("failed to insert workspace", "error", err) return fmt.Errorf("failed to insert workspace: %w", err) } id, err := result.LastInsertId() if err != nil { - 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", + log.Info("workspace created", "workspace_id", workspace.ID, "user_id", workspace.UserID) return nil @@ -87,22 +84,16 @@ func (db *database) GetWorkspaceByID(id int) (*models.Workspace, error) { return nil, fmt.Errorf("workspace not found") } if err != nil { - 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", + log.Debug("workspace retrieved", "workspace_id", id, "user_id", workspace.UserID) return workspace, nil @@ -143,19 +134,12 @@ func (db *database) GetWorkspaceByName(userID int, workspaceName string) (*model return nil, fmt.Errorf("workspace not found") } if err != nil { - 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) } @@ -168,7 +152,7 @@ func (db *database) GetWorkspaceByName(userID int, workspaceName string) (*model // UpdateWorkspace updates a workspace record in the database func (db *database) UpdateWorkspace(workspace *models.Workspace) error { log := getLogger().WithGroup("workspaces") - log.Info("updating workspace", + log.Debug("updating workspace", "workspace_id", workspace.ID, "user_id", workspace.UserID, "git_enabled", workspace.GitEnabled) @@ -176,7 +160,6 @@ func (db *database) UpdateWorkspace(workspace *models.Workspace) error { // 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) } @@ -212,17 +195,11 @@ func (db *database) UpdateWorkspace(workspace *models.Workspace) error { workspace.UserID, ) 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) } @@ -233,7 +210,7 @@ func (db *database) UpdateWorkspace(workspace *models.Workspace) error { return fmt.Errorf("workspace not found") } - log.Info("workspace updated successfully", + log.Debug("workspace updated", "workspace_id", workspace.ID, "user_id", workspace.UserID) return nil @@ -256,9 +233,6 @@ func (db *database) GetWorkspacesByUserID(userID int) ([]*models.Workspace, erro userID, ) if err != nil { - log.Error("failed to query workspaces", - "error", err, - "user_id", userID) return nil, fmt.Errorf("failed to query workspaces: %w", err) } defer rows.Close() @@ -275,16 +249,12 @@ func (db *database) GetWorkspacesByUserID(userID int) ([]*models.Workspace, erro &workspace.GitCommitName, &workspace.GitCommitEmail, ) if err != nil { - 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) } @@ -292,9 +262,6 @@ func (db *database) GetWorkspacesByUserID(userID int) ([]*models.Workspace, erro } 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) } @@ -307,7 +274,7 @@ func (db *database) GetWorkspacesByUserID(userID int) ([]*models.Workspace, erro // UpdateWorkspaceSettings updates only the settings portion of a workspace func (db *database) UpdateWorkspaceSettings(workspace *models.Workspace) error { log := getLogger().WithGroup("workspaces") - log.Info("updating workspace settings", + log.Debug("updating workspace settings", "workspace_id", workspace.ID, "git_enabled", workspace.GitEnabled) @@ -340,17 +307,11 @@ func (db *database) UpdateWorkspaceSettings(workspace *models.Workspace) error { workspace.ID, ) 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) } @@ -360,7 +321,7 @@ func (db *database) UpdateWorkspaceSettings(workspace *models.Workspace) error { return fmt.Errorf("workspace not found") } - log.Info("workspace settings updated successfully", + log.Info("workspace settings updated", "workspace_id", workspace.ID) return nil } @@ -368,21 +329,15 @@ func (db *database) UpdateWorkspaceSettings(workspace *models.Workspace) error { // DeleteWorkspace removes a workspace record from the database func (db *database) DeleteWorkspace(id int) error { log := getLogger().WithGroup("workspaces") - log.Info("deleting workspace", "workspace_id", id) + log.Debug("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) } @@ -391,7 +346,7 @@ func (db *database) DeleteWorkspace(id int) error { return fmt.Errorf("workspace not found") } - log.Info("workspace deleted successfully", "workspace_id", id) + log.Info("workspace deleted", "workspace_id", id) return nil } @@ -402,17 +357,11 @@ func (db *database) DeleteWorkspaceTx(tx *sql.Tx, id int) error { 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) } @@ -437,18 +386,11 @@ func (db *database) UpdateLastWorkspaceTx(tx *sql.Tx, userID, workspaceID int) e 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) } @@ -474,17 +416,11 @@ func (db *database) UpdateLastOpenedFile(workspaceID int, filePath string) error 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) } @@ -513,9 +449,6 @@ func (db *database) GetLastOpenedFile(workspaceID int) (string, error) { 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) } @@ -545,7 +478,6 @@ func (db *database) GetAllWorkspaces() ([]*models.Workspace, error) { FROM workspaces`, ) if err != nil { - log.Error("failed to query workspaces", "error", err) return nil, fmt.Errorf("failed to query workspaces: %w", err) } defer rows.Close() @@ -562,16 +494,12 @@ func (db *database) GetAllWorkspaces() ([]*models.Workspace, error) { &workspace.GitCommitName, &workspace.GitCommitEmail, ) if err != nil { - 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) } @@ -579,7 +507,6 @@ func (db *database) GetAllWorkspaces() ([]*models.Workspace, error) { } if err = rows.Err(); err != nil { - log.Error("error iterating workspace rows", "error", err) return nil, fmt.Errorf("error iterating workspace rows: %w", err) } From 3edce8a0b947585c64f681dc2d37090ad39ff40f Mon Sep 17 00:00:00 2001 From: LordMathis Date: Sun, 15 Dec 2024 18:03:04 +0100 Subject: [PATCH 10/22] Add logging to auth package --- server/internal/auth/cookies.go | 46 ++++++++++++++++++++++++ server/internal/auth/jwt.go | 48 +++++++++++++++++++++---- server/internal/auth/jwt_test.go | 2 -- server/internal/auth/middleware.go | 33 ++++++++++++++++-- server/internal/auth/session.go | 56 ++++++++++++++++++++++++++++-- 5 files changed, 172 insertions(+), 13 deletions(-) diff --git a/server/internal/auth/cookies.go b/server/internal/auth/cookies.go index 7249bad..3eb3fbb 100644 --- a/server/internal/auth/cookies.go +++ b/server/internal/auth/cookies.go @@ -3,8 +3,22 @@ package auth import ( "net/http" + "novamd/internal/logging" ) +var logger logging.Logger + +func getAuthLogger() logging.Logger { + if logger == nil { + logger = logging.WithGroup("auth") + } + return logger +} + +func getCookieLogger() logging.Logger { + return getAuthLogger().WithGroup("cookie") +} + // CookieManager interface defines methods for generating cookies type CookieManager interface { GenerateAccessTokenCookie(token string) *http.Cookie @@ -22,6 +36,8 @@ type cookieManager struct { // NewCookieService creates a new cookie service func NewCookieService(isDevelopment bool, domain string) CookieManager { + log := getCookieLogger() + secure := !isDevelopment var sameSite http.SameSite @@ -31,6 +47,11 @@ func NewCookieService(isDevelopment bool, domain string) CookieManager { sameSite = http.SameSiteStrictMode } + log.Debug("creating cookie service", + "secure", secure, + "sameSite", sameSite, + "domain", domain) + return &cookieManager{ Domain: domain, Secure: secure, @@ -40,6 +61,12 @@ func NewCookieService(isDevelopment bool, domain string) CookieManager { // GenerateAccessTokenCookie creates a new cookie for the access token func (c *cookieManager) GenerateAccessTokenCookie(token string) *http.Cookie { + log := getCookieLogger() + log.Debug("generating access token cookie", + "secure", c.Secure, + "sameSite", c.SameSite, + "maxAge", 900) + return &http.Cookie{ Name: "access_token", Value: token, @@ -53,6 +80,12 @@ func (c *cookieManager) GenerateAccessTokenCookie(token string) *http.Cookie { // GenerateRefreshTokenCookie creates a new cookie for the refresh token func (c *cookieManager) GenerateRefreshTokenCookie(token string) *http.Cookie { + log := getCookieLogger() + log.Debug("generating refresh token cookie", + "secure", c.Secure, + "sameSite", c.SameSite, + "maxAge", 604800) + return &http.Cookie{ Name: "refresh_token", Value: token, @@ -66,6 +99,13 @@ func (c *cookieManager) GenerateRefreshTokenCookie(token string) *http.Cookie { // GenerateCSRFCookie creates a new cookie for the CSRF token func (c *cookieManager) GenerateCSRFCookie(token string) *http.Cookie { + log := getCookieLogger() + log.Debug("generating CSRF cookie", + "secure", c.Secure, + "sameSite", c.SameSite, + "maxAge", 900, + "httpOnly", false) + return &http.Cookie{ Name: "csrf_token", Value: token, @@ -79,6 +119,12 @@ func (c *cookieManager) GenerateCSRFCookie(token string) *http.Cookie { // InvalidateCookie creates a new cookie with a MaxAge of -1 to invalidate the cookie func (c *cookieManager) InvalidateCookie(cookieType string) *http.Cookie { + log := getCookieLogger() + log.Debug("invalidating cookie", + "type", cookieType, + "secure", c.Secure, + "sameSite", c.SameSite) + return &http.Cookie{ Name: cookieType, Value: "", diff --git a/server/internal/auth/jwt.go b/server/internal/auth/jwt.go index 9df0de0..7746b6c 100644 --- a/server/internal/auth/jwt.go +++ b/server/internal/auth/jwt.go @@ -4,11 +4,16 @@ package auth import ( "crypto/rand" "fmt" + "novamd/internal/logging" "time" "github.com/golang-jwt/jwt/v5" ) +func getJWTLogger() logging.Logger { + return getAuthLogger().WithGroup("jwt") +} + // TokenType represents the type of JWT token (access or refresh) type TokenType string @@ -47,16 +52,26 @@ type jwtService struct { // NewJWTService creates a new JWT service with the provided configuration // Returns an error if the signing key is missing func NewJWTService(config JWTConfig) (JWTManager, error) { + log := getJWTLogger() + if config.SigningKey == "" { return nil, fmt.Errorf("signing key is required") } + // Set default expiry times if not provided if config.AccessTokenExpiry == 0 { - config.AccessTokenExpiry = 15 * time.Minute // Default to 15 minutes + config.AccessTokenExpiry = 15 * time.Minute + log.Debug("using default access token expiry", "expiry", config.AccessTokenExpiry) } if config.RefreshTokenExpiry == 0 { - config.RefreshTokenExpiry = 7 * 24 * time.Hour // Default to 7 days + config.RefreshTokenExpiry = 7 * 24 * time.Hour + log.Debug("using default refresh token expiry", "expiry", config.RefreshTokenExpiry) } + + log.Info("initialized JWT service", + "accessExpiry", config.AccessTokenExpiry, + "refreshExpiry", config.RefreshTokenExpiry) + return &jwtService{config: config}, nil } @@ -72,6 +87,7 @@ func (s *jwtService) GenerateRefreshToken(userID int, role, sessionID string) (s // generateToken is an internal helper function that creates a new JWT token func (s *jwtService) generateToken(userID int, role string, sessionID string, tokenType TokenType, expiry time.Duration) (string, error) { + log := getJWTLogger() now := time.Now() // Add a random nonce to ensure uniqueness @@ -93,11 +109,24 @@ func (s *jwtService) generateToken(userID int, role string, sessionID string, to } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - return token.SignedString([]byte(s.config.SigningKey)) + signedToken, err := token.SignedString([]byte(s.config.SigningKey)) + if err != nil { + return "", err + } + + log.Debug("generated JWT token", + "userId", userID, + "role", role, + "tokenType", tokenType, + "expiresAt", claims.ExpiresAt) + + return signedToken, nil } // ValidateToken validates and parses a JWT token func (s *jwtService) ValidateToken(tokenString string) (*Claims, error) { + log := getJWTLogger() + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { // Validate the signing method if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { @@ -110,9 +139,16 @@ func (s *jwtService) ValidateToken(tokenString string) (*Claims, error) { return nil, fmt.Errorf("invalid token: %w", err) } - if claims, ok := token.Claims.(*Claims); ok && token.Valid { - return claims, nil + claims, ok := token.Claims.(*Claims) + if !ok || !token.Valid { + return nil, fmt.Errorf("invalid token claims") } - return nil, fmt.Errorf("invalid token claims") + log.Debug("token validated", + "userId", claims.UserID, + "role", claims.Role, + "tokenType", claims.Type, + "expiresAt", claims.ExpiresAt) + + return claims, nil } diff --git a/server/internal/auth/jwt_test.go b/server/internal/auth/jwt_test.go index 14b1a50..57b9e7e 100644 --- a/server/internal/auth/jwt_test.go +++ b/server/internal/auth/jwt_test.go @@ -8,8 +8,6 @@ import ( "novamd/internal/auth" ) -// jwt_test.go tests - func TestNewJWTService(t *testing.T) { testCases := []struct { name string diff --git a/server/internal/auth/middleware.go b/server/internal/auth/middleware.go index 6748fa3..7e8809f 100644 --- a/server/internal/auth/middleware.go +++ b/server/internal/auth/middleware.go @@ -3,10 +3,14 @@ package auth import ( "crypto/subtle" "net/http" - "novamd/internal/context" + "novamd/internal/logging" ) +func getMiddlewareLogger() logging.Logger { + return getAuthLogger().WithGroup("middleware") +} + // Middleware handles JWT authentication for protected routes type Middleware struct { jwtManager JWTManager @@ -16,6 +20,9 @@ type Middleware struct { // NewMiddleware creates a new authentication middleware func NewMiddleware(jwtManager JWTManager, sessionManager SessionManager, cookieManager CookieManager) *Middleware { + log := getMiddlewareLogger() + log.Info("initialized auth middleware") + return &Middleware{ jwtManager: jwtManager, sessionManager: sessionManager, @@ -26,7 +33,9 @@ func NewMiddleware(jwtManager JWTManager, sessionManager SessionManager, cookieM // Authenticate middleware validates JWT tokens and sets user information in context func (m *Middleware) Authenticate(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Extract token from Authorization header + log := getMiddlewareLogger() + + // Extract token from cookie cookie, err := r.Cookie("access_token") if err != nil { http.Error(w, "Unauthorized", http.StatusUnauthorized) @@ -82,6 +91,12 @@ func (m *Middleware) Authenticate(next http.Handler) http.Handler { UserRole: claims.Role, } + log.Debug("authentication completed", + "userId", claims.UserID, + "role", claims.Role, + "method", r.Method, + "path", r.URL.Path) + // Add context to request and continue next.ServeHTTP(w, context.WithHandlerContext(r, hctx)) }) @@ -91,6 +106,8 @@ func (m *Middleware) Authenticate(next http.Handler) http.Handler { func (m *Middleware) RequireRole(role string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log := getMiddlewareLogger() + ctx, ok := context.GetRequestContext(w, r) if !ok { return @@ -101,6 +118,11 @@ func (m *Middleware) RequireRole(role string) func(http.Handler) http.Handler { return } + log.Debug("role requirement satisfied", + "requiredRole", role, + "userRole", ctx.UserRole, + "path", r.URL.Path) + next.ServeHTTP(w, r) }) } @@ -109,6 +131,8 @@ func (m *Middleware) RequireRole(role string) func(http.Handler) http.Handler { // RequireWorkspaceAccess returns a middleware that ensures the user has access to the workspace func (m *Middleware) RequireWorkspaceAccess(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log := getMiddlewareLogger() + ctx, ok := context.GetRequestContext(w, r) if !ok { return @@ -126,6 +150,11 @@ func (m *Middleware) RequireWorkspaceAccess(next http.Handler) http.Handler { return } + log.Debug("workspace access granted", + "userId", ctx.UserID, + "workspaceId", ctx.Workspace.ID, + "path", r.URL.Path) + next.ServeHTTP(w, r) }) } diff --git a/server/internal/auth/session.go b/server/internal/auth/session.go index bb3df67..ca7e682 100644 --- a/server/internal/auth/session.go +++ b/server/internal/auth/session.go @@ -3,12 +3,17 @@ package auth import ( "fmt" "novamd/internal/db" + "novamd/internal/logging" "novamd/internal/models" "time" "github.com/google/uuid" ) +func getSessionLogger() logging.Logger { + return getAuthLogger().WithGroup("session") +} + // SessionManager is an interface for managing user sessions type SessionManager interface { CreateSession(userID int, role string) (*models.Session, string, error) @@ -27,6 +32,9 @@ type sessionManager struct { // NewSessionService creates a new session service with the given database and JWT manager // revive:disable:unexported-return func NewSessionService(db db.SessionStore, jwtManager JWTManager) *sessionManager { + log := getSessionLogger() + log.Info("initialized session manager") + return &sessionManager{ db: db, jwtManager: jwtManager, @@ -35,6 +43,7 @@ func NewSessionService(db db.SessionStore, jwtManager JWTManager) *sessionManage // CreateSession creates a new user session for a user with the given userID and role func (s *sessionManager) CreateSession(userID int, role string) (*models.Session, string, error) { + log := getSessionLogger() // Generate a new session ID sessionID := uuid.New().String() @@ -70,11 +79,19 @@ func (s *sessionManager) CreateSession(userID int, role string) (*models.Session return nil, "", err } + log.Debug("created new session", + "userId", userID, + "role", role, + "sessionId", sessionID, + "expiresAt", claims.ExpiresAt.Time) + return session, accessToken, nil } // RefreshSession creates a new access token using a refreshToken func (s *sessionManager) RefreshSession(refreshToken string) (string, error) { + log := getSessionLogger() + // Get session from database first session, err := s.db.GetSessionByRefreshToken(refreshToken) if err != nil { @@ -93,11 +110,22 @@ func (s *sessionManager) RefreshSession(refreshToken string) (string, error) { } // Generate a new access token - return s.jwtManager.GenerateAccessToken(claims.UserID, claims.Role, session.ID) + newToken, err := s.jwtManager.GenerateAccessToken(claims.UserID, claims.Role, session.ID) + if err != nil { + return "", err + } + + log.Debug("refreshed session", + "userId", claims.UserID, + "role", claims.Role, + "sessionId", session.ID) + + return newToken, nil } // ValidateSession checks if a session with the given sessionID is valid func (s *sessionManager) ValidateSession(sessionID string) (*models.Session, error) { + log := getSessionLogger() // Get the session from the database session, err := s.db.GetSessionByID(sessionID) @@ -105,21 +133,43 @@ func (s *sessionManager) ValidateSession(sessionID string) (*models.Session, err return nil, fmt.Errorf("failed to get session: %w", err) } + log.Debug("validated session", + "sessionId", sessionID, + "userId", session.UserID, + "expiresAt", session.ExpiresAt) + return session, nil } // InvalidateSession removes a session with the given sessionID from the database func (s *sessionManager) InvalidateSession(token string) error { + log := getSessionLogger() + // Parse the JWT to get the session info claims, err := s.jwtManager.ValidateToken(token) if err != nil { return fmt.Errorf("invalid token: %w", err) } - return s.db.DeleteSession(claims.ID) + if err := s.db.DeleteSession(claims.ID); err != nil { + return err + } + + log.Debug("invalidated session", + "sessionId", claims.ID, + "userId", claims.UserID) + + return nil } // CleanExpiredSessions removes all expired sessions from the database func (s *sessionManager) CleanExpiredSessions() error { - return s.db.CleanExpiredSessions() + log := getSessionLogger() + + if err := s.db.CleanExpiredSessions(); err != nil { + return err + } + + log.Info("cleaned expired sessions") + return nil } From e7a48fcd27eb5e77a0aef8a70b92a43f2778be44 Mon Sep 17 00:00:00 2001 From: LordMathis Date: Mon, 16 Dec 2024 20:56:17 +0100 Subject: [PATCH 11/22] Add logging to context --- server/internal/context/context.go | 13 +++++++++++++ server/internal/context/middleware.go | 22 ++++++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/server/internal/context/context.go b/server/internal/context/context.go index dda7eeb..e40353a 100644 --- a/server/internal/context/context.go +++ b/server/internal/context/context.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "net/http" + "novamd/internal/logging" "novamd/internal/models" ) @@ -28,10 +29,22 @@ type HandlerContext struct { Workspace *models.Workspace // Optional, only set for workspace routes } +var logger logging.Logger + +func getLogger() logging.Logger { + if logger == nil { + logger = logging.WithGroup("context") + } + return logger +} + // GetRequestContext retrieves the handler context from the request func GetRequestContext(w http.ResponseWriter, r *http.Request) (*HandlerContext, bool) { ctx := r.Context().Value(HandlerContextKey) if ctx == nil { + getLogger().Error("missing handler context in request", + "path", r.URL.Path, + "method", r.Method) http.Error(w, "Internal server error", http.StatusInternalServerError) return nil, false } diff --git a/server/internal/context/middleware.go b/server/internal/context/middleware.go index c916e96..a079eff 100644 --- a/server/internal/context/middleware.go +++ b/server/internal/context/middleware.go @@ -13,6 +13,9 @@ func WithUserContextMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { claims, err := GetUserFromContext(r.Context()) if err != nil { + getLogger().Error("failed to get user from context", + "error", err, + "path", r.URL.Path) http.Error(w, "Unauthorized", http.StatusUnauthorized) return } @@ -22,6 +25,11 @@ func WithUserContextMiddleware(next http.Handler) http.Handler { UserRole: claims.Role, } + getLogger().Debug("user context extracted from claims", + "userID", claims.UserID, + "role", claims.Role, + "path", r.URL.Path) + r = WithHandlerContext(r, hctx) next.ServeHTTP(w, r) }) @@ -39,11 +47,21 @@ func WithWorkspaceContextMiddleware(db db.WorkspaceReader) func(http.Handler) ht workspaceName := chi.URLParam(r, "workspaceName") workspace, err := db.GetWorkspaceByName(ctx.UserID, workspaceName) if err != nil { - http.Error(w, "Workspace not found", http.StatusNotFound) + getLogger().Error("failed to get workspace", + "error", err, + "userID", ctx.UserID, + "workspace", workspaceName, + "path", r.URL.Path) + http.Error(w, "Failed to get workspace", http.StatusNotFound) return } - // Update existing context with workspace + getLogger().Debug("workspace context added", + "userID", ctx.UserID, + "workspaceID", workspace.ID, + "workspaceName", workspace.Name, + "path", r.URL.Path) + ctx.Workspace = workspace r = WithHandlerContext(r, ctx) next.ServeHTTP(w, r) From 51004d980d9884434016af546ecaa16e55be4b1e Mon Sep 17 00:00:00 2001 From: LordMathis Date: Mon, 16 Dec 2024 21:16:31 +0100 Subject: [PATCH 12/22] Implement logging for git package --- server/internal/git/client.go | 62 +++++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/server/internal/git/client.go b/server/internal/git/client.go index 24e09c1..e853cb1 100644 --- a/server/internal/git/client.go +++ b/server/internal/git/client.go @@ -7,6 +7,8 @@ import ( "path/filepath" "time" + "novamd/internal/logging" + "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" @@ -46,8 +48,23 @@ type client struct { repo *git.Repository } +var logger logging.Logger + +func getLogger() logging.Logger { + if logger == nil { + logger = logging.WithGroup("git") + } + return logger +} + // New creates a new git Client instance func New(url, username, token, workDir, commitName, commitEmail string) Client { + getLogger().Debug("creating new git client", + "url", url, + "username", username, + "workDir", workDir, + "commitName", commitName, + "commitEmail", commitEmail) return &client{ Config: Config{ URL: url, @@ -62,6 +79,10 @@ func New(url, username, token, workDir, commitName, commitEmail string) Client { // Clone clones the Git repository to the local directory func (c *client) Clone() error { + getLogger().Info("cloning git repository", + "url", c.URL, + "workDir", c.WorkDir) + auth := &http.BasicAuth{ Username: c.Username, Password: c.Token, @@ -73,11 +94,13 @@ func (c *client) Clone() error { Auth: auth, Progress: os.Stdout, }) - if err != nil { return fmt.Errorf("failed to clone repository: %w", err) } + getLogger().Info("repository cloned", + "url", c.URL, + "workDir", c.WorkDir) return nil } @@ -87,6 +110,9 @@ func (c *client) Pull() error { return fmt.Errorf("repository not initialized") } + getLogger().Debug("pulling repository changes", + "workDir", c.WorkDir) + w, err := c.repo.Worktree() if err != nil { return fmt.Errorf("failed to get worktree: %w", err) @@ -101,11 +127,17 @@ func (c *client) Pull() error { Auth: auth, Progress: os.Stdout, }) - if err != nil && err != git.NoErrAlreadyUpToDate { return fmt.Errorf("failed to pull changes: %w", err) } + if err == git.NoErrAlreadyUpToDate { + getLogger().Debug("repository already up to date", + "workDir", c.WorkDir) + } else { + getLogger().Info("pulled repository changes", + "workDir", c.WorkDir) + } return nil } @@ -115,6 +147,10 @@ func (c *client) Commit(message string) (CommitHash, error) { return CommitHash(plumbing.ZeroHash), fmt.Errorf("repository not initialized") } + getLogger().Debug("preparing to commit changes", + "workDir", c.WorkDir, + "message", message) + w, err := c.repo.Worktree() if err != nil { return CommitHash(plumbing.ZeroHash), fmt.Errorf("failed to get worktree: %w", err) @@ -136,6 +172,10 @@ func (c *client) Commit(message string) (CommitHash, error) { return CommitHash(plumbing.ZeroHash), fmt.Errorf("failed to commit changes: %w", err) } + getLogger().Info("changes committed", + "hash", hash.String(), + "workDir", c.WorkDir, + "message", message) return CommitHash(hash), nil } @@ -145,6 +185,9 @@ func (c *client) Push() error { return fmt.Errorf("repository not initialized") } + getLogger().Debug("pushing repository changes", + "workDir", c.WorkDir) + auth := &http.BasicAuth{ Username: c.Username, Password: c.Token, @@ -154,17 +197,28 @@ func (c *client) Push() error { Auth: auth, Progress: os.Stdout, }) - if err != nil && err != git.NoErrAlreadyUpToDate { return fmt.Errorf("failed to push changes: %w", err) } + if err == git.NoErrAlreadyUpToDate { + getLogger().Debug("remote already up to date", + "workDir", c.WorkDir) + } else { + getLogger().Info("pushed repository changes", + "workDir", c.WorkDir) + } return nil } // EnsureRepo ensures the local repository is cloned and up-to-date func (c *client) EnsureRepo() error { + getLogger().Debug("ensuring repository exists and is up to date", + "workDir", c.WorkDir) + if _, err := os.Stat(filepath.Join(c.WorkDir, ".git")); os.IsNotExist(err) { + getLogger().Info("repository not found, initiating clone", + "workDir", c.WorkDir) return c.Clone() } @@ -174,5 +228,7 @@ func (c *client) EnsureRepo() error { return fmt.Errorf("failed to open existing repository: %w", err) } + getLogger().Debug("repository opened, pulling latest changes", + "workDir", c.WorkDir) return c.Pull() } From b00f01f03372cf20d43a042f281f4f138fea119f Mon Sep 17 00:00:00 2001 From: LordMathis Date: Mon, 16 Dec 2024 21:25:06 +0100 Subject: [PATCH 13/22] Add logging to secrets package --- server/internal/secrets/secrets.go | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/server/internal/secrets/secrets.go b/server/internal/secrets/secrets.go index 2eab8d1..9a66824 100644 --- a/server/internal/secrets/secrets.go +++ b/server/internal/secrets/secrets.go @@ -8,6 +8,8 @@ import ( "encoding/base64" "fmt" "io" + + "novamd/internal/logging" ) // Service is an interface for encrypting and decrypting strings @@ -20,8 +22,19 @@ type encryptor struct { gcm cipher.AEAD } +var logger logging.Logger + +func getLogger() logging.Logger { + if logger == nil { + logger = logging.WithGroup("secrets") + } + return logger +} + // ValidateKey checks if the provided base64-encoded key is suitable for AES-256 func ValidateKey(key string) error { + log := getLogger() + log.Debug("validating encryption key") _, err := decodeAndValidateKey(key) return err } @@ -53,6 +66,9 @@ func decodeAndValidateKey(key string) ([]byte, error) { // NewService creates a new Encryptor instance with the provided base64-encoded key func NewService(key string) (Service, error) { + log := getLogger() + log.Debug("creating new encryption service") + keyBytes, err := decodeAndValidateKey(key) if err != nil { return nil, err @@ -68,12 +84,16 @@ func NewService(key string) (Service, error) { return nil, fmt.Errorf("failed to create GCM: %w", err) } + log.Info("encryption service created") return &encryptor{gcm: gcm}, nil } // Encrypt encrypts the plaintext using AES-256-GCM func (e *encryptor) Encrypt(plaintext string) (string, error) { + log := getLogger() + if plaintext == "" { + log.Debug("empty plaintext provided, returning empty string") return "", nil } @@ -83,12 +103,18 @@ func (e *encryptor) Encrypt(plaintext string) (string, error) { } ciphertext := e.gcm.Seal(nonce, nonce, []byte(plaintext), nil) - return base64.StdEncoding.EncodeToString(ciphertext), nil + encoded := base64.StdEncoding.EncodeToString(ciphertext) + + log.Debug("data encrypted", "inputLength", len(plaintext), "outputLength", len(encoded)) + return encoded, nil } // Decrypt decrypts the ciphertext using AES-256-GCM func (e *encryptor) Decrypt(ciphertext string) (string, error) { + log := getLogger() + if ciphertext == "" { + log.Debug("empty ciphertext provided, returning empty string") return "", nil } @@ -108,5 +134,6 @@ func (e *encryptor) Decrypt(ciphertext string) (string, error) { return "", err } + log.Debug("data decrypted", "inputLength", len(ciphertext), "outputLength", len(plaintext)) return string(plaintext), nil } From 03e78c3e6bce9387ff2c3327fc12f54b942ff2e4 Mon Sep 17 00:00:00 2001 From: LordMathis Date: Mon, 16 Dec 2024 21:25:17 +0100 Subject: [PATCH 14/22] Harmonize logging --- server/internal/context/middleware.go | 10 +++++--- server/internal/git/client.go | 35 +++++++++++++++++---------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/server/internal/context/middleware.go b/server/internal/context/middleware.go index a079eff..d2da13c 100644 --- a/server/internal/context/middleware.go +++ b/server/internal/context/middleware.go @@ -10,10 +10,11 @@ import ( // WithUserContextMiddleware extracts user information from JWT claims // and adds it to the request context func WithUserContextMiddleware(next http.Handler) http.Handler { + log := getLogger() return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { claims, err := GetUserFromContext(r.Context()) if err != nil { - getLogger().Error("failed to get user from context", + log.Error("failed to get user from context", "error", err, "path", r.URL.Path) http.Error(w, "Unauthorized", http.StatusUnauthorized) @@ -25,7 +26,7 @@ func WithUserContextMiddleware(next http.Handler) http.Handler { UserRole: claims.Role, } - getLogger().Debug("user context extracted from claims", + log.Debug("user context extracted from claims", "userID", claims.UserID, "role", claims.Role, "path", r.URL.Path) @@ -38,6 +39,7 @@ func WithUserContextMiddleware(next http.Handler) http.Handler { // WithWorkspaceContextMiddleware adds workspace information to the request context func WithWorkspaceContextMiddleware(db db.WorkspaceReader) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { + log := getLogger() return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx, ok := GetRequestContext(w, r) if !ok { @@ -47,7 +49,7 @@ func WithWorkspaceContextMiddleware(db db.WorkspaceReader) func(http.Handler) ht workspaceName := chi.URLParam(r, "workspaceName") workspace, err := db.GetWorkspaceByName(ctx.UserID, workspaceName) if err != nil { - getLogger().Error("failed to get workspace", + log.Error("failed to get workspace", "error", err, "userID", ctx.UserID, "workspace", workspaceName, @@ -56,7 +58,7 @@ func WithWorkspaceContextMiddleware(db db.WorkspaceReader) func(http.Handler) ht return } - getLogger().Debug("workspace context added", + log.Debug("workspace context added", "userID", ctx.UserID, "workspaceID", workspace.ID, "workspaceName", workspace.Name, diff --git a/server/internal/git/client.go b/server/internal/git/client.go index e853cb1..cde61d2 100644 --- a/server/internal/git/client.go +++ b/server/internal/git/client.go @@ -79,7 +79,8 @@ func New(url, username, token, workDir, commitName, commitEmail string) Client { // Clone clones the Git repository to the local directory func (c *client) Clone() error { - getLogger().Info("cloning git repository", + log := getLogger() + log.Info("cloning git repository", "url", c.URL, "workDir", c.WorkDir) @@ -98,7 +99,7 @@ func (c *client) Clone() error { return fmt.Errorf("failed to clone repository: %w", err) } - getLogger().Info("repository cloned", + log.Info("repository cloned", "url", c.URL, "workDir", c.WorkDir) return nil @@ -106,11 +107,13 @@ func (c *client) Clone() error { // Pull pulls the latest changes from the remote repository func (c *client) Pull() error { + log := getLogger() + if c.repo == nil { return fmt.Errorf("repository not initialized") } - getLogger().Debug("pulling repository changes", + log.Debug("pulling repository changes", "workDir", c.WorkDir) w, err := c.repo.Worktree() @@ -132,10 +135,10 @@ func (c *client) Pull() error { } if err == git.NoErrAlreadyUpToDate { - getLogger().Debug("repository already up to date", + log.Debug("repository already up to date", "workDir", c.WorkDir) } else { - getLogger().Info("pulled repository changes", + log.Info("pulled repository changes", "workDir", c.WorkDir) } return nil @@ -143,11 +146,13 @@ func (c *client) Pull() error { // Commit commits the changes in the repository with the given message func (c *client) Commit(message string) (CommitHash, error) { + log := getLogger() + if c.repo == nil { return CommitHash(plumbing.ZeroHash), fmt.Errorf("repository not initialized") } - getLogger().Debug("preparing to commit changes", + log.Debug("preparing to commit changes", "workDir", c.WorkDir, "message", message) @@ -172,7 +177,7 @@ func (c *client) Commit(message string) (CommitHash, error) { return CommitHash(plumbing.ZeroHash), fmt.Errorf("failed to commit changes: %w", err) } - getLogger().Info("changes committed", + log.Info("changes committed", "hash", hash.String(), "workDir", c.WorkDir, "message", message) @@ -181,11 +186,13 @@ func (c *client) Commit(message string) (CommitHash, error) { // Push pushes the changes to the remote repository func (c *client) Push() error { + log := getLogger() + if c.repo == nil { return fmt.Errorf("repository not initialized") } - getLogger().Debug("pushing repository changes", + log.Debug("pushing repository changes", "workDir", c.WorkDir) auth := &http.BasicAuth{ @@ -202,10 +209,10 @@ func (c *client) Push() error { } if err == git.NoErrAlreadyUpToDate { - getLogger().Debug("remote already up to date", + log.Debug("remote already up to date", "workDir", c.WorkDir) } else { - getLogger().Info("pushed repository changes", + log.Info("pushed repository changes", "workDir", c.WorkDir) } return nil @@ -213,11 +220,13 @@ func (c *client) Push() error { // EnsureRepo ensures the local repository is cloned and up-to-date func (c *client) EnsureRepo() error { - getLogger().Debug("ensuring repository exists and is up to date", + log := getLogger() + + log.Debug("ensuring repository exists and is up to date", "workDir", c.WorkDir) if _, err := os.Stat(filepath.Join(c.WorkDir, ".git")); os.IsNotExist(err) { - getLogger().Info("repository not found, initiating clone", + log.Info("repository not found, initiating clone", "workDir", c.WorkDir) return c.Clone() } @@ -228,7 +237,7 @@ func (c *client) EnsureRepo() error { return fmt.Errorf("failed to open existing repository: %w", err) } - getLogger().Debug("repository opened, pulling latest changes", + log.Debug("repository opened, pulling latest changes", "workDir", c.WorkDir) return c.Pull() } From 032ae1354cc38578486a42f49e97d38ab6927844 Mon Sep 17 00:00:00 2001 From: LordMathis Date: Mon, 16 Dec 2024 22:38:21 +0100 Subject: [PATCH 15/22] Add logging to storage package --- server/internal/storage/files.go | 99 +++++++++++++++++++++++++-- server/internal/storage/filesystem.go | 10 +++ server/internal/storage/git.go | 50 +++++++++++++- server/internal/storage/service.go | 12 ++++ server/internal/storage/workspace.go | 16 +++++ 5 files changed, 177 insertions(+), 10 deletions(-) diff --git a/server/internal/storage/files.go b/server/internal/storage/files.go index 51fff58..8d8d781 100644 --- a/server/internal/storage/files.go +++ b/server/internal/storage/files.go @@ -1,5 +1,3 @@ -// Package storage provides functionalities to interact with the file system, -// including listing files, finding files by name, getting file content, saving files, and deleting files. package storage import ( @@ -32,8 +30,22 @@ type FileNode struct { // ListFilesRecursively returns a list of all files in the workspace directory and its subdirectories. // Workspace is identified by the given userID and workspaceID. func (s *Service) ListFilesRecursively(userID, workspaceID int) ([]FileNode, error) { + log := getLogger() + log.Debug("listing files recursively", + "userID", userID, + "workspaceID", workspaceID) + workspacePath := s.GetWorkspacePath(userID, workspaceID) - return s.walkDirectory(workspacePath, "") + nodes, err := s.walkDirectory(workspacePath, "") + if err != nil { + return nil, err + } + + log.Debug("file listing complete", + "userID", userID, + "workspaceID", workspaceID, + "nodeCount", len(nodes)) + return nodes, nil } // walkDirectory recursively walks the directory and returns a list of files and directories. @@ -104,6 +116,12 @@ func (s *Service) walkDirectory(dir, prefix string) ([]FileNode, error) { // Files are searched recursively in the workspace directory and its subdirectories. // Workspace is identified by the given userID and workspaceID. func (s *Service) FindFileByName(userID, workspaceID int, filename string) ([]string, error) { + log := getLogger() + log.Debug("searching for file by name", + "userID", userID, + "workspaceID", workspaceID, + "filename", filename) + var foundPaths []string workspacePath := s.GetWorkspacePath(userID, workspaceID) @@ -131,12 +149,23 @@ func (s *Service) FindFileByName(userID, workspaceID int, filename string) ([]st return nil, fmt.Errorf("file not found") } + log.Debug("file search complete", + "userID", userID, + "workspaceID", workspaceID, + "filename", filename, + "matchCount", len(foundPaths)) return foundPaths, nil } // GetFileContent returns the content of the file at the given filePath. // Path must be a relative path within the workspace directory given by userID and workspaceID. func (s *Service) GetFileContent(userID, workspaceID int, filePath string) ([]byte, error) { + log := getLogger() + log.Debug("reading file content", + "userID", userID, + "workspaceID", workspaceID, + "path", filePath) + fullPath, err := s.ValidatePath(userID, workspaceID, filePath) if err != nil { return nil, err @@ -147,6 +176,13 @@ func (s *Service) GetFileContent(userID, workspaceID int, filePath string) ([]by // SaveFile writes the content to the file at the given filePath. // Path must be a relative path within the workspace directory given by userID and workspaceID. func (s *Service) SaveFile(userID, workspaceID int, filePath string, content []byte) error { + log := getLogger() + log.Debug("saving file", + "userID", userID, + "workspaceID", workspaceID, + "path", filePath, + "contentSize", len(content)) + fullPath, err := s.ValidatePath(userID, workspaceID, filePath) if err != nil { return err @@ -157,17 +193,41 @@ func (s *Service) SaveFile(userID, workspaceID int, filePath string, content []b return err } - return s.fs.WriteFile(fullPath, content, 0644) + if err := s.fs.WriteFile(fullPath, content, 0644); err != nil { + return err + } + + log.Debug("file saved", + "userID", userID, + "workspaceID", workspaceID, + "path", filePath, + "size", len(content)) + return nil } // DeleteFile deletes the file at the given filePath. // Path must be a relative path within the workspace directory given by userID and workspaceID. func (s *Service) DeleteFile(userID, workspaceID int, filePath string) error { + log := getLogger() + log.Debug("deleting file", + "userID", userID, + "workspaceID", workspaceID, + "path", filePath) + fullPath, err := s.ValidatePath(userID, workspaceID, filePath) if err != nil { return err } - return s.fs.Remove(fullPath) + + if err := s.fs.Remove(fullPath); err != nil { + return err + } + + log.Debug("file deleted", + "userID", userID, + "workspaceID", workspaceID, + "path", filePath) + return nil } // FileCountStats holds statistics about files in a workspace @@ -179,6 +239,11 @@ type FileCountStats struct { // GetFileStats returns the total number of files and related statistics in a workspace // Workspace is identified by the given userID and workspaceID func (s *Service) GetFileStats(userID, workspaceID int) (*FileCountStats, error) { + log := getLogger() + log.Debug("gathering file statistics", + "userID", userID, + "workspaceID", workspaceID) + workspacePath := s.GetWorkspacePath(userID, workspaceID) // Check if workspace exists @@ -186,13 +251,33 @@ func (s *Service) GetFileStats(userID, workspaceID int) (*FileCountStats, error) return nil, fmt.Errorf("workspace directory does not exist") } - return s.countFilesInPath(workspacePath) + stats, err := s.countFilesInPath(workspacePath) + if err != nil { + return nil, err + } + log.Debug("file statistics collected", + "userID", userID, + "workspaceID", workspaceID, + "totalFiles", stats.TotalFiles, + "totalSize", stats.TotalSize) + return stats, nil } // GetTotalFileStats returns the total file statistics for the storage. func (s *Service) GetTotalFileStats() (*FileCountStats, error) { - return s.countFilesInPath(s.RootDir) + log := getLogger() + log.Debug("gathering total storage statistics") + + stats, err := s.countFilesInPath(s.RootDir) + if err != nil { + return nil, err + } + + log.Debug("total storage statistics collected", + "totalFiles", stats.TotalFiles, + "totalSize", stats.TotalSize) + return stats, nil } // countFilesInPath counts the total number of files and the total size of files in the given directory. diff --git a/server/internal/storage/filesystem.go b/server/internal/storage/filesystem.go index f5ca0b9..0d6b7b3 100644 --- a/server/internal/storage/filesystem.go +++ b/server/internal/storage/filesystem.go @@ -2,6 +2,7 @@ package storage import ( "io/fs" + "novamd/internal/logging" "os" ) @@ -17,6 +18,15 @@ type fileSystem interface { IsNotExist(err error) bool } +var logger logging.Logger + +func getLogger() logging.Logger { + if logger == nil { + logger = logging.WithGroup("storage") + } + return logger +} + // osFS implements the FileSystem interface using the real filesystem. type osFS struct{} diff --git a/server/internal/storage/git.go b/server/internal/storage/git.go index a4d5d78..c2998b9 100644 --- a/server/internal/storage/git.go +++ b/server/internal/storage/git.go @@ -16,19 +16,37 @@ type RepositoryManager interface { // SetupGitRepo sets up a Git repository for the given userID and workspaceID. // The repository is cloned from the given gitURL using the given gitUser and gitToken. func (s *Service) SetupGitRepo(userID, workspaceID int, gitURL, gitUser, gitToken, commitName, commitEmail string) error { + log := getLogger() + log.Info("setting up git repository", + "userID", userID, + "workspaceID", workspaceID, + ) + workspacePath := s.GetWorkspacePath(userID, workspaceID) + if _, ok := s.GitRepos[userID]; !ok { + log.Debug("initializing git repo map for user", + "userID", userID) s.GitRepos[userID] = make(map[int]git.Client) } + s.GitRepos[userID][workspaceID] = s.newGitClient(gitURL, gitUser, gitToken, workspacePath, commitName, commitEmail) + return s.GitRepos[userID][workspaceID].EnsureRepo() } // DisableGitRepo disables the Git repository for the given userID and workspaceID. func (s *Service) DisableGitRepo(userID, workspaceID int) { + log := getLogger() + log.Info("disabling git repository", + "userID", userID, + "workspaceID", workspaceID) + if userRepos, ok := s.GitRepos[userID]; ok { delete(userRepos, workspaceID) if len(userRepos) == 0 { + log.Debug("removing empty user git repos map", + "userID", userID) delete(s.GitRepos, userID) } } @@ -37,6 +55,12 @@ func (s *Service) DisableGitRepo(userID, workspaceID int) { // StageCommitAndPush stages, commit with the message, and pushes the changes to the Git repository. // The git repository belongs to the given userID and is associated with the given workspaceID. func (s *Service) StageCommitAndPush(userID, workspaceID int, message string) (git.CommitHash, error) { + log := getLogger() + log.Debug("preparing to stage, commit and push changes", + "userID", userID, + "workspaceID", workspaceID, + "message", message) + repo, ok := s.getGitRepo(userID, workspaceID) if !ok { return git.CommitHash{}, fmt.Errorf("git settings not configured for this workspace") @@ -47,19 +71,39 @@ func (s *Service) StageCommitAndPush(userID, workspaceID int, message string) (g return git.CommitHash{}, err } - err = repo.Push() - return hash, err + if err = repo.Push(); err != nil { + return hash, err + } + + log.Debug("changes committed and pushed", + "userID", userID, + "workspaceID", workspaceID, + "commitHash", hash.String()) + return hash, nil } // Pull pulls the changes from the remote Git repository. // The git repository belongs to the given userID and is associated with the given workspaceID. func (s *Service) Pull(userID, workspaceID int) error { + log := getLogger() + log.Debug("preparing to pull changes", + "userID", userID, + "workspaceID", workspaceID) + repo, ok := s.getGitRepo(userID, workspaceID) if !ok { return fmt.Errorf("git settings not configured for this workspace") } - return repo.Pull() + err := repo.Pull() + if err != nil { + return err + } + + log.Debug("changes pulled from remote", + "userID", userID, + "workspaceID", workspaceID) + return nil } // getGitRepo returns the Git repository for the given user and workspace IDs. diff --git a/server/internal/storage/service.go b/server/internal/storage/service.go index 07e6b1e..6cc7c15 100644 --- a/server/internal/storage/service.go +++ b/server/internal/storage/service.go @@ -27,6 +27,9 @@ type Options struct { // NewService creates a new Storage instance with the default options and the given rootDir root directory. func NewService(rootDir string) *Service { + getLogger().Debug("creating new storage service", + "rootDir", rootDir, + "options", "default") return NewServiceWithOptions(rootDir, Options{ Fs: &osFS{}, NewGitClient: git.New, @@ -35,14 +38,23 @@ func NewService(rootDir string) *Service { // NewServiceWithOptions creates a new Storage instance with the given options and the given rootDir root directory. func NewServiceWithOptions(rootDir string, options Options) *Service { + log := getLogger() + log.Debug("creating new storage service with custom options", + "rootDir", rootDir) + if options.Fs == nil { + log.Debug("filesystem not provided, using default osFS") options.Fs = &osFS{} } if options.NewGitClient == nil { + log.Debug("git client factory not provided, using default git.New") options.NewGitClient = git.New } + log.Info("storage service created", + "rootDir", rootDir) + return &Service{ fs: options.Fs, newGitClient: options.NewGitClient, diff --git a/server/internal/storage/workspace.go b/server/internal/storage/workspace.go index 560a1b0..564f519 100644 --- a/server/internal/storage/workspace.go +++ b/server/internal/storage/workspace.go @@ -17,6 +17,12 @@ type WorkspaceManager interface { // ValidatePath validates the if the given path is valid within the workspace directory. // Workspace directory is defined as the directory for the given userID and workspaceID. func (s *Service) ValidatePath(userID, workspaceID int, path string) (string, error) { + log := getLogger() + log.Debug("validating path", + "userID", userID, + "workspaceID", workspaceID, + "path", path) + workspacePath := s.GetWorkspacePath(userID, workspaceID) // First check if the path is absolute @@ -43,6 +49,11 @@ func (s *Service) GetWorkspacePath(userID, workspaceID int) string { // InitializeUserWorkspace creates the workspace directory for the given userID and workspaceID. func (s *Service) InitializeUserWorkspace(userID, workspaceID int) error { + log := getLogger() + log.Debug("initializing workspace directory", + "userID", userID, + "workspaceID", workspaceID) + workspacePath := s.GetWorkspacePath(userID, workspaceID) err := s.fs.MkdirAll(workspacePath, 0755) if err != nil { @@ -54,6 +65,11 @@ func (s *Service) InitializeUserWorkspace(userID, workspaceID int) error { // DeleteUserWorkspace deletes the workspace directory for the given userID and workspaceID. func (s *Service) DeleteUserWorkspace(userID, workspaceID int) error { + log := getLogger() + log.Debug("deleting workspace directory", + "userID", userID, + "workspaceID", workspaceID) + workspacePath := s.GetWorkspacePath(userID, workspaceID) err := s.fs.RemoveAll(workspacePath) if err != nil { From 54cefac4b78877ca54089124f6b2e0801db5b2fb Mon Sep 17 00:00:00 2001 From: LordMathis Date: Mon, 16 Dec 2024 22:39:26 +0100 Subject: [PATCH 16/22] Update package comment --- server/internal/storage/errors.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/internal/storage/errors.go b/server/internal/storage/errors.go index dbfdf02..e4623cd 100644 --- a/server/internal/storage/errors.go +++ b/server/internal/storage/errors.go @@ -1,5 +1,4 @@ -// storage/errors.go - +// Package storage provides functionalities to interact with the storage system (filesystem). package storage import ( From 7ccd36f0e447f0db00452dfe8a65aae8c27f7eaa Mon Sep 17 00:00:00 2001 From: LordMathis Date: Tue, 17 Dec 2024 23:28:01 +0100 Subject: [PATCH 17/22] Add logging to handlers --- server/internal/handlers/admin_handlers.go | 204 ++++++++++++++++-- server/internal/handlers/auth_handlers.go | 102 +++++++-- server/internal/handlers/file_handlers.go | 155 ++++++++++++- server/internal/handlers/git_handlers.go | 36 +++- server/internal/handlers/handlers.go | 10 + server/internal/handlers/static_handler.go | 52 ++++- server/internal/handlers/user_handlers.go | 92 ++++++-- .../internal/handlers/workspace_handlers.go | 144 ++++++++++++- 8 files changed, 739 insertions(+), 56 deletions(-) diff --git a/server/internal/handlers/admin_handlers.go b/server/internal/handlers/admin_handlers.go index 8c01590..1db95f0 100644 --- a/server/internal/handlers/admin_handlers.go +++ b/server/internal/handlers/admin_handlers.go @@ -6,6 +6,7 @@ import ( "net/http" "novamd/internal/context" "novamd/internal/db" + "novamd/internal/logging" "novamd/internal/models" "novamd/internal/storage" "strconv" @@ -47,6 +48,10 @@ type SystemStats struct { *storage.FileCountStats } +func getAdminLogger() logging.Logger { + return getHandlersLogger().WithGroup("admin") +} + // AdminListUsers godoc // @Summary List all users // @Description Returns the list of all users @@ -58,9 +63,22 @@ type SystemStats struct { // @Failure 500 {object} ErrorResponse "Failed to list users" // @Router /admin/users [get] func (h *Handler) AdminListUsers() http.HandlerFunc { - return func(w http.ResponseWriter, _ *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + ctx, ok := context.GetRequestContext(w, r) + if !ok { + return + } + log := getAdminLogger().With( + "handler", "AdminListUsers", + "adminID", ctx.UserID, + "clientIP", r.RemoteAddr, + ) + users, err := h.DB.GetAllUsers() if err != nil { + log.Error("failed to fetch users from database", + "error", err.Error(), + ) respondError(w, "Failed to list users", http.StatusInternalServerError) return } @@ -89,39 +107,63 @@ func (h *Handler) AdminListUsers() http.HandlerFunc { // @Router /admin/users [post] func (h *Handler) AdminCreateUser() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + ctx, ok := context.GetRequestContext(w, r) + if !ok { + return + } + log := getAdminLogger().With( + "handler", "AdminCreateUser", + "adminID", ctx.UserID, + "clientIP", r.RemoteAddr, + ) + var req CreateUserRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + log.Debug("failed to decode request body", + "error", err.Error(), + ) respondError(w, "Invalid request body", http.StatusBadRequest) return } - // Validate request + // Validation logging if req.Email == "" || req.Password == "" || req.Role == "" { + log.Debug("missing required fields", + "hasEmail", req.Email != "", + "hasPassword", req.Password != "", + "hasRole", req.Role != "", + ) respondError(w, "Email, password, and role are required", http.StatusBadRequest) return } - // Check if email already exists + // Email existence check existingUser, err := h.DB.GetUserByEmail(req.Email) if err == nil && existingUser != nil { + log.Warn("attempted to create user with existing email", + "email", req.Email, + ) respondError(w, "Email already exists", http.StatusConflict) return } - // Check if password is long enough if len(req.Password) < 8 { + log.Debug("password too short", + "passwordLength", len(req.Password), + ) respondError(w, "Password must be at least 8 characters", http.StatusBadRequest) return } - // Hash password hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { + log.Error("failed to hash password", + "error", err.Error(), + ) respondError(w, "Failed to hash password", http.StatusInternalServerError) return } - // Create user user := &models.User{ Email: req.Email, DisplayName: req.DisplayName, @@ -131,16 +173,30 @@ func (h *Handler) AdminCreateUser() http.HandlerFunc { insertedUser, err := h.DB.CreateUser(user) if err != nil { + log.Error("failed to create user in database", + "error", err.Error(), + "email", req.Email, + "role", req.Role, + ) respondError(w, "Failed to create user", http.StatusInternalServerError) return } - // Initialize user workspace if err := h.Storage.InitializeUserWorkspace(insertedUser.ID, insertedUser.LastWorkspaceID); err != nil { + log.Error("failed to initialize user workspace", + "error", err.Error(), + "userID", insertedUser.ID, + "workspaceID", insertedUser.LastWorkspaceID, + ) respondError(w, "Failed to initialize user workspace", http.StatusInternalServerError) return } + log.Info("user created", + "newUserID", insertedUser.ID, + "email", insertedUser.Email, + "role", insertedUser.Role, + ) respondJSON(w, insertedUser) } } @@ -159,14 +215,32 @@ func (h *Handler) AdminCreateUser() http.HandlerFunc { // @Router /admin/users/{userId} [get] func (h *Handler) AdminGetUser() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + ctx, ok := context.GetRequestContext(w, r) + if !ok { + return + } + log := getAdminLogger().With( + "handler", "AdminGetUser", + "adminID", ctx.UserID, + "clientIP", r.RemoteAddr, + ) + userID, err := strconv.Atoi(chi.URLParam(r, "userId")) if err != nil { + log.Debug("invalid user ID format", + "userIDParam", chi.URLParam(r, "userId"), + "error", err.Error(), + ) respondError(w, "Invalid user ID", http.StatusBadRequest) return } user, err := h.DB.GetUserByID(userID) if err != nil { + log.Debug("user not found", + "targetUserID", userID, + "error", err.Error(), + ) respondError(w, "User not found", http.StatusNotFound) return } @@ -194,49 +268,86 @@ func (h *Handler) AdminGetUser() http.HandlerFunc { // @Router /admin/users/{userId} [put] func (h *Handler) AdminUpdateUser() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + ctx, ok := context.GetRequestContext(w, r) + if !ok { + return + } + log := getAdminLogger().With( + "handler", "AdminUpdateUser", + "adminID", ctx.UserID, + "clientIP", r.RemoteAddr, + ) + userID, err := strconv.Atoi(chi.URLParam(r, "userId")) if err != nil { + log.Debug("invalid user ID format", + "userIDParam", chi.URLParam(r, "userId"), + "error", err.Error(), + ) respondError(w, "Invalid user ID", http.StatusBadRequest) return } - // Get existing user user, err := h.DB.GetUserByID(userID) if err != nil { + log.Debug("user not found", + "targetUserID", userID, + "error", err.Error(), + ) respondError(w, "User not found", http.StatusNotFound) return } var req UpdateUserRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + log.Debug("failed to decode request body", + "error", err.Error(), + ) respondError(w, "Invalid request body", http.StatusBadRequest) return } - // Update fields if provided + // Track what's being updated for logging + updates := make(map[string]interface{}) + if req.Email != "" { user.Email = req.Email + updates["email"] = req.Email } if req.DisplayName != "" { user.DisplayName = req.DisplayName + updates["displayName"] = req.DisplayName } if req.Role != "" { user.Role = req.Role + updates["role"] = req.Role } if req.Password != "" { hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { + log.Error("failed to hash password", + "error", err.Error(), + ) respondError(w, "Failed to hash password", http.StatusInternalServerError) return } user.PasswordHash = string(hashedPassword) + updates["passwordUpdated"] = true } if err := h.DB.UpdateUser(user); err != nil { + log.Error("failed to update user in database", + "error", err.Error(), + "targetUserID", userID, + ) respondError(w, "Failed to update user", http.StatusInternalServerError) return } + log.Info("user updated", + "targetUserID", userID, + "updates", updates, + ) respondJSON(w, user) } } @@ -261,37 +372,61 @@ func (h *Handler) AdminDeleteUser() http.HandlerFunc { if !ok { return } + log := getAdminLogger().With( + "handler", "AdminDeleteUser", + "adminID", ctx.UserID, + "clientIP", r.RemoteAddr, + ) userID, err := strconv.Atoi(chi.URLParam(r, "userId")) if err != nil { + log.Debug("invalid user ID format", + "userIDParam", chi.URLParam(r, "userId"), + "error", err.Error(), + ) respondError(w, "Invalid user ID", http.StatusBadRequest) return } - // Prevent admin from deleting themselves if userID == ctx.UserID { + log.Warn("admin attempted to delete own account") respondError(w, "Cannot delete your own account", http.StatusBadRequest) return } - // Get user before deletion to check role user, err := h.DB.GetUserByID(userID) if err != nil { + log.Debug("user not found", + "targetUserID", userID, + "error", err.Error(), + ) respondError(w, "User not found", http.StatusNotFound) return } - // Prevent deletion of other admin users if user.Role == models.RoleAdmin && ctx.UserID != userID { + log.Warn("attempted to delete another admin user", + "targetUserID", userID, + "targetUserEmail", user.Email, + ) respondError(w, "Cannot delete other admin users", http.StatusForbidden) return } if err := h.DB.DeleteUser(userID); err != nil { + log.Error("failed to delete user from database", + "error", err.Error(), + "targetUserID", userID, + ) respondError(w, "Failed to delete user", http.StatusInternalServerError) return } + log.Info("user deleted", + "targetUserID", userID, + "targetUserEmail", user.Email, + "targetUserRole", user.Role, + ) w.WriteHeader(http.StatusNoContent) } } @@ -309,9 +444,22 @@ func (h *Handler) AdminDeleteUser() http.HandlerFunc { // @Failure 500 {object} ErrorResponse "Failed to get file stats" // @Router /admin/workspaces [get] func (h *Handler) AdminListWorkspaces() http.HandlerFunc { - return func(w http.ResponseWriter, _ *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + ctx, ok := context.GetRequestContext(w, r) + if !ok { + return + } + log := getAdminLogger().With( + "handler", "AdminListWorkspaces", + "adminID", ctx.UserID, + "clientIP", r.RemoteAddr, + ) + workspaces, err := h.DB.GetAllWorkspaces() if err != nil { + log.Error("failed to fetch workspaces from database", + "error", err.Error(), + ) respondError(w, "Failed to list workspaces", http.StatusInternalServerError) return } @@ -319,11 +467,15 @@ func (h *Handler) AdminListWorkspaces() http.HandlerFunc { workspacesStats := make([]*WorkspaceStats, 0, len(workspaces)) for _, ws := range workspaces { - workspaceData := &WorkspaceStats{} user, err := h.DB.GetUserByID(ws.UserID) if err != nil { + log.Error("failed to fetch user for workspace", + "error", err.Error(), + "workspaceID", ws.ID, + "userID", ws.UserID, + ) respondError(w, "Failed to get user", http.StatusInternalServerError) return } @@ -336,12 +488,16 @@ func (h *Handler) AdminListWorkspaces() http.HandlerFunc { fileStats, err := h.Storage.GetFileStats(ws.UserID, ws.ID) if err != nil { + log.Error("failed to fetch file stats for workspace", + "error", err.Error(), + "workspaceID", ws.ID, + "userID", ws.UserID, + ) respondError(w, "Failed to get file stats", http.StatusInternalServerError) return } workspaceData.FileCountStats = fileStats - workspacesStats = append(workspacesStats, workspaceData) } @@ -361,15 +517,31 @@ func (h *Handler) AdminListWorkspaces() http.HandlerFunc { // @Failure 500 {object} ErrorResponse "Failed to get file stats" // @Router /admin/stats [get] func (h *Handler) AdminGetSystemStats() http.HandlerFunc { - return func(w http.ResponseWriter, _ *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + ctx, ok := context.GetRequestContext(w, r) + if !ok { + return + } + log := getAdminLogger().With( + "handler", "AdminGetSystemStats", + "adminID", ctx.UserID, + "clientIP", r.RemoteAddr, + ) + userStats, err := h.DB.GetSystemStats() if err != nil { + log.Error("failed to fetch user statistics", + "error", err.Error(), + ) respondError(w, "Failed to get user stats", http.StatusInternalServerError) return } fileStats, err := h.Storage.GetTotalFileStats() if err != nil { + log.Error("failed to fetch file statistics", + "error", err.Error(), + ) respondError(w, "Failed to get file stats", http.StatusInternalServerError) return } diff --git a/server/internal/handlers/auth_handlers.go b/server/internal/handlers/auth_handlers.go index 2f1cbb2..3392d0c 100644 --- a/server/internal/handlers/auth_handlers.go +++ b/server/internal/handlers/auth_handlers.go @@ -7,6 +7,7 @@ import ( "net/http" "novamd/internal/auth" "novamd/internal/context" + "novamd/internal/logging" "novamd/internal/models" "time" @@ -26,6 +27,10 @@ type LoginResponse struct { ExpiresAt time.Time `json:"expiresAt,omitempty"` } +func getAuthLogger() logging.Logger { + return getHandlersLogger().WithGroup("auth") +} + // Login godoc // @Summary Login // @Description Logs in a user and returns a session with access and refresh tokens @@ -43,62 +48,88 @@ type LoginResponse struct { // @Router /auth/login [post] func (h *Handler) Login(authManager auth.SessionManager, cookieService auth.CookieManager) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + log := getAuthLogger().With( + "handler", "Login", + "clientIP", r.RemoteAddr, + ) + var req LoginRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + log.Debug("failed to decode request body", + "error", err.Error(), + ) respondError(w, "Invalid request body", http.StatusBadRequest) return } - // Validate request if req.Email == "" || req.Password == "" { + log.Debug("missing required fields", + "hasEmail", req.Email != "", + "hasPassword", req.Password != "", + ) respondError(w, "Email and password are required", http.StatusBadRequest) return } - // Get user from database user, err := h.DB.GetUserByEmail(req.Email) if err != nil { + log.Debug("user not found", + "email", req.Email, + "error", err.Error(), + ) respondError(w, "Invalid credentials", http.StatusUnauthorized) return } - // Verify password err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)) if err != nil { + log.Warn("invalid password attempt", + "userID", user.ID, + "email", user.Email, + ) respondError(w, "Invalid credentials", http.StatusUnauthorized) return } - // Create session and generate tokens session, accessToken, err := authManager.CreateSession(user.ID, string(user.Role)) if err != nil { + log.Error("failed to create session", + "error", err.Error(), + "userID", user.ID, + ) respondError(w, "Failed to create session", http.StatusInternalServerError) return } - // Generate CSRF token csrfToken := make([]byte, 32) if _, err := rand.Read(csrfToken); err != nil { + log.Error("failed to generate CSRF token", + "error", err.Error(), + "userID", user.ID, + ) respondError(w, "Failed to generate CSRF token", http.StatusInternalServerError) return } csrfTokenString := hex.EncodeToString(csrfToken) - // Set cookies http.SetCookie(w, cookieService.GenerateAccessTokenCookie(accessToken)) http.SetCookie(w, cookieService.GenerateRefreshTokenCookie(session.RefreshToken)) http.SetCookie(w, cookieService.GenerateCSRFCookie(csrfTokenString)) - // Send CSRF token in header for initial setup w.Header().Set("X-CSRF-Token", csrfTokenString) - // Only send user info in response, not tokens response := LoginResponse{ User: user, SessionID: session.ID, ExpiresAt: session.ExpiresAt, } + log.Info("user logged in successfully", + "userID", user.ID, + "email", user.Email, + "role", user.Role, + "sessionID", session.ID, + ) respondJSON(w, response) } } @@ -114,24 +145,41 @@ func (h *Handler) Login(authManager auth.SessionManager, cookieService auth.Cook // @Router /auth/logout [post] func (h *Handler) Logout(authManager auth.SessionManager, cookieService auth.CookieManager) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - // Get session ID from cookie + ctx, ok := context.GetRequestContext(w, r) + if !ok { + return + } + log := getAuthLogger().With( + "handler", "Logout", + "userID", ctx.UserID, + "clientIP", r.RemoteAddr, + ) + sessionCookie, err := r.Cookie("access_token") if err != nil { - http.Error(w, "Unauthorized", http.StatusUnauthorized) + log.Debug("missing access token cookie", + "error", err.Error(), + ) + respondError(w, "Access token required", http.StatusBadRequest) return } - // Invalidate the session in the database if err := authManager.InvalidateSession(sessionCookie.Value); err != nil { + log.Error("failed to invalidate session", + "error", err.Error(), + "sessionID", sessionCookie.Value, + ) respondError(w, "Failed to invalidate session", http.StatusInternalServerError) return } - // Clear cookies http.SetCookie(w, cookieService.InvalidateCookie("access_token")) http.SetCookie(w, cookieService.InvalidateCookie("refresh_token")) http.SetCookie(w, cookieService.InvalidateCookie("csrf_token")) + log.Info("user logged out successfully", + "sessionID", sessionCookie.Value, + ) w.WriteHeader(http.StatusNoContent) } } @@ -151,22 +199,39 @@ func (h *Handler) Logout(authManager auth.SessionManager, cookieService auth.Coo // @Router /auth/refresh [post] func (h *Handler) RefreshToken(authManager auth.SessionManager, cookieService auth.CookieManager) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + ctx, ok := context.GetRequestContext(w, r) + if !ok { + return + } + log := getAuthLogger().With( + "handler", "RefreshToken", + "userID", ctx.UserID, + "clientIP", r.RemoteAddr, + ) + refreshCookie, err := r.Cookie("refresh_token") if err != nil { + log.Debug("missing refresh token cookie", + "error", err.Error(), + ) respondError(w, "Refresh token required", http.StatusBadRequest) return } - // Generate new access token accessToken, err := authManager.RefreshSession(refreshCookie.Value) if err != nil { + log.Error("failed to refresh session", + "error", err.Error(), + ) respondError(w, "Invalid refresh token", http.StatusUnauthorized) return } - // Generate new CSRF token csrfToken := make([]byte, 32) if _, err := rand.Read(csrfToken); err != nil { + log.Error("failed to generate CSRF token", + "error", err.Error(), + ) respondError(w, "Failed to generate CSRF token", http.StatusInternalServerError) return } @@ -196,10 +261,17 @@ func (h *Handler) GetCurrentUser() http.HandlerFunc { if !ok { return } + log := getAuthLogger().With( + "handler", "GetCurrentUser", + "userID", ctx.UserID, + "clientIP", r.RemoteAddr, + ) - // Get user from database user, err := h.DB.GetUserByID(ctx.UserID) if err != nil { + log.Error("failed to fetch user", + "error", err.Error(), + ) respondError(w, "User not found", http.StatusNotFound) return } diff --git a/server/internal/handlers/file_handlers.go b/server/internal/handlers/file_handlers.go index a19597c..950d642 100644 --- a/server/internal/handlers/file_handlers.go +++ b/server/internal/handlers/file_handlers.go @@ -8,6 +8,7 @@ import ( "time" "novamd/internal/context" + "novamd/internal/logging" "novamd/internal/storage" "github.com/go-chi/chi/v5" @@ -35,6 +36,10 @@ type UpdateLastOpenedFileRequest struct { FilePath string `json:"filePath"` } +func getFilesLogger() logging.Logger { + return getHandlersLogger().WithGroup("files") +} + // ListFiles godoc // @Summary List files // @Description Lists all files in the user's workspace @@ -52,13 +57,25 @@ func (h *Handler) ListFiles() http.HandlerFunc { if !ok { return } + log := getFilesLogger().With( + "handler", "ListFiles", + "userID", ctx.UserID, + "workspaceID", ctx.Workspace.ID, + "clientIP", r.RemoteAddr, + ) files, err := h.Storage.ListFilesRecursively(ctx.UserID, ctx.Workspace.ID) if err != nil { + log.Error("failed to list files in workspace", + "error", err.Error(), + ) respondError(w, "Failed to list files", http.StatusInternalServerError) return } + log.Debug("files listed successfully", + "fileCount", len(files), + ) respondJSON(w, files) } } @@ -82,19 +99,40 @@ func (h *Handler) LookupFileByName() http.HandlerFunc { if !ok { return } + log := getFilesLogger().With( + "handler", "LookupFileByName", + "userID", ctx.UserID, + "workspaceID", ctx.Workspace.ID, + "clientIP", r.RemoteAddr, + ) filename := r.URL.Query().Get("filename") if filename == "" { + log.Debug("missing filename parameter") respondError(w, "Filename is required", http.StatusBadRequest) return } filePaths, err := h.Storage.FindFileByName(ctx.UserID, ctx.Workspace.ID, filename) if err != nil { + if !os.IsNotExist(err) { + log.Error("failed to lookup file", + "filename", filename, + "error", err.Error(), + ) + } else { + log.Debug("file not found", + "filename", filename, + ) + } respondError(w, "File not found", http.StatusNotFound) return } + log.Debug("file lookup successful", + "filename", filename, + "matchCount", len(filePaths), + ) respondJSON(w, &LookupResponse{Paths: filePaths}) } } @@ -120,21 +158,37 @@ func (h *Handler) GetFileContent() http.HandlerFunc { if !ok { return } + log := getFilesLogger().With( + "handler", "GetFileContent", + "userID", ctx.UserID, + "workspaceID", ctx.Workspace.ID, + "clientIP", r.RemoteAddr, + ) filePath := chi.URLParam(r, "*") content, err := h.Storage.GetFileContent(ctx.UserID, ctx.Workspace.ID, filePath) if err != nil { - if storage.IsPathValidationError(err) { + log.Error("invalid file path attempted", + "filePath", filePath, + "error", err.Error(), + ) respondError(w, "Invalid file path", http.StatusBadRequest) return } if os.IsNotExist(err) { + log.Debug("file not found", + "filePath", filePath, + ) respondError(w, "File not found", http.StatusNotFound) return } + log.Error("failed to read file content", + "filePath", filePath, + "error", err.Error(), + ) respondError(w, "Failed to read file", http.StatusInternalServerError) return } @@ -142,9 +196,18 @@ func (h *Handler) GetFileContent() http.HandlerFunc { w.Header().Set("Content-Type", "text/plain") _, err = w.Write(content) if err != nil { + log.Error("failed to write response", + "filePath", filePath, + "error", err.Error(), + ) respondError(w, "Failed to write response", http.StatusInternalServerError) return } + + log.Debug("file content retrieved", + "filePath", filePath, + "contentSize", len(content), + ) } } @@ -169,10 +232,20 @@ func (h *Handler) SaveFile() http.HandlerFunc { if !ok { return } + log := getFilesLogger().With( + "handler", "SaveFile", + "userID", ctx.UserID, + "workspaceID", ctx.Workspace.ID, + "clientIP", r.RemoteAddr, + ) filePath := chi.URLParam(r, "*") content, err := io.ReadAll(r.Body) if err != nil { + log.Error("failed to read request body", + "filePath", filePath, + "error", err.Error(), + ) respondError(w, "Failed to read request body", http.StatusBadRequest) return } @@ -180,10 +253,19 @@ func (h *Handler) SaveFile() http.HandlerFunc { err = h.Storage.SaveFile(ctx.UserID, ctx.Workspace.ID, filePath, content) if err != nil { if storage.IsPathValidationError(err) { + log.Error("invalid file path attempted", + "filePath", filePath, + "error", err.Error(), + ) respondError(w, "Invalid file path", http.StatusBadRequest) return } + log.Error("failed to save file", + "filePath", filePath, + "contentSize", len(content), + "error", err.Error(), + ) respondError(w, "Failed to save file", http.StatusInternalServerError) return } @@ -194,7 +276,11 @@ func (h *Handler) SaveFile() http.HandlerFunc { UpdatedAt: time.Now().UTC(), } - w.WriteHeader(http.StatusOK) + log.Debug("file saved", + "filePath", filePath, + "size", response.Size, + "updatedAt", response.UpdatedAt, + ) respondJSON(w, response) } } @@ -211,7 +297,6 @@ func (h *Handler) SaveFile() http.HandlerFunc { // @Failure 400 {object} ErrorResponse "Invalid file path" // @Failure 404 {object} ErrorResponse "File not found" // @Failure 500 {object} ErrorResponse "Failed to delete file" -// @Failure 500 {object} ErrorResponse "Failed to write response" // @Router /workspaces/{workspace_name}/files/{file_path} [delete] func (h *Handler) DeleteFile() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -219,24 +304,44 @@ func (h *Handler) DeleteFile() http.HandlerFunc { if !ok { return } + log := getFilesLogger().With( + "handler", "DeleteFile", + "userID", ctx.UserID, + "workspaceID", ctx.Workspace.ID, + "clientIP", r.RemoteAddr, + ) filePath := chi.URLParam(r, "*") err := h.Storage.DeleteFile(ctx.UserID, ctx.Workspace.ID, filePath) if err != nil { if storage.IsPathValidationError(err) { + log.Error("invalid file path attempted", + "filePath", filePath, + "error", err.Error(), + ) respondError(w, "Invalid file path", http.StatusBadRequest) return } if os.IsNotExist(err) { + log.Debug("file not found", + "filePath", filePath, + ) respondError(w, "File not found", http.StatusNotFound) return } + log.Error("failed to delete file", + "filePath", filePath, + "error", err.Error(), + ) respondError(w, "Failed to delete file", http.StatusInternalServerError) return } + log.Debug("file deleted", + "filePath", filePath, + ) w.WriteHeader(http.StatusNoContent) } } @@ -259,18 +364,34 @@ func (h *Handler) GetLastOpenedFile() http.HandlerFunc { if !ok { return } + log := getFilesLogger().With( + "handler", "GetLastOpenedFile", + "userID", ctx.UserID, + "workspaceID", ctx.Workspace.ID, + "clientIP", r.RemoteAddr, + ) filePath, err := h.DB.GetLastOpenedFile(ctx.Workspace.ID) if err != nil { + log.Error("failed to get last opened file from database", + "error", err.Error(), + ) respondError(w, "Failed to get last opened file", http.StatusInternalServerError) return } if _, err := h.Storage.ValidatePath(ctx.UserID, ctx.Workspace.ID, filePath); err != nil { + log.Error("invalid file path stored", + "filePath", filePath, + "error", err.Error(), + ) respondError(w, "Invalid file path", http.StatusBadRequest) return } + log.Debug("last opened file retrieved successfully", + "filePath", filePath, + ) respondJSON(w, &LastOpenedFileResponse{LastOpenedFilePath: filePath}) } } @@ -297,10 +418,18 @@ func (h *Handler) UpdateLastOpenedFile() http.HandlerFunc { if !ok { return } + log := getFilesLogger().With( + "handler", "UpdateLastOpenedFile", + "userID", ctx.UserID, + "workspaceID", ctx.Workspace.ID, + "clientIP", r.RemoteAddr, + ) var requestBody UpdateLastOpenedFileRequest - if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil { + log.Error("failed to decode request body", + "error", err.Error(), + ) respondError(w, "Invalid request body", http.StatusBadRequest) return } @@ -310,25 +439,43 @@ func (h *Handler) UpdateLastOpenedFile() http.HandlerFunc { _, err := h.Storage.GetFileContent(ctx.UserID, ctx.Workspace.ID, requestBody.FilePath) if err != nil { if storage.IsPathValidationError(err) { + log.Error("invalid file path attempted", + "filePath", requestBody.FilePath, + "error", err.Error(), + ) respondError(w, "Invalid file path", http.StatusBadRequest) return } if os.IsNotExist(err) { + log.Debug("file not found", + "filePath", requestBody.FilePath, + ) respondError(w, "File not found", http.StatusNotFound) return } + log.Error("failed to validate file path", + "filePath", requestBody.FilePath, + "error", err.Error(), + ) respondError(w, "Failed to update last opened file", http.StatusInternalServerError) return } } if err := h.DB.UpdateLastOpenedFile(ctx.Workspace.ID, requestBody.FilePath); err != nil { + log.Error("failed to update last opened file in database", + "filePath", requestBody.FilePath, + "error", err.Error(), + ) respondError(w, "Failed to update last opened file", http.StatusInternalServerError) return } + log.Debug("last opened file updated successfully", + "filePath", requestBody.FilePath, + ) w.WriteHeader(http.StatusNoContent) } } diff --git a/server/internal/handlers/git_handlers.go b/server/internal/handlers/git_handlers.go index 3135b6e..b57493f 100644 --- a/server/internal/handlers/git_handlers.go +++ b/server/internal/handlers/git_handlers.go @@ -3,8 +3,8 @@ package handlers import ( "encoding/json" "net/http" - "novamd/internal/context" + "novamd/internal/logging" ) // CommitRequest represents a request to commit changes @@ -22,6 +22,10 @@ type PullResponse struct { Message string `json:"message" example:"Pulled changes from remote"` } +func getGitLogger() logging.Logger { + return getHandlersLogger().WithGroup("git") +} + // StageCommitAndPush godoc // @Summary Stage, commit, and push changes // @Description Stages, commits, and pushes changes to the remote repository @@ -42,25 +46,43 @@ func (h *Handler) StageCommitAndPush() http.HandlerFunc { if !ok { return } + log := getGitLogger().With( + "handler", "StageCommitAndPush", + "userID", ctx.UserID, + "workspaceID", ctx.Workspace.ID, + "clientIP", r.RemoteAddr, + ) var requestBody CommitRequest - if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil { + log.Error("failed to decode request body", + "error", err.Error(), + ) respondError(w, "Invalid request body", http.StatusBadRequest) return } if requestBody.Message == "" { + log.Debug("empty commit message provided") respondError(w, "Commit message is required", http.StatusBadRequest) return } hash, err := h.Storage.StageCommitAndPush(ctx.UserID, ctx.Workspace.ID, requestBody.Message) if err != nil { + log.Error("failed to perform git operations", + "error", err.Error(), + "commitMessage", requestBody.Message, + ) respondError(w, "Failed to stage, commit, and push changes: "+err.Error(), http.StatusInternalServerError) return } + log.Debug("git operations completed successfully", + "commitHash", hash.String(), + "commitMessage", requestBody.Message, + ) + respondJSON(w, CommitResponse{CommitHash: hash.String()}) } } @@ -82,13 +104,23 @@ func (h *Handler) PullChanges() http.HandlerFunc { if !ok { return } + log := getGitLogger().With( + "handler", "PullChanges", + "userID", ctx.UserID, + "workspaceID", ctx.Workspace.ID, + "clientIP", r.RemoteAddr, + ) err := h.Storage.Pull(ctx.UserID, ctx.Workspace.ID) if err != nil { + log.Error("failed to pull changes from remote", + "error", err.Error(), + ) respondError(w, "Failed to pull changes: "+err.Error(), http.StatusInternalServerError) return } + log.Debug("successfully pulled changes from remote") respondJSON(w, PullResponse{Message: "Successfully pulled changes from remote"}) } } diff --git a/server/internal/handlers/handlers.go b/server/internal/handlers/handlers.go index da20999..b3dc4fb 100644 --- a/server/internal/handlers/handlers.go +++ b/server/internal/handlers/handlers.go @@ -4,6 +4,7 @@ import ( "encoding/json" "net/http" "novamd/internal/db" + "novamd/internal/logging" "novamd/internal/storage" ) @@ -18,6 +19,15 @@ type Handler struct { Storage storage.Manager } +var logger logging.Logger + +func getHandlersLogger() logging.Logger { + if logger == nil { + logger = logging.WithGroup("handlers") + } + return logger +} + // NewHandler creates a new handler with the given dependencies func NewHandler(db db.Database, s storage.Manager) *Handler { return &Handler{ diff --git a/server/internal/handlers/static_handler.go b/server/internal/handlers/static_handler.go index 752e7b9..b70d219 100644 --- a/server/internal/handlers/static_handler.go +++ b/server/internal/handlers/static_handler.go @@ -2,6 +2,7 @@ package handlers import ( "net/http" + "novamd/internal/logging" "os" "path/filepath" "strings" @@ -19,8 +20,19 @@ func NewStaticHandler(staticPath string) *StaticHandler { } } +func getStaticLogger() logging.Logger { + return logging.WithGroup("static") +} + // ServeHTTP serves the static files func (h *StaticHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + log := getStaticLogger().With( + "handler", "ServeHTTP", + "clientIP", r.RemoteAddr, + "method", r.Method, + "url", r.URL.Path, + ) + // Get the requested path requestedPath := r.URL.Path fullPath := filepath.Join(h.staticPath, requestedPath) @@ -28,6 +40,10 @@ func (h *StaticHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Security check to prevent directory traversal if !strings.HasPrefix(cleanPath, h.staticPath) { + log.Warn("directory traversal attempt detected", + "requestedPath", requestedPath, + "cleanPath", cleanPath, + ) respondError(w, "Invalid path", http.StatusBadRequest) return } @@ -35,11 +51,29 @@ func (h *StaticHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Set cache headers for assets if strings.HasPrefix(requestedPath, "/assets/") { w.Header().Set("Cache-Control", "public, max-age=31536000") // 1 year + log.Debug("cache headers set for asset", + "path", requestedPath, + ) } // Check if file exists (not counting .gz files) stat, err := os.Stat(cleanPath) if err != nil || stat.IsDir() { + if os.IsNotExist(err) { + log.Debug("file not found, serving index.html", + "requestedPath", requestedPath, + ) + } else if stat != nil && stat.IsDir() { + log.Debug("directory requested, serving index.html", + "requestedPath", requestedPath, + ) + } else { + log.Error("error checking file status", + "requestedPath", requestedPath, + "error", err.Error(), + ) + } + // Serve index.html for SPA routing indexPath := filepath.Join(h.staticPath, "index.html") http.ServeFile(w, r, indexPath) @@ -53,20 +87,32 @@ func (h *StaticHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Encoding", "gzip") // Set proper content type based on original file + contentType := "application/octet-stream" switch filepath.Ext(cleanPath) { case ".js": - w.Header().Set("Content-Type", "application/javascript") + contentType = "application/javascript" case ".css": - w.Header().Set("Content-Type", "text/css") + contentType = "text/css" case ".html": - w.Header().Set("Content-Type", "text/html") + contentType = "text/html" } + w.Header().Set("Content-Type", contentType) + log.Debug("serving gzipped file", + "path", requestedPath, + "gzPath", gzPath, + "contentType", contentType, + ) http.ServeFile(w, r, gzPath) return } } // Serve original file + log.Debug("serving original file", + "path", requestedPath, + "size", stat.Size(), + "modTime", stat.ModTime(), + ) http.ServeFile(w, r, cleanPath) } diff --git a/server/internal/handlers/user_handlers.go b/server/internal/handlers/user_handlers.go index 7249b63..e0cf99b 100644 --- a/server/internal/handlers/user_handlers.go +++ b/server/internal/handlers/user_handlers.go @@ -5,6 +5,7 @@ import ( "net/http" "novamd/internal/context" + "novamd/internal/logging" "golang.org/x/crypto/bcrypt" ) @@ -22,6 +23,10 @@ type DeleteAccountRequest struct { Password string `json:"password"` } +func getProfileLogger() logging.Logger { + return getHandlersLogger().WithGroup("profile") +} + // UpdateProfile godoc // @Summary Update profile // @Description Updates the user's profile @@ -48,9 +53,17 @@ func (h *Handler) UpdateProfile() http.HandlerFunc { if !ok { return } + log := getProfileLogger().With( + "handler", "UpdateProfile", + "userID", ctx.UserID, + "clientIP", r.RemoteAddr, + ) var req UpdateProfileRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + log.Debug("failed to decode request body", + "error", err.Error(), + ) respondError(w, "Invalid request body", http.StatusBadRequest) return } @@ -58,76 +71,97 @@ func (h *Handler) UpdateProfile() http.HandlerFunc { // Get current user user, err := h.DB.GetUserByID(ctx.UserID) if err != nil { + log.Error("failed to fetch user from database", + "error", err.Error(), + ) respondError(w, "User not found", http.StatusNotFound) return } + // Track what's being updated for logging + updates := make(map[string]bool) + // Handle password update if requested if req.NewPassword != "" { - // Current password must be provided to change password if req.CurrentPassword == "" { + log.Debug("password change attempted without current password") respondError(w, "Current password is required to change password", http.StatusBadRequest) return } - // Verify current password if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.CurrentPassword)); err != nil { + log.Warn("incorrect password provided for password change") respondError(w, "Current password is incorrect", http.StatusUnauthorized) return } - // Validate new password if len(req.NewPassword) < 8 { + log.Debug("password change rejected - too short", + "passwordLength", len(req.NewPassword), + ) respondError(w, "New password must be at least 8 characters long", http.StatusBadRequest) return } - // Hash new password hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost) if err != nil { + log.Error("failed to hash new password", + "error", err.Error(), + ) respondError(w, "Failed to process new password", http.StatusInternalServerError) return } user.PasswordHash = string(hashedPassword) + updates["passwordChanged"] = true } // Handle email update if requested if req.Email != "" && req.Email != user.Email { - // Check if email change requires password verification if req.CurrentPassword == "" { + log.Debug("email change attempted without current password") respondError(w, "Current password is required to change email", http.StatusBadRequest) return } - // Verify current password if not already verified for password change if req.NewPassword == "" { if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.CurrentPassword)); err != nil { + log.Warn("incorrect password provided for email change") respondError(w, "Current password is incorrect", http.StatusUnauthorized) return } } - // Check if new email is already in use existingUser, err := h.DB.GetUserByEmail(req.Email) if err == nil && existingUser.ID != user.ID { + log.Debug("email change rejected - already in use", + "requestedEmail", req.Email, + ) respondError(w, "Email already in use", http.StatusConflict) return } user.Email = req.Email + updates["emailChanged"] = true } - // Update display name if provided (no password required) + // Update display name if provided if req.DisplayName != "" { user.DisplayName = req.DisplayName + updates["displayNameChanged"] = true } // Update user in database if err := h.DB.UpdateUser(user); err != nil { + log.Error("failed to update user in database", + "error", err.Error(), + "updates", updates, + ) respondError(w, "Failed to update profile", http.StatusInternalServerError) return } - // Return updated user data + log.Debug("profile updated successfully", + "updates", updates, + ) respondJSON(w, user) } } @@ -155,9 +189,17 @@ func (h *Handler) DeleteAccount() http.HandlerFunc { if !ok { return } + log := getProfileLogger().With( + "handler", "DeleteAccount", + "userID", ctx.UserID, + "clientIP", r.RemoteAddr, + ) var req DeleteAccountRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + log.Debug("failed to decode request body", + "error", err.Error(), + ) respondError(w, "Invalid request body", http.StatusBadRequest) return } @@ -165,25 +207,32 @@ func (h *Handler) DeleteAccount() http.HandlerFunc { // Get current user user, err := h.DB.GetUserByID(ctx.UserID) if err != nil { + log.Error("failed to fetch user from database", + "error", err.Error(), + ) respondError(w, "User not found", http.StatusNotFound) return } // Verify password if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil { - respondError(w, "Password is incorrect", http.StatusUnauthorized) + log.Warn("incorrect password provided for account deletion") + respondError(w, "Incorrect password", http.StatusUnauthorized) return } // Prevent admin from deleting their own account if they're the last admin if user.Role == "admin" { - // Count number of admin users adminCount, err := h.DB.CountAdminUsers() if err != nil { - respondError(w, "Failed to verify admin status", http.StatusInternalServerError) + log.Error("failed to count admin users", + "error", err.Error(), + ) + respondError(w, "Failed to get admin count", http.StatusInternalServerError) return } if adminCount <= 1 { + log.Warn("attempted to delete last admin account") respondError(w, "Cannot delete the last admin account", http.StatusForbidden) return } @@ -192,6 +241,9 @@ func (h *Handler) DeleteAccount() http.HandlerFunc { // Get user's workspaces for cleanup workspaces, err := h.DB.GetWorkspacesByUserID(ctx.UserID) if err != nil { + log.Error("failed to fetch user workspaces", + "error", err.Error(), + ) respondError(w, "Failed to get user workspaces", http.StatusInternalServerError) return } @@ -199,17 +251,31 @@ func (h *Handler) DeleteAccount() http.HandlerFunc { // Delete workspace directories for _, workspace := range workspaces { if err := h.Storage.DeleteUserWorkspace(ctx.UserID, workspace.ID); err != nil { + log.Error("failed to delete workspace directory", + "error", err.Error(), + "workspaceID", workspace.ID, + ) respondError(w, "Failed to delete workspace files", http.StatusInternalServerError) return } + log.Debug("workspace deleted", + "workspaceID", workspace.ID, + ) } - // Delete user from database (this will cascade delete workspaces and sessions) + // Delete user from database if err := h.DB.DeleteUser(ctx.UserID); err != nil { + log.Error("failed to delete user from database", + "error", err.Error(), + ) respondError(w, "Failed to delete account", http.StatusInternalServerError) return } + log.Info("user account deleted", + "email", user.Email, + "role", user.Role, + ) w.WriteHeader(http.StatusNoContent) } } diff --git a/server/internal/handlers/workspace_handlers.go b/server/internal/handlers/workspace_handlers.go index e04e543..de32f1a 100644 --- a/server/internal/handlers/workspace_handlers.go +++ b/server/internal/handlers/workspace_handlers.go @@ -6,6 +6,7 @@ import ( "net/http" "novamd/internal/context" + "novamd/internal/logging" "novamd/internal/models" ) @@ -19,6 +20,10 @@ type LastWorkspaceNameResponse struct { LastWorkspaceName string `json:"lastWorkspaceName"` } +func getWorkspaceLogger() logging.Logger { + return getHandlersLogger().WithGroup("workspace") +} + // ListWorkspaces godoc // @Summary List workspaces // @Description Lists all workspaces for the current user @@ -35,13 +40,24 @@ func (h *Handler) ListWorkspaces() http.HandlerFunc { if !ok { return } + log := getWorkspaceLogger().With( + "handler", "ListWorkspaces", + "userID", ctx.UserID, + "clientIP", r.RemoteAddr, + ) workspaces, err := h.DB.GetWorkspacesByUserID(ctx.UserID) if err != nil { + log.Error("failed to fetch workspaces from database", + "error", err.Error(), + ) respondError(w, "Failed to list workspaces", http.StatusInternalServerError) return } + log.Debug("workspaces retrieved successfully", + "count", len(workspaces), + ) respondJSON(w, workspaces) } } @@ -68,30 +84,54 @@ func (h *Handler) CreateWorkspace() http.HandlerFunc { if !ok { return } + log := getWorkspaceLogger().With( + "handler", "CreateWorkspace", + "userID", ctx.UserID, + "clientIP", r.RemoteAddr, + ) var workspace models.Workspace if err := json.NewDecoder(r.Body).Decode(&workspace); err != nil { + log.Debug("invalid request body received", + "error", err.Error(), + ) respondError(w, "Invalid request body", http.StatusBadRequest) return } if err := workspace.ValidateGitSettings(); err != nil { + log.Debug("invalid git settings provided", + "error", err.Error(), + ) respondError(w, "Invalid workspace", http.StatusBadRequest) return } workspace.UserID = ctx.UserID if err := h.DB.CreateWorkspace(&workspace); err != nil { + log.Error("failed to create workspace in database", + "error", err.Error(), + "workspaceName", workspace.Name, + ) respondError(w, "Failed to create workspace", http.StatusInternalServerError) return } if err := h.Storage.InitializeUserWorkspace(workspace.UserID, workspace.ID); err != nil { + log.Error("failed to initialize workspace directory", + "error", err.Error(), + "workspaceID", workspace.ID, + ) respondError(w, "Failed to initialize workspace directory", http.StatusInternalServerError) return } if workspace.GitEnabled { + log.Debug("setting up git repository", + "workspaceID", workspace.ID, + "gitURL", workspace.GitURL, + ) + if err := h.Storage.SetupGitRepo( ctx.UserID, workspace.ID, @@ -101,11 +141,20 @@ func (h *Handler) CreateWorkspace() http.HandlerFunc { workspace.GitCommitName, workspace.GitCommitEmail, ); err != nil { + log.Error("failed to setup git repository", + "error", err.Error(), + "workspaceID", workspace.ID, + ) respondError(w, "Failed to setup git repo: "+err.Error(), http.StatusInternalServerError) return } } + log.Info("workspace created", + "workspaceID", workspace.ID, + "workspaceName", workspace.Name, + "gitEnabled", workspace.GitEnabled, + ) respondJSON(w, workspace) } } @@ -171,9 +220,18 @@ func (h *Handler) UpdateWorkspace() http.HandlerFunc { if !ok { return } + log := getWorkspaceLogger().With( + "handler", "UpdateWorkspace", + "userID", ctx.UserID, + "workspaceID", ctx.Workspace.ID, + "clientIP", r.RemoteAddr, + ) var workspace models.Workspace if err := json.NewDecoder(r.Body).Decode(&workspace); err != nil { + log.Debug("invalid request body received", + "error", err.Error(), + ) respondError(w, "Invalid request body", http.StatusBadRequest) return } @@ -184,13 +242,28 @@ func (h *Handler) UpdateWorkspace() http.HandlerFunc { // Validate the workspace if err := workspace.Validate(); err != nil { + log.Debug("invalid workspace configuration", + "error", err.Error(), + ) respondError(w, err.Error(), http.StatusBadRequest) return } + // Track what's changed for logging + changes := map[string]bool{ + "gitSettings": gitSettingsChanged(&workspace, ctx.Workspace), + "name": workspace.Name != ctx.Workspace.Name, + "theme": workspace.Theme != ctx.Workspace.Theme, + "autoSave": workspace.AutoSave != ctx.Workspace.AutoSave, + } + // Handle Git repository setup/teardown if Git settings changed - if gitSettingsChanged(&workspace, ctx.Workspace) { + if changes["gitSettings"] { if workspace.GitEnabled { + log.Debug("updating git repository configuration", + "gitURL", workspace.GitURL, + ) + if err := h.Storage.SetupGitRepo( ctx.UserID, ctx.Workspace.ID, @@ -200,20 +273,29 @@ func (h *Handler) UpdateWorkspace() http.HandlerFunc { workspace.GitCommitName, workspace.GitCommitEmail, ); err != nil { + log.Error("failed to setup git repository", + "error", err.Error(), + ) respondError(w, "Failed to setup git repo: "+err.Error(), http.StatusInternalServerError) return } - } else { + log.Debug("disabling git repository") h.Storage.DisableGitRepo(ctx.UserID, ctx.Workspace.ID) } } if err := h.DB.UpdateWorkspace(&workspace); err != nil { + log.Error("failed to update workspace in database", + "error", err.Error(), + ) respondError(w, "Failed to update workspace", http.StatusInternalServerError) return } + log.Debug("workspace updated", + "changes", changes, + ) respondJSON(w, workspace) } } @@ -241,15 +323,25 @@ func (h *Handler) DeleteWorkspace() http.HandlerFunc { if !ok { return } + log := getWorkspaceLogger().With( + "handler", "DeleteWorkspace", + "userID", ctx.UserID, + "workspaceID", ctx.Workspace.ID, + "clientIP", r.RemoteAddr, + ) // Check if this is the user's last workspace workspaces, err := h.DB.GetWorkspacesByUserID(ctx.UserID) if err != nil { + log.Error("failed to fetch workspaces from database", + "error", err.Error(), + ) respondError(w, "Failed to get workspaces", http.StatusInternalServerError) return } if len(workspaces) <= 1 { + log.Debug("attempted to delete last workspace") respondError(w, "Cannot delete the last workspace", http.StatusBadRequest) return } @@ -265,14 +357,19 @@ func (h *Handler) DeleteWorkspace() http.HandlerFunc { } } - // Start transaction tx, err := h.DB.Begin() if err != nil { + log.Error("failed to start database transaction", + "error", err.Error(), + ) respondError(w, "Failed to start transaction", http.StatusInternalServerError) return } defer func() { if err := tx.Rollback(); err != nil && err != sql.ErrTxDone { + log.Error("failed to rollback transaction", + "error", err.Error(), + ) respondError(w, "Failed to rollback transaction", http.StatusInternalServerError) } }() @@ -280,6 +377,10 @@ func (h *Handler) DeleteWorkspace() http.HandlerFunc { // Update last workspace ID first err = h.DB.UpdateLastWorkspaceTx(tx, ctx.UserID, nextWorkspaceID) if err != nil { + log.Error("failed to update last workspace reference", + "error", err.Error(), + "nextWorkspaceID", nextWorkspaceID, + ) respondError(w, "Failed to update last workspace", http.StatusInternalServerError) return } @@ -287,16 +388,27 @@ func (h *Handler) DeleteWorkspace() http.HandlerFunc { // Delete the workspace err = h.DB.DeleteWorkspaceTx(tx, ctx.Workspace.ID) if err != nil { + log.Error("failed to delete workspace from database", + "error", err.Error(), + ) respondError(w, "Failed to delete workspace", http.StatusInternalServerError) return } // Commit transaction if err = tx.Commit(); err != nil { + log.Error("failed to commit transaction", + "error", err.Error(), + ) respondError(w, "Failed to commit transaction", http.StatusInternalServerError) return } + log.Info("workspace deleted", + "workspaceName", ctx.Workspace.Name, + "nextWorkspaceName", nextWorkspaceName, + ) + // Return the next workspace ID in the response so frontend knows where to redirect respondJSON(w, &DeleteWorkspaceResponse{NextWorkspaceName: nextWorkspaceName}) } @@ -318,13 +430,24 @@ func (h *Handler) GetLastWorkspaceName() http.HandlerFunc { if !ok { return } + log := getWorkspaceLogger().With( + "handler", "GetLastWorkspaceName", + "userID", ctx.UserID, + "clientIP", r.RemoteAddr, + ) workspaceName, err := h.DB.GetLastWorkspaceName(ctx.UserID) if err != nil { + log.Error("failed to fetch last workspace name", + "error", err.Error(), + ) respondError(w, "Failed to get last workspace", http.StatusInternalServerError) return } + log.Debug("last workspace name retrieved", + "workspaceName", workspaceName, + ) respondJSON(w, &LastWorkspaceNameResponse{LastWorkspaceName: workspaceName}) } } @@ -347,21 +470,36 @@ func (h *Handler) UpdateLastWorkspaceName() http.HandlerFunc { if !ok { return } + log := getWorkspaceLogger().With( + "handler", "UpdateLastWorkspaceName", + "userID", ctx.UserID, + "clientIP", r.RemoteAddr, + ) var requestBody struct { WorkspaceName string `json:"workspaceName"` } if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil { + log.Debug("invalid request body received", + "error", err.Error(), + ) respondError(w, "Invalid request body", http.StatusBadRequest) return } if err := h.DB.UpdateLastWorkspace(ctx.UserID, requestBody.WorkspaceName); err != nil { + log.Error("failed to update last workspace", + "error", err.Error(), + "workspaceName", requestBody.WorkspaceName, + ) respondError(w, "Failed to update last workspace", http.StatusInternalServerError) return } + log.Debug("last workspace name updated", + "workspaceName", requestBody.WorkspaceName, + ) w.WriteHeader(http.StatusNoContent) } } From f6de4fb839e6a5453359725aa13e82689a3f4c24 Mon Sep 17 00:00:00 2001 From: LordMathis Date: Wed, 18 Dec 2024 22:16:20 +0100 Subject: [PATCH 18/22] Log the config after loading --- server/cmd/server/main.go | 7 ++++- server/internal/app/config.go | 43 +++++------------------------- server/internal/logging/logger.go | 22 +++++++++++++++ server/internal/secrets/secrets.go | 2 -- 4 files changed, 34 insertions(+), 40 deletions(-) diff --git a/server/cmd/server/main.go b/server/cmd/server/main.go index e7fc445..cd7dc2c 100644 --- a/server/cmd/server/main.go +++ b/server/cmd/server/main.go @@ -5,6 +5,7 @@ import ( "log" "novamd/internal/app" + "novamd/internal/logging" ) // @title NovaMD API @@ -23,6 +24,10 @@ func main() { log.Fatal("Failed to load configuration:", err) } + // Setup logging + logging.Setup(cfg.LogLevel) + logging.Debug("Configuration loaded", "config", logging.Redact(cfg)) + // Initialize and start server options, err := app.DefaultOptions(cfg) if err != nil { @@ -32,7 +37,7 @@ func main() { server := app.NewServer(options) defer func() { if err := server.Close(); err != nil { - log.Println("Error closing server:", err) + logging.Error("Failed to close server:", err) } }() diff --git a/server/internal/app/config.go b/server/internal/app/config.go index 08a4815..0b70a82 100644 --- a/server/internal/app/config.go +++ b/server/internal/app/config.go @@ -19,10 +19,10 @@ type Config struct { RootURL string Domain string CORSOrigins []string - AdminEmail string - AdminPassword string - EncryptionKey string - JWTSigningKey string + AdminEmail string `log:"redact"` + AdminPassword string `log:"redact"` + EncryptionKey string `log:"redact"` + JWTSigningKey string `log:"redact"` RateLimitRequests int RateLimitWindow time.Duration IsDevelopment bool @@ -58,46 +58,37 @@ func (c *Config) validate() error { // LoadConfig creates a new Config instance with values from environment variables func LoadConfig() (*Config, error) { - logging.Info("loading configuration from environment variables") config := DefaultConfig() if env := os.Getenv("NOVAMD_ENV"); env != "" { - logging.Debug("loading config for environment", "env", env) config.IsDevelopment = env == "development" } if dbPath := os.Getenv("NOVAMD_DB_PATH"); dbPath != "" { - logging.Debug("loading config for database path", "path", dbPath) config.DBPath = dbPath } if workDir := os.Getenv("NOVAMD_WORKDIR"); workDir != "" { - logging.Debug("loading config for work directory", "dir", workDir) config.WorkDir = workDir } if staticPath := os.Getenv("NOVAMD_STATIC_PATH"); staticPath != "" { - logging.Debug("loading config for static path", "path", staticPath) config.StaticPath = staticPath } if port := os.Getenv("NOVAMD_PORT"); port != "" { - logging.Debug("loading config for port", "port", port) config.Port = port } if rootURL := os.Getenv("NOVAMD_ROOT_URL"); rootURL != "" { - logging.Debug("loading config for root URL", "url", rootURL) config.RootURL = rootURL } if domain := os.Getenv("NOVAMD_DOMAIN"); domain != "" { - logging.Debug("loading config for domain", "domain", domain) config.Domain = domain } if corsOrigins := os.Getenv("NOVAMD_CORS_ORIGINS"); corsOrigins != "" { - logging.Debug("loading config for CORS origins", "origins", corsOrigins) config.CORSOrigins = strings.Split(corsOrigins, ",") } @@ -106,35 +97,17 @@ func LoadConfig() (*Config, error) { config.EncryptionKey = os.Getenv("NOVAMD_ENCRYPTION_KEY") config.JWTSigningKey = os.Getenv("NOVAMD_JWT_SIGNING_KEY") - logging.Debug("sensitive configuration loaded", - "adminEmailSet", config.AdminEmail != "", - "adminPasswordSet", config.AdminPassword != "", - "encryptionKeySet", config.EncryptionKey != "", - "jwtSigningKeySet", config.JWTSigningKey != "") - // Configure rate limiting if reqStr := os.Getenv("NOVAMD_RATE_LIMIT_REQUESTS"); reqStr != "" { parsed, err := strconv.Atoi(reqStr) - if err != nil { - logging.Warn("invalid rate limit requests value, using default", - "value", reqStr, - "default", config.RateLimitRequests, - "error", err) - } else { - logging.Debug("loading config for rate limit requests", "requests", parsed) + if err == nil { config.RateLimitRequests = parsed } } if windowStr := os.Getenv("NOVAMD_RATE_LIMIT_WINDOW"); windowStr != "" { parsed, err := time.ParseDuration(windowStr) - if err != nil { - logging.Warn("invalid rate limit window value, using default", - "value", windowStr, - "default", config.RateLimitWindow, - "error", err) - } else { - logging.Debug("loading config for rate limit window", "window", parsed) + if err == nil { config.RateLimitWindow = parsed } } @@ -142,13 +115,10 @@ func LoadConfig() (*Config, error) { // Configure log level, if isDevelopment is set, default to debug if logLevel := os.Getenv("NOVAMD_LOG_LEVEL"); logLevel != "" { parsed := logging.ParseLogLevel(logLevel) - logging.Debug("loading config for log level", "level", parsed) config.LogLevel = parsed } else if config.IsDevelopment { - logging.Debug("setting log level to debug for development") config.LogLevel = logging.DEBUG } else { - logging.Debug("setting log level to info for production") config.LogLevel = logging.INFO } @@ -157,6 +127,5 @@ func LoadConfig() (*Config, error) { return nil, err } - logging.Info("configuration loaded successfully") return config, nil } diff --git a/server/internal/logging/logger.go b/server/internal/logging/logger.go index e05ea32..7e23261 100644 --- a/server/internal/logging/logger.go +++ b/server/internal/logging/logger.go @@ -4,6 +4,7 @@ package logging import ( "log/slog" "os" + "reflect" ) // Logger represents the interface for logging operations @@ -60,6 +61,27 @@ func ParseLogLevel(level string) LogLevel { } } +// Redact redacts sensitive fields from a struct based on the `log` struct tag +// if the tag is set to "redact" the field value is replaced with "[REDACTED]" +func Redact(v any) map[string]any { + result := make(map[string]any) + val := reflect.ValueOf(v) + typ := val.Type() + + for i := 0; i < val.NumField(); i++ { + field := typ.Field(i) + if tag := field.Tag.Get("log"); tag != "" { + switch tag { + case "redact": + result[field.Name] = "[REDACTED]" + default: + result[field.Name] = val.Field(i).Interface() + } + } + } + return result +} + // Implementation of Logger interface methods func (l *logger) Debug(msg string, args ...any) { l.logger.Debug(msg, args...) diff --git a/server/internal/secrets/secrets.go b/server/internal/secrets/secrets.go index 9a66824..a79ef7d 100644 --- a/server/internal/secrets/secrets.go +++ b/server/internal/secrets/secrets.go @@ -33,8 +33,6 @@ func getLogger() logging.Logger { // ValidateKey checks if the provided base64-encoded key is suitable for AES-256 func ValidateKey(key string) error { - log := getLogger() - log.Debug("validating encryption key") _, err := decodeAndValidateKey(key) return err } From 0aa67f5cc2846e67f717c4f2b50358c58ba0a61a Mon Sep 17 00:00:00 2001 From: LordMathis Date: Wed, 18 Dec 2024 22:17:22 +0100 Subject: [PATCH 19/22] Ignore env dev file --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7cbeb64..1c632f7 100644 --- a/.gitignore +++ b/.gitignore @@ -157,6 +157,7 @@ go.work.sum # env file .env +.env.dev main *.db From b0659382112add904428818dfd79f11a4897166d Mon Sep 17 00:00:00 2001 From: LordMathis Date: Thu, 19 Dec 2024 22:00:42 +0100 Subject: [PATCH 20/22] Remove too many debug messages --- server/internal/app/init.go | 30 ---- server/internal/app/routes.go | 6 - server/internal/auth/jwt.go | 9 -- server/internal/auth/middleware.go | 50 +++--- server/internal/auth/session.go | 13 +- server/internal/context/middleware.go | 11 -- server/internal/context/middleware_test.go | 6 +- server/internal/db/db.go | 12 -- server/internal/db/migrations.go | 12 +- server/internal/db/sessions.go | 32 +--- server/internal/db/system.go | 22 +-- server/internal/db/users.go | 81 +--------- server/internal/db/workspaces.go | 146 +----------------- server/internal/git/client.go | 57 +++---- server/internal/handlers/admin_handlers.go | 2 +- server/internal/handlers/auth_handlers.go | 2 +- server/internal/handlers/file_handlers.go | 26 ---- server/internal/handlers/git_handlers.go | 6 - server/internal/handlers/static_handler.go | 14 -- server/internal/handlers/user_handlers.go | 5 +- .../internal/handlers/workspace_handlers.go | 22 --- server/internal/secrets/secrets.go | 2 - server/internal/storage/files.go | 52 ------- server/internal/storage/git.go | 32 +--- server/internal/storage/service.go | 5 - server/internal/storage/workspace.go | 6 - 26 files changed, 75 insertions(+), 586 deletions(-) diff --git a/server/internal/app/init.go b/server/internal/app/init.go index dd003f7..74073e8 100644 --- a/server/internal/app/init.go +++ b/server/internal/app/init.go @@ -23,7 +23,6 @@ func initSecretsService(cfg *Config) (secrets.Service, error) { if err != nil { return nil, fmt.Errorf("failed to initialize secrets service: %w", err) } - logging.Debug("secrets service initialized") return secretsService, nil } @@ -36,12 +35,10 @@ func initDatabase(cfg *Config, secretsService secrets.Service) (db.Database, err return nil, fmt.Errorf("failed to initialize database: %w", err) } - logging.Debug("running database migrations") if err := database.Migrate(); err != nil { return nil, fmt.Errorf("failed to apply database migrations: %w", err) } - logging.Debug("database initialization complete") return database, nil } @@ -61,14 +58,8 @@ func initAuth(cfg *Config, database db.Database) (auth.JWTManager, auth.SessionM if err != nil { return nil, nil, nil, fmt.Errorf("failed to ensure JWT secret: %w", err) } - logging.Debug("JWT signing key generated") } - logging.Debug("initializing JWT service", - "accessTokenExpiry", accessTokeExpiry.String(), - "refreshTokenExpiry", refreshTokenExpiry.String()) - - // Initialize JWT service jwtManager, err := auth.NewJWTService(auth.JWTConfig{ SigningKey: signingKey, AccessTokenExpiry: accessTokeExpiry, @@ -78,24 +69,14 @@ func initAuth(cfg *Config, database db.Database) (auth.JWTManager, auth.SessionM return nil, nil, nil, fmt.Errorf("failed to initialize JWT service: %w", err) } - // Initialize session service - logging.Debug("initializing session service") sessionManager := auth.NewSessionService(database, jwtManager) - - // Initialize cookie service - logging.Debug("initializing cookie service", - "isDevelopment", cfg.IsDevelopment, - "domain", cfg.Domain) cookieService := auth.NewCookieService(cfg.IsDevelopment, cfg.Domain) - logging.Debug("authentication services initialized") return jwtManager, sessionManager, cookieService, nil } // setupAdminUser creates the admin user if it doesn't exist func setupAdminUser(database db.Database, storageManager storage.Manager, cfg *Config) error { - logging.Debug("checking for existing admin user", "email", cfg.AdminEmail) - // Check if admin user exists adminUser, err := database.GetUserByEmail(cfg.AdminEmail) if err != nil && err != sql.ErrNoRows { @@ -107,8 +88,6 @@ func setupAdminUser(database db.Database, storageManager storage.Manager, cfg *C return nil } - logging.Debug("creating new admin user") - // Hash the password hashedPassword, err := bcrypt.GenerateFromPassword([]byte(cfg.AdminPassword), bcrypt.DefaultCost) if err != nil { @@ -128,15 +107,6 @@ func setupAdminUser(database db.Database, storageManager storage.Manager, cfg *C return fmt.Errorf("failed to create admin user: %w", err) } - logging.Debug("admin user created", - "userId", createdUser.ID, - "workspaceId", createdUser.LastWorkspaceID) - - // Initialize workspace directory - logging.Debug("initializing admin workspace directory", - "userId", createdUser.ID, - "workspaceId", createdUser.LastWorkspaceID) - err = storageManager.InitializeUserWorkspace(createdUser.ID, createdUser.LastWorkspaceID) if err != nil { return fmt.Errorf("failed to initialize admin workspace: %w", err) diff --git a/server/internal/app/routes.go b/server/internal/app/routes.go index b22e1f8..4d626c7 100644 --- a/server/internal/app/routes.go +++ b/server/internal/app/routes.go @@ -31,7 +31,6 @@ func setupRouter(o Options) *chi.Mux { r.Use(middleware.Timeout(30 * time.Second)) // Security headers - logging.Debug("setting up security headers") r.Use(secure.New(secure.Options{ SSLRedirect: false, SSLProxyHeaders: map[string]string{"X-Forwarded-Proto": "https"}, @@ -39,7 +38,6 @@ func setupRouter(o Options) *chi.Mux { }).Handler) // CORS if origins are configured - logging.Debug("setting up CORS") if len(o.Config.CORSOrigins) > 0 { r.Use(cors.Handler(cors.Options{ AllowedOrigins: o.Config.CORSOrigins, @@ -52,7 +50,6 @@ func setupRouter(o Options) *chi.Mux { } // Initialize auth middleware and handler - logging.Debug("setting up authentication middleware") authMiddleware := auth.NewMiddleware(o.JWTManager, o.SessionManager, o.CookieService) handler := &handlers.Handler{ DB: o.Database, @@ -60,14 +57,12 @@ func setupRouter(o Options) *chi.Mux { } if o.Config.IsDevelopment { - logging.Debug("setting up Swagger docs") r.Get("/swagger/*", httpSwagger.Handler( httpSwagger.URL("/swagger/doc.json"), // The URL pointing to API definition )) } // API routes - logging.Debug("setting up API routes") r.Route("/api/v1", func(r chi.Router) { // Rate limiting for API routes if o.Config.RateLimitRequests > 0 { @@ -154,7 +149,6 @@ func setupRouter(o Options) *chi.Mux { }) // Handle all other routes with static file server - logging.Debug("setting up static file server") r.Get("/*", handlers.NewStaticHandler(o.Config.StaticPath).ServeHTTP) return r diff --git a/server/internal/auth/jwt.go b/server/internal/auth/jwt.go index 7746b6c..8953440 100644 --- a/server/internal/auth/jwt.go +++ b/server/internal/auth/jwt.go @@ -61,11 +61,9 @@ func NewJWTService(config JWTConfig) (JWTManager, error) { // Set default expiry times if not provided if config.AccessTokenExpiry == 0 { config.AccessTokenExpiry = 15 * time.Minute - log.Debug("using default access token expiry", "expiry", config.AccessTokenExpiry) } if config.RefreshTokenExpiry == 0 { config.RefreshTokenExpiry = 7 * 24 * time.Hour - log.Debug("using default refresh token expiry", "expiry", config.RefreshTokenExpiry) } log.Info("initialized JWT service", @@ -87,7 +85,6 @@ func (s *jwtService) GenerateRefreshToken(userID int, role, sessionID string) (s // generateToken is an internal helper function that creates a new JWT token func (s *jwtService) generateToken(userID int, role string, sessionID string, tokenType TokenType, expiry time.Duration) (string, error) { - log := getJWTLogger() now := time.Now() // Add a random nonce to ensure uniqueness @@ -114,12 +111,6 @@ func (s *jwtService) generateToken(userID int, role string, sessionID string, to return "", err } - log.Debug("generated JWT token", - "userId", userID, - "role", role, - "tokenType", tokenType, - "expiresAt", claims.ExpiresAt) - return signedToken, nil } diff --git a/server/internal/auth/middleware.go b/server/internal/auth/middleware.go index 7e8809f..e9fe458 100644 --- a/server/internal/auth/middleware.go +++ b/server/internal/auth/middleware.go @@ -20,9 +20,6 @@ type Middleware struct { // NewMiddleware creates a new authentication middleware func NewMiddleware(jwtManager JWTManager, sessionManager SessionManager, cookieManager CookieManager) *Middleware { - log := getMiddlewareLogger() - log.Info("initialized auth middleware") - return &Middleware{ jwtManager: jwtManager, sessionManager: sessionManager, @@ -33,11 +30,15 @@ func NewMiddleware(jwtManager JWTManager, sessionManager SessionManager, cookieM // Authenticate middleware validates JWT tokens and sets user information in context func (m *Middleware) Authenticate(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - log := getMiddlewareLogger() + log := getMiddlewareLogger().With( + "handler", "Authenticate", + "clientIP", r.RemoteAddr, + ) // Extract token from cookie cookie, err := r.Cookie("access_token") if err != nil { + log.Warn("attempt to access protected route without token") http.Error(w, "Unauthorized", http.StatusUnauthorized) return } @@ -45,12 +46,14 @@ func (m *Middleware) Authenticate(next http.Handler) http.Handler { // Validate token claims, err := m.jwtManager.ValidateToken(cookie.Value) if err != nil { + log.Warn("attempt to access protected route with invalid token", "error", err.Error()) http.Error(w, "Invalid token", http.StatusUnauthorized) return } // Check token type if claims.Type != AccessToken { + log.Warn("attempt to access protected route with invalid token type", "type", claims.Type) http.Error(w, "Invalid token type", http.StatusUnauthorized) return } @@ -58,6 +61,7 @@ func (m *Middleware) Authenticate(next http.Handler) http.Handler { // Check if session is still valid in database session, err := m.sessionManager.ValidateSession(claims.ID) if err != nil || session == nil { + log.Warn("attempt to access protected route with invalid session", "error", err.Error()) m.cookieManager.InvalidateCookie("access_token") m.cookieManager.InvalidateCookie("refresh_token") m.cookieManager.InvalidateCookie("csrf_token") @@ -69,17 +73,20 @@ func (m *Middleware) Authenticate(next http.Handler) http.Handler { if r.Method != http.MethodGet && r.Method != http.MethodHead && r.Method != http.MethodOptions { csrfCookie, err := r.Cookie("csrf_token") if err != nil { + log.Warn("attempt to access protected route without CSRF token", "error", err.Error()) http.Error(w, "CSRF cookie not found", http.StatusForbidden) return } csrfHeader := r.Header.Get("X-CSRF-Token") if csrfHeader == "" { + log.Warn("attempt to access protected route without CSRF header") http.Error(w, "CSRF token header not found", http.StatusForbidden) return } if subtle.ConstantTimeCompare([]byte(csrfCookie.Value), []byte(csrfHeader)) != 1 { + log.Warn("attempt to access protected route with invalid CSRF token") http.Error(w, "CSRF token mismatch", http.StatusForbidden) return } @@ -91,12 +98,6 @@ func (m *Middleware) Authenticate(next http.Handler) http.Handler { UserRole: claims.Role, } - log.Debug("authentication completed", - "userId", claims.UserID, - "role", claims.Role, - "method", r.Method, - "path", r.URL.Path) - // Add context to request and continue next.ServeHTTP(w, context.WithHandlerContext(r, hctx)) }) @@ -106,7 +107,11 @@ func (m *Middleware) Authenticate(next http.Handler) http.Handler { func (m *Middleware) RequireRole(role string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - log := getMiddlewareLogger() + log := getMiddlewareLogger().With( + "handler", "RequireRole", + "requiredRole", role, + "clientIP", r.RemoteAddr, + ) ctx, ok := context.GetRequestContext(w, r) if !ok { @@ -114,15 +119,11 @@ func (m *Middleware) RequireRole(role string) func(http.Handler) http.Handler { } if ctx.UserRole != role && ctx.UserRole != "admin" { + log.Warn("attempt to access protected route without required role") http.Error(w, "Insufficient permissions", http.StatusForbidden) return } - log.Debug("role requirement satisfied", - "requiredRole", role, - "userRole", ctx.UserRole, - "path", r.URL.Path) - next.ServeHTTP(w, r) }) } @@ -131,14 +132,19 @@ func (m *Middleware) RequireRole(role string) func(http.Handler) http.Handler { // RequireWorkspaceAccess returns a middleware that ensures the user has access to the workspace func (m *Middleware) RequireWorkspaceAccess(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - log := getMiddlewareLogger() - ctx, ok := context.GetRequestContext(w, r) if !ok { return } - // If no workspace in context, allow the request (might be a non-workspace endpoint) + log := getMiddlewareLogger().With( + "handler", "RequireWorkspaceAccess", + "clientIP", r.RemoteAddr, + "userId", ctx.UserID, + "workspaceId", ctx.Workspace.ID, + ) + + // If no workspace in context, allow the request if ctx.Workspace == nil { next.ServeHTTP(w, r) return @@ -146,15 +152,11 @@ func (m *Middleware) RequireWorkspaceAccess(next http.Handler) http.Handler { // Check if user has access (either owner or admin) if ctx.Workspace.UserID != ctx.UserID && ctx.UserRole != "admin" { + log.Warn("attempt to access workspace without permission") http.Error(w, "Not Found", http.StatusNotFound) return } - log.Debug("workspace access granted", - "userId", ctx.UserID, - "workspaceId", ctx.Workspace.ID, - "path", r.URL.Path) - next.ServeHTTP(w, r) }) } diff --git a/server/internal/auth/session.go b/server/internal/auth/session.go index ca7e682..ba32dc8 100644 --- a/server/internal/auth/session.go +++ b/server/internal/auth/session.go @@ -32,9 +32,6 @@ type sessionManager struct { // NewSessionService creates a new session service with the given database and JWT manager // revive:disable:unexported-return func NewSessionService(db db.SessionStore, jwtManager JWTManager) *sessionManager { - log := getSessionLogger() - log.Info("initialized session manager") - return &sessionManager{ db: db, jwtManager: jwtManager, @@ -90,9 +87,7 @@ func (s *sessionManager) CreateSession(userID int, role string) (*models.Session // RefreshSession creates a new access token using a refreshToken func (s *sessionManager) RefreshSession(refreshToken string) (string, error) { - log := getSessionLogger() - - // Get session from database first + // Get session from database session, err := s.db.GetSessionByRefreshToken(refreshToken) if err != nil { return "", fmt.Errorf("invalid session: %w", err) @@ -104,7 +99,6 @@ func (s *sessionManager) RefreshSession(refreshToken string) (string, error) { return "", fmt.Errorf("invalid refresh token: %w", err) } - // Double check that the claims match the session if claims.UserID != session.UserID { return "", fmt.Errorf("token does not match session") } @@ -115,11 +109,6 @@ func (s *sessionManager) RefreshSession(refreshToken string) (string, error) { return "", err } - log.Debug("refreshed session", - "userId", claims.UserID, - "role", claims.Role, - "sessionId", session.ID) - return newToken, nil } diff --git a/server/internal/context/middleware.go b/server/internal/context/middleware.go index d2da13c..be20ea3 100644 --- a/server/internal/context/middleware.go +++ b/server/internal/context/middleware.go @@ -26,11 +26,6 @@ func WithUserContextMiddleware(next http.Handler) http.Handler { UserRole: claims.Role, } - log.Debug("user context extracted from claims", - "userID", claims.UserID, - "role", claims.Role, - "path", r.URL.Path) - r = WithHandlerContext(r, hctx) next.ServeHTTP(w, r) }) @@ -58,12 +53,6 @@ func WithWorkspaceContextMiddleware(db db.WorkspaceReader) func(http.Handler) ht return } - log.Debug("workspace context added", - "userID", ctx.UserID, - "workspaceID", workspace.ID, - "workspaceName", workspace.Name, - "path", r.URL.Path) - ctx.Workspace = workspace r = WithHandlerContext(r, ctx) next.ServeHTTP(w, r) diff --git a/server/internal/context/middleware_test.go b/server/internal/context/middleware_test.go index eae1c97..f57a230 100644 --- a/server/internal/context/middleware_test.go +++ b/server/internal/context/middleware_test.go @@ -89,6 +89,10 @@ func TestWithUserContextMiddleware(t *testing.T) { } } +type contextKey string + +const workspaceNameKey contextKey = "workspaceName" + func TestWithWorkspaceContextMiddleware(t *testing.T) { tests := []struct { name string @@ -158,7 +162,7 @@ func TestWithWorkspaceContextMiddleware(t *testing.T) { } // Add workspace name to request context via chi URL params - req = req.WithContext(stdctx.WithValue(req.Context(), "workspaceName", tt.workspaceName)) + req = req.WithContext(stdctx.WithValue(req.Context(), workspaceNameKey, tt.workspaceName)) nextCalled := false next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/server/internal/db/db.go b/server/internal/db/db.go index d8ccdd1..6a23bff 100644 --- a/server/internal/db/db.go +++ b/server/internal/db/db.go @@ -113,7 +113,6 @@ 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 { @@ -136,7 +135,6 @@ func Init(dbPath string, secretsService secrets.Service) (Database, error) { secretsService: secretsService, } - log.Info("database initialized successfully") return database, nil } @@ -148,17 +146,12 @@ func (db *database) Close() error { if err := db.DB.Close(); err != nil { 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 } @@ -167,15 +160,11 @@ func (db *database) encryptToken(token string) (string, error) { 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 } @@ -184,6 +173,5 @@ func (db *database) decryptToken(token string) (string, error) { 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 dfa0100..5182844 100644 --- a/server/internal/db/migrations.go +++ b/server/internal/db/migrations.go @@ -82,7 +82,6 @@ func (db *database) Migrate() error { 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 )`) @@ -91,58 +90,49 @@ func (db *database) Migrate() error { } // 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 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 := log.With("migration_version", migration.Version) - log.Info("applying migration") - tx, err := db.Begin() if err != nil { 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 { if rbErr := tx.Rollback(); rbErr != nil { return fmt.Errorf("migration %d failed: %v, rollback failed: %v", migration.Version, err, rbErr) } - 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 { if rbErr := tx.Rollback(); rbErr != nil { return fmt.Errorf("failed to update migration version: %v, rollback failed: %v", err, rbErr) } - 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: %w", migration.Version, err) } currentVersion = migration.Version - log.Info("migration applied successfully", "new_version", currentVersion) + log.Debug("migration applied", "new_version", currentVersion) } } diff --git a/server/internal/db/sessions.go b/server/internal/db/sessions.go index ce5ba16..28ce752 100644 --- a/server/internal/db/sessions.go +++ b/server/internal/db/sessions.go @@ -10,12 +10,6 @@ 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 (?, ?, ?, ?, ?)`, @@ -25,17 +19,11 @@ func (db *database) CreateSession(session *models.Session) error { 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 @@ -45,24 +33,17 @@ 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 { 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 @@ -72,24 +53,17 @@ func (db *database) GetSessionByID(sessionID string) (*models.Session, error) { ).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 { 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 { return fmt.Errorf("failed to delete session: %w", err) @@ -101,19 +75,15 @@ func (db *database) DeleteSession(sessionID string) error { } 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 { log := getLogger().WithGroup("sessions") - log.Info("cleaning expired sessions") - result, err := db.Exec("DELETE FROM sessions WHERE expires_at <= ?", time.Now()) if err != nil { return fmt.Errorf("failed to clean expired sessions: %w", err) @@ -124,6 +94,6 @@ func (db *database) CleanExpiredSessions() error { return fmt.Errorf("failed to get rows affected: %w", err) } - log.Info("expired sessions cleaned successfully", "sessions_removed", rowsAffected) + log.Info("cleaned expired sessions", "sessions_removed", rowsAffected) return nil } diff --git a/server/internal/db/system.go b/server/internal/db/system.go index 536d072..447d421 100644 --- a/server/internal/db/system.go +++ b/server/internal/db/system.go @@ -22,17 +22,13 @@ type UserStats struct { // 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 { @@ -45,30 +41,24 @@ func (db *database) EnsureJWTSecret() (string, error) { return "", fmt.Errorf("failed to store JWT secret: %w", err) } - log.Info("new JWT secret generated and stored successfully") + log.Info("new JWT secret generated and stored") + 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 { 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 (?, ?) @@ -79,7 +69,6 @@ func (db *database) SetSystemSetting(key, value string) error { return fmt.Errorf("failed to store system setting: %w", err) } - log.Info("system setting stored successfully", "key", key) return nil } @@ -95,15 +84,11 @@ func generateRandomSecret(bytes int) (string, error) { } 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 @@ -111,14 +96,12 @@ func (db *database) GetSystemStats() (*UserStats, error) { if err != nil { 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, 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(` @@ -129,6 +112,5 @@ func (db *database) GetSystemStats() (*UserStats, error) { if err != nil { return nil, fmt.Errorf("failed to get active users count: %w", err) } - log.Debug("got active users count", "count", stats.ActiveUsers) return stats, nil } diff --git a/server/internal/db/users.go b/server/internal/db/users.go index 5f23fe8..1d04cc2 100644 --- a/server/internal/db/users.go +++ b/server/internal/db/users.go @@ -9,9 +9,7 @@ import ( // CreateUser inserts a new user record into the database func (db *database) CreateUser(user *models.User) (*models.User, error) { log := getLogger().WithGroup("users") - log.Debug("creating new user", - "email", user.Email, - "role", user.Role) + log.Debug("creating user", "email", user.Email) tx, err := db.Begin() if err != nil { @@ -40,7 +38,6 @@ func (db *database) CreateUser(user *models.User) (*models.User, error) { } // 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", @@ -54,9 +51,6 @@ func (db *database) CreateUser(user *models.User) (*models.User, error) { } // 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, fmt.Errorf("failed to update last workspace ID: %w", err) @@ -67,21 +61,15 @@ func (db *database) CreateUser(user *models.User) (*models.User, error) { return nil, fmt.Errorf("failed to commit transaction: %w", err) } + log.Debug("created user", "user_id", user.ID) + user.LastWorkspaceID = defaultWorkspace.ID - log.Info("user created", - "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, @@ -106,16 +94,13 @@ func (db *database) createWorkspaceTx(tx *sql.Tx, workspace *models.Workspace) e } workspace.ID = int(id) - log.Debug("workspace created successfully", + log.Debug("created user workspace", "workspace_id", workspace.ID, "user_id", workspace.UserID) return nil } 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 @@ -127,21 +112,15 @@ func (db *database) GetUserByID(id int) (*models.User, error) { &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 { return nil, fmt.Errorf("failed to fetch user: %w", err) } - - log.Debug("user retrieved successfully", "user_id", id) return user, nil } 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 @@ -153,19 +132,16 @@ func (db *database) GetUserByEmail(email string) (*models.User, error) { &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, fmt.Errorf("failed to fetch user: %w", err) } - log.Debug("user retrieved successfully", "user_id", user.ID) return user, nil } func (db *database) UpdateUser(user *models.User) error { - log := getLogger().WithGroup("users") result, err := db.Exec(` UPDATE users SET email = ?, display_name = ?, password_hash = ?, role = ?, last_workspace_id = ? @@ -183,18 +159,13 @@ func (db *database) UpdateUser(user *models.User) error { } if rowsAffected == 0 { - log.Warn("no user found to update", "user_id", user.ID) return fmt.Errorf("user not found") } - log.Info("user updated", "user_id", user.ID) return nil } 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, @@ -219,16 +190,10 @@ func (db *database) GetAllUsers() ([]*models.User, error) { users = append(users, user) } - log.Debug("users retrieved successfully", "count", len(users)) return users, nil } 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 fmt.Errorf("failed to begin transaction: %w", err) @@ -253,9 +218,6 @@ func (db *database) UpdateLastWorkspace(userID int, workspaceName string) error return fmt.Errorf("failed to commit transaction: %w", err) } - log.Info("last workspace updated", - "user_id", userID, - "workspace_id", workspaceID) return nil } @@ -271,48 +233,27 @@ func (db *database) DeleteUser(id int) error { // Delete all user's workspaces first log.Debug("deleting user workspaces", "user_id", id) - result, err := tx.Exec("DELETE FROM workspaces WHERE user_id = ?", id) + _, err = tx.Exec("DELETE FROM workspaces WHERE user_id = ?", id) if err != nil { return fmt.Errorf("failed to delete workspaces: %w", err) } - workspacesDeleted, err := result.RowsAffected() - if err != nil { - return fmt.Errorf("failed to get deleted workspaces count: %w", err) - } - // Delete the user - log.Debug("deleting user record", "user_id", id) - result, err = tx.Exec("DELETE FROM users WHERE id = ?", id) + _, err = tx.Exec("DELETE FROM users WHERE id = ?", id) if err != nil { return fmt.Errorf("failed to delete user: %w", err) } - userDeleted, err := result.RowsAffected() - if err != nil { - 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 { return fmt.Errorf("failed to commit transaction: %w", err) } - log.Info("user deleted", - "user_id", id, - "workspaces_deleted", workspacesDeleted) + log.Debug("deleted user", "user_id", id) return nil } 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 @@ -323,30 +264,22 @@ func (db *database) GetLastWorkspaceName(userID int) (string, error) { Scan(&workspaceName) if err == sql.ErrNoRows { - log.Debug("no last workspace found", "user_id", userID) return "", fmt.Errorf("no last workspace found") } if err != nil { 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) if err != nil { 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 58616f0..11efb19 100644 --- a/server/internal/db/workspaces.go +++ b/server/internal/db/workspaces.go @@ -16,7 +16,6 @@ func (db *database) CreateWorkspace(workspace *models.Workspace) error { // Set default settings if not provided if workspace.Theme == "" { - log.Debug("setting default workspace settings") workspace.SetDefaultSettings() } @@ -47,17 +46,11 @@ func (db *database) CreateWorkspace(workspace *models.Workspace) error { } workspace.ID = int(id) - log.Info("workspace created", - "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 @@ -80,7 +73,6 @@ func (db *database) GetWorkspaceByID(id int) (*models.Workspace, error) { ) if err == sql.ErrNoRows { - log.Debug("workspace not found", "workspace_id", id) return nil, fmt.Errorf("workspace not found") } if err != nil { @@ -93,19 +85,11 @@ func (db *database) GetWorkspaceByID(id int) (*models.Workspace, error) { return nil, fmt.Errorf("failed to decrypt token: %w", err) } - log.Debug("workspace retrieved", - "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 @@ -128,9 +112,6 @@ func (db *database) GetWorkspaceByName(userID int, workspaceName string) (*model ) 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 { @@ -143,27 +124,18 @@ func (db *database) GetWorkspaceByName(userID int, workspaceName string) (*model 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.Debug("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 { return fmt.Errorf("failed to encrypt token: %w", err) } - result, err := db.Exec(` + _, err = db.Exec(` UPDATE workspaces SET name = ?, @@ -198,29 +170,11 @@ func (db *database) UpdateWorkspace(workspace *models.Workspace) error { return fmt.Errorf("failed to update workspace: %w", err) } - rowsAffected, err := result.RowsAffected() - if err != nil { - 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.Debug("workspace updated", - "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, @@ -265,20 +219,12 @@ func (db *database) GetWorkspacesByUserID(userID int) ([]*models.Workspace, erro 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 func (db *database) UpdateWorkspaceSettings(workspace *models.Workspace) error { - log := getLogger().WithGroup("workspaces") - log.Debug("updating workspace settings", - "workspace_id", workspace.ID, - "git_enabled", workspace.GitEnabled) - - result, err := db.Exec(` + _, err := db.Exec(` UPDATE workspaces SET theme = ?, @@ -310,142 +256,74 @@ func (db *database) UpdateWorkspaceSettings(workspace *models.Workspace) error { return fmt.Errorf("failed to update workspace settings: %w", err) } - rowsAffected, err := result.RowsAffected() - if err != nil { - 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", - "workspace_id", workspace.ID) return nil } // DeleteWorkspace removes a workspace record from the database func (db *database) DeleteWorkspace(id int) error { log := getLogger().WithGroup("workspaces") - log.Debug("deleting workspace", "workspace_id", id) - result, err := db.Exec("DELETE FROM workspaces WHERE id = ?", id) + _, err := db.Exec("DELETE FROM workspaces WHERE id = ?", id) if err != nil { return fmt.Errorf("failed to delete workspace: %w", err) } - rowsAffected, err := result.RowsAffected() - if err != nil { - 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", "workspace_id", id) + log.Debug("workspace deleted", "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 { 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 { return fmt.Errorf("failed to delete workspace in transaction: %w", err) } - rowsAffected, err := result.RowsAffected() + _, err = result.RowsAffected() if err != nil { 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", + log.Debug("workspace deleted", "workspace_id", id) return nil } // UpdateLastWorkspaceTx sets the last workspace for a user in a transaction func (db *database) UpdateLastWorkspaceTx(tx *sql.Tx, userID, workspaceID int) error { - 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 { return fmt.Errorf("failed to update last workspace in transaction: %w", err) } - rowsAffected, err := result.RowsAffected() + _, err = result.RowsAffected() if err != nil { 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 { - 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 = ?", + _, err := db.Exec("UPDATE workspaces SET last_opened_file_path = ? WHERE id = ?", filePath, workspaceID) if err != nil { return fmt.Errorf("failed to update last opened file: %w", err) } - rowsAffected, err := result.RowsAffected() - if err != nil { - 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 == sql.ErrNoRows { - log.Debug("workspace not found", "workspace_id", workspaceID) return "", fmt.Errorf("workspace not found") } if err != nil { @@ -453,21 +331,14 @@ func (db *database) GetLastOpenedFile(workspaceID int) (string, error) { } 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, @@ -510,6 +381,5 @@ func (db *database) GetAllWorkspaces() ([]*models.Workspace, error) { return nil, fmt.Errorf("error iterating workspace rows: %w", err) } - log.Debug("all workspaces retrieved successfully", "count", len(workspaces)) return workspaces, nil } diff --git a/server/internal/git/client.go b/server/internal/git/client.go index cde61d2..fdc6ce2 100644 --- a/server/internal/git/client.go +++ b/server/internal/git/client.go @@ -59,12 +59,6 @@ func getLogger() logging.Logger { // New creates a new git Client instance func New(url, username, token, workDir, commitName, commitEmail string) Client { - getLogger().Debug("creating new git client", - "url", url, - "username", username, - "workDir", workDir, - "commitName", commitName, - "commitEmail", commitEmail) return &client{ Config: Config{ URL: url, @@ -99,23 +93,19 @@ func (c *client) Clone() error { return fmt.Errorf("failed to clone repository: %w", err) } - log.Info("repository cloned", - "url", c.URL, - "workDir", c.WorkDir) return nil } // Pull pulls the latest changes from the remote repository func (c *client) Pull() error { - log := getLogger() + log := getLogger().With( + "workDir", c.WorkDir, + ) if c.repo == nil { return fmt.Errorf("repository not initialized") } - log.Debug("pulling repository changes", - "workDir", c.WorkDir) - w, err := c.repo.Worktree() if err != nil { return fmt.Errorf("failed to get worktree: %w", err) @@ -135,27 +125,24 @@ func (c *client) Pull() error { } if err == git.NoErrAlreadyUpToDate { - log.Debug("repository already up to date", - "workDir", c.WorkDir) + log.Debug("repository already up to date") } else { - log.Info("pulled repository changes", - "workDir", c.WorkDir) + log.Debug("pulled latest changes") } + return nil } // Commit commits the changes in the repository with the given message func (c *client) Commit(message string) (CommitHash, error) { - log := getLogger() + log := getLogger().With( + "workDir", c.WorkDir, + ) if c.repo == nil { return CommitHash(plumbing.ZeroHash), fmt.Errorf("repository not initialized") } - log.Debug("preparing to commit changes", - "workDir", c.WorkDir, - "message", message) - w, err := c.repo.Worktree() if err != nil { return CommitHash(plumbing.ZeroHash), fmt.Errorf("failed to get worktree: %w", err) @@ -177,24 +164,20 @@ func (c *client) Commit(message string) (CommitHash, error) { return CommitHash(plumbing.ZeroHash), fmt.Errorf("failed to commit changes: %w", err) } - log.Info("changes committed", - "hash", hash.String(), - "workDir", c.WorkDir, - "message", message) + log.Debug("changes committed") return CommitHash(hash), nil } // Push pushes the changes to the remote repository func (c *client) Push() error { - log := getLogger() + log := getLogger().With( + "workDir", c.WorkDir, + ) if c.repo == nil { return fmt.Errorf("repository not initialized") } - log.Debug("pushing repository changes", - "workDir", c.WorkDir) - auth := &http.BasicAuth{ Username: c.Username, Password: c.Token, @@ -212,7 +195,7 @@ func (c *client) Push() error { log.Debug("remote already up to date", "workDir", c.WorkDir) } else { - log.Info("pushed repository changes", + log.Debug("pushed repository changes", "workDir", c.WorkDir) } return nil @@ -220,14 +203,14 @@ func (c *client) Push() error { // EnsureRepo ensures the local repository is cloned and up-to-date func (c *client) EnsureRepo() error { - log := getLogger() + log := getLogger().With( + "workDir", c.WorkDir, + ) - log.Debug("ensuring repository exists and is up to date", - "workDir", c.WorkDir) + log.Debug("ensuring repository exists and is up to date") if _, err := os.Stat(filepath.Join(c.WorkDir, ".git")); os.IsNotExist(err) { - log.Info("repository not found, initiating clone", - "workDir", c.WorkDir) + log.Info("repository not found, initiating clone") return c.Clone() } @@ -237,7 +220,5 @@ func (c *client) EnsureRepo() error { return fmt.Errorf("failed to open existing repository: %w", err) } - log.Debug("repository opened, pulling latest changes", - "workDir", c.WorkDir) return c.Pull() } diff --git a/server/internal/handlers/admin_handlers.go b/server/internal/handlers/admin_handlers.go index 1db95f0..f880d88 100644 --- a/server/internal/handlers/admin_handlers.go +++ b/server/internal/handlers/admin_handlers.go @@ -344,7 +344,7 @@ func (h *Handler) AdminUpdateUser() http.HandlerFunc { return } - log.Info("user updated", + log.Debug("user updated", "targetUserID", userID, "updates", updates, ) diff --git a/server/internal/handlers/auth_handlers.go b/server/internal/handlers/auth_handlers.go index 3392d0c..e5861f6 100644 --- a/server/internal/handlers/auth_handlers.go +++ b/server/internal/handlers/auth_handlers.go @@ -124,7 +124,7 @@ func (h *Handler) Login(authManager auth.SessionManager, cookieService auth.Cook ExpiresAt: session.ExpiresAt, } - log.Info("user logged in successfully", + log.Debug("user logged in", "userID", user.ID, "email", user.Email, "role", user.Role, diff --git a/server/internal/handlers/file_handlers.go b/server/internal/handlers/file_handlers.go index 950d642..9fea5bf 100644 --- a/server/internal/handlers/file_handlers.go +++ b/server/internal/handlers/file_handlers.go @@ -73,9 +73,6 @@ func (h *Handler) ListFiles() http.HandlerFunc { return } - log.Debug("files listed successfully", - "fileCount", len(files), - ) respondJSON(w, files) } } @@ -129,10 +126,6 @@ func (h *Handler) LookupFileByName() http.HandlerFunc { return } - log.Debug("file lookup successful", - "filename", filename, - "matchCount", len(filePaths), - ) respondJSON(w, &LookupResponse{Paths: filePaths}) } } @@ -203,11 +196,6 @@ func (h *Handler) GetFileContent() http.HandlerFunc { respondError(w, "Failed to write response", http.StatusInternalServerError) return } - - log.Debug("file content retrieved", - "filePath", filePath, - "contentSize", len(content), - ) } } @@ -276,11 +264,6 @@ func (h *Handler) SaveFile() http.HandlerFunc { UpdatedAt: time.Now().UTC(), } - log.Debug("file saved", - "filePath", filePath, - "size", response.Size, - "updatedAt", response.UpdatedAt, - ) respondJSON(w, response) } } @@ -339,9 +322,6 @@ func (h *Handler) DeleteFile() http.HandlerFunc { return } - log.Debug("file deleted", - "filePath", filePath, - ) w.WriteHeader(http.StatusNoContent) } } @@ -389,9 +369,6 @@ func (h *Handler) GetLastOpenedFile() http.HandlerFunc { return } - log.Debug("last opened file retrieved successfully", - "filePath", filePath, - ) respondJSON(w, &LastOpenedFileResponse{LastOpenedFilePath: filePath}) } } @@ -473,9 +450,6 @@ func (h *Handler) UpdateLastOpenedFile() http.HandlerFunc { return } - log.Debug("last opened file updated successfully", - "filePath", requestBody.FilePath, - ) w.WriteHeader(http.StatusNoContent) } } diff --git a/server/internal/handlers/git_handlers.go b/server/internal/handlers/git_handlers.go index b57493f..18089c1 100644 --- a/server/internal/handlers/git_handlers.go +++ b/server/internal/handlers/git_handlers.go @@ -78,11 +78,6 @@ func (h *Handler) StageCommitAndPush() http.HandlerFunc { return } - log.Debug("git operations completed successfully", - "commitHash", hash.String(), - "commitMessage", requestBody.Message, - ) - respondJSON(w, CommitResponse{CommitHash: hash.String()}) } } @@ -120,7 +115,6 @@ func (h *Handler) PullChanges() http.HandlerFunc { return } - log.Debug("successfully pulled changes from remote") respondJSON(w, PullResponse{Message: "Successfully pulled changes from remote"}) } } diff --git a/server/internal/handlers/static_handler.go b/server/internal/handlers/static_handler.go index b70d219..e5ba92e 100644 --- a/server/internal/handlers/static_handler.go +++ b/server/internal/handlers/static_handler.go @@ -51,9 +51,6 @@ func (h *StaticHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Set cache headers for assets if strings.HasPrefix(requestedPath, "/assets/") { w.Header().Set("Cache-Control", "public, max-age=31536000") // 1 year - log.Debug("cache headers set for asset", - "path", requestedPath, - ) } // Check if file exists (not counting .gz files) @@ -97,22 +94,11 @@ func (h *StaticHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { contentType = "text/html" } w.Header().Set("Content-Type", contentType) - - log.Debug("serving gzipped file", - "path", requestedPath, - "gzPath", gzPath, - "contentType", contentType, - ) http.ServeFile(w, r, gzPath) return } } // Serve original file - log.Debug("serving original file", - "path", requestedPath, - "size", stat.Size(), - "modTime", stat.ModTime(), - ) http.ServeFile(w, r, cleanPath) } diff --git a/server/internal/handlers/user_handlers.go b/server/internal/handlers/user_handlers.go index e0cf99b..634e3d0 100644 --- a/server/internal/handlers/user_handlers.go +++ b/server/internal/handlers/user_handlers.go @@ -118,7 +118,7 @@ func (h *Handler) UpdateProfile() http.HandlerFunc { // Handle email update if requested if req.Email != "" && req.Email != user.Email { if req.CurrentPassword == "" { - log.Debug("email change attempted without current password") + log.Warn("attempted email change without current password") respondError(w, "Current password is required to change email", http.StatusBadRequest) return } @@ -159,9 +159,6 @@ func (h *Handler) UpdateProfile() http.HandlerFunc { return } - log.Debug("profile updated successfully", - "updates", updates, - ) respondJSON(w, user) } } diff --git a/server/internal/handlers/workspace_handlers.go b/server/internal/handlers/workspace_handlers.go index de32f1a..fb6c433 100644 --- a/server/internal/handlers/workspace_handlers.go +++ b/server/internal/handlers/workspace_handlers.go @@ -55,9 +55,6 @@ func (h *Handler) ListWorkspaces() http.HandlerFunc { return } - log.Debug("workspaces retrieved successfully", - "count", len(workspaces), - ) respondJSON(w, workspaces) } } @@ -127,11 +124,6 @@ func (h *Handler) CreateWorkspace() http.HandlerFunc { } if workspace.GitEnabled { - log.Debug("setting up git repository", - "workspaceID", workspace.ID, - "gitURL", workspace.GitURL, - ) - if err := h.Storage.SetupGitRepo( ctx.UserID, workspace.ID, @@ -260,10 +252,6 @@ func (h *Handler) UpdateWorkspace() http.HandlerFunc { // Handle Git repository setup/teardown if Git settings changed if changes["gitSettings"] { if workspace.GitEnabled { - log.Debug("updating git repository configuration", - "gitURL", workspace.GitURL, - ) - if err := h.Storage.SetupGitRepo( ctx.UserID, ctx.Workspace.ID, @@ -280,7 +268,6 @@ func (h *Handler) UpdateWorkspace() http.HandlerFunc { return } } else { - log.Debug("disabling git repository") h.Storage.DisableGitRepo(ctx.UserID, ctx.Workspace.ID) } } @@ -293,9 +280,6 @@ func (h *Handler) UpdateWorkspace() http.HandlerFunc { return } - log.Debug("workspace updated", - "changes", changes, - ) respondJSON(w, workspace) } } @@ -445,9 +429,6 @@ func (h *Handler) GetLastWorkspaceName() http.HandlerFunc { return } - log.Debug("last workspace name retrieved", - "workspaceName", workspaceName, - ) respondJSON(w, &LastWorkspaceNameResponse{LastWorkspaceName: workspaceName}) } } @@ -497,9 +478,6 @@ func (h *Handler) UpdateLastWorkspaceName() http.HandlerFunc { return } - log.Debug("last workspace name updated", - "workspaceName", requestBody.WorkspaceName, - ) w.WriteHeader(http.StatusNoContent) } } diff --git a/server/internal/secrets/secrets.go b/server/internal/secrets/secrets.go index a79ef7d..b28304e 100644 --- a/server/internal/secrets/secrets.go +++ b/server/internal/secrets/secrets.go @@ -65,8 +65,6 @@ func decodeAndValidateKey(key string) ([]byte, error) { // NewService creates a new Encryptor instance with the provided base64-encoded key func NewService(key string) (Service, error) { log := getLogger() - log.Debug("creating new encryption service") - keyBytes, err := decodeAndValidateKey(key) if err != nil { return nil, err diff --git a/server/internal/storage/files.go b/server/internal/storage/files.go index 8d8d781..87c5632 100644 --- a/server/internal/storage/files.go +++ b/server/internal/storage/files.go @@ -30,21 +30,12 @@ type FileNode struct { // ListFilesRecursively returns a list of all files in the workspace directory and its subdirectories. // Workspace is identified by the given userID and workspaceID. func (s *Service) ListFilesRecursively(userID, workspaceID int) ([]FileNode, error) { - log := getLogger() - log.Debug("listing files recursively", - "userID", userID, - "workspaceID", workspaceID) - workspacePath := s.GetWorkspacePath(userID, workspaceID) nodes, err := s.walkDirectory(workspacePath, "") if err != nil { return nil, err } - log.Debug("file listing complete", - "userID", userID, - "workspaceID", workspaceID, - "nodeCount", len(nodes)) return nodes, nil } @@ -116,12 +107,6 @@ func (s *Service) walkDirectory(dir, prefix string) ([]FileNode, error) { // Files are searched recursively in the workspace directory and its subdirectories. // Workspace is identified by the given userID and workspaceID. func (s *Service) FindFileByName(userID, workspaceID int, filename string) ([]string, error) { - log := getLogger() - log.Debug("searching for file by name", - "userID", userID, - "workspaceID", workspaceID, - "filename", filename) - var foundPaths []string workspacePath := s.GetWorkspacePath(userID, workspaceID) @@ -149,23 +134,12 @@ func (s *Service) FindFileByName(userID, workspaceID int, filename string) ([]st return nil, fmt.Errorf("file not found") } - log.Debug("file search complete", - "userID", userID, - "workspaceID", workspaceID, - "filename", filename, - "matchCount", len(foundPaths)) return foundPaths, nil } // GetFileContent returns the content of the file at the given filePath. // Path must be a relative path within the workspace directory given by userID and workspaceID. func (s *Service) GetFileContent(userID, workspaceID int, filePath string) ([]byte, error) { - log := getLogger() - log.Debug("reading file content", - "userID", userID, - "workspaceID", workspaceID, - "path", filePath) - fullPath, err := s.ValidatePath(userID, workspaceID, filePath) if err != nil { return nil, err @@ -177,11 +151,6 @@ func (s *Service) GetFileContent(userID, workspaceID int, filePath string) ([]by // Path must be a relative path within the workspace directory given by userID and workspaceID. func (s *Service) SaveFile(userID, workspaceID int, filePath string, content []byte) error { log := getLogger() - log.Debug("saving file", - "userID", userID, - "workspaceID", workspaceID, - "path", filePath, - "contentSize", len(content)) fullPath, err := s.ValidatePath(userID, workspaceID, filePath) if err != nil { @@ -209,11 +178,6 @@ func (s *Service) SaveFile(userID, workspaceID int, filePath string, content []b // Path must be a relative path within the workspace directory given by userID and workspaceID. func (s *Service) DeleteFile(userID, workspaceID int, filePath string) error { log := getLogger() - log.Debug("deleting file", - "userID", userID, - "workspaceID", workspaceID, - "path", filePath) - fullPath, err := s.ValidatePath(userID, workspaceID, filePath) if err != nil { return err @@ -239,11 +203,6 @@ type FileCountStats struct { // GetFileStats returns the total number of files and related statistics in a workspace // Workspace is identified by the given userID and workspaceID func (s *Service) GetFileStats(userID, workspaceID int) (*FileCountStats, error) { - log := getLogger() - log.Debug("gathering file statistics", - "userID", userID, - "workspaceID", workspaceID) - workspacePath := s.GetWorkspacePath(userID, workspaceID) // Check if workspace exists @@ -256,27 +215,16 @@ func (s *Service) GetFileStats(userID, workspaceID int) (*FileCountStats, error) return nil, err } - log.Debug("file statistics collected", - "userID", userID, - "workspaceID", workspaceID, - "totalFiles", stats.TotalFiles, - "totalSize", stats.TotalSize) return stats, nil } // GetTotalFileStats returns the total file statistics for the storage. func (s *Service) GetTotalFileStats() (*FileCountStats, error) { - log := getLogger() - log.Debug("gathering total storage statistics") - stats, err := s.countFilesInPath(s.RootDir) if err != nil { return nil, err } - log.Debug("total storage statistics collected", - "totalFiles", stats.TotalFiles, - "totalSize", stats.TotalSize) return stats, nil } diff --git a/server/internal/storage/git.go b/server/internal/storage/git.go index c2998b9..b0d2b0f 100644 --- a/server/internal/storage/git.go +++ b/server/internal/storage/git.go @@ -16,17 +16,9 @@ type RepositoryManager interface { // SetupGitRepo sets up a Git repository for the given userID and workspaceID. // The repository is cloned from the given gitURL using the given gitUser and gitToken. func (s *Service) SetupGitRepo(userID, workspaceID int, gitURL, gitUser, gitToken, commitName, commitEmail string) error { - log := getLogger() - log.Info("setting up git repository", - "userID", userID, - "workspaceID", workspaceID, - ) - workspacePath := s.GetWorkspacePath(userID, workspaceID) if _, ok := s.GitRepos[userID]; !ok { - log.Debug("initializing git repo map for user", - "userID", userID) s.GitRepos[userID] = make(map[int]git.Client) } @@ -37,16 +29,14 @@ func (s *Service) SetupGitRepo(userID, workspaceID int, gitURL, gitUser, gitToke // DisableGitRepo disables the Git repository for the given userID and workspaceID. func (s *Service) DisableGitRepo(userID, workspaceID int) { - log := getLogger() - log.Info("disabling git repository", + log := getLogger().WithGroup("git") + log.Debug("disabling git repository", "userID", userID, "workspaceID", workspaceID) if userRepos, ok := s.GitRepos[userID]; ok { delete(userRepos, workspaceID) if len(userRepos) == 0 { - log.Debug("removing empty user git repos map", - "userID", userID) delete(s.GitRepos, userID) } } @@ -55,12 +45,6 @@ func (s *Service) DisableGitRepo(userID, workspaceID int) { // StageCommitAndPush stages, commit with the message, and pushes the changes to the Git repository. // The git repository belongs to the given userID and is associated with the given workspaceID. func (s *Service) StageCommitAndPush(userID, workspaceID int, message string) (git.CommitHash, error) { - log := getLogger() - log.Debug("preparing to stage, commit and push changes", - "userID", userID, - "workspaceID", workspaceID, - "message", message) - repo, ok := s.getGitRepo(userID, workspaceID) if !ok { return git.CommitHash{}, fmt.Errorf("git settings not configured for this workspace") @@ -75,21 +59,12 @@ func (s *Service) StageCommitAndPush(userID, workspaceID int, message string) (g return hash, err } - log.Debug("changes committed and pushed", - "userID", userID, - "workspaceID", workspaceID, - "commitHash", hash.String()) return hash, nil } // Pull pulls the changes from the remote Git repository. // The git repository belongs to the given userID and is associated with the given workspaceID. func (s *Service) Pull(userID, workspaceID int) error { - log := getLogger() - log.Debug("preparing to pull changes", - "userID", userID, - "workspaceID", workspaceID) - repo, ok := s.getGitRepo(userID, workspaceID) if !ok { return fmt.Errorf("git settings not configured for this workspace") @@ -100,9 +75,6 @@ func (s *Service) Pull(userID, workspaceID int) error { return err } - log.Debug("changes pulled from remote", - "userID", userID, - "workspaceID", workspaceID) return nil } diff --git a/server/internal/storage/service.go b/server/internal/storage/service.go index 6cc7c15..507381c 100644 --- a/server/internal/storage/service.go +++ b/server/internal/storage/service.go @@ -43,18 +43,13 @@ func NewServiceWithOptions(rootDir string, options Options) *Service { "rootDir", rootDir) if options.Fs == nil { - log.Debug("filesystem not provided, using default osFS") options.Fs = &osFS{} } if options.NewGitClient == nil { - log.Debug("git client factory not provided, using default git.New") options.NewGitClient = git.New } - log.Info("storage service created", - "rootDir", rootDir) - return &Service{ fs: options.Fs, newGitClient: options.NewGitClient, diff --git a/server/internal/storage/workspace.go b/server/internal/storage/workspace.go index 564f519..4f63f55 100644 --- a/server/internal/storage/workspace.go +++ b/server/internal/storage/workspace.go @@ -17,12 +17,6 @@ type WorkspaceManager interface { // ValidatePath validates the if the given path is valid within the workspace directory. // Workspace directory is defined as the directory for the given userID and workspaceID. func (s *Service) ValidatePath(userID, workspaceID int, path string) (string, error) { - log := getLogger() - log.Debug("validating path", - "userID", userID, - "workspaceID", workspaceID, - "path", path) - workspacePath := s.GetWorkspacePath(userID, workspaceID) // First check if the path is absolute From cf2e1809a4da5730133d98cc847095564820c4fd Mon Sep 17 00:00:00 2001 From: LordMathis Date: Thu, 19 Dec 2024 22:26:29 +0100 Subject: [PATCH 21/22] Delete more debug logs --- server/cmd/server/main.go | 2 +- server/internal/app/config.go | 18 ++++++++++++++---- server/internal/auth/jwt.go | 6 ------ server/internal/db/db.go | 1 - server/internal/logging/logger.go | 22 ---------------------- server/internal/secrets/secrets.go | 2 -- server/internal/storage/service.go | 7 ------- 7 files changed, 15 insertions(+), 43 deletions(-) diff --git a/server/cmd/server/main.go b/server/cmd/server/main.go index cd7dc2c..5846abe 100644 --- a/server/cmd/server/main.go +++ b/server/cmd/server/main.go @@ -26,7 +26,7 @@ func main() { // Setup logging logging.Setup(cfg.LogLevel) - logging.Debug("Configuration loaded", "config", logging.Redact(cfg)) + logging.Debug("Configuration loaded", "config", cfg.Redact()) // Initialize and start server options, err := app.DefaultOptions(cfg) diff --git a/server/internal/app/config.go b/server/internal/app/config.go index 0b70a82..f80057c 100644 --- a/server/internal/app/config.go +++ b/server/internal/app/config.go @@ -19,10 +19,10 @@ type Config struct { RootURL string Domain string CORSOrigins []string - AdminEmail string `log:"redact"` - AdminPassword string `log:"redact"` - EncryptionKey string `log:"redact"` - JWTSigningKey string `log:"redact"` + AdminEmail string + AdminPassword string + EncryptionKey string + JWTSigningKey string RateLimitRequests int RateLimitWindow time.Duration IsDevelopment bool @@ -56,6 +56,16 @@ func (c *Config) validate() error { return nil } +// Redact redacts sensitive fields from a Config instance +func (c *Config) Redact() *Config { + redacted := *c + redacted.AdminPassword = "[REDACTED]" + redacted.AdminEmail = "[REDACTED]" + redacted.EncryptionKey = "[REDACTED]" + redacted.JWTSigningKey = "[REDACTED]" + return &redacted +} + // LoadConfig creates a new Config instance with values from environment variables func LoadConfig() (*Config, error) { config := DefaultConfig() diff --git a/server/internal/auth/jwt.go b/server/internal/auth/jwt.go index 8953440..2b976fe 100644 --- a/server/internal/auth/jwt.go +++ b/server/internal/auth/jwt.go @@ -52,8 +52,6 @@ type jwtService struct { // NewJWTService creates a new JWT service with the provided configuration // Returns an error if the signing key is missing func NewJWTService(config JWTConfig) (JWTManager, error) { - log := getJWTLogger() - if config.SigningKey == "" { return nil, fmt.Errorf("signing key is required") } @@ -66,10 +64,6 @@ func NewJWTService(config JWTConfig) (JWTManager, error) { config.RefreshTokenExpiry = 7 * 24 * time.Hour } - log.Info("initialized JWT service", - "accessExpiry", config.AccessTokenExpiry, - "refreshExpiry", config.RefreshTokenExpiry) - return &jwtService{config: config}, nil } diff --git a/server/internal/db/db.go b/server/internal/db/db.go index 6a23bff..739dc34 100644 --- a/server/internal/db/db.go +++ b/server/internal/db/db.go @@ -122,7 +122,6 @@ func Init(dbPath string, secretsService secrets.Service) (Database, error) { if err := db.Ping(); err != nil { 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 { diff --git a/server/internal/logging/logger.go b/server/internal/logging/logger.go index 7e23261..e05ea32 100644 --- a/server/internal/logging/logger.go +++ b/server/internal/logging/logger.go @@ -4,7 +4,6 @@ package logging import ( "log/slog" "os" - "reflect" ) // Logger represents the interface for logging operations @@ -61,27 +60,6 @@ func ParseLogLevel(level string) LogLevel { } } -// Redact redacts sensitive fields from a struct based on the `log` struct tag -// if the tag is set to "redact" the field value is replaced with "[REDACTED]" -func Redact(v any) map[string]any { - result := make(map[string]any) - val := reflect.ValueOf(v) - typ := val.Type() - - for i := 0; i < val.NumField(); i++ { - field := typ.Field(i) - if tag := field.Tag.Get("log"); tag != "" { - switch tag { - case "redact": - result[field.Name] = "[REDACTED]" - default: - result[field.Name] = val.Field(i).Interface() - } - } - } - return result -} - // Implementation of Logger interface methods func (l *logger) Debug(msg string, args ...any) { l.logger.Debug(msg, args...) diff --git a/server/internal/secrets/secrets.go b/server/internal/secrets/secrets.go index b28304e..6d9d15a 100644 --- a/server/internal/secrets/secrets.go +++ b/server/internal/secrets/secrets.go @@ -64,7 +64,6 @@ func decodeAndValidateKey(key string) ([]byte, error) { // NewService creates a new Encryptor instance with the provided base64-encoded key func NewService(key string) (Service, error) { - log := getLogger() keyBytes, err := decodeAndValidateKey(key) if err != nil { return nil, err @@ -80,7 +79,6 @@ func NewService(key string) (Service, error) { return nil, fmt.Errorf("failed to create GCM: %w", err) } - log.Info("encryption service created") return &encryptor{gcm: gcm}, nil } diff --git a/server/internal/storage/service.go b/server/internal/storage/service.go index 507381c..07e6b1e 100644 --- a/server/internal/storage/service.go +++ b/server/internal/storage/service.go @@ -27,9 +27,6 @@ type Options struct { // NewService creates a new Storage instance with the default options and the given rootDir root directory. func NewService(rootDir string) *Service { - getLogger().Debug("creating new storage service", - "rootDir", rootDir, - "options", "default") return NewServiceWithOptions(rootDir, Options{ Fs: &osFS{}, NewGitClient: git.New, @@ -38,10 +35,6 @@ func NewService(rootDir string) *Service { // NewServiceWithOptions creates a new Storage instance with the given options and the given rootDir root directory. func NewServiceWithOptions(rootDir string, options Options) *Service { - log := getLogger() - log.Debug("creating new storage service with custom options", - "rootDir", rootDir) - if options.Fs == nil { options.Fs = &osFS{} } From f0b6aa0d6efc338edd0a2521fc98756ad796f449 Mon Sep 17 00:00:00 2001 From: LordMathis Date: Thu, 19 Dec 2024 23:42:19 +0100 Subject: [PATCH 22/22] Setup test logging --- server/internal/app/config_test.go | 2 ++ server/internal/auth/jwt_test.go | 1 + server/internal/auth/middleware.go | 1 - server/internal/auth/middleware_test.go | 1 + server/internal/auth/session_test.go | 1 + server/internal/context/context_test.go | 1 + server/internal/context/middleware_test.go | 1 + server/internal/db/migrations_test.go | 2 ++ server/internal/db/sessions_test.go | 1 + server/internal/db/system_test.go | 1 + server/internal/db/users_test.go | 1 + server/internal/db/workspaces_test.go | 6 +++--- server/internal/handlers/auth_handlers.go | 5 ----- server/internal/handlers/integration_test.go | 2 ++ server/internal/secrets/secrets_test.go | 1 + server/internal/storage/files_test.go | 2 ++ server/internal/storage/filesystem_test.go | 2 ++ server/internal/storage/git_test.go | 1 + server/internal/storage/workspace_test.go | 1 + server/internal/testenv/testenv.go | 9 +++++++++ 20 files changed, 33 insertions(+), 9 deletions(-) create mode 100644 server/internal/testenv/testenv.go diff --git a/server/internal/app/config_test.go b/server/internal/app/config_test.go index 71ae11f..cbf1a8b 100644 --- a/server/internal/app/config_test.go +++ b/server/internal/app/config_test.go @@ -5,6 +5,8 @@ import ( "os" "testing" "time" + + _ "novamd/internal/testenv" ) func TestDefaultConfig(t *testing.T) { diff --git a/server/internal/auth/jwt_test.go b/server/internal/auth/jwt_test.go index 57b9e7e..55e1d44 100644 --- a/server/internal/auth/jwt_test.go +++ b/server/internal/auth/jwt_test.go @@ -6,6 +6,7 @@ import ( "time" "novamd/internal/auth" + _ "novamd/internal/testenv" ) func TestNewJWTService(t *testing.T) { diff --git a/server/internal/auth/middleware.go b/server/internal/auth/middleware.go index e9fe458..617eb62 100644 --- a/server/internal/auth/middleware.go +++ b/server/internal/auth/middleware.go @@ -141,7 +141,6 @@ func (m *Middleware) RequireWorkspaceAccess(next http.Handler) http.Handler { "handler", "RequireWorkspaceAccess", "clientIP", r.RemoteAddr, "userId", ctx.UserID, - "workspaceId", ctx.Workspace.ID, ) // If no workspace in context, allow the request diff --git a/server/internal/auth/middleware_test.go b/server/internal/auth/middleware_test.go index 3bac129..f9a1f6e 100644 --- a/server/internal/auth/middleware_test.go +++ b/server/internal/auth/middleware_test.go @@ -11,6 +11,7 @@ import ( "novamd/internal/auth" "novamd/internal/context" "novamd/internal/models" + _ "novamd/internal/testenv" ) // Mock SessionManager diff --git a/server/internal/auth/session_test.go b/server/internal/auth/session_test.go index 91410fb..8efc14d 100644 --- a/server/internal/auth/session_test.go +++ b/server/internal/auth/session_test.go @@ -8,6 +8,7 @@ import ( "novamd/internal/auth" "novamd/internal/models" + _ "novamd/internal/testenv" ) // Mock SessionStore diff --git a/server/internal/context/context_test.go b/server/internal/context/context_test.go index 8a0d947..3f06d2f 100644 --- a/server/internal/context/context_test.go +++ b/server/internal/context/context_test.go @@ -7,6 +7,7 @@ import ( "testing" "novamd/internal/context" + _ "novamd/internal/testenv" ) func TestGetRequestContext(t *testing.T) { diff --git a/server/internal/context/middleware_test.go b/server/internal/context/middleware_test.go index f57a230..c97969a 100644 --- a/server/internal/context/middleware_test.go +++ b/server/internal/context/middleware_test.go @@ -9,6 +9,7 @@ import ( "novamd/internal/context" "novamd/internal/models" + _ "novamd/internal/testenv" ) // MockDB implements the minimal database interface needed for testing diff --git a/server/internal/db/migrations_test.go b/server/internal/db/migrations_test.go index 7272793..a40da56 100644 --- a/server/internal/db/migrations_test.go +++ b/server/internal/db/migrations_test.go @@ -5,6 +5,8 @@ import ( "novamd/internal/db" + _ "novamd/internal/testenv" + _ "github.com/mattn/go-sqlite3" ) diff --git a/server/internal/db/sessions_test.go b/server/internal/db/sessions_test.go index f5f87eb..b86a58d 100644 --- a/server/internal/db/sessions_test.go +++ b/server/internal/db/sessions_test.go @@ -7,6 +7,7 @@ import ( "novamd/internal/db" "novamd/internal/models" + _ "novamd/internal/testenv" "github.com/google/uuid" ) diff --git a/server/internal/db/system_test.go b/server/internal/db/system_test.go index 2e2ca03..fa1fa83 100644 --- a/server/internal/db/system_test.go +++ b/server/internal/db/system_test.go @@ -9,6 +9,7 @@ import ( "novamd/internal/db" "novamd/internal/models" + _ "novamd/internal/testenv" "github.com/google/uuid" ) diff --git a/server/internal/db/users_test.go b/server/internal/db/users_test.go index e0683ea..c5df793 100644 --- a/server/internal/db/users_test.go +++ b/server/internal/db/users_test.go @@ -6,6 +6,7 @@ import ( "novamd/internal/db" "novamd/internal/models" + _ "novamd/internal/testenv" ) func TestUserOperations(t *testing.T) { diff --git a/server/internal/db/workspaces_test.go b/server/internal/db/workspaces_test.go index 722c7a1..bcbffe7 100644 --- a/server/internal/db/workspaces_test.go +++ b/server/internal/db/workspaces_test.go @@ -1,12 +1,12 @@ package db_test import ( - "database/sql" "strings" "testing" "novamd/internal/db" "novamd/internal/models" + _ "novamd/internal/testenv" ) func TestWorkspaceOperations(t *testing.T) { @@ -385,8 +385,8 @@ func TestWorkspaceOperations(t *testing.T) { // Verify workspace is gone _, err = database.GetWorkspaceByID(workspace.ID) - if err != sql.ErrNoRows { - t.Errorf("expected sql.ErrNoRows, got %v", err) + if !strings.Contains(err.Error(), "workspace not found") { + t.Errorf("expected workspace not found, got %v", err) } }) } diff --git a/server/internal/handlers/auth_handlers.go b/server/internal/handlers/auth_handlers.go index e5861f6..19f38f2 100644 --- a/server/internal/handlers/auth_handlers.go +++ b/server/internal/handlers/auth_handlers.go @@ -199,13 +199,8 @@ func (h *Handler) Logout(authManager auth.SessionManager, cookieService auth.Coo // @Router /auth/refresh [post] func (h *Handler) RefreshToken(authManager auth.SessionManager, cookieService auth.CookieManager) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - ctx, ok := context.GetRequestContext(w, r) - if !ok { - return - } log := getAuthLogger().With( "handler", "RefreshToken", - "userID", ctx.UserID, "clientIP", r.RemoteAddr, ) diff --git a/server/internal/handlers/integration_test.go b/server/internal/handlers/integration_test.go index 5e91a9d..1cb7f18 100644 --- a/server/internal/handlers/integration_test.go +++ b/server/internal/handlers/integration_test.go @@ -21,6 +21,8 @@ import ( "novamd/internal/models" "novamd/internal/secrets" "novamd/internal/storage" + + _ "novamd/internal/testenv" ) // testHarness encapsulates all the dependencies needed for testing diff --git a/server/internal/secrets/secrets_test.go b/server/internal/secrets/secrets_test.go index f1db818..58b2dc0 100644 --- a/server/internal/secrets/secrets_test.go +++ b/server/internal/secrets/secrets_test.go @@ -6,6 +6,7 @@ import ( "testing" "novamd/internal/secrets" + _ "novamd/internal/testenv" ) func TestValidateKey(t *testing.T) { diff --git a/server/internal/storage/files_test.go b/server/internal/storage/files_test.go index d5bf20b..ef36597 100644 --- a/server/internal/storage/files_test.go +++ b/server/internal/storage/files_test.go @@ -5,6 +5,8 @@ import ( "novamd/internal/storage" "path/filepath" "testing" + + _ "novamd/internal/testenv" ) // TestFileNode ensures FileNode structs are created correctly diff --git a/server/internal/storage/filesystem_test.go b/server/internal/storage/filesystem_test.go index a109198..c19e727 100644 --- a/server/internal/storage/filesystem_test.go +++ b/server/internal/storage/filesystem_test.go @@ -5,6 +5,8 @@ import ( "io/fs" "path/filepath" "time" + + _ "novamd/internal/testenv" ) type mockDirEntry struct { diff --git a/server/internal/storage/git_test.go b/server/internal/storage/git_test.go index f852616..49ba53d 100644 --- a/server/internal/storage/git_test.go +++ b/server/internal/storage/git_test.go @@ -6,6 +6,7 @@ import ( "novamd/internal/git" "novamd/internal/storage" + _ "novamd/internal/testenv" ) // MockGitClient implements git.Client interface for testing diff --git a/server/internal/storage/workspace_test.go b/server/internal/storage/workspace_test.go index 2fc11b2..636b0ab 100644 --- a/server/internal/storage/workspace_test.go +++ b/server/internal/storage/workspace_test.go @@ -7,6 +7,7 @@ import ( "testing" "novamd/internal/storage" + _ "novamd/internal/testenv" ) func TestValidatePath(t *testing.T) { diff --git a/server/internal/testenv/testenv.go b/server/internal/testenv/testenv.go new file mode 100644 index 0000000..06c7c37 --- /dev/null +++ b/server/internal/testenv/testenv.go @@ -0,0 +1,9 @@ +// Package testenv provides a setup for testing the application. +package testenv + +import "novamd/internal/logging" + +func init() { + // Initialize the logger + logging.Setup(logging.ERROR) +}