package main import ( "fmt" "log" "net/http" "os" "path/filepath" "strings" "github.com/gorilla/mux" "github.com/jinzhu/gorm" "github.com/rs/cors" ) var ( db *gorm.DB // Declare db globally config *Config JWTSecret *[]byte ImageStorage *string DatabasePath *string ) func main() { log := GetLogger() // Load configuration var err error config, err = loadAndValidateConfig() if err != nil { log.Fatalf("Failed to load config: %v", err) } log.Printf("Config loaded successfully in main(), DB path %s\n", config.DatabasePath) // 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 { log.Fatalf("Failed to connect to database: %v", err) } defer db.Close() // Create routers baseRouter := mux.NewRouter() baseRouter.Use(loggingMiddleware) 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 return config, nil } func setupLogging(logFile string) error { // Initialize returns a *Logger, but we don't need to store it _, err := Initialize(logFile, config.LogOutput) if err != nil { return fmt.Errorf("failed to initialize logger: %v", err) } // 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 { log.Warn("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("Log Output: %s", config.LogOutput) 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) { log := GetLogger() if config == nil { log.Error("config is nil in connectToDatabase") return nil, fmt.Errorf("config is nil") } db, err := ConnectDB(config.DatabasePath) if err != nil || db == nil { log.Error("Failed to connect to database in connectToDatabase") return nil, fmt.Errorf("failed to connect to database: %v", err) } log.Info("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 := 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) { if log != nil { log.Warn("File not found: %s", fullPath) } http.NotFound(w, r) return } http.FileServer(http.Dir(staticPath)).ServeHTTP(w, r) } } func createCORSHandler(allowedOrigins string) func(http.Handler) http.Handler { origins := strings.Split(allowedOrigins, ",") if len(origins) == 0 { origins = []string{"http://localhost:3000"} } 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, }).Handler } func startServer(port int, handler http.Handler) { log := GetLogger() if log != nil { log.Info("Server starting on port %d", port) } log.Info("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 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) }