Initial commit: Orchard content-addressable storage system
- Go server with Gin framework - PostgreSQL for metadata storage - MinIO/S3 for artifact storage with SHA256 content addressing - REST API for grove/tree/fruit operations - Web UI for managing artifacts - Docker Compose setup for local development
This commit is contained in:
153
internal/api/router.go
Normal file
153
internal/api/router.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
|
||||
"github.com/bsf/orchard/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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user