354 lines
15 KiB
TypeScript
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>
|
|
);
|
|
}
|