- Créé sendInvitationWithActivationEmail() pour unifier les invitations - Modifié /api/staff/users/invite pour utiliser generateLink + email - Modifié /api/access/nouveau pour envoyer email d'activation - Modifié /api/access POST pour remplacer pending_invites par système direct - Template account-activation mis à jour : * Titre 'Activez votre compte' * Encart avec infos : invitant (statut), organisation, niveau d'accès * Message de contact formaté comme autres emails * Renommage 'Odentas Paie' → 'Espace Paie Odentas' - Fix page /activate : délai 100ms pour hash fragment + redirection 1s - Liens d'activation forcés vers paie.odentas.fr (tests depuis localhost) - Messages UI cohérents : 'Invitation envoyée' au lieu de 'Compte créé'
248 lines
10 KiB
TypeScript
248 lines
10 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useSearchParams, useRouter } from "next/navigation";
|
|
import { createClientComponentClient } from "@supabase/auth-helpers-nextjs";
|
|
|
|
export default function ActivateContent() {
|
|
const router = useRouter();
|
|
const searchParams = useSearchParams();
|
|
const [status, setStatus] = useState<"loading" | "success" | "error">("loading");
|
|
const [message, setMessage] = useState("");
|
|
|
|
useEffect(() => {
|
|
const activateAccount = async () => {
|
|
console.log("🔄 [ACTIVATE] Début de l'activation");
|
|
|
|
// Attendre un instant pour que le hash fragment soit disponible
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
|
|
// Récupérer les paramètres depuis le fragment URL (#)
|
|
const hashParams = new URLSearchParams(window.location.hash.substring(1));
|
|
const access_token = hashParams.get("access_token");
|
|
const refresh_token = hashParams.get("refresh_token");
|
|
const type = hashParams.get("type");
|
|
|
|
// Fallback sur les paramètres de requête normaux
|
|
const queryToken = searchParams.get("token_hash");
|
|
const queryType = searchParams.get("type");
|
|
const next = searchParams.get("next") || "/";
|
|
|
|
console.log("📋 [ACTIVATE] Paramètres reçus:", {
|
|
access_token: access_token ? `${access_token.substring(0, 10)}...` : null,
|
|
refresh_token: refresh_token ? `${refresh_token.substring(0, 10)}...` : null,
|
|
type: type || queryType,
|
|
queryToken: queryToken ? `${queryToken.substring(0, 10)}...` : null,
|
|
next,
|
|
fullUrl: window.location.href
|
|
});
|
|
|
|
// Vérifier si on a les tokens dans le fragment URL (nouveau format)
|
|
if (access_token && refresh_token && type === "invite") {
|
|
console.log("🆕 [ACTIVATE] Nouveau format d'invitation (fragment URL)");
|
|
|
|
try {
|
|
const supabase = createClientComponentClient();
|
|
|
|
// Établir la session avec les tokens du fragment
|
|
const { data, error } = await supabase.auth.setSession({
|
|
access_token,
|
|
refresh_token
|
|
});
|
|
|
|
if (error) {
|
|
console.log("❌ [ACTIVATE] Erreur lors de l'établissement de la session:", error);
|
|
setStatus("error");
|
|
setMessage(`Erreur lors de l'activation: ${error.message}`);
|
|
return;
|
|
}
|
|
|
|
console.log("✅ [ACTIVATE] Session établie avec succès:", {
|
|
userId: data?.user?.id,
|
|
email: data?.user?.email,
|
|
sessionExists: !!data?.session
|
|
});
|
|
|
|
if (data?.session && data?.user) {
|
|
setStatus("success");
|
|
setMessage("Compte activé avec succès ! Redirection en cours...");
|
|
|
|
// Nettoyer l'URL (supprimer le fragment)
|
|
window.history.replaceState({}, document.title, window.location.pathname);
|
|
|
|
// Récupérer et définir l'organisation active de l'utilisateur
|
|
try {
|
|
const { data: memberData } = await supabase
|
|
.from("organization_members")
|
|
.select("org_id, organizations(id, name)")
|
|
.eq("user_id", data.user.id)
|
|
.eq("revoked", false)
|
|
.maybeSingle();
|
|
|
|
if (memberData?.org_id) {
|
|
console.log("🏛️ [ACTIVATE] Définition de l'organisation active:", memberData.org_id);
|
|
|
|
// Définir le cookie active_org_id pour que l'utilisateur soit connecté à sa structure
|
|
document.cookie = `active_org_id=${memberData.org_id}; path=/; secure; samesite=lax`;
|
|
|
|
// Optionnel : stocker le nom de l'organisation en localStorage pour l'UI
|
|
const orgData = memberData.organizations as any;
|
|
if (orgData?.name) {
|
|
localStorage.setItem("company_name", orgData.name);
|
|
}
|
|
}
|
|
} catch (orgError) {
|
|
console.log("⚠️ [ACTIVATE] Impossible de récupérer l'organisation:", orgError);
|
|
// Ne pas faire échouer l'activation pour ça
|
|
}
|
|
|
|
// Redirection immédiate - les cookies sont déjà définis
|
|
setTimeout(() => {
|
|
router.push(next);
|
|
}, 1000);
|
|
} else {
|
|
setStatus("error");
|
|
setMessage("Erreur: Session non créée après l'activation.");
|
|
}
|
|
} catch (error: any) {
|
|
console.log("💥 [ACTIVATE] Erreur lors de l'établissement de la session:", error);
|
|
setStatus("error");
|
|
setMessage(`Erreur inattendue: ${error.message || "Erreur inconnue"}`);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Fallback sur l'ancien format avec token_hash
|
|
if (!queryToken || !queryType) {
|
|
console.log("❌ [ACTIVATE] Paramètres manquants");
|
|
setStatus("error");
|
|
setMessage("Lien d'activation invalide. Paramètres manquants.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const supabase = createClientComponentClient();
|
|
|
|
console.log("🔄 [ACTIVATE] Vérification de la session...");
|
|
|
|
// Vérifier le type d'invitation (ancien format)
|
|
if (queryType === "invite") {
|
|
console.log("👥 [ACTIVATE] Traitement d'une invitation (ancien format)");
|
|
|
|
// Accepter l'invitation avec le token
|
|
const { data, error } = await supabase.auth.verifyOtp({
|
|
token_hash: queryToken,
|
|
type: "invite"
|
|
});
|
|
|
|
if (error) {
|
|
console.log("❌ [ACTIVATE] Erreur lors de l'acceptation de l'invitation:", error);
|
|
setStatus("error");
|
|
setMessage(`Erreur lors de l'activation: ${error.message}`);
|
|
return;
|
|
}
|
|
|
|
console.log("✅ [ACTIVATE] Invitation acceptée:", {
|
|
userId: data?.user?.id,
|
|
email: data?.user?.email,
|
|
sessionExists: !!data?.session
|
|
});
|
|
|
|
if (data?.session && data?.user) {
|
|
setStatus("success");
|
|
setMessage("Compte activé avec succès ! Redirection en cours...");
|
|
|
|
// Récupérer et définir l'organisation active de l'utilisateur
|
|
try {
|
|
const { data: memberData } = await supabase
|
|
.from("organization_members")
|
|
.select("org_id, organizations(id, name)")
|
|
.eq("user_id", data.user.id)
|
|
.eq("revoked", false)
|
|
.maybeSingle();
|
|
|
|
if (memberData?.org_id) {
|
|
console.log("🏛️ [ACTIVATE] Définition de l'organisation active:", memberData.org_id);
|
|
|
|
// Définir le cookie active_org_id pour que l'utilisateur soit connecté à sa structure
|
|
document.cookie = `active_org_id=${memberData.org_id}; path=/; secure; samesite=lax`;
|
|
|
|
// Optionnel : stocker le nom de l'organisation en localStorage pour l'UI
|
|
const orgData = memberData.organizations as any;
|
|
if (orgData?.name) {
|
|
localStorage.setItem("company_name", orgData.name);
|
|
}
|
|
}
|
|
} catch (orgError) {
|
|
console.log("⚠️ [ACTIVATE] Impossible de récupérer l'organisation:", orgError);
|
|
// Ne pas faire échouer l'activation pour ça
|
|
}
|
|
|
|
// Redirection immédiate - les cookies sont déjà définis
|
|
setTimeout(() => {
|
|
router.push(next);
|
|
}, 1000);
|
|
} else {
|
|
setStatus("error");
|
|
setMessage("Erreur: Session non créée après l'activation.");
|
|
}
|
|
} else {
|
|
console.log("❓ [ACTIVATE] Type d'activation non géré:", queryType);
|
|
setStatus("error");
|
|
setMessage(`Type d'activation non supporté: ${queryType}`);
|
|
}
|
|
} catch (error: any) {
|
|
console.log("💥 [ACTIVATE] Erreur inattendue:", error);
|
|
setStatus("error");
|
|
setMessage(`Erreur inattendue: ${error.message || "Erreur inconnue"}`);
|
|
}
|
|
};
|
|
|
|
activateAccount();
|
|
}, [searchParams, router]);
|
|
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
|
<div className="max-w-md w-full space-y-8">
|
|
<div className="text-center">
|
|
<h2 className="mt-6 text-3xl font-extrabold text-gray-900">
|
|
Activation de votre compte
|
|
</h2>
|
|
|
|
{status === "loading" && (
|
|
<div className="mt-4">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
|
|
<p className="mt-2 text-sm text-gray-600">Activation en cours...</p>
|
|
</div>
|
|
)}
|
|
|
|
{status === "success" && (
|
|
<div className="mt-4">
|
|
<div className="text-green-600">
|
|
<svg className="mx-auto h-12 w-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
</div>
|
|
<p className="mt-2 text-sm text-green-600">{message}</p>
|
|
</div>
|
|
)}
|
|
|
|
{status === "error" && (
|
|
<div className="mt-4">
|
|
<div className="text-red-600">
|
|
<svg className="mx-auto h-12 w-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16c-.77.833.192 2.5 1.732 2.5z" />
|
|
</svg>
|
|
</div>
|
|
<p className="mt-2 text-sm text-red-600">{message}</p>
|
|
<p className="mt-4 text-xs text-gray-500">
|
|
Si le problème persiste, contactez le support ou demandez un nouveau lien d'invitation.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|