2
0
Files
Royal-Pop-Accessory/Backend/internal/httpx/router.go
T
2026-06-23 18:50:37 +01:00

133 lines
4.0 KiB
Go

package httpx
import (
"context"
"net/http"
"strings"
"time"
"github.com/go-chi/chi/v5"
chimiddleware "github.com/go-chi/chi/v5/middleware"
"royal-pop-backend/internal/config"
"royal-pop-backend/internal/database"
"royal-pop-backend/internal/inventory"
"royal-pop-backend/internal/mailer"
"royal-pop-backend/internal/orders"
)
type RouterConfig struct {
Config *config.Config
DB *database.DB
Orders *orders.Store
Stock *inventory.Store
Mailer *mailer.ResendMailer
}
func NewRouter(cfg RouterConfig) http.Handler {
router := chi.NewRouter()
router.Use(chimiddleware.RealIP)
router.Use(chimiddleware.RequestID)
router.Use(chimiddleware.Recoverer)
router.Use(chimiddleware.Timeout(30 * time.Second))
router.Use(corsMiddleware(cfg.Config.AllowedOrigins))
router.Get("/healthz", func(w http.ResponseWriter, _ *http.Request) {
databaseStatus := "disabled"
if cfg.DB != nil {
healthCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := cfg.DB.Health(healthCtx); err != nil {
WriteJSON(w, http.StatusServiceUnavailable, map[string]any{
"service": cfg.Config.AppName,
"status": "degraded",
"database": "unreachable",
})
return
}
databaseStatus = "ok"
}
WriteJSON(w, http.StatusOK, map[string]any{
"service": cfg.Config.AppName,
"status": "ok",
"database": databaseStatus,
})
})
router.Post("/webhooks/stripe", handleStripeWebhook(cfg.Config, cfg.Orders, cfg.Mailer))
router.Route("/v1", func(api chi.Router) {
api.Get("/inventory", handleListPublicInventory(cfg.Stock))
api.Get("/storefront/pricing", handleGetStorefrontPricing(cfg.Config))
api.Get("/", func(w http.ResponseWriter, _ *http.Request) {
WriteJSON(w, http.StatusOK, map[string]any{
"service": cfg.Config.AppName,
"status": "scaffolded",
"version": "v1",
})
})
api.Post("/checkout/payment-intent", handleCreatePaymentIntent(cfg.Config, cfg.Orders))
api.Get("/checkout/payment-intent/{paymentIntentID}", handleGetPaymentIntentStatus(cfg.Config, cfg.Orders))
api.Get("/client/session", handleGetClientSession(cfg.Config))
api.Post("/client/login", handleClientLogin(cfg.Config))
api.Post("/client/logout", handleClientLogout(cfg.Config))
api.Group(func(client chi.Router) {
client.Use(requireClientSession(cfg.Config))
client.Get("/client/orders", handleListClientOrders(cfg.Orders))
client.Get("/client/orders/{orderID}", handleGetClientOrder(cfg.Orders))
client.Patch("/client/orders/{orderID}", handleUpdateClientOrder(cfg.Orders, cfg.Mailer))
client.Get("/client/stock", handleListClientStock(cfg.Stock))
client.Put("/client/stock", handleUpsertClientStock(cfg.Stock))
client.Post("/client/reseed", handleClientReseed(cfg.DB))
})
})
router.NotFound(func(w http.ResponseWriter, _ *http.Request) {
WriteError(w, http.StatusNotFound, "not_found", "The requested endpoint does not exist.")
})
return router
}
func corsMiddleware(allowedOrigins []string) func(http.Handler) http.Handler {
normalized := make([]string, 0, len(allowedOrigins))
for _, origin := range allowedOrigins {
trimmed := strings.TrimSpace(origin)
if trimmed == "" {
continue
}
normalized = append(normalized, trimmed)
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if origin != "" {
for _, allowedOrigin := range normalized {
if allowedOrigin == "*" || strings.EqualFold(allowedOrigin, origin) {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Vary", "Origin")
w.Header().Set("Access-Control-Allow-Credentials", "true")
break
}
}
}
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, OPTIONS")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
}