Switch from net/http to gin-gonic web framework
- Added gin-gonic v1.10.0 dependency - Refactored router.go: clean route groups with middleware chains - Refactored all handlers to use gin.Context instead of http.ResponseWriter/*http.Request - Simplified response helpers (JSON, Error, Success, Created, NoContent) - Clean auth middleware using Gin's c.Set() for context - Cleaner route definitions with path parameters (e.g., /domains/:name/users/:id) - Admin routes moved to /api/admin group with RequireAdmin middleware
This commit is contained in:
parent
68285d861a
commit
2834657125
13 changed files with 474 additions and 812 deletions
|
|
@ -11,6 +11,7 @@ import (
|
|||
"os" // OS-level operations like reading command-line args
|
||||
"strings" // string manipulation utilities
|
||||
|
||||
"github.com/gin-gonic/gin" // web framework
|
||||
"github.com/imc-vibe/backend/internal/api" // HTTP API routing and handlers
|
||||
"github.com/imc-vibe/backend/internal/auth" // JWT authentication
|
||||
"github.com/imc-vibe/backend/internal/config" // configuration loading
|
||||
|
|
@ -110,37 +111,47 @@ func main() {
|
|||
// The frontend is compiled into the binary during build time.
|
||||
frontendFS := FrontendFileSystem()
|
||||
|
||||
// Create a new HTTP request multiplexer (router).
|
||||
// The router maps URL paths to handler functions.
|
||||
router := http.NewServeMux()
|
||||
// Set Gin to release mode for production.
|
||||
// This disables debug logging and other development features.
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
|
||||
// Add health check endpoint.
|
||||
// This is useful for load balancers and monitoring systems.
|
||||
// Returns {"status":"ok"} with HTTP 200 status.
|
||||
router.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"status":"ok"}`))
|
||||
})
|
||||
// Create a new Gin engine (router).
|
||||
engine := gin.New()
|
||||
engine.Use(gin.Recovery())
|
||||
|
||||
// Set up the API router for all /api/* endpoints.
|
||||
// The API router handles authentication, domain management, user management, etc.
|
||||
// Register API routes from the router.
|
||||
apiRouter := api.New(database, cfg)
|
||||
router.Handle("/api/", apiRouter.Handler()) // The trailing slash is important!
|
||||
apiRouter.RegisterRoutes(engine)
|
||||
|
||||
// Serve static files from the embedded frontend.
|
||||
// These routes handle the SvelteKit frontend application.
|
||||
// Set up SPA fallback for non-API routes.
|
||||
// This must be the last route to catch all unmatched paths.
|
||||
engine.NoRoute(func(c *gin.Context) {
|
||||
path := c.Request.URL.Path
|
||||
|
||||
// Serve the favicon at /favicon.png
|
||||
router.HandleFunc("/favicon.png", serveStaticFile(frontendFS, "embed/favicon.png"))
|
||||
// If it's an API path, return 404.
|
||||
// The API router should have handled /api/* routes.
|
||||
if strings.HasPrefix(path, "/api/") {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Serve SvelteKit's hashed assets (JavaScript, CSS) at /_app/*
|
||||
// The StripPrefix removes "/_app" from the URL before looking up the file.
|
||||
router.Handle("/_app/", http.StripPrefix("/_app/", serveStaticPrefixed(frontendFS, "embed/_app/")))
|
||||
// Serve SvelteKit hashed assets at /_app/*
|
||||
if strings.HasPrefix(path, "/_app/") {
|
||||
assetPath := "embed/_app/" + strings.TrimPrefix(path, "/_app/")
|
||||
serveGinStaticFile(c, frontendFS, assetPath)
|
||||
return
|
||||
}
|
||||
|
||||
// Serve the main HTML file for all other routes.
|
||||
// This is a Single Page Application (SPA) router - all routes serve index.html,
|
||||
// and JavaScript handles showing the correct page.
|
||||
router.HandleFunc("/", serveSPA(frontendFS))
|
||||
// Serve favicon
|
||||
if path == "/favicon.png" {
|
||||
serveGinStaticFile(c, frontendFS, "embed/favicon.png")
|
||||
return
|
||||
}
|
||||
|
||||
// For all other paths, serve the SPA index.html.
|
||||
// This allows client-side routing (e.g., /domains/example.org).
|
||||
serveGinStaticFile(c, frontendFS, "embed/index.html")
|
||||
})
|
||||
|
||||
// Build the address string for binding: "IP:PORT"
|
||||
// Example: "0.0.0.0:8080" or "127.0.0.1:8080"
|
||||
|
|
@ -152,7 +163,32 @@ func main() {
|
|||
// Start the HTTP server.
|
||||
// ListenAndServe blocks until the server is stopped.
|
||||
// log.Fatal prints the error and exits if the server can't start.
|
||||
log.Fatal(http.ListenAndServe(addr, router))
|
||||
log.Fatal(http.ListenAndServe(addr, engine))
|
||||
}
|
||||
|
||||
// serveGinStaticFile serves a static file from the embedded filesystem.
|
||||
func serveGinStaticFile(c *gin.Context, fs http.FileSystem, path string) {
|
||||
file, err := fs.Open(path)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
fi, err := file.Stat()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if fi.IsDir() {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
|
||||
contentType := mimeType(path)
|
||||
c.Header("Content-Type", contentType)
|
||||
http.ServeContent(c.Writer, c.Request, fi.Name(), fi.ModTime(), file)
|
||||
}
|
||||
|
||||
// generateRandomPassword creates a cryptographically secure random password.
|
||||
|
|
@ -182,109 +218,6 @@ func generateRandomPassword(length int) string {
|
|||
return string(result) // Convert bytes to string
|
||||
}
|
||||
|
||||
// serveStaticPrefixed serves static files from a specific prefix in the embedded filesystem.
|
||||
// fsys: the embedded filesystem containing the files.
|
||||
// prefix: the directory prefix, e.g., "embed/_app/" for SvelteKit assets.
|
||||
// Returns: an HTTP handler function.
|
||||
func serveStaticPrefixed(fsys http.FileSystem, prefix string) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Build the full path by combining prefix and URL path.
|
||||
// Example: prefix="embed/_app/", URL="/_app/chunk.js" -> "embed/_app/chunk.js"
|
||||
path := prefix + r.URL.Path
|
||||
|
||||
// Try to open the file from the embedded filesystem.
|
||||
file, err := fsys.Open(path)
|
||||
if err != nil {
|
||||
// File not found - return 404.
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
defer file.Close() // Always close the file, even if there's an error later
|
||||
|
||||
// Get file information (size, modification time, etc.).
|
||||
fi, err := file.Stat()
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Don't serve directories - return 404.
|
||||
if fi.IsDir() {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Set the Content-Type header based on file extension.
|
||||
contentType := mimeType(path)
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
|
||||
// Serve the file content.
|
||||
// ServeContent handles Range requests (for video/audio), caching headers, etc.
|
||||
http.ServeContent(w, r, fi.Name(), fi.ModTime(), file)
|
||||
}
|
||||
}
|
||||
|
||||
// serveStaticFile serves a single static file at a fixed path.
|
||||
// fsys: the embedded filesystem.
|
||||
// filename: the exact path to the file, e.g., "embed/favicon.png".
|
||||
// Returns: an HTTP handler function.
|
||||
func serveStaticFile(fsys http.FileSystem, filename string) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
file, err := fsys.Open(filename)
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
fi, err := file.Stat()
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
contentType := mimeType(filename)
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
http.ServeContent(w, r, fi.Name(), fi.ModTime(), file)
|
||||
}
|
||||
}
|
||||
|
||||
// serveSPA serves the Single Page Application (SvelteKit frontend).
|
||||
// It always serves index.html for any route that isn't an API call.
|
||||
// The JavaScript running in the browser then handles routing to the correct page.
|
||||
// fsys: the embedded filesystem containing the frontend files.
|
||||
// Returns: an HTTP handler function.
|
||||
func serveSPA(fsys http.FileSystem) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
|
||||
// If this looks like an API call, something is wrong - return 404.
|
||||
// The API router should have handled /api/* routes.
|
||||
// If we get here, the request slipped through somehow.
|
||||
if strings.HasPrefix(path, "/api/") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// For SPA, always serve index.html regardless of the requested path.
|
||||
// This allows client-side routing (e.g., /domains/example.org).
|
||||
path = "embed/index.html"
|
||||
|
||||
file, err := fsys.Open(path)
|
||||
if err != nil {
|
||||
log.Printf("Error opening %s: %v", path, err)
|
||||
http.Error(w, "Not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
fi, _ := file.Stat()
|
||||
|
||||
// Tell the browser this is HTML.
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
http.ServeContent(w, r, fi.Name(), fi.ModTime(), file)
|
||||
}
|
||||
}
|
||||
|
||||
// mimeType returns the MIME type for a file based on its extension.
|
||||
// MIME types tell the browser what kind of file it is.
|
||||
// This is important for browsers to correctly interpret files.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue