Files
Scriberr/internal/web/static.go
2025-12-16 18:21:36 -08:00

146 lines
4.1 KiB
Go

package web
import (
"embed"
"io/fs"
"net/http"
"strings"
"scriberr/internal/auth"
"github.com/gin-gonic/gin"
)
//go:embed dist/*
var staticFiles embed.FS
// GetStaticHandler returns a handler for serving embedded static files
func GetStaticHandler() http.Handler {
// Get the dist subdirectory from embedded files
distFS, err := fs.Sub(staticFiles, "dist")
if err != nil {
panic("failed to get dist subdirectory: " + err.Error())
}
return http.FileServer(http.FS(distFS))
}
// GetIndexHTML returns the index.html content
func GetIndexHTML() ([]byte, error) {
return staticFiles.ReadFile("dist/index.html")
}
// SetupStaticRoutes configures static file serving in Gin
func SetupStaticRoutes(router *gin.Engine, authService *auth.AuthService) {
// Serve static assets (CSS, JS, images) directly from embedded filesystem
router.GET("/assets/*filepath", func(c *gin.Context) {
// Extract the file path
filepath := c.Param("filepath")
// Remove leading slash if present
if filepath[0] == '/' {
filepath = filepath[1:]
}
fullPath := "assets/" + filepath
// Try to read the file from embedded filesystem
fileContent, err := staticFiles.ReadFile("dist/" + fullPath)
if err != nil {
c.Status(http.StatusNotFound)
return
}
// Set appropriate content type based on file extension
if strings.Contains(fullPath, ".css") {
c.Data(http.StatusOK, "text/css", fileContent)
} else if strings.Contains(fullPath, ".js") {
c.Data(http.StatusOK, "application/javascript", fileContent)
} else {
c.Data(http.StatusOK, "application/octet-stream", fileContent)
}
})
// Serve vite.svg
router.GET("/vite.svg", func(c *gin.Context) {
fileContent, err := staticFiles.ReadFile("dist/vite.svg")
if err != nil {
c.Status(http.StatusNotFound)
return
}
c.Data(http.StatusOK, "image/svg+xml", fileContent)
})
// Serve scriberr-logo.png
router.GET("/scriberr-logo.png", func(c *gin.Context) {
fileContent, err := staticFiles.ReadFile("dist/scriberr-logo.png")
if err != nil {
c.Status(http.StatusNotFound)
return
}
c.Data(http.StatusOK, "image/png", fileContent)
})
// Serve scriberr-thumb.png
router.GET("/scriberr-thumb.png", func(c *gin.Context) {
fileContent, err := staticFiles.ReadFile("dist/scriberr-thumb.png")
if err != nil {
c.Status(http.StatusNotFound)
return
}
c.Data(http.StatusOK, "image/png", fileContent)
})
// Serve index.html for root and any unmatched routes (SPA behavior)
router.NoRoute(func(c *gin.Context) {
// For API routes, return 404
if strings.HasPrefix(c.Request.URL.Path, "/api") {
c.JSON(404, gin.H{"error": "API endpoint not found"})
return
}
// Try to serve file from dist directly (for PWA assets like sw.js, manifest.webmanifest)
path := strings.TrimPrefix(c.Request.URL.Path, "/")
// Prevent directory traversal (basic check, though embed.FS is safe)
if strings.Contains(path, "..") {
c.Status(http.StatusForbidden)
return
}
// Try to read the file from embedded filesystem
fileContent, err := staticFiles.ReadFile("dist/" + path)
if err == nil {
// File exists, serve it
contentType := "application/octet-stream"
if strings.HasSuffix(path, ".css") {
contentType = "text/css"
} else if strings.HasSuffix(path, ".js") {
contentType = "application/javascript"
} else if strings.HasSuffix(path, ".png") {
contentType = "image/png"
} else if strings.HasSuffix(path, ".svg") {
contentType = "image/svg+xml"
} else if strings.HasSuffix(path, ".ico") {
contentType = "image/x-icon"
} else if strings.HasSuffix(path, ".webmanifest") {
contentType = "application/manifest+json"
} else if strings.HasSuffix(path, ".html") {
contentType = "text/html; charset=utf-8"
}
c.Data(http.StatusOK, contentType, fileContent)
return
}
// For all other routes, serve the React app
// The React app will handle authentication client-side
indexHTML, err := GetIndexHTML()
if err != nil {
c.String(http.StatusInternalServerError, "Error loading page")
return
}
c.Data(http.StatusOK, "text/html; charset=utf-8", indexHTML)
})
}