Initial commit: Open sourcing all of the Maple Open Technologies code.
This commit is contained in:
commit
755d54a99d
2010 changed files with 448675 additions and 0 deletions
400
cloud/maplefile-backend/internal/interface/http/wire_server.go
Normal file
400
cloud/maplefile-backend/internal/interface/http/wire_server.go
Normal file
|
|
@ -0,0 +1,400 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/auth"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
|
||||
svc_auth "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/auth"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/validation"
|
||||
)
|
||||
|
||||
// WireServer is a Wire-compatible HTTP server (without fx.Lifecycle dependency)
|
||||
type WireServer struct {
|
||||
server *http.Server
|
||||
logger *zap.Logger
|
||||
config *config.Config
|
||||
mux *http.ServeMux
|
||||
handlers *Handlers
|
||||
registerHandler *auth.RegisterHandler
|
||||
requestOTTHandler *auth.RequestOTTHandler
|
||||
verifyOTTHandler *auth.VerifyOTTHandler
|
||||
completeLoginHandler *auth.CompleteLoginHandler
|
||||
refreshTokenHandler *auth.RefreshTokenHandler
|
||||
verifyEmailHandler *auth.VerifyEmailHandler
|
||||
resendVerificationHandler *auth.ResendVerificationHandler
|
||||
recoveryInitiateHandler *auth.RecoveryInitiateHandler
|
||||
recoveryVerifyHandler *auth.RecoveryVerifyHandler
|
||||
recoveryCompleteHandler *auth.RecoveryCompleteHandler
|
||||
rateLimitMiddleware *middleware.RateLimitMiddleware
|
||||
securityHeadersMiddleware *middleware.SecurityHeadersMiddleware
|
||||
}
|
||||
|
||||
// NewWireServer creates a Wire-compatible HTTP server
|
||||
func NewWireServer(
|
||||
cfg *config.Config,
|
||||
logger *zap.Logger,
|
||||
handlers *Handlers,
|
||||
registerService svc_auth.RegisterService,
|
||||
verifyEmailService svc_auth.VerifyEmailService,
|
||||
resendVerificationService svc_auth.ResendVerificationService,
|
||||
requestOTTService svc_auth.RequestOTTService,
|
||||
verifyOTTService svc_auth.VerifyOTTService,
|
||||
completeLoginService svc_auth.CompleteLoginService,
|
||||
refreshTokenService svc_auth.RefreshTokenService,
|
||||
recoveryInitiateService svc_auth.RecoveryInitiateService,
|
||||
recoveryVerifyService svc_auth.RecoveryVerifyService,
|
||||
recoveryCompleteService svc_auth.RecoveryCompleteService,
|
||||
rateLimitMiddleware *middleware.RateLimitMiddleware,
|
||||
securityHeadersMiddleware *middleware.SecurityHeadersMiddleware,
|
||||
) *WireServer {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Initialize auth handlers with services
|
||||
registerHandler := auth.NewRegisterHandler(logger, registerService)
|
||||
verifyEmailHandler := auth.NewVerifyEmailHandler(logger, verifyEmailService)
|
||||
resendVerificationHandler := auth.NewResendVerificationHandler(logger, resendVerificationService)
|
||||
requestOTTHandler := auth.NewRequestOTTHandler(logger, requestOTTService)
|
||||
verifyOTTHandler := auth.NewVerifyOTTHandler(logger, verifyOTTService)
|
||||
completeLoginHandler := auth.NewCompleteLoginHandler(logger, completeLoginService)
|
||||
refreshTokenHandler := auth.NewRefreshTokenHandler(logger, refreshTokenService)
|
||||
recoveryInitiateHandler := auth.NewRecoveryInitiateHandler(logger, recoveryInitiateService)
|
||||
recoveryVerifyHandler := auth.NewRecoveryVerifyHandler(logger, recoveryVerifyService)
|
||||
recoveryCompleteHandler := auth.NewRecoveryCompleteHandler(logger, recoveryCompleteService)
|
||||
|
||||
s := &WireServer{
|
||||
logger: logger,
|
||||
config: cfg,
|
||||
mux: mux,
|
||||
handlers: handlers,
|
||||
registerHandler: registerHandler,
|
||||
requestOTTHandler: requestOTTHandler,
|
||||
verifyOTTHandler: verifyOTTHandler,
|
||||
completeLoginHandler: completeLoginHandler,
|
||||
refreshTokenHandler: refreshTokenHandler,
|
||||
verifyEmailHandler: verifyEmailHandler,
|
||||
resendVerificationHandler: resendVerificationHandler,
|
||||
recoveryInitiateHandler: recoveryInitiateHandler,
|
||||
recoveryVerifyHandler: recoveryVerifyHandler,
|
||||
recoveryCompleteHandler: recoveryCompleteHandler,
|
||||
rateLimitMiddleware: rateLimitMiddleware,
|
||||
securityHeadersMiddleware: securityHeadersMiddleware,
|
||||
}
|
||||
|
||||
// Register routes (simplified for Phase 2)
|
||||
s.registerRoutes()
|
||||
|
||||
// Create HTTP server with middleware
|
||||
s.server = &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port),
|
||||
Handler: s.applyMiddleware(mux),
|
||||
ReadTimeout: cfg.Server.ReadTimeout,
|
||||
WriteTimeout: cfg.Server.WriteTimeout,
|
||||
IdleTimeout: cfg.Server.IdleTimeout,
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// Start starts the HTTP server
|
||||
func (s *WireServer) Start() error {
|
||||
s.logger.Info("Starting HTTP server",
|
||||
zap.String("address", s.server.Addr),
|
||||
zap.String("environment", s.config.App.Environment),
|
||||
)
|
||||
|
||||
if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
return fmt.Errorf("HTTP server failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown gracefully shuts down the HTTP server
|
||||
func (s *WireServer) Shutdown(ctx context.Context) error {
|
||||
s.logger.Info("Shutting down HTTP server")
|
||||
return s.server.Shutdown(ctx)
|
||||
}
|
||||
|
||||
// registerRoutes registers all HTTP routes
|
||||
func (s *WireServer) registerRoutes() {
|
||||
s.logger.Info("Registering HTTP routes")
|
||||
|
||||
// ===== Public Routes =====
|
||||
s.mux.HandleFunc("GET /health", s.healthCheckHandler)
|
||||
s.mux.HandleFunc("GET /version", s.versionHandler)
|
||||
|
||||
// User lookup - Public route for user public key lookup
|
||||
s.mux.HandleFunc("GET /iam/api/v1/users/lookup", s.handlers.UserPublicLookup.ServeHTTP)
|
||||
|
||||
// Auth routes - Registration & Email Verification (with auth rate limiting)
|
||||
// These endpoints use general auth rate limiting to prevent automated abuse
|
||||
s.mux.HandleFunc("POST /api/v1/register", s.rateLimitMiddleware.AuthRateLimit(s.registerHandler.ServeHTTP))
|
||||
s.mux.HandleFunc("POST /api/v1/verify-email-code", s.rateLimitMiddleware.AuthRateLimit(s.verifyEmailHandler.ServeHTTP))
|
||||
s.mux.HandleFunc("POST /api/v1/resend-verification", s.rateLimitMiddleware.AuthRateLimit(s.resendVerificationHandler.ServeHTTP))
|
||||
|
||||
// Auth routes - Login Flow (OTT-based) (with login rate limiting)
|
||||
// These endpoints use login-specific rate limiting with account lockout
|
||||
// CWE-307: Protection against brute force attacks
|
||||
s.mux.HandleFunc("POST /api/v1/request-ott", s.rateLimitMiddleware.LoginRateLimit(s.requestOTTHandler.ServeHTTP))
|
||||
s.mux.HandleFunc("POST /api/v1/verify-ott", s.rateLimitMiddleware.LoginRateLimit(s.verifyOTTHandler.ServeHTTP))
|
||||
s.mux.HandleFunc("POST /api/v1/complete-login", s.rateLimitMiddleware.LoginRateLimit(s.completeLoginHandler.ServeHTTP))
|
||||
|
||||
// Auth routes - Token Management (with auth rate limiting)
|
||||
s.mux.HandleFunc("POST /api/v1/token/refresh", s.rateLimitMiddleware.AuthRateLimit(s.refreshTokenHandler.ServeHTTP))
|
||||
|
||||
// Auth routes - Account Recovery (with login rate limiting)
|
||||
// Recovery endpoints need same protection as login to prevent enumeration attacks
|
||||
s.mux.HandleFunc("POST /api/v1/recovery/initiate", s.rateLimitMiddleware.LoginRateLimit(s.recoveryInitiateHandler.ServeHTTP))
|
||||
s.mux.HandleFunc("POST /api/v1/recovery/verify", s.rateLimitMiddleware.LoginRateLimit(s.recoveryVerifyHandler.ServeHTTP))
|
||||
s.mux.HandleFunc("POST /api/v1/recovery/complete", s.rateLimitMiddleware.LoginRateLimit(s.recoveryCompleteHandler.ServeHTTP))
|
||||
|
||||
// ===== Protected Routes =====
|
||||
|
||||
// Me / Profile routes
|
||||
s.mux.HandleFunc("GET /api/v1/me", s.handlers.GetMe.ServeHTTP)
|
||||
s.mux.HandleFunc("PUT /api/v1/me", s.handlers.UpdateMe.ServeHTTP)
|
||||
s.mux.HandleFunc("DELETE /api/v1/me", s.handlers.DeleteMe.ServeHTTP)
|
||||
|
||||
// Blocked Email routes
|
||||
s.mux.HandleFunc("POST /api/v1/me/blocked-emails", s.handlers.CreateBlockedEmail.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/me/blocked-emails", s.handlers.ListBlockedEmails.ServeHTTP)
|
||||
s.mux.HandleFunc("DELETE /api/v1/me/blocked-emails/{email}", s.handlers.DeleteBlockedEmail.ServeHTTP)
|
||||
|
||||
// Invite Email routes
|
||||
s.mux.HandleFunc("POST /api/v1/invites/send-email", s.handlers.SendInviteEmail.ServeHTTP)
|
||||
|
||||
// Dashboard
|
||||
s.mux.HandleFunc("GET /api/v1/dashboard", s.handlers.GetDashboard.ServeHTTP)
|
||||
|
||||
// Collections - Basic CRUD
|
||||
s.mux.HandleFunc("POST /api/v1/collections", s.handlers.CreateCollection.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/collections", s.handlers.ListUserCollections.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/collections/{id}", s.handlers.GetCollection.ServeHTTP)
|
||||
s.mux.HandleFunc("PUT /api/v1/collections/{id}", s.handlers.UpdateCollection.ServeHTTP)
|
||||
s.mux.HandleFunc("DELETE /api/v1/collections/{id}", s.handlers.SoftDeleteCollection.ServeHTTP)
|
||||
|
||||
// Collections - Hierarchical
|
||||
s.mux.HandleFunc("GET /api/v1/collections/root", s.handlers.FindRootCollections.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/collections/parent/{parent_id}", s.handlers.FindCollectionsByParent.ServeHTTP)
|
||||
s.mux.HandleFunc("PUT /api/v1/collections/{id}/move", s.handlers.MoveCollection.ServeHTTP)
|
||||
|
||||
// Collections - Sharing
|
||||
s.mux.HandleFunc("POST /api/v1/collections/{id}/share", s.handlers.ShareCollection.ServeHTTP)
|
||||
s.mux.HandleFunc("DELETE /api/v1/collections/{id}/members/{user_id}", s.handlers.RemoveMember.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/collections/shared", s.handlers.ListSharedCollections.ServeHTTP)
|
||||
|
||||
// Collections - Operations
|
||||
s.mux.HandleFunc("PUT /api/v1/collections/{id}/archive", s.handlers.ArchiveCollection.ServeHTTP)
|
||||
s.mux.HandleFunc("PUT /api/v1/collections/{id}/restore", s.handlers.RestoreCollection.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/collections/filtered", s.handlers.GetFilteredCollections.ServeHTTP)
|
||||
s.mux.HandleFunc("POST /api/v1/collections/sync", s.handlers.CollectionSync.ServeHTTP)
|
||||
|
||||
// Tags - Basic CRUD (non-parameterized routes first)
|
||||
s.mux.HandleFunc("POST /api/v1/tags", s.handlers.CreateTag.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/tags", s.handlers.ListTags.ServeHTTP)
|
||||
|
||||
// Tags - Filtering operations (specific paths before wildcards)
|
||||
s.mux.HandleFunc("GET /api/v1/tags/collections", s.handlers.ListCollectionsByTag.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/tags/files", s.handlers.ListFilesByTag.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/tags/search", s.handlers.SearchByTags.ServeHTTP)
|
||||
|
||||
// Tags - Retrieval by entity (using /for/ prefix to avoid route conflicts)
|
||||
s.mux.HandleFunc("GET /api/v1/tags/for/collection/{collection_id}", s.handlers.GetTagsForCollection.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/tags/for/file/{file_id}", s.handlers.GetTagsForFile.ServeHTTP)
|
||||
|
||||
// Tags - Assignment operations (specific paths before generic {id})
|
||||
s.mux.HandleFunc("POST /api/v1/tags/{id}/assign", s.handlers.AssignTag.ServeHTTP)
|
||||
s.mux.HandleFunc("DELETE /api/v1/tags/{tagId}/entities/{entityId}", s.handlers.UnassignTag.ServeHTTP)
|
||||
|
||||
// Tags - Generic CRUD with {id} parameter (MUST come last to avoid conflicts)
|
||||
s.mux.HandleFunc("GET /api/v1/tags/{id}", s.handlers.GetTag.ServeHTTP)
|
||||
s.mux.HandleFunc("PUT /api/v1/tags/{id}", s.handlers.UpdateTag.ServeHTTP)
|
||||
s.mux.HandleFunc("DELETE /api/v1/tags/{id}", s.handlers.DeleteTag.ServeHTTP)
|
||||
|
||||
// Files - Non-parameterized routes (no wildcards)
|
||||
s.mux.HandleFunc("POST /api/v1/files/pending", s.handlers.CreatePendingFile.ServeHTTP)
|
||||
s.mux.HandleFunc("POST /api/v1/files/delete-multiple", s.handlers.DeleteMultipleFiles.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/files/recent", s.handlers.ListRecentFiles.ServeHTTP)
|
||||
s.mux.HandleFunc("POST /api/v1/files/sync", s.handlers.FileSync.ServeHTTP)
|
||||
|
||||
// Files - Parameterized routes under /file/ prefix (singular) to avoid conflicts
|
||||
s.mux.HandleFunc("POST /api/v1/file/{id}/complete", s.handlers.CompleteFileUpload.ServeHTTP)
|
||||
s.mux.HandleFunc("POST /api/v1/file/{id}/download-completed", s.handlers.ReportDownloadCompleted.ServeHTTP)
|
||||
s.mux.HandleFunc("PUT /api/v1/file/{id}/archive", s.handlers.ArchiveFile.ServeHTTP)
|
||||
s.mux.HandleFunc("PUT /api/v1/file/{id}/restore", s.handlers.RestoreFile.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/file/{id}/upload-url", s.handlers.GetPresignedUploadURL.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/file/{id}/download-url", s.handlers.GetPresignedDownloadURL.ServeHTTP)
|
||||
s.mux.HandleFunc("GET /api/v1/file/{id}", s.handlers.GetFile.ServeHTTP)
|
||||
s.mux.HandleFunc("PUT /api/v1/file/{id}", s.handlers.UpdateFile.ServeHTTP)
|
||||
s.mux.HandleFunc("DELETE /api/v1/file/{id}", s.handlers.SoftDeleteFile.ServeHTTP)
|
||||
|
||||
// Files by collection - under /collection/ prefix
|
||||
s.mux.HandleFunc("GET /api/v1/collection/{collection_id}/files", s.handlers.ListFilesByCollection.ServeHTTP)
|
||||
|
||||
s.logger.Info("HTTP routes registered", zap.Int("total_routes", 71))
|
||||
}
|
||||
|
||||
// Health check handler
|
||||
func (s *WireServer) healthCheckHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"status":"healthy","service":"maplefile-backend","di":"Wire"}`))
|
||||
}
|
||||
|
||||
// Version handler
|
||||
func (s *WireServer) versionHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
version := fmt.Sprintf(`{"version":"%s","environment":"%s","di":"Wire"}`,
|
||||
s.config.App.Version,
|
||||
s.config.App.Environment)
|
||||
w.Write([]byte(version))
|
||||
}
|
||||
|
||||
// Middleware implementations
|
||||
|
||||
// applyMiddleware applies global middleware to the handler
|
||||
func (s *WireServer) applyMiddleware(handler http.Handler) http.Handler {
|
||||
// Apply middleware in reverse order (last applied is executed first)
|
||||
|
||||
// Logging middleware (outermost)
|
||||
handler = s.loggingMiddleware(handler)
|
||||
|
||||
// CORS middleware
|
||||
handler = s.corsMiddleware(handler)
|
||||
|
||||
// Security headers middleware (adds security headers to all responses)
|
||||
handler = s.securityHeadersMiddleware.Handler(handler)
|
||||
|
||||
// Recovery middleware (catches panics)
|
||||
handler = s.recoveryMiddleware(handler)
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
func (s *WireServer) loggingMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Skip logging health check requests
|
||||
if r.URL.Path == "/health" {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Simple logging for Wire version
|
||||
s.logger.Info("HTTP request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", validation.MaskIP(r.RemoteAddr)),
|
||||
)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *WireServer) corsMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Get the origin from the request
|
||||
origin := r.Header.Get("Origin")
|
||||
|
||||
// Build allowed origins map
|
||||
allowedOrigins := make(map[string]bool)
|
||||
|
||||
// In development, always allow localhost origins
|
||||
if s.config.App.Environment == "development" {
|
||||
allowedOrigins["http://localhost:5173"] = true // Vite dev server
|
||||
allowedOrigins["http://localhost:5174"] = true // Alternative Vite port
|
||||
allowedOrigins["http://localhost:3000"] = true // Common React port
|
||||
allowedOrigins["http://127.0.0.1:5173"] = true
|
||||
allowedOrigins["http://127.0.0.1:5174"] = true
|
||||
allowedOrigins["http://127.0.0.1:3000"] = true
|
||||
}
|
||||
|
||||
// Add production origins from configuration
|
||||
for _, allowedOrigin := range s.config.Security.AllowedOrigins {
|
||||
if allowedOrigin != "" {
|
||||
allowedOrigins[allowedOrigin] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the request origin is allowed
|
||||
if allowedOrigins[origin] {
|
||||
// SECURITY FIX: Validate origin before setting CORS headers
|
||||
// CWE-942: Permissive Cross-domain Policy with Untrusted Domains
|
||||
// OWASP A05:2021: Security Misconfiguration - Secure CORS configuration
|
||||
|
||||
// Prevent wildcard origin with credentials (major security risk)
|
||||
if origin == "*" {
|
||||
s.logger.Error("CRITICAL: Wildcard origin (*) cannot be used with credentials",
|
||||
zap.String("path", r.URL.Path))
|
||||
// Don't set CORS headers for wildcard - this is a misconfiguration
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// In production, enforce HTTPS origins for security
|
||||
if s.config.App.Environment == "production" {
|
||||
if len(origin) >= 5 && origin[:5] == "http:" {
|
||||
s.logger.Warn("Non-HTTPS origin rejected in production",
|
||||
zap.String("origin", origin),
|
||||
zap.String("path", r.URL.Path))
|
||||
// Don't set CORS headers for non-HTTPS origins in production
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Set CORS headers for validated origins
|
||||
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
|
||||
// Only set credentials for specific, non-wildcard origins
|
||||
// This prevents credential leakage to untrusted domains
|
||||
if origin != "*" && origin != "" {
|
||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
}
|
||||
|
||||
w.Header().Set("Access-Control-Max-Age", "3600") // Cache preflight for 1 hour
|
||||
|
||||
s.logger.Debug("CORS headers added",
|
||||
zap.String("origin", origin),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.Bool("credentials_allowed", origin != "*"))
|
||||
} else if origin != "" {
|
||||
// Log rejected origins for debugging
|
||||
s.logger.Warn("CORS request from disallowed origin",
|
||||
zap.String("origin", origin),
|
||||
zap.String("path", r.URL.Path))
|
||||
}
|
||||
|
||||
// Handle preflight requests
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *WireServer) recoveryMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
s.logger.Error("Panic recovered",
|
||||
zap.Any("error", err),
|
||||
zap.String("path", r.URL.Path),
|
||||
)
|
||||
problem := httperror.NewInternalServerError("An unexpected error occurred")
|
||||
problem.WithInstance(r.URL.Path)
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
}
|
||||
}()
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue