344 lines
10 KiB
Go
344 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/jinzhu/gorm"
|
|
"github.com/rs/cors"
|
|
)
|
|
|
|
// Global variables
|
|
var (
|
|
db *gorm.DB
|
|
config *Config
|
|
JWTSecret *[]byte
|
|
ImageStorage *string
|
|
DatabasePath *string
|
|
)
|
|
|
|
func main() {
|
|
// Load configuration first
|
|
var err error
|
|
config, err = loadAndValidateConfig()
|
|
if err != nil {
|
|
log.Fatalf("Failed to load config: %v", err)
|
|
}
|
|
|
|
// Initialize logger with config values
|
|
logger, err := Initialize(config.LogFile, "both") // "both" means log to both file and stdout
|
|
if err != nil {
|
|
log.Fatalf("Failed to initialize logger: %v", err)
|
|
}
|
|
defer logger.Close()
|
|
|
|
// Set up logging configuration
|
|
if err := setupLogging(config.LogLevel); err != nil {
|
|
logger.Error("Failed to set up logging: %v", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Log configuration details
|
|
logConfigDetails(config)
|
|
|
|
// Connect to database
|
|
db, err = connectToDatabase(config)
|
|
if err != nil {
|
|
logger.Error("Failed to connect to database: %v", err)
|
|
os.Exit(1)
|
|
}
|
|
defer db.Close()
|
|
|
|
// Create and configure the router
|
|
router := createRouter()
|
|
|
|
// Create CORS handler
|
|
corsHandler := createCORSHandler(config.AllowedOrigins)
|
|
|
|
// Start the server
|
|
startServer(config.ListeningPort, corsHandler(router))
|
|
}
|
|
|
|
func createRouter() *mux.Router {
|
|
baseRouter := mux.NewRouter()
|
|
baseRouter.Use(loggingMiddleware)
|
|
|
|
// API routes should be registered first with a strict prefix match
|
|
apiRouter := baseRouter.PathPrefix("/api/v1").Subrouter()
|
|
apiRouter.StrictSlash(true) // This ensures /api/v1/ and /api/v1 are treated the same
|
|
setupAPIRoutes(apiRouter)
|
|
|
|
// Static file serving should be last and only match if no API routes matched
|
|
staticRouter := baseRouter.NewRoute().Subrouter()
|
|
setupStaticRoutes(staticRouter, config.StaticFilesDir)
|
|
|
|
// Add a catch-all NotFoundHandler to the base router
|
|
baseRouter.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// If the path starts with /api, return a 404 API error
|
|
if strings.HasPrefix(r.URL.Path, "/api/") {
|
|
http.Error(w, "API endpoint not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
// Otherwise serve the index.html for client-side routing
|
|
http.ServeFile(w, r, filepath.Join(config.StaticFilesDir, "index.html"))
|
|
})
|
|
|
|
return baseRouter
|
|
}
|
|
|
|
func setupAPIRoutes(router *mux.Router) {
|
|
// Public routes
|
|
router.Handle("/login", http.HandlerFunc(LoginHandler)).Methods("POST", "OPTIONS")
|
|
|
|
// Protected routes
|
|
protected := router.NewRoute().Subrouter()
|
|
protected.Use(AuthMiddleware)
|
|
|
|
// Box routes
|
|
protected.Handle("/boxes", http.HandlerFunc(GetBoxesHandler)).Methods("GET", "OPTIONS")
|
|
protected.Handle("/boxes", http.HandlerFunc(CreateBoxHandler)).Methods("POST", "OPTIONS")
|
|
protected.Handle("/boxes/{id}", http.HandlerFunc(GetBoxHandler)).Methods("GET", "OPTIONS")
|
|
protected.Handle("/boxes/{id}", http.HandlerFunc(DeleteBoxHandler)).Methods("DELETE", "OPTIONS")
|
|
protected.Handle("/boxes/{id}/items", http.HandlerFunc(GetItemsInBoxHandler)).Methods("GET", "OPTIONS")
|
|
|
|
// Item routes
|
|
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("/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("/items/{id}/upload", http.HandlerFunc(UploadItemImageHandler)).Methods("POST", "OPTIONS")
|
|
protected.Handle("/search/items", http.HandlerFunc(SearchItemsHandler)).Methods("GET", "OPTIONS")
|
|
|
|
// Tag routes
|
|
protected.Handle("/tags", http.HandlerFunc(GetTagsHandler)).Methods("GET", "OPTIONS")
|
|
protected.Handle("/tags", http.HandlerFunc(CreateTagHandler)).Methods("POST", "OPTIONS")
|
|
protected.Handle("/tags/{id}", http.HandlerFunc(UpdateTagHandler)).Methods("PUT", "OPTIONS")
|
|
protected.Handle("/tags/{id}", http.HandlerFunc(DeleteTagHandler)).Methods("DELETE", "OPTIONS")
|
|
protected.Handle("/tags/{id}/items", http.HandlerFunc(GetItemsByTagHandler)).Methods("GET", "OPTIONS")
|
|
|
|
// Item-Tag relationship routes
|
|
protected.Handle("/items/{id}/tags", http.HandlerFunc(AddItemTagsHandler)).Methods("POST", "OPTIONS")
|
|
protected.Handle("/items/{id}/tags/{tagId}", http.HandlerFunc(RemoveItemTagHandler)).Methods("DELETE", "OPTIONS")
|
|
|
|
// Admin routes
|
|
adminRouter := protected.PathPrefix("/admin").Subrouter()
|
|
setupManagementRoutes(adminRouter)
|
|
}
|
|
|
|
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) {
|
|
// Ensure static directory exists
|
|
if err := validateStaticDirectory(staticPath); err != nil {
|
|
log.Printf("Warning: Static directory validation failed: %v", err)
|
|
}
|
|
|
|
fileServer := http.FileServer(http.Dir(staticPath))
|
|
|
|
// Only serve static files for paths that don't start with /api
|
|
router.PathPrefix("/").Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
logger := GetLogger()
|
|
|
|
// Don't serve static files for API paths
|
|
if strings.HasPrefix(r.URL.Path, "/api/") {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
// Log static file request
|
|
if logger != nil {
|
|
logger.Debug("Static file request: %s", r.URL.Path)
|
|
}
|
|
|
|
// Check if the file exists
|
|
path := filepath.Join(staticPath, r.URL.Path)
|
|
_, err := os.Stat(path)
|
|
|
|
// If file doesn't exist, serve index.html for SPA routing
|
|
if os.IsNotExist(err) {
|
|
if logger != nil {
|
|
logger.Debug("File not found, serving index.html: %s", path)
|
|
}
|
|
http.ServeFile(w, r, filepath.Join(staticPath, "index.html"))
|
|
return
|
|
}
|
|
|
|
// Strip the leading "/" before serving
|
|
http.StripPrefix("/", fileServer).ServeHTTP(w, r)
|
|
}))
|
|
}
|
|
|
|
func createCustomStaticHandler(staticPath string) http.HandlerFunc {
|
|
fileServer := http.FileServer(http.Dir(staticPath))
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
logger := GetLogger()
|
|
|
|
// Log static file request
|
|
if logger != nil {
|
|
logger.Debug("Static file request: %s", r.URL.Path)
|
|
}
|
|
|
|
// Check if the file exists
|
|
path := filepath.Join(staticPath, r.URL.Path)
|
|
_, err := os.Stat(path)
|
|
|
|
// If file doesn't exist, serve index.html for SPA routing
|
|
if os.IsNotExist(err) {
|
|
if logger != nil {
|
|
logger.Debug("File not found, serving index.html: %s", path)
|
|
}
|
|
http.ServeFile(w, r, filepath.Join(staticPath, "index.html"))
|
|
return
|
|
}
|
|
|
|
fileServer.ServeHTTP(w, r)
|
|
}
|
|
}
|
|
|
|
func loadAndValidateConfig() (*Config, error) {
|
|
configFile := os.Getenv("BOXES_API_CONFIG")
|
|
if configFile == "" {
|
|
configFile = "config.yaml" // Default config path
|
|
}
|
|
|
|
var err error
|
|
config, err := LoadConfig(configFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load config: %v", err)
|
|
}
|
|
|
|
// Set global variables
|
|
jwtSecretBytes := []byte(config.JWTSecret)
|
|
JWTSecret = &jwtSecretBytes
|
|
ImageStorage = &config.ImageStorageDir
|
|
DatabasePath = &config.DatabasePath
|
|
|
|
return config, nil
|
|
}
|
|
|
|
func setupLogging(logLevel string) error {
|
|
logger := GetLogger()
|
|
if logger == nil {
|
|
return fmt.Errorf("logger not initialized")
|
|
}
|
|
|
|
if logLevel != "" {
|
|
if err := logger.SetLogLevel(logLevel); err != nil {
|
|
return fmt.Errorf("failed to set log level: %v", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func logConfigDetails(config *Config) {
|
|
logger := GetLogger()
|
|
if logger == nil {
|
|
return
|
|
}
|
|
|
|
logger.Info("Configuration loaded:")
|
|
logger.Info("Database Path: %s", config.DatabasePath)
|
|
logger.Info("Image Storage Dir: %s", config.ImageStorageDir)
|
|
logger.Info("Log File: %s", config.LogFile)
|
|
logger.Info("Log Level: %s", config.LogLevel)
|
|
logger.Info("Listening Port: %d", config.ListeningPort)
|
|
logger.Info("Allowed Origins: %s", config.AllowedOrigins)
|
|
logger.Info("Static Files Dir: %s", config.StaticFilesDir)
|
|
}
|
|
|
|
func connectToDatabase(config *Config) (*gorm.DB, error) {
|
|
if config == nil {
|
|
return nil, fmt.Errorf("config is nil")
|
|
}
|
|
return ConnectDB(config.DatabasePath)
|
|
}
|
|
|
|
func createCORSHandler(allowedOrigins string) func(http.Handler) http.Handler {
|
|
corsOpts := cors.Options{
|
|
AllowedOrigins: []string{allowedOrigins},
|
|
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
|
AllowedHeaders: []string{"Authorization", "Content-Type"},
|
|
ExposedHeaders: []string{"Content-Length"},
|
|
AllowCredentials: true,
|
|
}
|
|
|
|
// If allowedOrigins is "*", allow all origins
|
|
if allowedOrigins == "*" {
|
|
corsOpts.AllowedOrigins = []string{"*"}
|
|
}
|
|
|
|
return cors.New(corsOpts).Handler
|
|
}
|
|
|
|
func startServer(port int, handler http.Handler) {
|
|
logger := GetLogger()
|
|
addr := fmt.Sprintf(":%d", port)
|
|
|
|
if logger != nil {
|
|
logger.Info("Server starting on port %d", port)
|
|
}
|
|
|
|
if err := http.ListenAndServe(addr, handler); err != nil {
|
|
if logger != nil {
|
|
logger.Error("Server failed to start: %v", err)
|
|
}
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func validateStaticDirectory(path string) error {
|
|
if info, err := os.Stat(path); err != nil {
|
|
if os.IsNotExist(err) {
|
|
return fmt.Errorf("static directory does not exist: %s", path)
|
|
}
|
|
return fmt.Errorf("error accessing static directory: %v", err)
|
|
} else if !info.IsDir() {
|
|
return fmt.Errorf("static path is not a directory: %s", path)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type responseWriter struct {
|
|
http.ResponseWriter
|
|
statusCode int
|
|
}
|
|
|
|
func (rw *responseWriter) WriteHeader(code int) {
|
|
rw.statusCode = code
|
|
rw.ResponseWriter.WriteHeader(code)
|
|
}
|
|
|
|
func loggingMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
logger := GetLogger()
|
|
|
|
username := "anonymous"
|
|
if user, ok := r.Context().Value(userKey).(string); ok {
|
|
username = user
|
|
}
|
|
|
|
rw := &responseWriter{w, http.StatusOK}
|
|
|
|
next.ServeHTTP(rw, r)
|
|
|
|
if logger != nil {
|
|
logger.HTTPRequest(r.Method, r.URL.Path, username, rw.statusCode)
|
|
}
|
|
})
|
|
}
|