diff --git a/cmd/community/main.go b/cmd/community/main.go index 3926690..3d15966 100644 --- a/cmd/community/main.go +++ b/cmd/community/main.go @@ -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) } diff --git a/go.mod b/go.mod index 5cc9a15..21ae021 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index dde3f2b..4c2249d 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/presentation/handlers/auth.go b/internal/presentation/handlers/auth.go index 1ade6a7..e83b1c0 100644 --- a/internal/presentation/handlers/auth.go +++ b/internal/presentation/handlers/auth.go @@ -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") diff --git a/internal/presentation/handlers/badge.go b/internal/presentation/handlers/badge.go index d51c34c..65c08e1 100644 --- a/internal/presentation/handlers/badge.go +++ b/internal/presentation/handlers/badge.go @@ -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) diff --git a/internal/presentation/handlers/handlers_test.go b/internal/presentation/handlers/handlers_test.go index 2628de7..516ee8c 100644 --- a/internal/presentation/handlers/handlers_test.go +++ b/internal/presentation/handlers/handlers_test.go @@ -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) diff --git a/internal/presentation/handlers/handlers_utils_test.go b/internal/presentation/handlers/handlers_utils_test.go index 200cac6..39177a1 100644 --- a/internal/presentation/handlers/handlers_utils_test.go +++ b/internal/presentation/handlers/handlers_utils_test.go @@ -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) diff --git a/internal/presentation/handlers/health.go b/internal/presentation/handlers/health.go index fd0c9c4..aae2f88 100644 --- a/internal/presentation/handlers/health.go +++ b/internal/presentation/handlers/health.go @@ -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(), diff --git a/internal/presentation/handlers/middleware.go b/internal/presentation/handlers/middleware.go index f2c9476..dd95dc7 100644 --- a/internal/presentation/handlers/middleware.go +++ b/internal/presentation/handlers/middleware.go @@ -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) } } diff --git a/internal/presentation/handlers/oembed.go b/internal/presentation/handlers/oembed.go index 92278b4..c343222 100644 --- a/internal/presentation/handlers/oembed.go +++ b/internal/presentation/handlers/oembed.go @@ -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) diff --git a/internal/presentation/handlers/signature.go b/internal/presentation/handlers/signature.go index 3f96634..7a35d11 100644 --- a/internal/presentation/handlers/signature.go +++ b/internal/presentation/handlers/signature.go @@ -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) diff --git a/pkg/web/server.go b/pkg/web/server.go index 10266e0..f768888 100644 --- a/pkg/web/server.go +++ b/pkg/web/server.go @@ -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 }