Files
nexarch/internal/migrationuser/migrate.go
rootandClaude Sonnet 5 7877c89eb2 COR-04: ldap-feld-migration
MapEntry/MapStore und TenantUserStore um die LDAP-Kopplungsfelder
erweitert; MigrateApproved uebernimmt sie in die Tenant-Datenbank.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-14 14:30:14 +00:00

109 lines
3.2 KiB
Go

package migrationuser
import (
"context"
"fmt"
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/internal/tenant"
"gitea.perlbach24.de/scripte/nexarch/internal/user"
)
// PoolOpener oeffnet eine Verbindung zu einer Tenant-Datenbank per DSN.
// Austauschbar fuer Tests, damit kein echter zweiter Netzwerk-Connect-Pfad
// gebraucht wird.
type PoolOpener func(ctx context.Context, dsn string) (*pgxpool.Pool, error)
func defaultPoolOpener(ctx context.Context, dsn string) (*pgxpool.Pool, error) {
return pgxpool.New(ctx, dsn)
}
// Migrator uebernimmt jeden freigegebenen user_migration_map-Eintrag in die
// zugehoerige Tenant-Datenbank, mit der nexarch_user_id aus der Map als
// users.id und dem bcrypt-Hash 1:1 (kein Rehash).
type Migrator struct {
mapStore *MapStore
registry *tenant.Registry
openPool PoolOpener
// tenantPools cached geoeffnete Tenant-Pools je nexarch_tenant_id,
// damit mehrere Benutzer desselben Mandanten nicht wiederholt neu
// verbinden.
tenantPools map[string]*pgxpool.Pool
}
func NewMigrator(mapStore *MapStore, registry *tenant.Registry) *Migrator {
return &Migrator{
mapStore: mapStore,
registry: registry,
openPool: defaultPoolOpener,
tenantPools: map[string]*pgxpool.Pool{},
}
}
// WithPoolOpener ersetzt den Standard-Verbindungsaufbau (fuer Tests).
func (m *Migrator) WithPoolOpener(opener PoolOpener) *Migrator {
m.openPool = opener
return m
}
// Result fasst den Ausgang eines Migrationslaufs zusammen.
type Result struct {
Migrated []user.User
}
// MigrateApproved liest die freigegebene Benutzer-Map und uebernimmt jeden
// Eintrag genau einmal in die passende Tenant-Datenbank (Akzeptanzkriterium
// 1). Ein erneuter Lauf gegen dieselbe Quelle erzeugt keine Duplikate, da
// TenantUserStore.CreateWithID bei bereits existierender ID unveraendert
// zurueckkehrt (Akzeptanzkriterium 3).
func (m *Migrator) MigrateApproved(ctx context.Context) (Result, error) {
entries, err := m.mapStore.Approved(ctx)
if err != nil {
return Result{}, err
}
var res Result
for _, e := range entries {
pool, err := m.tenantPool(ctx, e.NexarchTenantID)
if err != nil {
return res, fmt.Errorf("tenant-verbindung fuer benutzer %q (map-eintrag %d): %w", e.Email, e.ID, err)
}
store := user.NewTenantUserStore(pool)
u, err := store.CreateWithIDAndLDAP(ctx, e.NexarchUserID, e.Email, e.Name, e.PasswordHash, e.LDAPUID, e.LDAPSource, e.LDAPSyncedAt)
if err != nil {
return res, fmt.Errorf("benutzer %q (map-eintrag %d) uebernehmen: %w", e.Email, e.ID, err)
}
res.Migrated = append(res.Migrated, u)
}
return res, nil
}
func (m *Migrator) tenantPool(ctx context.Context, tenantID string) (*pgxpool.Pool, error) {
if pool, ok := m.tenantPools[tenantID]; ok {
return pool, nil
}
t, err := m.registry.GetByID(ctx, tenantID)
if err != nil {
return nil, fmt.Errorf("tenant %q laden: %w", tenantID, err)
}
pool, err := m.openPool(ctx, t.DBDSN)
if err != nil {
return nil, fmt.Errorf("tenant %q verbinden: %w", tenantID, err)
}
m.tenantPools[tenantID] = pool
return pool, nil
}
// Close schliesst alle im Lauf geoeffneten Tenant-Pools.
func (m *Migrator) Close() {
for _, pool := range m.tenantPools {
pool.Close()
}
m.tenantPools = map[string]*pgxpool.Pool{}
}