package main import ( "embed" "flag" "fmt" "io/fs" "log" "net/http" "strings" "github.com/gin-gonic/gin" "gorm.io/driver/postgres" "gorm.io/gorm" ) //go:embed dist/* var distFS embed.FS func main() { port := flag.Int("port", 1111, "port to listen on") flag.Parse() dsn := "host=localhost user=user password=pass dbname=app port=5432 sslmode=disable" db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) if err != nil { log.Fatalf("failed to connect to database: %v", err) } db.AutoMigrate( &Template{}, &File{}, &Sheet{}, &Attribute{}, &TemplateAttribute{}, &Standard{}, &Group{}, &Laboratory{}, &ConditionType{}, &DailyCondition{}, ) r := gin.Default() // API routes under /api/ api := r.Group("/api") registerRoutesOnGroup(api, db) // SPA: embedded frontend distSub, err := fs.Sub(distFS, "dist") if err != nil { log.Fatalf("failed to open embedded dist: %v", err) } fileServer := http.FileServer(http.FS(distSub)) r.NoRoute(func(c *gin.Context) { c.Header("Cache-Control", "no-store") path := strings.TrimPrefix(c.Request.URL.Path, "/") if path == "" { path = "index.html" } // Serve static file if exists if f, err := distSub.Open(path); err == nil { f.Close() fileServer.ServeHTTP(c.Writer, c.Request) return } // SPA fallback indexFile, err := fs.ReadFile(distSub, "index.html") if err != nil { c.String(500, "index.html not found") return } c.Data(200, "text/html; charset=utf-8", indexFile) }) addr := fmt.Sprintf(":%d", *port) log.Printf("Serving on http://localhost%s", addr) r.Run(addr) } func registerRoutesOnGroup(api *gin.RouterGroup, db *gorm.DB) { api.GET("/templates/", func(c *gin.Context) { getTemplates(c, db) }) api.GET("/templates/:id", func(c *gin.Context) { getTemplate(c, db) }) api.POST("/templates/", func(c *gin.Context) { createTemplate(c, db) }) api.PATCH("/templates/:id", func(c *gin.Context) { patchTemplate(c, db) }) api.DELETE("/templates/:id", func(c *gin.Context) { deleteTemplate(c, db) }) api.POST("/templates/copy", func(c *gin.Context) { copyTemplate(c, db) }) api.GET("/files/", func(c *gin.Context) { getFiles(c, db) }) api.GET("/files/:id", func(c *gin.Context) { getFile(c, db) }) api.POST("/files/upload", func(c *gin.Context) { uploadFile(c, db) }) api.GET("/sheets/", func(c *gin.Context) { getSheets(c, db) }) api.GET("/sheets/:id", func(c *gin.Context) { getSheet(c, db) }) api.POST("/sheets/", func(c *gin.Context) { createSheet(c, db) }) api.PATCH("/sheets/:id", func(c *gin.Context) { patchSheet(c, db) }) api.GET("/attributes/", func(c *gin.Context) { getAttributes(c, db) }) api.POST("/attributes/", func(c *gin.Context) { createAttribute(c, db) }) api.GET("/attributes/:id/values", func(c *gin.Context) { getAttributeValues(c, db) }) api.GET("/standards/", func(c *gin.Context) { getStandards(c, db) }) api.GET("/standards/:id", func(c *gin.Context) { getStandard(c, db) }) api.POST("/standards/", func(c *gin.Context) { createStandard(c, db) }) api.PATCH("/standards/:id", func(c *gin.Context) { patchStandard(c, db) }) api.DELETE("/standards/:id", func(c *gin.Context) { deleteStandard(c, db) }) api.GET("/laboratories/", func(c *gin.Context) { getLaboratories(c, db) }) api.GET("/laboratories/:id", func(c *gin.Context) { getLaboratory(c, db) }) api.GET("/daily-conditions/", func(c *gin.Context) { getDailyConditions(c, db) }) api.GET("/daily-conditions/:id", func(c *gin.Context) { getDailyCondition(c, db) }) api.POST("/daily-conditions/", func(c *gin.Context) { createDailyCondition(c, db) }) api.PATCH("/daily-conditions/:id", func(c *gin.Context) { updateDailyCondition(c, db) }) api.DELETE("/daily-conditions/:id", func(c *gin.Context) { deleteDailyCondition(c, db) }) api.GET("/groups/", func(c *gin.Context) { getGroups(c, db) }) api.GET("/groups/:id", func(c *gin.Context) { getGroup(c, db) }) api.POST("/groups/", func(c *gin.Context) { createGroup(c, db) }) api.PATCH("/groups/:id", func(c *gin.Context) { patchGroup(c, db) }) api.DELETE("/groups/:id", func(c *gin.Context) { deleteGroup(c, db) }) api.POST("/protocols/", func(c *gin.Context) { createProtocol(c, db) }) }