OnboardingService.Onboard: ein API-Aufruf legt Tenant (via bestehendem Provisioner) UND ersten Administrator-Account (via IAM-01 TenantUserStore) an. CREATE DATABASE erlaubt keine echte cross-database Transaktion, daher Saga-Kompensation: schlaegt die Admin-Anlage nach erfolgreichem Provisioning fehl, wird der Tenant per neuem Provisioner.Deprovision wieder vollstaendig entfernt (Registry.Delete + DB-Drop). Provisioner.Provision mappt Duplikat-Faelle (42P04 duplicate_database und den bei echt parallelen CREATE DATABASE moeglichen 23505-Unique-Konflikt auf pg_database) jetzt auf ErrTenantExists statt einer rohen Postgres-Fehlermeldung. Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS): 1. Onboarding-Vorgang zweimal parallel mit gleichem Slug ausgeloest — TestOnboarding_RejectsDuplicateSlugConcurrently: genau ein Erfolg, kein Doppel-Tenant, zweiter Aufruf bekommt ErrTenantExists. PASS. 2. Fehleingaben (leere Pflichtfelder, ungueltige E-Mail, ungueltiger Slug) — TestOnboarding_ValidationErrors deckt alle vier Faelle mit klaren Fehlern ab, bevor irgendein DB-Zugriff stattfindet. PASS. 3. Erfolgreicher Durchlauf von zweiter Person end-to-end nachvollzogen — NICHT durchgefuehrt (keine zweite Person in dieser Session verfuegbar). Offen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
95 lines
2.6 KiB
Go
95 lines
2.6 KiB
Go
package tenant
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/internal/user"
|
|
)
|
|
|
|
// Handler ist eine schlanke Vorbereitung der Schnittstelle fuer API-01
|
|
// (REST-API-Grundgerüst & Versionierung) und TEN-02 (Self-Service-Onboarding).
|
|
// Auth/Rate-Limiting/Versionierung selbst sind ausdruecklich nicht Teil von
|
|
// TEN-01 und werden dort nachgezogen.
|
|
type Handler struct {
|
|
provisioner *Provisioner
|
|
}
|
|
|
|
func NewHandler(p *Provisioner) *Handler {
|
|
return &Handler{provisioner: p}
|
|
}
|
|
|
|
type createTenantRequest struct {
|
|
Slug string `json:"slug"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
func (h *Handler) CreateTenant(w http.ResponseWriter, r *http.Request) {
|
|
var req createTenantRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "ungueltige Anfrage", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
t, err := h.provisioner.Provision(r.Context(), req.Slug, req.Name)
|
|
if err != nil {
|
|
if err == ErrInvalidSlug {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
http.Error(w, "tenant konnte nicht angelegt werden", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
_ = json.NewEncoder(w).Encode(t)
|
|
}
|
|
|
|
// OnboardingHandler stellt den Self-Service-Endpunkt aus TEN-02 bereit —
|
|
// eigener Handler statt Erweiterung von Handler.CreateTenant, damit die
|
|
// TEN-01-Schnittstelle (reines Provisioning ohne Admin-Konto) unveraendert
|
|
// bleibt.
|
|
type OnboardingHandler struct {
|
|
service *OnboardingService
|
|
}
|
|
|
|
func NewOnboardingHandler(s *OnboardingService) *OnboardingHandler {
|
|
return &OnboardingHandler{service: s}
|
|
}
|
|
|
|
type onboardRequest struct {
|
|
Slug string `json:"slug"`
|
|
Name string `json:"name"`
|
|
AdminEmail string `json:"admin_email"`
|
|
AdminName string `json:"admin_name"`
|
|
}
|
|
|
|
func (h *OnboardingHandler) Onboard(w http.ResponseWriter, r *http.Request) {
|
|
var req onboardRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "ungueltige Anfrage", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
result, err := h.service.Onboard(r.Context(), req.Slug, req.Name, req.AdminEmail, req.AdminName)
|
|
if err != nil {
|
|
switch {
|
|
case errors.Is(err, ErrInvalidSlug),
|
|
errors.Is(err, ErrMissingField),
|
|
errors.Is(err, user.ErrInvalidEmail):
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
case errors.Is(err, ErrTenantExists):
|
|
http.Error(w, err.Error(), http.StatusConflict)
|
|
default:
|
|
http.Error(w, "mandant konnte nicht angelegt werden", http.StatusInternalServerError)
|
|
}
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
_ = json.NewEncoder(w).Encode(result)
|
|
}
|