package api import ( "embed" "io/fs" "gitlab.global.bsf.tools/esv/bsf/bsf-integration/orchard/orchard-mvp/internal/storage" "github.com/gin-gonic/gin" "go.uber.org/zap" ) //go:embed static/* var staticFiles embed.FS // SetupRouter configures all API routes func SetupRouter(db *storage.Database, s3 *storage.S3Storage, logger *zap.Logger) *gin.Engine { router := gin.New() router.Use(gin.Recovery()) router.Use(LoggerMiddleware(logger)) handler := NewHandler(db, s3, logger) // Serve static files from embedded FS staticFS, _ := fs.Sub(staticFiles, "static") // Root - serve index.html router.GET("/", func(c *gin.Context) { data, err := fs.ReadFile(staticFS, "index.html") if err != nil { c.String(500, "Error loading page: "+err.Error()) return } c.Data(200, "text/html; charset=utf-8", data) }) // CSS file router.GET("/static/style.css", func(c *gin.Context) { data, err := fs.ReadFile(staticFS, "style.css") if err != nil { c.String(404, "Not found: "+err.Error()) return } c.Data(200, "text/css; charset=utf-8", data) }) // JS file router.GET("/static/app.js", func(c *gin.Context) { data, err := fs.ReadFile(staticFS, "app.js") if err != nil { c.String(404, "Not found: "+err.Error()) return } c.Data(200, "application/javascript; charset=utf-8", data) }) // Health check router.GET("/health", handler.Health) // API v1 v1 := router.Group("/api/v1") { // Authentication middleware v1.Use(AuthMiddleware(db)) // Grove routes groves := v1.Group("/groves") { groves.GET("", handler.ListGroves) groves.POST("", handler.CreateGrove) groves.GET("/:grove", handler.GetGrove) } // Tree routes trees := v1.Group("/grove/:grove/trees") { trees.GET("", handler.ListTrees) trees.POST("", handler.CreateTree) } // Fruit routes (content-addressable storage) fruits := v1.Group("/grove/:grove/:tree") { // Upload artifact (cultivate) fruits.POST("/cultivate", handler.Cultivate) // Download artifact (harvest) fruits.GET("/+/:ref", handler.Harvest) // List grafts (tags/versions) fruits.GET("/grafts", handler.ListGrafts) // Create graft (tag) fruits.POST("/graft", handler.CreateGraft) // Get consumers fruits.GET("/consumers", handler.GetConsumers) } // Direct fruit access by hash v1.GET("/fruit/:fruit", handler.GetFruit) // Search v1.GET("/search", handler.Search) } // Compatibility endpoint matching the URL pattern from spec // /grove/{project}/{tree}/+/{ref} router.GET("/grove/:grove/:tree/+/:ref", AuthMiddleware(db), handler.Harvest) return router } // LoggerMiddleware logs requests func LoggerMiddleware(logger *zap.Logger) gin.HandlerFunc { return func(c *gin.Context) { c.Next() logger.Info("request", zap.String("method", c.Request.Method), zap.String("path", c.Request.URL.Path), zap.Int("status", c.Writer.Status()), zap.String("client_ip", c.ClientIP()), ) } } // AuthMiddleware handles authentication func AuthMiddleware(db *storage.Database) gin.HandlerFunc { return func(c *gin.Context) { // Check for API key in header apiKey := c.GetHeader("X-Orchard-API-Key") if apiKey != "" { // TODO: Validate API key against database // For now, extract user ID from key c.Set("user_id", "api-user") c.Next() return } // Check for Bearer token authHeader := c.GetHeader("Authorization") if authHeader != "" { // TODO: Implement OIDC/SAML validation c.Set("user_id", "bearer-user") c.Next() return } // Allow anonymous access for public groves (read only) c.Set("user_id", "anonymous") c.Next() } }