From 54b412a3fefb25786dfb2f2d2e0b1c12e1c9f3db Mon Sep 17 00:00:00 2001 From: Steve White Date: Fri, 25 Oct 2024 13:23:41 -0500 Subject: [PATCH] refactored by AI to be clearer and easier to support. Seems to work. --- .gitignore | 1 + admin.go | 12 +- config.go | 2 +- config/boxes.db | 0 config/config.yaml | 6 +- config/config_local.yaml | 8 +- handlers.go | 31 ++++- main.go | 240 +++++++++++++++++++++-------------- scripts/createAdminUser.bash | 13 +- scripts/get_token.bash | 31 +++++ scripts/getboxes.bash | 2 +- scripts/getusers.bash | 5 +- 12 files changed, 233 insertions(+), 118 deletions(-) create mode 100644 config/boxes.db create mode 100644 scripts/get_token.bash diff --git a/.gitignore b/.gitignore index 0a0f3d9..1668e40 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ data/* images/* .DS_Store build/* +boxes-api diff --git a/admin.go b/admin.go index d12c348..c1812a5 100644 --- a/admin.go +++ b/admin.go @@ -71,7 +71,7 @@ func BackupDatabaseHandler(w http.ResponseWriter, r *http.Request) { // ... fmt.Println("BackupDatabaseHandler called") // Open the database file using the path from the config - file, err := os.Open(config.DatabasePath) + file, err := os.Open(*DatabasePath) if err != nil { http.Error(w, "Failed to open database file", http.StatusInternalServerError) return @@ -122,13 +122,13 @@ func RestoreDatabaseHandler(w http.ResponseWriter, r *http.Request) { func createDatabaseBackup() error { // Create a backup of the existing database - src, err := os.Open(config.DatabasePath) + src, err := os.Open(*DatabasePath) if err != nil { return err } defer src.Close() - dst, err := os.Create(config.DatabasePath + ".bak") + dst, err := os.Create(*DatabasePath + ".bak") if err != nil { return err } @@ -146,7 +146,7 @@ func saveNewDatabase(r *http.Request) error { } defer file.Close() - dst, err := os.Create(config.DatabasePath) + dst, err := os.Create(*DatabasePath) if err != nil { return err } @@ -158,7 +158,7 @@ func saveNewDatabase(r *http.Request) error { func validateNewDatabase() error { // Validate the new database is properly initialized - db, err := ConnectDB(config.DatabasePath) + db, err := ConnectDB(*DatabasePath) if err != nil { return err } @@ -179,7 +179,7 @@ func validateNewDatabase() error { func switchToNewDatabase() error { // Switch to the new database app-wide - db, err := ConnectDB(config.DatabasePath) + db, err := ConnectDB(*DatabasePath) if err != nil { return err } diff --git a/config.go b/config.go index 0a2aeda..ea47a6b 100644 --- a/config.go +++ b/config.go @@ -44,7 +44,7 @@ func LoadConfig(configPath string) (*Config, error) { } if config.AllowedOrigins == "" { - config.AllowedOrigins = "http://localhost:3000" + config.AllowedOrigins = "http://localhost:8080" } return &config, nil diff --git a/config/boxes.db b/config/boxes.db new file mode 100644 index 0000000..e69de29 diff --git a/config/config.yaml b/config/config.yaml index 5de9a2d..55fbab6 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -1,7 +1,7 @@ -database_path: "data/boxes.db" -test_database_path: "data/test_database.db" +database_path: "/app/data/boxes.db" +test_database_path: "/app/data/test_database.db" jwt_secret: "super_secret_key" -image_storage_dir: "images" +image_storage_dir: "/app/images" listening_port: 8080 log_file: "boxes.log" static_files_dir: "/app/build/" diff --git a/config/config_local.yaml b/config/config_local.yaml index 6c2776c..2ddd791 100644 --- a/config/config_local.yaml +++ b/config/config_local.yaml @@ -1,8 +1,8 @@ -database_path: "data/boxes.db" -test_database_path: "data/test_database.db" +database_path: "./data/boxes.db" +test_database_path: "./data/test_database.db" jwt_secret: "super_secret_key" -image_storage_dir: "images/" +image_storage_dir: "./images/" listening_port: 8080 log_file: "boxes.log" -static_files_dir: "build/" +static_files_dir: "./build/" allowed_origins: "*" diff --git a/handlers.go b/handlers.go index d0fc555..8f9f253 100644 --- a/handlers.go +++ b/handlers.go @@ -32,11 +32,32 @@ type LoginResponse struct { Token string `json:"token"` } +func init() { + fmt.Printf("handlers.go init: config = %+v", config) +} + // loginHandler handles the /login endpoint. // LoginHandler handles the /login endpoint. func LoginHandler(w http.ResponseWriter, r *http.Request) { var req LoginRequest - fmt.Println(db, config) + fmt.Println("db is ", db) + //fmt.Println("config is ", config) + if db == nil { + fmt.Println("DB is nil") + 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") + + 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 { @@ -65,7 +86,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { "exp": time.Now().Add(time.Hour * 24).Unix(), // Token expires in 24 hours }) - tokenString, err := token.SignedString([]byte(config.JWTSecret)) + tokenString, err := token.SignedString(*JWTSecret) if err != nil { http.Error(w, "Failed to generate token", http.StatusInternalServerError) return @@ -213,12 +234,12 @@ func UploadItemImageHandler(w http.ResponseWriter, r *http.Request) { // Save the uploaded file locally or to a storage service // Ensure the directory exists - if err := os.MkdirAll(config.ImageStorageDir, 0755); err != nil { + if err := os.MkdirAll(*ImageStorage, 0755); err != nil { http.Error(w, "Unable to create image storage directory", http.StatusInternalServerError) return } - filePath := fmt.Sprintf("%s/%s", config.ImageStorageDir, handler.Filename) + filePath := fmt.Sprintf("%s/%s", *ImageStorage, handler.Filename) outFile, err := os.Create(filePath) if err != nil { http.Error(w, "Unable to save the file", http.StatusInternalServerError) @@ -390,7 +411,7 @@ func AuthMiddleware(next http.Handler) http.Handler { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } - return []byte(config.JWTSecret), nil + return *JWTSecret, nil }) if err != nil || !token.Valid { http.Error(w, "Invalid token", http.StatusUnauthorized) diff --git a/main.go b/main.go index a17ba91..faa5425 100644 --- a/main.go +++ b/main.go @@ -5,8 +5,8 @@ import ( "log" "net/http" "os" - "strings" "path/filepath" + "strings" "github.com/gorilla/mux" "github.com/jinzhu/gorm" @@ -14,56 +14,131 @@ import ( ) var ( - db *gorm.DB // Declare db globally - config *Config + db *gorm.DB // Declare db globally + config *Config + JWTSecret *[]byte + ImageStorage *string + DatabasePath *string ) func main() { - - configFile := os.Getenv("BOXES_API_CONFIG") - var err error - config, err = LoadConfig(configFile) - - // get the static files directory - staticPath := config.StaticFilesDir - - // Add this before setting up the handler - if _, err := os.Stat(staticPath); os.IsNotExist(err) { - log.Fatalf("Static directory does not exist: %s", staticPath) - } - - // Get the allowed origins from the ALLOWED_ORIGINS environment variable - // If empty, defaults to http://localhost:3000 - allowedOrigins := config.AllowedOrigins - fmt.Println("Allowed origins: ", allowedOrigins) - origins := []string{"http://localhost:3000"} // Default value - - if allowedOrigins != "" { - // Split the comma-separated string into a slice of strings - origins = strings.Split(allowedOrigins, ",") - fmt.Println("Listening for connections from: ", origins) - } - - // check for errors - if err != nil || config == nil { + // Load configuration + config, err := loadAndValidateConfig() + if err != nil { log.Fatalf("Failed to load config: %v", err) } - - fmt.Println(config.DatabasePath) - fmt.Println(config.ImageStorageDir) - fmt.Println(config.JWTSecret) - fmt.Println(config.LogFile) - fmt.Println(config.ListeningPort) + fmt.Printf("Config loaded successfully in main(), DB path %s", config.DatabasePath) // Connect to the database - db, err = ConnectDB(config.DatabasePath) - if err != nil || db == nil { + db, err = connectToDatabase(config) + if err != nil { log.Fatalf("Failed to connect to database: %v", err) } defer db.Close() - // Modify your custom handler to include more detailed logging - customHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Create routers + baseRouter := mux.NewRouter() + apiRouter := createAPIRouter(baseRouter) + staticRouter := createStaticRouter(baseRouter, config.StaticFilesDir) + + // Set up routes + setupAPIRoutes(apiRouter) + setupStaticRoutes(staticRouter, config.StaticFilesDir) + + // Create CORS handler + corsHandler := createCORSHandler(config.AllowedOrigins) + + // Start the server + startServer(config.ListeningPort, corsHandler(baseRouter)) +} + +func loadAndValidateConfig() (*Config, error) { + configFile := os.Getenv("BOXES_API_CONFIG") + config, err := LoadConfig(configFile) + if err != nil || config == nil { + return nil, fmt.Errorf("failed to load config: %v", err) + } + + // Validate the database path + if config.DatabasePath == "" { + return nil, fmt.Errorf("database path is not set in config") + } + DatabasePath = &config.DatabasePath + + // Set JWTSecret + jwtSecretBytes := []byte(config.JWTSecret) + JWTSecret = &jwtSecretBytes + + ImageStorage = &config.ImageStorageDir + + // Log config details + logConfigDetails(config) + return config, nil +} + +func connectToDatabase(config *Config) (*gorm.DB, error) { + if config == nil { + return nil, fmt.Errorf("config is nil in connectToDatabase") + } + db, err := ConnectDB(config.DatabasePath) + if err != nil || db == nil { + return nil, fmt.Errorf("failed to connect to database: %v", err) + } + fmt.Println("Connected to database in connectToDatabase") + return db, nil +} + +func createAPIRouter(baseRouter *mux.Router) *mux.Router { + return baseRouter.PathPrefix("/api/v1").Subrouter() +} + +func createStaticRouter(baseRouter *mux.Router, staticPath string) *mux.Router { + if err := validateStaticDirectory(staticPath); err != nil { + log.Fatalf("Static directory error: %v", err) + } + return baseRouter.PathPrefix("/").Subrouter() +} + +func setupAPIRoutes(router *mux.Router) { + router.Handle("/login", http.HandlerFunc(LoginHandler)).Methods("POST", "OPTIONS") + + // Protected routes + protected := router.NewRoute().Subrouter() + protected.Use(AuthMiddleware) + + protected.Handle("/boxes", http.HandlerFunc(GetBoxesHandler)).Methods("GET", "OPTIONS") + protected.Handle("/boxes", http.HandlerFunc(CreateBoxHandler)).Methods("POST", "OPTIONS") + protected.Handle("/boxes/{id}", http.HandlerFunc(DeleteBoxHandler)).Methods("DELETE", "OPTIONS") + protected.Handle("/boxes/{id}", http.HandlerFunc(GetBoxHandler)).Methods("GET", "OPTIONS") + protected.Handle("/items", http.HandlerFunc(GetItemsHandler)).Methods("GET", "OPTIONS") + protected.Handle("/items", http.HandlerFunc(CreateItemHandler)).Methods("POST", "OPTIONS") + protected.Handle("/items/{id}", http.HandlerFunc(GetItemHandler)).Methods("GET", "OPTIONS") + protected.Handle("/boxes/{id}/items", http.HandlerFunc(GetItemsInBoxHandler)).Methods("GET", "OPTIONS") + protected.Handle("/items/{id}", http.HandlerFunc(UpdateItemHandler)).Methods("PUT", "OPTIONS") + protected.Handle("/items/{id}", http.HandlerFunc(DeleteItemHandler)).Methods("DELETE", "OPTIONS") + protected.Handle("/items/{id}/image", http.HandlerFunc(GetItemImageHandler)).Methods("GET", "OPTIONS") + protected.Handle("/search/items", http.HandlerFunc(SearchItemsHandler)).Methods("GET", "OPTIONS") + protected.Handle("/items/{id}/upload", http.HandlerFunc(UploadItemImageHandler)).Methods("POST", "OPTIONS") + + setupManagementRoutes(protected.PathPrefix("/admin").Subrouter()) +} + +func setupManagementRoutes(router *mux.Router) { + router.Handle("/user", http.HandlerFunc(GetUsersHandler)).Methods("GET", "OPTIONS") + router.Handle("/user", http.HandlerFunc(CreateUserHandler)).Methods("POST", "OPTIONS") + router.Handle("/user/{id}", http.HandlerFunc(GetUserHandler)).Methods("GET", "OPTIONS") + router.Handle("/user/{id}", http.HandlerFunc(DeleteUserHandler)).Methods("DELETE", "OPTIONS") + router.Handle("/db", http.HandlerFunc(BackupDatabaseHandler)).Methods("GET", "OPTIONS") + router.Handle("/db", http.HandlerFunc(RestoreDatabaseHandler)).Methods("POST", "OPTIONS") +} + +func setupStaticRoutes(router *mux.Router, staticPath string) { + customHandler := createCustomStaticHandler(staticPath) + router.PathPrefix("/").Handler(http.StripPrefix("/", customHandler)) +} + +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) fullPath := filepath.Join(staticPath, r.URL.Path) if _, err := os.Stat(fullPath); os.IsNotExist(err) { @@ -72,65 +147,42 @@ func main() { return } http.FileServer(http.Dir(staticPath)).ServeHTTP(w, r) - }) + } +} - fmt.Println("Default user 'boxuser' created successfully!") +func createCORSHandler(allowedOrigins string) func(http.Handler) http.Handler { + origins := strings.Split(allowedOrigins, ",") + if len(origins) == 0 { + origins = []string{"http://localhost:3000"} + } - // Create the router - baseRouter := mux.NewRouter() - - router := baseRouter.PathPrefix("/api/v1").Subrouter() - staticRouter := baseRouter.PathPrefix("/").Subrouter() - - // Define your routes - router.Handle("/login", http.HandlerFunc(LoginHandler)).Methods("POST", "OPTIONS") - router.Handle("/boxes", AuthMiddleware(http.HandlerFunc(GetBoxesHandler))).Methods("GET", "OPTIONS") - router.Handle("/boxes", AuthMiddleware(http.HandlerFunc(CreateBoxHandler))).Methods("POST", "OPTIONS") - router.Handle("/boxes/{id}", AuthMiddleware(http.HandlerFunc(DeleteBoxHandler))).Methods("DELETE", "OPTIONS") - router.Handle("/boxes/{id}", AuthMiddleware(http.HandlerFunc(GetBoxHandler))).Methods("GET", "OPTIONS") - router.Handle("/items", AuthMiddleware(http.HandlerFunc(GetItemsHandler))).Methods("GET", "OPTIONS") - router.Handle("/items", AuthMiddleware(http.HandlerFunc(CreateItemHandler))).Methods("POST", "OPTIONS") - router.Handle("/items/{id}", AuthMiddleware(http.HandlerFunc(GetItemHandler))).Methods("GET", "OPTIONS") - router.Handle("/boxes/{id}/items", AuthMiddleware(http.HandlerFunc(GetItemsInBoxHandler))).Methods("GET", "OPTIONS") - router.Handle("/items/{id}", AuthMiddleware(http.HandlerFunc(UpdateItemHandler))).Methods("PUT", "OPTIONS") - router.Handle("/items/{id}", AuthMiddleware(http.HandlerFunc(DeleteItemHandler))).Methods("DELETE", "OPTIONS") - router.Handle("/items/{id}/image", AuthMiddleware(http.HandlerFunc(GetItemImageHandler))).Methods("GET", "OPTIONS") - fmt.Println("Registering route for search items...") - router.Handle("/search/items", AuthMiddleware(http.HandlerFunc(SearchItemsHandler))).Methods("GET", "OPTIONS") - // Add a new route for uploading an image with AuthMiddleware - router.HandleFunc("/items/{id}/upload", UploadItemImageHandler). - Methods("POST"). - Handler(AuthMiddleware(http.HandlerFunc(UploadItemImageHandler))) - - managementRouter := router.PathPrefix("/admin").Subrouter() - managementRouter.Use(AuthMiddleware) - - managementRouter.Handle("/user", http.HandlerFunc(GetUsersHandler)).Methods("GET", "OPTIONS") - managementRouter.Handle("/user", http.HandlerFunc(CreateUserHandler)).Methods("POST", "OPTIONS") - managementRouter.Handle("/user/{id}", http.HandlerFunc(GetUserHandler)).Methods("GET", "OPTIONS") - managementRouter.Handle("/user/{id}", http.HandlerFunc(DeleteUserHandler)).Methods("DELETE", "OPTIONS") - managementRouter.Handle("/db", http.HandlerFunc(BackupDatabaseHandler)).Methods("GET", "OPTIONS") - managementRouter.Handle("/db", http.HandlerFunc(RestoreDatabaseHandler)).Methods("POST", "OPTIONS") - - // Define a route for serving static files - fmt.Println("Serving static files from:", staticPath) - //staticHandler := http.FileServer(http.Dir(staticPath)) - fmt.Println("Registering route for serving static files, no StripPrefix") - //staticRouter.HandleFunc("/", customHandler).Methods("GET", "OPTIONS") - // perplexity recommends: - staticRouter.PathPrefix("/").Handler(http.StripPrefix("/", customHandler)) - - - // Apply CORS middleware - c := cors.New(cors.Options{ + return cors.New(cors.Options{ AllowedOrigins: origins, AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, AllowedHeaders: []string{"Authorization", "Content-Type"}, ExposedHeaders: []string{"Content-Length", "Access-Control-Allow-Origin", "Access-Control-Allow-Headers", "Cache-Control", "Content-Language", "Content-Type", "Expires", "Last-Modified", "Pragma", "ETag"}, AllowCredentials: true, - }) - - // Start the server with CORS middleware - fmt.Printf("Server listening on port %d\n", config.ListeningPort) - http.ListenAndServe(fmt.Sprintf(":%d", config.ListeningPort), c.Handler(baseRouter)) + }).Handler +} + +func startServer(port int, handler http.Handler) { + fmt.Printf("Server listening on port %d\n", port) + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), handler)) +} + +func validateStaticDirectory(path string) error { + if _, err := os.Stat(path); os.IsNotExist(err) { + return fmt.Errorf("static directory does not exist: %s", path) + } + 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) } diff --git a/scripts/createAdminUser.bash b/scripts/createAdminUser.bash index 59509d6..ef8591e 100644 --- a/scripts/createAdminUser.bash +++ b/scripts/createAdminUser.bash @@ -1,10 +1,19 @@ #!/binb/ash # Set the password you want to use -PASSWORD="12m0nk3ys" +PASSWORD="boxuser" +<<<<<<< Updated upstream +DATABASE="/Users/stwhite/CODE/Boxes-App/boxes-api/data/boxes.db" +======= +DATABASE="../data/boxes.db" +>>>>>>> Stashed changes # Use htpasswd to create a bcrypt hash of the password HASHED_PASSWORD=$(htpasswd -nbBC 10 "" "$PASSWORD" | tr -d ':\n') # Create the user in the SQLite database -sqlite3 your_database.db "INSERT INTO users (username, password, email, email, email, email, email, email, email, email, email) VALUES ('boxuser', '$HASHED_PASSWORD','boxuser@r8z.us');" \ No newline at end of file +<<<<<<< Updated upstream +sqlite3 $DATABASE "INSERT INTO users (username, password, email) VALUES ('boxuser', '$HASHED_PASSWORD','boxuser@r8z.us')" +======= +sqlite3 $DATABASE "INSERT INTO users (username, password, email) VALUES ('boxuser', '$HASHED_PASSWORD','boxuser@r8z.us');" +>>>>>>> Stashed changes diff --git a/scripts/get_token.bash b/scripts/get_token.bash new file mode 100644 index 0000000..82e3ff0 --- /dev/null +++ b/scripts/get_token.bash @@ -0,0 +1,31 @@ +#!/bin/bash + +API_BASE_URL="http://localhost:8080/api/v1" # Adjusted to include /api/v1 +USERNAME="boxuser" +PASSWORD="boxuser" + +echo "Sending request to: $API_BASE_URL/login" +echo "Username: $USERNAME" +echo "Password: $PASSWORD" + +response=$(curl -vi -w "\n%{http_code}" -X POST -H "Content-Type: application/json" \ + -d "{\"username\":\"$USERNAME\", \"password\":\"$PASSWORD\"}" \ + "$API_BASE_URL/login") + +http_status=$(echo "$response" | tail -n1) +body=$(echo "$response" | sed '$d') + +echo "HTTP Status: $http_status" +echo "Response body: $body" + +if [ "$http_status" -eq 200 ]; then + TOKEN=$(echo "$body" | jq -r '.token // empty') + if [ -n "$TOKEN" ]; then + echo "Token obtained successfully:" + echo "$TOKEN" + else + echo "Failed to extract token from response." + fi +else + echo "Failed to obtain token. Server returned status $http_status" +fi diff --git a/scripts/getboxes.bash b/scripts/getboxes.bash index c466333..476a878 100644 --- a/scripts/getboxes.bash +++ b/scripts/getboxes.bash @@ -1,7 +1,7 @@ #!/bin/bash # API base URL -API_BASE_URL="http://10.0.0.66:8080" +API_BASE_URL="http://localhost:8080/api/v1" # Login credentials USERNAME="boxuser" diff --git a/scripts/getusers.bash b/scripts/getusers.bash index 3440108..7f8ab76 100644 --- a/scripts/getusers.bash +++ b/scripts/getusers.bash @@ -1,7 +1,7 @@ #!/bin/bash # API base URL -API_BASE_URL="http://localhost:8080" +API_BASE_URL="http://localhost:8080/api/v1/" # Login credentials USERNAME="boxuser" @@ -11,8 +11,9 @@ PASSWORD="boxuser" TOKEN=$(curl -s -X POST -H "Content-Type: application/json" \ -d "{\"username\":\"$USERNAME\", \"password\":\"$PASSWORD\"}" \ "$API_BASE_URL/login" | jq -r '.token') +echo $TOKEN curl -X GET \ $API_BASE_URL/admin/user \ -H "Authorization: Bearer $TOKEN" \ - -H "Content-Type: application/json" \ No newline at end of file + -H "Content-Type: application/json"