From 84521d8d596bb6f9b2ca0e3b1a4b75742fc6bb4b Mon Sep 17 00:00:00 2001 From: Flavio Fois Date: Mon, 23 Mar 2026 09:18:16 +0100 Subject: [PATCH] refactor routing into a modular and versioned structure Move inline route definitions from main.go into a dedicated internal/routes package. This organization introduces support for API versioning (v1) and separates endpoint logic into specialized modules for administration and bug reporting. --- internal/routes/routes.go | 22 +++++++++ internal/routes/v1/admin.go | 37 +++++++++++++++ internal/routes/v1/bug_reports.go | 41 ++++++++++++++++ internal/routes/v1/v1.go | 34 ++++++++++++++ main.go | 78 +------------------------------ 5 files changed, 136 insertions(+), 76 deletions(-) create mode 100644 internal/routes/routes.go create mode 100644 internal/routes/v1/admin.go create mode 100644 internal/routes/v1/bug_reports.go create mode 100644 internal/routes/v1/v1.go diff --git a/internal/routes/routes.go b/internal/routes/routes.go new file mode 100644 index 0000000..ea1afbd --- /dev/null +++ b/internal/routes/routes.go @@ -0,0 +1,22 @@ +package routes + +import ( + "net/http" + + v1 "emly-api-go/internal/routes/v1" + + "github.com/go-chi/chi/v5" + "github.com/jmoiron/sqlx" +) + +// RegisterAll mounts every versioned API onto the root router. +// To add a new API version, create internal/routes/v2 and add: +// +// r.Mount("/v2", v2.NewRouter(db)) +func RegisterAll(r chi.Router, db *sqlx.DB) { + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("emly-api-go")) + }) + + r.Mount("/v1", v1.NewRouter(db)) +} diff --git a/internal/routes/v1/admin.go b/internal/routes/v1/admin.go new file mode 100644 index 0000000..7301796 --- /dev/null +++ b/internal/routes/v1/admin.go @@ -0,0 +1,37 @@ +package v1 + +import ( + apimw "emly-api-go/internal/middleware" + "time" + + "emly-api-go/internal/handlers" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/httprate" + "github.com/jmoiron/sqlx" +) + +func registerAdmin(r chi.Router, db *sqlx.DB) { + r.Route("/admin", func(r chi.Router) { + r.Use(httprate.LimitByIP(30, time.Minute)) + + // Auth — public, handles its own credential checks + r.Route("/auth", func(r chi.Router) { + r.Post("/login", handlers.LoginUser(db)) + r.Get("/validate", handlers.ValidateSession(db)) + r.Post("/logout", handlers.LogoutSession(db)) + }) + + // User management — protected via Admin Key + r.Route("/users", func(r chi.Router) { + r.Use(apimw.AdminKeyAuth(db)) + + r.Get("/", handlers.ListUsers(db)) + r.Post("/", handlers.CreateUser(db)) + r.Get("/{id}", handlers.GetUserByID(db)) + r.Patch("/{id}", handlers.UpdateUser(db)) + r.Post("/{id}/reset-password", handlers.ResetPassword(db)) + r.Delete("/{id}", handlers.DeleteUser(db)) + }) + }) +} \ No newline at end of file diff --git a/internal/routes/v1/bug_reports.go b/internal/routes/v1/bug_reports.go new file mode 100644 index 0000000..3e6320b --- /dev/null +++ b/internal/routes/v1/bug_reports.go @@ -0,0 +1,41 @@ +package v1 + +import ( + apimw "emly-api-go/internal/middleware" + "time" + + "emly-api-go/internal/handlers" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/httprate" + "github.com/jmoiron/sqlx" +) + +func registerBugReports(r chi.Router, db *sqlx.DB) { + r.Route("/bug-reports", func(r chi.Router) { + // API key only: submit a report and check count + r.Group(func(r chi.Router) { + r.Use(apimw.APIKeyAuth(db)) + r.Use(httprate.LimitByIP(30, time.Minute)) + + r.Get("/count", handlers.GetReportsCount(db)) + r.Post("/", handlers.CreateBugReport(db)) + }) + + // API key + admin key: full read/write access + r.Group(func(r chi.Router) { + r.Use(apimw.APIKeyAuth(db)) + r.Use(apimw.AdminKeyAuth(db)) + r.Use(httprate.LimitByIP(30, time.Minute)) + + r.Get("/", handlers.GetAllBugReports(db)) + r.Get("/{id}", handlers.GetBugReportByID(db)) + r.Get("/{id}/status", handlers.GetReportStatusByID(db)) + r.Get("/{id}/files", handlers.GetReportFilesByReportID(db)) + r.Get("/{id}/files/{file_id}", handlers.GetReportFileByFileID(db)) + r.Get("/{id}/download", handlers.GetBugReportZipById(db)) + r.Patch("/{id}/status", handlers.PatchBugReportStatus(db)) + r.Delete("/{id}", handlers.DeleteBugReportByID(db)) + }) + }) +} \ No newline at end of file diff --git a/internal/routes/v1/v1.go b/internal/routes/v1/v1.go new file mode 100644 index 0000000..67c29fe --- /dev/null +++ b/internal/routes/v1/v1.go @@ -0,0 +1,34 @@ +package v1 + +import ( + "net/http" + + "emly-api-go/internal/handlers" + + "github.com/go-chi/chi/v5" + "github.com/jmoiron/sqlx" +) + +// NewRouter returns a chi.Router with all /v1 routes mounted. +// Add new API versions by creating an analogous package (e.g. v2) and +// mounting it alongside this one in internal/routes/routes.go. +func NewRouter(db *sqlx.DB) http.Handler { + r := chi.NewRouter() + + r.Use(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-Server", "emly-api-go") + w.Header().Set("X-Powered-By", "Pure Protogen sillyness :3") + next.ServeHTTP(w, r) + }) + }) + + r.Get("/health", handlers.Health(db)) + + r.Route("/api", func(r chi.Router) { + registerAdmin(r, db) + registerBugReports(r, db) + }) + + return r +} diff --git a/main.go b/main.go index 0d6cd78..9dc417d 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,6 @@ package main import ( - apimw "emly-api-go/internal/middleware" "fmt" "log" "net/http" @@ -16,7 +15,7 @@ import ( "emly-api-go/internal/config" "emly-api-go/internal/database" "emly-api-go/internal/database/schema" - "emly-api-go/internal/handlers" + "emly-api-go/internal/routes" ) func main() { @@ -53,80 +52,7 @@ func main() { // Global rate limit to 100 requests per minute r.Use(httprate.LimitByIP(100, time.Minute)) - // Public routes (Not protected by any API Key) - r.Get("/", func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("emly-api-go")) - }) - - r.Route("/v1", func(r chi.Router) { - r.Use(func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("X-Server", "emly-api-go") - w.Header().Set("X-Powered-By", "Pure Protogen sillyness :3") - next.ServeHTTP(w, r) - }) - }) - - // Health – public, no API key required - r.Get("/health", handlers.Health(db)) - - r.Route("/api", func(r chi.Router) { - r.Route("/admin", func(r chi.Router) { - r.Use(httprate.LimitByIP(30, time.Minute)) - - // ROUTE: Auth — public, handles its own credential checks - r.Route("/auth", func(r chi.Router) { - r.Post("/login", handlers.LoginUser(db)) - r.Get("/validate", handlers.ValidateSession(db)) - r.Post("/logout", handlers.LogoutSession(db)) - }) - - // ROUTE: User management — protected via Admin Key - r.Route("/users", func(r chi.Router) { - r.Use(apimw.AdminKeyAuth(db)) - - r.Get("/", handlers.ListUsers(db)) - r.Post("/", handlers.CreateUser(db)) - r.Get("/{id}", handlers.GetUserByID(db)) - r.Patch("/{id}", handlers.UpdateUser(db)) - r.Post("/{id}/reset-password", handlers.ResetPassword(db)) - r.Delete("/{id}", handlers.DeleteUser(db)) - }) - }) - - // ROUTE: Bug Reports - Protected via API Key - r.Route("/bug-reports", func(r chi.Router) { - r.Group(func(r chi.Router) { - r.Use(apimw.APIKeyAuth(db)) - - // Tighter rate-limit on protected group: 30 req / min per IP - r.Use(httprate.LimitByIP(30, time.Minute)) - - r.Get("/count", handlers.GetReportsCount(db)) - r.Post("/", handlers.CreateBugReport(db)) - }) - - r.Group(func(r chi.Router) { - // More strict auth due to sensitive info - r.Use(apimw.APIKeyAuth(db)) - r.Use(apimw.AdminKeyAuth(db)) - - // Tighter rate-limit on protected group: 30 req / min per IP - r.Use(httprate.LimitByIP(30, time.Minute)) - - r.Get("/", handlers.GetAllBugReports(db)) - r.Get("/{id}", handlers.GetBugReportByID(db)) - r.Get("/{id}/status", handlers.GetReportStatusByID(db)) - r.Get("/{id}/files", handlers.GetReportFilesByReportID(db)) - r.Get("/{id}/files/{file_id}", handlers.GetReportFileByFileID(db)) - r.Get("/{id}/download", handlers.GetBugReportZipById(db)) - r.Patch("/{id}/status", handlers.PatchBugReportStatus(db)) - r.Delete("/{id}", handlers.DeleteBugReportByID(db)) - }) - }) - }) - - }) + routes.RegisterAll(r, db) addr := fmt.Sprintf(":%s", cfg.Port) log.Printf("server listening on %s", addr)