Switch from net/http to gin-gonic web framework

- Added gin-gonic v1.10.0 dependency
- Refactored router.go: clean route groups with middleware chains
- Refactored all handlers to use gin.Context instead of http.ResponseWriter/*http.Request
- Simplified response helpers (JSON, Error, Success, Created, NoContent)
- Clean auth middleware using Gin's c.Set() for context
- Cleaner route definitions with path parameters (e.g., /domains/:name/users/:id)
- Admin routes moved to /api/admin group with RequireAdmin middleware
This commit is contained in:
Christoph Haas 2026-03-22 23:28:28 +01:00
parent 68285d861a
commit 2834657125
13 changed files with 474 additions and 812 deletions

View file

@ -1,10 +1,9 @@
package api
import (
"context"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/imc-vibe/backend/internal/api/handlers"
"github.com/imc-vibe/backend/internal/auth"
"github.com/imc-vibe/backend/internal/config"
@ -21,14 +20,13 @@ type Router struct {
queueHandler *handlers.QueueHandler
logsHandler *handlers.LogsHandler
jwtManager *auth.JWTManager
db *db.DB
}
func New(database *db.DB, cfg *config.Config) *Router {
jwtManager := auth.NewJWTManager(cfg.JWTSecret, "imc-vibe")
emailService := mail.NewEmailService(cfg)
return &Router{
r := &Router{
authHandler: handlers.NewAuthHandler(database, jwtManager, emailService),
domainHandler: handlers.NewDomainHandler(database),
userHandler: handlers.NewUserHandler(database),
@ -37,213 +35,53 @@ func New(database *db.DB, cfg *config.Config) *Router {
queueHandler: handlers.NewQueueHandler(),
logsHandler: handlers.NewLogsHandler(),
jwtManager: jwtManager,
db: database,
}
return r
}
func (r *Router) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
path := req.URL.Path
// Only handle /api/* routes
if !strings.HasPrefix(path, "/api/") {
http.NotFound(w, req)
return
}
// Public routes - no auth required
switch path {
case "/api/auth/login":
r.authHandler.Login(w, req)
return
case "/api/auth/forgot":
r.authHandler.ForgotPassword(w, req)
return
case "/api/auth/logout":
r.authHandler.Logout(w, req)
return
}
// Protected routes - auth required
req = r.validateAuth(req)
if req == nil {
http.Error(w, `{"error":"not authenticated"}`, http.StatusUnauthorized)
return
}
switch {
case path == "/api/auth/me":
r.authHandler.Me(w, req)
case path == "/api/auth/change-password":
r.authHandler.ChangePassword(w, req)
case path == "/api/stats":
r.statsHandler.Get(w, req)
case strings.HasPrefix(path, "/api/domains/"):
if strings.Contains(strings.TrimPrefix(path, "/api/domains/"), "/users") {
r.handleDomainUsers(w, req)
} else if strings.Contains(strings.TrimPrefix(path, "/api/domains/"), "/aliases") {
r.handleDomainAliases(w, req)
} else {
r.handleDomains(w, req)
}
case strings.HasPrefix(path, "/api/domains"):
r.handleDomains(w, req)
case strings.HasPrefix(path, "/api/queue"):
r.handleQueue(w, req)
case path == "/api/logs":
r.logsHandler.List(w, req)
default:
http.NotFound(w, req)
}
func (r *Router) RegisterRoutes(engine *gin.Engine) {
engine.GET("/api/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
}
func (r *Router) validateAuth(req *http.Request) *http.Request {
authHeader := req.Header.Get("Authorization")
if authHeader == "" {
return nil
engine.POST("/api/auth/login", r.authHandler.Login)
engine.POST("/api/auth/forgot", r.authHandler.ForgotPassword)
engine.POST("/api/auth/logout", r.authHandler.Logout)
authGroup := engine.Group("/api")
authGroup.Use(handlers.AuthMiddleware(r.jwtManager))
{
authGroup.GET("/auth/me", r.authHandler.Me)
authGroup.POST("/auth/change-password", r.authHandler.ChangePassword)
authGroup.GET("/stats", r.statsHandler.Get)
authGroup.GET("/domains", r.domainHandler.List)
authGroup.GET("/domains/:name", r.domainHandler.Get)
authGroup.GET("/domains/:name/users", r.userHandler.List)
authGroup.GET("/domains/:name/users/:id", r.userHandler.Get)
authGroup.POST("/domains/:name/users", r.userHandler.Create)
authGroup.PUT("/domains/:name/users/:id", r.userHandler.Update)
authGroup.DELETE("/domains/:name/users/:id", r.userHandler.Delete)
authGroup.GET("/domains/:name/aliases", r.aliasHandler.List)
authGroup.POST("/domains/:name/aliases", r.aliasHandler.Create)
authGroup.DELETE("/domains/:name/aliases/:id", r.aliasHandler.Delete)
authGroup.GET("/queue", r.queueHandler.List)
authGroup.POST("/queue/:id/requeue", r.queueHandler.Requeue)
authGroup.DELETE("/queue/:id", r.queueHandler.Delete)
authGroup.GET("/logs", r.logsHandler.List)
}
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
return nil
}
claims, err := r.jwtManager.ValidateToken(parts[1])
if err != nil {
return nil
}
authCtx := &auth.Context{
UserID: claims.UserID,
Username: claims.Username,
Role: auth.Role(claims.Role),
}
ctx := context.WithValue(req.Context(), handlers.AuthContextKey, authCtx)
return req.WithContext(ctx)
}
func (r *Router) handleDomains(w http.ResponseWriter, req *http.Request) {
switch req.Method {
case http.MethodGet:
if len(req.URL.Path) > len("/api/domains/") {
r.domainHandler.Get(w, req)
} else {
r.domainHandler.List(w, req)
}
case http.MethodPost:
r.domainHandler.Create(w, req)
case http.MethodDelete:
r.domainHandler.Delete(w, req)
default:
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
}
}
func (r *Router) handleDomainUsers(w http.ResponseWriter, req *http.Request) {
path := req.URL.Path
parts := strings.Split(strings.TrimPrefix(path, "/api/domains/"), "/")
if len(parts) < 2 || parts[1] != "users" {
http.NotFound(w, req)
return
}
domainName := parts[0]
var idStr string
if len(parts) >= 3 {
idStr = parts[2]
}
switch req.Method {
case http.MethodGet:
if idStr != "" {
req.URL.Path = "/api/domains/" + domainName + "/users/" + idStr
r.userHandler.Get(w, req)
} else {
req.URL.Path = "/api/domains/" + domainName + "/users"
r.userHandler.List(w, req)
}
case http.MethodPost:
req.URL.Path = "/api/domains/" + domainName + "/users"
r.userHandler.Create(w, req)
case http.MethodPut:
if idStr != "" {
req.URL.Path = "/api/domains/" + domainName + "/users/" + idStr
r.userHandler.Update(w, req)
} else {
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
}
case http.MethodDelete:
if idStr != "" {
req.URL.Path = "/api/domains/" + domainName + "/users/" + idStr
r.userHandler.Delete(w, req)
} else {
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
}
default:
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
}
}
func (r *Router) handleDomainAliases(w http.ResponseWriter, req *http.Request) {
path := req.URL.Path
parts := strings.Split(strings.TrimPrefix(path, "/api/domains/"), "/")
if len(parts) < 2 || parts[1] != "aliases" {
http.NotFound(w, req)
return
}
domainName := parts[0]
var idStr string
if len(parts) >= 3 {
idStr = parts[2]
}
switch req.Method {
case http.MethodGet:
req.URL.Path = "/api/domains/" + domainName + "/aliases"
r.aliasHandler.List(w, req)
case http.MethodPost:
req.URL.Path = "/api/domains/" + domainName + "/aliases"
r.aliasHandler.Create(w, req)
case http.MethodDelete:
if idStr != "" {
req.URL.Path = "/api/domains/" + domainName + "/aliases/" + idStr
r.aliasHandler.Delete(w, req)
} else {
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
}
default:
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
}
}
func (r *Router) handleQueue(w http.ResponseWriter, req *http.Request) {
path := req.URL.Path
id := ""
if len(path) > len("/api/queue/") {
id = path[len("/api/queue/"):]
}
switch req.Method {
case http.MethodGet:
r.queueHandler.List(w, req)
case http.MethodPost:
if id != "" {
req.URL.Path = "/api/queue/" + id
r.queueHandler.Requeue(w, req)
} else {
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
}
case http.MethodDelete:
if id != "" {
req.URL.Path = "/api/queue/" + id
r.queueHandler.Delete(w, req)
} else {
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
}
default:
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
adminGroup := engine.Group("/api/admin")
adminGroup.Use(handlers.AuthMiddleware(r.jwtManager), handlers.RequireAdmin())
{
adminGroup.POST("/domains", r.domainHandler.Create)
adminGroup.DELETE("/domains/:name", r.domainHandler.Delete)
adminGroup.GET("/domains/:name/permissions", r.domainHandler.GetPermissions)
adminGroup.POST("/domains/:name/permissions", r.domainHandler.AddPermission)
adminGroup.DELETE("/domains/:name/permissions/:userId", r.domainHandler.RemovePermission)
}
}