ultisuite-client/lib/api/hooks/use-hosted-mail.ts
R3D347HR4Y 4d31ac294b
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
feat(migration): enhance migration projects panel with shared drive management and roster import
- Added support for managing shared drives within the MigrationProjectsPanel.
- Implemented functionality to approve and reject shared drives.
- Introduced roster import feature for bulk email invitations with alternate email support.
- Updated state management to handle new roster and shared drive functionalities.
- Enhanced UI components for better user experience during migration processes.
2026-06-13 13:11:28 +02:00

547 lines
16 KiB
TypeScript

"use client"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { apiClient } from "@/lib/api/client"
export type MailDomain = {
id: string
name: string
status: string
verification_token?: string
dkim_selector?: string
dkim_public_key?: string
is_platform_domain: boolean
mx_verified_at?: string | null
txt_verified_at?: string | null
created_at: string
}
export type MigrationProject = {
id: string
domain_id?: string
name: string
source_provider: string
auth_mode: string
status: string
cutover_at?: string | null
delta_mode: boolean
shared_drive_mode?: "auto" | "manual"
created_at: string
microsoft_tenant_id?: string
microsoft_admin_consent_at?: string | null
microsoft_admin_consent_error?: string
cutover_dns?: DNSCheckReport
}
export type MigrationSharedDrive = {
id: string
project_id: string
drive_id: string
name: string
status: "pending" | "approved" | "rejected"
discovered_by_user_id?: string | null
created_at: string
updated_at: string
}
export type MicrosoftAdminConsent = {
tenant_id: string
client_id: string
project_id?: string
granted: boolean
error_code?: string
error_description?: string
consented_at: string
updated_at: string
}
export type MigrationJob = {
id: string
project_id: string
user_id: string
service: string
status: string
stats_json?: Record<string, number | boolean | string>
error?: string
user_email?: string
started_at?: string | null
completed_at?: string | null
}
export type MigrationJobAuditItem = {
source_id: string
rel_path?: string
status: "imported" | "failed" | "skipped"
reason?: string
imported_at: string
}
export type MigrationJobAuditSummary = {
service: string
imported: number
failed: number
skipped: number
total: number
by_status?: Record<string, number>
}
export type MigrationOnboardingHints = {
needs_user_oauth: boolean
oauth_provider?: string
waiting_for_admin: boolean
waiting_reason?: string
has_migration_credentials?: boolean
needs_microsoft_admin_consent?: boolean
}
export type MigrationUserStatus = {
project?: MigrationProject
invite?: MigrationInvite
jobs?: MigrationJob[]
onboarding?: MigrationOnboardingHints
}
export type MigrationInviteResponse = {
invite: MigrationInvite
project: MigrationProject
onboarding?: MigrationOnboardingHints
}
export type MigrationInvite = {
id: string
project_id: string
email: string
alternate_emails?: string[]
token?: string
status: string
}
export type MigrationRosterEntry = {
id: string
project_id: string
email: string
display_name?: string
alternate_emails?: string[]
status: "pending" | "invited" | "claimed"
invite_id?: string
created_at: string
updated_at: string
}
export type MigrationRosterImportResult = {
created: number
skipped_duplicates: number
errors?: Array<{ row: number; email?: string; message: string }>
}
export type DNSCheckReport = {
domain?: string
txt_verified: boolean
txt_records?: string[]
txt_expected?: string
mx_verified: boolean
mx_records?: string[]
expected_mx?: string[]
warnings?: string[]
errors?: string[]
}
export type MigrationCutoverResult = {
project: MigrationProject
dns: DNSCheckReport
}
export function useMailDomains() {
return useQuery({
queryKey: ["admin", "mail", "domains"],
queryFn: () => apiClient.get<{ domains: MailDomain[] }>("/admin/mail/domains"),
})
}
export function useCreateMailDomain() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (body: { name: string }) =>
apiClient.post<MailDomain>("/admin/mail/domains", body),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["admin", "mail", "domains"] })
},
})
}
export function useVerifyMailDomainTXT(domainId: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: () => apiClient.post<MailDomain>(`/admin/mail/domains/${domainId}/verify-txt`),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["admin", "mail", "domains"] })
},
})
}
export function useVerifyMailDomainMX(domainId: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: () => apiClient.post<MailDomain>(`/admin/mail/domains/${domainId}/verify-mx`),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["admin", "mail", "domains"] })
},
})
}
export function useMigrationProjects() {
return useQuery({
queryKey: ["admin", "migration", "projects"],
queryFn: () =>
apiClient.get<{ projects: MigrationProject[] }>("/admin/migration/projects"),
})
}
export function useCreateMigrationProject() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (body: {
name: string
source_provider: string
domain_id?: string
auth_mode?: string
}) => apiClient.post<MigrationProject>("/admin/migration/projects", body),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["admin", "migration", "projects"] })
},
})
}
export function useActivateMigrationProject() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (projectId: string) =>
apiClient.post<MigrationProject>(`/admin/migration/projects/${projectId}/activate`),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["admin", "migration", "projects"] })
},
})
}
export function useCutoverMigrationProject() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (projectId: string) =>
apiClient.post<MigrationCutoverResult>(`/admin/migration/projects/${projectId}/cutover`),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["admin", "migration", "projects"] })
},
})
}
export function usePreflightCutoverDNS(projectId: string) {
return useQuery({
queryKey: ["admin", "migration", "projects", projectId, "cutover-dns"],
enabled: Boolean(projectId),
queryFn: () =>
apiClient.get<{ dns: DNSCheckReport }>(
`/admin/migration/projects/${projectId}/cutover-dns`
),
})
}
export function useCreateMigrationInvite(projectId: string) {
return useMutation({
mutationFn: (body: { email: string; alternate_emails?: string[] }) =>
apiClient.post<MigrationInvite>(`/admin/migration/projects/${projectId}/invites`, body),
})
}
export function useImportMigrationInvites(projectId: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (emails: string[]) =>
apiClient.post<{ imported: number }>(
`/admin/migration/projects/${projectId}/invites/import`,
{ emails }
),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["admin", "migration", "projects"] })
},
meta: { requiresProjectId: projectId },
})
}
export function useMigrationRoster(projectId: string, enabled = true) {
return useQuery({
queryKey: ["admin", "migration", "projects", projectId, "roster"],
enabled: Boolean(projectId) && enabled,
queryFn: () =>
apiClient.get<{ roster: MigrationRosterEntry[] }>(
`/admin/migration/projects/${projectId}/roster`
),
})
}
export function useImportMigrationRoster(projectId: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (csv: string) =>
apiClient.post<MigrationRosterImportResult>(
`/admin/migration/projects/${projectId}/roster`,
{ csv }
),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: ["admin", "migration", "projects", projectId, "roster"],
})
},
meta: { requiresProjectId: projectId },
})
}
export function useMigrationProjectJobs(projectId: string, enabled = true) {
return useQuery({
queryKey: ["admin", "migration", "projects", projectId, "jobs"],
enabled: Boolean(projectId) && enabled,
queryFn: () =>
apiClient.get<{ jobs: MigrationJob[] }>(
`/admin/migration/projects/${projectId}/jobs`
),
refetchInterval: 10_000,
})
}
export function useMigrationJobAuditSummary(projectId: string, jobId: string | null) {
return useQuery({
queryKey: ["admin", "migration", "projects", projectId, "jobs", jobId, "audit", "summary"],
enabled: Boolean(projectId && jobId),
queryFn: () =>
apiClient.get<MigrationJobAuditSummary>(
`/admin/migration/projects/${projectId}/jobs/${jobId}/audit/summary`
),
refetchInterval: 10_000,
})
}
export function useMigrationJobAudit(
projectId: string,
jobId: string | null,
status: "failed" | "skipped" | "imported" | "all" = "failed"
) {
const statusParam = status === "all" ? "" : status
return useQuery({
queryKey: ["admin", "migration", "projects", projectId, "jobs", jobId, "audit", status],
enabled: Boolean(projectId && jobId),
queryFn: () => {
const params = new URLSearchParams({ page_size: "50" })
if (statusParam) params.set("status", statusParam)
return apiClient.get<{ items: MigrationJobAuditItem[]; pagination: { total?: number } }>(
`/admin/migration/projects/${projectId}/jobs/${jobId}/audit?${params.toString()}`
)
},
refetchInterval: 10_000,
})
}
export async function downloadMigrationJobAudit(
projectId: string,
jobId: string,
format: "csv" | "ndjson"
) {
const blob = await apiClient.getBlob(
`/admin/migration/projects/${projectId}/jobs/${jobId}/audit/export?format=${format}`
)
const url = URL.createObjectURL(blob)
const anchor = document.createElement("a")
anchor.href = url
anchor.download = `migration-job-audit-${jobId.slice(0, 8)}.${format === "csv" ? "csv" : "ndjson"}`
anchor.click()
URL.revokeObjectURL(url)
}
export async function downloadMigrationProjectAudit(
projectId: string,
format: "csv" | "ndjson"
) {
const blob = await apiClient.getBlob(
`/admin/migration/projects/${projectId}/audit/export?format=${format}`
)
const url = URL.createObjectURL(blob)
const anchor = document.createElement("a")
anchor.href = url
anchor.download = `migration-project-audit-${projectId.slice(0, 8)}.${format === "csv" ? "csv" : "ndjson"}`
anchor.click()
URL.revokeObjectURL(url)
}
export function useUpdateMigrationSharedDriveMode(projectId: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (shared_drive_mode: "auto" | "manual") =>
apiClient.patch<MigrationProject>(
`/admin/migration/projects/${projectId}/shared-drive-mode`,
{ shared_drive_mode }
),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["admin", "migration", "projects"] })
void queryClient.invalidateQueries({
queryKey: ["admin", "migration", "projects", projectId, "shared-drives"],
})
},
})
}
export function useMigrationSharedDrives(projectId: string, enabled = true) {
return useQuery({
queryKey: ["admin", "migration", "projects", projectId, "shared-drives"],
enabled: Boolean(projectId) && enabled,
queryFn: () =>
apiClient.get<{ shared_drives: MigrationSharedDrive[] }>(
`/admin/migration/projects/${projectId}/shared-drives`
),
refetchInterval: 15_000,
})
}
export function useApproveMigrationSharedDrive(projectId: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (driveId: string) =>
apiClient.post<MigrationSharedDrive>(
`/admin/migration/projects/${projectId}/shared-drives/${encodeURIComponent(driveId)}/approve`
),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: ["admin", "migration", "projects", projectId, "shared-drives"],
})
},
})
}
export function useRejectMigrationSharedDrive(projectId: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (driveId: string) =>
apiClient.post<MigrationSharedDrive>(
`/admin/migration/projects/${projectId}/shared-drives/${encodeURIComponent(driveId)}/reject`
),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: ["admin", "migration", "projects", projectId, "shared-drives"],
})
},
})
}
export function useRetryMigrationJob(projectId: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (jobId: string) =>
apiClient.post<MigrationJob>(
`/admin/migration/projects/${projectId}/jobs/${jobId}/retry`
),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: ["admin", "migration", "projects", projectId, "jobs"],
})
},
})
}
export function useResetMigrationJobCursor(projectId: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (jobId: string) =>
apiClient.post<MigrationJob>(
`/admin/migration/projects/${projectId}/jobs/${jobId}/reset-cursor`
),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: ["admin", "migration", "projects", projectId, "jobs"],
})
},
})
}
export function useRetryMigrationFailedJobs(projectId: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: () =>
apiClient.post<{ retried: number }>(
`/admin/migration/projects/${projectId}/jobs/retry-failed`
),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: ["admin", "migration", "projects", projectId, "jobs"],
})
},
})
}
export function useMicrosoftMigrationAdminConsentURL() {
return useMutation({
mutationFn: (body: { tenant?: string; projectId?: string }) => {
const params = new URLSearchParams()
if (body.tenant?.trim()) params.set("tenant", body.tenant.trim())
if (body.projectId) params.set("project_id", body.projectId)
const query = params.toString()
return apiClient.get<{ url: string }>(
`/admin/migration/microsoft/admin-consent-url${query ? `?${query}` : ""}`
)
},
})
}
export function useMicrosoftAdminConsents() {
return useQuery({
queryKey: ["admin", "migration", "microsoft", "admin-consents"],
queryFn: () =>
apiClient.get<{ consents: MicrosoftAdminConsent[] }>(
"/admin/migration/microsoft/admin-consents"
),
})
}
export function useMigrationInvite(token: string | null) {
return useQuery({
queryKey: ["migration", "invite", token],
enabled: Boolean(token),
queryFn: () =>
apiClient.get<MigrationInviteResponse>(
`/migration/invite?token=${encodeURIComponent(token ?? "")}`
),
})
}
export function useMigrationStatus() {
return useQuery({
queryKey: ["migration", "status"],
queryFn: () => apiClient.get<MigrationUserStatus>("/migration/status"),
})
}
export function useClaimMigration() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (body: { token: string; password?: string }) =>
apiClient.post("/migration/claim", body),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["migration"] })
},
})
}
export function useStartMigrationOAuth() {
return useMutation({
mutationFn: (body: { provider: string; invite_token?: string; project_id?: string }) =>
apiClient.post<{ auth_url: string }>("/migration/oauth/start", body),
})
}
export function useCheckMailAddress(local: string, domain: string) {
return useQuery({
queryKey: ["mail", "address-check", local, domain],
enabled: local.length > 0 && domain.length > 0,
queryFn: () =>
apiClient.get<{ available: boolean; reason?: string }>(
`/mail/addresses/check?local=${encodeURIComponent(local)}&domain=${encodeURIComponent(domain)}`
),
})
}