package tenant import ( "context" "errors" "fmt" "os" "github.com/jackc/pgx/v5/pgxpool" "gitea.perlbach24.de/scripte/nexarch/internal/user" ) var ErrMissingField = errors.New("tenant: pflichtfeld fehlt") // OnboardingService implementiert TEN-02: Self-Service-Anlage eines neuen // Mandanten inklusive Ersteinrichtung des Administrator-Kontos in einem // Vorgang. Schlaegt die Admin-Anlage fehl, wird der bereits provisionierte // Tenant per Deprovision wieder vollstaendig entfernt (Saga-Kompensation, // da CREATE DATABASE keine echte cross-database Transaktion erlaubt). type OnboardingService struct { provisioner *Provisioner // tenantSchemaPath zeigt auf migrations/tenant/0001_users.up.sql, mit der // die frisch angelegte Tenant-Datenbank initialisiert wird, bevor der // erste Administrator angelegt werden kann. tenantSchemaPath string } func NewOnboardingService(provisioner *Provisioner, tenantSchemaPath string) *OnboardingService { return &OnboardingService{provisioner: provisioner, tenantSchemaPath: tenantSchemaPath} } type OnboardResult struct { Tenant Tenant Admin user.User } func (s *OnboardingService) Onboard(ctx context.Context, slug, tenantName, adminEmail, adminName string) (OnboardResult, error) { if tenantName == "" { return OnboardResult{}, fmt.Errorf("%w: name", ErrMissingField) } if adminName == "" { return OnboardResult{}, fmt.Errorf("%w: admin-name", ErrMissingField) } if err := ValidateSlug(slug); err != nil { return OnboardResult{}, err } if err := user.ValidateEmail(adminEmail); err != nil { return OnboardResult{}, err } t, err := s.provisioner.Provision(ctx, slug, tenantName) if err != nil { return OnboardResult{}, err } admin, err := s.setupAdmin(ctx, t, adminEmail, adminName) if err != nil { _ = s.provisioner.Deprovision(ctx, t) return OnboardResult{}, err } return OnboardResult{Tenant: t, Admin: admin}, nil } func (s *OnboardingService) setupAdmin(ctx context.Context, t Tenant, adminEmail, adminName string) (user.User, error) { pool, err := pgxpool.New(ctx, t.DBDSN) if err != nil { return user.User{}, fmt.Errorf("verbindung zur neuen tenant-datenbank: %w", err) } defer pool.Close() schema, err := os.ReadFile(s.tenantSchemaPath) if err != nil { return user.User{}, fmt.Errorf("tenant-schema lesen: %w", err) } if _, err := pool.Exec(ctx, string(schema)); err != nil { return user.User{}, fmt.Errorf("tenant-schema anwenden: %w", err) } store := user.NewTenantUserStore(pool) return store.Create(ctx, adminEmail, adminName) }