package onboardingapi import ( "context" "encoding/json" "io" "net/http" "strings" "github.com/itworx/pulse/internal/audit" "github.com/itworx/pulse/internal/auth" "github.com/itworx/pulse/internal/correlation" "github.com/itworx/pulse/internal/onboarding" "github.com/itworx/pulse/internal/problem" ) type Service interface { Status(context.Context) (onboarding.Status, error) Complete(context.Context, onboarding.Choice) (onboarding.Status, error) } type Handler struct { Service Service Audit audit.Store } func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { principal, ok := auth.PrincipalFromContext(r.Context()) if !ok { fail(w, r, http.StatusUnauthorized, "UNAUTHORIZED", "Authentication required.") return } if strings.TrimSuffix(r.URL.Path, "/") != "/api/v1/onboarding" { fail(w, r, http.StatusNotFound, "NOT_FOUND", "Onboarding not found.") return } switch r.Method { case http.MethodGet: status, err := h.Service.Status(r.Context()) if err != nil { fail(w, r, http.StatusServiceUnavailable, "ONBOARDING_UNAVAILABLE", "Onboarding status is unavailable.") return } write(w, http.StatusOK, status) case http.MethodPost: if !auth.Allows(principal.Role, auth.PermissionAdmin) { fail(w, r, http.StatusForbidden, "FORBIDDEN", "Only administrators can complete onboarding.") return } var choice onboarding.Choice decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 16<<10)) decoder.DisallowUnknownFields() if err := decoder.Decode(&choice); err != nil { fail(w, r, http.StatusBadRequest, "INVALID_CHOICES", "Onboarding choices are invalid.") return } var extra any if err := decoder.Decode(&extra); err != io.EOF { fail(w, r, http.StatusBadRequest, "INVALID_CHOICES", "Onboarding choices are invalid.") return } status, err := h.Service.Complete(r.Context(), choice) if err != nil { fail(w, r, http.StatusBadRequest, "ONBOARDING_FAILED", "Onboarding kon niet veilig worden afgerond.") return } if h.Audit != nil { if err := h.Audit.Append(r.Context(), audit.Event{Actor: principal.Subject, Action: "onboarding.complete", ResourceType: "system", ResourceID: "00000000-0000-0000-0000-000000000000", Result: "success", CorrelationID: correlation.FromContext(r.Context())}); err != nil { fail(w, r, http.StatusServiceUnavailable, "AUDIT_UNAVAILABLE", "The onboarding audit event could not be recorded.") return } } write(w, http.StatusOK, status) default: fail(w, r, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "This method is not supported.") } } func fail(w http.ResponseWriter, r *http.Request, status int, code, detail string) { problem.Write(w, r, status, code, http.StatusText(status), detail, nil) } func write(w http.ResponseWriter, status int, value any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(value) }