From 3d72d5ac3c3fe59bfbebb13acd55bbfbe6a7ad90 Mon Sep 17 00:00:00 2001 From: senke Date: Wed, 17 Dec 2025 08:07:35 -0500 Subject: [PATCH] stabilizing apps/web: FIRST BATCH --- .github/workflows/playwright.yml | 27 + .gitignore | 7 + apps/web/AUDIT_PRODUCTION_FRONTEND.md | 128 + apps/web/DIAGNOSTIC_REPORT.md | 272 ++ apps/web/RUNTIME_AUDIT_REPORT.md | 69 + apps/web/RUNTIME_ISSUES.json | 32 + apps/web/e2e-results.json | 207 + apps/web/e2e/deep_audit.spec.ts | 710 ++++ apps/web/e2e/diagnostic-login-page.png | Bin 0 -> 119727 bytes apps/web/e2e/diagnostic.spec.ts | 346 ++ apps/web/src/components/ErrorBoundary.tsx | 2 +- apps/web/src/components/ui/LazyComponent.tsx | 18 +- apps/web/src/config/constants.ts | 8 +- apps/web/src/config/env.ts | 8 +- apps/web/src/features/chat/hooks/useChat.ts | 6 +- .../library/components/LibraryManager.tsx | 3 +- .../player/components/PlayerError.tsx | 2 +- .../features/player/hooks/useStreamSync.ts | 7 +- .../playlists/hooks/usePlaylistPermissions.ts | 15 +- .../playlists/pages/PlaylistDetailPage.tsx | 127 + apps/web/src/features/playlists/routes.tsx | 15 + .../profile/pages/UserProfilePage.tsx | 154 + .../src/features/roles/pages/RolesPage.tsx | 208 + .../components/PreferenceSettings.tsx | 137 + .../settings/components/SettingsTabs.tsx | 82 + .../features/settings/pages/SettingsPage.tsx | 130 + .../streaming/hooks/usePlaybackRealtime.ts | 12 +- .../features/tracks/pages/TrackDetailPage.tsx | 262 ++ apps/web/src/main.tsx | 9 +- apps/web/src/pages/DashboardPage.tsx | 7 +- .../src/pages/marketplace/MarketplaceHome.tsx | 92 + apps/web/src/router/index.tsx | 13 +- apps/web/src/services/api.ts | 31 +- apps/web/src/services/secure-auth.ts | 13 +- apps/web/src/services/tokenRefresh.ts | 14 +- apps/web/src/services/websocket.ts | 12 +- apps/web/src/stores/library.ts | 32 +- apps/web/src/utils/csp.ts | 2 +- apps/web/src/utils/logger.ts | 50 + apps/web/src/utils/sanitize.ts | 19 + apps/web/vite.config.ts | 44 +- e2e/example.spec.ts | 18 + e2e/test-1.spec.ts | 15 + package-lock.json | 3506 +++++++++++++++++ package.json | 5 +- playwright.config.ts | 79 + .../cmd/tools/create_test_user/main.go | 109 + .../000000_cleanup_refresh_tokens.sql | 18 - .../migrations/011_cleanup_refresh_tokens.sql | 49 + 49 files changed, 7049 insertions(+), 82 deletions(-) create mode 100644 .github/workflows/playwright.yml create mode 100644 apps/web/AUDIT_PRODUCTION_FRONTEND.md create mode 100644 apps/web/DIAGNOSTIC_REPORT.md create mode 100644 apps/web/RUNTIME_AUDIT_REPORT.md create mode 100644 apps/web/RUNTIME_ISSUES.json create mode 100644 apps/web/e2e-results.json create mode 100644 apps/web/e2e/deep_audit.spec.ts create mode 100644 apps/web/e2e/diagnostic-login-page.png create mode 100644 apps/web/e2e/diagnostic.spec.ts create mode 100644 apps/web/src/features/playlists/pages/PlaylistDetailPage.tsx create mode 100644 apps/web/src/features/playlists/routes.tsx create mode 100644 apps/web/src/features/profile/pages/UserProfilePage.tsx create mode 100644 apps/web/src/features/roles/pages/RolesPage.tsx create mode 100644 apps/web/src/features/settings/components/PreferenceSettings.tsx create mode 100644 apps/web/src/features/settings/components/SettingsTabs.tsx create mode 100644 apps/web/src/features/settings/pages/SettingsPage.tsx create mode 100644 apps/web/src/features/tracks/pages/TrackDetailPage.tsx create mode 100644 apps/web/src/pages/marketplace/MarketplaceHome.tsx create mode 100644 apps/web/src/utils/logger.ts create mode 100644 e2e/example.spec.ts create mode 100644 e2e/test-1.spec.ts create mode 100644 package-lock.json create mode 100644 playwright.config.ts create mode 100644 veza-backend-api/cmd/tools/create_test_user/main.go delete mode 100644 veza-backend-api/migrations/000000_cleanup_refresh_tokens.sql create mode 100644 veza-backend-api/migrations/011_cleanup_refresh_tokens.sql diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 000000000..3eb13143c --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,27 @@ +name: Playwright Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Install dependencies + run: npm ci + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Run Playwright tests + run: npx playwright test + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index 3728529b4..220b250c3 100644 --- a/.gitignore +++ b/.gitignore @@ -76,3 +76,10 @@ veza-backend-api/api veza-backend-api/migrate_tool chat_exports/!veza-stream-server/src/bin/ !veza-stream-server/.env + +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/.auth/ diff --git a/apps/web/AUDIT_PRODUCTION_FRONTEND.md b/apps/web/AUDIT_PRODUCTION_FRONTEND.md new file mode 100644 index 000000000..f3294b05f --- /dev/null +++ b/apps/web/AUDIT_PRODUCTION_FRONTEND.md @@ -0,0 +1,128 @@ +# 🔍 Audit de Production - Frontend Veza + +**Date** : 2025-01-27 +**Cible** : `apps/web` (Frontend React/Vite/TypeScript) +**Objectif** : Identification des bloquants pour un dĂ©ploiement en production stable et sĂ©curisĂ© + +--- + +## 📊 RĂ©sumĂ© ExĂ©cutif + +### Score Global : **62/100** ⚠ + +**Verdict** : **NON PRÊT POUR LA PRODUCTION** + +Le frontend prĂ©sente plusieurs problĂšmes critiques de sĂ©curitĂ© et de qualitĂ© qui doivent ĂȘtre rĂ©solus avant tout dĂ©ploiement en production. + +### Points Forts ✅ + +1. **Architecture** : Routes protĂ©gĂ©es correctement implĂ©mentĂ©es avec `ProtectedRoute` +2. **TypeScript** : Configuration TypeScript prĂ©sente et fonctionnelle +3. **Build** : Configuration Vite optimisĂ©e avec chunk splitting et sourcemaps +4. **Tests** : Infrastructure de tests prĂ©sente (Vitest + Playwright) + +### Points Faibles Majeurs 🔮 + +1. **SĂ©curitĂ© Critique** : + - CSP avec `unsafe-inline` et `unsafe-eval` (CRITICAL) + - Tokens JWT stockĂ©s dans `localStorage` (vulnĂ©rable au XSS) + - Utilisation de `dangerouslySetInnerHTML` sans sanitisation complĂšte + +2. **QualitĂ© du Code** : + - 122+ `console.log/error/warn` qui pollueront les logs de production + - 480+ utilisations de `any` (perte de sĂ©curitĂ© de type) + - Hardcoding de `localhost/127.0.0.1` dans plusieurs fichiers + +3. **Gestion d'Erreur** : + - Nombreux `catch` qui loggent uniquement sans informer l'utilisateur + - Erreurs API silencieuses dans certains composants + +4. **Dette Technique** : + - 11 TODO/FIXME/HACK identifiĂ©s + - Code temporaire (`console.log` avec commentaire "Temporary") + +--- + +## 📋 DĂ©tail des ProblĂšmes par CatĂ©gorie + +### 🔮 CRITICAL (Bloquant Production) + +1. **CSP avec unsafe-inline/unsafe-eval** (`vite.config.ts:64`) + - Impact : VulnĂ©rabilitĂ© XSS, injection de scripts + - Fix : Utiliser des nonces CSP stricts, supprimer `unsafe-eval` + +2. **Tokens dans localStorage** (Multiple fichiers) + - Impact : Vol de tokens via XSS + - Fix : Migrer vers httpOnly cookies ou sessionStorage avec rotation + +### 🟠 HIGH (Risque SĂ©curitĂ©/StabilitĂ©) + +3. **dangerouslySetInnerHTML sans sanitisation complĂšte** (2 occurrences) + - Impact : Risque XSS si sanitisation Ă©choue + - Fix : VĂ©rifier `sanitizeChatMessage`, considĂ©rer une alternative + +4. **Gestion d'erreur silencieuse** (Multiple fichiers) + - Impact : UX dĂ©gradĂ©e, bugs non visibles + - Fix : Afficher des toasts/notifications pour toutes les erreurs utilisateur + +5. **Hardcoding localhost** (Multiple fichiers) + - Impact : Build de production avec URLs de dev + - Fix : Utiliser uniquement `import.meta.env` avec fallbacks appropriĂ©s + +### 🟡 MEDIUM (QualitĂ©/Performance) + +6. **480+ utilisations de `any`** + - Impact : Perte de sĂ©curitĂ© de type, bugs potentiels + - Fix : Typage progressif, interfaces strictes + +7. **122+ console.log/error/warn** + - Impact : Pollution des logs de production, fuite d'informations + - Fix : Utiliser un logger conditionnel basĂ© sur `import.meta.env.DEV` + +8. **11 TODO/FIXME/HACK** + - Impact : Dette technique, fonctionnalitĂ©s incomplĂštes + - Fix : Prioriser et rĂ©soudre ou documenter + +### 🟱 LOW (CosmĂ©tique/Maintenance) + +9. **Code temporaire** (`LibraryManager.tsx:112`) + - Impact : Code mort, confusion + - Fix : Supprimer ou implĂ©menter la fonctionnalitĂ© + +10. **Tests manquants** (Plusieurs composants) + - Impact : Risque de rĂ©gression + - Fix : Augmenter la couverture de tests + +--- + +## 🎯 Plan d'Action RecommandĂ© + +### Phase 1 : SĂ©curitĂ© Critique (1-2 jours) +- [ ] Corriger CSP (supprimer unsafe-inline/unsafe-eval) +- [ ] Migrer tokens vers httpOnly cookies +- [ ] Audit complet de `dangerouslySetInnerHTML` + +### Phase 2 : QualitĂ© Code (2-3 jours) +- [ ] Remplacer tous les `console.*` par un logger conditionnel +- [ ] Corriger hardcoding localhost +- [ ] AmĂ©liorer gestion d'erreur (toasts partout) + +### Phase 3 : Dette Technique (1-2 jours) +- [ ] RĂ©soudre/prioriser les TODO +- [ ] RĂ©duire les `any` (typage progressif) +- [ ] Nettoyer le code temporaire + +--- + +## 📈 MĂ©triques + +- **Fichiers analysĂ©s** : ~363 fichiers +- **ProblĂšmes identifiĂ©s** : 50+ (voir JSON dĂ©taillĂ©) +- **Bloquants production** : 2 (CRITICAL) +- **Risques sĂ©curitĂ©** : 3 (HIGH) +- **ProblĂšmes qualitĂ©** : 8 (MEDIUM) +- **AmĂ©liorations** : 10+ (LOW) + +--- + +**Prochaine Ă©tape** : Consulter le JSON dĂ©taillĂ© (`AUDIT_ISSUES.json`) pour la liste complĂšte des problĂšmes avec localisation exacte. diff --git a/apps/web/DIAGNOSTIC_REPORT.md b/apps/web/DIAGNOSTIC_REPORT.md new file mode 100644 index 000000000..c4c7eeaf6 --- /dev/null +++ b/apps/web/DIAGNOSTIC_REPORT.md @@ -0,0 +1,272 @@ +# Rapport de Diagnostic - CompatibilitĂ© Full Stack + +**Date** : 2025-01-27 +**Test** : Login Flow - Full Stack Compatibility Diagnostic +**Environnement** : Frontend (localhost:3000) + Backend (localhost:8080) + +--- + +## 1. État Visuel & Navigation + +### ✅ Navigation de Base +- **Page accessible** : ✅ Oui (`http://localhost:3000/login`) +- **Titre de la page** : ✅ "Veza - Plateforme de streaming musical" +- **URL finale** : `http://localhost:3000/login` + +### ❌ Formulaire de Login +- **Formulaire visible** : ❌ **NON** +- **Inputs email/password** : ❌ **0 inputs dĂ©tectĂ©s sur la page** +- **Boutons** : ❌ **0 boutons dĂ©tectĂ©s sur la page** +- **Contenu HTML** : La page contient le mot "form" mais aucun Ă©lĂ©ment de formulaire n'est rendu + +**Diagnostic** : Le formulaire React ne se charge pas. La page est essentiellement vide cĂŽtĂ© contenu interactif. + +--- + +## 2. Analyse "Sous le capot" (Backend Compatibility) + +### 🔮 Erreurs RĂ©seau + +**Erreur 500 dĂ©tectĂ©e** lors du chargement des ressources Vite : + +1. **`Failed to load resource: the server responded with a status of 500 (Internal Server Error)`** + - **Impact** : Les scripts Vite ne peuvent pas se charger, empĂȘchant React de s'initialiser + - **Scripts affectĂ©s** : + - `/@vite/client` - Client Vite pour le HMR + - `/src/main.tsx?t=...` - Point d'entrĂ©e React + +**Diagnostic** : Le serveur Vite retourne une erreur 500, probablement due Ă  : +- Une erreur de compilation TypeScript/JavaScript +- Un problĂšme d'import de module +- Une erreur dans le code React qui empĂȘche le build + +#### RequĂȘtes Attendues (si le formulaire fonctionnait) : +1. `POST http://localhost:8080/api/v1/auth/login` + - **Payload attendu** : `{ email: string, password: string, remember_me?: boolean }` + - **Format attendu** : `{ success: true, data: { access_token, refresh_token, expires_in, token_type, user } }` + +#### Erreurs Potentielles Ă  Surveiller : +- **401 Unauthorized** : Identifiants invalides +- **400 Bad Request** : Format de payload incorrect +- **404 Not Found** : Endpoint `/auth/login` introuvable +- **500 Internal Server Error** : Erreur serveur +- **CORS** : Blocage cross-origin + +### 🔮 Erreurs Console + +**Erreurs capturĂ©es** : + +1. **`Failed to load resource: the server responded with a status of 500 (Internal Server Error)`** + - **Type** : Error + - **Cause** : Le serveur Vite ne peut pas servir les modules JavaScript/TypeScript + - **Impact** : React ne peut pas s'initialiser, le formulaire ne se rend pas + +**Messages console capturĂ©s** : +- `[debug] [vite] connecting...` +- `[debug] [vite] connected.` +- `[error] Failed to load resource: the server responded with a status of 500 (Internal Server Error)` + +**Diagnostic** : Le problĂšme vient du serveur de dĂ©veloppement Vite qui retourne une erreur 500 lors du chargement des modules. Cela empĂȘche complĂštement le rendu de l'application React. + +**Erreurs TypeScript DĂ©tectĂ©es** : +- Plusieurs erreurs TypeScript dans des composants non liĂ©s Ă  l'auth (player, search, forms) +- Ces erreurs peuvent empĂȘcher Vite de compiler correctement +- **Note** : Ces erreurs existaient probablement avant le refactoring de l'auth + +### 🟠 CORS + +**Aucune erreur CORS dĂ©tectĂ©e** car aucune requĂȘte n'a Ă©tĂ© faite. + +**Configuration Backend Requise** : +```go +CORS_ALLOWED_ORIGINS=http://localhost:3000 +``` + +### 🟱 Token + +**LocalStorage aprĂšs test** : Non vĂ©rifiĂ© (le formulaire ne s'est pas chargĂ©) + +**ClĂ©s attendues** : +- `access_token` ou `veza_access_token` +- `refresh_token` ou `veza_refresh_token` + +--- + +## 3. Verdict & Bloquants + +### 🔮 **BLOQUANT CRITIQUE #1 : Erreur 500 du Serveur Vite** + +**ProblĂšme** : Le serveur Vite retourne une erreur 500 lors du chargement des modules, empĂȘchant React de s'initialiser. + +**Cause IdentifiĂ©e** : +- **Erreur 500** sur `/@vite/client` et `/src/main.tsx` +- Les scripts ne peuvent pas se charger +- React ne peut pas s'initialiser +- Le formulaire ne se rend pas (0 inputs, 0 boutons) + +**Causes Possibles** : +1. **Erreur de compilation TypeScript** dans `src/main.tsx` ou un module importĂ© + - ✅ **ConfirmĂ©** : Plusieurs erreurs TypeScript dĂ©tectĂ©es (player, search, forms) + - Ces erreurs empĂȘchent Vite de compiler correctement +2. **Erreur d'import** - un module ne peut pas ĂȘtre rĂ©solu + - Exemples : `@/stores/player`, `@/hooks/use-toast`, `@/components/ui/scroll-area` +3. **Erreur de syntaxe** dans un fichier rĂ©cemment modifiĂ© +4. **ProblĂšme de configuration Vite** - alias ou plugins mal configurĂ©s + +**Actions ImmĂ©diates** : + +1. **VĂ©rifier les logs du serveur Vite** (PRIORITÉ ABSOLUE) : + ```bash + # ArrĂȘter le serveur actuel (Ctrl+C) + # Relancer avec logs dĂ©taillĂ©s + npm run dev + # Observer les erreurs de compilation TypeScript/JavaScript + ``` + +2. **VĂ©rifier les erreurs TypeScript** : + ```bash + npm run typecheck + # Corriger toutes les erreurs TypeScript + ``` + +3. **VĂ©rifier les erreurs dans la console du navigateur** : + - Ouvrir `http://localhost:3000/login` dans un navigateur + - Ouvrir DevTools (F12) + - VĂ©rifier l'onglet Console pour les erreurs dĂ©taillĂ©es + - VĂ©rifier l'onglet Network pour voir quelle requĂȘte retourne 500 + +4. **VĂ©rifier les imports rĂ©cents** : + ```bash + # VĂ©rifier que tous les imports dans les fichiers modifiĂ©s sont valides + grep -r "from.*@/stores/auth" src/ + grep -r "from.*@/services/api/auth" src/ + ``` + +5. **VĂ©rifier les erreurs de build** : + ```bash + npm run build + # Observer les erreurs de compilation + ``` + +### 🟡 **BLOQUANT MOYEN : Test Incomplet** + +**ProblĂšme** : Le test ne peut pas continuer car le formulaire ne se charge pas. + +**AmĂ©liorations NĂ©cessaires** : +1. Capturer les erreurs console dĂšs le chargement de la page +2. Prendre des captures d'Ă©cran automatiques +3. Logger le HTML complet de la page en cas d'Ă©chec +4. VĂ©rifier si le serveur backend rĂ©pond avant de tester le login + +--- + +## 4. Plan d'Action ImmĂ©diat + +### PrioritĂ© 1 : Diagnostiquer le ProblĂšme de Rendu (30 min) + +1. **VĂ©rifier le serveur frontend** : + ```bash + cd apps/web + npm run dev + # Ouvrir http://localhost:3000/login dans un navigateur + ``` + +2. **VĂ©rifier les erreurs console** : + - Ouvrir DevTools + - VĂ©rifier l'onglet Console + - VĂ©rifier l'onglet Network pour les erreurs 404/500 + +3. **VĂ©rifier le routing** : + ```bash + # VĂ©rifier que la route /login existe + grep -r "/login" src/router/ + ``` + +4. **VĂ©rifier les imports** : + ```bash + # VĂ©rifier que LoginForm est bien importĂ© dans LoginPage + grep -r "LoginForm" src/pages/LoginPage.tsx + ``` + +### PrioritĂ© 2 : Une fois le Formulaire Visible (15 min) + +1. **Relancer le test de diagnostic** +2. **VĂ©rifier les requĂȘtes rĂ©seau** vers `localhost:8080` +3. **VĂ©rifier le format du payload** envoyĂ© +4. **VĂ©rifier le format de la rĂ©ponse** reçue +5. **VĂ©rifier le stockage du token** dans localStorage + +### PrioritĂ© 3 : Tests d'IntĂ©gration Complets (30 min) + +1. **Test avec identifiants valides** (si backend disponible) +2. **Test avec identifiants invalides** (vĂ©rifier les messages d'erreur) +3. **Test de redirection** aprĂšs login rĂ©ussi +4. **Test de persistance** du token (refresh de page) + +--- + +## 5. Commandes de Diagnostic + +### VĂ©rifier le Serveur Frontend +```bash +curl http://localhost:3000/login +``` + +### VĂ©rifier le Serveur Backend +```bash +curl http://localhost:8080/api/v1/health +``` + +### Lancer le Test de Diagnostic +```bash +cd apps/web +npx playwright test e2e/diagnostic.spec.ts --reporter=list +``` + +### VĂ©rifier les Erreurs TypeScript +```bash +cd apps/web +npm run typecheck +``` + +### VĂ©rifier les Erreurs de Build +```bash +cd apps/web +npm run build +``` + +--- + +## 6. Notes Techniques + +### Configuration Playwright +- **Base URL** : `http://localhost:3000` (configurĂ© dans `playwright.config.ts`) +- **Timeout** : 30 secondes pour la navigation +- **Browser** : Chromium Headless Shell + +### Variables d'Environnement +- `VITE_API_URL` : URL du backend (dĂ©faut: `http://localhost:8080/api/v1`) +- `TEST_EMAIL` : Email de test (dĂ©faut: `user@example.com`) +- `TEST_PASSWORD` : Mot de passe de test (dĂ©faut: `password123`) + +--- + +## Conclusion + +**État Actuel** : 🔮 **BLOQUÉ** - Erreur 500 du serveur Vite empĂȘchant le chargement de React. + +**Cause Racine IdentifiĂ©e** : Le serveur Vite retourne une erreur 500 lors du chargement des modules (`/@vite/client` et `/src/main.tsx`), probablement due Ă  une erreur de compilation TypeScript ou un problĂšme d'import. + +**Prochaine Étape** : +1. **URGENT** : VĂ©rifier les logs du serveur Vite (`npm run dev`) pour identifier l'erreur exacte +2. Corriger l'erreur de compilation +3. Relancer le serveur +4. Relancer le test de diagnostic + +**Une fois le formulaire visible** : Relancer le test de diagnostic pour vĂ©rifier la communication avec le backend et le format des requĂȘtes/rĂ©ponses. + +--- + +**GĂ©nĂ©rĂ© par** : Script de diagnostic Playwright (`e2e/diagnostic.spec.ts`) +**Date du test** : 2025-01-27 + diff --git a/apps/web/RUNTIME_AUDIT_REPORT.md b/apps/web/RUNTIME_AUDIT_REPORT.md new file mode 100644 index 000000000..6763d96a1 --- /dev/null +++ b/apps/web/RUNTIME_AUDIT_REPORT.md @@ -0,0 +1,69 @@ +# Runtime Audit Report + +**Generated:** 2025-12-17T12:09:00.157Z + +--- + +## État Global + +**Status:** ❌ UNSTABLE +**Login Success:** ✅ Yes + +## Parcours Utilisateur + +| Page | Loaded | Has Content | Load Time (ms) | +|------|--------|-------------|----------------| +| /dashboard | ✅ | ✅ | 15ms | +| /profile | ❌ | ❌ | N/A | +| /settings | ❌ | ❌ | N/A | +| /library | ❌ | ❌ | N/A | + +## RĂ©sumĂ© des Erreurs + +**Total Issues:** 3 + +### Par SĂ©vĂ©ritĂ© + +- **CRITICAL:** 3 +- **HIGH:** 0 +- **MEDIUM:** 0 +- **LOW:** 0 + +### Par CatĂ©gorie + +- **NETWORK:** 0 +- **CONSOLE:** 0 +- **NAVIGATION:** 3 +- **UX:** 0 + +## Erreurs Navigation + +### RUN-001 - CRITICAL + +- **Location:** /profile +- **Message:** Failed to navigate to /profile +- **Details:** page.waitForURL: Timeout 10000ms exceeded. +=========================== logs =========================== +waiting for navigation until "domcontentloaded" +============================================================ +- **Reproduction:** Navigate to /profile after login + +### RUN-002 - CRITICAL + +- **Location:** /settings +- **Message:** Failed to navigate to /settings +- **Details:** page.waitForURL: Timeout 10000ms exceeded. +=========================== logs =========================== +waiting for navigation until "domcontentloaded" +============================================================ +- **Reproduction:** Navigate to /settings after login + +### RUN-003 - CRITICAL + +- **Location:** /library +- **Message:** Failed to navigate to /library +- **Details:** page.waitForURL: Timeout 10000ms exceeded. +=========================== logs =========================== +waiting for navigation until "domcontentloaded" +============================================================ +- **Reproduction:** Navigate to /library after login diff --git a/apps/web/RUNTIME_ISSUES.json b/apps/web/RUNTIME_ISSUES.json new file mode 100644 index 000000000..2e3fa86a4 --- /dev/null +++ b/apps/web/RUNTIME_ISSUES.json @@ -0,0 +1,32 @@ +[ + { + "category": "NAVIGATION", + "severity": "CRITICAL", + "location": "/profile", + "message": "Failed to navigate to /profile", + "details": "page.waitForURL: Timeout 10000ms exceeded.\n=========================== logs ===========================\nwaiting for navigation until \"domcontentloaded\"\n============================================================", + "reproduction_steps": "Navigate to /profile after login", + "id": "RUN-001", + "timestamp": "2025-12-17T12:08:36.888Z" + }, + { + "category": "NAVIGATION", + "severity": "CRITICAL", + "location": "/settings", + "message": "Failed to navigate to /settings", + "details": "page.waitForURL: Timeout 10000ms exceeded.\n=========================== logs ===========================\nwaiting for navigation until \"domcontentloaded\"\n============================================================", + "reproduction_steps": "Navigate to /settings after login", + "id": "RUN-002", + "timestamp": "2025-12-17T12:08:48.535Z" + }, + { + "category": "NAVIGATION", + "severity": "CRITICAL", + "location": "/library", + "message": "Failed to navigate to /library", + "details": "page.waitForURL: Timeout 10000ms exceeded.\n=========================== logs ===========================\nwaiting for navigation until \"domcontentloaded\"\n============================================================", + "reproduction_steps": "Navigate to /library after login", + "id": "RUN-003", + "timestamp": "2025-12-17T12:09:00.142Z" + } +] \ No newline at end of file diff --git a/apps/web/e2e-results.json b/apps/web/e2e-results.json new file mode 100644 index 000000000..fa6e0ba9d --- /dev/null +++ b/apps/web/e2e-results.json @@ -0,0 +1,207 @@ +{ + "config": { + "configFile": "/home/senke/git/talas/veza/apps/web/playwright.config.ts", + "rootDir": "/home/senke/git/talas/veza/apps/web/e2e", + "forbidOnly": false, + "fullyParallel": true, + "globalSetup": null, + "globalTeardown": null, + "globalTimeout": 0, + "grep": {}, + "grepInvert": null, + "maxFailures": 0, + "metadata": { + "actualWorkers": 1 + }, + "preserveOutput": "always", + "reporter": [ + [ + "html", + null + ], + [ + "json", + { + "outputFile": "e2e-results.json" + } + ] + ], + "reportSlowTests": { + "max": 5, + "threshold": 300000 + }, + "quiet": false, + "projects": [ + { + "outputDir": "/home/senke/git/talas/veza/apps/web/test-results", + "repeatEach": 1, + "retries": 0, + "metadata": { + "actualWorkers": 1 + }, + "id": "chromium", + "name": "chromium", + "testDir": "/home/senke/git/talas/veza/apps/web/e2e", + "testIgnore": [], + "testMatch": [ + "**/*.@(spec|test).?(c|m)[jt]s?(x)" + ], + "timeout": 30000 + } + ], + "shard": null, + "tags": [], + "updateSnapshots": "missing", + "updateSourceMethod": "patch", + "version": "1.57.0", + "workers": 6, + "webServer": { + "command": "npm run dev", + "url": "http://localhost:3000", + "reuseExistingServer": true, + "timeout": 120000 + } + }, + "suites": [ + { + "title": "deep_audit.spec.ts", + "file": "deep_audit.spec.ts", + "column": 0, + "line": 0, + "specs": [], + "suites": [ + { + "title": "Deep E2E Runtime Audit", + "file": "deep_audit.spec.ts", + "line": 175, + "column": 6, + "specs": [ + { + "title": "Complete User Journey - Runtime Audit", + "ok": true, + "tags": [], + "tests": [ + { + "timeout": 60000, + "annotations": [], + "expectedStatus": "passed", + "projectId": "chromium", + "projectName": "chromium", + "results": [ + { + "workerIndex": 0, + "parallelIndex": 0, + "status": "passed", + "duration": 38597, + "errors": [], + "stdout": [ + { + "text": "🔍 [AUDIT] Starting comprehensive E2E audit...\n" + }, + { + "text": "🔍 [AUDIT] Step 1: Navigating to login...\n" + }, + { + "text": "🔍 [AUDIT] Step 2: Submitting login form...\n" + }, + { + "text": "✅ [AUDIT] Login successful, redirected to: http://localhost:3000/dashboard\n" + }, + { + "text": "🔍 [AUDIT] Step 3: Testing page navigation and lazy loading...\n" + }, + { + "text": " → Checking /dashboard...\n" + }, + { + "text": " → Navigating to /profile...\n" + }, + { + "text": " → Navigating to /settings...\n" + }, + { + "text": " → Navigating to /library...\n" + }, + { + "text": "\n📊 [AUDIT] === AUDIT SUMMARY ===\n" + }, + { + "text": "Global Status: UNSTABLE\n" + }, + { + "text": "Login Success: true\n" + }, + { + "text": "Pages Checked: 4\n" + }, + { + "text": "Total Issues: 3\n" + }, + { + "text": " - Critical: 3\n" + }, + { + "text": " - High: 0\n" + }, + { + "text": " - Medium: 0\n" + }, + { + "text": " - Low: 0\n" + }, + { + "text": "By Category:\n" + }, + { + "text": " - NETWORK: 0\n" + }, + { + "text": " - CONSOLE: 0\n" + }, + { + "text": " - NAVIGATION: 3\n" + }, + { + "text": " - UX: 0\n" + }, + { + "text": "📄 [AUDIT] JSON report written to: /home/senke/git/talas/veza/apps/web/RUNTIME_ISSUES.json\n" + }, + { + "text": "📄 [AUDIT] Markdown report written to: /home/senke/git/talas/veza/apps/web/RUNTIME_AUDIT_REPORT.md\n" + } + ], + "stderr": [ + { + "text": "❌ [AUDIT] Application is UNSTABLE\n" + } + ], + "retry": 0, + "startTime": "2025-12-17T12:08:22.106Z", + "annotations": [], + "attachments": [] + } + ], + "status": "expected" + } + ], + "id": "31d47f59884aa29aa4c6-10f97fe6bc18ecc2bb0a", + "file": "deep_audit.spec.ts", + "line": 202, + "column": 3 + } + ] + } + ] + } + ], + "errors": [], + "stats": { + "startTime": "2025-12-17T12:08:21.296Z", + "duration": 39518.613, + "expected": 1, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + } +} \ No newline at end of file diff --git a/apps/web/e2e/deep_audit.spec.ts b/apps/web/e2e/deep_audit.spec.ts new file mode 100644 index 000000000..edd77cc8d --- /dev/null +++ b/apps/web/e2e/deep_audit.spec.ts @@ -0,0 +1,710 @@ +import { test, expect, type Page } from '@playwright/test'; +import { writeFileSync } from 'fs'; +import { join } from 'path'; + +/** + * Deep E2E Audit - Runtime Stability Check + * + * Ce test effectue un parcours utilisateur complet et capture toutes les erreurs + * Runtime, RĂ©seau, et d'IntĂ©gration pour valider la stabilitĂ© aprĂšs les corrections + * de lazy loading. + */ + +// Configuration +const FRONTEND_URL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000'; +const TEST_EMAIL = process.env.TEST_EMAIL || 'user@example.com'; +const TEST_PASSWORD = process.env.TEST_PASSWORD || 'password123'; + +// Types pour le rapport +interface RuntimeIssue { + id: string; + category: 'NETWORK' | 'CONSOLE' | 'NAVIGATION' | 'UX'; + severity: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW'; + location: string; + message: string; + details?: string; + reproduction_steps: string; + timestamp?: string; +} + +interface PageCheckResult { + path: string; + loaded: boolean; + hasContent: boolean; + errors: RuntimeIssue[]; + loadTime?: number; +} + +interface AuditReport { + globalStatus: 'STABLE' | 'UNSTABLE'; + loginSuccess: boolean; + pages: PageCheckResult[]; + allIssues: RuntimeIssue[]; + summary: { + totalIssues: number; + critical: number; + high: number; + medium: number; + low: number; + byCategory: { + NETWORK: number; + CONSOLE: number; + NAVIGATION: number; + UX: number; + }; + }; +} + +// Collecteurs globaux +let allIssues: RuntimeIssue[] = []; +let issueCounter = 1; + +function generateIssueId(): string { + return `RUN-${String(issueCounter++).padStart(3, '0')}`; +} + +function addIssue(issue: Omit): void { + allIssues.push({ + ...issue, + id: generateIssueId(), + timestamp: new Date().toISOString(), + }); +} + +// Helper pour vĂ©rifier qu'une page a du contenu +async function checkPageHasContent(page: Page, selectors: string[]): Promise { + // VĂ©rifier d'abord que le body n'est pas vide + const bodyText = await page.locator('body').textContent().catch(() => ''); + if (!bodyText || bodyText.trim().length < 10) { + return false; + } + + // VĂ©rifier les sĂ©lecteurs spĂ©cifiques + for (const selector of selectors) { + try { + const count = await page.locator(selector).count(); + if (count > 0) { + const element = await page.locator(selector).first(); + if (await element.isVisible({ timeout: 2000 })) { + return true; + } + } + } catch { + continue; + } + } + + // Si aucun sĂ©lecteur spĂ©cifique n'est trouvĂ©, vĂ©rifier qu'il y a au moins du contenu dans main ou body + const mainContent = await page.locator('main, [role="main"], .main-content').first().textContent().catch(() => ''); + if (mainContent && mainContent.trim().length > 10) { + return true; + } + + return false; +} + +// Helper pour attendre qu'une page charge +async function waitForPageLoad( + page: Page, + expectedPath: string, + contentSelectors: string[], + timeout = 10000 +): Promise { + const startTime = Date.now(); + const result: PageCheckResult = { + path: expectedPath, + loaded: false, + hasContent: false, + errors: [], + }; + + try { + // VĂ©rifier d'abord si on est dĂ©jĂ  sur la bonne page + const currentPath = new URL(page.url()).pathname; + if (currentPath !== expectedPath) { + // Attendre la navigation seulement si on n'est pas dĂ©jĂ  sur la bonne page + await page.waitForURL( + (url) => url.pathname === expectedPath, + { timeout, waitUntil: 'domcontentloaded' } + ); + } + result.loaded = true; + + // Attendre que le rĂ©seau soit idle + await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => { + addIssue({ + category: 'NAVIGATION', + severity: 'MEDIUM', + location: expectedPath, + message: 'Page took too long to reach networkidle state', + details: `Timeout after 5s waiting for networkidle`, + reproduction_steps: `Navigate to ${expectedPath}`, + }); + }); + + // VĂ©rifier qu'il y a du contenu + result.hasContent = await checkPageHasContent(page, contentSelectors); + + if (!result.hasContent) { + addIssue({ + category: 'UX', + severity: 'HIGH', + location: expectedPath, + message: 'Page appears to be blank or empty', + details: `None of the expected selectors found: ${contentSelectors.join(', ')}`, + reproduction_steps: `Navigate to ${expectedPath} after login`, + }); + } + + result.loadTime = Date.now() - startTime; + } catch (error) { + result.loaded = false; + addIssue({ + category: 'NAVIGATION', + severity: 'CRITICAL', + location: expectedPath, + message: `Failed to navigate to ${expectedPath}`, + details: error instanceof Error ? error.message : String(error), + reproduction_steps: `Navigate to ${expectedPath} after login`, + }); + } + + return result; +} + +test.describe('Deep E2E Runtime Audit', () => { + let report: AuditReport; + + test.beforeEach(() => { + allIssues = []; + issueCounter = 1; + report = { + globalStatus: 'STABLE', + loginSuccess: false, + pages: [], + allIssues: [], + summary: { + totalIssues: 0, + critical: 0, + high: 0, + medium: 0, + low: 0, + byCategory: { + NETWORK: 0, + CONSOLE: 0, + NAVIGATION: 0, + UX: 0, + }, + }, + }; + }); + + test('Complete User Journey - Runtime Audit', async ({ page, context }) => { + test.setTimeout(60000); // 60 secondes pour le test complet + console.log('🔍 [AUDIT] Starting comprehensive E2E audit...'); + + // ============================================ + // PHASE 1: Setup Error Listeners + // ============================================ + + // Console errors & warnings + page.on('console', (msg) => { + const type = msg.type(); + const text = msg.text(); + const location = page.url(); + + if (type === 'error') { + addIssue({ + category: 'CONSOLE', + severity: 'HIGH', + location, + message: text, + details: `Console error: ${text}`, + reproduction_steps: `Navigate to ${location}`, + }); + console.log(`🔮 [CONSOLE ERROR] ${text}`); + } else if (type === 'warning') { + // MĂȘme les warnings sont capturĂ©s (comme demandĂ©) + addIssue({ + category: 'CONSOLE', + severity: 'MEDIUM', + location, + message: text, + details: `Console warning: ${text}`, + reproduction_steps: `Navigate to ${location}`, + }); + console.log(`🟡 [CONSOLE WARNING] ${text}`); + } + }); + + // Page errors (uncaught exceptions) + page.on('pageerror', (error) => { + addIssue({ + category: 'CONSOLE', + severity: 'CRITICAL', + location: page.url(), + message: error.message, + details: error.stack, + reproduction_steps: `Navigate to ${page.url()}`, + }); + console.log(`🔮 [PAGE ERROR] ${error.message}`); + }); + + // Network errors (4xx, 5xx) + page.on('response', async (response) => { + const status = response.status(); + const url = response.url(); + const method = response.request().method(); + + if (status >= 400) { + const severity = status >= 500 ? 'CRITICAL' : status >= 400 ? 'HIGH' : 'MEDIUM'; + + // Essayer de rĂ©cupĂ©rer le body de l'erreur pour plus de dĂ©tails + let errorDetails = `Server responded with status ${status}`; + try { + const responseBody = await response.text().catch(() => ''); + if (responseBody) { + try { + const parsed = JSON.parse(responseBody); + if (parsed && parsed.error) { + errorDetails = `${errorDetails}. Error: ${parsed.error}`; + } else if (parsed && parsed.message) { + errorDetails = `${errorDetails}. Message: ${parsed.message}`; + } + } catch { + // Si ce n'est pas du JSON, prendre un extrait du texte + if (responseBody.length < 200) { + errorDetails = `${errorDetails}. Response: ${responseBody.substring(0, 200)}`; + } + } + } + } catch { + // Ignore si on ne peut pas parser la rĂ©ponse + } + + addIssue({ + category: 'NETWORK', + severity, + location: page.url(), + message: `HTTP ${status} - ${method} ${url}`, + details: errorDetails, + reproduction_steps: `Navigate to ${page.url()}`, + }); + console.log(`🔮 [NETWORK ERROR] ${method} ${url} -> ${status}`); + } + }); + + // Failed requests (network failures) + page.on('requestfailed', (request) => { + const failure = request.failure(); + if (failure) { + const url = request.url(); + const method = request.method(); + + // Ne pas reporter les erreurs de favicon ou de ressources statiques non critiques + if (url.includes('favicon') || url.includes('.ico') || url.includes('chrome-extension')) { + return; + } + + addIssue({ + category: 'NETWORK', + severity: 'CRITICAL', + location: page.url(), + message: `Request failed: ${method} ${url}`, + details: failure.errorText || 'Network error', + reproduction_steps: `Navigate to ${page.url()}`, + }); + console.log(`🔮 [REQUEST FAILED] ${method} ${url}: ${failure.errorText}`); + } + }); + + // ============================================ + // PHASE 2: Login Flow + // ============================================ + + console.log('🔍 [AUDIT] Step 1: Navigating to login...'); + await page.goto(`${FRONTEND_URL}/login`, { + waitUntil: 'domcontentloaded', + timeout: 30000, + }); + + await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => { + console.warn('⚠ [AUDIT] Timeout on networkidle for login page'); + }); + + // Attendre que le formulaire soit chargĂ© + await page.waitForSelector('input[type="email"], input[name="email"]', { + timeout: 10000, + }).catch(() => { + addIssue({ + category: 'UX', + severity: 'CRITICAL', + location: '/login', + message: 'Login form not found', + details: 'Email input field not visible', + reproduction_steps: 'Navigate to /login', + }); + }); + + // Remplir le formulaire + const emailInput = page.locator('input[type="email"], input[name="email"]').first(); + const passwordInput = page.locator('input[type="password"]').first(); + const submitButton = page.locator('button[type="submit"], button:has-text("connecter"), button:has-text("login"), button:has-text("Se connecter")').first(); + + await emailInput.fill(TEST_EMAIL); + await passwordInput.fill(TEST_PASSWORD); + + console.log('🔍 [AUDIT] Step 2: Submitting login form...'); + + // Attendre la navigation aprĂšs login + await submitButton.click(); + + // Attendre soit la navigation, soit un message d'erreur + try { + await page.waitForURL( + (url) => url.pathname === '/dashboard' || url.pathname === '/', + { timeout: 15000 } + ); + report.loginSuccess = true; + console.log('✅ [AUDIT] Login successful, redirected to:', page.url()); + } catch (error) { + // VĂ©rifier si on est toujours sur /login ou si on a une erreur + const currentUrl = page.url(); + const currentPath = new URL(currentUrl).pathname; + + if (currentPath === '/login') { + report.loginSuccess = false; + addIssue({ + category: 'NAVIGATION', + severity: 'CRITICAL', + location: '/login', + message: 'Login failed or did not redirect', + details: `Still on ${currentUrl} after login attempt. Check for error messages or network failures.`, + reproduction_steps: `Login with ${TEST_EMAIL}`, + }); + console.error('❌ [AUDIT] Login failed or did not redirect'); + + // Si le login Ă©choue, on gĂ©nĂšre quand mĂȘme le rapport avec les erreurs capturĂ©es + report.allIssues = allIssues; + report.summary.totalIssues = allIssues.length; + report.summary.critical = allIssues.filter((i) => i.severity === 'CRITICAL').length; + report.summary.high = allIssues.filter((i) => i.severity === 'HIGH').length; + report.summary.medium = allIssues.filter((i) => i.severity === 'MEDIUM').length; + report.summary.low = allIssues.filter((i) => i.severity === 'LOW').length; + report.summary.byCategory.NETWORK = allIssues.filter((i) => i.category === 'NETWORK').length; + report.summary.byCategory.CONSOLE = allIssues.filter((i) => i.category === 'CONSOLE').length; + report.summary.byCategory.NAVIGATION = allIssues.filter((i) => i.category === 'NAVIGATION').length; + report.summary.byCategory.UX = allIssues.filter((i) => i.category === 'UX').length; + report.globalStatus = 'UNSTABLE'; + + // Sauvegarder le rapport mĂȘme en cas d'Ă©chec + await page.evaluate((report) => { + (window as any).__auditReport = report; + }, report); + + return; + } else { + // On a naviguĂ© ailleurs (peut-ĂȘtre une page d'erreur ou autre) + report.loginSuccess = false; + addIssue({ + category: 'NAVIGATION', + severity: 'HIGH', + location: currentPath, + message: 'Login redirected to unexpected page', + details: `Expected /dashboard but got ${currentUrl}`, + reproduction_steps: `Login with ${TEST_EMAIL}`, + }); + console.warn('⚠ [AUDIT] Login redirected to unexpected page:', currentUrl); + } + } + + // Attendre un peu pour que l'app se stabilise + await page.waitForTimeout(2000); + + // ============================================ + // PHASE 3: Navigation & Lazy Loading Check + // ============================================ + + console.log('🔍 [AUDIT] Step 3: Testing page navigation and lazy loading...'); + + // Dashboard (dĂ©jĂ  chargĂ©) + console.log(' → Checking /dashboard...'); + // S'assurer qu'on est bien sur /dashboard + if (new URL(page.url()).pathname !== '/dashboard') { + await page.goto(`${FRONTEND_URL}/dashboard`, { waitUntil: 'domcontentloaded' }); + } + const dashboardCheck = await waitForPageLoad( + page, + '/dashboard', + ['[data-testid="dashboard"]', 'h1', 'main', '.container', 'nav', 'aside'], + ); + report.pages.push(dashboardCheck); + + // Profile page + console.log(' → Navigating to /profile...'); + await page.goto(`${FRONTEND_URL}/profile`, { waitUntil: 'domcontentloaded', timeout: 15000 }); + await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {}); + await page.waitForTimeout(1000); // Attendre que le lazy loading se stabilise + const profileCheck = await waitForPageLoad( + page, + '/profile', + ['h1', 'main', '.container', '[data-testid="profile"]', 'form', 'button'], + ); + report.pages.push(profileCheck); + + // Settings page + console.log(' → Navigating to /settings...'); + await page.goto(`${FRONTEND_URL}/settings`, { waitUntil: 'domcontentloaded', timeout: 15000 }); + await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {}); + await page.waitForTimeout(1000); // Attendre que le lazy loading se stabilise + const settingsCheck = await waitForPageLoad( + page, + '/settings', + ['h1:has-text("ParamĂštres"), h1:has-text("Settings"), h1', 'main', '.container', 'form', 'button'], + ); + report.pages.push(settingsCheck); + + // Library page + console.log(' → Navigating to /library...'); + await page.goto(`${FRONTEND_URL}/library`, { waitUntil: 'domcontentloaded', timeout: 15000 }); + await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {}); + await page.waitForTimeout(1000); // Attendre que le lazy loading se stabilise + const libraryCheck = await waitForPageLoad( + page, + '/library', + ['h1', 'main', '.container', '[data-testid="library"]', 'button', 'div'], + ); + report.pages.push(libraryCheck); + + // ============================================ + // PHASE 4: Generate Report + // ============================================ + + report.allIssues = allIssues; + + // Calculer le rĂ©sumĂ© + report.summary.totalIssues = allIssues.length; + report.summary.critical = allIssues.filter((i) => i.severity === 'CRITICAL').length; + report.summary.high = allIssues.filter((i) => i.severity === 'HIGH').length; + report.summary.medium = allIssues.filter((i) => i.severity === 'MEDIUM').length; + report.summary.low = allIssues.filter((i) => i.severity === 'LOW').length; + + report.summary.byCategory.NETWORK = allIssues.filter((i) => i.category === 'NETWORK').length; + report.summary.byCategory.CONSOLE = allIssues.filter((i) => i.category === 'CONSOLE').length; + report.summary.byCategory.NAVIGATION = allIssues.filter((i) => i.category === 'NAVIGATION').length; + report.summary.byCategory.UX = allIssues.filter((i) => i.category === 'UX').length; + + // DĂ©terminer le statut global + if ( + report.summary.critical > 0 || + !report.loginSuccess || + report.pages.some((p) => !p.loaded || !p.hasContent) + ) { + report.globalStatus = 'UNSTABLE'; + } + + // Afficher le rĂ©sumĂ© dans la console + console.log('\n📊 [AUDIT] === AUDIT SUMMARY ==='); + console.log(`Global Status: ${report.globalStatus}`); + console.log(`Login Success: ${report.loginSuccess}`); + console.log(`Pages Checked: ${report.pages.length}`); + console.log(`Total Issues: ${report.summary.totalIssues}`); + console.log(` - Critical: ${report.summary.critical}`); + console.log(` - High: ${report.summary.high}`); + console.log(` - Medium: ${report.summary.medium}`); + console.log(` - Low: ${report.summary.low}`); + console.log(`By Category:`); + console.log(` - NETWORK: ${report.summary.byCategory.NETWORK}`); + console.log(` - CONSOLE: ${report.summary.byCategory.CONSOLE}`); + console.log(` - NAVIGATION: ${report.summary.byCategory.NAVIGATION}`); + console.log(` - UX: ${report.summary.byCategory.UX}`); + + // Sauvegarder le rapport dans la page pour rĂ©cupĂ©ration + await page.evaluate((report) => { + (window as any).__auditReport = report; + }, report); + + // Assertions finales (ne pas faire Ă©chouer le test, juste logger) + if (report.globalStatus === 'UNSTABLE') { + console.error('❌ [AUDIT] Application is UNSTABLE'); + } else { + console.log('✅ [AUDIT] Application appears STABLE'); + } + }); + + test.afterEach(async ({ page }) => { + // RĂ©cupĂ©rer le rapport depuis la page + const savedReport = await page + .evaluate(() => { + return (window as any).__auditReport; + }) + .catch(() => null); + + if (savedReport) { + report = savedReport; + } + + // Écrire les rapports dans des fichiers + // Utiliser process.cwd() car __dirname peut ne pas ĂȘtre disponible en ESM + const projectRoot = process.cwd(); + + // Rapport JSON + const jsonPath = join(projectRoot, 'RUNTIME_ISSUES.json'); + writeFileSync(jsonPath, JSON.stringify(report.allIssues, null, 2)); + console.log(`📄 [AUDIT] JSON report written to: ${jsonPath}`); + + // Rapport Markdown + const mdPath = join(projectRoot, 'RUNTIME_AUDIT_REPORT.md'); + const mdContent = generateMarkdownReport(report); + writeFileSync(mdPath, mdContent); + console.log(`📄 [AUDIT] Markdown report written to: ${mdPath}`); + }); +}); + +function generateMarkdownReport(report: AuditReport): string { + const lines: string[] = []; + + lines.push('# Runtime Audit Report'); + lines.push(''); + lines.push(`**Generated:** ${new Date().toISOString()}`); + lines.push(''); + lines.push('---'); + lines.push(''); + + // État Global + lines.push('## État Global'); + lines.push(''); + lines.push(`**Status:** ${report.globalStatus === 'STABLE' ? '✅ STABLE' : '❌ UNSTABLE'}`); + lines.push(`**Login Success:** ${report.loginSuccess ? '✅ Yes' : '❌ No'}`); + lines.push(''); + + // Parcours + lines.push('## Parcours Utilisateur'); + lines.push(''); + lines.push('| Page | Loaded | Has Content | Load Time (ms) |'); + lines.push('|------|--------|-------------|----------------|'); + for (const page of report.pages) { + const loaded = page.loaded ? '✅' : '❌'; + const content = page.hasContent ? '✅' : '❌'; + const loadTime = page.loadTime ? `${page.loadTime}ms` : 'N/A'; + lines.push(`| ${page.path} | ${loaded} | ${content} | ${loadTime} |`); + } + lines.push(''); + + // RĂ©sumĂ© des erreurs + lines.push('## RĂ©sumĂ© des Erreurs'); + lines.push(''); + lines.push(`**Total Issues:** ${report.summary.totalIssues}`); + lines.push(''); + lines.push('### Par SĂ©vĂ©ritĂ©'); + lines.push(''); + lines.push(`- **CRITICAL:** ${report.summary.critical}`); + lines.push(`- **HIGH:** ${report.summary.high}`); + lines.push(`- **MEDIUM:** ${report.summary.medium}`); + lines.push(`- **LOW:** ${report.summary.low}`); + lines.push(''); + + lines.push('### Par CatĂ©gorie'); + lines.push(''); + lines.push(`- **NETWORK:** ${report.summary.byCategory.NETWORK}`); + lines.push(`- **CONSOLE:** ${report.summary.byCategory.CONSOLE}`); + lines.push(`- **NAVIGATION:** ${report.summary.byCategory.NAVIGATION}`); + lines.push(`- **UX:** ${report.summary.byCategory.UX}`); + lines.push(''); + + // Erreurs Console + const consoleErrors = report.allIssues.filter((i) => i.category === 'CONSOLE'); + if (consoleErrors.length > 0) { + lines.push('## Erreurs Console'); + lines.push(''); + for (const error of consoleErrors) { + lines.push(`### ${error.id} - ${error.severity}`); + lines.push(''); + lines.push(`- **Location:** ${error.location}`); + lines.push(`- **Message:** ${error.message}`); + if (error.details) { + lines.push(`- **Details:** ${error.details}`); + } + lines.push(`- **Reproduction:** ${error.reproduction_steps}`); + lines.push(''); + } + } + + // Erreurs RĂ©seau + const networkErrors = report.allIssues.filter((i) => i.category === 'NETWORK'); + if (networkErrors.length > 0) { + lines.push('## Erreurs RĂ©seau'); + lines.push(''); + for (const error of networkErrors) { + lines.push(`### ${error.id} - ${error.severity}`); + lines.push(''); + lines.push(`- **Location:** ${error.location}`); + lines.push(`- **Message:** ${error.message}`); + if (error.details) { + lines.push(`- **Details:** ${error.details}`); + } + lines.push(`- **Reproduction:** ${error.reproduction_steps}`); + lines.push(''); + } + } + + // Erreurs Navigation + const navErrors = report.allIssues.filter((i) => i.category === 'NAVIGATION'); + if (navErrors.length > 0) { + lines.push('## Erreurs Navigation'); + lines.push(''); + for (const error of navErrors) { + lines.push(`### ${error.id} - ${error.severity}`); + lines.push(''); + lines.push(`- **Location:** ${error.location}`); + lines.push(`- **Message:** ${error.message}`); + if (error.details) { + lines.push(`- **Details:** ${error.details}`); + } + lines.push(`- **Reproduction:** ${error.reproduction_steps}`); + lines.push(''); + } + } + + // Erreurs UX + const uxErrors = report.allIssues.filter((i) => i.category === 'UX'); + if (uxErrors.length > 0) { + lines.push('## Erreurs UX'); + lines.push(''); + for (const error of uxErrors) { + lines.push(`### ${error.id} - ${error.severity}`); + lines.push(''); + lines.push(`- **Location:** ${error.location}`); + lines.push(`- **Message:** ${error.message}`); + if (error.details) { + lines.push(`- **Details:** ${error.details}`); + } + lines.push(`- **Reproduction:** ${error.reproduction_steps}`); + lines.push(''); + } + } + + if (report.allIssues.length === 0) { + lines.push('## ✅ Aucune Erreur DĂ©tectĂ©e'); + lines.push(''); + lines.push('L\'application semble stable. Aucune erreur runtime, rĂ©seau ou d\'intĂ©gration n\'a Ă©tĂ© dĂ©tectĂ©e.'); + } + + // Note sur les limitations du test + if (!report.loginSuccess) { + lines.push('---'); + lines.push(''); + lines.push('## ⚠ Note sur les Limitations du Test'); + lines.push(''); + lines.push('Le test n\'a pas pu continuer au-delĂ  de la page de login car le backend n\'Ă©tait pas accessible.'); + lines.push('Les pages protĂ©gĂ©es (Dashboard, Profile, Settings, Library) n\'ont donc pas pu ĂȘtre testĂ©es.'); + lines.push(''); + lines.push('**Pour un audit complet :**'); + lines.push('1. DĂ©marrer le backend API sur `http://localhost:8080`'); + lines.push('2. Configurer CORS pour autoriser les requĂȘtes depuis `http://localhost:3000`'); + lines.push('3. Relancer le test avec `npx playwright test e2e/deep_audit.spec.ts`'); + lines.push(''); + } + + return lines.join('\n'); +} diff --git a/apps/web/e2e/diagnostic-login-page.png b/apps/web/e2e/diagnostic-login-page.png new file mode 100644 index 0000000000000000000000000000000000000000..bf218f30d41cb10495858ee960e65f3e2e984ce9 GIT binary patch literal 119727 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 Cz3GAg literal 0 HcmV?d00001 diff --git a/apps/web/e2e/diagnostic.spec.ts b/apps/web/e2e/diagnostic.spec.ts new file mode 100644 index 000000000..ecba54785 --- /dev/null +++ b/apps/web/e2e/diagnostic.spec.ts @@ -0,0 +1,346 @@ +import { test, expect, type Page } from '@playwright/test'; + +/** + * Diagnostic Test - Full Stack Compatibility Check + * + * Ce test vĂ©rifie l'intĂ©gration Frontend-Backend aprĂšs le refactoring de l'authentification. + * Il capture toutes les erreurs rĂ©seau, console, CORS et vĂ©rifie le stockage des tokens. + */ + +// Configuration +const BASE_URL = process.env.VITE_API_URL || 'http://localhost:8080/api/v1'; +const FRONTEND_URL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000'; +const TEST_EMAIL = process.env.TEST_EMAIL || 'user@example.com'; +const TEST_PASSWORD = process.env.TEST_PASSWORD || 'password123'; + +// Collecteurs d'erreurs +interface DiagnosticReport { + networkErrors: Array<{ + url: string; + status: number; + method: string; + error: string; + }>; + consoleErrors: Array<{ + type: string; + message: string; + stack?: string; + }>; + corsErrors: Array<{ + url: string; + reason: string; + }>; + localStorage: Record; + navigationSuccess: boolean; + finalUrl: string; + formVisible: boolean; + errorMessage?: string; +} + +test.describe('Full Stack Compatibility Diagnostic', () => { + let report: DiagnosticReport; + + test.beforeEach(() => { + report = { + networkErrors: [], + consoleErrors: [], + corsErrors: [], + localStorage: {}, + navigationSuccess: false, + finalUrl: '', + formVisible: false, + }; + }); + + test('Login Flow - Complete Diagnostic', async ({ page, context }) => { + // Setup: Écouter les erreurs console AVANT toute navigation + const consoleMessages: Array<{ type: string; text: string }> = []; + page.on('console', (msg) => { + const type = msg.type(); + const text = msg.text(); + consoleMessages.push({ type, text }); + + if (type === 'error' || type === 'warning') { + report.consoleErrors.push({ + type, + message: text, + stack: msg.location()?.url, + }); + console.log(`🔮 [CONSOLE ${type.toUpperCase()}] ${text}`); + } + }); + + // Setup: Écouter les erreurs de page (uncaught exceptions) + page.on('pageerror', (error) => { + report.consoleErrors.push({ + type: 'pageerror', + message: error.message, + stack: error.stack, + }); + console.log(`🔮 [PAGE ERROR] ${error.message}`); + }); + + // Setup: Écouter les requĂȘtes rĂ©seau Ă©chouĂ©es + page.on('response', (response) => { + const status = response.status(); + const url = response.url(); + + // Capturer les erreurs 4xx et 5xx + if (status >= 400) { + report.networkErrors.push({ + url, + status, + method: response.request().method(), + error: `HTTP ${status}`, + }); + + // DĂ©tecter les erreurs CORS potentielles + if (status === 0 || url.includes('localhost:8080')) { + const headers = response.headers(); + if (!headers['access-control-allow-origin']) { + report.corsErrors.push({ + url, + reason: 'Missing CORS headers', + }); + } + } + } + }); + + // Setup: Écouter les requĂȘtes Ă©chouĂ©es (network errors) + page.on('requestfailed', (request) => { + const failure = request.failure(); + if (failure) { + report.networkErrors.push({ + url: request.url(), + status: 0, + method: request.method(), + error: failure.errorText || 'Network error', + }); + + // DĂ©tecter les erreurs CORS + if (failure.errorText?.includes('CORS') || failure.errorText?.includes('Access-Control')) { + report.corsErrors.push({ + url: request.url(), + reason: failure.errorText, + }); + } + } + }); + + // Étape 1: Aller sur la page de login + console.log('🔍 [DIAGNOSTIC] Navigation vers /login...'); + try { + await page.goto(`${FRONTEND_URL}/login`, { waitUntil: 'domcontentloaded', timeout: 30000 }); + } catch (error) { + console.error('❌ [DIAGNOSTIC] Erreur lors de la navigation:', error); + report.finalUrl = page.url(); + return; + } + + // Attendre que la page soit chargĂ©e + await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => { + console.warn('⚠ [DIAGNOSTIC] Timeout sur networkidle, continuation...'); + }); + + // Prendre une capture d'Ă©cran pour debug + await page.screenshot({ path: 'e2e/diagnostic-login-page.png', fullPage: true }); + + // Attendre que le formulaire soit chargĂ© (plusieurs stratĂ©gies) + try { + // Essayer d'attendre un Ă©lĂ©ment de formulaire + await page.waitForSelector('form, input[type="email"], input[type="password"]', { timeout: 10000 }); + } catch (e) { + console.warn('⚠ [DIAGNOSTIC] Timeout en attendant le formulaire'); + } + + // Attendre un peu pour que React hydrate + await page.waitForTimeout(3000); + + // VĂ©rifier que le formulaire est visible avec plusieurs sĂ©lecteurs possibles + const emailInput = page.locator('input[type="email"], input[name="email"], input[placeholder*="email" i]').first(); + const passwordInput = page.locator('input[type="password"], input[name="password"]').first(); + const submitButton = page.locator('button[type="submit"], button:has-text("connecter"), button:has-text("login"), button:has-text("Se connecter")').first(); + + // VĂ©rifier aussi avec des sĂ©lecteurs plus gĂ©nĂ©riques + const allInputs = await page.locator('input').count(); + const allButtons = await page.locator('button').count(); + console.log('📄 [DIAGNOSTIC] Nombre d\'inputs sur la page:', allInputs); + console.log('📄 [DIAGNOSTIC] Nombre de boutons sur la page:', allButtons); + + const emailVisible = await emailInput.isVisible().catch(() => false); + const passwordVisible = await passwordInput.isVisible().catch(() => false); + const submitVisible = await submitButton.isVisible().catch(() => false); + + report.formVisible = emailVisible && passwordVisible; + + // Logger le contenu de la page pour debug + const pageContent = await page.content(); + const hasForm = pageContent.includes('form') || pageContent.includes('email') || pageContent.includes('password'); + + console.log('📄 [DIAGNOSTIC] Page title:', await page.title()); + console.log('📄 [DIAGNOSTIC] URL actuelle:', page.url()); + console.log('📄 [DIAGNOSTIC] Email input visible:', emailVisible); + console.log('📄 [DIAGNOSTIC] Password input visible:', passwordVisible); + console.log('📄 [DIAGNOSTIC] Submit button visible:', submitVisible); + console.log('📄 [DIAGNOSTIC] Page contient "form":', hasForm); + + if (!report.formVisible) { + console.error('❌ [DIAGNOSTIC] Le formulaire de login n\'est pas visible'); + + // Logger le HTML pour debug + const bodyText = await page.locator('body').textContent(); + console.log('📄 [DIAGNOSTIC] Contenu de la page (premiers 500 chars):', bodyText?.substring(0, 500)); + + // Logger toutes les erreurs console capturĂ©es + if (consoleMessages.length > 0) { + console.log('\n🔮 [DIAGNOSTIC] Messages console capturĂ©s:'); + consoleMessages.forEach((msg) => { + console.log(` [${msg.type}] ${msg.text}`); + }); + } + + // VĂ©rifier s'il y a des scripts qui ont Ă©chouĂ© Ă  charger + const failedResources = await page.evaluate(() => { + const resources: Array<{ url: string; error: string }> = []; + const scripts = document.querySelectorAll('script[src]'); + scripts.forEach((script) => { + const src = script.getAttribute('src'); + if (src && !(script as any).loaded) { + resources.push({ url: src, error: 'Script not loaded' }); + } + }); + return resources; + }); + + if (failedResources.length > 0) { + console.log('🔮 [DIAGNOSTIC] Scripts non chargĂ©s:', failedResources); + } + + // Sauvegarder le rapport mĂȘme en cas d'Ă©chec + await page.evaluate((report) => { + (window as any).__diagnosticReport = report; + }, report); + + return; + } + + console.log('✅ [DIAGNOSTIC] Formulaire de login visible'); + + // Étape 2: Remplir le formulaire + console.log('🔍 [DIAGNOSTIC] Remplissage du formulaire...'); + await emailInput.fill(TEST_EMAIL); + await passwordInput.fill(TEST_PASSWORD); + + // VĂ©rifier si checkbox "remember me" existe + const rememberMeCheckbox = page.locator('input[type="checkbox"][id*="remember"]'); + if (await rememberMeCheckbox.count() > 0) { + await rememberMeCheckbox.check(); + } + + // Étape 3: Cliquer sur le bouton de connexion + console.log('🔍 [DIAGNOSTIC] Clic sur le bouton de connexion...'); + + // Attendre la navigation ou un message d'erreur + const navigationPromise = page.waitForURL( + (url) => url.pathname === '/dashboard' || url.pathname === '/', + { timeout: 10000 } + ).catch(() => null); + + const errorMessagePromise = page + .waitForSelector('.bg-red-100, [role="alert"], .text-red-700', { timeout: 5000 }) + .catch(() => null); + + await submitButton.click(); + + // Attendre soit la navigation, soit un message d'erreur + const navigationResult = await navigationPromise; + const errorElement = await errorMessagePromise; + + if (navigationResult) { + report.navigationSuccess = true; + report.finalUrl = page.url(); + console.log('✅ [DIAGNOSTIC] Navigation rĂ©ussie vers:', report.finalUrl); + } else if (errorElement) { + report.errorMessage = await errorElement.textContent() || 'Erreur inconnue'; + console.log('❌ [DIAGNOSTIC] Message d\'erreur dĂ©tectĂ©:', report.errorMessage); + } else { + // Attendre un peu plus pour voir si quelque chose se passe + await page.waitForTimeout(2000); + report.finalUrl = page.url(); + console.log('⚠ [DIAGNOSTIC] Pas de navigation ni d\'erreur visible. URL actuelle:', report.finalUrl); + } + + // Étape 4: VĂ©rifier le localStorage + console.log('🔍 [DIAGNOSTIC] VĂ©rification du localStorage...'); + const localStorageData = await context.storageState(); + const localStorageItems = await page.evaluate(() => { + const items: Record = {}; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key) { + items[key] = localStorage.getItem(key) || ''; + } + } + return items; + }); + + report.localStorage = localStorageItems; + + // VĂ©rifier spĂ©cifiquement les tokens + const hasAccessToken = 'access_token' in localStorageItems || + 'veza_access_token' in localStorageItems || + localStorageItems['access_token'] !== undefined || + localStorageItems['veza_access_token'] !== undefined; + + console.log('📩 [DIAGNOSTIC] LocalStorage:', Object.keys(localStorageItems)); + console.log(hasAccessToken ? '✅ [DIAGNOSTIC] Token d\'accĂšs prĂ©sent' : '❌ [DIAGNOSTIC] Token d\'accĂšs absent'); + + // GĂ©nĂ©rer le rapport + console.log('\n📊 [DIAGNOSTIC] === RAPPORT DE DIAGNOSTIC ==='); + console.log('Erreurs rĂ©seau:', report.networkErrors.length); + console.log('Erreurs console:', report.consoleErrors.length); + console.log('Erreurs CORS:', report.corsErrors.length); + console.log('Navigation rĂ©ussie:', report.navigationSuccess); + console.log('Token prĂ©sent:', hasAccessToken); + + // Afficher les dĂ©tails des erreurs + if (report.networkErrors.length > 0) { + console.log('\n🔮 Erreurs rĂ©seau:'); + report.networkErrors.forEach((err) => { + console.log(` - ${err.method} ${err.url}: ${err.error}`); + }); + } + + if (report.consoleErrors.length > 0) { + console.log('\n🔮 Erreurs console:'); + report.consoleErrors.forEach((err) => { + console.log(` - [${err.type}] ${err.message}`); + }); + } + + if (report.corsErrors.length > 0) { + console.log('\n🟠 Erreurs CORS:'); + report.corsErrors.forEach((err) => { + console.log(` - ${err.url}: ${err.reason}`); + }); + } + + // Sauvegarder le rapport pour l'analyse + await page.evaluate((report) => { + (window as any).__diagnosticReport = report; + }, report); + }); + + test.afterEach(async ({ page }) => { + // RĂ©cupĂ©rer le rapport depuis la page si disponible + const savedReport = await page.evaluate(() => { + return (window as any).__diagnosticReport; + }).catch(() => null); + + if (savedReport) { + report = savedReport; + } + }); +}); + diff --git a/apps/web/src/components/ErrorBoundary.tsx b/apps/web/src/components/ErrorBoundary.tsx index 044b7ad34..a99eda6d5 100644 --- a/apps/web/src/components/ErrorBoundary.tsx +++ b/apps/web/src/components/ErrorBoundary.tsx @@ -65,7 +65,7 @@ export class ErrorBoundary extends Component { - {process.env.NODE_ENV === 'development' && this.state.error && ( + {import.meta.env.DEV && this.state.error && (

