boxes-api/main.go

344 lines
10 KiB
Go
Raw Permalink Normal View History

2024-10-05 01:10:35 +00:00
package main
import (
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"strings"
2024-10-05 01:10:35 +00:00
"github.com/gorilla/mux"
"github.com/jinzhu/gorm"
2024-10-05 03:05:30 +00:00
"github.com/rs/cors"
2024-10-05 01:10:35 +00:00
)
// Global variables
2024-10-05 01:10:35 +00:00
var (
db *gorm.DB
config *Config
JWTSecret *[]byte
ImageStorage *string
DatabasePath *string
2024-10-05 01:10:35 +00:00
)
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)
2024-10-26 17:22:57 +00:00
}
defer logger.Close()
2024-10-26 17:22:57 +00:00
// 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)
2024-10-18 15:00:04 +00:00
// Start the server
startServer(config.ListeningPort, corsHandler(router))
}
2024-10-18 15:00:04 +00:00
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
}
2024-10-05 01:10:35 +00:00
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},
2024-10-05 03:05:30 +00:00
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Authorization", "Content-Type"},
ExposedHeaders: []string{"Content-Length"},
2024-10-05 03:05:30 +00:00
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
}
2024-10-05 03:05:30 +00:00
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)
}
})
}