refactored for ordered consistent logging

This commit is contained in:
Steve White 2024-10-29 10:09:14 -05:00
parent d0eb8f733c
commit cb202de53f
9 changed files with 562 additions and 117 deletions

147
admin.go
View File

@ -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
}

75
auth.go
View File

@ -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
}

View File

@ -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
}

View File

@ -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"`
}

View File

@ -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: "*"

73
db.go
View File

@ -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
}

View File

@ -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)
}

178
logger.go Normal file
View File

@ -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,
)
}

104
main.go
View File

@ -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)
}