Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- Added OnboardClaimPage and OnboardMigrationPage components for user onboarding. - Integrated OAuth login flow for Google and Microsoft accounts. - Implemented error handling and user feedback for claim and migration processes. - Created MigrationStepList and MigrationOnboardingAlerts components for progress tracking. - Added MailDomainsSection and MigrationProjectsPanel for admin settings. - Introduced e2e tests for onboarding migration scenarios.
399 lines
11 KiB
TypeScript
399 lines
11 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
|
|
created_at: string
|
|
microsoft_tenant_id?: string
|
|
microsoft_admin_consent_at?: string | null
|
|
microsoft_admin_consent_error?: string
|
|
cutover_dns?: DNSCheckReport
|
|
}
|
|
|
|
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 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 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 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)}`
|
|
),
|
|
})
|
|
}
|