Détails de l'erreur : diff --git a/apps/web/src/components/ui/LazyComponent.tsx b/apps/web/src/components/ui/LazyComponent.tsx index 58b5804bd..bafcd3716 100644 --- a/apps/web/src/components/ui/LazyComponent.tsx +++ b/apps/web/src/components/ui/LazyComponent.tsx @@ -26,8 +26,10 @@ export function createLazyComponent>( export const LazyDashboard = createLazyComponent(() => import('@/pages/DashboardPage').then((m) => ({ default: m.DashboardPage })), ); -export const LazyChat = createLazyComponent( - () => import('@/features/chat/pages/ChatPage'), +export const LazyChat = createLazyComponent(() => + import('@/features/chat/pages/ChatPage').then((m) => ({ + default: m.ChatPage, + })), ); export const LazyLibrary = createLazyComponent( () => import('@/features/library/pages/LibraryPage'), @@ -35,8 +37,10 @@ export const LazyLibrary = createLazyComponent( export const LazyProfile = createLazyComponent(() => import('@/pages/ProfilePage').then((m) => ({ default: m.ProfilePage })), ); -export const LazySettings = createLazyComponent( - () => import('@/features/settings/pages/SettingsPage'), +export const LazySettings = createLazyComponent(() => + import('@/features/settings/pages/SettingsPage').then((m) => ({ + default: m.SettingsPage, + })), ); export const LazyLogin = createLazyComponent(() => import('@/pages/LoginPage').then((m) => ({ default: m.LoginPage })), @@ -82,6 +86,8 @@ export const LazyPlaylistRoutes = createLazyComponent(() => default: m.PlaylistRoutes, })), ); -export const LazyMarketplace = createLazyComponent( - () => import('@/pages/marketplace/MarketplaceHome'), +export const LazyMarketplace = createLazyComponent(() => + import('@/pages/marketplace/MarketplaceHome').then((m) => ({ + default: m.MarketplaceHome, + })), ); diff --git a/apps/web/src/config/constants.ts b/apps/web/src/config/constants.ts index ee14f8941..95d0585da 100644 --- a/apps/web/src/config/constants.ts +++ b/apps/web/src/config/constants.ts @@ -4,10 +4,10 @@ // URLs de l'API export const API_URLS = { - BASE: process.env.VITE_API_URL || 'http://localhost:8080', - WS: process.env.VITE_WS_URL || 'ws://localhost:8081', - UPLOAD: process.env.VITE_UPLOAD_URL || 'http://localhost:8080/upload', - STREAM: process.env.VITE_STREAM_URL || 'http://localhost:8082', + BASE: import.meta.env.VITE_API_URL || 'http://127.0.0.1:8080', + WS: import.meta.env.VITE_WS_URL || 'ws://127.0.0.1:8081', + UPLOAD: import.meta.env.VITE_UPLOAD_URL || 'http://127.0.0.1:8080/upload', + STREAM: import.meta.env.VITE_STREAM_URL || 'ws://127.0.0.1:8082', } as const; // Endpoints de l'API diff --git a/apps/web/src/config/env.ts b/apps/web/src/config/env.ts index 9486b5836..8bc7e196c 100644 --- a/apps/web/src/config/env.ts +++ b/apps/web/src/config/env.ts @@ -3,10 +3,10 @@ import { z } from 'zod'; // Schéma de validation pour les variables d'environnement // Aligné avec FRONTEND_INTEGRATION.md const envSchema = z.object({ - VITE_API_URL: z.string().url().default('http://localhost:8080/api/v1'), - VITE_WS_URL: z.string().url().default('ws://localhost:8081/ws'), - VITE_STREAM_URL: z.string().url().default('ws://localhost:8082/stream'), - VITE_UPLOAD_URL: z.string().url().default('http://localhost:8080/upload'), + VITE_API_URL: z.string().url().default('http://127.0.0.1:8080/api/v1'), + VITE_WS_URL: z.string().url().default('ws://127.0.0.1:8081/ws'), + VITE_STREAM_URL: z.string().url().default('ws://127.0.0.1:8082/stream'), + VITE_UPLOAD_URL: z.string().url().default('http://127.0.0.1:8080/upload'), VITE_APP_NAME: z.string().default('Veza'), VITE_DEBUG: z .string() diff --git a/apps/web/src/features/chat/hooks/useChat.ts b/apps/web/src/features/chat/hooks/useChat.ts index 406ce6f25..347091d99 100644 --- a/apps/web/src/features/chat/hooks/useChat.ts +++ b/apps/web/src/features/chat/hooks/useChat.ts @@ -31,7 +31,7 @@ export const useChat = () => { ws.current.onopen = () => { setWsStatus('connected'); - console.log('WebSocket connected'); + // WebSocket connection successful - no logging needed in production // Send any queued messages setMessagesToSend((prev) => { prev.forEach((msg) => ws.current?.send(JSON.stringify(msg))); @@ -59,7 +59,7 @@ export const useChat = () => { ws.current.onclose = () => { setWsStatus('disconnected'); - console.log('WebSocket disconnected'); + // WebSocket disconnected - no logging needed in production // Optional: Reconnect logic }; @@ -96,7 +96,7 @@ export const useChat = () => { !currentConversationId || !userId ) { - console.warn('WebSocket not open, cannot send message'); + // WebSocket not ready - message will be queued // Queue message to send later setMessagesToSend((prev) => [ ...prev, diff --git a/apps/web/src/features/library/components/LibraryManager.tsx b/apps/web/src/features/library/components/LibraryManager.tsx index 6d639ae29..c9ff08f3d 100644 --- a/apps/web/src/features/library/components/LibraryManager.tsx +++ b/apps/web/src/features/library/components/LibraryManager.tsx @@ -109,7 +109,8 @@ export function LibraryManager({ onTrackSelect }: LibraryManagerProps) { if (originalTrack) { // setSelectedTrack(originalTrack); // setIsEditDialogOpen(true); - console.log('Edit track', originalTrack); // Temporary + // TODO: Implement edit track functionality + // Removed temporary console.log for production } }; diff --git a/apps/web/src/features/player/components/PlayerError.tsx b/apps/web/src/features/player/components/PlayerError.tsx index 06877176c..66fd9dcdc 100644 --- a/apps/web/src/features/player/components/PlayerError.tsx +++ b/apps/web/src/features/player/components/PlayerError.tsx @@ -123,7 +123,7 @@ export function PlayerError({ )} - {process.env.NODE_ENV === 'development' && ( + {import.meta.env.DEV && (
Détails techniques diff --git a/apps/web/src/features/player/hooks/useStreamSync.ts b/apps/web/src/features/player/hooks/useStreamSync.ts index 5a2a8c8ef..1d9442946 100644 --- a/apps/web/src/features/player/hooks/useStreamSync.ts +++ b/apps/web/src/features/player/hooks/useStreamSync.ts @@ -62,11 +62,8 @@ export function useStreamSync(params: { const targetTime = currentSec - correctionSec; - if (import.meta.env.DEV) { - console.log( - `[Sync] Correcting drift: ${driftMs}ms. Seek ${currentSec} -> ${targetTime}`, - ); - } + // Stream sync correction - logging removed for production + // Debug info available via onDebug callback if needed audioPlayerService.seek(targetTime); } }, diff --git a/apps/web/src/features/playlists/hooks/usePlaylistPermissions.ts b/apps/web/src/features/playlists/hooks/usePlaylistPermissions.ts index 59e10e08f..6cffe142a 100644 --- a/apps/web/src/features/playlists/hooks/usePlaylistPermissions.ts +++ b/apps/web/src/features/playlists/hooks/usePlaylistPermissions.ts @@ -7,7 +7,15 @@ export function usePlaylistPermissions(playlist?: Playlist) { return useMemo(() => { if (!playlist || !user) { - return { canEdit: false, canDelete: false }; + return { + canEdit: false, + canDelete: false, + canAddTracks: false, + canRemoveTracks: false, + canManageCollaborators: false, + canRead: false, + isOwner: false, + }; } const isOwner = String(playlist.user_id) === String(user.id); @@ -16,6 +24,11 @@ export function usePlaylistPermissions(playlist?: Playlist) { return { canEdit: isOwner, canDelete: isOwner, + canAddTracks: isOwner, + canRemoveTracks: isOwner, + canManageCollaborators: isOwner, + canRead: true, // Anyone can read public playlists, owner can read private ones + isOwner, }; }, [playlist, user]); } diff --git a/apps/web/src/features/playlists/pages/PlaylistDetailPage.tsx b/apps/web/src/features/playlists/pages/PlaylistDetailPage.tsx new file mode 100644 index 000000000..1f22cb469 --- /dev/null +++ b/apps/web/src/features/playlists/pages/PlaylistDetailPage.tsx @@ -0,0 +1,127 @@ +import { useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { usePlaylist, useCollaborators } from '../hooks/usePlaylist'; +import { usePlaylistPermissions } from '../hooks/usePlaylistPermissions'; +import { PlaylistHeader } from '../components/PlaylistHeader'; +import { PlaylistActions } from '../components/PlaylistActions'; +import { PlaylistTrackList } from '../components/PlaylistTrackList'; +import { AddTrackToPlaylistModal } from '../components/AddTrackToPlaylistModal'; +import { Button } from '@/components/ui/button'; +import { LoadingSpinner } from '@/components/ui/loading-spinner'; +import { Card, CardContent } from '@/components/ui/card'; +import { Plus } from 'lucide-react'; +import { toast } from 'react-hot-toast'; + +export function PlaylistDetailPage() { + const { id } = useParams<{ id: string }>(); + const [isAddTrackModalOpen, setIsAddTrackModalOpen] = useState(false); + const [isShareModalOpen, setIsShareModalOpen] = useState(false); + + const { + data: playlist, + isLoading, + error, + refetch, + } = usePlaylist(id || ''); + + const { data: collaborators } = useCollaborators(id || ''); + + const permissions = usePlaylistPermissions(playlist); + + if (isLoading) { + return ( +
+
+ +
+
+ ); + } + + if (error || !playlist) { + return ( +
+ + +
+

Error

+

+ {error instanceof Error + ? error.message + : 'Failed to load playlist'} +

+
+
+
+
+ ); + } + + const tracks = playlist.tracks?.map((pt) => pt.track).filter(Boolean) || []; + const playlistTracks = playlist.tracks || []; + + const handleTrackAdded = () => { + setIsAddTrackModalOpen(false); + refetch(); + toast.success('Track added to playlist'); + }; + + const handleTrackRemoved = () => { + refetch(); + toast.success('Track removed from playlist'); + }; + + const handleTracksReordered = () => { + refetch(); + toast.success('Playlist tracks reordered'); + }; + + const handleShareClick = () => { + setIsShareModalOpen(true); + }; + + return ( +
+ + +
+ +
+ +
+

Tracks

+ {permissions.canAddTracks && ( + + )} +
+ + + + setIsAddTrackModalOpen(false)} + playlistId={playlist.id} + onTracksAdded={handleTrackAdded} + /> +
+ ); +} diff --git a/apps/web/src/features/playlists/routes.tsx b/apps/web/src/features/playlists/routes.tsx new file mode 100644 index 000000000..78a0187cf --- /dev/null +++ b/apps/web/src/features/playlists/routes.tsx @@ -0,0 +1,15 @@ +import { Routes, Route, Navigate } from 'react-router-dom'; +import { PlaylistListPage } from './pages/PlaylistListPage'; +import { PlaylistDetailPage } from './pages/PlaylistDetailPage'; + +export function PlaylistRoutes() { + return ( + + } /> + } /> + } /> + } /> + } /> + + ); +} diff --git a/apps/web/src/features/profile/pages/UserProfilePage.tsx b/apps/web/src/features/profile/pages/UserProfilePage.tsx new file mode 100644 index 000000000..8ceacd7d5 --- /dev/null +++ b/apps/web/src/features/profile/pages/UserProfilePage.tsx @@ -0,0 +1,154 @@ +import { useParams } from 'react-router-dom'; +import { useQuery } from '@tanstack/react-query'; +import { getProfileByUsername, type UserProfile } from '../services/profileService'; +import { LoadingSpinner } from '@/components/ui/loading-spinner'; +import { FollowButton } from '../components/FollowButton'; +import { format } from 'date-fns'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; + +export function UserProfilePage() { + const { username } = useParams<{ username: string }>(); + + const { + data: profile, + isLoading, + error, + } = useQuery({ + queryKey: ['userProfile', username], + queryFn: async () => { + if (!username) { + throw new Error('Username is required'); + } + return getProfileByUsername(username); + }, + enabled: !!username, + retry: false, + }); + + if (isLoading) { + return ( +
+
+
+ +

Loading profile...

+
+
+
+ ); + } + + if (error || !username) { + return ( +
+ + +
+

Error

+

+ {error instanceof Error + ? error.message + : !username + ? 'Username is required' + : 'Failed to load profile'} +

+
+
+
+
+ ); + } + + if (!profile) { + return ( +
+ + +
+

User Not Found

+

+ The user profile you're looking for doesn't exist. +

+
+
+
+
+ ); + } + + const displayName = + profile.first_name || profile.last_name + ? `${profile.first_name || ''} ${profile.last_name || ''}`.trim() + : profile.username; + + const initials = displayName + .split(' ') + .map((n) => n[0]) + .join('') + .toUpperCase() + .slice(0, 2); + + const memberSince = profile.created_at + ? format(new Date(profile.created_at), 'M/d/yyyy') + : null; + + return ( +
+
+ + +
+
+ + + {initials} + +
+ {profile.username} + {displayName !== profile.username && ( +

{displayName}

+ )} + {memberSince && ( +

+ Member since {memberSince} +

+ )} +
+
+ +
+
+ +
+ {profile.bio && ( +
+

Bio

+

+ {profile.bio} +

+
+ )} + + {profile.location && ( +
+

Location

+

{profile.location}

+
+ )} + + {!profile.bio && !profile.location && ( +

+ No additional information available. +

+ )} +
+
+
+
+
+ ); +} diff --git a/apps/web/src/features/roles/pages/RolesPage.tsx b/apps/web/src/features/roles/pages/RolesPage.tsx new file mode 100644 index 000000000..2625fb6ab --- /dev/null +++ b/apps/web/src/features/roles/pages/RolesPage.tsx @@ -0,0 +1,208 @@ +import { useState, useEffect } from 'react'; +import { getRoles, deleteRole, updateRole } from '../services/roleService'; +import type { Role } from '../types/role'; +import { Button } from '@/components/ui/button'; +import { LoadingSpinner } from '@/components/ui/loading-spinner'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { Badge } from '@/components/ui/badge'; +import { toast } from 'react-hot-toast'; +import { Shield, Edit, Trash2, Plus } from 'lucide-react'; + +export function RolesPage() { + const [roles, setRoles] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + const loadRoles = async () => { + try { + setIsLoading(true); + setError(null); + const loadedRoles = await getRoles(); + setRoles(loadedRoles); + } catch (err) { + const errorMessage = + err instanceof Error ? err.message : 'Failed to load roles'; + setError(errorMessage); + toast.error(errorMessage); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + loadRoles(); + }, []); + + const handleToggleActive = async (role: Role) => { + try { + await updateRole(role.id, { is_active: !role.is_active }); + toast.success( + `Role ${!role.is_active ? 'activated' : 'deactivated'} successfully`, + ); + loadRoles(); + } catch (err) { + const errorMessage = + err instanceof Error ? err.message : 'Failed to update role'; + toast.error(errorMessage); + } + }; + + const handleDelete = async (role: Role) => { + if (role.is_system) { + toast.error('Cannot delete system roles'); + return; + } + + if (!confirm(`Are you sure you want to delete role "${role.display_name}"?`)) { + return; + } + + try { + await deleteRole(role.id); + toast.success('Role deleted successfully'); + loadRoles(); + } catch (err) { + const errorMessage = + err instanceof Error ? err.message : 'Failed to delete role'; + toast.error(errorMessage); + } + }; + + if (isLoading) { + return ( +
+
+ +
+
+ ); + } + + if (error) { + return ( +
+ + +
+

Error

+

{error}

+ +
+
+
+
+ ); + } + + return ( +
+
+
+

Roles Management

+

+ Manage user roles and permissions +

+
+ +
+ + + + + + Roles + + + + {roles.length === 0 ? ( +
+

No roles found

+
+ ) : ( + + + + Name + Display Name + Description + Status + Type + Permissions + Actions + + + + {roles.map((role) => ( + + {role.name} + {role.display_name} + + {role.description} + + + + {role.is_active ? 'Active' : 'Inactive'} + + + + {role.is_system ? ( + System + ) : ( + Custom + )} + + + {role.permissions?.length || 0} permissions + + +
+ + + +
+
+
+ ))} +
+
+ )} +
+
+
+ ); +} diff --git a/apps/web/src/features/settings/components/PreferenceSettings.tsx b/apps/web/src/features/settings/components/PreferenceSettings.tsx new file mode 100644 index 000000000..3c2f062c5 --- /dev/null +++ b/apps/web/src/features/settings/components/PreferenceSettings.tsx @@ -0,0 +1,137 @@ +import { Label } from '@/components/ui/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { + RadioGroup, + RadioGroupItem, +} from '@/components/ui/radio-group'; +import { PreferenceSettings as PreferenceSettingsType } from '../types/settings'; + +interface PreferenceSettingsProps { + preferences: PreferenceSettingsType; + onChange: (preferences: PreferenceSettingsType) => void; +} + +const supportedLanguages = [ + { value: 'en', label: 'English' }, + { value: 'fr', label: 'Français' }, + { value: 'es', label: 'Español' }, + { value: 'de', label: 'Deutsch' }, + { value: 'it', label: 'Italiano' }, + { value: 'pt', label: 'PortuguĂȘs' }, + { value: 'ru', label: 'РуссĐșĐžĐč' }, + { value: 'ja', label: 'æ—„æœŹèȘž' }, + { value: 'zh', label: 'äž­æ–‡' }, + { value: 'ko', label: '한ꔭ얎' }, +]; + +const commonTimezones = [ + { value: 'UTC', label: 'UTC' }, + { value: 'Europe/Paris', label: 'Europe/Paris' }, + { value: 'America/New_York', label: 'America/New_York' }, + { value: 'America/Los_Angeles', label: 'America/Los_Angeles' }, + { value: 'Asia/Tokyo', label: 'Asia/Tokyo' }, + { value: 'Asia/Shanghai', label: 'Asia/Shanghai' }, +]; + +export function PreferenceSettings({ + preferences, + onChange, +}: PreferenceSettingsProps) { + const handleLanguageChange = (value: string) => { + onChange({ + ...preferences, + language: value, + }); + }; + + const handleTimezoneChange = (value: string) => { + onChange({ + ...preferences, + timezone: value, + }); + }; + + const handleThemeChange = (value: string) => { + onChange({ + ...preferences, + theme: value as 'light' | 'dark' | 'auto', + }); + }; + + return ( +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ ); +} diff --git a/apps/web/src/features/settings/components/SettingsTabs.tsx b/apps/web/src/features/settings/components/SettingsTabs.tsx new file mode 100644 index 000000000..b4669d337 --- /dev/null +++ b/apps/web/src/features/settings/components/SettingsTabs.tsx @@ -0,0 +1,82 @@ +import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; +import { UserSettings } from '../types/settings'; +import { PreferenceSettings } from './PreferenceSettings'; +import { NotificationSettings } from './NotificationSettings'; +import { PrivacySettings } from './PrivacySettings'; +import { ContentSettings } from './ContentSettings'; + +interface SettingsTabsProps { + settings: UserSettings; + onChange: (settings: UserSettings) => void; +} + +export function SettingsTabs({ settings, onChange }: SettingsTabsProps) { + const handlePreferencesChange = (preferences: typeof settings.preferences) => { + onChange({ + ...settings, + preferences, + }); + }; + + const handleNotificationsChange = ( + notifications: typeof settings.notifications, + ) => { + onChange({ + ...settings, + notifications, + }); + }; + + const handlePrivacyChange = (privacy: typeof settings.privacy) => { + onChange({ + ...settings, + privacy, + }); + }; + + const handleContentChange = (content: typeof settings.content) => { + onChange({ + ...settings, + content, + }); + }; + + return ( + + + PrĂ©fĂ©rences + Notifications + ConfidentialitĂ© + Contenu + + + + + + + + + + + + + + + + + + + ); +} diff --git a/apps/web/src/features/settings/pages/SettingsPage.tsx b/apps/web/src/features/settings/pages/SettingsPage.tsx new file mode 100644 index 000000000..53bada08c --- /dev/null +++ b/apps/web/src/features/settings/pages/SettingsPage.tsx @@ -0,0 +1,130 @@ +import { useState, useEffect } from 'react'; +import { useAuthStore } from '@/stores/auth'; +import { getSettings, updateSettings } from '../services/settingsService'; +import { UserSettings } from '../types/settings'; +import { SettingsTabs } from '../components/SettingsTabs'; +import { Button } from '@/components/ui/button'; +import { LoadingSpinner } from '@/components/ui/loading-spinner'; +import { toast } from 'react-hot-toast'; +import { settingsSchema } from '../schemas/settingsSchema'; + +export function SettingsPage() { + const { user } = useAuthStore(); + const [settings, setSettings] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isSaving, setIsSaving] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (!user?.id) { + setError('Vous devez ĂȘtre connectĂ© pour accĂ©der aux paramĂštres'); + setIsLoading(false); + return; + } + + const loadSettings = async () => { + try { + setIsLoading(true); + setError(null); + const userSettings = await getSettings(user.id); + setSettings(userSettings); + } catch (err) { + const errorMessage = + err instanceof Error ? err.message : 'Erreur de chargement des paramĂštres'; + setError(errorMessage); + toast.error(errorMessage); + } finally { + setIsLoading(false); + } + }; + + loadSettings(); + }, [user?.id]); + + const handleSave = async () => { + if (!user?.id || !settings) { + return; + } + + // Valider les settings avec le schĂ©ma Zod + const validationResult = settingsSchema.safeParse(settings); + if (!validationResult.success) { + const errors = validationResult.error.errors + .map((e) => `${e.path.join('.')}: ${e.message}`) + .join(', '); + toast.error(`Erreur de validation: ${errors}`); + return; + } + + try { + setIsSaving(true); + await updateSettings(user.id, settings); + toast.success('ParamĂštres sauvegardĂ©s avec succĂšs'); + } catch (err) { + const errorMessage = + err instanceof Error ? err.message : 'Erreur lors de la sauvegarde'; + toast.error(errorMessage); + } finally { + setIsSaving(false); + } + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (error && !settings) { + return ( +
+
+

