ultisuite-backend/internal/migration/invite_provision.go
R3D347HR4Y 1ffd0817d8
Some checks are pending
CI / Go tests (push) Waiting to run
CI / Integration tests (push) Waiting to run
CI / DB migrations (push) Waiting to run
feat(migration): enhance migration API with roster and audit export features
- Added endpoints for listing and importing migration rosters.
- Introduced audit export functionality for migration jobs in CSV and NDJSON formats.
- Implemented tenant mismatch validation for Microsoft migration claims.
- Enhanced error handling for email claiming and migration processes.
- Added integration tests for roster import and claim workflows.
2026-06-13 13:11:30 +02:00

98 lines
2.8 KiB
Go

package migration
import (
"context"
"strings"
"github.com/jackc/pgx/v5/pgxpool"
)
// HasPendingMigrationInvite reports whether an unclaimed invite exists for the email.
func HasPendingMigrationInvite(ctx context.Context, db *pgxpool.Pool, email string) (bool, error) {
if db == nil {
return false, nil
}
email = normalizeInviteEmail(email)
if email == "" {
return false, nil
}
var exists bool
err := db.QueryRow(ctx, `
SELECT EXISTS(
SELECT 1 FROM migration_invites
WHERE status = 'invited' AND lower(email) = lower($1)
)
`, email).Scan(&exists)
return exists, err
}
// ProvisionAudit counts user-linked provision artifacts for test verification.
type ProvisionAudit struct {
Users int
Mailboxes int
MailAccounts int
NCPrincipals int
}
// AuditProvisionByEmail counts rows tied to an email across users, mailboxes, mail accounts, and Nextcloud credentials.
func AuditProvisionByEmail(ctx context.Context, db *pgxpool.Pool, email string) (ProvisionAudit, error) {
var audit ProvisionAudit
if db == nil {
return audit, nil
}
email = strings.ToLower(strings.TrimSpace(email))
if email == "" {
return audit, nil
}
if err := db.QueryRow(ctx, `
SELECT COUNT(*) FROM users WHERE lower(email) = $1
`, email).Scan(&audit.Users); err != nil {
return audit, err
}
if err := db.QueryRow(ctx, `
SELECT COUNT(*)
FROM mailboxes mb
JOIN mail_domains md ON md.id = mb.domain_id
WHERE lower(mb.local_part || '@' || md.name) = $1
`, email).Scan(&audit.Mailboxes); err != nil {
return audit, err
}
if err := db.QueryRow(ctx, `
SELECT COUNT(*) FROM mail_accounts WHERE lower(email) = $1
`, email).Scan(&audit.MailAccounts); err != nil {
return audit, err
}
if err := db.QueryRow(ctx, `
SELECT COUNT(*) FROM nextcloud_dav_credentials WHERE nc_user_id = $1
`, email).Scan(&audit.NCPrincipals); err != nil {
return audit, err
}
return audit, nil
}
// LinkHostedMailboxByEmail attaches orphan mailboxes/mail_accounts (e.g. from claim-before-enroll) to a user.
func LinkHostedMailboxByEmail(ctx context.Context, db *pgxpool.Pool, userID, email string) error {
email = strings.ToLower(strings.TrimSpace(email))
if email == "" {
return nil
}
_, err := db.Exec(ctx, `
UPDATE mailboxes SET user_id = $1::uuid, updated_at = NOW()
WHERE user_id IS NULL AND lower(local_part || '@' || (SELECT name FROM mail_domains d WHERE d.id = mailboxes.domain_id)) = $2
`, userID, email)
if err != nil {
return err
}
_, err = db.Exec(ctx, `
UPDATE mail_accounts ma SET user_id = $1::uuid, updated_at = NOW()
FROM mailboxes mb
JOIN mail_domains md ON md.id = mb.domain_id
WHERE mb.mail_account_id = ma.id
AND ma.user_id IS NULL
AND mb.user_id = $1::uuid
AND lower(mb.local_part || '@' || md.name) = $2
`, userID, email)
return err
}