Files
orchard/internal/api/router.go
Mondo Diaz 2871ddbc55 Update module path to match GitLab repository
- Changed module from github.com/bsf/orchard to
  gitlab.global.bsf.tools/esv/bsf/bsf-integration/orchard/orchard-mvp
- Updated all internal import paths to match

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 19:08:30 -06:00

154 lines
3.6 KiB
Go

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()
}
}