Added the 4NK authentication

This commit is contained in:
Sadrinho27 2025-09-30 00:19:30 +02:00
parent 778abe903d
commit cc7414a441
9 changed files with 868 additions and 1037 deletions

View File

@ -25,8 +25,9 @@ import {
Home, Home,
} from "lucide-react" } from "lucide-react"
import { AuthModal } from "@/components/4nk/AuthModal" import { AuthModal } from "@/components/4nk/AuthModal"
import { MessageBus } from "@/lib/4nk/MessageBus" import { Iframe } from "@/components/4nk/Iframe"
import { UserStore } from "@/lib/4nk/UserStore" import MessageBus from "@/lib/4nk/MessageBus"
import UserStore from "@/lib/4nk/UserStore"
// DebugInfo supprimé // DebugInfo supprimé
export default function DashboardLayout({ children }: { children: React.ReactNode }) { export default function DashboardLayout({ children }: { children: React.ReactNode }) {
@ -40,7 +41,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
const router = useRouter() const router = useRouter()
const pathname = usePathname() 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 = [ const navigation = [
{ name: "Tableau de bord", href: "/dashboard", icon: LayoutDashboard }, { 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) const messageBus = MessageBus.getInstance(iframeUrl)
if (accessToken) { if (accessToken) {
// Vérifier si on est en mode mock // Mode normal (pas de mock)
const mockMode = messageBus.isInMockMode() setIsMockMode(false)
setIsMockMode(mockMode)
if (mockMode) { if (false) {
console.log("🎭 Dashboard en mode mock") console.log("🎭 Dashboard en mode mock")
setIsAuthenticated(true) setIsAuthenticated(true)
setUserInfo({ setUserInfo({
@ -117,7 +117,6 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
const messageBus = MessageBus.getInstance(iframeUrl) const messageBus = MessageBus.getInstance(iframeUrl)
userStore.disconnect() userStore.disconnect()
messageBus.disableMockMode()
// Afficher un message de confirmation avec options // Afficher un message de confirmation avec options
setShowLogoutConfirm(true) setShowLogoutConfirm(true)
@ -321,6 +320,14 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
</div> </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é */} {/* Debug info retiré */}
</div> </div>
) )

View File

@ -26,7 +26,7 @@ import {
HardDrive, HardDrive,
X, X,
} from "lucide-react" } from "lucide-react"
import { MessageBus } from "@/lib/4nk/MessageBus" import MessageBus from "@/lib/4nk/MessageBus"
import Link from "next/link" import Link from "next/link"
export default function DashboardPage() { export default function DashboardPage() {
@ -54,13 +54,12 @@ export default function DashboardPage() {
const [notifications, setNotifications] = useState<any[]>([]) const [notifications, setNotifications] = useState<any[]>([])
useEffect(() => { 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 messageBus = MessageBus.getInstance(iframeUrl)
const mockMode = messageBus.isInMockMode() setIsMockMode(false)
setIsMockMode(mockMode)
// Simuler le chargement des données // Simuler le chargement des données
if (mockMode) { if (false) {
setStats({ setStats({
totalDocuments: 1247, totalDocuments: 1247,
totalFolders: 89, totalFolders: 89,

View File

@ -23,9 +23,9 @@ import {
EyeOff, EyeOff,
} from "lucide-react" } from "lucide-react"
import { AuthModal } from "@/components/4nk/AuthModal" 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 { MockService } from "@/lib/4nk/MockService"
import { UserStore } from "@/lib/4nk/UserStore" import UserStore from "@/lib/4nk/UserStore"
export default function LoginPage() { export default function LoginPage() {
const [companyId, setCompanyId] = useState("") const [companyId, setCompanyId] = useState("")
@ -34,11 +34,17 @@ export default function LoginPage() {
const [showPairingSection, setShowPairingSection] = useState(false) const [showPairingSection, setShowPairingSection] = useState(false)
const [pairingWords, setPairingWords] = useState(["", "", "", ""]) const [pairingWords, setPairingWords] = useState(["", "", "", ""])
const [pairingError, setPairingError] = useState("") const [pairingError, setPairingError] = useState("")
const [error, setError] = useState<string | null>(null)
const [pairingSuccess, setPairingSuccess] = useState(false) const [pairingSuccess, setPairingSuccess] = useState(false)
const router = useRouter() const router = useRouter()
const [showPairingInput, setShowPairingInput] = useState(false) 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) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
@ -58,8 +64,7 @@ export default function LoginPage() {
const mockService = MockService.getInstance() const mockService = MockService.getInstance()
const userStore = UserStore.getInstance() const userStore = UserStore.getInstance()
// Activer le mode mock // Mode normal (pas de mock)
messageBus.enableMockMode()
// Authentification mock // Authentification mock
const authResult = await mockService.mockAuthentication(companyId) 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> <p className="text-gray-600">Gestion électronique de documents sécurisée</p>
</div> </div>
{/* Navigation entre connexion et pairing */} {/* Carte de connexion 4NK */}
<div className="flex space-x-1 bg-gray-100 p-1 rounded-lg"> <Card>
<button <CardHeader>
onClick={() => setShowPairingSection(false)} <CardTitle className="text-center">
className={`flex-1 py-2 px-4 text-sm font-medium rounded-md transition-colors ${ <Shield className="h-8 w-8 mx-auto mb-4 text-blue-600" />
!showPairingSection ? "bg-white text-gray-900 shadow-sm" : "text-gray-600 hover:text-gray-900" Connexion sécurisée 4NK
}`} </CardTitle>
> <CardDescription className="text-center">
<Building2 className="h-4 w-4 inline mr-2" /> Authentification cryptographique sans mot de passe
Connexion </CardDescription>
</button> </CardHeader>
<button <CardContent className="space-y-6">
onClick={() => setShowPairingSection(true)} {/* Description de la connexion 4NK */}
className={`flex-1 py-2 px-4 text-sm font-medium rounded-md transition-colors ${ <div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
showPairingSection ? "bg-white text-gray-900 shadow-sm" : "text-gray-600 hover:text-gray-900" <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>
<Key className="h-4 w-4 inline mr-2" /> <li> Identité cryptographique sécurisée</li>
Pairing <li> Chiffrement bout en bout</li>
</button> <li> Protection par blockchain</li>
</div> </ul>
</div>
{!showPairingSection ? ( {/* Affichage des erreurs */}
/* Carte de connexion */ {error && (
<Card> <div className="bg-red-50 border border-red-200 rounded-lg p-4">
<CardHeader> <p className="text-red-700 font-medium">Erreur de connexion :</p>
<CardTitle className="flex items-center"> <p className="text-red-600 text-sm">{error}</p>
<Building2 className="h-5 w-5 mr-2 text-blue-600" /> </div>
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>
{/* Info mode démonstration */} {/* Bouton de connexion */}
<div className="bg-green-50 border border-green-200 rounded-lg p-3"> <Button
<div className="flex items-start space-x-2"> onClick={handleLogin}
<TestTube className="h-4 w-4 text-green-600 mt-0.5 flex-shrink-0" /> className="w-full"
<div className="text-xs text-green-700"> size="lg"
<p className="font-medium mb-1">Mode démonstration</p> disabled={isLoading}
<p> >
Utilisez l'identifiant <strong>"1234"</strong> pour accéder directement aux écrans de <Shield className="h-5 w-5 mr-2" />
démonstration avec des données simulées. {isLoading ? "Connexion en cours..." : "Se connecter avec 4NK"}
</p> </Button>
</div>
</div>
</div>
<Button type="submit" className="w-full" disabled={isLoading}> {/* Informations sur l'iframe */}
{isLoading ? "Connexion en cours..." : "Se connecter"} <div className="bg-gray-50 border border-gray-200 rounded-lg p-3">
</Button> <p className="text-xs text-gray-600 text-center">
</form> <strong>URL d'authentification :</strong><br />
</CardContent> {iframeUrl}
</Card> </p>
) : ( </div>
/* Carte de pairing */ </CardContent>
<Card> </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 é ajouté à votre compte avec succès.</p>
<div className="animate-pulse text-blue-600">Redirection vers le dashboard...</div>
</div>
)}
</CardContent>
</Card>
)}
{/* Badges de sécurité */} {/* Badges de sécurité */}
<div className="flex flex-wrap justify-center gap-2"> <div className="flex flex-wrap justify-center gap-2">

View File

@ -3,10 +3,9 @@
import { useState, useEffect, memo } from "react" import { useState, useEffect, memo } from "react"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" 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 { Iframe } from "./Iframe"
import { MessageBus } from "@/lib/4nk/MessageBus" import MessageBus from "@/lib/4nk/MessageBus"
import { IframeReference } from "@/lib/4nk/IframeReference"
interface AuthModalProps { interface AuthModalProps {
isOpen: boolean isOpen: boolean
@ -19,188 +18,38 @@ export const AuthModal = memo(function AuthModal({ isOpen, onConnect, onClose, i
const [isIframeReady, setIsIframeReady] = useState(false) const [isIframeReady, setIsIframeReady] = useState(false)
const [showIframe, setShowIframe] = useState(false) const [showIframe, setShowIframe] = useState(false)
const [authSuccess, setAuthSuccess] = 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(() => { useEffect(() => {
if (!isOpen) { if (!isOpen) {
// Reset des états à la fermeture
setIsIframeReady(false)
setShowIframe(false) setShowIframe(false)
setIsIframeReady(false)
setAuthSuccess(false) setAuthSuccess(false)
setIsLoading(false)
setError(null)
setLoadingStep("")
setRetryCount(0)
setIframeLoaded(false)
return
} }
}, [isOpen])
initAuth() useEffect(() => {
}, [isOpen, iframeUrl, retryCount]) if (isIframeReady && !showIframe) {
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()
setShowIframe(true) setShowIframe(true)
// Étape 3: Attendre le message LISTENING de l'iframe MessageBus.getInstance(iframeUrl).requestLink().then(() => {
setLoadingStep("Attente du signal LISTENING...") setAuthSuccess(true)
const messageBus = MessageBus.getInstance(iframeUrl)
console.log("⏳ Attente du message LISTENING de l'iframe...") setTimeout(() => onConnect(), 500)
await messageBus.isReady() }).catch((_error: string) => {
console.log("✅ Iframe prête et en écoute") setShowIframe(false)
setIsIframeReady(false)
setAuthSuccess(false)
setIsIframeReady(true) onClose()
})
// É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("")
} }
} }, [isIframeReady, showIframe, iframeUrl, onConnect, 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
}
}
if (!isOpen) return null 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> <CardDescription>Connexion sécurisée avec votre identité cryptographique</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-6"> <CardContent className="space-y-6">
{error && ( {!isIframeReady && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4"> <div className="flex flex-col items-center justify-center h-96 gap-4">
<div className="flex items-start space-x-2"> <Loader2 className="h-10 w-10 animate-spin text-blue-600" />
<AlertCircle className="h-5 w-5 text-red-600 mt-0.5 flex-shrink-0" /> <div className="font-semibold text-lg">Chargement de l'authentification...</div>
<div className="flex-1"> </div>
<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> {authSuccess ? (
<div className="flex flex-col items-center justify-center h-96 gap-5">
<div className="flex flex-col gap-2"> <CheckCircle className="h-12 w-12 text-green-600" />
{retryCount < maxRetries && ( <div className="font-semibold text-lg text-green-700">
<Button Authentification réussie !
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>
</div> </div>
</div> </div>
)} ) : (
<div className="flex justify-center items-center w-full">
{isLoading && !authSuccess && ( <Iframe
<div className="text-center py-8"> iframeUrl={iframeUrl}
<Loader2 className="h-8 w-8 animate-spin mx-auto mb-4 text-blue-600" /> showIframe={showIframe}
<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> </div>
)} )}

View File

@ -1,7 +1,7 @@
"use client" "use client"
import { useRef, useEffect, memo } from "react" import { useRef, useEffect, memo } from "react"
import { IframeReference } from "@/lib/4nk/IframeReference" import IframeReference from "@/lib/4nk/IframeReference"
interface IframeProps { interface IframeProps {
iframeUrl: string iframeUrl: string
@ -19,19 +19,19 @@ export const Iframe = memo(function Iframe({ iframeUrl, showIframe = false }: If
return () => { return () => {
IframeReference.setIframe(null) IframeReference.setIframe(null)
} }
}, []) }, [iframeRef.current])
return ( return (
<iframe <iframe
ref={iframeRef} ref={iframeRef}
src={iframeUrl} src={iframeUrl}
style={{ style={{
width: showIframe ? "100%" : "0", display: showIframe ? 'block' : 'none',
height: showIframe ? "400px" : "0", width: '400px',
border: "none", height: '400px',
display: showIframe ? "block" : "none", border: 'none',
overflow: 'hidden'
}} }}
sandbox="allow-scripts allow-same-origin allow-forms"
title="4NK Authentication" title="4NK Authentication"
/> />
) )

View File

@ -1,47 +1,33 @@
/** export default class EventBus {
* EventBus - Bus d'événements intra-onglet pour la communication interne private static instance: EventBus;
* Pattern Singleton avec pub/sub private listeners: Record<string, Array<(...args: any[]) => void>> = {};
*/
export class EventBus {
private static instance: EventBus | null = null
private listeners: Map<string, Array<(...args: any[]) => void>> = new Map()
private constructor() {} private constructor() { }
static getInstance(): EventBus { public static getInstance(): EventBus {
if (!EventBus.instance) { 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 { public on(event: string, callback: (...args: any[]) => void): () => void {
if (!this.listeners.has(event)) { if (!this.listeners[event]) {
this.listeners.set(event, []) this.listeners[event] = [];
} }
this.listeners[event].push(callback);
const eventListeners = this.listeners.get(event)!
eventListeners.push(callback)
// Retourne une fonction d'unsubscribe
return () => { return () => {
const index = eventListeners.indexOf(callback) if (this.listeners[event]) {
if (index > -1) { this.listeners[event] = this.listeners[event].filter(cb => cb !== callback);
eventListeners.splice(index, 1)
} }
} };
} }
emit(event: string, ...args: any[]): void { public emit(event: string, ...args: any[]): void {
const eventListeners = this.listeners.get(event) if (this.listeners[event]) {
if (eventListeners) { this.listeners[event].forEach(callback => {
eventListeners.forEach((callback) => { callback(...args);
try { });
callback(...args)
} catch (error) {
console.error(`Error in event listener for ${event}:`, error)
}
})
} }
} }
} }

View File

@ -1,15 +1,13 @@
/** export default class IframeReference {
* IframeReference - Référence globale pour l'iframe 4NK private static iframe: HTMLIFrameElement | null = null;
* Permet aux autres services d'accéder à l'iframe pour postMessage
*/
export class IframeReference {
private static iframe: HTMLIFrameElement | null = null
static setIframe(iframe: HTMLIFrameElement | null): void { private constructor() { }
IframeReference.iframe = iframe
public static setIframe(iframe: HTMLIFrameElement | null): void {
this.iframe = iframe;
} }
static getIframe(): HTMLIFrameElement | null { public static getIframe(): HTMLIFrameElement | null {
return IframeReference.iframe return this.iframe;
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,59 +1,43 @@
/** export default class UserStore {
* UserStore - Singleton pour la gestion des tokens et identifiants utilisateur private static instance: UserStore;
* Stockage en sessionStorage selon les spécifications 4NK
*/
export class UserStore {
private static instance: UserStore | null = null
private constructor() {} private constructor() { }
static getInstance(): UserStore { public static getInstance(): UserStore {
if (!UserStore.instance) { if (!UserStore.instance) {
UserStore.instance = new UserStore() UserStore.instance = new UserStore();
} }
return UserStore.instance return UserStore.instance;
} }
connect(accessToken: string, refreshToken: string): void { public connect(accessToken: string, refreshToken: string): void {
if (typeof window !== "undefined") { sessionStorage.setItem('accessToken', accessToken);
sessionStorage.setItem("4nk_access_token", accessToken) sessionStorage.setItem('refreshToken', refreshToken);
sessionStorage.setItem("4nk_refresh_token", refreshToken)
}
} }
isConnected(): boolean { public isConnected(): boolean {
if (typeof window === "undefined") return false return sessionStorage.getItem('accessToken') !== null && sessionStorage.getItem('refreshToken') !== null;
const accessToken = sessionStorage.getItem("4nk_access_token")
const refreshToken = sessionStorage.getItem("4nk_refresh_token")
return !!(accessToken && refreshToken)
} }
disconnect(): void { public disconnect(): void {
if (typeof window !== "undefined") { sessionStorage.removeItem('accessToken');
sessionStorage.removeItem("4nk_access_token") sessionStorage.removeItem('refreshToken');
sessionStorage.removeItem("4nk_refresh_token") sessionStorage.removeItem('userPairingId');
sessionStorage.removeItem("4nk_user_pairing_id")
}
} }
getAccessToken(): string | null { public getAccessToken(): string | null {
if (typeof window === "undefined") return null return sessionStorage.getItem('accessToken');
return sessionStorage.getItem("4nk_access_token")
} }
getRefreshToken(): string | null { public getRefreshToken(): string | null {
if (typeof window === "undefined") return null return sessionStorage.getItem('refreshToken');
return sessionStorage.getItem("4nk_refresh_token")
} }
pair(userPairingId: string): void { public pair(userPairingId: string): void {
if (typeof window !== "undefined") { sessionStorage.setItem('userPairingId', userPairingId);
sessionStorage.setItem("4nk_user_pairing_id", userPairingId)
}
} }
getUserPairingId(): string | null { public getUserPairingId(): string | null {
if (typeof window === "undefined") return null return sessionStorage.getItem('userPairingId');
return sessionStorage.getItem("4nk_user_pairing_id")
} }
} }