feat: migrate to chi router for ee integration

This commit is contained in:
Benjamin
2025-09-14 15:54:51 +02:00
parent dbe27434e4
commit 18c8d61d75
12 changed files with 64 additions and 73 deletions

View File

@@ -16,8 +16,8 @@ import (
func main() {
ctx := context.Background()
// Create server instance with multitenant disabled (Community Edition)
server, err := web.NewServer(ctx, false)
// Create server instance (Community Edition)
server, err := web.NewServer(ctx)
if err != nil {
log.Fatalf("Failed to create server: %v", err)
}

2
go.mod
View File

@@ -3,9 +3,9 @@ module github.com/btouchard/ackify-ce
go 1.24.5
require (
github.com/go-chi/chi/v5 v5.2.3
github.com/gorilla/securecookie v1.1.2
github.com/gorilla/sessions v1.4.0
github.com/julienschmidt/httprouter v1.3.0
github.com/lib/pq v1.10.9
github.com/stretchr/testify v1.11.1
golang.org/x/oauth2 v0.31.0

4
go.sum
View File

@@ -1,14 +1,14 @@
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=

View File

@@ -3,8 +3,6 @@ package handlers
import (
"net/http"
"net/url"
"github.com/julienschmidt/httprouter"
)
// AuthHandlers handles authentication-related HTTP requests
@@ -22,7 +20,7 @@ func NewAuthHandlers(authService authService, baseURL string) *AuthHandlers {
}
// HandleLogin handles login requests
func (h *AuthHandlers) HandleLogin(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
func (h *AuthHandlers) HandleLogin(w http.ResponseWriter, r *http.Request) {
next := r.URL.Query().Get("next")
if next == "" {
next = h.baseURL + "/"
@@ -33,13 +31,13 @@ func (h *AuthHandlers) HandleLogin(w http.ResponseWriter, r *http.Request, _ htt
}
// HandleLogout handles logout requests
func (h *AuthHandlers) HandleLogout(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
func (h *AuthHandlers) HandleLogout(w http.ResponseWriter, r *http.Request) {
h.authService.Logout(w, r)
http.Redirect(w, r, "/", http.StatusFound)
}
// HandleOAuthCallback handles OAuth callback from the configured provider
func (h *AuthHandlers) HandleOAuthCallback(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
func (h *AuthHandlers) HandleOAuthCallback(w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("code")
state := r.URL.Query().Get("state")

View File

@@ -9,8 +9,6 @@ import (
"image/png"
"net/http"
"github.com/julienschmidt/httprouter"
"github.com/btouchard/ackify-ce/internal/domain/models"
)
@@ -31,7 +29,7 @@ func NewBadgeHandler(checkService checkService) *BadgeHandler {
}
// HandleStatusPNG generates a PNG badge showing signature status
func (h *BadgeHandler) HandleStatusPNG(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
func (h *BadgeHandler) HandleStatusPNG(w http.ResponseWriter, r *http.Request) {
docID, err := validateDocID(r)
if err != nil {
HandleError(w, models.ErrInvalidDocument)

View File

@@ -229,7 +229,7 @@ func TestAuthHandlers_HandleLogin(t *testing.T) {
}
w := httptest.NewRecorder()
handlers.HandleLogin(w, req, nil)
handlers.HandleLogin(w, req)
if w.Code != http.StatusFound {
t.Errorf("Expected status %d, got %d", http.StatusFound, w.Code)
@@ -250,7 +250,7 @@ func TestAuthHandlers_HandleLogout(t *testing.T) {
req := httptest.NewRequest("GET", "/logout", nil)
w := httptest.NewRecorder()
handlers.HandleLogout(w, req, nil)
handlers.HandleLogout(w, req)
if w.Code != http.StatusFound {
t.Errorf("Expected status %d, got %d", http.StatusFound, w.Code)
@@ -329,7 +329,7 @@ func TestAuthHandlers_HandleOAuthCallback(t *testing.T) {
req.URL.RawQuery = q.Encode()
w := httptest.NewRecorder()
handlers.HandleOAuthCallback(w, req, nil)
handlers.HandleOAuthCallback(w, req)
if w.Code != tt.expectedStatus {
t.Errorf("Expected status %d, got %d", tt.expectedStatus, w.Code)
@@ -375,7 +375,7 @@ func TestSignatureHandlers_HandleIndex(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
handlers.HandleIndex(w, req, nil)
handlers.HandleIndex(w, req)
if w.Code != http.StatusOK {
t.Errorf("Expected status %d, got %d", http.StatusOK, w.Code)
@@ -469,7 +469,7 @@ func TestSignatureHandlers_HandleSignGET(t *testing.T) {
}
w := httptest.NewRecorder()
handlers.HandleSignGET(w, req, nil)
handlers.HandleSignGET(w, req)
if w.Code != tt.expectedStatus {
t.Errorf("Expected status %d, got %d", tt.expectedStatus, w.Code)
@@ -571,7 +571,7 @@ func TestSignatureHandlers_HandleSignPOST(t *testing.T) {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
handlers.HandleSignPOST(w, req, nil)
handlers.HandleSignPOST(w, req)
if w.Code != tt.expectedStatus {
t.Errorf("Expected status %d, got %d", tt.expectedStatus, w.Code)
@@ -634,7 +634,7 @@ func TestSignatureHandlers_HandleStatusJSON(t *testing.T) {
}
w := httptest.NewRecorder()
handlers.HandleStatusJSON(w, req, nil)
handlers.HandleStatusJSON(w, req)
if w.Code != tt.expectedStatus {
t.Errorf("Expected status %d, got %d", tt.expectedStatus, w.Code)
@@ -696,7 +696,7 @@ func TestSignatureHandlers_HandleUserSignatures(t *testing.T) {
req := httptest.NewRequest("GET", "/signatures", nil)
w := httptest.NewRecorder()
handlers.HandleUserSignatures(w, req, nil)
handlers.HandleUserSignatures(w, req)
if w.Code != tt.expectedStatus {
t.Errorf("Expected status %d, got %d", tt.expectedStatus, w.Code)

View File

@@ -9,8 +9,6 @@ import (
"strings"
"testing"
"github.com/julienschmidt/httprouter"
"github.com/btouchard/ackify-ce/internal/domain/models"
)
@@ -95,7 +93,7 @@ func TestBadgeHandler_HandleStatusPNG(t *testing.T) {
req.URL.RawQuery = q.Encode()
w := httptest.NewRecorder()
handler.HandleStatusPNG(w, req, nil)
handler.HandleStatusPNG(w, req)
if w.Code != tt.expectedStatus {
t.Errorf("Expected status %d, got %d", tt.expectedStatus, w.Code)
@@ -131,7 +129,7 @@ func TestHealthHandler_HandleHealth(t *testing.T) {
req := httptest.NewRequest("GET", "/health", nil)
w := httptest.NewRecorder()
handler.HandleHealth(w, req, nil)
handler.HandleHealth(w, req)
if w.Code != http.StatusOK {
t.Errorf("Expected status %d, got %d", http.StatusOK, w.Code)
@@ -252,7 +250,7 @@ func TestOEmbedHandler_HandleOEmbed(t *testing.T) {
req.URL.RawQuery = q.Encode()
w := httptest.NewRecorder()
handler.HandleOEmbed(w, req, nil)
handler.HandleOEmbed(w, req)
if w.Code != tt.expectedStatus {
t.Errorf("Expected status %d, got %d", tt.expectedStatus, w.Code)
@@ -318,7 +316,7 @@ func TestOEmbedHandler_HandleEmbedView(t *testing.T) {
}
w := httptest.NewRecorder()
handler.HandleEmbedView(w, req, nil)
handler.HandleEmbedView(w, req)
if w.Code != tt.expectedStatus {
t.Errorf("Expected status %d, got %d", tt.expectedStatus, w.Code)
@@ -447,7 +445,7 @@ func TestAuthMiddleware_RequireAuth(t *testing.T) {
middleware := NewAuthMiddleware(userService, "https://example.com")
// Create a test handler that returns 200 OK
testHandler := func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
testHandler := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("OK"))
}
@@ -457,7 +455,7 @@ func TestAuthMiddleware_RequireAuth(t *testing.T) {
req := httptest.NewRequest("GET", "/protected", nil)
w := httptest.NewRecorder()
wrappedHandler(w, req, nil)
wrappedHandler(w, req)
if w.Code != tt.expectedStatus {
t.Errorf("Expected status %d, got %d", tt.expectedStatus, w.Code)

View File

@@ -4,8 +4,6 @@ import (
"encoding/json"
"net/http"
"time"
"github.com/julienschmidt/httprouter"
)
// HealthHandler handles health check requests
@@ -23,7 +21,7 @@ type HealthResponse struct {
}
// HandleHealth returns the application health status
func (h *HealthHandler) HandleHealth(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
func (h *HealthHandler) HandleHealth(w http.ResponseWriter, r *http.Request) {
response := HealthResponse{
OK: true,
Time: time.Now().UTC(),

View File

@@ -4,8 +4,6 @@ import (
"errors"
"net/http"
"github.com/julienschmidt/httprouter"
"github.com/btouchard/ackify-ce/internal/domain/models"
)
@@ -24,8 +22,8 @@ func NewAuthMiddleware(userService userService, baseURL string) *AuthMiddleware
}
// RequireAuth wraps a handler to require authentication
func (m *AuthMiddleware) RequireAuth(next httprouter.Handle) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
func (m *AuthMiddleware) RequireAuth(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
_, err := m.userService.GetUser(r)
if err != nil {
nextURL := m.baseURL + r.URL.RequestURI()
@@ -33,7 +31,7 @@ func (m *AuthMiddleware) RequireAuth(next httprouter.Handle) httprouter.Handle {
http.Redirect(w, r, loginURL, http.StatusFound)
return
}
next(w, r, ps)
next(w, r)
}
}

View File

@@ -9,8 +9,6 @@ import (
"strconv"
"strings"
"github.com/julienschmidt/httprouter"
"github.com/btouchard/ackify-ce/internal/domain/models"
)
@@ -65,7 +63,7 @@ type SignatoryInfo struct {
}
// HandleOEmbed handles oEmbed requests for signature lists
func (h *OEmbedHandler) HandleOEmbed(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
func (h *OEmbedHandler) HandleOEmbed(w http.ResponseWriter, r *http.Request) {
// Parse query parameters
targetURL := r.URL.Query().Get("url")
format := r.URL.Query().Get("format")
@@ -171,7 +169,7 @@ func (h *OEmbedHandler) HandleOEmbed(w http.ResponseWriter, r *http.Request, _ h
}
// HandleEmbedView handles direct embed view requests
func (h *OEmbedHandler) HandleEmbedView(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
func (h *OEmbedHandler) HandleEmbedView(w http.ResponseWriter, r *http.Request) {
docID := strings.TrimSpace(r.URL.Query().Get("doc"))
if docID == "" {
http.Error(w, "Missing document ID", http.StatusBadRequest)

View File

@@ -9,8 +9,6 @@ import (
"net/http"
"time"
"github.com/julienschmidt/httprouter"
"github.com/btouchard/ackify-ce/internal/domain/models"
"github.com/btouchard/ackify-ce/pkg/services"
)
@@ -61,13 +59,13 @@ type PageData struct {
}
// HandleIndex serves the main index page
func (h *SignatureHandlers) HandleIndex(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
func (h *SignatureHandlers) HandleIndex(w http.ResponseWriter, r *http.Request) {
user, _ := h.userService.GetUser(r)
h.render(w, r, "index", PageData{User: user})
}
// HandleSignGET displays the signature page
func (h *SignatureHandlers) HandleSignGET(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
func (h *SignatureHandlers) HandleSignGET(w http.ResponseWriter, r *http.Request) {
user, err := h.userService.GetUser(r)
if err != nil {
HandleError(w, err)
@@ -154,7 +152,7 @@ func (h *SignatureHandlers) HandleSignGET(w http.ResponseWriter, r *http.Request
}
// HandleSignPOST processes signature creation
func (h *SignatureHandlers) HandleSignPOST(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
func (h *SignatureHandlers) HandleSignPOST(w http.ResponseWriter, r *http.Request) {
user, err := h.userService.GetUser(r)
if err != nil {
if docID := r.FormValue("doc"); docID != "" {
@@ -205,7 +203,7 @@ func (h *SignatureHandlers) HandleSignPOST(w http.ResponseWriter, r *http.Reques
}
// HandleStatusJSON returns signature status as JSON
func (h *SignatureHandlers) HandleStatusJSON(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
func (h *SignatureHandlers) HandleStatusJSON(w http.ResponseWriter, r *http.Request) {
docID, err := validateDocID(r)
if err != nil {
HandleError(w, models.ErrInvalidDocument)
@@ -252,7 +250,7 @@ func (h *SignatureHandlers) HandleStatusJSON(w http.ResponseWriter, r *http.Requ
}
// HandleUserSignatures displays the user's signatures page
func (h *SignatureHandlers) HandleUserSignatures(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
func (h *SignatureHandlers) HandleUserSignatures(w http.ResponseWriter, r *http.Request) {
user, err := h.userService.GetUser(r)
if err != nil {
HandleError(w, err)

View File

@@ -7,7 +7,7 @@ import (
"html/template"
"net/http"
"github.com/julienschmidt/httprouter"
"github.com/go-chi/chi/v5"
"github.com/btouchard/ackify-ce/internal/application/services"
"github.com/btouchard/ackify-ce/internal/infrastructure/auth"
@@ -22,11 +22,11 @@ import (
type Server struct {
httpServer *http.Server
db *sql.DB
router *chi.Mux
}
// NewServer creates a new Ackify CE server instance
// multitenant parameter enables enterprise features when true
func NewServer(ctx context.Context, multitenant bool) (*Server, error) {
func NewServer(ctx context.Context) (*Server, error) {
// Initialize infrastructure
cfg, db, tmpl, signer, err := initInfrastructure(ctx)
if err != nil {
@@ -60,7 +60,7 @@ func NewServer(ctx context.Context, multitenant bool) (*Server, error) {
healthHandler := handlers.NewHealthHandler()
// Setup HTTP router
router := setupRouter(authHandlers, authMiddleware, signatureHandlers, badgeHandler, oembedHandler, healthHandler, multitenant)
router := setupRouter(authHandlers, authMiddleware, signatureHandlers, badgeHandler, oembedHandler, healthHandler)
// Create HTTP server
httpServer := &http.Server{
@@ -71,6 +71,7 @@ func NewServer(ctx context.Context, multitenant bool) (*Server, error) {
return &Server{
httpServer: httpServer,
db: db,
router: router,
}, nil
}
@@ -95,6 +96,16 @@ func (s *Server) GetAddr() string {
return s.httpServer.Addr
}
// Router returns the underlying Chi router for composition
func (s *Server) Router() *chi.Mux {
return s.router
}
// RegisterRoutes allows external packages to register additional routes
func (s *Server) RegisterRoutes(fn func(r *chi.Mux)) {
fn(s.router)
}
// initInfrastructure initializes the basic infrastructure components
func initInfrastructure(ctx context.Context) (*config.Config, *sql.DB, *template.Template, *crypto.Ed25519Signer, error) {
// Load configuration
@@ -134,32 +145,26 @@ func setupRouter(
badgeHandler *handlers.BadgeHandler,
oembedHandler *handlers.OEmbedHandler,
healthHandler *handlers.HealthHandler,
multitenant bool,
) *httprouter.Router {
router := httprouter.New()
) *chi.Mux {
router := chi.NewRouter()
// Public routes
router.GET("/", signatureHandlers.HandleIndex)
router.GET("/login", authHandlers.HandleLogin)
router.GET("/logout", authHandlers.HandleLogout)
router.GET("/oauth2/callback", authHandlers.HandleOAuthCallback)
router.GET("/status", signatureHandlers.HandleStatusJSON)
router.GET("/status.png", badgeHandler.HandleStatusPNG)
router.GET("/oembed", oembedHandler.HandleOEmbed)
router.GET("/embed", oembedHandler.HandleEmbedView)
router.GET("/health", healthHandler.HandleHealth)
router.Get("/", signatureHandlers.HandleIndex)
router.Get("/login", authHandlers.HandleLogin)
router.Get("/logout", authHandlers.HandleLogout)
router.Get("/oauth2/callback", authHandlers.HandleOAuthCallback)
router.Get("/status", signatureHandlers.HandleStatusJSON)
router.Get("/status.png", badgeHandler.HandleStatusPNG)
router.Get("/oembed", oembedHandler.HandleOEmbed)
router.Get("/embed", oembedHandler.HandleEmbedView)
router.Get("/health", healthHandler.HandleHealth)
// Protected routes (require authentication)
router.GET("/sign", authMiddleware.RequireAuth(signatureHandlers.HandleSignGET))
router.POST("/sign", authMiddleware.RequireAuth(signatureHandlers.HandleSignPOST))
router.GET("/signatures", authMiddleware.RequireAuth(signatureHandlers.HandleUserSignatures))
router.Get("/sign", authMiddleware.RequireAuth(signatureHandlers.HandleSignGET))
router.Post("/sign", authMiddleware.RequireAuth(signatureHandlers.HandleSignPOST))
router.Get("/signatures", authMiddleware.RequireAuth(signatureHandlers.HandleUserSignatures))
// Enterprise routes (only enabled if multitenant is true)
if multitenant {
// Add placeholder routes for enterprise features
// These will be overridden/extended by the EE edition
router.GET("/healthz", healthHandler.HandleHealth) // Alternative health endpoint for EE
}
// Note: Enterprise routes can be added via RegisterRoutes method
return router
}