retrieve the docv code source instead of skeleton
This commit is contained in:
parent
aedd3b9f10
commit
d5ac9879bb
371
README.md
371
README.md
@ -1,372 +1 @@
|
|||||||
# 🛡️ DocV - GED Souveraine et Sécurisée
|
|
||||||
|
|
||||||
> **Une approche révolutionnaire de la gestion documentaire avec sécurité, souveraineté et conformité garanties.**
|
|
||||||
|
|
||||||
[](VERSION)
|
|
||||||
[](https://nextjs.org/)
|
|
||||||
[](https://www.typescriptlang.org/)
|
|
||||||
[](https://tailwindcss.com/)
|
|
||||||
[](#license)
|
|
||||||
|
|
||||||
## 📋 Table des Matières
|
|
||||||
|
|
||||||
- [🎯 Vue d'ensemble](#-vue-densemble)
|
|
||||||
- [✨ Fonctionnalités](#-fonctionnalités)
|
|
||||||
- [🚀 Installation Rapide](#-installation-rapide)
|
|
||||||
- [⚙️ Configuration](#️-configuration)
|
|
||||||
- [🔧 Commandes de Développement](#-commandes-de-développement)
|
|
||||||
- [📚 Documentation](#-documentation)
|
|
||||||
- [🏗️ Architecture](#️-architecture)
|
|
||||||
- [🔒 Sécurité](#-sécurité)
|
|
||||||
- [🤝 Contribution](#-contribution)
|
|
||||||
- [📞 Support](#-support)
|
|
||||||
|
|
||||||
## 🎯 Vue d'ensemble
|
|
||||||
|
|
||||||
**DocV** est une plateforme de gestion documentaire (GED) révolutionnaire qui combine :
|
|
||||||
|
|
||||||
- **🔐 Authentification cryptographique** sans mots de passe
|
|
||||||
- **🤖 IA embarquée** pour l'OCR et la classification
|
|
||||||
- **🌐 Architecture souveraine** sans dépendance cloud
|
|
||||||
- **⚡ Interface conversationnelle** pour le suivi des dossiers
|
|
||||||
- **🔗 Ancrage blockchain** pour la traçabilité
|
|
||||||
|
|
||||||
### 🎯 Cas d'Usage Principaux
|
|
||||||
|
|
||||||
- **Entreprises** : Gestion documentaire sécurisée
|
|
||||||
- **Notaires** : Échanges documentaires via lecoffre.io
|
|
||||||
- **Secteur public** : Conformité et souveraineté des données
|
|
||||||
- **Éditeurs** : Intégration marque blanche
|
|
||||||
|
|
||||||
## ✨ Fonctionnalités
|
|
||||||
|
|
||||||
### 🔑 Authentification Ultra-Simplifiée
|
|
||||||
- ✅ Aucun mot de passe requis
|
|
||||||
- ✅ Aucun OTP ou code SMS
|
|
||||||
- ✅ Aucune application mobile
|
|
||||||
- ✅ Identité auto-générée et auto-portée
|
|
||||||
|
|
||||||
### 🤖 Intelligence Artificielle Locale
|
|
||||||
- ✅ OCR automatique des documents
|
|
||||||
- ✅ Classification intelligente
|
|
||||||
- ✅ Extraction de données
|
|
||||||
- ✅ Interface conversationnelle
|
|
||||||
- ✅ Traitements 100% locaux
|
|
||||||
|
|
||||||
### 🛡️ Sécurité de Bout en Bout
|
|
||||||
- ✅ Chiffrement natif
|
|
||||||
- ✅ Aucune interface admin exposée
|
|
||||||
- ✅ Aucun serveur d'identité
|
|
||||||
- ✅ Aucune dépendance cloud
|
|
||||||
- ✅ Conformité RGPD, ISO 27001, SecNumCloud
|
|
||||||
|
|
||||||
### 🌐 Architecture Souveraine
|
|
||||||
- ✅ Déploiement local
|
|
||||||
- ✅ Migration automatisée
|
|
||||||
- ✅ Compatible bases existantes
|
|
||||||
- ✅ APIs souveraines
|
|
||||||
- ✅ Accompagnement personnalisé
|
|
||||||
|
|
||||||
## 🚀 Installation Rapide
|
|
||||||
|
|
||||||
### 📋 Prérequis
|
|
||||||
|
|
||||||
- **Node.js** : Version 18.0+ (recommandé 20.x)
|
|
||||||
- **npm** ou **pnpm** : Gestionnaire de paquets
|
|
||||||
- **Git** : Pour le clonage du repository
|
|
||||||
|
|
||||||
### 1️⃣ Cloner le Repository
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Cloner le projet
|
|
||||||
git clone <REPO_URL>
|
|
||||||
cd docv
|
|
||||||
|
|
||||||
# Installer les dépendances
|
|
||||||
npm install
|
|
||||||
# ou
|
|
||||||
pnpm install
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2️⃣ Configuration d'Environnement
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Créer le fichier d'environnement
|
|
||||||
cp .env.example .env.local
|
|
||||||
|
|
||||||
# Éditer les variables d'environnement
|
|
||||||
nano .env.local
|
|
||||||
```
|
|
||||||
|
|
||||||
**Variables essentielles :**
|
|
||||||
```env
|
|
||||||
# Configuration de base
|
|
||||||
NEXT_PUBLIC_APP_NAME=DocV
|
|
||||||
NEXT_PUBLIC_APP_VERSION=0.1.0
|
|
||||||
|
|
||||||
# Base de données (si applicable)
|
|
||||||
DATABASE_URL=your_database_url
|
|
||||||
|
|
||||||
# Authentification
|
|
||||||
NEXTAUTH_SECRET=your_secret_key
|
|
||||||
NEXTAUTH_URL=http://localhost:3000
|
|
||||||
|
|
||||||
# Services externes (optionnels)
|
|
||||||
EMAIL_SERVICE_API_KEY=your_email_api_key
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3️⃣ Démarrage en Mode Développement
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Démarrer le serveur de développement
|
|
||||||
npm run dev
|
|
||||||
# ou
|
|
||||||
pnpm dev
|
|
||||||
|
|
||||||
# L'application sera disponible sur http://localhost:3000
|
|
||||||
```
|
|
||||||
|
|
||||||
## ⚙️ Configuration
|
|
||||||
|
|
||||||
### 🎨 Configuration de l'Interface
|
|
||||||
|
|
||||||
Le projet utilise **Tailwind CSS** avec des composants **Radix UI** pour une interface moderne et accessible.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Fichier de configuration Tailwind
|
|
||||||
tailwind.config.js
|
|
||||||
|
|
||||||
# Composants UI personnalisés
|
|
||||||
components/ui/
|
|
||||||
```
|
|
||||||
|
|
||||||
### 🔧 Configuration TypeScript
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Configuration TypeScript
|
|
||||||
tsconfig.json
|
|
||||||
|
|
||||||
# Types personnalisés
|
|
||||||
types/
|
|
||||||
```
|
|
||||||
|
|
||||||
### 📱 Configuration Responsive
|
|
||||||
|
|
||||||
L'interface s'adapte automatiquement aux différentes tailles d'écran :
|
|
||||||
- 📱 Mobile (< 768px)
|
|
||||||
- 📟 Tablet (768px - 1024px)
|
|
||||||
- 💻 Desktop (> 1024px)
|
|
||||||
|
|
||||||
## 🔧 Commandes de Développement
|
|
||||||
|
|
||||||
### 🚀 Commandes Principales
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Développement
|
|
||||||
npm run dev # Serveur de développement (port 3000)
|
|
||||||
npm run build # Build de production
|
|
||||||
npm run start # Serveur de production
|
|
||||||
npm run lint # Vérification du code
|
|
||||||
|
|
||||||
# Tests (si configurés)
|
|
||||||
npm run test # Tests unitaires
|
|
||||||
npm run test:watch # Tests en mode watch
|
|
||||||
npm run test:coverage # Tests avec couverture
|
|
||||||
|
|
||||||
# Maintenance
|
|
||||||
npm run clean # Nettoyer les fichiers temporaires
|
|
||||||
npm run type-check # Vérification TypeScript
|
|
||||||
```
|
|
||||||
|
|
||||||
### 🛠️ Commandes de Maintenance
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Mise à jour des dépendances
|
|
||||||
npm update # Mise à jour des paquets
|
|
||||||
npm audit # Audit de sécurité
|
|
||||||
npm audit fix # Correction automatique
|
|
||||||
|
|
||||||
# Gestion des dépendances
|
|
||||||
npm install <package> # Installer un paquet
|
|
||||||
npm uninstall <package> # Désinstaller un paquet
|
|
||||||
npm list # Lister les paquets installés
|
|
||||||
```
|
|
||||||
|
|
||||||
### 📦 Commandes de Build
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build de production
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# Analyse du bundle
|
|
||||||
npm run analyze # (si configuré)
|
|
||||||
|
|
||||||
# Build statique
|
|
||||||
npm run export # (si configuré)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 🔍 Commandes de Debug
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Logs détaillés
|
|
||||||
DEBUG=* npm run dev
|
|
||||||
|
|
||||||
# Profiling
|
|
||||||
npm run dev -- --profile
|
|
||||||
|
|
||||||
# Inspection du bundle
|
|
||||||
npm run build -- --debug
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📚 Documentation
|
|
||||||
|
|
||||||
### 📖 Guides Disponibles
|
|
||||||
|
|
||||||
- **[Installation](docs/INSTALLATION.md)** - Guide d'installation complet
|
|
||||||
- **[Configuration](docs/CONFIGURATION.md)** - Configuration avancée
|
|
||||||
- **[Architecture](docs/ARCHITECTURE.md)** - Architecture technique
|
|
||||||
- **[API](docs/API.md)** - Documentation des APIs
|
|
||||||
- **[Sécurité](docs/SECURITY_AUDIT.md)** - Audit de sécurité
|
|
||||||
- **[Utilisation](docs/USAGE.md)** - Guide d'utilisation
|
|
||||||
|
|
||||||
### 🔗 Ressources Externes
|
|
||||||
|
|
||||||
- [Next.js Documentation](https://nextjs.org/docs)
|
|
||||||
- [Tailwind CSS](https://tailwindcss.com/docs)
|
|
||||||
- [Radix UI](https://www.radix-ui.com/docs)
|
|
||||||
- [TypeScript Handbook](https://www.typescriptlang.org/docs/)
|
|
||||||
|
|
||||||
## 🏗️ Architecture
|
|
||||||
|
|
||||||
### 📁 Structure du Projet
|
|
||||||
|
|
||||||
```
|
|
||||||
docv/
|
|
||||||
├── app/ # Pages et routes Next.js 13+
|
|
||||||
│ ├── dashboard/ # Interface utilisateur
|
|
||||||
│ ├── login/ # Authentification
|
|
||||||
│ ├── formation/ # Module formation
|
|
||||||
│ └── contact/ # Contact
|
|
||||||
├── components/ # Composants réutilisables
|
|
||||||
│ ├── ui/ # Composants UI de base
|
|
||||||
│ └── 4nk/ # Composants spécifiques 4NK
|
|
||||||
├── lib/ # Utilitaires et logique métier
|
|
||||||
│ ├── 4nk/ # Modules 4NK
|
|
||||||
│ └── utils.ts # Fonctions utilitaires
|
|
||||||
├── public/ # Assets statiques
|
|
||||||
├── styles/ # Styles globaux
|
|
||||||
└── docs/ # Documentation
|
|
||||||
```
|
|
||||||
|
|
||||||
### 🔧 Technologies Utilisées
|
|
||||||
|
|
||||||
| Technologie | Version | Description |
|
|
||||||
|-------------|---------|-------------|
|
|
||||||
| **Next.js** | 15.2.4 | Framework React full-stack |
|
|
||||||
| **React** | 19.1.1 | Bibliothèque UI |
|
|
||||||
| **TypeScript** | 5.0+ | Typage statique |
|
|
||||||
| **Tailwind CSS** | 4.1.9 | Framework CSS |
|
|
||||||
| **Radix UI** | Latest | Composants accessibles |
|
|
||||||
| **Lucide React** | 0.454.0 | Icônes |
|
|
||||||
| **Zod** | 3.25.67 | Validation de schémas |
|
|
||||||
|
|
||||||
### 🌐 Architecture de Sécurité
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TB
|
|
||||||
A[Client] --> B[Next.js App]
|
|
||||||
B --> C[Authentification Cryptographique]
|
|
||||||
C --> D[Base de Données Locale]
|
|
||||||
D --> E[Chiffrement Bout en Bout]
|
|
||||||
E --> F[Ancrage Blockchain]
|
|
||||||
|
|
||||||
G[IA Locale] --> H[Traitement OCR]
|
|
||||||
H --> I[Classification]
|
|
||||||
I --> J[Extraction de Données]
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔒 Sécurité
|
|
||||||
|
|
||||||
### 🛡️ Mesures de Sécurité Implémentées
|
|
||||||
|
|
||||||
- ✅ **Authentification sans mot de passe** - Clés cryptographiques locales
|
|
||||||
- ✅ **Chiffrement bout en bout** - Données protégées en transit et au repos
|
|
||||||
- ✅ **Aucune interface admin** - Pas d'accès privilégié exposé
|
|
||||||
- ✅ **Conformité réglementaire** - RGPD, ISO 27001, SecNumCloud
|
|
||||||
- ✅ **Audit de sécurité** - Voir [SECURITY_AUDIT.md](docs/SECURITY_AUDIT.md)
|
|
||||||
|
|
||||||
### 🔐 Bonnes Pratiques
|
|
||||||
|
|
||||||
1. **Variables d'environnement** - Jamais de secrets en dur
|
|
||||||
2. **Validation des données** - Schémas Zod pour toutes les entrées
|
|
||||||
3. **HTTPS obligatoire** - En production uniquement
|
|
||||||
4. **Audit régulier** - `npm audit` avant chaque déploiement
|
|
||||||
|
|
||||||
## 🤝 Contribution
|
|
||||||
|
|
||||||
### 🚀 Comment Contribuer
|
|
||||||
|
|
||||||
1. **Fork** le repository
|
|
||||||
2. **Créer** une branche feature (`git checkout -b feature/amazing-feature`)
|
|
||||||
3. **Commit** vos changements (`git commit -m 'Add amazing feature'`)
|
|
||||||
4. **Push** vers la branche (`git push origin feature/amazing-feature`)
|
|
||||||
5. **Ouvrir** une Pull Request
|
|
||||||
|
|
||||||
### 📝 Standards de Code
|
|
||||||
|
|
||||||
- **TypeScript** strict activé
|
|
||||||
- **ESLint** pour la qualité du code
|
|
||||||
- **Prettier** pour le formatage
|
|
||||||
- **Conventional Commits** pour les messages
|
|
||||||
|
|
||||||
### 🧪 Tests
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Avant de contribuer, assurez-vous que :
|
|
||||||
npm run lint # ✅ Pas d'erreurs ESLint
|
|
||||||
npm run type-check # ✅ Pas d'erreurs TypeScript
|
|
||||||
npm run build # ✅ Build réussi
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📞 Support
|
|
||||||
|
|
||||||
### 🆘 Obtenir de l'Aide
|
|
||||||
|
|
||||||
- **📧 Email** : contact@docv.fr
|
|
||||||
- **📚 Documentation** : [docs/](docs/)
|
|
||||||
- **🐛 Issues** : [GitHub Issues](https://github.com/your-org/docv/issues)
|
|
||||||
- **💬 Discussions** : [GitHub Discussions](https://github.com/your-org/docv/discussions)
|
|
||||||
|
|
||||||
### 🏢 Entreprise
|
|
||||||
|
|
||||||
**4NK** - Pionnier du Web 5.0
|
|
||||||
- 🏢 Solutions de souveraineté
|
|
||||||
- 🔒 Sécurité de bout en bout
|
|
||||||
- 🌐 Architecture distribuée
|
|
||||||
|
|
||||||
### 📋 Checklist de Support
|
|
||||||
|
|
||||||
Avant de demander de l'aide, vérifiez :
|
|
||||||
|
|
||||||
- [ ] Version de Node.js compatible (18.0+)
|
|
||||||
- [ ] Dépendances installées (`npm install`)
|
|
||||||
- [ ] Variables d'environnement configurées
|
|
||||||
- [ ] Logs d'erreur consultés
|
|
||||||
- [ ] Documentation parcourue
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📄 Licence
|
|
||||||
|
|
||||||
Ce projet est propriétaire et confidentiel. Tous droits réservés à **4NK**.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
|
|
||||||
**🛡️ DocV - Sécurisez votre entreprise avec la GED simple et souveraine**
|
|
||||||
|
|
||||||
[](https://4nkweb.com)
|
|
||||||
[](mailto:contact@docv.fr)
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|||||||
@ -24,9 +24,9 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
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 { MessageBus } from "@/lib/4nk/MessageBus"
|
||||||
import UserStore from "@/lib/4nk/UserStore"
|
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 +40,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://dev3.4nkweb.com"
|
const iframeUrl = process.env.NEXT_PUBLIC_4NK_IFRAME_URL || "https://dev.4nk.io"
|
||||||
|
|
||||||
const navigation = [
|
const navigation = [
|
||||||
{ name: "Tableau de bord", href: "/dashboard", icon: LayoutDashboard },
|
{ name: "Tableau de bord", href: "/dashboard", icon: LayoutDashboard },
|
||||||
@ -61,10 +61,10 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
|||||||
|
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
// Vérifier si on est en mode mock
|
// Vérifier si on est en mode mock
|
||||||
// const mockMode = messageBus.isInMockMode()
|
const mockMode = messageBus.isInMockMode()
|
||||||
// setIsMockMode(mockMode)
|
setIsMockMode(mockMode)
|
||||||
|
|
||||||
if (true) {
|
if (mockMode) {
|
||||||
console.log("🎭 Dashboard en mode mock")
|
console.log("🎭 Dashboard en mode mock")
|
||||||
setIsAuthenticated(true)
|
setIsAuthenticated(true)
|
||||||
setUserInfo({
|
setUserInfo({
|
||||||
@ -117,7 +117,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
|||||||
const messageBus = MessageBus.getInstance(iframeUrl)
|
const messageBus = MessageBus.getInstance(iframeUrl)
|
||||||
|
|
||||||
userStore.disconnect()
|
userStore.disconnect()
|
||||||
// messageBus.disableMockMode()
|
messageBus.disableMockMode()
|
||||||
|
|
||||||
// Afficher un message de confirmation avec options
|
// Afficher un message de confirmation avec options
|
||||||
setShowLogoutConfirm(true)
|
setShowLogoutConfirm(true)
|
||||||
|
|||||||
@ -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,13 @@ 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://dev3.4nkweb.com"
|
const iframeUrl = process.env.NEXT_PUBLIC_4NK_IFRAME_URL || "https://dev.4nk.io"
|
||||||
const messageBus = MessageBus.getInstance(iframeUrl)
|
const messageBus = MessageBus.getInstance(iframeUrl)
|
||||||
// const mockMode = messageBus.isInMockMode()
|
const mockMode = messageBus.isInMockMode()
|
||||||
// setIsMockMode(mockMode)
|
setIsMockMode(mockMode)
|
||||||
|
|
||||||
// Simuler le chargement des données
|
// Simuler le chargement des données
|
||||||
if (true) {
|
if (mockMode) {
|
||||||
setStats({
|
setStats({
|
||||||
totalDocuments: 1247,
|
totalDocuments: 1247,
|
||||||
totalFolders: 89,
|
totalFolders: 89,
|
||||||
|
|||||||
@ -6,78 +6,134 @@ import { useState } from "react"
|
|||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import {
|
import {
|
||||||
Shield,
|
Shield,
|
||||||
|
Building2,
|
||||||
|
TestTube,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
Home,
|
Home,
|
||||||
|
Key,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
|
AlertTriangle,
|
||||||
|
Eye,
|
||||||
|
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 UserStore from "@/lib/4nk/UserStore"
|
import { MockService } from "@/lib/4nk/MockService"
|
||||||
|
import { UserStore } from "@/lib/4nk/UserStore"
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
|
const [companyId, setCompanyId] = useState("")
|
||||||
const [isAuthModalOpen, setIsAuthModalOpen] = useState(false)
|
const [isAuthModalOpen, setIsAuthModalOpen] = useState(false)
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [isConnected, setIsConnected] = useState(false)
|
const [showPairingSection, setShowPairingSection] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [pairingWords, setPairingWords] = useState(["", "", "", ""])
|
||||||
|
const [pairingError, setPairingError] = useState("")
|
||||||
|
const [pairingSuccess, setPairingSuccess] = useState(false)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const [showPairingInput, setShowPairingInput] = useState(false)
|
||||||
|
|
||||||
const iframeUrl = process.env.NEXT_PUBLIC_4NK_IFRAME_URL || "https://dev3.4nkweb.com"
|
const iframeUrl = process.env.NEXT_PUBLIC_4NK_IFRAME_URL || "https://dev.4nk.io"
|
||||||
|
|
||||||
// Vérifier l'état de connexion au chargement
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
useState(() => {
|
e.preventDefault()
|
||||||
const userStore = UserStore.getInstance()
|
|
||||||
setIsConnected(userStore.isConnected())
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleLogin = () => {
|
if (!companyId.trim()) {
|
||||||
setIsAuthModalOpen(true)
|
return
|
||||||
setError(null)
|
}
|
||||||
}
|
|
||||||
|
setIsLoading(true)
|
||||||
|
|
||||||
const handleAuthSuccess = async () => {
|
|
||||||
setIsAuthModalOpen(false)
|
|
||||||
setIsConnected(true)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Récupérer l'ID d'appairage après connexion
|
// Si l'identifiant est "1234", activer le mode mock directement
|
||||||
const messageBus = MessageBus.getInstance(iframeUrl)
|
if (companyId === "1234") {
|
||||||
await messageBus.isReady()
|
console.log("🎭 Activation du mode mock avec l'identifiant:", companyId)
|
||||||
const pairingId = await messageBus.getUserPairingId()
|
|
||||||
|
const messageBus = MessageBus.getInstance(iframeUrl)
|
||||||
console.log("✅ Authentification 4NK réussie, ID d'appairage:", pairingId)
|
const mockService = MockService.getInstance()
|
||||||
|
const userStore = UserStore.getInstance()
|
||||||
// Redirection vers le dashboard
|
|
||||||
router.push("/dashboard")
|
// Activer le mode mock
|
||||||
} catch (err) {
|
messageBus.enableMockMode()
|
||||||
console.error("Erreur lors de la récupération de l'ID d'appairage:", err)
|
|
||||||
// Redirection quand même vers le dashboard
|
// Authentification mock
|
||||||
router.push("/dashboard")
|
const authResult = await mockService.mockAuthentication(companyId)
|
||||||
|
if (!authResult) {
|
||||||
|
throw new Error("Échec de l'authentification de démonstration")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simuler la récupération des tokens
|
||||||
|
const tokens = await mockService.mockRequestLink()
|
||||||
|
userStore.connect(tokens.accessToken, tokens.refreshToken)
|
||||||
|
|
||||||
|
// Simuler la récupération de l'ID d'appairage
|
||||||
|
const pairingId = await mockService.mockGetUserPairingId()
|
||||||
|
userStore.pair(pairingId)
|
||||||
|
|
||||||
|
console.log("✅ Mode mock activé avec succès")
|
||||||
|
|
||||||
|
// Redirection directe vers le dashboard
|
||||||
|
router.push("/dashboard")
|
||||||
|
} else {
|
||||||
|
// Mode normal - ouvrir la modal d'authentification 4NK
|
||||||
|
setIsAuthModalOpen(true)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de l'activation du mode mock:", error)
|
||||||
|
// En cas d'erreur, ouvrir quand même la modal d'authentification
|
||||||
|
setIsAuthModalOpen(true)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAuthError = (errorMessage: string) => {
|
const handleAuthSuccess = () => {
|
||||||
setError(errorMessage)
|
|
||||||
setIsAuthModalOpen(false)
|
setIsAuthModalOpen(false)
|
||||||
|
router.push("/dashboard")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si déjà connecté, rediriger vers le dashboard
|
const handlePairingSubmit = async (e: React.FormEvent) => {
|
||||||
if (isConnected) {
|
e.preventDefault()
|
||||||
router.push("/dashboard")
|
setPairingError("")
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4">
|
// Vérifier que tous les mots sont remplis
|
||||||
<Card className="w-full max-w-md">
|
if (pairingWords.some((word) => !word.trim())) {
|
||||||
<CardContent className="text-center py-8">
|
setPairingError("Veuillez saisir les 4 mots de pairing")
|
||||||
<CheckCircle className="h-12 w-12 mx-auto text-green-600 mb-4" />
|
return
|
||||||
<h2 className="text-xl font-semibold mb-2">Déjà connecté</h2>
|
}
|
||||||
<p className="text-gray-600">Redirection vers le dashboard...</p>
|
|
||||||
</CardContent>
|
// Simuler la vérification des mots de pairing
|
||||||
</Card>
|
const validWords = ["alpha", "bravo", "charlie", "delta"]
|
||||||
</div>
|
const isValid = pairingWords.every((word, index) => word.toLowerCase().trim() === validWords[index])
|
||||||
)
|
|
||||||
|
if (isValid) {
|
||||||
|
setPairingSuccess(true)
|
||||||
|
setTimeout(() => {
|
||||||
|
// Simuler l'ajout de l'appareil et la connexion
|
||||||
|
const userStore = UserStore.getInstance()
|
||||||
|
const mockService = MockService.getInstance()
|
||||||
|
|
||||||
|
// Simuler des tokens pour le pairing
|
||||||
|
userStore.connect("paired_access_token", "paired_refresh_token")
|
||||||
|
userStore.pair("paired_device_id")
|
||||||
|
|
||||||
|
router.push("/dashboard")
|
||||||
|
}, 2000)
|
||||||
|
} else {
|
||||||
|
setPairingError("Mots de pairing incorrects. Vérifiez les mots saisis sur votre autre appareil.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePairingWordChange = (index: number, value: string) => {
|
||||||
|
const newWords = [...pairingWords]
|
||||||
|
newWords[index] = value
|
||||||
|
setPairingWords(newWords)
|
||||||
|
setPairingError("")
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -103,57 +159,160 @@ 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>
|
||||||
|
|
||||||
{/* Carte de connexion 4NK */}
|
{/* Navigation entre connexion et pairing */}
|
||||||
<Card>
|
<div className="flex space-x-1 bg-gray-100 p-1 rounded-lg">
|
||||||
<CardHeader>
|
<button
|
||||||
<CardTitle className="text-center">
|
onClick={() => setShowPairingSection(false)}
|
||||||
<Shield className="h-8 w-8 mx-auto mb-4 text-blue-600" />
|
className={`flex-1 py-2 px-4 text-sm font-medium rounded-md transition-colors ${
|
||||||
Connexion sécurisée 4NK
|
!showPairingSection ? "bg-white text-gray-900 shadow-sm" : "text-gray-600 hover:text-gray-900"
|
||||||
</CardTitle>
|
}`}
|
||||||
<CardDescription className="text-center">
|
>
|
||||||
Authentification cryptographique sans mot de passe
|
<Building2 className="h-4 w-4 inline mr-2" />
|
||||||
</CardDescription>
|
Connexion
|
||||||
</CardHeader>
|
</button>
|
||||||
<CardContent className="space-y-6">
|
<button
|
||||||
{/* Description de la connexion 4NK */}
|
onClick={() => setShowPairingSection(true)}
|
||||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
className={`flex-1 py-2 px-4 text-sm font-medium rounded-md transition-colors ${
|
||||||
<h3 className="font-semibold text-blue-900 mb-2">🔐 Authentification 4NK</h3>
|
showPairingSection ? "bg-white text-gray-900 shadow-sm" : "text-gray-600 hover:text-gray-900"
|
||||||
<ul className="text-sm text-blue-800 space-y-1">
|
}`}
|
||||||
<li>• Aucun mot de passe requis</li>
|
>
|
||||||
<li>• Identité cryptographique sécurisée</li>
|
<Key className="h-4 w-4 inline mr-2" />
|
||||||
<li>• Chiffrement bout en bout</li>
|
Pairing
|
||||||
<li>• Protection par blockchain</li>
|
</button>
|
||||||
</ul>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Affichage des erreurs */}
|
{!showPairingSection ? (
|
||||||
{error && (
|
/* Carte de connexion */
|
||||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
<Card>
|
||||||
<p className="text-red-700 font-medium">Erreur de connexion :</p>
|
<CardHeader>
|
||||||
<p className="text-red-600 text-sm">{error}</p>
|
<CardTitle className="flex items-center">
|
||||||
</div>
|
<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>
|
||||||
|
|
||||||
{/* Bouton de connexion */}
|
{/* Info mode démonstration */}
|
||||||
<Button
|
<div className="bg-green-50 border border-green-200 rounded-lg p-3">
|
||||||
onClick={handleLogin}
|
<div className="flex items-start space-x-2">
|
||||||
className="w-full"
|
<TestTube className="h-4 w-4 text-green-600 mt-0.5 flex-shrink-0" />
|
||||||
size="lg"
|
<div className="text-xs text-green-700">
|
||||||
disabled={isLoading}
|
<p className="font-medium mb-1">Mode démonstration</p>
|
||||||
>
|
<p>
|
||||||
<Shield className="h-5 w-5 mr-2" />
|
Utilisez l'identifiant <strong>"1234"</strong> pour accéder directement aux écrans de
|
||||||
{isLoading ? "Connexion en cours..." : "Se connecter avec 4NK"}
|
démonstration avec des données simulées.
|
||||||
</Button>
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Informations sur l'iframe */}
|
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||||
<div className="bg-gray-50 border border-gray-200 rounded-lg p-3">
|
{isLoading ? "Connexion en cours..." : "Se connecter"}
|
||||||
<p className="text-xs text-gray-600 text-center">
|
</Button>
|
||||||
<strong>URL d'authentification :</strong><br />
|
</form>
|
||||||
{iframeUrl}
|
</CardContent>
|
||||||
</p>
|
</Card>
|
||||||
</div>
|
) : (
|
||||||
</CardContent>
|
/* Carte de pairing */
|
||||||
</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 é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">
|
||||||
|
|||||||
@ -1,101 +1,330 @@
|
|||||||
import { useState, useEffect, memo } from 'react';
|
"use client"
|
||||||
import Iframe from './Iframe';
|
|
||||||
import MessageBus from '@/lib/4nk/MessageBus';
|
import { useState, useEffect, memo } from "react"
|
||||||
import Loader from '@/lib/4nk/Loader';
|
import { Button } from "@/components/ui/button"
|
||||||
import Modal from '../modal/Modal';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
import { Shield, CheckCircle, Loader2, AlertCircle, RefreshCw } from "lucide-react"
|
||||||
|
import { Iframe } from "./Iframe"
|
||||||
|
import { MessageBus } from "@/lib/4nk/MessageBus"
|
||||||
|
import { IframeReference } from "@/lib/4nk/IframeReference"
|
||||||
|
|
||||||
interface AuthModalProps {
|
interface AuthModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean
|
||||||
onConnect: () => void;
|
onConnect: () => void
|
||||||
onClose: () => void;
|
onClose: () => void
|
||||||
iframeUrl: string;
|
iframeUrl: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function AuthModal({ isOpen, onConnect, onClose, iframeUrl }: AuthModalProps) {
|
export const AuthModal = memo(function AuthModal({ isOpen, onConnect, onClose, iframeUrl }: AuthModalProps) {
|
||||||
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)
|
||||||
|
|
||||||
useEffect(() => {
|
const maxRetries = 3
|
||||||
MessageBus.getInstance(iframeUrl).isReady().then(() => {
|
|
||||||
setIsIframeReady(true);
|
|
||||||
});
|
|
||||||
}, [iframeUrl]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
setShowIframe(false);
|
// Reset des états à la fermeture
|
||||||
setIsIframeReady(false);
|
setIsIframeReady(false)
|
||||||
setAuthSuccess(false);
|
setShowIframe(false)
|
||||||
|
setAuthSuccess(false)
|
||||||
|
setIsLoading(false)
|
||||||
|
setError(null)
|
||||||
|
setLoadingStep("")
|
||||||
|
setRetryCount(0)
|
||||||
|
setIframeLoaded(false)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
initAuth()
|
||||||
if (isIframeReady && !showIframe) {
|
}, [isOpen, iframeUrl, retryCount])
|
||||||
setShowIframe(true);
|
|
||||||
|
|
||||||
MessageBus.getInstance(iframeUrl).requestLink().then(() => {
|
const initAuth = async () => {
|
||||||
setAuthSuccess(true);
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
|
setError(null)
|
||||||
|
setLoadingStep("Initialisation...")
|
||||||
|
|
||||||
setTimeout(() => onConnect(), 500);
|
console.log("🔗 Initialisation authentification 4NK avec:", iframeUrl)
|
||||||
}).catch((_error: string) => {
|
console.log("🔄 Tentative:", retryCount + 1, "/", maxRetries + 1)
|
||||||
setShowIframe(false);
|
|
||||||
setIsIframeReady(false);
|
|
||||||
setAuthSuccess(false);
|
|
||||||
|
|
||||||
onClose();
|
// É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)
|
||||||
|
|
||||||
|
// Étape 3: Attendre le message LISTENING de l'iframe
|
||||||
|
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)
|
||||||
|
|
||||||
|
// 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]);
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||||
isOpen={isOpen}
|
<Card className="w-full max-w-md">
|
||||||
onClose={onClose}
|
<CardHeader className="text-center">
|
||||||
title='Authentification 4nk'
|
<div className="flex items-center justify-center mb-4">
|
||||||
>
|
<Shield className="h-12 w-12 text-blue-600" />
|
||||||
{!isIframeReady && (
|
|
||||||
<div style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
height: '400px',
|
|
||||||
gap: 16
|
|
||||||
}}>
|
|
||||||
<Loader width={40} />
|
|
||||||
<div style={{ fontWeight: 600, fontSize: 18 }}>Chargement de l'authentification...</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{authSuccess ? (
|
|
||||||
<div style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
height: '400px',
|
|
||||||
gap: 20
|
|
||||||
}}>
|
|
||||||
<div style={{ fontWeight: 600, fontSize: 18, color: '#43a047' }}>
|
|
||||||
Authentification réussie !
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<CardTitle className="text-2xl">Authentification 4NK</CardTitle>
|
||||||
) : (
|
<CardDescription>Connexion sécurisée avec votre identité cryptographique</CardDescription>
|
||||||
<div style={{
|
</CardHeader>
|
||||||
display: showIframe ? 'flex' : 'none',
|
<CardContent className="space-y-6">
|
||||||
justifyContent: 'center',
|
{error && (
|
||||||
alignItems: 'center',
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||||
width: '100%'
|
<div className="flex items-start space-x-2">
|
||||||
}}>
|
<AlertCircle className="h-5 w-5 text-red-600 mt-0.5 flex-shrink-0" />
|
||||||
<Iframe
|
<div className="flex-1">
|
||||||
iframeUrl={iframeUrl}
|
<p className="text-red-700 text-sm font-medium mb-2">Erreur de connexion</p>
|
||||||
showIframe={showIframe}
|
<p className="text-red-600 text-xs mb-3">{error}</p>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AuthModal.displayName = 'AuthModal';
|
<div className="flex flex-col gap-2">
|
||||||
export default memo(AuthModal);
|
{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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<Button variant="outline" onClick={onClose}>
|
||||||
|
Annuler
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|||||||
@ -1,32 +1,38 @@
|
|||||||
import { useRef, useEffect, memo } from 'react';
|
"use client"
|
||||||
import IframeReference from '@/lib/4nk/IframeReference';
|
|
||||||
|
|
||||||
function Iframe({ iframeUrl, showIframe = false }: { iframeUrl: string; showIframe?: boolean }) {
|
import { useRef, useEffect, memo } from "react"
|
||||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
import { IframeReference } from "@/lib/4nk/IframeReference"
|
||||||
|
|
||||||
|
interface IframeProps {
|
||||||
|
iframeUrl: string
|
||||||
|
showIframe?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Iframe = memo(function Iframe({ iframeUrl, showIframe = false }: IframeProps) {
|
||||||
|
const iframeRef = useRef<HTMLIFrameElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (iframeRef.current) {
|
if (iframeRef.current) {
|
||||||
IframeReference.setIframe(iframeRef.current);
|
IframeReference.setIframe(iframeRef.current)
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
IframeReference.setIframe(null);
|
IframeReference.setIframe(null)
|
||||||
};
|
}
|
||||||
}, [iframeRef.current]);
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<iframe
|
<iframe
|
||||||
ref={iframeRef}
|
ref={iframeRef}
|
||||||
src={iframeUrl}
|
src={iframeUrl}
|
||||||
style={{
|
style={{
|
||||||
display: showIframe ? 'block' : 'none',
|
width: showIframe ? "100%" : "0",
|
||||||
width: '400px',
|
height: showIframe ? "400px" : "0",
|
||||||
height: '400px',
|
border: "none",
|
||||||
border: 'none',
|
display: showIframe ? "block" : "none",
|
||||||
overflow: 'hidden'
|
|
||||||
}}
|
}}
|
||||||
|
sandbox="allow-scripts allow-same-origin allow-forms"
|
||||||
|
title="4NK Authentication"
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
Iframe.displayName = 'Iframe';
|
|
||||||
export default memo(Iframe);
|
|
||||||
|
|||||||
@ -1,163 +0,0 @@
|
|||||||
.modal-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
background: rgba(35, 36, 42, 0.82);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 1000;
|
|
||||||
animation: modal-fadein 0.33s cubic-bezier(.4, 0, .2, 1);
|
|
||||||
backdrop-filter: blur(3.5px);
|
|
||||||
-webkit-backdrop-filter: blur(3.5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@keyframes modal-fadein {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-container {
|
|
||||||
background: #23242a;
|
|
||||||
border-radius: 18px;
|
|
||||||
min-width: 340px;
|
|
||||||
max-width: 95vw;
|
|
||||||
min-height: 0;
|
|
||||||
padding: 0 0 24px 0;
|
|
||||||
position: relative;
|
|
||||||
box-shadow: 0 12px 48px 0 rgba(0, 0, 0, 0.34), 0 2px 12px 0 rgba(30, 34, 44, 0.10);
|
|
||||||
overflow: hidden;
|
|
||||||
animation: modal-popin 0.34s cubic-bezier(.4, 0, .2, 1);
|
|
||||||
transition: box-shadow 0.2s, opacity 0.25s cubic-bezier(.4, 0, .2, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-container.modal-closing {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(32px) scale(0.97);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@keyframes modal-popin {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(32px) scale(0.97);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0) scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header {
|
|
||||||
background: linear-gradient(90deg, #23242a 85%, #23242aEE 100%);
|
|
||||||
color: #fff;
|
|
||||||
padding: 22px 30px 14px 30px;
|
|
||||||
border-radius: 18px 18px 0 0;
|
|
||||||
box-shadow: 0 2px 12px 0 rgba(30, 34, 44, 0.06);
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 52px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.modal-header h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.3rem;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0.01em;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-close {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
right: 16px;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
font-size: 2rem;
|
|
||||||
color: #e3e4e8;
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
min-width: 36px;
|
|
||||||
min-height: 36px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.18s, color 0.18s;
|
|
||||||
z-index: 2;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-close svg {
|
|
||||||
display: block;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
background: none;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-close:hover,
|
|
||||||
.modal-close:focus {
|
|
||||||
background: rgba(255, 255, 255, 0.10);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-close:active {
|
|
||||||
background: rgba(67, 160, 71, 0.13);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-close:active {
|
|
||||||
background: rgba(67, 160, 71, 0.13);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.modal-body {
|
|
||||||
padding: 28px 28px 0 28px;
|
|
||||||
max-height: 70vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
color: #e3e4e8;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.modal-container {
|
|
||||||
min-width: 0;
|
|
||||||
width: 98vw;
|
|
||||||
padding: 0 0 12px 0;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header {
|
|
||||||
padding: 16px 10px 10px 14px;
|
|
||||||
border-radius: 12px 12px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body {
|
|
||||||
padding: 14px 8px 0 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-close {
|
|
||||||
top: 6px;
|
|
||||||
right: 6px;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
import React, { memo } from 'react';
|
|
||||||
import './Modal.css';
|
|
||||||
|
|
||||||
interface ModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
title?: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Modal({ isOpen, onClose, title, children }: ModalProps) {
|
|
||||||
if (!isOpen) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className="modal-overlay modal-fadein">
|
|
||||||
<div className="modal-container modal-popin">
|
|
||||||
<button className="close-button modal-close" onClick={onClose} aria-label="Fermer">
|
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
||||||
<path d="M6 6L18 18M18 6L6 18" stroke="#fff" strokeWidth="2.4" strokeLinecap="round" filter="url(#shadow)" />
|
|
||||||
<defs>
|
|
||||||
<filter id="shadow" x="-2" y="-2" width="28" height="28" filterUnits="userSpaceOnUse">
|
|
||||||
<feDropShadow dx="0" dy="0" stdDeviation="1.2" floodColor="#23242a" />
|
|
||||||
</filter>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
{title && <div className="modal-header modal-header"><h2>{title}</h2></div>}
|
|
||||||
<div className="modal-body modal-body">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Modal.displayName = 'Modal';
|
|
||||||
export default memo(Modal);
|
|
||||||
@ -1,33 +1,47 @@
|
|||||||
export default class EventBus {
|
/**
|
||||||
private static instance: EventBus;
|
* EventBus - Bus d'événements intra-onglet pour la communication interne
|
||||||
private listeners: Record<string, Array<(...args: any[]) => void>> = {};
|
* Pattern Singleton avec pub/sub
|
||||||
|
*/
|
||||||
|
export class EventBus {
|
||||||
|
private static instance: EventBus | null = null
|
||||||
|
private listeners: Map<string, Array<(...args: any[]) => void>> = new Map()
|
||||||
|
|
||||||
private constructor() { }
|
private constructor() {}
|
||||||
|
|
||||||
public static getInstance(): EventBus {
|
static getInstance(): EventBus {
|
||||||
if (!EventBus.instance) {
|
if (!EventBus.instance) {
|
||||||
EventBus.instance = new EventBus();
|
EventBus.instance = new EventBus()
|
||||||
}
|
}
|
||||||
return EventBus.instance;
|
return EventBus.instance
|
||||||
}
|
}
|
||||||
|
|
||||||
public on(event: string, callback: (...args: any[]) => void): () => void {
|
on(event: string, callback: (...args: any[]) => void): () => void {
|
||||||
if (!this.listeners[event]) {
|
if (!this.listeners.has(event)) {
|
||||||
this.listeners[event] = [];
|
this.listeners.set(event, [])
|
||||||
}
|
}
|
||||||
this.listeners[event].push(callback);
|
|
||||||
|
const eventListeners = this.listeners.get(event)!
|
||||||
|
eventListeners.push(callback)
|
||||||
|
|
||||||
|
// Retourne une fonction d'unsubscribe
|
||||||
return () => {
|
return () => {
|
||||||
if (this.listeners[event]) {
|
const index = eventListeners.indexOf(callback)
|
||||||
this.listeners[event] = this.listeners[event].filter(cb => cb !== callback);
|
if (index > -1) {
|
||||||
|
eventListeners.splice(index, 1)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public emit(event: string, ...args: any[]): void {
|
emit(event: string, ...args: any[]): void {
|
||||||
if (this.listeners[event]) {
|
const eventListeners = this.listeners.get(event)
|
||||||
this.listeners[event].forEach(callback => {
|
if (eventListeners) {
|
||||||
callback(...args);
|
eventListeners.forEach((callback) => {
|
||||||
});
|
try {
|
||||||
|
callback(...args)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error in event listener for ${event}:`, error)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
export default class IframeReference {
|
/**
|
||||||
private static iframe: HTMLIFrameElement | null = null;
|
* 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
|
||||||
|
|
||||||
private constructor() { }
|
static setIframe(iframe: HTMLIFrameElement | null): void {
|
||||||
|
IframeReference.iframe = iframe
|
||||||
public static setIframe(iframe: HTMLIFrameElement | null): void {
|
|
||||||
this.iframe = iframe;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static getIframe(): HTMLIFrameElement | null {
|
static getIframe(): HTMLIFrameElement | null {
|
||||||
return this.iframe;
|
return IframeReference.iframe
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,23 +0,0 @@
|
|||||||
import { memo } from 'react';
|
|
||||||
|
|
||||||
function Loader({ width = 40 }: { width?: number }) {
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: width }}>
|
|
||||||
<div
|
|
||||||
className='loader'
|
|
||||||
style={{
|
|
||||||
width,
|
|
||||||
height: width,
|
|
||||||
border: '4px solid #eee',
|
|
||||||
borderTop: '4px solid #333',
|
|
||||||
borderRadius: '50%',
|
|
||||||
animation: 'spin 1s linear infinite',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<style>{`@keyframes spin { 0% { transform: rotate(0deg);} 100% { transform: rotate(360deg);} }`}</style>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader.displayName = 'Loader';
|
|
||||||
export default memo(Loader);
|
|
||||||
File diff suppressed because it is too large
Load Diff
363
lib/4nk/MockService.ts
Normal file
363
lib/4nk/MockService.ts
Normal file
@ -0,0 +1,363 @@
|
|||||||
|
/**
|
||||||
|
* MockService - Service de simulation 4NK pour les tests et développement
|
||||||
|
* S'active avec l'identifiant d'entreprise "1234"
|
||||||
|
*/
|
||||||
|
export class MockService {
|
||||||
|
private static instance: MockService | null = null
|
||||||
|
private isAuthenticated = false
|
||||||
|
private mockTokens = {
|
||||||
|
accessToken: "mock_access_token_1234567890",
|
||||||
|
refreshToken: "mock_refresh_token_0987654321",
|
||||||
|
}
|
||||||
|
private mockUserPairingId = "mock_pairing_id_abcdef123456"
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
static getInstance(): MockService {
|
||||||
|
if (!MockService.instance) {
|
||||||
|
MockService.instance = new MockService()
|
||||||
|
}
|
||||||
|
return MockService.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
// Données mockées
|
||||||
|
private mockProcesses = [
|
||||||
|
{
|
||||||
|
id: "process_001",
|
||||||
|
type: "document",
|
||||||
|
name: "Contrat de travail - Jean Dupont",
|
||||||
|
status: "validated",
|
||||||
|
created_at: "2024-01-15T10:30:00Z",
|
||||||
|
updated_at: "2024-01-15T14:20:00Z",
|
||||||
|
owner: "user_001",
|
||||||
|
states: [
|
||||||
|
{
|
||||||
|
state_id: "state_001_001",
|
||||||
|
timestamp: "2024-01-15T10:30:00Z",
|
||||||
|
status: "created",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state_id: "state_001_002",
|
||||||
|
timestamp: "2024-01-15T14:20:00Z",
|
||||||
|
status: "validated",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "process_002",
|
||||||
|
type: "folder",
|
||||||
|
name: "Dossier RH - Marie Martin",
|
||||||
|
status: "pending",
|
||||||
|
created_at: "2024-01-16T09:15:00Z",
|
||||||
|
updated_at: "2024-01-16T16:45:00Z",
|
||||||
|
owner: "user_002",
|
||||||
|
states: [
|
||||||
|
{
|
||||||
|
state_id: "state_002_001",
|
||||||
|
timestamp: "2024-01-16T09:15:00Z",
|
||||||
|
status: "created",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "process_003",
|
||||||
|
type: "document",
|
||||||
|
name: "Facture SARL TechCorp",
|
||||||
|
status: "signed",
|
||||||
|
created_at: "2024-01-17T11:00:00Z",
|
||||||
|
updated_at: "2024-01-17T17:30:00Z",
|
||||||
|
owner: "user_001",
|
||||||
|
states: [
|
||||||
|
{
|
||||||
|
state_id: "state_003_001",
|
||||||
|
timestamp: "2024-01-17T11:00:00Z",
|
||||||
|
status: "created",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state_id: "state_003_002",
|
||||||
|
timestamp: "2024-01-17T17:30:00Z",
|
||||||
|
status: "signed",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "process_004",
|
||||||
|
type: "document",
|
||||||
|
name: "Rapport d'audit 2024",
|
||||||
|
status: "draft",
|
||||||
|
created_at: "2024-01-18T08:45:00Z",
|
||||||
|
updated_at: "2024-01-18T08:45:00Z",
|
||||||
|
owner: "user_003",
|
||||||
|
states: [
|
||||||
|
{
|
||||||
|
state_id: "state_004_001",
|
||||||
|
timestamp: "2024-01-18T08:45:00Z",
|
||||||
|
status: "draft",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "process_005",
|
||||||
|
type: "folder",
|
||||||
|
name: "Dossier Comptabilité Q1",
|
||||||
|
status: "validated",
|
||||||
|
created_at: "2024-01-10T14:20:00Z",
|
||||||
|
updated_at: "2024-01-19T10:15:00Z",
|
||||||
|
owner: "user_001",
|
||||||
|
states: [
|
||||||
|
{
|
||||||
|
state_id: "state_005_001",
|
||||||
|
timestamp: "2024-01-10T14:20:00Z",
|
||||||
|
status: "created",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state_id: "state_005_002",
|
||||||
|
timestamp: "2024-01-19T10:15:00Z",
|
||||||
|
status: "validated",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
private mockMyProcesses = ["process_001", "process_003", "process_005"]
|
||||||
|
|
||||||
|
private mockProcessData = {
|
||||||
|
process_001: {
|
||||||
|
employee_name: "Jean Dupont",
|
||||||
|
employee_email: "jean.dupont@example.com",
|
||||||
|
position: "Développeur Senior",
|
||||||
|
salary: "45000",
|
||||||
|
start_date: "2024-02-01",
|
||||||
|
contract_type: "CDI",
|
||||||
|
},
|
||||||
|
process_002: {
|
||||||
|
employee_name: "Marie Martin",
|
||||||
|
employee_email: "marie.martin@example.com",
|
||||||
|
documents_count: 12,
|
||||||
|
last_review: "2023-12-15",
|
||||||
|
department: "Ressources Humaines",
|
||||||
|
},
|
||||||
|
process_003: {
|
||||||
|
client_name: "SARL TechCorp",
|
||||||
|
amount: "15750.00",
|
||||||
|
invoice_number: "INV-2024-001",
|
||||||
|
due_date: "2024-02-15",
|
||||||
|
status: "paid",
|
||||||
|
},
|
||||||
|
process_004: {
|
||||||
|
audit_year: "2024",
|
||||||
|
auditor: "Cabinet Expertise",
|
||||||
|
scope: "Audit financier complet",
|
||||||
|
completion: "25%",
|
||||||
|
},
|
||||||
|
process_005: {
|
||||||
|
quarter: "Q1 2024",
|
||||||
|
documents_count: 45,
|
||||||
|
total_amount: "125000.00",
|
||||||
|
status: "closed",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulation de l'authentification
|
||||||
|
async mockAuthentication(companyId: string): Promise<boolean> {
|
||||||
|
console.log("🎭 Mock authentication for company:", companyId)
|
||||||
|
|
||||||
|
// Simuler un délai d'authentification
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1500))
|
||||||
|
|
||||||
|
if (companyId === "1234") {
|
||||||
|
this.isAuthenticated = true
|
||||||
|
console.log("✅ Mock authentication successful")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("❌ Mock authentication failed - invalid company ID")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulation des méthodes 4NK
|
||||||
|
async mockRequestLink(): Promise<{ accessToken: string; refreshToken: string }> {
|
||||||
|
console.log("🎭 Mock REQUEST_LINK")
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
|
|
||||||
|
if (!this.isAuthenticated) {
|
||||||
|
throw new Error("Not authenticated")
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.mockTokens
|
||||||
|
}
|
||||||
|
|
||||||
|
async mockGetUserPairingId(): Promise<string> {
|
||||||
|
console.log("🎭 Mock GET_PAIRING_ID")
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||||
|
|
||||||
|
if (!this.isAuthenticated) {
|
||||||
|
throw new Error("Not authenticated")
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.mockUserPairingId
|
||||||
|
}
|
||||||
|
|
||||||
|
async mockGetProcesses(): Promise<any[]> {
|
||||||
|
console.log("🎭 Mock GET_PROCESSES")
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 800))
|
||||||
|
|
||||||
|
if (!this.isAuthenticated) {
|
||||||
|
throw new Error("Not authenticated")
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.mockProcesses
|
||||||
|
}
|
||||||
|
|
||||||
|
async mockGetMyProcesses(): Promise<string[]> {
|
||||||
|
console.log("🎭 Mock GET_MY_PROCESSES")
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 600))
|
||||||
|
|
||||||
|
if (!this.isAuthenticated) {
|
||||||
|
throw new Error("Not authenticated")
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.mockMyProcesses
|
||||||
|
}
|
||||||
|
|
||||||
|
async mockGetData(processId: string, stateId: string): Promise<Record<string, any>> {
|
||||||
|
console.log("🎭 Mock RETRIEVE_DATA for process:", processId)
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 700))
|
||||||
|
|
||||||
|
if (!this.isAuthenticated) {
|
||||||
|
throw new Error("Not authenticated")
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.mockProcessData[processId as keyof typeof this.mockProcessData] || {}
|
||||||
|
}
|
||||||
|
|
||||||
|
async mockCreateProcess(processData: any, privateFields: string[], roles: any): Promise<any> {
|
||||||
|
console.log("🎭 Mock CREATE_PROCESS")
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1200))
|
||||||
|
|
||||||
|
if (!this.isAuthenticated) {
|
||||||
|
throw new Error("Not authenticated")
|
||||||
|
}
|
||||||
|
|
||||||
|
const newProcessId = `process_${Date.now()}`
|
||||||
|
const newProcess = {
|
||||||
|
id: newProcessId,
|
||||||
|
type: processData.type || "document",
|
||||||
|
name: processData.name || "Nouveau process",
|
||||||
|
status: "created",
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
owner: "current_user",
|
||||||
|
states: [
|
||||||
|
{
|
||||||
|
state_id: `state_${newProcessId}_001`,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
status: "created",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajouter aux processes mockés
|
||||||
|
this.mockProcesses.push(newProcess)
|
||||||
|
this.mockMyProcesses.push(newProcessId)
|
||||||
|
this.mockProcessData[newProcessId as keyof typeof this.mockProcessData] = processData
|
||||||
|
|
||||||
|
return {
|
||||||
|
processId: newProcessId,
|
||||||
|
process: newProcess,
|
||||||
|
processData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async mockNotifyProcessUpdate(processId: string, stateId: string): Promise<void> {
|
||||||
|
console.log("🎭 Mock NOTIFY_UPDATE for process:", processId)
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||||
|
|
||||||
|
if (!this.isAuthenticated) {
|
||||||
|
throw new Error("Not authenticated")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simuler la notification
|
||||||
|
console.log("✅ Mock notification sent")
|
||||||
|
}
|
||||||
|
|
||||||
|
async mockValidateState(processId: string, stateId: string): Promise<any> {
|
||||||
|
console.log("🎭 Mock VALIDATE_STATE for process:", processId)
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 800))
|
||||||
|
|
||||||
|
if (!this.isAuthenticated) {
|
||||||
|
throw new Error("Not authenticated")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trouver le process et mettre à jour son statut
|
||||||
|
const process = this.mockProcesses.find((p) => p.id === processId)
|
||||||
|
if (process) {
|
||||||
|
process.status = "validated"
|
||||||
|
process.updated_at = new Date().toISOString()
|
||||||
|
process.states.push({
|
||||||
|
state_id: `${stateId}_validated`,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
status: "validated",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return process
|
||||||
|
}
|
||||||
|
|
||||||
|
async mockValidateToken(): Promise<boolean> {
|
||||||
|
console.log("🎭 Mock VALIDATE_TOKEN")
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 300))
|
||||||
|
return this.isAuthenticated
|
||||||
|
}
|
||||||
|
|
||||||
|
async mockRenewToken(): Promise<{ accessToken: string; refreshToken: string }> {
|
||||||
|
console.log("🎭 Mock RENEW_TOKEN")
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||||
|
|
||||||
|
if (!this.isAuthenticated) {
|
||||||
|
throw new Error("Not authenticated")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Générer de nouveaux tokens mockés
|
||||||
|
this.mockTokens = {
|
||||||
|
accessToken: `mock_access_token_${Date.now()}`,
|
||||||
|
refreshToken: `mock_refresh_token_${Date.now()}`,
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.mockTokens
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utilitaires
|
||||||
|
isInMockMode(): boolean {
|
||||||
|
return this.isAuthenticated
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect(): void {
|
||||||
|
this.isAuthenticated = false
|
||||||
|
console.log("🎭 Mock disconnected")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Données supplémentaires pour les écrans
|
||||||
|
getMockStats() {
|
||||||
|
return {
|
||||||
|
totalDocuments: this.mockProcesses.filter((p) => p.type === "document").length,
|
||||||
|
totalFolders: this.mockProcesses.filter((p) => p.type === "folder").length,
|
||||||
|
myProcesses: this.mockMyProcesses.length,
|
||||||
|
pendingValidations: this.mockProcesses.filter((p) => p.status === "pending" || p.status === "draft").length,
|
||||||
|
validatedProcesses: this.mockProcesses.filter((p) => p.status === "validated").length,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getMockRecentActivity() {
|
||||||
|
return this.mockProcesses
|
||||||
|
.sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime())
|
||||||
|
.slice(0, 5)
|
||||||
|
.map((process) => ({
|
||||||
|
id: process.id,
|
||||||
|
name: process.name,
|
||||||
|
type: process.type,
|
||||||
|
status: process.status,
|
||||||
|
updated_at: process.updated_at,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,43 +1,59 @@
|
|||||||
export default class UserStore {
|
/**
|
||||||
private static instance: UserStore;
|
* 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
|
||||||
|
|
||||||
private constructor() { }
|
private constructor() {}
|
||||||
|
|
||||||
public static getInstance(): UserStore {
|
static getInstance(): UserStore {
|
||||||
if (!UserStore.instance) {
|
if (!UserStore.instance) {
|
||||||
UserStore.instance = new UserStore();
|
UserStore.instance = new UserStore()
|
||||||
}
|
}
|
||||||
return UserStore.instance;
|
return UserStore.instance
|
||||||
}
|
}
|
||||||
|
|
||||||
public connect(accessToken: string, refreshToken: string): void {
|
connect(accessToken: string, refreshToken: string): void {
|
||||||
sessionStorage.setItem('accessToken', accessToken);
|
if (typeof window !== "undefined") {
|
||||||
sessionStorage.setItem('refreshToken', refreshToken);
|
sessionStorage.setItem("4nk_access_token", accessToken)
|
||||||
|
sessionStorage.setItem("4nk_refresh_token", refreshToken)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public isConnected(): boolean {
|
isConnected(): boolean {
|
||||||
return sessionStorage.getItem('accessToken') !== null && sessionStorage.getItem('refreshToken') !== null;
|
if (typeof window === "undefined") return false
|
||||||
|
const accessToken = sessionStorage.getItem("4nk_access_token")
|
||||||
|
const refreshToken = sessionStorage.getItem("4nk_refresh_token")
|
||||||
|
return !!(accessToken && refreshToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
public disconnect(): void {
|
disconnect(): void {
|
||||||
sessionStorage.removeItem('accessToken');
|
if (typeof window !== "undefined") {
|
||||||
sessionStorage.removeItem('refreshToken');
|
sessionStorage.removeItem("4nk_access_token")
|
||||||
sessionStorage.removeItem('userPairingId');
|
sessionStorage.removeItem("4nk_refresh_token")
|
||||||
|
sessionStorage.removeItem("4nk_user_pairing_id")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAccessToken(): string | null {
|
getAccessToken(): string | null {
|
||||||
return sessionStorage.getItem('accessToken');
|
if (typeof window === "undefined") return null
|
||||||
|
return sessionStorage.getItem("4nk_access_token")
|
||||||
}
|
}
|
||||||
|
|
||||||
public getRefreshToken(): string | null {
|
getRefreshToken(): string | null {
|
||||||
return sessionStorage.getItem('refreshToken');
|
if (typeof window === "undefined") return null
|
||||||
|
return sessionStorage.getItem("4nk_refresh_token")
|
||||||
}
|
}
|
||||||
|
|
||||||
public pair(userPairingId: string): void {
|
pair(userPairingId: string): void {
|
||||||
sessionStorage.setItem('userPairingId', userPairingId);
|
if (typeof window !== "undefined") {
|
||||||
|
sessionStorage.setItem("4nk_user_pairing_id", userPairingId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getUserPairingId(): string | null {
|
getUserPairingId(): string | null {
|
||||||
return sessionStorage.getItem('userPairingId');
|
if (typeof window === "undefined") return null
|
||||||
|
return sessionStorage.getItem("4nk_user_pairing_id")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +0,0 @@
|
|||||||
export interface FileBlob {
|
|
||||||
type: string,
|
|
||||||
data: Uint8Array
|
|
||||||
};
|
|
||||||
|
|
||||||
export function isFileBlob(data: any): data is FileBlob {
|
|
||||||
return (
|
|
||||||
typeof data === 'object' &&
|
|
||||||
data !== null &&
|
|
||||||
'type' in data &&
|
|
||||||
typeof data.type === 'string' &&
|
|
||||||
'data' in data &&
|
|
||||||
data.data instanceof Uint8Array
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,137 +0,0 @@
|
|||||||
import { isFileBlob, type FileBlob } from "./Data";
|
|
||||||
import type { RoleDefinition } from "./Roles";
|
|
||||||
|
|
||||||
export interface FolderData {
|
|
||||||
folderNumber: string;
|
|
||||||
name: string;
|
|
||||||
deedType: string;
|
|
||||||
description: string;
|
|
||||||
archived_description: string;
|
|
||||||
status: string;
|
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
customers: string[];
|
|
||||||
documents: FileBlob[];
|
|
||||||
motes: string[];
|
|
||||||
stakeholders: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isFolderData(data: any): data is FolderData {
|
|
||||||
if (typeof data !== 'object' || data === null) return false;
|
|
||||||
|
|
||||||
const requiredStringFields = [
|
|
||||||
'folderNumber',
|
|
||||||
'name',
|
|
||||||
'deedType',
|
|
||||||
'description',
|
|
||||||
'archived_description',
|
|
||||||
'status',
|
|
||||||
'created_at',
|
|
||||||
'updated_at'
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const field of requiredStringFields) {
|
|
||||||
if (typeof data[field] !== 'string') return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const requiredArrayFields = [
|
|
||||||
'customers',
|
|
||||||
'motes',
|
|
||||||
'stakeholders'
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const field of requiredArrayFields) {
|
|
||||||
if (!Array.isArray(data[field]) || !data[field].every((item: any) => typeof item === 'string')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const requiredFileBlobArrayFields = [
|
|
||||||
'documents',
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const field of requiredFileBlobArrayFields) {
|
|
||||||
if (!Array.isArray(data[field])) return false;
|
|
||||||
if (data[field].length > 0 && !data[field].every(isFileBlob)) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const emptyFolderData: FolderData = {
|
|
||||||
folderNumber: '',
|
|
||||||
name: '',
|
|
||||||
deedType: '',
|
|
||||||
description: '',
|
|
||||||
archived_description: '',
|
|
||||||
status: '',
|
|
||||||
created_at: '',
|
|
||||||
updated_at: '',
|
|
||||||
customers: [],
|
|
||||||
documents: [],
|
|
||||||
motes: [],
|
|
||||||
stakeholders: []
|
|
||||||
};
|
|
||||||
|
|
||||||
const folderDataFields: string[] = Object.keys(emptyFolderData);
|
|
||||||
|
|
||||||
const FolderPublicFields: string[] = [];
|
|
||||||
|
|
||||||
// All the attributes are private in that case
|
|
||||||
export const FolderPrivateFields = [
|
|
||||||
...folderDataFields.filter(key => !FolderPublicFields.includes(key))
|
|
||||||
];
|
|
||||||
|
|
||||||
export interface FolderCreated {
|
|
||||||
processId: string,
|
|
||||||
process: any, // Process
|
|
||||||
folderData: FolderData,
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setDefaultFolderRoles(ownerId: string, stakeholdersId: string[], customersId: string[]): Record<string, RoleDefinition> {
|
|
||||||
return {
|
|
||||||
demiurge: {
|
|
||||||
members: [ownerId],
|
|
||||||
validation_rules: [],
|
|
||||||
storages: []
|
|
||||||
},
|
|
||||||
owner: {
|
|
||||||
members: [ownerId],
|
|
||||||
validation_rules: [
|
|
||||||
{
|
|
||||||
quorum: 0.5,
|
|
||||||
fields: [...folderDataFields, 'roles'],
|
|
||||||
min_sig_member: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
storages: []
|
|
||||||
},
|
|
||||||
stakeholders: {
|
|
||||||
members: stakeholdersId,
|
|
||||||
validation_rules: [
|
|
||||||
{
|
|
||||||
quorum: 0.5,
|
|
||||||
fields: ['documents', 'motes'],
|
|
||||||
min_sig_member: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
storages: []
|
|
||||||
},
|
|
||||||
customers: {
|
|
||||||
members: customersId,
|
|
||||||
validation_rules: [
|
|
||||||
{
|
|
||||||
quorum: 0.0,
|
|
||||||
fields: folderDataFields,
|
|
||||||
min_sig_member: 0.0,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
storages: []
|
|
||||||
},
|
|
||||||
apophis: {
|
|
||||||
members: [ownerId],
|
|
||||||
validation_rules: [],
|
|
||||||
storages: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -1,122 +0,0 @@
|
|||||||
import { isFileBlob, type FileBlob } from "./Data";
|
|
||||||
import type { RoleDefinition } from "./Roles";
|
|
||||||
|
|
||||||
export interface ProfileData {
|
|
||||||
name: string;
|
|
||||||
surname: string;
|
|
||||||
email: string;
|
|
||||||
phone: string;
|
|
||||||
address: string;
|
|
||||||
postalCode: string;
|
|
||||||
city: string;
|
|
||||||
country: string;
|
|
||||||
idDocument: FileBlob | null;
|
|
||||||
idCertified: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isProfileData(data: any): data is ProfileData{
|
|
||||||
if (typeof data !== 'object' || data === null) return false;
|
|
||||||
|
|
||||||
const requiredStringFields = [
|
|
||||||
'name',
|
|
||||||
'surname',
|
|
||||||
'email',
|
|
||||||
'phone',
|
|
||||||
'address',
|
|
||||||
'postalCode',
|
|
||||||
'city',
|
|
||||||
'country',
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const field of requiredStringFields) {
|
|
||||||
if (typeof data[field] !== 'string') return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const requiredBooleanFields = [
|
|
||||||
'idCertified',
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const field of requiredBooleanFields) {
|
|
||||||
if (typeof data[field] !== 'boolean') return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const requiredFileFields = [
|
|
||||||
'idDocument',
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const field of requiredFileFields) {
|
|
||||||
if (!isFileBlob(data[field]) && data[field] !== null) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const emptyProfileData: ProfileData = {
|
|
||||||
name: '',
|
|
||||||
surname: '',
|
|
||||||
email: '',
|
|
||||||
phone: '',
|
|
||||||
address: '',
|
|
||||||
postalCode: '',
|
|
||||||
city: '',
|
|
||||||
country: '',
|
|
||||||
idDocument: null,
|
|
||||||
idCertified: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const profileDataFields: string[] = Object.keys(emptyProfileData);
|
|
||||||
|
|
||||||
const ProfilePublicFields: string[] = ['idCertified'];
|
|
||||||
|
|
||||||
export const ProfilePrivateFields = [
|
|
||||||
...profileDataFields.filter(key => !ProfilePublicFields.includes(key))
|
|
||||||
];
|
|
||||||
|
|
||||||
export interface ProfileCreated {
|
|
||||||
processId: string,
|
|
||||||
process: any, // Process
|
|
||||||
profileData: ProfileData,
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setDefaultProfileRoles(ownerId: string[], validatorId: string): Record<string, RoleDefinition> {
|
|
||||||
return {
|
|
||||||
demiurge: {
|
|
||||||
members: [...ownerId, validatorId],
|
|
||||||
validation_rules: [],
|
|
||||||
storages: []
|
|
||||||
},
|
|
||||||
owner: {
|
|
||||||
members: ownerId,
|
|
||||||
validation_rules: [
|
|
||||||
{
|
|
||||||
quorum: 0.5,
|
|
||||||
fields: [...ProfilePrivateFields, 'roles'],
|
|
||||||
min_sig_member: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
storages: []
|
|
||||||
},
|
|
||||||
validator: {
|
|
||||||
members: [validatorId],
|
|
||||||
validation_rules: [
|
|
||||||
{
|
|
||||||
quorum: 0.5,
|
|
||||||
fields: ['idCertified', 'roles'],
|
|
||||||
min_sig_member: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
quorum: 0.0,
|
|
||||||
fields: [...profileDataFields],
|
|
||||||
min_sig_member: 0,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
storages: []
|
|
||||||
},
|
|
||||||
apophis: {
|
|
||||||
members: ownerId,
|
|
||||||
validation_rules: [],
|
|
||||||
storages: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
export interface ValidationRule {
|
|
||||||
quorum: number,
|
|
||||||
fields: string[],
|
|
||||||
min_sig_member: number,
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RoleDefinition {
|
|
||||||
members: string[],
|
|
||||||
validation_rules: ValidationRule[],
|
|
||||||
storages: string[]
|
|
||||||
}
|
|
||||||
36
package-lock.json
generated
36
package-lock.json
generated
@ -48,9 +48,9 @@
|
|||||||
"next": "15.2.4",
|
"next": "15.2.4",
|
||||||
"next-themes": "latest",
|
"next-themes": "latest",
|
||||||
"nodemailer": "latest",
|
"nodemailer": "latest",
|
||||||
"react": "^19.1.1",
|
"react": "^19",
|
||||||
"react-day-picker": "9.8.0",
|
"react-day-picker": "9.8.0",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19",
|
||||||
"react-hook-form": "^7.60.0",
|
"react-hook-form": "^7.60.0",
|
||||||
"react-resizable-panels": "^2.1.7",
|
"react-resizable-panels": "^2.1.7",
|
||||||
"recharts": "2.15.4",
|
"recharts": "2.15.4",
|
||||||
@ -58,7 +58,7 @@
|
|||||||
"tailwind-merge": "^2.5.5",
|
"tailwind-merge": "^2.5.5",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"uuid": "latest",
|
"uuid": "latest",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^0.9.9",
|
||||||
"zod": "3.25.67"
|
"zod": "3.25.67"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -3730,9 +3730,8 @@
|
|||||||
"version": "19.1.10",
|
"version": "19.1.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz",
|
||||||
"integrity": "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==",
|
"integrity": "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
@ -3741,9 +3740,8 @@
|
|||||||
"version": "19.1.7",
|
"version": "19.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz",
|
||||||
"integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==",
|
"integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.0.0"
|
"@types/react": "^19.0.0"
|
||||||
}
|
}
|
||||||
@ -3823,7 +3821,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001733",
|
"caniuse-lite": "^1.0.30001733",
|
||||||
"electron-to-chromium": "^1.5.199",
|
"electron-to-chromium": "^1.5.199",
|
||||||
@ -4151,8 +4148,7 @@
|
|||||||
"version": "8.5.1",
|
"version": "8.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.5.1.tgz",
|
||||||
"integrity": "sha512-JUb5+FOHobSiWQ2EJNaueCNT/cQU9L6XWBbWmorWPQT9bkbk+fhsuLr8wWrzXKagO3oWszBO7MSx+GfaRk4E6A==",
|
"integrity": "sha512-JUb5+FOHobSiWQ2EJNaueCNT/cQU9L6XWBbWmorWPQT9bkbk+fhsuLr8wWrzXKagO3oWszBO7MSx+GfaRk4E6A==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/embla-carousel-react": {
|
"node_modules/embla-carousel-react": {
|
||||||
"version": "8.5.1",
|
"version": "8.5.1",
|
||||||
@ -4632,7 +4628,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/next/-/next-15.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/next/-/next-15.2.4.tgz",
|
||||||
"integrity": "sha512-VwL+LAaPSxEkd3lU2xWbgEOtrM8oedmyhBqaVNmgKB+GvZlCy9rgaEc+y2on0wv+l0oSFqLtYD6dcC1eAedUaQ==",
|
"integrity": "sha512-VwL+LAaPSxEkd3lU2xWbgEOtrM8oedmyhBqaVNmgKB+GvZlCy9rgaEc+y2on0wv+l0oSFqLtYD6dcC1eAedUaQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next/env": "15.2.4",
|
"@next/env": "15.2.4",
|
||||||
"@swc/counter": "0.1.3",
|
"@swc/counter": "0.1.3",
|
||||||
@ -4763,6 +4758,7 @@
|
|||||||
"version": "8.5.6",
|
"version": "8.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||||
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@ -4778,7 +4774,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@ -4816,7 +4811,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
|
||||||
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
|
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@ -4847,7 +4841,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
|
||||||
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
|
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.26.0"
|
"scheduler": "^0.26.0"
|
||||||
},
|
},
|
||||||
@ -4860,7 +4853,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz",
|
||||||
"integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==",
|
"integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
},
|
},
|
||||||
@ -5153,8 +5145,8 @@
|
|||||||
"version": "4.1.11",
|
"version": "4.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",
|
||||||
"integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==",
|
"integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==",
|
||||||
"license": "MIT",
|
"dev": true,
|
||||||
"peer": true
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/tailwindcss-animate": {
|
"node_modules/tailwindcss-animate": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
@ -5332,16 +5324,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vaul": {
|
"node_modules/vaul": {
|
||||||
"version": "1.1.2",
|
"version": "0.9.9",
|
||||||
"resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vaul/-/vaul-0.9.9.tgz",
|
||||||
"integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==",
|
"integrity": "sha512-7afKg48srluhZwIkaU+lgGtFCUsYBSGOl8vcc8N/M3YQlZFlynHD15AE+pwrYdc826o7nrIND4lL9Y6b9WWZZQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-dialog": "^1.1.1"
|
"@radix-ui/react-dialog": "^1.1.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc",
|
"react": "^16.8 || ^17.0 || ^18.0",
|
||||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc"
|
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/victory-vendor": {
|
"node_modules/victory-vendor": {
|
||||||
|
|||||||
@ -49,9 +49,9 @@
|
|||||||
"next": "15.2.4",
|
"next": "15.2.4",
|
||||||
"next-themes": "latest",
|
"next-themes": "latest",
|
||||||
"nodemailer": "latest",
|
"nodemailer": "latest",
|
||||||
"react": "^19.1.1",
|
"react": "^19",
|
||||||
"react-day-picker": "9.8.0",
|
"react-day-picker": "9.8.0",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19",
|
||||||
"react-hook-form": "^7.60.0",
|
"react-hook-form": "^7.60.0",
|
||||||
"react-resizable-panels": "^2.1.7",
|
"react-resizable-panels": "^2.1.7",
|
||||||
"recharts": "2.15.4",
|
"recharts": "2.15.4",
|
||||||
@ -59,7 +59,7 @@
|
|||||||
"tailwind-merge": "^2.5.5",
|
"tailwind-merge": "^2.5.5",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"uuid": "latest",
|
"uuid": "latest",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^0.9.9",
|
||||||
"zod": "3.25.67"
|
"zod": "3.25.67"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user