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.
175 lines
5.9 KiB
TypeScript
175 lines
5.9 KiB
TypeScript
"use client"
|
|
|
|
import { Suspense, useEffect, useMemo, useState } from "react"
|
|
import Link from "next/link"
|
|
import { useRouter, useSearchParams } from "next/navigation"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Label } from "@/components/ui/label"
|
|
import { MigrationStepList, appOnlyAuthStepLabel } from "@/components/migration/onboarding-ui"
|
|
import { ApiRequestError } from "@/lib/api/client"
|
|
import { useClaimMigration, useMigrationInvite } from "@/lib/api/hooks/use-hosted-mail"
|
|
import { useAuthReady } from "@/lib/api/use-auth-ready"
|
|
import { buildOidcLoginUrl } from "@/lib/auth/login-url"
|
|
|
|
export default function OnboardClaimPage() {
|
|
return (
|
|
<Suspense
|
|
fallback={
|
|
<main className="mx-auto max-w-lg px-4 py-16">
|
|
<p className="text-muted-foreground">Chargement…</p>
|
|
</main>
|
|
}
|
|
>
|
|
<OnboardClaimContent />
|
|
</Suspense>
|
|
)
|
|
}
|
|
|
|
function claimErrorMessage(error: unknown): string {
|
|
if (error instanceof ApiRequestError) {
|
|
switch (error.code) {
|
|
case "email_mismatch":
|
|
return "L'adresse de connexion ne correspond pas à l'invitation. Utilisez le compte invité ou contactez votre administrateur pour ajouter une adresse SSO alternative."
|
|
case "invite_already_claimed":
|
|
return "Cette invitation a déjà été revendiquée."
|
|
case "invite_not_found":
|
|
return "Invitation introuvable ou expirée."
|
|
case "migration_domain_not_active":
|
|
return "Le domaine mail du projet n'est pas encore actif — contactez votre administrateur."
|
|
case "migration_domain_mismatch":
|
|
return "L'adresse invitée ne correspond pas au domaine mail du projet."
|
|
}
|
|
return error.message
|
|
}
|
|
if (error instanceof Error) {
|
|
return error.message
|
|
}
|
|
return "Échec de la revendication — vérifiez que vous êtes connecté avec la bonne adresse."
|
|
}
|
|
|
|
function OnboardClaimContent() {
|
|
const params = useSearchParams()
|
|
const router = useRouter()
|
|
const token = params.get("token")
|
|
const { authenticated } = useAuthReady()
|
|
const inviteQuery = useMigrationInvite(token)
|
|
const claimMutation = useClaimMigration()
|
|
const [password, setPassword] = useState("")
|
|
|
|
const loginHref = useMemo(() => {
|
|
const returnTo = token ? `/onboard/claim?token=${encodeURIComponent(token)}` : "/onboard/claim"
|
|
return buildOidcLoginUrl({ returnTo })
|
|
}, [token])
|
|
|
|
const invite = inviteQuery.data?.invite
|
|
const project = inviteQuery.data?.project
|
|
const alreadyClaimed = invite?.status === "claimed"
|
|
|
|
useEffect(() => {
|
|
if (!token || !alreadyClaimed) return
|
|
router.replace(`/onboard/migration?token=${encodeURIComponent(token)}`)
|
|
}, [token, alreadyClaimed, router])
|
|
|
|
if (!token) {
|
|
return (
|
|
<main className="mx-auto max-w-lg px-4 py-16">
|
|
<h1 className="text-2xl font-semibold">Lien d'invitation invalide</h1>
|
|
<p className="mt-2 text-muted-foreground">Ce lien ne contient pas de jeton de migration.</p>
|
|
</main>
|
|
)
|
|
}
|
|
|
|
if (!authenticated) {
|
|
return (
|
|
<main className="mx-auto max-w-lg px-4 py-16">
|
|
<h1 className="text-2xl font-semibold">Revendiquer votre compte</h1>
|
|
<p className="mt-2 text-muted-foreground">
|
|
Connectez-vous avec le même compte que l'adresse invitée (
|
|
{inviteQuery.data?.invite.email ?? "…"}) pour provisionner votre boîte Ultimail.
|
|
</p>
|
|
<div className="mt-6">
|
|
<MigrationStepList
|
|
steps={[
|
|
{ label: "Connexion (Google / Microsoft / Ultimail)", done: false, current: true },
|
|
{ label: "Revendiquer la boîte mail", done: false, current: false },
|
|
{ label: "Autoriser l'import des données", done: false, current: false },
|
|
]}
|
|
/>
|
|
</div>
|
|
<Button asChild className="mt-6">
|
|
<Link href={loginHref}>Se connecter pour continuer</Link>
|
|
</Button>
|
|
</main>
|
|
)
|
|
}
|
|
|
|
if (alreadyClaimed) {
|
|
return (
|
|
<main className="mx-auto max-w-lg px-4 py-16">
|
|
<p className="text-muted-foreground">Redirection vers le suivi de migration…</p>
|
|
</main>
|
|
)
|
|
}
|
|
|
|
const steps = [
|
|
{ label: "Connexion", done: true, current: false },
|
|
{ label: "Revendiquer la boîte mail", done: false, current: true },
|
|
{
|
|
label: appOnlyAuthStepLabel(project),
|
|
done: false,
|
|
current: false,
|
|
},
|
|
]
|
|
|
|
return (
|
|
<main className="mx-auto max-w-lg px-4 py-16">
|
|
<h1 className="text-2xl font-semibold">Revendiquer votre compte</h1>
|
|
<p className="mt-2 text-muted-foreground">
|
|
Projet {project?.name ?? ""} — {invite?.email}
|
|
</p>
|
|
|
|
<div className="mt-6">
|
|
<MigrationStepList steps={steps} />
|
|
</div>
|
|
|
|
<p className="mt-4 text-sm text-muted-foreground">
|
|
Connectez-vous avec le compte correspondant à l'invitation ({invite?.email}). Les
|
|
alias SSO reconnus (UPN, adresse alternative) sont acceptés si configurés par
|
|
l'administrateur.
|
|
</p>
|
|
|
|
<div className="mt-8 space-y-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="mailbox-password">Mot de passe boîte mail (optionnel)</Label>
|
|
<Input
|
|
id="mailbox-password"
|
|
type="password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
placeholder="Pour IMAP/SMTP hébergé"
|
|
/>
|
|
</div>
|
|
|
|
{claimMutation.isError && (
|
|
<p className="text-sm text-destructive">{claimErrorMessage(claimMutation.error)}</p>
|
|
)}
|
|
|
|
<Button
|
|
className="w-full"
|
|
disabled={claimMutation.isPending}
|
|
onClick={() => {
|
|
void claimMutation
|
|
.mutateAsync({ token, password: password || undefined })
|
|
.then(() => {
|
|
window.location.href = `/onboard/migration?token=${encodeURIComponent(token)}`
|
|
})
|
|
}}
|
|
>
|
|
Revendiquer mon compte
|
|
</Button>
|
|
</div>
|
|
</main>
|
|
)
|
|
}
|