From f7cc2784230eed4fd40df55d29030c79a86bed8f Mon Sep 17 00:00:00 2001 From: Stephen Adamson Date: Thu, 18 Jun 2026 16:26:50 +0100 Subject: [PATCH] feat(backend): update main entry and test DB utilities Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- backend/main.go | 33 +++++++++++++++++++++++++----- backend/testutils/testdb/testdb.go | 8 +++++++- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/backend/main.go b/backend/main.go index 1c36cc2..e01ecb2 100644 --- a/backend/main.go +++ b/backend/main.go @@ -127,12 +127,13 @@ func main() { initDav() initS3() initSquare() + auth.StartJTICleanup() r := chi.NewRouter() // --- Global Middleware --- r.Use(middleware.RequestID) - r.Use(middleware.RealIP) + r.Use(middleware.ClientIPFromHeader("X-Real-IP")) r.Use(middleware.Logger) r.Use(middleware.Recoverer) r.Use(middleware.Timeout(15 * time.Second)) @@ -145,6 +146,23 @@ func main() { w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains") // TODO: Enable Referrer-Policy in production w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") + w.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'") + + // Reflect origin (not wildcard '*') so credentialed cross-origin + // requests with Authorization: Bearer work in browsers. + origin := r.Header.Get("Origin") + if origin != "" { + w.Header().Set("Access-Control-Allow-Origin", origin) + w.Header().Set("Vary", "Origin") + } + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, Idempotency-Key") + + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusNoContent) + return + } + next.ServeHTTP(w, r) }) }) @@ -159,11 +177,11 @@ func main() { r.Get("/services/eligible-for/{user_id}", services.ServicesEligibleForUserHandler) }) - // Registration: 10/min to prevent spam - r.With(mw.RateLimit(10, time.Minute), limitBody(defaultBodyLimit)).Post("/register", authHandlers.RegisterHandler) + // Registration: 10/min to prevent spam + progressive per-IP backoff + r.With(mw.ProgressiveRateLimit, mw.RateLimit(10, time.Minute), limitBody(defaultBodyLimit)).Post("/register", authHandlers.RegisterHandler) - // Login: Has its own internal rate limiting - r.With(mw.RateLimit(10, time.Minute), limitBody(defaultBodyLimit)).Post("/login", authHandlers.LoginHandler) + // Login: Has its own internal rate limiting + progressive per-IP backoff + r.With(mw.ProgressiveRateLimit, mw.RateLimit(10, time.Minute), limitBody(defaultBodyLimit)).Post("/login", authHandlers.LoginHandler) // Logout: requires valid token r.With(mw.RequireAuth).Post("/logout", authHandlers.LogoutHandler) @@ -257,11 +275,16 @@ func main() { // User payment routes r.Post("/bookings/{id}/payment", payments.CreateBookingPayment) + r.Post("/bookings/{id}/apply-redemption", payments.ApplyLoyaltyRedemption) + r.Post("/bookings/{id}/payment-lock", payments.AcquirePaymentLock) + r.Delete("/bookings/{id}/payment-lock", payments.ReleasePaymentLock) r.Get("/user/payment-methods", payments.GetUserPaymentMethods) r.Post("/user/payment-methods", payments.CreatePaymentMethod) r.Delete("/user/payment-methods/{id}", payments.DeletePaymentMethod) r.Post("/bookings/{id}/tip", payments.CreateTipPayment) r.Get("/bookings/{id}/payment-summary", payments.GetBookingPaymentSummary) + r.With(mw.RateLimit(10, time.Minute), limitBody(defaultBodyLimit)). + Get("/bookings/{id}/discount-preview", payments.GetDiscountPreviewHandler) // User gift card routes r.Post("/user/giftcards/redeem", payments.RedeemGiftCard) diff --git a/backend/testutils/testdb/testdb.go b/backend/testutils/testdb/testdb.go index 83838b7..19eea81 100644 --- a/backend/testutils/testdb/testdb.go +++ b/backend/testutils/testdb/testdb.go @@ -15,7 +15,7 @@ import ( "github.com/jackc/pgx/v5/pgxpool" ) -const defaultTestDSN = "postgres://myuser:mypassword@localhost:5432/crussell_test?sslmode=disable" +const defaultTestDSN = "postgres://myuser:mypassword@localhost:5432/crussell_test?sslmode=disable&require_auth=scram-sha-256" func Pool(t *testing.T) *pgxpool.Pool { t.Helper() @@ -140,6 +140,9 @@ func Migrate(t *testing.T, pool *pgxpool.Pool) { "exceptional_working_hours", "exceptional_working_hours_groups", "business_settings", + "login_audit", + "refresh_tokens", + "revoked_jtis", } for _, table := range dropOrder { @@ -312,6 +315,9 @@ func TruncateTables(t *testing.T, pool *pgxpool.Pool) { "users", "images", "tags", + "login_audit", + "refresh_tokens", + "revoked_jtis", } for _, table := range tables {