Added the 4NK authentication
This commit is contained in:
parent
778abe903d
commit
cc7414a441
@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
{!showPairingSection ? (
|
|
||||||
/* Carte de connexion */
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center">
|
<CardTitle className="text-center">
|
||||||
<Building2 className="h-5 w-5 mr-2 text-blue-600" />
|
<Shield className="h-8 w-8 mx-auto mb-4 text-blue-600" />
|
||||||
Identification d'entreprise
|
Connexion sécurisée 4NK
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>Connectez-vous avec votre identifiant unique sécurisé par 4NK</CardDescription>
|
<CardDescription className="text-center">
|
||||||
|
Authentification cryptographique sans mot de passe
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="space-y-6">
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
{/* Description de la connexion 4NK */}
|
||||||
<div className="space-y-2">
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||||
<Label htmlFor="companyId">Votre identifiant unique</Label>
|
<h3 className="font-semibold text-blue-900 mb-2">🔐 Authentification 4NK</h3>
|
||||||
<Input
|
<ul className="text-sm text-blue-800 space-y-1">
|
||||||
id="companyId"
|
<li>• Aucun mot de passe requis</li>
|
||||||
type="text"
|
<li>• Identité cryptographique sécurisée</li>
|
||||||
placeholder="Saisissez votre identifiant d'entreprise"
|
<li>• Chiffrement bout en bout</li>
|
||||||
value={companyId}
|
<li>• Protection par blockchain</li>
|
||||||
onChange={(e) => setCompanyId(e.target.value)}
|
</ul>
|
||||||
required
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Info mode démonstration */}
|
{/* Affichage des erreurs */}
|
||||||
<div className="bg-green-50 border border-green-200 rounded-lg p-3">
|
{error && (
|
||||||
<div className="flex items-start space-x-2">
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||||
<TestTube className="h-4 w-4 text-green-600 mt-0.5 flex-shrink-0" />
|
<p className="text-red-700 font-medium">Erreur de connexion :</p>
|
||||||
<div className="text-xs text-green-700">
|
<p className="text-red-600 text-sm">{error}</p>
|
||||||
<p className="font-medium mb-1">Mode démonstration</p>
|
</div>
|
||||||
<p>
|
)}
|
||||||
Utilisez l'identifiant <strong>"1234"</strong> pour accéder directement aux écrans de
|
|
||||||
démonstration avec des données simulées.
|
{/* 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>
|
||||||
|
|
||||||
|
{/* 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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
|
||||||
{isLoading ? "Connexion en cours..." : "Se connecter"}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 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">
|
||||||
|
|||||||
@ -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...")
|
|
||||||
const messageBus = MessageBus.getInstance(iframeUrl)
|
|
||||||
|
|
||||||
console.log("⏳ Attente du message LISTENING de l'iframe...")
|
|
||||||
await messageBus.isReady()
|
|
||||||
console.log("✅ Iframe prête et en écoute")
|
|
||||||
|
|
||||||
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)
|
setAuthSuccess(true)
|
||||||
|
|
||||||
// Délai avant de déclencher onConnect
|
setTimeout(() => onConnect(), 500)
|
||||||
setTimeout(() => {
|
}).catch((_error: string) => {
|
||||||
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)
|
setShowIframe(false)
|
||||||
} finally {
|
setIsIframeReady(false)
|
||||||
setIsLoading(false)
|
setAuthSuccess(false)
|
||||||
setLoadingStep("")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const waitForIframeLoad = (): Promise<void> => {
|
onClose()
|
||||||
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()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}, [isIframeReady, showIframe, iframeUrl, onConnect, onClose])
|
||||||
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,107 +64,26 @@ 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">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{authSuccess ? (
|
||||||
{isLoading && !authSuccess && (
|
<div className="flex flex-col items-center justify-center h-96 gap-5">
|
||||||
<div className="text-center py-8">
|
<CheckCircle className="h-12 w-12 text-green-600" />
|
||||||
<Loader2 className="h-8 w-8 animate-spin mx-auto mb-4 text-blue-600" />
|
<div className="font-semibold text-lg text-green-700">
|
||||||
<p className="text-gray-600 font-medium">{loadingStep}</p>
|
Authentification réussie !
|
||||||
{loadingStep && <p className="text-gray-500 text-sm mt-2">Protocole 4NK en cours...</p>}
|
</div>
|
||||||
|
</div>
|
||||||
{/* Barre de progression visuelle */}
|
) : (
|
||||||
<div className="mt-4 w-full bg-gray-200 rounded-full h-2">
|
<div className="flex justify-center items-center w-full">
|
||||||
<div
|
<Iframe
|
||||||
className="bg-blue-600 h-2 rounded-full transition-all duration-500"
|
iframeUrl={iframeUrl}
|
||||||
style={{
|
showIframe={showIframe}
|
||||||
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>
|
||||||
</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 className="text-center">
|
<div className="text-center">
|
||||||
|
|||||||
@ -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"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
@ -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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user