- Added configuration options for Stalwart hosted mail in .env.example. - Updated Docker Compose to include Stalwart service with health checks. - Introduced new API endpoints for managing mail domains and migration projects. - Enhanced Authentik blueprints for user enrollment and post-migration security. - Updated OAuth handling for Google and Microsoft migration processes. - Improved error handling and response structures in the mail API. - Added integration tests for email claiming and migration workflows.
159 lines
4.1 KiB
Go
159 lines
4.1 KiB
Go
package migration
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
"github.com/ultisuite/ulti-backend/internal/api/query"
|
|
)
|
|
|
|
const (
|
|
ItemStatusImported = "imported"
|
|
ItemStatusFailed = "failed"
|
|
ItemStatusSkipped = "skipped"
|
|
)
|
|
|
|
// JobAuditItem is one row in a migration job item audit report.
|
|
type JobAuditItem struct {
|
|
SourceID string `json:"source_id"`
|
|
RelPath string `json:"rel_path,omitempty"`
|
|
Status string `json:"status"`
|
|
Reason string `json:"reason,omitempty"`
|
|
ImportedAt string `json:"imported_at"`
|
|
}
|
|
|
|
// JobAuditSummary counts items by status for a migration job.
|
|
type JobAuditSummary struct {
|
|
Service string `json:"service"`
|
|
Imported int64 `json:"imported"`
|
|
Failed int64 `json:"failed"`
|
|
Skipped int64 `json:"skipped"`
|
|
Total int64 `json:"total"`
|
|
ByStatus map[string]int64 `json:"by_status,omitempty"`
|
|
}
|
|
|
|
func (s *Service) verifyJobInProject(ctx context.Context, projectID, jobID string) (service string, err error) {
|
|
err = s.db.QueryRow(ctx, `
|
|
SELECT service FROM migration_jobs
|
|
WHERE id = $1::uuid AND project_id = $2::uuid
|
|
`, jobID, projectID).Scan(&service)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return "", fmt.Errorf("job not found")
|
|
}
|
|
return service, err
|
|
}
|
|
|
|
func (s *Service) JobAuditSummary(ctx context.Context, projectID, jobID string) (JobAuditSummary, error) {
|
|
service, err := s.verifyJobInProject(ctx, projectID, jobID)
|
|
if err != nil {
|
|
return JobAuditSummary{}, err
|
|
}
|
|
|
|
rows, err := s.db.Query(ctx, `
|
|
SELECT status, COUNT(*)
|
|
FROM migration_imported_items
|
|
WHERE job_id = $1::uuid
|
|
GROUP BY status
|
|
`, jobID)
|
|
if err != nil {
|
|
return JobAuditSummary{}, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
summary := JobAuditSummary{Service: service, ByStatus: map[string]int64{}}
|
|
for rows.Next() {
|
|
var status string
|
|
var count int64
|
|
if err := rows.Scan(&status, &count); err != nil {
|
|
return JobAuditSummary{}, err
|
|
}
|
|
summary.ByStatus[status] = count
|
|
summary.Total += count
|
|
switch status {
|
|
case ItemStatusImported:
|
|
summary.Imported = count
|
|
case ItemStatusFailed:
|
|
summary.Failed = count
|
|
case ItemStatusSkipped:
|
|
summary.Skipped = count
|
|
}
|
|
}
|
|
return summary, rows.Err()
|
|
}
|
|
|
|
func (s *Service) ListJobAudit(
|
|
ctx context.Context,
|
|
projectID, jobID, statusFilter string,
|
|
params query.ListParams,
|
|
) ([]JobAuditItem, query.PaginationMeta, error) {
|
|
if _, err := s.verifyJobInProject(ctx, projectID, jobID); err != nil {
|
|
return nil, query.PaginationMeta{}, err
|
|
}
|
|
|
|
statusFilter = normalizeAuditStatusFilter(statusFilter)
|
|
|
|
var total int64
|
|
countSQL := `SELECT COUNT(*) FROM migration_imported_items WHERE job_id = $1::uuid`
|
|
countArgs := []any{jobID}
|
|
if statusFilter != "" {
|
|
countSQL += ` AND status = $2`
|
|
countArgs = append(countArgs, statusFilter)
|
|
}
|
|
if err := s.db.QueryRow(ctx, countSQL, countArgs...).Scan(&total); err != nil {
|
|
return nil, query.PaginationMeta{}, err
|
|
}
|
|
|
|
listSQL := `
|
|
SELECT source_id, rel_path, status, reason, imported_at::text
|
|
FROM migration_imported_items
|
|
WHERE job_id = $1::uuid
|
|
`
|
|
listArgs := []any{jobID}
|
|
if statusFilter != "" {
|
|
listSQL += ` AND status = $2`
|
|
listArgs = append(listArgs, statusFilter)
|
|
}
|
|
listSQL += ` ORDER BY imported_at DESC, source_id ASC LIMIT $` + fmt.Sprint(len(listArgs)+1) +
|
|
` OFFSET $` + fmt.Sprint(len(listArgs)+2)
|
|
listArgs = append(listArgs, params.Limit(), params.Offset())
|
|
|
|
rows, err := s.db.Query(ctx, listSQL, listArgs...)
|
|
if err != nil {
|
|
return nil, query.PaginationMeta{}, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
out := make([]JobAuditItem, 0, params.Limit())
|
|
for rows.Next() {
|
|
var item JobAuditItem
|
|
if err := rows.Scan(&item.SourceID, &item.RelPath, &item.Status, &item.Reason, &item.ImportedAt); err != nil {
|
|
return nil, query.PaginationMeta{}, err
|
|
}
|
|
out = append(out, item)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, query.PaginationMeta{}, err
|
|
}
|
|
return out, params.Meta(&total), nil
|
|
}
|
|
|
|
func normalizeAuditStatusFilter(raw string) string {
|
|
switch raw {
|
|
case ItemStatusImported, ItemStatusFailed, ItemStatusSkipped:
|
|
return raw
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func incJobStat(stats map[string]any, key string) {
|
|
if stats == nil {
|
|
return
|
|
}
|
|
v, _ := stats[key].(float64)
|
|
stats[key] = v + 1
|
|
}
|