diff --git a/admin.go b/admin.go index c1812a5..1c8802c 100644 --- a/admin.go +++ b/admin.go @@ -13,66 +13,113 @@ import ( // GetUsersHandler handles GET requests to /admin/user func GetUsersHandler(w http.ResponseWriter, r *http.Request) { + log := GetLogger() + username := r.Context().Value(userKey).(string) + + log.Info("Admin request to get all users by user: %s", username) + var users []User - db.Find(&users) + if err := db.Find(&users).Error; err != nil { + log.Error("Failed to fetch users: %v", err) + http.Error(w, "Failed to fetch users", http.StatusInternalServerError) + return + } + + log.Info("Successfully retrieved %d users", len(users)) json.NewEncoder(w).Encode(users) } // CreateUserHandler handles POST requests to /admin/user func CreateUserHandler(w http.ResponseWriter, r *http.Request) { + log := GetLogger() + adminUser := r.Context().Value(userKey).(string) + var user User err := json.NewDecoder(r.Body).Decode(&user) if err != nil { + log.Error("Failed to decode user creation request: %v", err) http.Error(w, "Invalid request body", http.StatusBadRequest) return } + log.Info("Admin user %s attempting to create new user: %s", adminUser, user.Username) + // Hash the password before storing hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), 12) if err != nil { + log.Error("Failed to hash password for new user %s: %v", user.Username, err) http.Error(w, "Failed to hash password", http.StatusInternalServerError) return } user.Password = string(hashedPassword) - db.Create(&user) + if err := db.Create(&user).Error; err != nil { + log.Error("Failed to create user %s: %v", user.Username, err) + http.Error(w, "Failed to create user", http.StatusInternalServerError) + return + } + + log.DatabaseAction("create", fmt.Sprintf("User %s created by admin %s", user.Username, adminUser)) json.NewEncoder(w).Encode(user) } // GetUserHandler handles GET requests to /admin/user/{id} func GetUserHandler(w http.ResponseWriter, r *http.Request) { + log := GetLogger() + adminUser := r.Context().Value(userKey).(string) vars := mux.Vars(r) id := vars["id"] + + log.Info("Admin user %s requesting details for user ID: %s", adminUser, id) + var user User - db.First(&user, id) - if user.ID == 0 { + if err := db.First(&user, id).Error; err != nil { + log.Warn("User not found with ID %s, requested by admin %s", id, adminUser) http.Error(w, "User not found", http.StatusNotFound) return } + + log.Info("Successfully retrieved user %s details", user.Username) json.NewEncoder(w).Encode(user) } // DeleteUserHandler handles DELETE requests to /admin/user/{id} func DeleteUserHandler(w http.ResponseWriter, r *http.Request) { + log := GetLogger() + adminUser := r.Context().Value(userKey).(string) vars := mux.Vars(r) id := vars["id"] + + log.Info("Admin user %s attempting to delete user ID: %s", adminUser, id) + var user User - db.First(&user, id) - if user.ID == 0 { + if err := db.First(&user, id).Error; err != nil { + log.Warn("Attempt to delete non-existent user ID %s by admin %s", id, adminUser) http.Error(w, "User not found", http.StatusNotFound) return } - db.Delete(&user) + + if err := db.Delete(&user).Error; err != nil { + log.Error("Failed to delete user %s: %v", user.Username, err) + http.Error(w, "Failed to delete user", http.StatusInternalServerError) + return + } + + log.DatabaseAction("delete", fmt.Sprintf("User %s deleted by admin %s", user.Username, adminUser)) w.WriteHeader(http.StatusNoContent) } // BackupDatabaseHandler handles GET requests to /admin/db func BackupDatabaseHandler(w http.ResponseWriter, r *http.Request) { - // ... - fmt.Println("BackupDatabaseHandler called") + log := GetLogger() + adminUser := r.Context().Value(userKey).(string) + + log.Info("Database backup requested by admin user: %s", adminUser) + // Open the database file using the path from the config file, err := os.Open(*DatabasePath) if err != nil { + log.Error("Failed to open database file for backup: %v", err) http.Error(w, "Failed to open database file", http.StatusInternalServerError) return } @@ -81,85 +128,129 @@ func BackupDatabaseHandler(w http.ResponseWriter, r *http.Request) { // Copy the file to the response writer _, err = io.Copy(w, file) if err != nil { + log.Error("Failed to send database backup: %v", err) http.Error(w, "Failed to send database file", http.StatusInternalServerError) return } + + log.DatabaseAction("backup", fmt.Sprintf("Database backup created by admin %s", adminUser)) + log.Info("Database backup successfully completed") } // RestoreDatabaseHandler handles POST requests to /admin/db func RestoreDatabaseHandler(w http.ResponseWriter, r *http.Request) { + log := GetLogger() + adminUser := r.Context().Value(userKey).(string) + + log.Info("Database restore requested by admin user: %s", adminUser) + // Create a backup of the existing database err := createDatabaseBackup() if err != nil { + log.Error("Failed to create backup before restore: %v", err) http.Error(w, "Failed to create database backup", http.StatusInternalServerError) return } + log.Info("Created backup of existing database") // Save the new database err = saveNewDatabase(r) if err != nil { + log.Error("Failed to save new database during restore: %v", err) http.Error(w, "Failed to save new database", http.StatusInternalServerError) return } + log.Info("Saved new database file") // Validate the new database is properly initialized err = validateNewDatabase() if err != nil { + log.Error("New database validation failed: %v", err) http.Error(w, "New database is not properly initialized", http.StatusInternalServerError) return } + log.Info("New database validation successful") // Switch to the new database app-wide err = switchToNewDatabase() if err != nil { + log.Error("Failed to switch to new database: %v", err) http.Error(w, "Failed to switch to new database", http.StatusInternalServerError) return } - fmt.Println("Database restored successfully") + + log.DatabaseAction("restore", fmt.Sprintf("Database restored by admin %s", adminUser)) + log.Info("Database restore completed successfully") + w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{"message": "Database restored successfully"}) } func createDatabaseBackup() error { - // Create a backup of the existing database + log := GetLogger() + backupPath := *DatabasePath + ".bak" + + log.Info("Creating database backup at: %s", backupPath) + src, err := os.Open(*DatabasePath) if err != nil { + log.Error("Failed to open source database for backup: %v", err) return err } defer src.Close() - dst, err := os.Create(*DatabasePath + ".bak") + dst, err := os.Create(backupPath) if err != nil { + log.Error("Failed to create backup file: %v", err) return err } defer dst.Close() _, err = io.Copy(dst, src) - return err + if err != nil { + log.Error("Failed to copy database to backup: %v", err) + return err + } + + log.Info("Database backup created successfully") + return nil } func saveNewDatabase(r *http.Request) error { - // Save the new database + log := GetLogger() + file, _, err := r.FormFile("database") if err != nil { + log.Error("Failed to get database file from request: %v", err) return err } defer file.Close() dst, err := os.Create(*DatabasePath) if err != nil { + log.Error("Failed to create new database file: %v", err) return err } defer dst.Close() _, err = io.Copy(dst, file) - return err + if err != nil { + log.Error("Failed to save new database file: %v", err) + return err + } + + log.Info("New database file saved successfully") + return nil } func validateNewDatabase() error { - // Validate the new database is properly initialized + log := GetLogger() + + log.Info("Validating new database") + db, err := ConnectDB(*DatabasePath) if err != nil { + log.Error("Failed to connect to new database: %v", err) return err } defer db.Close() @@ -168,22 +259,36 @@ func validateNewDatabase() error { tables := []string{"users", "boxes", "items"} for _, table := range tables { var count int - db.Debug().Raw("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?;", table).Row().Scan(&count) + err := db.Debug().Raw("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?;", table).Row().Scan(&count) + if err != nil { + log.Error("Error checking for table %s: %v", table, err) + return err + } if count == 0 { + log.Error("Required table %s does not exist in new database", table) return fmt.Errorf("table %s does not exist", table) } + log.Info("Validated table exists: %s", table) } + log.Info("Database validation completed successfully") return nil } func switchToNewDatabase() error { - // Switch to the new database app-wide - db, err := ConnectDB(*DatabasePath) + log := GetLogger() + + log.Info("Switching to new database") + + newDB, err := ConnectDB(*DatabasePath) if err != nil { + log.Error("Failed to connect to new database during switch: %v", err) return err } - // Update the db variable with the new database connection - db = db + + // Update the global db variable + db = newDB + + log.Info("Successfully switched to new database") return nil } diff --git a/auth.go b/auth.go index a2db5da..05a7888 100644 --- a/auth.go +++ b/auth.go @@ -30,42 +30,46 @@ type LoginResponse struct { } func init() { - fmt.Printf("handlers.go init: config = %+v", config) + log := GetLogger() + if log != nil { + log.Info("Initializing authentication module") + log.Debug("Current config: %+v", config) + } } -// loginHandler handles the /login endpoint. // LoginHandler handles the /login endpoint. func LoginHandler(w http.ResponseWriter, r *http.Request) { + log := GetLogger() var req LoginRequest - //fmt.Println("db is ", db) - //fmt.Println("config is ", config) + if db == nil { - fmt.Println("DB is nil") + if log != nil { + log.Error("Database connection not initialized in LoginHandler") + } http.Error(w, "Database not initialized", http.StatusInternalServerError) return } - fmt.Println("DB is not nil") - //if config == nil { - //fmt.Println("Config is nil in LoginHandler") - //h//ttp.Error(w, "Configuration not loaded", http.StatusInternalServerError) - //return - //} - //fmt.Println("Config is not nil") + if log != nil { + log.Info("Processing login request") + } - fmt.Printf("DB: %+v\n", db) - //fmt.Printf("Config: %+v\n", config) - fmt.Println("LoginHandler called") err := json.NewDecoder(r.Body).Decode(&req) if err != nil { + if log != nil { + log.Error("Failed to decode login request: %v", err) + } http.Error(w, "Invalid request body", http.StatusBadRequest) return } // Check if the user exists and the password matches var user User - db.Where("username = ?", req.Username).First(&user) - if user.ID == 0 { + result := db.Where("username = ?", req.Username).First(&user) + if result.Error != nil || user.ID == 0 { + if log != nil { + log.Warn("Login attempt failed for username: %s - user not found", req.Username) + } http.Error(w, "Invalid username or password", http.StatusUnauthorized) return } @@ -73,6 +77,9 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { // Compare the provided password with the stored hashed password err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)) if err != nil { + if log != nil { + log.Warn("Login attempt failed for username: %s - invalid password", req.Username) + } http.Error(w, "Invalid username or password", http.StatusUnauthorized) return } @@ -85,19 +92,29 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { tokenString, err := token.SignedString(*JWTSecret) if err != nil { + if log != nil { + log.Error("Failed to generate JWT token for user %s: %v", user.Username, err) + } http.Error(w, "Failed to generate token", http.StatusInternalServerError) return } + if log != nil { + log.Info("Successful login for user: %s", user.Username) + log.UserAction(user.Username, "login") + } + // Return the token in the response json.NewEncoder(w).Encode(LoginResponse{Token: tokenString}) } -// authMiddleware is a middleware function that checks for a valid JWT token in the request header and enables CORS. +// AuthMiddleware is a middleware function that checks for a valid JWT token in the request header and enables CORS. func AuthMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log := GetLogger() + // Set CORS headers - w.Header().Set("Access-Control-Allow-Origin", "*") // Replace "*" with your allowed frontend origin if needed + w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") @@ -110,6 +127,9 @@ func AuthMiddleware(next http.Handler) http.Handler { // Get the token from the request header tokenString := r.Header.Get("Authorization") if tokenString == "" { + if log != nil { + log.Warn("Request rejected: missing Authorization header") + } http.Error(w, "Authorization header missing", http.StatusUnauthorized) return } @@ -121,21 +141,36 @@ func AuthMiddleware(next http.Handler) http.Handler { token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { // Make sure that the signing method is HMAC if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + if log != nil { + log.Warn("Invalid signing method in token: %v", token.Header["alg"]) + } return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return *JWTSecret, nil }) + if err != nil || !token.Valid { + if log != nil { + log.Warn("Invalid token: %v", err) + } http.Error(w, "Invalid token", http.StatusUnauthorized) return } // Extract the user claims from the token if claims, ok := token.Claims.(jwt.MapClaims); ok { + username := claims["username"].(string) // Add the "user" claim to the request context - newCtx := context.WithValue(r.Context(), userKey, claims["username"]) + newCtx := context.WithValue(r.Context(), userKey, username) r = r.WithContext(newCtx) + + if log != nil { + log.Debug("Authenticated request for user: %s", username) + } } else { + if log != nil { + log.Warn("Invalid token claims structure") + } http.Error(w, "Invalid token claims", http.StatusUnauthorized) return } diff --git a/boxes_handlers.go b/boxes_handlers.go index 92f8f6b..e1ad8ed 100644 --- a/boxes_handlers.go +++ b/boxes_handlers.go @@ -10,18 +10,22 @@ import ( // getBoxesHandler handles the GET /boxes endpoint. func GetBoxesHandler(w http.ResponseWriter, r *http.Request) { - fmt.Printf("Received %s request to %s\n", r.Method, r.URL) + log := GetLogger() + log.Info("Received %s request to %s", r.Method, r.URL) + var boxes []Box db.Find(&boxes) json.NewEncoder(w).Encode(boxes) } func GetBoxHandler(w http.ResponseWriter, r *http.Request) { + log := GetLogger() vars := mux.Vars(r) id := vars["id"] var box Box if err := db.First(&box, id).Error; err != nil { + log.Warn("Box not found with ID: %s", id) http.Error(w, "Box not found", http.StatusNotFound) return } @@ -31,9 +35,11 @@ func GetBoxHandler(w http.ResponseWriter, r *http.Request) { // createBoxHandler handles the POST /boxes endpoint. func CreateBoxHandler(w http.ResponseWriter, r *http.Request) { + log := GetLogger() var box Box err := json.NewDecoder(r.Body).Decode(&box) if err != nil { + log.Error("Failed to decode box creation request: %v", err) http.Error(w, "Invalid request body", http.StatusBadRequest) return } @@ -51,26 +57,26 @@ func CreateBoxHandler(w http.ResponseWriter, r *http.Request) { Name: box.Name, } + log.DatabaseAction("create", fmt.Sprintf("Created box with ID %d", box.ID)) json.NewEncoder(w).Encode(response) } // deleteBoxHandler handles the DELETE /boxes/{id} endpoint. func DeleteBoxHandler(w http.ResponseWriter, r *http.Request) { + log := GetLogger() vars := mux.Vars(r) id := vars["id"] // Retrieve the box from the database var box Box if err := db.First(&box, id).Error; err != nil { + log.Warn("Attempt to delete non-existent box with ID: %s", id) http.Error(w, "Box not found", http.StatusNotFound) return } - // Optionally, delete associated items (if you want cascading delete) - // db.Where("box_id = ?", id).Delete(&Item{}) - // Delete the box db.Delete(&box) - + log.DatabaseAction("delete", fmt.Sprintf("Deleted box with ID %d", box.ID)) w.WriteHeader(http.StatusNoContent) // 204 No Content } diff --git a/config.go b/config.go index ea47a6b..e083711 100644 --- a/config.go +++ b/config.go @@ -16,6 +16,7 @@ type Config struct { ImageStorageDir string `yaml:"image_storage_dir"` ListeningPort int `yaml:"listening_port"` LogFile string `yaml:"log_file"` + LogLevel string `yaml:"log_level"` StaticFilesDir string `yaml:"static_files_dir"` AllowedOrigins string `yaml:"allowed_origins"` } diff --git a/config/config.yaml b/config/config.yaml index abc4df1..c0cca05 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -4,5 +4,6 @@ jwt_secret: "super_secret_key" image_storage_dir: "/app/images" listening_port: 8080 log_file: "/app/data/boxes.log" +log_level: "INFO" static_files_dir: "/app/build/" allowed_origins: "*" diff --git a/db.go b/db.go index e75ac89..b4ad292 100644 --- a/db.go +++ b/db.go @@ -30,17 +30,82 @@ type User struct { Email string `json:"email"` } +// ConnectDB establishes a connection to the database and sets up the schema func ConnectDB(dbPath string) (*gorm.DB, error) { + log := GetLogger() + if log != nil { + log.Info("Attempting to connect to database at: %s", dbPath) + } + db, err := gorm.Open("sqlite3", dbPath) if err != nil { + if log != nil { + log.Error("Failed to connect to database: %v", err) + } return nil, fmt.Errorf("failed to connect to database: %v", err) } - // set auto_vacuum mode to ON - // this automagically removes old rows from the database when idle - db.Exec("PRAGMA auto_vacuum = ON;") + // Enable detailed SQL logging if we're in DEBUG mode + if log != nil && log.GetLogLevel() == "DEBUG" { + db.LogMode(true) + } + + if log != nil { + log.Info("Successfully connected to database") + } + + // Set auto_vacuum mode to ON + if err := db.Exec("PRAGMA auto_vacuum = ON;").Error; err != nil { + if log != nil { + log.Error("Failed to set auto_vacuum pragma: %v", err) + } + return nil, fmt.Errorf("failed to set auto_vacuum pragma: %v", err) + } + + if log != nil { + log.Info("Auto-vacuum mode enabled") + } + // AutoMigrate will create the tables if they don't exist - db.AutoMigrate(&Box{}, &Item{}, &User{}) + if err := autoMigrateSchema(db); err != nil { + if log != nil { + log.Error("Schema migration failed: %v", err) + } + return nil, err + } + + if log != nil { + log.Info("Database schema migration completed successfully") + } return db, nil } + +// autoMigrateSchema handles the database schema migration +func autoMigrateSchema(db *gorm.DB) error { + log := GetLogger() + if log != nil { + log.Info("Starting schema migration") + } + + // List of models to migrate + models := []interface{}{ + &Box{}, + &Item{}, + &User{}, + } + + for _, model := range models { + if err := db.AutoMigrate(model).Error; err != nil { + if log != nil { + log.Error("Failed to migrate model %T: %v", model, err) + } + return fmt.Errorf("failed to migrate model %T: %v", model, err) + } + if log != nil { + log.Debug("Successfully migrated model: %T", model) + } + } + + return nil +} diff --git a/item_handlers.go b/item_handlers.go index 854a06e..1a0af4c 100644 --- a/item_handlers.go +++ b/item_handlers.go @@ -11,38 +11,29 @@ import ( "github.com/gorilla/mux" ) -// Move all item-related handlers here: -// - GetItemsHandler -// - CreateItemHandler -// - GetItemHandler -// - UpdateItemHandler -// - DeleteItemHandler -// - UploadItemImageHandler -// - GetItemImageHandler -// - SearchItemsHandler -// - GetItemsInBoxHandler - -// getItemsHandler handles the GET /items endpoint. func GetItemsHandler(w http.ResponseWriter, r *http.Request) { + log := GetLogger() + log.Info("Received %s request to %s", r.Method, r.URL) + var items []Item db.Find(&items) json.NewEncoder(w).Encode(items) } -// createItemHandler handles the POST /items endpoint. func CreateItemHandler(w http.ResponseWriter, r *http.Request) { + log := GetLogger() var item Item err := json.NewDecoder(r.Body).Decode(&item) if err != nil { + log.Error("Failed to decode item creation request: %v", err) http.Error(w, "Invalid request body", http.StatusBadRequest) return } - fmt.Println(item) + log.Info("Creating new item: %s", item.Name) db.Create(&item) - // Create a response struct to include the ID type createItemResponse struct { ID uint `json:"id"` Name string `json:"name"` @@ -53,47 +44,46 @@ func CreateItemHandler(w http.ResponseWriter, r *http.Request) { Name: item.Name, } + log.DatabaseAction("create", fmt.Sprintf("Created item with ID %d", item.ID)) json.NewEncoder(w).Encode(response) } -// UploadItemImageHandler handles the image upload for an item func UploadItemImageHandler(w http.ResponseWriter, r *http.Request) { - // Extract the authenticated user from context (assuming this is how AuthMiddleware works) + log := GetLogger() user, ok := r.Context().Value(userKey).(string) if !ok || user == "" { + log.Warn("Unauthorized image upload attempt") http.Error(w, "Unauthorized", http.StatusUnauthorized) return } - // Parse the form data, 10MB limit for file uploads err := r.ParseMultipartForm(10 << 20) if err != nil { + log.Error("Failed to parse multipart form: %v", err) http.Error(w, "Unable to parse form", http.StatusBadRequest) return } - // Get the file from the form data file, handler, err := r.FormFile("image") if err != nil { + log.Error("Error retrieving file from form: %v", err) http.Error(w, "Error retrieving the file", http.StatusBadRequest) return } defer file.Close() - // Get item ID from the URL vars := mux.Vars(r) itemID := vars["id"] - // Validate that the item exists (fetch from DB using itemID) var item Item if err := db.First(&item, itemID).Error; err != nil { + log.Warn("Attempt to upload image for non-existent item with ID: %s", itemID) http.Error(w, "Item not found", http.StatusNotFound) return } - // Save the uploaded file locally or to a storage service - // Ensure the directory exists if err := os.MkdirAll(*ImageStorage, 0755); err != nil { + log.Error("Failed to create image storage directory: %v", err) http.Error(w, "Unable to create image storage directory", http.StatusInternalServerError) return } @@ -101,88 +91,87 @@ func UploadItemImageHandler(w http.ResponseWriter, r *http.Request) { filePath := fmt.Sprintf("%s/%s", *ImageStorage, handler.Filename) outFile, err := os.Create(filePath) if err != nil { + log.Error("Failed to create file: %v", err) http.Error(w, "Unable to save the file", http.StatusInternalServerError) return } defer outFile.Close() - // Copy the uploaded file to the destination _, err = io.Copy(outFile, file) if err != nil { + log.Error("Failed to save file: %v", err) http.Error(w, "Unable to save the file", http.StatusInternalServerError) return } - // Update the item record in the database with the image path item.ImagePath = filePath if err := db.Save(&item).Error; err != nil { + log.Error("Failed to update item with image path: %v", err) http.Error(w, "Unable to save image path in database", http.StatusInternalServerError) return } - fmt.Println("Image upload called") - // Return the image path in the response + + log.Info("Image uploaded successfully for item %s by user %s", itemID, user) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{"imagePath": filePath}) } -// GetItemImageHandler retrieves an item's image by item ID. func GetItemImageHandler(w http.ResponseWriter, r *http.Request) { + log := GetLogger() vars := mux.Vars(r) itemID := vars["id"] - // fmt.Println("Getting image") - // Retrieve the item from the database + var item Item if err := db.First(&item, itemID).Error; err != nil { + log.Info("Item not found, using default image for ID: %s", itemID) item.ImagePath = "images/default.jpg" } else if item.ImagePath == "" { item.ImagePath = "images/default.jpg" } - // Open the image file imageFile, err := os.Open(item.ImagePath) if err != nil { - // Log the error for debugging, but don't return an HTTP error - fmt.Println("Error opening image.", err) + log.Error("Error opening image file: %v", err) item.ImagePath = "images/default.jpg" return } defer imageFile.Close() - // Determine the content type of the image imageData, err := io.ReadAll(imageFile) if err != nil { - fmt.Println("Error reading image") + log.Error("Error reading image file: %v", err) item.ImagePath = "images/default.jpg" return } contentType := http.DetectContentType(imageData) - // Set the content type header and write the image data to the response w.Header().Set("Content-Type", contentType) w.Write(imageData) } -// searchItemsHandler handles the GET /items/search endpoint. func SearchItemsHandler(w http.ResponseWriter, r *http.Request) { + log := GetLogger() query := r.URL.Query().Get("q") if query == "" { + log.Warn("Search attempt with empty query") http.Error(w, "Search query is required", http.StatusBadRequest) return } - fmt.Println(query) + + log.Info("Searching for items with query: %s", query) var items []Item db.Where("name GLOB ? OR description GLOB ?", "*"+query+"*", "*"+query+"*").Find(&items) json.NewEncoder(w).Encode(items) - } -// getItemHandler handles the GET /items/{id} endpoint. func GetItemHandler(w http.ResponseWriter, r *http.Request) { + log := GetLogger() vars := mux.Vars(r) id := vars["id"] var item Item if err := db.First(&item, id).Error; err != nil { + log.Warn("Item not found with ID: %s", id) http.Error(w, "Item not found", http.StatusNotFound) return } @@ -190,52 +179,59 @@ func GetItemHandler(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(item) } -// getItemsInBoxHandler handles the GET /boxes/{id}/items endpoint. func GetItemsInBoxHandler(w http.ResponseWriter, r *http.Request) { + log := GetLogger() vars := mux.Vars(r) id := vars["id"] var items []Item if err := db.Where("box_id = ?", id).Find(&items).Error; err != nil { + log.Warn("Failed to fetch items for box ID: %s", id) http.Error(w, "Items not found", http.StatusNotFound) return } + log.Info("Retrieved %d items from box %s", len(items), id) json.NewEncoder(w).Encode(items) } -// updateItemHandler handles the PUT /items/{id} endpoint. func UpdateItemHandler(w http.ResponseWriter, r *http.Request) { + log := GetLogger() vars := mux.Vars(r) id := vars["id"] var item Item if err := db.First(&item, id).Error; err != nil { + log.Warn("Attempt to update non-existent item with ID: %s", id) http.Error(w, "Item not found", http.StatusNotFound) return } err := json.NewDecoder(r.Body).Decode(&item) if err != nil { + log.Error("Failed to decode item update request: %v", err) http.Error(w, "Invalid request body", http.StatusBadRequest) return } - fmt.Println(item) + + log.DatabaseAction("update", fmt.Sprintf("Updated item with ID %s", id)) db.Save(&item) json.NewEncoder(w).Encode(item) } -// deleteItemHandler handles the DELETE /items/{id} endpoint. func DeleteItemHandler(w http.ResponseWriter, r *http.Request) { + log := GetLogger() vars := mux.Vars(r) id := vars["id"] var item Item if err := db.First(&item, id).Error; err != nil { + log.Warn("Attempt to delete non-existent item with ID: %s", id) http.Error(w, "Item not found", http.StatusNotFound) return } + log.DatabaseAction("delete", fmt.Sprintf("Deleted item with ID %s", id)) db.Delete(&item) w.WriteHeader(http.StatusNoContent) } diff --git a/logger.go b/logger.go new file mode 100644 index 0000000..cb6933f --- /dev/null +++ b/logger.go @@ -0,0 +1,178 @@ +// logger.go +package main + +import ( + "fmt" + "log" + "os" + "runtime" + "time" +) + +type Logger struct { + *log.Logger + file *os.File + level LogLevel +} + +type LogLevel int + +const ( + DEBUG LogLevel = iota + INFO + WARN + ERROR +) + +var ( + instance *Logger + // Map for converting string to LogLevel + logLevelMap = map[string]LogLevel{ + "DEBUG": DEBUG, + "INFO": INFO, + "WARN": WARN, + "ERROR": ERROR, + } +) + +// Initialize creates a new logger instance with specified level +func Initialize(logPath string) (*Logger, error) { + if instance != nil { + return instance, nil + } + + file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + if err != nil { + return nil, fmt.Errorf("failed to open log file: %v", err) + } + + // Default to INFO if no level specified + logger := &Logger{ + Logger: log.New(file, "", 0), + file: file, + level: INFO, + } + instance = logger + return logger, nil +} + +// SetLogLevel sets the logging level from a string +func (l *Logger) SetLogLevel(levelStr string) error { + level, ok := logLevelMap[levelStr] + if !ok { + return fmt.Errorf("invalid log level: %s. Must be one of: DEBUG, INFO, WARN, ERROR", levelStr) + } + l.level = level + return nil +} + +// GetLogLevel returns the current logging level as a string +func (l *Logger) GetLogLevel() string { + for str, level := range logLevelMap { + if level == l.level { + return str + } + } + return "UNKNOWN" +} + +// GetLogger returns the singleton logger instance +func GetLogger() *Logger { + return instance +} + +// Close closes the log file +func (l *Logger) Close() error { + if l.file != nil { + return l.file.Close() + } + return nil +} + +func (l *Logger) log(level LogLevel, format string, v ...interface{}) { + if l == nil { + return + } + + // Check if this message should be logged based on current level + if level < l.level { + return + } + + // Get caller information + _, file, line, _ := runtime.Caller(2) + + // Create timestamp + timestamp := time.Now().Format("2006-01-02 15:04:05") + + // Create level string + levelStr := "INFO" + switch level { + case DEBUG: + levelStr = "DEBUG" + case WARN: + levelStr = "WARN" + case ERROR: + levelStr = "ERROR" + } + + // Format message + message := fmt.Sprintf(format, v...) + + // Final log format + logLine := fmt.Sprintf("[%s] [%s] [%s:%d] %s\n", + timestamp, + levelStr, + file, + line, + message, + ) + + l.Logger.Print(logLine) +} + +// Debug logs a debug message +func (l *Logger) Debug(format string, v ...interface{}) { + l.log(DEBUG, format, v...) +} + +// Info logs an info message +func (l *Logger) Info(format string, v ...interface{}) { + l.log(INFO, format, v...) +} + +// Warn logs a warning message +func (l *Logger) Warn(format string, v ...interface{}) { + l.log(WARN, format, v...) +} + +// Error logs an error message +func (l *Logger) Error(format string, v ...interface{}) { + l.log(ERROR, format, v...) +} + +// HTTPRequest logs an HTTP request +func (l *Logger) HTTPRequest(method, path, username string, statusCode int) { + l.log(INFO, "HTTP %s %s - User: %s - Status: %d", + method, + path, + username, + statusCode, + ) +} + +// UserAction logs user actions like login, logout, etc. +func (l *Logger) UserAction(username, action string) { + l.log(INFO, "User Action - Username: %s - Action: %s", + username, + action, + ) +} + +// DatabaseAction logs database operations +func (l *Logger) DatabaseAction(operation, details string) { + l.log(INFO, "Database Action - Operation: %s - Details: %s", + operation, + details, + ) +} diff --git a/main.go b/main.go index e78653d..0d4486a 100644 --- a/main.go +++ b/main.go @@ -23,17 +23,21 @@ var ( func main() { // Load configuration - config, err := loadAndValidateConfig() + var err error + config, err = loadAndValidateConfig() if err != nil { log.Fatalf("Failed to load config: %v", err) } - fmt.Printf("Config loaded successfully in main(), DB path %s", config.DatabasePath) + fmt.Printf("Config loaded successfully in main(), DB path %s\n", config.DatabasePath) - // Set up logging + // Set up logging BEFORE logging config details if err := setupLogging(config.LogFile); err != nil { log.Fatalf("Failed to set up logging: %v", err) } + // Now that logging is set up, log the config details + logConfigDetails(config) + // Connect to the database db, err = connectToDatabase(config) if err != nil { @@ -43,6 +47,7 @@ func main() { // Create routers baseRouter := mux.NewRouter() + baseRouter.Use(loggingMiddleware) apiRouter := createAPIRouter(baseRouter) staticRouter := createStaticRouter(baseRouter, config.StaticFilesDir) @@ -76,24 +81,49 @@ func loadAndValidateConfig() (*Config, error) { ImageStorage = &config.ImageStorageDir - // Log config details - logConfigDetails(config) return config, nil } func setupLogging(logFile string) error { - fmt.Printf("Attempting to set up logging to file: %s\n", logFile) - file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + // Initialize returns a *Logger, but we don't need to store it + _, err := Initialize(logFile) if err != nil { - fmt.Printf("Failed to open log file: %v\n", err) - return fmt.Errorf("failed to open log file: %v", err) + return fmt.Errorf("failed to initialize logger: %v", err) } - log.SetOutput(file) - log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) - fmt.Println("Logging initialized") - log.Println("Logging initialized") + + // Get the logger instance + log := GetLogger() + if log == nil { + return fmt.Errorf("logger not initialized") + } + + // Set log level from config if specified + if config.LogLevel != "" { + if err := log.SetLogLevel(config.LogLevel); err != nil { + return fmt.Errorf("failed to set log level: %v", err) + } + } + return nil } +func logConfigDetails(config *Config) { + log := GetLogger() + if log == nil { + fmt.Println("Warning: Logger not initialized when attempting to log config details") + return + } + + log.Info("Configuration loaded:") + log.Info("Database Path: %s", config.DatabasePath) + log.Info("Image Storage Dir: %s", config.ImageStorageDir) + log.Info("Log File: %s", config.LogFile) + log.Info("Log Level: %s", config.LogLevel) + log.Info("Listening Port: %d", config.ListeningPort) + log.Info("Allowed Origins: %s", config.AllowedOrigins) + + // Don't log the JWT secret for security reasons + log.Info("JWT Secret: [REDACTED]") +} func connectToDatabase(config *Config) (*gorm.DB, error) { if config == nil { @@ -158,10 +188,16 @@ func setupStaticRoutes(router *mux.Router, staticPath string) { func createCustomStaticHandler(staticPath string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - log.Printf("Attempting to serve: %s from directory: %s", r.URL.Path, staticPath) + log := GetLogger() + if log != nil { + log.Info("Attempting to serve: %s from directory: %s", r.URL.Path, staticPath) + } + fullPath := filepath.Join(staticPath, r.URL.Path) if _, err := os.Stat(fullPath); os.IsNotExist(err) { - log.Printf("File not found: %s", fullPath) + if log != nil { + log.Warn("File not found: %s", fullPath) + } http.NotFound(w, r) return } @@ -185,6 +221,10 @@ func createCORSHandler(allowedOrigins string) func(http.Handler) http.Handler { } func startServer(port int, handler http.Handler) { + log := GetLogger() + if log != nil { + log.Info("Server starting on port %d", port) + } fmt.Printf("Server listening on port %d\n", port) log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), handler)) } @@ -196,12 +236,30 @@ func validateStaticDirectory(path string) error { return nil } -func logConfigDetails(config *Config) { - fmt.Println("Config details:") - fmt.Printf("Database Path: %s\n", config.DatabasePath) - fmt.Printf("Image Storage Dir: %s\n", config.ImageStorageDir) - fmt.Printf("JWT Secret: %s\n", config.JWTSecret) - fmt.Printf("Log File: %s\n", config.LogFile) - fmt.Printf("Listening Port: %d\n", config.ListeningPort) - fmt.Printf("Allowed Origins: %s\n", config.AllowedOrigins) +func loggingMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + username := "anonymous" + if user, ok := r.Context().Value(userKey).(string); ok { + username = user + } + + // Create a response wrapper to capture the status code + rw := &responseWriter{w, http.StatusOK} + + next.ServeHTTP(rw, r) + + if log := GetLogger(); log != nil { + log.HTTPRequest(r.Method, r.URL.Path, username, rw.statusCode) + } + }) +} + +type responseWriter struct { + http.ResponseWriter + statusCode int +} + +func (rw *responseWriter) WriteHeader(code int) { + rw.statusCode = code + rw.ResponseWriter.WriteHeader(code) }