Added the 4NK authentication
This commit is contained in:
parent
778abe903d
commit
cc7414a441
@ -25,8 +25,9 @@ import {
|
||||
Home,
|
||||
} from "lucide-react"
|
||||
import { AuthModal } from "@/components/4nk/AuthModal"
|
||||
import { MessageBus } from "@/lib/4nk/MessageBus"
|
||||
import { UserStore } from "@/lib/4nk/UserStore"
|
||||
import { Iframe } from "@/components/4nk/Iframe"
|
||||
import MessageBus from "@/lib/4nk/MessageBus"
|
||||
import UserStore from "@/lib/4nk/UserStore"
|
||||
// DebugInfo supprimé
|
||||
|
||||
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
|
||||
@ -40,7 +41,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
||||
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const iframeUrl = process.env.NEXT_PUBLIC_4NK_IFRAME_URL || "https://dev.4nk.io"
|
||||
const iframeUrl = process.env.NEXT_PUBLIC_4NK_IFRAME_URL || "https://dev3.4nkweb.com"
|
||||
|
||||
const navigation = [
|
||||
{ name: "Tableau de bord", href: "/dashboard", icon: LayoutDashboard },
|
||||
@ -60,11 +61,10 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
||||
const messageBus = MessageBus.getInstance(iframeUrl)
|
||||
|
||||
if (accessToken) {
|
||||
// Vérifier si on est en mode mock
|
||||
const mockMode = messageBus.isInMockMode()
|
||||
setIsMockMode(mockMode)
|
||||
// Mode normal (pas de mock)
|
||||
setIsMockMode(false)
|
||||
|
||||
if (mockMode) {
|
||||
if (false) {
|
||||
console.log("🎭 Dashboard en mode mock")
|
||||
setIsAuthenticated(true)
|
||||
setUserInfo({
|
||||
@ -117,7 +117,6 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
||||
const messageBus = MessageBus.getInstance(iframeUrl)
|
||||
|
||||
userStore.disconnect()
|
||||
messageBus.disableMockMode()
|
||||
|
||||
// Afficher un message de confirmation avec options
|
||||
setShowLogoutConfirm(true)
|
||||
@ -321,6 +320,14 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Iframe 4NK - affichée seulement si connecté */}
|
||||
{isAuthenticated && (
|
||||
<Iframe
|
||||
iframeUrl={process.env.NEXT_PUBLIC_4NK_IFRAME_URL || "https://dev3.4nkweb.com"}
|
||||
showIframe={true}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Debug info retiré */}
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -26,7 +26,7 @@ import {
|
||||
HardDrive,
|
||||
X,
|
||||
} from "lucide-react"
|
||||
import { MessageBus } from "@/lib/4nk/MessageBus"
|
||||
import MessageBus from "@/lib/4nk/MessageBus"
|
||||
import Link from "next/link"
|
||||
|
||||
export default function DashboardPage() {
|
||||
@ -54,13 +54,12 @@ export default function DashboardPage() {
|
||||
const [notifications, setNotifications] = useState<any[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
const iframeUrl = process.env.NEXT_PUBLIC_4NK_IFRAME_URL || "https://dev.4nk.io"
|
||||
const iframeUrl = process.env.NEXT_PUBLIC_4NK_IFRAME_URL || "https://dev3.4nkweb.com"
|
||||
const messageBus = MessageBus.getInstance(iframeUrl)
|
||||
const mockMode = messageBus.isInMockMode()
|
||||
setIsMockMode(mockMode)
|
||||
setIsMockMode(false)
|
||||
|
||||
// Simuler le chargement des données
|
||||
if (mockMode) {
|
||||
if (false) {
|
||||
setStats({
|
||||
totalDocuments: 1247,
|
||||
totalFolders: 89,
|
||||
|
||||
@ -23,9 +23,9 @@ import {
|
||||
EyeOff,
|
||||
} from "lucide-react"
|
||||
import { AuthModal } from "@/components/4nk/AuthModal"
|
||||
import { MessageBus } from "@/lib/4nk/MessageBus"
|
||||
import MessageBus from "@/lib/4nk/MessageBus"
|
||||
import { MockService } from "@/lib/4nk/MockService"
|
||||
import { UserStore } from "@/lib/4nk/UserStore"
|
||||
import UserStore from "@/lib/4nk/UserStore"
|
||||
|
||||
export default function LoginPage() {
|
||||
const [companyId, setCompanyId] = useState("")
|
||||
@ -34,11 +34,17 @@ export default function LoginPage() {
|
||||
const [showPairingSection, setShowPairingSection] = useState(false)
|
||||
const [pairingWords, setPairingWords] = useState(["", "", "", ""])
|
||||
const [pairingError, setPairingError] = useState("")
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [pairingSuccess, setPairingSuccess] = useState(false)
|
||||
const router = useRouter()
|
||||
const [showPairingInput, setShowPairingInput] = useState(false)
|
||||
|
||||
const iframeUrl = process.env.NEXT_PUBLIC_4NK_IFRAME_URL || "https://dev.4nk.io"
|
||||
const iframeUrl = process.env.NEXT_PUBLIC_4NK_IFRAME_URL || "https://dev3.4nkweb.com"
|
||||
|
||||
const handleLogin = () => {
|
||||
setIsAuthModalOpen(true)
|
||||
setError(null)
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
@ -58,8 +64,7 @@ export default function LoginPage() {
|
||||
const mockService = MockService.getInstance()
|
||||
const userStore = UserStore.getInstance()
|
||||
|
||||
// Activer le mode mock
|
||||
messageBus.enableMockMode()
|
||||
// Mode normal (pas de mock)
|
||||
|
||||
// Authentification mock
|
||||
const authResult = await mockService.mockAuthentication(companyId)
|
||||
@ -159,160 +164,57 @@ export default function LoginPage() {
|
||||
<p className="text-gray-600">Gestion électronique de documents sécurisée</p>
|
||||
</div>
|
||||
|
||||
{/* Navigation entre connexion et pairing */}
|
||||
<div className="flex space-x-1 bg-gray-100 p-1 rounded-lg">
|
||||
<button
|
||||
onClick={() => setShowPairingSection(false)}
|
||||
className={`flex-1 py-2 px-4 text-sm font-medium rounded-md transition-colors ${
|
||||
!showPairingSection ? "bg-white text-gray-900 shadow-sm" : "text-gray-600 hover:text-gray-900"
|
||||
}`}
|
||||
>
|
||||
<Building2 className="h-4 w-4 inline mr-2" />
|
||||
Connexion
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowPairingSection(true)}
|
||||
className={`flex-1 py-2 px-4 text-sm font-medium rounded-md transition-colors ${
|
||||
showPairingSection ? "bg-white text-gray-900 shadow-sm" : "text-gray-600 hover:text-gray-900"
|
||||
}`}
|
||||
>
|
||||
<Key className="h-4 w-4 inline mr-2" />
|
||||
Pairing
|
||||
</button>
|
||||
</div>
|
||||
{/* Carte de connexion 4NK */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-center">
|
||||
<Shield className="h-8 w-8 mx-auto mb-4 text-blue-600" />
|
||||
Connexion sécurisée 4NK
|
||||
</CardTitle>
|
||||
<CardDescription className="text-center">
|
||||
Authentification cryptographique sans mot de passe
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Description de la connexion 4NK */}
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<h3 className="font-semibold text-blue-900 mb-2">🔐 Authentification 4NK</h3>
|
||||
<ul className="text-sm text-blue-800 space-y-1">
|
||||
<li>• Aucun mot de passe requis</li>
|
||||
<li>• Identité cryptographique sécurisée</li>
|
||||
<li>• Chiffrement bout en bout</li>
|
||||
<li>• Protection par blockchain</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{!showPairingSection ? (
|
||||
/* Carte de connexion */
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center">
|
||||
<Building2 className="h-5 w-5 mr-2 text-blue-600" />
|
||||
Identification d'entreprise
|
||||
</CardTitle>
|
||||
<CardDescription>Connectez-vous avec votre identifiant unique sécurisé par 4NK</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="companyId">Votre identifiant unique</Label>
|
||||
<Input
|
||||
id="companyId"
|
||||
type="text"
|
||||
placeholder="Saisissez votre identifiant d'entreprise"
|
||||
value={companyId}
|
||||
onChange={(e) => setCompanyId(e.target.value)}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
{/* Affichage des erreurs */}
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||
<p className="text-red-700 font-medium">Erreur de connexion :</p>
|
||||
<p className="text-red-600 text-sm">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Info mode démonstration */}
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-3">
|
||||
<div className="flex items-start space-x-2">
|
||||
<TestTube className="h-4 w-4 text-green-600 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-xs text-green-700">
|
||||
<p className="font-medium mb-1">Mode démonstration</p>
|
||||
<p>
|
||||
Utilisez l'identifiant <strong>"1234"</strong> pour accéder directement aux écrans de
|
||||
démonstration avec des données simulées.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Bouton de connexion */}
|
||||
<Button
|
||||
onClick={handleLogin}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Shield className="h-5 w-5 mr-2" />
|
||||
{isLoading ? "Connexion en cours..." : "Se connecter avec 4NK"}
|
||||
</Button>
|
||||
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading ? "Connexion en cours..." : "Se connecter"}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
/* Carte de pairing */
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center">
|
||||
<Key className="h-5 w-5 mr-2 text-blue-600" />
|
||||
Pairing d'appareil
|
||||
</CardTitle>
|
||||
<CardDescription>Ajoutez cet appareil à votre compte existant</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{!pairingSuccess ? (
|
||||
<form onSubmit={handlePairingSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>Mots de pairing temporaires</Label>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowPairingInput(!showPairingInput)}
|
||||
className="text-blue-700 border-blue-300"
|
||||
>
|
||||
{showPairingInput ? <EyeOff className="h-4 w-4 mr-1" /> : <Eye className="h-4 w-4 mr-1" />}
|
||||
{showPairingInput ? "Masquer" : "Afficher"}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">Saisissez les 4 mots affichés sur votre autre appareil</p>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{pairingWords.map((word, index) => (
|
||||
<Input
|
||||
key={index}
|
||||
type={showPairingInput ? "text" : "password"}
|
||||
placeholder={`Mot ${index + 1}`}
|
||||
value={word}
|
||||
onChange={(e) => handlePairingWordChange(index, e.target.value)}
|
||||
className="text-center font-mono select-none"
|
||||
style={{ userSelect: "none", WebkitUserSelect: "none" }}
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
onCopy={(e) => e.preventDefault()}
|
||||
onCut={(e) => e.preventDefault()}
|
||||
onPaste={(e) => e.preventDefault()}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
required
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{pairingError && (
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-3">
|
||||
<div className="flex items-start space-x-2">
|
||||
<AlertTriangle className="h-4 w-4 text-red-600 mt-0.5 flex-shrink-0" />
|
||||
<p className="text-sm text-red-700">{pairingError}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
<div className="text-xs text-blue-700">
|
||||
<p className="font-medium mb-1">Instructions :</p>
|
||||
<ol className="space-y-1">
|
||||
<li>1. Ouvrez DocV sur votre appareil principal</li>
|
||||
<li>2. Allez dans Paramètres → Sécurité</li>
|
||||
<li>3. Cliquez sur "Ajouter un appareil"</li>
|
||||
<li>4. Saisissez les 4 mots affichés ici</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-full">
|
||||
<Key className="h-4 w-4 mr-2" />
|
||||
Appairer cet appareil
|
||||
</Button>
|
||||
</form>
|
||||
) : (
|
||||
<div className="text-center py-6">
|
||||
<CheckCircle className="h-12 w-12 mx-auto text-green-600 mb-4" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">Pairing réussi !</h3>
|
||||
<p className="text-gray-600 mb-4">Cet appareil a été ajouté à votre compte avec succès.</p>
|
||||
<div className="animate-pulse text-blue-600">Redirection vers le dashboard...</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
{/* Informations sur l'iframe */}
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-lg p-3">
|
||||
<p className="text-xs text-gray-600 text-center">
|
||||
<strong>URL d'authentification :</strong><br />
|
||||
{iframeUrl}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Badges de sécurité */}
|
||||
<div className="flex flex-wrap justify-center gap-2">
|
||||
|
||||
@ -3,10 +3,9 @@
|
||||
import { useState, useEffect, memo } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Shield, CheckCircle, Loader2, AlertCircle, RefreshCw } from "lucide-react"
|
||||
import { Shield, CheckCircle, Loader2 } from "lucide-react"
|
||||
import { Iframe } from "./Iframe"
|
||||
import { MessageBus } from "@/lib/4nk/MessageBus"
|
||||
import { IframeReference } from "@/lib/4nk/IframeReference"
|
||||
import MessageBus from "@/lib/4nk/MessageBus"
|
||||
|
||||
interface AuthModalProps {
|
||||
isOpen: boolean
|
||||
@ -19,188 +18,38 @@ export const AuthModal = memo(function AuthModal({ isOpen, onConnect, onClose, i
|
||||
const [isIframeReady, setIsIframeReady] = useState(false)
|
||||
const [showIframe, setShowIframe] = useState(false)
|
||||
const [authSuccess, setAuthSuccess] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [loadingStep, setLoadingStep] = useState("")
|
||||
const [retryCount, setRetryCount] = useState(0)
|
||||
const [iframeLoaded, setIframeLoaded] = useState(false)
|
||||
|
||||
const maxRetries = 3
|
||||
useEffect(() => {
|
||||
MessageBus.getInstance(iframeUrl).isReady().then(() => {
|
||||
setIsIframeReady(true)
|
||||
})
|
||||
}, [iframeUrl])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
// Reset des états à la fermeture
|
||||
setIsIframeReady(false)
|
||||
setShowIframe(false)
|
||||
setIsIframeReady(false)
|
||||
setAuthSuccess(false)
|
||||
setIsLoading(false)
|
||||
setError(null)
|
||||
setLoadingStep("")
|
||||
setRetryCount(0)
|
||||
setIframeLoaded(false)
|
||||
return
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
initAuth()
|
||||
}, [isOpen, iframeUrl, retryCount])
|
||||
|
||||
const initAuth = async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
setLoadingStep("Initialisation...")
|
||||
|
||||
console.log("🔗 Initialisation authentification 4NK avec:", iframeUrl)
|
||||
console.log("🔄 Tentative:", retryCount + 1, "/", maxRetries + 1)
|
||||
|
||||
// Étape 1: Attendre que l'iframe soit disponible dans le DOM
|
||||
setLoadingStep("Chargement de l'iframe...")
|
||||
let attempts = 0
|
||||
const maxAttempts = 40 // 20 secondes
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
const iframe = IframeReference.getIframe()
|
||||
if (iframe && iframe.contentWindow) {
|
||||
console.log("✅ Iframe disponible après", attempts * 500, "ms")
|
||||
break
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
attempts++
|
||||
}
|
||||
|
||||
if (attempts >= maxAttempts) {
|
||||
throw new Error("Iframe 4NK non disponible dans le DOM après 20 secondes")
|
||||
}
|
||||
|
||||
// Étape 2: Attendre que l'iframe soit complètement chargée
|
||||
setLoadingStep("Attente du chargement complet...")
|
||||
await waitForIframeLoad()
|
||||
|
||||
useEffect(() => {
|
||||
if (isIframeReady && !showIframe) {
|
||||
setShowIframe(true)
|
||||
|
||||
// Étape 3: Attendre le message LISTENING de l'iframe
|
||||
setLoadingStep("Attente du signal LISTENING...")
|
||||
const messageBus = MessageBus.getInstance(iframeUrl)
|
||||
MessageBus.getInstance(iframeUrl).requestLink().then(() => {
|
||||
setAuthSuccess(true)
|
||||
|
||||
console.log("⏳ Attente du message LISTENING de l'iframe...")
|
||||
await messageBus.isReady()
|
||||
console.log("✅ Iframe prête et en écoute")
|
||||
setTimeout(() => onConnect(), 500)
|
||||
}).catch((_error: string) => {
|
||||
setShowIframe(false)
|
||||
setIsIframeReady(false)
|
||||
setAuthSuccess(false)
|
||||
|
||||
setIsIframeReady(true)
|
||||
|
||||
// Étape 4: Demander l'authentification (REQUEST_LINK)
|
||||
setLoadingStep("Demande d'authentification...")
|
||||
console.log("🔐 Envoi REQUEST_LINK...")
|
||||
await messageBus.requestLink()
|
||||
console.log("✅ LINK_ACCEPTED reçu, tokens stockés")
|
||||
|
||||
// Étape 5: Récupérer l'ID d'appairage
|
||||
setLoadingStep("Récupération de l'identité...")
|
||||
console.log("🆔 Récupération de l'ID d'appairage...")
|
||||
await messageBus.getUserPairingId()
|
||||
console.log("✅ ID d'appairage récupéré")
|
||||
|
||||
setAuthSuccess(true)
|
||||
|
||||
// Délai avant de déclencher onConnect
|
||||
setTimeout(() => {
|
||||
onConnect()
|
||||
}, 500)
|
||||
} catch (err) {
|
||||
console.error("❌ Authentication error:", err)
|
||||
const errorMessage = err instanceof Error ? err.message : "Erreur d'authentification"
|
||||
|
||||
// Messages d'erreur plus spécifiques selon le protocole 4NK
|
||||
if (errorMessage.includes("LINK_ACCEPTED")) {
|
||||
setError("Erreur d'authentification : réponse inattendue du serveur 4NK")
|
||||
} else if (errorMessage.includes("Tokens manquants")) {
|
||||
setError("Erreur : les tokens d'authentification n'ont pas été reçus")
|
||||
} else if (errorMessage.includes("LISTENING")) {
|
||||
setError("L'iframe 4NK n'est pas en écoute. Vérifiez l'URL de l'iframe.")
|
||||
} else if (errorMessage.includes("Timeout")) {
|
||||
setError("Timeout : L'iframe 4NK ne répond pas. Vérifiez votre connexion.")
|
||||
} else if (errorMessage.includes("origin")) {
|
||||
setError("Erreur de configuration : les domaines ne correspondent pas")
|
||||
} else {
|
||||
setError(errorMessage)
|
||||
}
|
||||
|
||||
setIsIframeReady(false)
|
||||
setShowIframe(false)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
setLoadingStep("")
|
||||
onClose()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const waitForIframeLoad = (): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const iframe = IframeReference.getIframe()
|
||||
if (!iframe) {
|
||||
reject(new Error("Iframe not found"))
|
||||
return
|
||||
}
|
||||
|
||||
// Si l'iframe est déjà chargée
|
||||
if (iframeLoaded) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
cleanup()
|
||||
reject(new Error("Timeout: L'iframe n'a pas fini de se charger"))
|
||||
}, 30000) // 30 secondes pour le chargement
|
||||
|
||||
const cleanup = () => {
|
||||
clearTimeout(timeout)
|
||||
iframe.removeEventListener("load", onLoad)
|
||||
iframe.removeEventListener("error", onError)
|
||||
}
|
||||
|
||||
const onLoad = () => {
|
||||
console.log("✅ Iframe loaded successfully")
|
||||
setIframeLoaded(true)
|
||||
cleanup()
|
||||
// Attendre un peu plus pour que le contenu soit prêt
|
||||
setTimeout(resolve, 2000)
|
||||
}
|
||||
|
||||
const onError = () => {
|
||||
console.error("❌ Iframe failed to load")
|
||||
cleanup()
|
||||
reject(new Error("Erreur de chargement de l'iframe"))
|
||||
}
|
||||
|
||||
iframe.addEventListener("load", onLoad)
|
||||
iframe.addEventListener("error", onError)
|
||||
|
||||
// Si l'iframe semble déjà chargée
|
||||
if (iframe.contentDocument?.readyState === "complete") {
|
||||
onLoad()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleRetry = () => {
|
||||
if (retryCount < maxRetries) {
|
||||
setRetryCount((prev) => prev + 1)
|
||||
setError(null)
|
||||
} else {
|
||||
setError("Nombre maximum de tentatives atteint. Veuillez vérifier votre connexion et réessayer plus tard.")
|
||||
}
|
||||
}
|
||||
|
||||
const handleForceRetry = () => {
|
||||
setRetryCount(0)
|
||||
setError(null)
|
||||
setIframeLoaded(false)
|
||||
// Forcer le rechargement de l'iframe
|
||||
const iframe = IframeReference.getIframe()
|
||||
if (iframe) {
|
||||
iframe.src = iframe.src
|
||||
}
|
||||
}
|
||||
}, [isIframeReady, showIframe, iframeUrl, onConnect, onClose])
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
@ -215,106 +64,25 @@ export const AuthModal = memo(function AuthModal({ isOpen, onConnect, onClose, i
|
||||
<CardDescription>Connexion sécurisée avec votre identité cryptographique</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||
<div className="flex items-start space-x-2">
|
||||
<AlertCircle className="h-5 w-5 text-red-600 mt-0.5 flex-shrink-0" />
|
||||
<div className="flex-1">
|
||||
<p className="text-red-700 text-sm font-medium mb-2">Erreur de connexion</p>
|
||||
<p className="text-red-600 text-xs mb-3">{error}</p>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
{retryCount < maxRetries && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleRetry}
|
||||
className="bg-transparent text-red-700 border-red-300 hover:bg-red-50"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
Réessayer ({retryCount + 1}/{maxRetries + 1})
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleForceRetry}
|
||||
className="bg-transparent text-red-700 border-red-300 hover:bg-red-50"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
Forcer le rechargement
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{!isIframeReady && (
|
||||
<div className="flex flex-col items-center justify-center h-96 gap-4">
|
||||
<Loader2 className="h-10 w-10 animate-spin text-blue-600" />
|
||||
<div className="font-semibold text-lg">Chargement de l'authentification...</div>
|
||||
</div>
|
||||
)}
|
||||
{authSuccess ? (
|
||||
<div className="flex flex-col items-center justify-center h-96 gap-5">
|
||||
<CheckCircle className="h-12 w-12 text-green-600" />
|
||||
<div className="font-semibold text-lg text-green-700">
|
||||
Authentification réussie !
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading && !authSuccess && (
|
||||
<div className="text-center py-8">
|
||||
<Loader2 className="h-8 w-8 animate-spin mx-auto mb-4 text-blue-600" />
|
||||
<p className="text-gray-600 font-medium">{loadingStep}</p>
|
||||
{loadingStep && <p className="text-gray-500 text-sm mt-2">Protocole 4NK en cours...</p>}
|
||||
|
||||
{/* Barre de progression visuelle */}
|
||||
<div className="mt-4 w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded-full transition-all duration-500"
|
||||
style={{
|
||||
width:
|
||||
loadingStep === "Initialisation..."
|
||||
? "15%"
|
||||
: loadingStep === "Chargement de l'iframe..."
|
||||
? "30%"
|
||||
: loadingStep === "Attente du chargement complet..."
|
||||
? "45%"
|
||||
: loadingStep === "Attente du signal LISTENING..."
|
||||
? "60%"
|
||||
: loadingStep === "Demande d'authentification..."
|
||||
? "80%"
|
||||
: loadingStep === "Récupération de l'identité..."
|
||||
? "95%"
|
||||
: "0%",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{authSuccess && (
|
||||
<div className="text-center py-8">
|
||||
<CheckCircle className="h-12 w-12 text-green-600 mx-auto mb-4" />
|
||||
<p className="text-green-700 font-semibold">Authentification réussie !</p>
|
||||
<p className="text-gray-600 text-sm mt-2">Tokens stockés • Redirection en cours...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showIframe && !authSuccess && !error && (
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<div className="bg-blue-50 p-2 text-center">
|
||||
<p className="text-blue-700 text-sm">Interface d'authentification 4NK</p>
|
||||
<p className="text-blue-600 text-xs">En attente de LISTENING → REQUEST_LINK → LINK_ACCEPTED</p>
|
||||
</div>
|
||||
<Iframe iframeUrl={iframeUrl} showIframe={true} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Informations de debug */}
|
||||
{(error || isLoading) && (
|
||||
<div className="bg-gray-50 p-3 rounded-lg text-xs">
|
||||
<p>
|
||||
<strong>URL:</strong> {iframeUrl}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Tentative:</strong> {retryCount + 1}/{maxRetries + 1}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Iframe chargée:</strong> {iframeLoaded ? "Oui" : "Non"}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Protocole:</strong> LISTENING → REQUEST_LINK → LINK_ACCEPTED
|
||||
</p>
|
||||
) : (
|
||||
<div className="flex justify-center items-center w-full">
|
||||
<Iframe
|
||||
iframeUrl={iframeUrl}
|
||||
showIframe={showIframe}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { useRef, useEffect, memo } from "react"
|
||||
import { IframeReference } from "@/lib/4nk/IframeReference"
|
||||
import IframeReference from "@/lib/4nk/IframeReference"
|
||||
|
||||
interface IframeProps {
|
||||
iframeUrl: string
|
||||
@ -19,19 +19,19 @@ export const Iframe = memo(function Iframe({ iframeUrl, showIframe = false }: If
|
||||
return () => {
|
||||
IframeReference.setIframe(null)
|
||||
}
|
||||
}, [])
|
||||
}, [iframeRef.current])
|
||||
|
||||
return (
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
src={iframeUrl}
|
||||
style={{
|
||||
width: showIframe ? "100%" : "0",
|
||||
height: showIframe ? "400px" : "0",
|
||||
border: "none",
|
||||
display: showIframe ? "block" : "none",
|
||||
display: showIframe ? 'block' : 'none',
|
||||
width: '400px',
|
||||
height: '400px',
|
||||
border: 'none',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
sandbox="allow-scripts allow-same-origin allow-forms"
|
||||
title="4NK Authentication"
|
||||
/>
|
||||
)
|
||||
|
||||
@ -1,47 +1,33 @@
|
||||
/**
|
||||
* EventBus - Bus d'événements intra-onglet pour la communication interne
|
||||
* Pattern Singleton avec pub/sub
|
||||
*/
|
||||
export class EventBus {
|
||||
private static instance: EventBus | null = null
|
||||
private listeners: Map<string, Array<(...args: any[]) => void>> = new Map()
|
||||
export default class EventBus {
|
||||
private static instance: EventBus;
|
||||
private listeners: Record<string, Array<(...args: any[]) => void>> = {};
|
||||
|
||||
private constructor() {}
|
||||
private constructor() { }
|
||||
|
||||
static getInstance(): EventBus {
|
||||
public static getInstance(): EventBus {
|
||||
if (!EventBus.instance) {
|
||||
EventBus.instance = new EventBus()
|
||||
EventBus.instance = new EventBus();
|
||||
}
|
||||
return EventBus.instance
|
||||
return EventBus.instance;
|
||||
}
|
||||
|
||||
on(event: string, callback: (...args: any[]) => void): () => void {
|
||||
if (!this.listeners.has(event)) {
|
||||
this.listeners.set(event, [])
|
||||
public on(event: string, callback: (...args: any[]) => void): () => void {
|
||||
if (!this.listeners[event]) {
|
||||
this.listeners[event] = [];
|
||||
}
|
||||
|
||||
const eventListeners = this.listeners.get(event)!
|
||||
eventListeners.push(callback)
|
||||
|
||||
// Retourne une fonction d'unsubscribe
|
||||
this.listeners[event].push(callback);
|
||||
return () => {
|
||||
const index = eventListeners.indexOf(callback)
|
||||
if (index > -1) {
|
||||
eventListeners.splice(index, 1)
|
||||
if (this.listeners[event]) {
|
||||
this.listeners[event] = this.listeners[event].filter(cb => cb !== callback);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
emit(event: string, ...args: any[]): void {
|
||||
const eventListeners = this.listeners.get(event)
|
||||
if (eventListeners) {
|
||||
eventListeners.forEach((callback) => {
|
||||
try {
|
||||
callback(...args)
|
||||
} catch (error) {
|
||||
console.error(`Error in event listener for ${event}:`, error)
|
||||
}
|
||||
})
|
||||
public emit(event: string, ...args: any[]): void {
|
||||
if (this.listeners[event]) {
|
||||
this.listeners[event].forEach(callback => {
|
||||
callback(...args);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,15 +1,13 @@
|
||||
/**
|
||||
* IframeReference - Référence globale pour l'iframe 4NK
|
||||
* Permet aux autres services d'accéder à l'iframe pour postMessage
|
||||
*/
|
||||
export class IframeReference {
|
||||
private static iframe: HTMLIFrameElement | null = null
|
||||
export default class IframeReference {
|
||||
private static iframe: HTMLIFrameElement | null = null;
|
||||
|
||||
static setIframe(iframe: HTMLIFrameElement | null): void {
|
||||
IframeReference.iframe = iframe
|
||||
private constructor() { }
|
||||
|
||||
public static setIframe(iframe: HTMLIFrameElement | null): void {
|
||||
this.iframe = iframe;
|
||||
}
|
||||
|
||||
static getIframe(): HTMLIFrameElement | null {
|
||||
return IframeReference.iframe
|
||||
public static getIframe(): HTMLIFrameElement | null {
|
||||
return this.iframe;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,59 +1,43 @@
|
||||
/**
|
||||
* UserStore - Singleton pour la gestion des tokens et identifiants utilisateur
|
||||
* Stockage en sessionStorage selon les spécifications 4NK
|
||||
*/
|
||||
export class UserStore {
|
||||
private static instance: UserStore | null = null
|
||||
export default class UserStore {
|
||||
private static instance: UserStore;
|
||||
|
||||
private constructor() {}
|
||||
private constructor() { }
|
||||
|
||||
static getInstance(): UserStore {
|
||||
public static getInstance(): UserStore {
|
||||
if (!UserStore.instance) {
|
||||
UserStore.instance = new UserStore()
|
||||
UserStore.instance = new UserStore();
|
||||
}
|
||||
return UserStore.instance
|
||||
return UserStore.instance;
|
||||
}
|
||||
|
||||
connect(accessToken: string, refreshToken: string): void {
|
||||
if (typeof window !== "undefined") {
|
||||
sessionStorage.setItem("4nk_access_token", accessToken)
|
||||
sessionStorage.setItem("4nk_refresh_token", refreshToken)
|
||||
}
|
||||
public connect(accessToken: string, refreshToken: string): void {
|
||||
sessionStorage.setItem('accessToken', accessToken);
|
||||
sessionStorage.setItem('refreshToken', refreshToken);
|
||||
}
|
||||
|
||||
isConnected(): boolean {
|
||||
if (typeof window === "undefined") return false
|
||||
const accessToken = sessionStorage.getItem("4nk_access_token")
|
||||
const refreshToken = sessionStorage.getItem("4nk_refresh_token")
|
||||
return !!(accessToken && refreshToken)
|
||||
public isConnected(): boolean {
|
||||
return sessionStorage.getItem('accessToken') !== null && sessionStorage.getItem('refreshToken') !== null;
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
if (typeof window !== "undefined") {
|
||||
sessionStorage.removeItem("4nk_access_token")
|
||||
sessionStorage.removeItem("4nk_refresh_token")
|
||||
sessionStorage.removeItem("4nk_user_pairing_id")
|
||||
}
|
||||
public disconnect(): void {
|
||||
sessionStorage.removeItem('accessToken');
|
||||
sessionStorage.removeItem('refreshToken');
|
||||
sessionStorage.removeItem('userPairingId');
|
||||
}
|
||||
|
||||
getAccessToken(): string | null {
|
||||
if (typeof window === "undefined") return null
|
||||
return sessionStorage.getItem("4nk_access_token")
|
||||
public getAccessToken(): string | null {
|
||||
return sessionStorage.getItem('accessToken');
|
||||
}
|
||||
|
||||
getRefreshToken(): string | null {
|
||||
if (typeof window === "undefined") return null
|
||||
return sessionStorage.getItem("4nk_refresh_token")
|
||||
public getRefreshToken(): string | null {
|
||||
return sessionStorage.getItem('refreshToken');
|
||||
}
|
||||
|
||||
pair(userPairingId: string): void {
|
||||
if (typeof window !== "undefined") {
|
||||
sessionStorage.setItem("4nk_user_pairing_id", userPairingId)
|
||||
}
|
||||
public pair(userPairingId: string): void {
|
||||
sessionStorage.setItem('userPairingId', userPairingId);
|
||||
}
|
||||
|
||||
getUserPairingId(): string | null {
|
||||
if (typeof window === "undefined") return null
|
||||
return sessionStorage.getItem("4nk_user_pairing_id")
|
||||
public getUserPairingId(): string | null {
|
||||
return sessionStorage.getItem('userPairingId');
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user