feat(backend): update main entry and test DB utilities

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-06-18 16:26:50 +01:00
co-authored by Sisyphus
parent 2dbb1486b0
commit f7cc278423
2 changed files with 35 additions and 6 deletions
+28 -5
View File
@@ -127,12 +127,13 @@ func main() {
initDav() initDav()
initS3() initS3()
initSquare() initSquare()
auth.StartJTICleanup()
r := chi.NewRouter() r := chi.NewRouter()
// --- Global Middleware --- // --- Global Middleware ---
r.Use(middleware.RequestID) r.Use(middleware.RequestID)
r.Use(middleware.RealIP) r.Use(middleware.ClientIPFromHeader("X-Real-IP"))
r.Use(middleware.Logger) r.Use(middleware.Logger)
r.Use(middleware.Recoverer) r.Use(middleware.Recoverer)
r.Use(middleware.Timeout(15 * time.Second)) r.Use(middleware.Timeout(15 * time.Second))
@@ -145,6 +146,23 @@ func main() {
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains") w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
// TODO: Enable Referrer-Policy in production // TODO: Enable Referrer-Policy in production
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") 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) next.ServeHTTP(w, r)
}) })
}) })
@@ -159,11 +177,11 @@ func main() {
r.Get("/services/eligible-for/{user_id}", services.ServicesEligibleForUserHandler) r.Get("/services/eligible-for/{user_id}", services.ServicesEligibleForUserHandler)
}) })
// Registration: 10/min to prevent spam // Registration: 10/min to prevent spam + progressive per-IP backoff
r.With(mw.RateLimit(10, time.Minute), limitBody(defaultBodyLimit)).Post("/register", authHandlers.RegisterHandler) r.With(mw.ProgressiveRateLimit, mw.RateLimit(10, time.Minute), limitBody(defaultBodyLimit)).Post("/register", authHandlers.RegisterHandler)
// Login: Has its own internal rate limiting // Login: Has its own internal rate limiting + progressive per-IP backoff
r.With(mw.RateLimit(10, time.Minute), limitBody(defaultBodyLimit)).Post("/login", authHandlers.LoginHandler) r.With(mw.ProgressiveRateLimit, mw.RateLimit(10, time.Minute), limitBody(defaultBodyLimit)).Post("/login", authHandlers.LoginHandler)
// Logout: requires valid token // Logout: requires valid token
r.With(mw.RequireAuth).Post("/logout", authHandlers.LogoutHandler) r.With(mw.RequireAuth).Post("/logout", authHandlers.LogoutHandler)
@@ -257,11 +275,16 @@ func main() {
// User payment routes // User payment routes
r.Post("/bookings/{id}/payment", payments.CreateBookingPayment) 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.Get("/user/payment-methods", payments.GetUserPaymentMethods)
r.Post("/user/payment-methods", payments.CreatePaymentMethod) r.Post("/user/payment-methods", payments.CreatePaymentMethod)
r.Delete("/user/payment-methods/{id}", payments.DeletePaymentMethod) r.Delete("/user/payment-methods/{id}", payments.DeletePaymentMethod)
r.Post("/bookings/{id}/tip", payments.CreateTipPayment) r.Post("/bookings/{id}/tip", payments.CreateTipPayment)
r.Get("/bookings/{id}/payment-summary", payments.GetBookingPaymentSummary) 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 // User gift card routes
r.Post("/user/giftcards/redeem", payments.RedeemGiftCard) r.Post("/user/giftcards/redeem", payments.RedeemGiftCard)
+7 -1
View File
@@ -15,7 +15,7 @@ import (
"github.com/jackc/pgx/v5/pgxpool" "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 { func Pool(t *testing.T) *pgxpool.Pool {
t.Helper() t.Helper()
@@ -140,6 +140,9 @@ func Migrate(t *testing.T, pool *pgxpool.Pool) {
"exceptional_working_hours", "exceptional_working_hours",
"exceptional_working_hours_groups", "exceptional_working_hours_groups",
"business_settings", "business_settings",
"login_audit",
"refresh_tokens",
"revoked_jtis",
} }
for _, table := range dropOrder { for _, table := range dropOrder {
@@ -312,6 +315,9 @@ func TruncateTables(t *testing.T, pool *pgxpool.Pool) {
"users", "users",
"images", "images",
"tags", "tags",
"login_audit",
"refresh_tokens",
"revoked_jtis",
} }
for _, table := range tables { for _, table := range tables {