// Package taskapi ist die HTTP-Bindung fuer die Aufgaben-Oberflaeche // (TSK-04): GET /tasks fuer Liste und Kanban-Ansicht zusammen, PATCH // /tasks/{id}/status fuer die Drag&Drop-Statusaenderung. Nutzeridentitaet // aus X-User-Id, Tenant-Kontext aus X-Tenant-Slug -- gleiches Muster wie // backchannelapi (VOR-04/VOR-06): der Tenant-Kontext des anfragenden Nutzers // muss zur eigenen Instanz passen und optional gegen Core bestaetigt werden, // bevor Aufgaben zurueckgegeben werden (Akzeptanzkriterium 5). package taskapi import ( "encoding/json" "errors" "log" "net/http" "gitea.perlbach24.de/scripte/nexarch/workflow/internal/task" ) type Server struct { store *task.Store ownTenantSlug string } func NewServer(store *task.Store, ownTenantSlug string) *Server { return &Server{store: store, ownTenantSlug: ownTenantSlug} } func (s *Server) RegisterRoutes(mux *http.ServeMux) { mux.HandleFunc("GET /tasks", s.handleList) mux.HandleFunc("PATCH /tasks/{id}/status", s.handleSetStatus) } func (s *Server) checkAuth(w http.ResponseWriter, r *http.Request) bool { if r.Header.Get("X-User-Id") == "" { writeError(w, http.StatusUnauthorized, "X-User-Id fehlt") return false } tenantSlug := r.Header.Get("X-Tenant-Slug") if tenantSlug == "" { writeError(w, http.StatusUnauthorized, "X-Tenant-Slug fehlt") return false } if tenantSlug != s.ownTenantSlug { log.Printf("taskapi: tenant-kontext abgelehnt (angefragt=%q, instanz=%q)", tenantSlug, s.ownTenantSlug) writeError(w, http.StatusForbidden, "nicht berechtigt") return false } return true } func (s *Server) handleList(w http.ResponseWriter, r *http.Request) { if !s.checkAuth(w, r) { return } tasks, err := s.store.ListAll(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, "aufgaben konnten nicht gelesen werden") return } writeJSON(w, http.StatusOK, tasks) } type setStatusRequest struct { Status string `json:"status"` } func (s *Server) handleSetStatus(w http.ResponseWriter, r *http.Request) { if !s.checkAuth(w, r) { return } id := r.PathValue("id") var req setStatusRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "ungueltige anfrage") return } err := s.store.SetStatus(r.Context(), id, task.Status(req.Status)) switch { case err == nil: w.WriteHeader(http.StatusNoContent) case errors.Is(err, task.ErrInvalidStatus): writeError(w, http.StatusBadRequest, "ungueltiger status") case errors.Is(err, task.ErrNotFound): writeError(w, http.StatusNotFound, "aufgabe nicht gefunden") default: writeError(w, http.StatusInternalServerError, "status konnte nicht gesetzt werden") } } func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(v) } func writeError(w http.ResponseWriter, status int, msg string) { writeJSON(w, status, map[string]string{"error": msg}) }