espace-paie-odentas/app/activate/ActivateContent.tsx

354 lines
15 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" | "already-active">("loading");
const [message, setMessage] = useState("");
const [hasProcessed, setHasProcessed] = useState(false);
// Fonction pour vérifier si le compte est déjà actif en utilisant le token
const checkIfAccountAlreadyActive = async (accessToken: string | null) => {
if (!accessToken) {
setStatus("error");
setMessage("Lien d'activation invalide ou expiré.");
return;
}
try {
// Décoder le JWT pour extraire l'email (sans vérifier la signature)
const payload = JSON.parse(atob(accessToken.split('.')[1]));
const email = payload.email;
if (!email) {
setStatus("error");
setMessage("Lien d'activation invalide ou expiré.");
return;
}
console.log("🔍 [ACTIVATE] Vérification du statut du compte pour:", email);
// Vérifier si l'utilisateur existe et a déjà signé in via l'API
const response = await fetch(`/api/check-user-status?email=${encodeURIComponent(email)}`);
if (response.ok) {
const data = await response.json();
if (data.exists && data.hasSignedIn) {
console.log("✅ [ACTIVATE] Compte déjà activé détecté");
setStatus("already-active");
setMessage("Votre compte a déjà été activé. Vous pouvez vous connecter.");
return;
}
}
// Sinon, message d'erreur standard
setStatus("error");
setMessage("Lien d'activation invalide ou expiré. Veuillez demander un nouveau lien d'invitation.");
} catch (error) {
console.log("⚠️ [ACTIVATE] Erreur lors de la vérification du compte:", error);
setStatus("error");
setMessage("Lien d'activation invalide ou expiré.");
}
};
useEffect(() => {
// Empêcher le double traitement
if (hasProcessed) {
console.log("⏸️ [ACTIVATE] Activation déjà traitée, skip");
return;
}
const activateAccount = async () => {
console.log("🔄 [ACTIVATE] Début de l'activation");
setHasProcessed(true);
// Attendre que le hash fragment soit disponible
await new Promise(resolve => setTimeout(resolve, 500));
console.log("🔍 [ACTIVATE] Hash disponible:", window.location.hash);
// 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)
// Accepter si on a access_token et refresh_token, peu importe le type
if (access_token && refresh_token) {
console.log("🆕 [ACTIVATE] Nouveau format d'invitation (fragment URL)");
console.log("🔍 [ACTIVATE] Type détecté:", type);
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);
// Détecter si c'est un lien expiré et vérifier si le compte existe déjà
if (error.message.toLowerCase().includes("expired") ||
error.message.toLowerCase().includes("invalid")) {
await checkIfAccountAlreadyActive(access_token);
return;
}
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;
}
// Traitement de l'ancien format avec token_hash
// Vérifier qu'on a bien queryToken et queryType avant de continuer
if (!queryToken || !queryType) {
console.log("❌ [ACTIVATE] Aucun paramètre d'activation valide trouvé");
// Vérifier si c'est une erreur dans le hash (lien expiré, etc.)
const hashParams = new URLSearchParams(window.location.hash.substring(1));
const error = hashParams.get("error");
const errorDescription = hashParams.get("error_description");
if (error) {
console.log("❌ [ACTIVATE] Erreur dans le hash:", error, errorDescription);
setStatus("error");
setMessage(errorDescription
? decodeURIComponent(errorDescription.replace(/\+/g, ' '))
: "Lien d'activation invalide ou expiré");
} else {
setStatus("error");
setMessage("Lien d'activation invalide. Veuillez demander un nouveau lien d'invitation.");
}
return;
}
try {
const supabase = createClientComponentClient();
console.log("🔄 [ACTIVATE] Vérification de la session...");
// Vérifier le type d'invitation (ancien format)
if (queryType === "invite" && queryToken) {
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);
// Détecter si c'est un lien expiré et vérifier si le compte existe déjà
if (error.message.toLowerCase().includes("expired") ||
error.message.toLowerCase().includes("invalid")) {
// Pour l'ancien format, on n'a pas facilement accès à l'email
// On affiche juste le message d'erreur standard
setStatus("error");
setMessage("Lien d'activation invalide ou expiré. Veuillez demander un nouveau lien d'invitation.");
return;
}
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>
)}
{status === "already-active" && (
<div className="mt-4">
<div className="text-blue-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="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<p className="mt-2 text-sm text-gray-900 font-medium">{message}</p>
<button
onClick={() => router.push("/signin")}
className="mt-6 w-full inline-flex justify-center items-center px-4 py-2 border border-transparent text-sm font-medium rounded-lg text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Se connecter
</button>
</div>
)}
</div>
</div>
</div>
);
}