ParamĂštres

+
+ {error} +
+
+
+ ); + } + + if (!settings) { + return null; + } + + return ( +
+
+
+

ParamĂštres

+

+ Gérez vos paramÚtres de compte et préférences +

+
+ +
+ + +
+ +
+
+
+
+ ); +} diff --git a/apps/web/src/features/streaming/hooks/usePlaybackRealtime.ts b/apps/web/src/features/streaming/hooks/usePlaybackRealtime.ts index b7f6ee89d..a6d80803b 100644 --- a/apps/web/src/features/streaming/hooks/usePlaybackRealtime.ts +++ b/apps/web/src/features/streaming/hooks/usePlaybackRealtime.ts @@ -144,7 +144,17 @@ export function usePlaybackRealtime( * Construit l'URL WebSocket pour le track */ const getWebSocketUrl = useCallback((trackId: number): string => { - const apiBaseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080'; + const apiBaseUrl = (() => { + const url = import.meta.env.VITE_API_URL; + if (!url) { + if (import.meta.env.PROD) { + throw new Error('VITE_API_URL must be defined in production'); + } + // Fallback uniquement en développement + return 'http://127.0.0.1:8080'; + } + return url; + })(); // Convertir http:// en ws:// ou https:// en wss:// const wsBaseUrl = apiBaseUrl.replace(/^http/, 'ws'); // Récupérer le token d'authentification depuis le client API diff --git a/apps/web/src/features/tracks/pages/TrackDetailPage.tsx b/apps/web/src/features/tracks/pages/TrackDetailPage.tsx new file mode 100644 index 000000000..60284191f --- /dev/null +++ b/apps/web/src/features/tracks/pages/TrackDetailPage.tsx @@ -0,0 +1,262 @@ +import { useState, useEffect } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { getTrack, TrackUploadError } from '../services/trackService'; +import { usePlayerStore } from '@/features/player/store/playerStore'; +import type { Track as PlayerTrack } from '@/features/player/types'; +import { toast } from 'react-hot-toast'; +import { Button } from '@/components/ui/button'; +import { LoadingSpinner } from '@/components/ui/loading-spinner'; +import { Card, CardContent } from '@/components/ui/card'; +import { Play, Pause, ArrowLeft, Share2, Plus } from 'lucide-react'; +import { Music } from 'lucide-react'; +import type { Track } from '../types/track'; + +function formatDuration(seconds: number): string { + const minutes = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + return `${minutes}:${secs.toString().padStart(2, '0')}`; +} + +export function TrackDetailPage() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const [track, setTrack] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + const { play, pause, currentTrack, isPlaying, addToQueue } = usePlayerStore(); + + useEffect(() => { + if (!id) { + setError('Track ID is required'); + setIsLoading(false); + return; + } + + const loadTrack = async () => { + try { + setIsLoading(true); + setError(null); + const trackId = Number(id); + if (isNaN(trackId)) { + throw new Error('Invalid track ID'); + } + const loadedTrack = await getTrack(trackId); + setTrack(loadedTrack); + } catch (err) { + const errorMessage = + err instanceof TrackUploadError + ? err.message + : err instanceof Error + ? err.message + : 'Failed to load track'; + setError(errorMessage); + if (err instanceof TrackUploadError) { + toast.error(errorMessage); + } + } finally { + setIsLoading(false); + } + }; + + loadTrack(); + }, [id]); + + const mapToPlayerTrack = (t: Track): PlayerTrack => ({ + id: t.id, + title: t.title, + artist: t.artist, + album: t.album, + duration: t.duration, + url: t.stream_manifest_url || t.file_path, + cover: t.cover_art_path, + genre: t.genre, + }); + + const handlePlay = () => { + if (!track) return; + const playerTrack = mapToPlayerTrack(track); + play(playerTrack); + }; + + const handlePause = () => { + pause(); + }; + + const handleAddToQueue = () => { + if (!track) return; + const playerTrack = mapToPlayerTrack(track); + addToQueue([playerTrack]); + toast.success('Track ajouté à la file d\'attente'); + }; + + const handleShare = async () => { + if (!track) return; + const shareUrl = `${window.location.origin}/tracks/${track.id}`; + try { + await navigator.clipboard.writeText(shareUrl); + toast.success('Lien copié dans le presse-papiers'); + } catch (err) { + toast.error('Impossible de copier le lien'); + } + }; + + const isCurrentTrack = currentTrack?.id === track?.id; + const isCurrentlyPlaying = isCurrentTrack && isPlaying; + + if (isLoading) { + return ( +
+
+ + Chargement du track... +
+
+ ); + } + + if (error || !track) { + return ( +
+ + +
+

Error

+

+ {error || 'Track introuvable'} +

+ +
+
+
+
+ ); + } + + return ( +
+ + +
+ {/* Cover Art */} +
+ + + {track.cover_art_path ? ( + {track.title} + ) : ( +
+ +
+ )} +
+
+
+ + {/* Track Details */} +
+ + +

{track.title}

+

{track.artist}

+ {track.album && ( +

Album: {track.album}

+ )} + +
+ {isCurrentlyPlaying ? ( + + ) : ( + + )} + + +
+ +
+
+

Durée

+

{formatDuration(track.duration)}

+
+ {track.genre && ( +
+

Genre

+

{track.genre}

+
+ )} + {track.year && ( +
+

Année

+

{track.year}

+
+ )} +
+

Lectures

+

{track.play_count}

+
+
+

Likes

+

{track.like_count}

+
+
+
+
+ + {/* Waveform */} + {track.waveform_path && ( + + +

Waveform

+ Waveform +
+
+ )} +
+
+
+ ); +} diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index d451bc4d8..92ca20ae0 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -5,6 +5,8 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { Toaster } from 'react-hot-toast'; import { App } from './app/App'; import './index.css'; +// Initialize i18next before React renders +import './lib/i18n'; // HMR Force Update: 1765126900 @@ -20,7 +22,12 @@ const queryClient = new QueryClient({ ReactDOM.createRoot(document.getElementById('root')!).render( - + diff --git a/apps/web/src/pages/DashboardPage.tsx b/apps/web/src/pages/DashboardPage.tsx index c7ec55b11..230950ccc 100644 --- a/apps/web/src/pages/DashboardPage.tsx +++ b/apps/web/src/pages/DashboardPage.tsx @@ -21,6 +21,9 @@ export function DashboardPage() { fetchItems({ limit: 5 }); }, [fetchItems]); + // Sécuriser items pour s'assurer que c'est toujours un tableau + const safeItems = Array.isArray(items) ? items : []; + const stats = [ { title: 'Pistes écoutées', @@ -152,7 +155,7 @@ export function DashboardPage() {

) : (
- {items.slice(0, 3).map((item) => ( + {safeItems.slice(0, 3).map((item) => (
@@ -167,7 +170,7 @@ export function DashboardPage() {
))} - {items.length === 0 && ( + {safeItems.length === 0 && (

Aucune piste dans votre bibliothĂšque

diff --git a/apps/web/src/pages/marketplace/MarketplaceHome.tsx b/apps/web/src/pages/marketplace/MarketplaceHome.tsx new file mode 100644 index 000000000..095e8a9a7 --- /dev/null +++ b/apps/web/src/pages/marketplace/MarketplaceHome.tsx @@ -0,0 +1,92 @@ +import { useState, useEffect } from 'react'; +import { marketplaceService } from '@/services/marketplaceService'; +import { ProductCard } from '@/features/marketplace/components/ProductCard'; +import { Product } from '@/types/marketplace'; +import { LoadingSpinner } from '@/components/ui/loading-spinner'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { toast } from 'react-hot-toast'; + +export function MarketplaceHome() { + const [products, setProducts] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [purchasingProductId, setPurchasingProductId] = useState(null); + + useEffect(() => { + const loadProducts = async () => { + try { + setIsLoading(true); + const fetchedProducts = await marketplaceService.fetchProducts(); + setProducts(fetchedProducts); + } catch (error) { + const errorMessage = + error instanceof Error + ? error.message + : 'Failed to load marketplace products'; + toast.error(errorMessage); + } finally { + setIsLoading(false); + } + }; + + loadProducts(); + }, []); + + const handlePurchase = async (product: Product) => { + try { + setPurchasingProductId(product.id); + await marketplaceService.purchaseProduct(product.id); + toast.success(`Successfully purchased ${product.title}`); + // Optionally refresh products or update UI + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Failed to purchase product'; + toast.error(errorMessage); + } finally { + setPurchasingProductId(null); + } + }; + + if (isLoading) { + return ( +
+
+ +
+
+ ); + } + + return ( +
+
+

Marketplace

+

+ Discover and purchase music products, samples, and licenses +

+
+ + {products.length === 0 ? ( + + +
+

+ No products available at the moment. +

+
+
+
+ ) : ( +
+ {products.map((product) => ( + + ))} +
+ )} +
+ ); +} diff --git a/apps/web/src/router/index.tsx b/apps/web/src/router/index.tsx index 24ac164bd..906dabab8 100644 --- a/apps/web/src/router/index.tsx +++ b/apps/web/src/router/index.tsx @@ -4,6 +4,7 @@ import { useAuth } from '@/features/auth/hooks/useAuth'; import { ProtectedRoute } from '@/components/auth/ProtectedRoute'; import { DashboardLayout } from '@/components/layout/DashboardLayout'; import { LoadingSpinner } from '@/components/ui/loading-spinner'; +import { ErrorBoundary } from '@/components/ErrorBoundary'; import { LazyLogin, LazyRegister, @@ -126,7 +127,9 @@ export const AppRouter = () => ( element={ - + + + } @@ -136,7 +139,9 @@ export const AppRouter = () => ( element={ - + + + } @@ -146,7 +151,9 @@ export const AppRouter = () => ( element={ - + + + } diff --git a/apps/web/src/services/api.ts b/apps/web/src/services/api.ts index 74e9bb96f..6d5d60688 100644 --- a/apps/web/src/services/api.ts +++ b/apps/web/src/services/api.ts @@ -15,9 +15,30 @@ import type { export type { Track }; // Configuration de base -const API_BASE_URL = - import.meta.env.VITE_API_URL || 'http://localhost:8080/api/v1'; -const WS_BASE_URL = import.meta.env.VITE_WS_URL || 'ws://localhost:8081/ws'; +// En production, les variables d'environnement doivent ĂȘtre dĂ©finies +const API_BASE_URL = (() => { + const url = import.meta.env.VITE_API_URL; + if (!url) { + if (import.meta.env.PROD) { + throw new Error('VITE_API_URL must be defined in production'); + } + // Fallback uniquement en dĂ©veloppement + return 'http://127.0.0.1:8080/api/v1'; + } + return url; +})(); + +const WS_BASE_URL = (() => { + const url = import.meta.env.VITE_WS_URL; + if (!url) { + if (import.meta.env.PROD) { + throw new Error('VITE_WS_URL must be defined in production'); + } + // Fallback uniquement en dĂ©veloppement + return 'ws://127.0.0.1:8081/ws'; + } + return url; +})(); // SchĂ©mas de validation Zod const UserSchema = z.object({ @@ -270,12 +291,14 @@ export class ApiService { } // MĂ©thodes pour la bibliothĂšque + // Note: Le backend n'a pas d'endpoint /library, on utilise /tracks Ă  la place async getLibraryItems(params?: { page?: number; limit?: number; type?: string; }): Promise> { - const response = await this.client.get('/library', { params }); + // Utiliser /tracks au lieu de /library qui n'existe pas + const response = await this.client.get('/tracks', { params }); return response.data; } diff --git a/apps/web/src/services/secure-auth.ts b/apps/web/src/services/secure-auth.ts index a8ad89c4e..e39e730c8 100644 --- a/apps/web/src/services/secure-auth.ts +++ b/apps/web/src/services/secure-auth.ts @@ -49,8 +49,17 @@ const AuthTokensSchema = z.object({ }); // Configuration -const API_BASE_URL = - import.meta.env.VITE_API_URL || 'http://localhost:8080/api/v1'; +const API_BASE_URL = (() => { + const url = import.meta.env.VITE_API_URL; + if (!url) { + if (import.meta.env.PROD) { + throw new Error('VITE_API_URL must be defined in production'); + } + // Fallback uniquement en dĂ©veloppement + return 'http://127.0.0.1:8080/api/v1'; + } + return url; +})(); export class SecureAuthService { private static instance: SecureAuthService; diff --git a/apps/web/src/services/tokenRefresh.ts b/apps/web/src/services/tokenRefresh.ts index 7b6577380..8a316a209 100644 --- a/apps/web/src/services/tokenRefresh.ts +++ b/apps/web/src/services/tokenRefresh.ts @@ -7,8 +7,20 @@ let refreshClient: AxiosInstance | null = null; function getRefreshClient(): AxiosInstance { if (!refreshClient) { + const baseURL = (() => { + const url = import.meta.env.VITE_API_URL; + if (!url) { + if (import.meta.env.PROD) { + throw new Error('VITE_API_URL must be defined in production'); + } + // Fallback uniquement en dĂ©veloppement + return 'http://127.0.0.1:8080/api/v1'; + } + return url; + })(); + refreshClient = axios.create({ - baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8080/api/v1', + baseURL, timeout: 10000, headers: { 'Content-Type': 'application/json', diff --git a/apps/web/src/services/websocket.ts b/apps/web/src/services/websocket.ts index b853fe428..f65cfa0af 100644 --- a/apps/web/src/services/websocket.ts +++ b/apps/web/src/services/websocket.ts @@ -44,7 +44,17 @@ class WebSocketServiceImpl implements WebSocketService { } // CORRECTION: Le serveur Rust expose le WebSocket sur /ws - const wsUrl = import.meta.env.VITE_WS_URL || 'ws://localhost:8081/ws'; + const wsUrl = (() => { + const url = import.meta.env.VITE_WS_URL; + if (!url) { + if (import.meta.env.PROD) { + throw new Error('VITE_WS_URL must be defined in production'); + } + // Fallback uniquement en dĂ©veloppement + return 'ws://127.0.0.1:8081/ws'; + } + return url; + })(); return new Promise((resolve, reject) => { try { diff --git a/apps/web/src/stores/library.ts b/apps/web/src/stores/library.ts index c6afc9c4f..d678aa8bd 100644 --- a/apps/web/src/stores/library.ts +++ b/apps/web/src/stores/library.ts @@ -73,20 +73,29 @@ export const useLibraryStore = create( type, }); + // SĂ©curiser response.data pour s'assurer que c'est toujours un tableau + const itemsArray = Array.isArray(response.data) + ? response.data + : Array.isArray(response) + ? response + : []; + set({ - items: response.data, + items: itemsArray, pagination: { - page: response.page, - limit: response.limit, - total: response.total, - hasNext: response.has_next, - hasPrev: response.has_prev, + page: response.page || 1, + limit: response.limit || limit, + total: response.total || 0, + hasNext: response.has_next || false, + hasPrev: response.has_prev || false, }, isLoading: false, error: null, }); } catch (error: any) { + // En cas d'erreur, s'assurer que items reste un tableau vide set({ + items: [], error: error as ApiError, isLoading: false, }); @@ -103,13 +112,22 @@ export const useLibraryStore = create( type: 'favorites', }); + // SĂ©curiser response.data pour s'assurer que c'est toujours un tableau + const favoritesArray = Array.isArray(response.data) + ? response.data + : Array.isArray(response) + ? response + : []; + set({ - favorites: response.data, + favorites: favoritesArray, isLoading: false, error: null, }); } catch (error: any) { + // En cas d'erreur, s'assurer que favorites reste un tableau vide set({ + favorites: [], error: error as ApiError, isLoading: false, }); diff --git a/apps/web/src/utils/csp.ts b/apps/web/src/utils/csp.ts index c92d147fa..2ac8550bf 100644 --- a/apps/web/src/utils/csp.ts +++ b/apps/web/src/utils/csp.ts @@ -159,7 +159,7 @@ export function createCSPMiddleware() { setCSPNonce(nonce); const cspHeader = - process.env.NODE_ENV === 'production' + import.meta.env.MODE === 'production' ? buildCSPHeader(nonce) : buildCSPHeaderDev(); diff --git a/apps/web/src/utils/logger.ts b/apps/web/src/utils/logger.ts new file mode 100644 index 000000000..c1cfaab5b --- /dev/null +++ b/apps/web/src/utils/logger.ts @@ -0,0 +1,50 @@ +/** + * Logger conditionnel pour la production + * Remplace console.log/info/warn/error par des fonctions conditionnelles + * En production, seuls les erreurs critiques sont loggĂ©es + */ + +type LogLevel = 'debug' | 'info' | 'warn' | 'error'; + +interface Logger { + debug: (...args: unknown[]) => void; + info: (...args: unknown[]) => void; + warn: (...args: unknown[]) => void; + error: (...args: unknown[]) => void; +} + +const isDev = import.meta.env.DEV; +const isProd = import.meta.env.PROD; + +/** + * Logger conditionnel qui filtre les logs selon l'environnement + */ +export const logger: Logger = { + debug: (...args: unknown[]) => { + if (isDev) { + console.debug('[DEBUG]', ...args); + } + }, + info: (...args: unknown[]) => { + if (isDev) { + console.info('[INFO]', ...args); + } + }, + warn: (...args: unknown[]) => { + // Warnings toujours loggĂ©s, mais avec prĂ©fixe en prod + if (isProd) { + console.warn('[WARN]', ...args); + } else { + console.warn('[WARN]', ...args); + } + }, + error: (...args: unknown[]) => { + // Erreurs toujours loggĂ©es en production pour le debugging + console.error('[ERROR]', ...args); + }, +}; + +/** + * Export par dĂ©faut pour faciliter l'import + */ +export default logger; diff --git a/apps/web/src/utils/sanitize.ts b/apps/web/src/utils/sanitize.ts index 77e531f7c..10e15ef0e 100644 --- a/apps/web/src/utils/sanitize.ts +++ b/apps/web/src/utils/sanitize.ts @@ -2,6 +2,7 @@ * XSS Protection utilities * Sanitise et valide le contenu utilisateur pour prĂ©venir les attaques XSS */ +import DOMPurify from 'dompurify'; // Types pour la configuration de sanitisation export interface SanitizeOptions { @@ -268,7 +269,25 @@ function escapeHTML(content: string): string { /** * Sanitise spĂ©cifiquement les messages de chat */ +/** + * Sanitise les messages de chat avec DOMPurify pour une protection XSS robuste + * Utilise DOMPurify en prioritĂ©, avec fallback sur sanitizeHTML si DOMPurify n'est pas disponible + */ export function sanitizeChatMessage(message: string): string { + // Utiliser DOMPurify pour une sanitisation robuste et Ă©prouvĂ©e + if (typeof window !== 'undefined' && DOMPurify.isSupported) { + return DOMPurify.sanitize(message, { + ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'i', 'b', 'span', 'a'], + ALLOWED_ATTR: ['class', 'href', 'title', 'target'], + ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|data):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i, + KEEP_CONTENT: true, + RETURN_DOM: false, + RETURN_DOM_FRAGMENT: false, + RETURN_TRUSTED_TYPE: false, + }); + } + + // Fallback sur la sanitisation manuelle si DOMPurify n'est pas disponible (SSR) const chatOptions: SanitizeOptions = { allowedTags: ['p', 'br', 'strong', 'em', 'u', 'i', 'b', 'span'], allowedAttributes: { diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index ada75e5e2..be575cf57 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -59,20 +59,36 @@ export default defineConfig(({ mode }) => { port: 3000, host: true, headers: { - 'Content-Security-Policy': [ - "default-src 'self'", - "script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:", - "worker-src 'self' blob:", - "style-src 'self' 'unsafe-inline'", - "img-src 'self' data: https: blob:", - "connect-src 'self' ws: wss: http: https:", - "font-src 'self' data:", - "object-src 'none'", - "base-uri 'self'", - "form-action 'self'", - "frame-ancestors 'none'", - "upgrade-insecure-requests" - ].join('; ') + 'Content-Security-Policy': (() => { + const basePolicy = [ + "default-src 'self'", + "worker-src 'self' blob:", + "img-src 'self' data: https: blob:", + "connect-src 'self' ws: wss: http: https:", + "font-src 'self' data:", + "object-src 'none'", + "base-uri 'self'", + "form-action 'self'", + "frame-ancestors 'none'", + "upgrade-insecure-requests" + ]; + + // Production: Remove unsafe-inline and unsafe-eval, use nonces + // Dev: Keep unsafe-inline for Vite HMR, but remove unsafe-eval + if (isProduction) { + basePolicy.push( + "script-src 'self' 'nonce-__CSP_NONCE__' blob:", + "style-src 'self' 'nonce-__CSP_NONCE__'" + ); + } else { + basePolicy.push( + "script-src 'self' 'unsafe-inline' blob:", + "style-src 'self' 'unsafe-inline'" + ); + } + + return basePolicy.join('; '); + })() } }, build: { diff --git a/e2e/example.spec.ts b/e2e/example.spec.ts new file mode 100644 index 000000000..54a906a4e --- /dev/null +++ b/e2e/example.spec.ts @@ -0,0 +1,18 @@ +import { test, expect } from '@playwright/test'; + +test('has title', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Playwright/); +}); + +test('get started link', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Click the get started link. + await page.getByRole('link', { name: 'Get started' }).click(); + + // Expects page to have a heading with the name of Installation. + await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); +}); diff --git a/e2e/test-1.spec.ts b/e2e/test-1.spec.ts new file mode 100644 index 000000000..fffb6026e --- /dev/null +++ b/e2e/test-1.spec.ts @@ -0,0 +1,15 @@ +import { test, expect } from '@playwright/test'; + +test('test', async ({ page }) => { + await page.goto('http://localhost:3000/login'); + await page.getByRole('link', { name: 'S\'inscrire' }).click(); + await page.locator('input[name="email"]').click(); + await page.locator('input[name="email"]').fill('test@free.fr'); + await page.locator('input[name="email"]').press('Tab'); + await page.locator('input[name="username"]').fill('tester'); + await page.locator('input[name="username"]').press('Tab'); + await page.locator('input[name="password"]').fill('Test12345678!'); + await page.locator('input[name="password_confirm"]').click(); + await page.locator('input[name="password_confirm"]').fill('Test12345678!'); + await page.getByRole('button', { name: 'S\'inscrire' }).click(); +}); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..6e152f425 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3506 @@ +{ + "name": "veza", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "@eslint/js": "^9.39.1", + "@playwright/test": "^1.57.0", + "@types/node": "^25.0.3", + "eslint": "^9.39.1", + "eslint-plugin-react": "^7.37.5", + "globals": "^16.5.0", + "prettier": "3.6.2", + "typescript": "^5.9.3", + "typescript-eslint": "^8.46.3" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@playwright/test": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", + "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", + "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.0.tgz", + "integrity": "sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.50.0", + "@typescript-eslint/type-utils": "8.50.0", + "@typescript-eslint/utils": "8.50.0", + "@typescript-eslint/visitor-keys": "8.50.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.50.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.0.tgz", + "integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.50.0", + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/typescript-estree": "8.50.0", + "@typescript-eslint/visitor-keys": "8.50.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.50.0.tgz", + "integrity": "sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.50.0", + "@typescript-eslint/types": "^8.50.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.0.tgz", + "integrity": "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/visitor-keys": "8.50.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.0.tgz", + "integrity": "sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.50.0.tgz", + "integrity": "sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/typescript-estree": "8.50.0", + "@typescript-eslint/utils": "8.50.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz", + "integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.0.tgz", + "integrity": "sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.50.0", + "@typescript-eslint/tsconfig-utils": "8.50.0", + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/visitor-keys": "8.50.0", + "debug": "^4.3.4", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.0.tgz", + "integrity": "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.50.0", + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/typescript-estree": "8.50.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.0.tgz", + "integrity": "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.50.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz", + "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.1", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.50.0.tgz", + "integrity": "sha512-Q1/6yNUmCpH94fbgMUMg2/BSAr/6U7GBk61kZTv1/asghQOWOjTlp9K8mixS5NcJmm2creY+UFfGeW/+OcA64A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.50.0", + "@typescript-eslint/parser": "8.50.0", + "@typescript-eslint/typescript-estree": "8.50.0", + "@typescript-eslint/utils": "8.50.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json index 03f2cbb72..6029c4c97 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,14 @@ { "devDependencies": { "@eslint/js": "^9.39.1", + "@playwright/test": "^1.57.0", + "@types/node": "^25.0.3", "eslint": "^9.39.1", "eslint-plugin-react": "^7.37.5", "globals": "^16.5.0", "prettier": "3.6.2", "typescript": "^5.9.3", "typescript-eslint": "^8.46.3" - } + }, + "scripts": {} } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 000000000..dfe72abfe --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,79 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './e2e', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('')`. */ + // baseURL: 'http://localhost:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://localhost:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/veza-backend-api/cmd/tools/create_test_user/main.go b/veza-backend-api/cmd/tools/create_test_user/main.go new file mode 100644 index 000000000..bbe1f9da5 --- /dev/null +++ b/veza-backend-api/cmd/tools/create_test_user/main.go @@ -0,0 +1,109 @@ +package main + +import ( + "fmt" + "log" + "os" + "strings" + + "github.com/joho/godotenv" + "golang.org/x/crypto/bcrypt" + "gorm.io/driver/postgres" + "gorm.io/gorm" + + "veza-backend-api/internal/models" +) + +func main() { + // Load .env file + if err := godotenv.Load(); err != nil { + log.Printf("Note: .env file not found, using system environment variables") + } + + // Get database connection string + databaseURL := os.Getenv("DATABASE_URL") + if databaseURL == "" { + // Fallback to individual components + dbHost := getEnv("DB_HOST", "localhost") + dbPort := getEnv("DB_PORT", "5432") + dbUser := getEnv("DB_USER", "veza") + dbPassword := getEnv("DB_PASSWORD", "password") + dbName := getEnv("DB_NAME", "veza") + databaseURL = fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", + dbHost, dbPort, dbUser, dbPassword, dbName) + } + + // Connect to database + db, err := gorm.Open(postgres.Open(databaseURL), &gorm.Config{}) + if err != nil { + log.Fatalf("Failed to connect to database: %v", err) + } + + // Get test user credentials from environment or use defaults + email := getEnv("TEST_EMAIL", "user@example.com") + password := getEnv("TEST_PASSWORD", "password123") + username := getEnv("TEST_USERNAME", "testuser") + + // Check if user already exists + var existingUser models.User + result := db.Where("email = ?", email).First(&existingUser) + if result.Error == nil { + log.Printf("User with email %s already exists (ID: %s)", email, existingUser.ID) + + // Update password if needed + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + log.Fatalf("Failed to hash password: %v", err) + } + + existingUser.PasswordHash = string(hashedPassword) + existingUser.IsVerified = true + existingUser.IsActive = true + + if err := db.Save(&existingUser).Error; err != nil { + log.Fatalf("Failed to update user: %v", err) + } + + log.Printf("✅ Updated existing user: %s (password reset, verified and active)", email) + return + } + + // Hash password + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + log.Fatalf("Failed to hash password: %v", err) + } + + // Generate slug from username + slug := strings.ToLower(strings.ReplaceAll(username, "_", "-")) + + // Create user + user := &models.User{ + Email: email, + Username: username, + Slug: slug, + PasswordHash: string(hashedPassword), + IsVerified: true, + IsActive: true, + Role: "user", + FirstName: "Test", + LastName: "User", + } + + if err := db.Create(user).Error; err != nil { + log.Fatalf("Failed to create user: %v", err) + } + + log.Printf("✅ Created test user successfully!") + log.Printf(" Email: %s", email) + log.Printf(" Username: %s", username) + log.Printf(" Password: %s", password) + log.Printf(" ID: %s", user.ID) +} + +func getEnv(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return strings.TrimSpace(value) + } + return defaultValue +} diff --git a/veza-backend-api/migrations/000000_cleanup_refresh_tokens.sql b/veza-backend-api/migrations/000000_cleanup_refresh_tokens.sql deleted file mode 100644 index d3b53e537..000000000 --- a/veza-backend-api/migrations/000000_cleanup_refresh_tokens.sql +++ /dev/null @@ -1,18 +0,0 @@ --- Migration to cleanup refresh_tokens table --- Remove legacy column 'token' which caused NULL constraint violations --- Ensure correct constraints on token_hash - -BEGIN; - --- 1. Remove the legacy 'token' column which is no longer used by the application --- The application now uses 'token_hash' for secure storage -ALTER TABLE refresh_tokens DROP COLUMN IF EXISTS token; - --- 2. Ensure token_hash has the correct constraints --- It should be NOT NULL and UNIQUE to prevent duplicates and ensure integrity -ALTER TABLE refresh_tokens ALTER COLUMN token_hash SET NOT NULL; - --- 3. Add comment to clarify the column usage -COMMENT ON COLUMN refresh_tokens.token_hash IS 'SHA-256 hash of the refresh token. The raw token is never stored.'; - -COMMIT; diff --git a/veza-backend-api/migrations/011_cleanup_refresh_tokens.sql b/veza-backend-api/migrations/011_cleanup_refresh_tokens.sql new file mode 100644 index 000000000..2580075f9 --- /dev/null +++ b/veza-backend-api/migrations/011_cleanup_refresh_tokens.sql @@ -0,0 +1,49 @@ +-- Migration to cleanup refresh_tokens table +-- Remove legacy column 'token' which caused NULL constraint violations +-- Ensure correct constraints on token_hash +-- This migration runs AFTER 010_auth_and_users.sql which creates the refresh_tokens table + +BEGIN; + +-- Check if the table exists before attempting to alter it +DO $$ +BEGIN + -- Only proceed if the refresh_tokens table exists + IF EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = 'refresh_tokens' + ) THEN + -- 1. Remove the legacy 'token' column which is no longer used by the application + -- The application now uses 'token_hash' for secure storage + ALTER TABLE refresh_tokens DROP COLUMN IF EXISTS token; + + -- 2. Ensure token_hash has the correct constraints + -- It should be NOT NULL and UNIQUE to prevent duplicates and ensure integrity + -- Only set NOT NULL if the column exists and doesn't already have the constraint + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'refresh_tokens' + AND column_name = 'token_hash' + ) THEN + -- Check if column is already NOT NULL + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'refresh_tokens' + AND column_name = 'token_hash' + AND is_nullable = 'YES' + ) THEN + ALTER TABLE refresh_tokens ALTER COLUMN token_hash SET NOT NULL; + END IF; + END IF; + + -- 3. Add comment to clarify the column usage + COMMENT ON COLUMN refresh_tokens.token_hash IS 'SHA-256 hash of the refresh token. The raw token is never stored.'; + ELSE + RAISE NOTICE 'Table refresh_tokens does not exist yet. Skipping cleanup migration.'; + END IF; +END $$; + +COMMIT;