// Package handlers provides HTTP request handlers. package handlers import ( "bytes" "encoding/json" "log/slog" "net/http" "github.com/gorilla/csrf" "go.uber.org/fx" "sneak.berlin/go/upaas/internal/database" "sneak.berlin/go/upaas/internal/docker" "sneak.berlin/go/upaas/internal/globals" "sneak.berlin/go/upaas/internal/healthcheck" "sneak.berlin/go/upaas/internal/logger" "sneak.berlin/go/upaas/internal/models" "sneak.berlin/go/upaas/internal/service/app" "sneak.berlin/go/upaas/internal/service/audit" "sneak.berlin/go/upaas/internal/service/auth" "sneak.berlin/go/upaas/internal/service/deploy" "sneak.berlin/go/upaas/internal/service/webhook" "sneak.berlin/go/upaas/templates" ) // Params contains dependencies for Handlers. type Params struct { fx.In Logger *logger.Logger Globals *globals.Globals Database *database.Database Healthcheck *healthcheck.Healthcheck Auth *auth.Service App *app.Service Deploy *deploy.Service Webhook *webhook.Service Docker *docker.Client Audit *audit.Service } // Handlers provides HTTP request handlers. type Handlers struct { log *slog.Logger params *Params db *database.Database hc *healthcheck.Healthcheck auth *auth.Service appService *app.Service deploy *deploy.Service webhook *webhook.Service docker *docker.Client audit *audit.Service globals *globals.Globals } // New creates a new Handlers instance. func New(_ fx.Lifecycle, params Params) (*Handlers, error) { return &Handlers{ log: params.Logger.Get(), params: ¶ms, db: params.Database, hc: params.Healthcheck, auth: params.Auth, appService: params.App, deploy: params.Deploy, webhook: params.Webhook, docker: params.Docker, audit: params.Audit, globals: params.Globals, }, nil } // currentUser returns the currently authenticated user, or nil if not authenticated. func (h *Handlers) currentUser(request *http.Request) *models.User { user, err := h.auth.GetCurrentUser(request.Context(), request) if err != nil || user == nil { return nil } return user } // auditLog records an audit entry for the current request. func (h *Handlers) auditLog( request *http.Request, action models.AuditAction, resourceType models.AuditResourceType, resourceID string, detail string, ) { user := h.currentUser(request) entry := audit.LogEntry{ Action: action, ResourceType: resourceType, ResourceID: resourceID, Detail: detail, } if user != nil { entry.UserID = user.ID entry.Username = user.Username } else { entry.Username = "anonymous" } h.audit.LogFromRequest(request.Context(), request, entry) } // addGlobals adds version info and CSRF token to template data map. func (h *Handlers) addGlobals( data map[string]any, request *http.Request, ) map[string]any { data["Version"] = h.globals.Version data["Appname"] = h.globals.Appname if request != nil { data["CSRFField"] = csrf.TemplateField(request) } return data } // renderTemplate executes the named template into a buffer first, then writes // to the ResponseWriter only on success. This prevents partial/corrupt HTML // responses when template execution fails partway through. func (h *Handlers) renderTemplate( writer http.ResponseWriter, tmpl *templates.TemplateExecutor, name string, data any, ) { var buf bytes.Buffer err := tmpl.ExecuteTemplate(&buf, name, data) if err != nil { h.log.Error("template execution failed", "error", err) http.Error(writer, "Internal Server Error", http.StatusInternalServerError) return } _, _ = buf.WriteTo(writer) } func (h *Handlers) respondJSON( writer http.ResponseWriter, _ *http.Request, data any, status int, ) { writer.Header().Set("Content-Type", "application/json") writer.WriteHeader(status) if data != nil { err := json.NewEncoder(writer).Encode(data) if err != nil { h.log.Error("json encode error", "error", err) } } }