From b0a945f07b41d15219ea9dec0e0722d89662d726 Mon Sep 17 00:00:00 2001 From: Yannick Le Duc Date: Mon, 25 Aug 2025 18:28:14 +0200 Subject: [PATCH] =?UTF-8?q?ajout=20envoi=20smtp=20(param=C3=A8tres,=20test?= =?UTF-8?q?=20envois,=20envoi=20=C3=A0=201=20participant).=20prot=C3=A8ge?= =?UTF-8?q?=20vue=20mot=20de=20passe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ajout filtre page statistiques --- SETTINGS.md | 144 ++ migration-smtp-settings.sql | 31 + package-lock.json | 1354 +++++++++++++++++ package.json | 3 + .../campaigns/[id]/participants/page.tsx | 23 + src/app/admin/campaigns/[id]/stats/page.tsx | 187 ++- src/app/admin/page.tsx | 18 +- src/app/admin/settings/page.tsx | 181 +++ src/app/api/send-participant-email/route.ts | 151 ++ src/app/api/test-email/route.ts | 134 ++ src/app/api/test-smtp/route.ts | 95 ++ .../[id]/vote/[participantId]/page.tsx | 22 +- src/components/SendParticipantEmailModal.tsx | 229 +++ src/components/SendTestEmailModal.tsx | 154 ++ src/components/SmtpSettingsForm.tsx | 400 +++++ src/components/ui/switch.tsx | 29 + src/lib/email.ts | 101 ++ src/lib/encryption.ts | 73 + src/lib/services.ts | 176 ++- src/types/index.ts | 25 + supabase-schema.sql | 23 + 21 files changed, 3523 insertions(+), 30 deletions(-) create mode 100644 SETTINGS.md create mode 100644 migration-smtp-settings.sql create mode 100644 src/app/admin/settings/page.tsx create mode 100644 src/app/api/send-participant-email/route.ts create mode 100644 src/app/api/test-email/route.ts create mode 100644 src/app/api/test-smtp/route.ts create mode 100644 src/components/SendParticipantEmailModal.tsx create mode 100644 src/components/SendTestEmailModal.tsx create mode 100644 src/components/SmtpSettingsForm.tsx create mode 100644 src/components/ui/switch.tsx create mode 100644 src/lib/email.ts create mode 100644 src/lib/encryption.ts diff --git a/SETTINGS.md b/SETTINGS.md new file mode 100644 index 0000000..9117201 --- /dev/null +++ b/SETTINGS.md @@ -0,0 +1,144 @@ +# Paramètres de l'Application + +## Vue d'ensemble + +L'application dispose maintenant d'un système de paramètres global qui permet de configurer le comportement de l'application. Les paramètres sont organisés par catégories pour une meilleure organisation. + +## Accès aux paramètres + +1. Connectez-vous à l'interface d'administration +2. Cliquez sur le bouton "Paramètres" dans la barre d'outils +3. Vous accédez à la page de configuration des paramètres + +## Catégories de paramètres + +### Affichage + +Cette catégorie contient les paramètres liés à l'affichage de l'interface utilisateur. + +#### Ordre aléatoire des propositions + +- **Clé** : `randomize_propositions` +- **Type** : Booléen (true/false) +- **Valeur par défaut** : `false` +- **Description** : Lorsque activé, les propositions sont affichées dans un ordre aléatoire pour chaque participant lors du vote. + +**Comportement :** +- **Désactivé (Off)** : Les propositions sont affichées dans l'ordre chronologique de création +- **Activé (On)** : Les propositions sont mélangées aléatoirement pour chaque participant + +**Impact :** +- Ce paramètre affecte uniquement la page de vote (`/campaigns/[id]/vote/[participantId]`) +- L'ordre aléatoire est appliqué à chaque chargement de la page +- Chaque participant voit un ordre différent, ce qui peut réduire les biais liés à l'ordre d'affichage + +### Email + +Cette catégorie contient les paramètres de configuration SMTP pour l'envoi d'emails automatiques. + +#### Configuration SMTP + +- **smtp_host** : Serveur SMTP (ex: smtp.gmail.com) +- **smtp_port** : Port SMTP (ex: 587 pour TLS, 465 pour SSL) +- **smtp_username** : Nom d'utilisateur SMTP +- **smtp_password** : Mot de passe SMTP (chiffré automatiquement) +- **smtp_secure** : Connexion sécurisée SSL/TLS (booléen) +- **smtp_from_email** : Adresse email d'expédition +- **smtp_from_name** : Nom d'expédition + +**Sécurité :** +- Le mot de passe SMTP est automatiquement chiffré avec AES-256-GCM avant stockage +- La clé de chiffrement est dérivée de `SUPABASE_ANON_KEY` +- Seules les personnes ayant accès à la clé peuvent déchiffrer le mot de passe + +**Fonctionnalités :** +- Test de connexion SMTP intégré +- Envoi d'email de test avec template HTML +- Validation des paramètres avant sauvegarde +- Interface avec masquage du mot de passe +- Sauvegarde automatique avec feedback visuel + +## Structure technique + +### Base de données + +La table `settings` contient les paramètres : + +```sql +CREATE TABLE settings ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + key TEXT NOT NULL UNIQUE, + value TEXT NOT NULL, + category TEXT NOT NULL, + description TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); +``` + +### Services + +Le service `settingsService` fournit les méthodes suivantes : + +- `getAll()` : Récupère tous les paramètres +- `getByCategory(category)` : Récupère les paramètres d'une catégorie +- `getByKey(key)` : Récupère un paramètre par sa clé +- `getValue(key, defaultValue)` : Récupère la valeur d'un paramètre +- `getBooleanValue(key, defaultValue)` : Récupère la valeur booléenne d'un paramètre +- `setValue(key, value)` : Définit la valeur d'un paramètre +- `setBooleanValue(key, value)` : Définit la valeur booléenne d'un paramètre +- `getSmtpSettings()` : Récupère tous les paramètres SMTP +- `setSmtpSettings(settings)` : Sauvegarde les paramètres SMTP avec chiffrement +- `testSmtpConnection(settings)` : Teste la connexion SMTP +- `sendTestEmail(settings, toEmail)` : Envoie un email de test + +### Chiffrement + +Le service `encryptionService` fournit les méthodes suivantes : + +- `encrypt(value)` : Chiffre une valeur avec AES-256-GCM +- `decrypt(encryptedValue)` : Déchiffre une valeur chiffrée +- `isEncrypted(value)` : Vérifie si une valeur est chiffrée +- `mask(value)` : Masque une valeur pour l'affichage + +### Utilisation dans le code + +```typescript +import { settingsService } from '@/lib/services'; + +// Récupérer un paramètre booléen +const randomizePropositions = await settingsService.getBooleanValue('randomize_propositions', false); + +// Utiliser le paramètre +if (randomizePropositions) { + // Mélanger les propositions + propositions.sort(() => Math.random() - 0.5); +} + +// Récupérer les paramètres SMTP +const smtpSettings = await settingsService.getSmtpSettings(); + +// Envoyer un email de test +const result = await settingsService.sendTestEmail(smtpSettings, 'test@exemple.com'); +``` + +## Migration + +Pour ajouter la table des paramètres à une base de données existante : + +1. **Paramètres de base** : Exécutez le script SQL dans `migration-settings.sql` dans l'éditeur SQL de Supabase +2. **Paramètres SMTP** : Exécutez ensuite le script SQL dans `migration-smtp-settings.sql` + +## Ajout de nouveaux paramètres + +Pour ajouter un nouveau paramètre : + +1. Ajoutez l'insertion dans le script de migration +2. Utilisez le service `settingsService` dans votre code +3. Mettez à jour cette documentation + +## Sécurité + +- Tous les paramètres sont accessibles en lecture publique +- Seuls les administrateurs peuvent modifier les paramètres via l'interface d'administration +- Les paramètres sont protégés par l'AuthGuard diff --git a/migration-smtp-settings.sql b/migration-smtp-settings.sql new file mode 100644 index 0000000..81a5cf7 --- /dev/null +++ b/migration-smtp-settings.sql @@ -0,0 +1,31 @@ +-- Migration pour ajouter les paramètres SMTP +-- À exécuter dans l'éditeur SQL de Supabase après la migration des paramètres de base + +-- Paramètres SMTP (seulement s'ils n'existent pas déjà) +INSERT INTO settings (key, value, category, description) VALUES + ('smtp_host', '', 'email', 'Serveur SMTP') +ON CONFLICT (key) DO NOTHING; + +INSERT INTO settings (key, value, category, description) VALUES + ('smtp_port', '587', 'email', 'Port SMTP') +ON CONFLICT (key) DO NOTHING; + +INSERT INTO settings (key, value, category, description) VALUES + ('smtp_username', '', 'email', 'Nom d''utilisateur SMTP') +ON CONFLICT (key) DO NOTHING; + +INSERT INTO settings (key, value, category, description) VALUES + ('smtp_password', '', 'email', 'Mot de passe SMTP (chiffré)') +ON CONFLICT (key) DO NOTHING; + +INSERT INTO settings (key, value, category, description) VALUES + ('smtp_secure', 'true', 'email', 'Connexion sécurisée SSL/TLS') +ON CONFLICT (key) DO NOTHING; + +INSERT INTO settings (key, value, category, description) VALUES + ('smtp_from_email', '', 'email', 'Adresse email d''expédition') +ON CONFLICT (key) DO NOTHING; + +INSERT INTO settings (key, value, category, description) VALUES + ('smtp_from_name', 'Mes Budgets Participatifs', 'email', 'Nom d''expédition') +ON CONFLICT (key) DO NOTHING; diff --git a/package-lock.json b/package-lock.json index 01daac9..2195416 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,12 +16,15 @@ "@radix-ui/react-progress": "^1.1.7", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@supabase/supabase-js": "^2.56.0", + "@types/nodemailer": "^7.0.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.541.0", "next": "15.5.0", + "nodemailer": "^7.0.5", "react": "19.1.0", "react-dom": "19.1.0", "tailwind-merge": "^3.3.1" @@ -52,6 +55,684 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.873.0.tgz", + "integrity": "sha512-4NofVF7QjEQv0wX1mM2ZTVb0IxOZ2paAw2nLv3tPSlXKFtVF3AfMLOvOvL4ympCZSi1zC9FvBGrRrIr+X9wTfg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.873.0", + "@aws-sdk/credential-provider-node": "3.873.0", + "@aws-sdk/middleware-host-header": "3.873.0", + "@aws-sdk/middleware-logger": "3.873.0", + "@aws-sdk/middleware-recursion-detection": "3.873.0", + "@aws-sdk/middleware-user-agent": "3.873.0", + "@aws-sdk/region-config-resolver": "3.873.0", + "@aws-sdk/signature-v4-multi-region": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.873.0", + "@aws-sdk/util-user-agent-browser": "3.873.0", + "@aws-sdk/util-user-agent-node": "3.873.0", + "@smithy/config-resolver": "^4.1.5", + "@smithy/core": "^3.8.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/hash-node": "^4.0.5", + "@smithy/invalid-dependency": "^4.0.5", + "@smithy/middleware-content-length": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/middleware-retry": "^4.1.19", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.26", + "@smithy/util-defaults-mode-node": "^4.0.26", + "@smithy/util-endpoints": "^3.0.7", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.873.0.tgz", + "integrity": "sha512-EmcrOgFODWe7IsLKFTeSXM9TlQ80/BO1MBISlr7w2ydnOaUYIiPGRRJnDpeIgMaNqT4Rr2cRN2RiMrbFO7gDdA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.873.0", + "@aws-sdk/middleware-host-header": "3.873.0", + "@aws-sdk/middleware-logger": "3.873.0", + "@aws-sdk/middleware-recursion-detection": "3.873.0", + "@aws-sdk/middleware-user-agent": "3.873.0", + "@aws-sdk/region-config-resolver": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.873.0", + "@aws-sdk/util-user-agent-browser": "3.873.0", + "@aws-sdk/util-user-agent-node": "3.873.0", + "@smithy/config-resolver": "^4.1.5", + "@smithy/core": "^3.8.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/hash-node": "^4.0.5", + "@smithy/invalid-dependency": "^4.0.5", + "@smithy/middleware-content-length": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/middleware-retry": "^4.1.19", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.26", + "@smithy/util-defaults-mode-node": "^4.0.26", + "@smithy/util-endpoints": "^3.0.7", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.873.0.tgz", + "integrity": "sha512-WrROjp8X1VvmnZ4TBzwM7RF+EB3wRaY9kQJLXw+Aes0/3zRjUXvGIlseobGJMqMEGnM0YekD2F87UaVfot1xeQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@aws-sdk/xml-builder": "3.873.0", + "@smithy/core": "^3.8.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/property-provider": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/signature-v4": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-utf8": "^4.0.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.873.0.tgz", + "integrity": "sha512-FWj1yUs45VjCADv80JlGshAttUHBL2xtTAbJcAxkkJZzLRKVkdyrepFWhv/95MvDyzfbT6PgJiWMdW65l/8ooA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.873.0.tgz", + "integrity": "sha512-0sIokBlXIsndjZFUfr3Xui8W6kPC4DAeBGAXxGi9qbFZ9PWJjn1vt2COLikKH3q2snchk+AsznREZG8NW6ezSg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/property-provider": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/util-stream": "^4.2.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.873.0.tgz", + "integrity": "sha512-bQdGqh47Sk0+2S3C+N46aNQsZFzcHs7ndxYLARH/avYXf02Nl68p194eYFaAHJSQ1re5IbExU1+pbums7FJ9fA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.873.0", + "@aws-sdk/credential-provider-env": "3.873.0", + "@aws-sdk/credential-provider-http": "3.873.0", + "@aws-sdk/credential-provider-process": "3.873.0", + "@aws-sdk/credential-provider-sso": "3.873.0", + "@aws-sdk/credential-provider-web-identity": "3.873.0", + "@aws-sdk/nested-clients": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.873.0.tgz", + "integrity": "sha512-+v/xBEB02k2ExnSDL8+1gD6UizY4Q/HaIJkNSkitFynRiiTQpVOSkCkA0iWxzksMeN8k1IHTE5gzeWpkEjNwbA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.873.0", + "@aws-sdk/credential-provider-http": "3.873.0", + "@aws-sdk/credential-provider-ini": "3.873.0", + "@aws-sdk/credential-provider-process": "3.873.0", + "@aws-sdk/credential-provider-sso": "3.873.0", + "@aws-sdk/credential-provider-web-identity": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.873.0.tgz", + "integrity": "sha512-ycFv9WN+UJF7bK/ElBq1ugWA4NMbYS//1K55bPQZb2XUpAM2TWFlEjG7DIyOhLNTdl6+CbHlCdhlKQuDGgmm0A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.873.0.tgz", + "integrity": "sha512-SudkAOZmjEEYgUrqlUUjvrtbWJeI54/0Xo87KRxm4kfBtMqSx0TxbplNUAk8Gkg4XQNY0o7jpG8tK7r2Wc2+uw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.873.0", + "@aws-sdk/core": "3.873.0", + "@aws-sdk/token-providers": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.873.0.tgz", + "integrity": "sha512-Gw2H21+VkA6AgwKkBtTtlGZ45qgyRZPSKWs0kUwXVlmGOiPz61t/lBX0vG6I06ZIz2wqeTJ5OA1pWZLqw1j0JQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.873.0", + "@aws-sdk/nested-clients": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.873.0.tgz", + "integrity": "sha512-KZ/W1uruWtMOs7D5j3KquOxzCnV79KQW9MjJFZM/M0l6KI8J6V3718MXxFHsTjUE4fpdV6SeCNLV1lwGygsjJA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.873.0.tgz", + "integrity": "sha512-QhNZ8X7pW68kFez9QxUSN65Um0Feo18ZmHxszQZNUhKDsXew/EG9NPQE/HgYcekcon35zHxC4xs+FeNuPurP2g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.873.0.tgz", + "integrity": "sha512-OtgY8EXOzRdEWR//WfPkA/fXl0+WwE8hq0y9iw2caNyKPtca85dzrrZWnPqyBK/cpImosrpR1iKMYr41XshsCg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.873.0.tgz", + "integrity": "sha512-bOoWGH57ORK2yKOqJMmxBV4b3yMK8Pc0/K2A98MNPuQedXaxxwzRfsT2Qw+PpfYkiijrrNFqDYmQRGntxJ2h8A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-arn-parser": "3.873.0", + "@smithy/core": "^3.8.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/protocol-http": "^5.1.3", + "@smithy/signature-v4": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-stream": "^4.2.4", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.873.0.tgz", + "integrity": "sha512-gHqAMYpWkPhZLwqB3Yj83JKdL2Vsb64sryo8LN2UdpElpS+0fT4yjqSxKTfp7gkhN6TCIxF24HQgbPk5FMYJWw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.873.0", + "@smithy/core": "^3.8.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.873.0.tgz", + "integrity": "sha512-yg8JkRHuH/xO65rtmLOWcd9XQhxX1kAonp2CliXT44eA/23OBds6XoheY44eZeHfCTgutDLTYitvy3k9fQY6ZA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.873.0", + "@aws-sdk/middleware-host-header": "3.873.0", + "@aws-sdk/middleware-logger": "3.873.0", + "@aws-sdk/middleware-recursion-detection": "3.873.0", + "@aws-sdk/middleware-user-agent": "3.873.0", + "@aws-sdk/region-config-resolver": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.873.0", + "@aws-sdk/util-user-agent-browser": "3.873.0", + "@aws-sdk/util-user-agent-node": "3.873.0", + "@smithy/config-resolver": "^4.1.5", + "@smithy/core": "^3.8.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/hash-node": "^4.0.5", + "@smithy/invalid-dependency": "^4.0.5", + "@smithy/middleware-content-length": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/middleware-retry": "^4.1.19", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.26", + "@smithy/util-defaults-mode-node": "^4.0.26", + "@smithy/util-endpoints": "^3.0.7", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.873.0.tgz", + "integrity": "sha512-q9sPoef+BBG6PJnc4x60vK/bfVwvRWsPgcoQyIra057S/QGjq5VkjvNk6H8xedf6vnKlXNBwq9BaANBXnldUJg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.873.0.tgz", + "integrity": "sha512-FQ5OIXw1rmDud7f/VO9y2Mg9rX1o4MnngRKUOD8mS9ALK4uxKrTczb4jA+uJLSLwTqMGs3bcB1RzbMW1zWTMwQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/signature-v4": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.873.0.tgz", + "integrity": "sha512-BWOCeFeV/Ba8fVhtwUw/0Hz4wMm9fjXnMb4Z2a5he/jFlz5mt1/rr6IQ4MyKgzOaz24YrvqsJW2a0VUKOaYDvg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.873.0", + "@aws-sdk/nested-clients": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.862.0.tgz", + "integrity": "sha512-Bei+RL0cDxxV+lW2UezLbCYYNeJm6Nzee0TpW0FfyTRBhH9C1XQh4+x+IClriXvgBnRquTMMYsmJfvx8iyLKrg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.873.0.tgz", + "integrity": "sha512-qag+VTqnJWDn8zTAXX4wiVioa0hZDQMtbZcGRERVnLar4/3/VIKBhxX2XibNQXFu1ufgcRn4YntT/XEPecFWcg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.873.0.tgz", + "integrity": "sha512-YByHrhjxYdjKRf/RQygRK1uh0As1FIi9+jXTcIEX/rBgN8mUByczr2u4QXBzw7ZdbdcOBMOkPnLRjNOWW1MkFg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-endpoints": "^3.0.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.873.0.tgz", + "integrity": "sha512-xcVhZF6svjM5Rj89T1WzkjQmrTF6dpR2UvIHPMTnSZoNe6CixejPZ6f0JJ2kAhO8H+dUHwNBlsUgOTIKiK/Syg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.873.0.tgz", + "integrity": "sha512-AcRdbK6o19yehEcywI43blIBhOCSo6UgyWcuOJX5CFF8k39xm1ILCjQlRRjchLAxWrm0lU0Q7XV90RiMMFMZtA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.873.0.tgz", + "integrity": "sha512-9MivTP+q9Sis71UxuBaIY3h5jxH0vN3/ZWGxO8ADL19S2OIfknrYSAfzE5fpoKROVBu0bS4VifHOFq4PY1zsxw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.873.0.tgz", + "integrity": "sha512-kLO7k7cGJ6KaHiExSJWojZurF7SnGMDHXRuQunFnEoD0n1yB6Lqy/S/zHiQ7oJnBhPr9q0TW9qFkrsZb1Uc54w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@emnapi/core": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", @@ -1556,6 +2237,35 @@ } } }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-tabs": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", @@ -1880,6 +2590,576 @@ "dev": true, "license": "MIT" }, + "node_modules/@smithy/abort-controller": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.5.tgz", + "integrity": "sha512-jcrqdTQurIrBbUm4W2YdLVMQDoL0sA9DTxYd2s+R/y+2U9NLOP7Xf/YqfSg1FZhlZIYEnvk2mwbyvIfdLEPo8g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.1.5.tgz", + "integrity": "sha512-viuHMxBAqydkB0AfWwHIdwf/PRH2z5KHGUzqyRtS/Wv+n3IHI993Sk76VCA7dD/+GzgGOmlJDITfPcJC1nIVIw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.8.0.tgz", + "integrity": "sha512-EYqsIYJmkR1VhVE9pccnk353xhs+lB6btdutJEtsp7R055haMJp2yE16eSxw8fv+G0WUY6vqxyYOP8kOqawxYQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.0.9", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-stream": "^4.2.4", + "@smithy/util-utf8": "^4.0.0", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.7.tgz", + "integrity": "sha512-dDzrMXA8d8riFNiPvytxn0mNwR4B3h8lgrQ5UjAGu6T9z/kRg/Xncf4tEQHE/+t25sY8IH3CowcmWi+1U5B1Gw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.4", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.1.1.tgz", + "integrity": "sha512-61WjM0PWmZJR+SnmzaKI7t7G0UkkNFboDpzIdzSoy7TByUzlxo18Qlh9s71qug4AY4hlH/CwXdubMtkcNEb/sQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.3", + "@smithy/querystring-builder": "^4.0.5", + "@smithy/types": "^4.3.2", + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.5.tgz", + "integrity": "sha512-cv1HHkKhpyRb6ahD8Vcfb2Hgz67vNIXEp2vnhzfxLFGRukLCNEA5QdsorbUEzXma1Rco0u3rx5VTqbM06GcZqQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.5.tgz", + "integrity": "sha512-IVnb78Qtf7EJpoEVo7qJ8BEXQwgC4n3igeJNNKEj/MLYtapnx8A67Zt/J3RXAj2xSO1910zk0LdFiygSemuLow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", + "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.5.tgz", + "integrity": "sha512-l1jlNZoYzoCC7p0zCtBDE5OBXZ95yMKlRlftooE5jPWQn4YBPLgsp+oeHp7iMHaTGoUdFqmHOPa8c9G3gBsRpQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.18.tgz", + "integrity": "sha512-ZhvqcVRPZxnZlokcPaTwb+r+h4yOIOCJmx0v2d1bpVlmP465g3qpVSf7wxcq5zZdu4jb0H4yIMxuPwDJSQc3MQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.8.0", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-middleware": "^4.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.1.19", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.19.tgz", + "integrity": "sha512-X58zx/NVECjeuUB6A8HBu4bhx72EoUz+T5jTMIyeNKx2lf+Gs9TmWPNNkH+5QF0COjpInP/xSpJGJ7xEnAklQQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.4", + "@smithy/protocol-http": "^5.1.3", + "@smithy/service-error-classification": "^4.0.7", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.9.tgz", + "integrity": "sha512-uAFFR4dpeoJPGz8x9mhxp+RPjo5wW0QEEIPPPbLXiRRWeCATf/Km3gKIVR5vaP8bN1kgsPhcEeh+IZvUlBv6Xg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.5.tgz", + "integrity": "sha512-/yoHDXZPh3ocRVyeWQFvC44u8seu3eYzZRveCMfgMOBcNKnAmOvjbL9+Cp5XKSIi9iYA9PECUuW2teDAk8T+OQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.1.4.tgz", + "integrity": "sha512-+UDQV/k42jLEPPHSn39l0Bmc4sB1xtdI9Gd47fzo/0PbXzJ7ylgaOByVjF5EeQIumkepnrJyfx86dPa9p47Y+w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.1.1.tgz", + "integrity": "sha512-RHnlHqFpoVdjSPPiYy/t40Zovf3BBHc2oemgD7VsVTFFZrU5erFFe0n52OANZZ/5sbshgD93sOh5r6I35Xmpaw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/querystring-builder": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.5.tgz", + "integrity": "sha512-R/bswf59T/n9ZgfgUICAZoWYKBHcsVDurAGX88zsiUtOTA/xUAPyiT+qkNCPwFn43pZqN84M4MiUsbSGQmgFIQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.3.tgz", + "integrity": "sha512-fCJd2ZR7D22XhDY0l+92pUag/7je2BztPRQ01gU5bMChcyI0rlly7QFibnYHzcxDvccMjlpM/Q1ev8ceRIb48w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.5.tgz", + "integrity": "sha512-NJeSCU57piZ56c+/wY+AbAw6rxCCAOZLCIniRE7wqvndqxcKKDOXzwWjrY7wGKEISfhL9gBbAaWWgHsUGedk+A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "@smithy/util-uri-escape": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.5.tgz", + "integrity": "sha512-6SV7md2CzNG/WUeTjVe6Dj8noH32r4MnUeFKZrnVYsQxpGSIcphAanQMayi8jJLZAWm6pdM9ZXvKCpWOsIGg0w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.7.tgz", + "integrity": "sha512-XvRHOipqpwNhEjDf2L5gJowZEm5nsxC16pAZOeEcsygdjv9A2jdOh3YoDQvOXBGTsaJk6mNWtzWalOB9976Wlg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.5.tgz", + "integrity": "sha512-YVVwehRDuehgoXdEL4r1tAAzdaDgaC9EQvhK0lEbfnbrd0bd5+CTQumbdPryX3J2shT7ZqQE+jPW4lmNBAB8JQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.1.3.tgz", + "integrity": "sha512-mARDSXSEgllNzMw6N+mC+r1AQlEBO3meEAkR/UlfAgnMzJUB3goRBWgip1EAMG99wh36MDqzo86SfIX5Y+VEaw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.4.10.tgz", + "integrity": "sha512-iW6HjXqN0oPtRS0NK/zzZ4zZeGESIFcxj2FkWed3mcK8jdSdHzvnCKXSjvewESKAgGKAbJRA+OsaqKhkdYRbQQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.8.0", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "@smithy/util-stream": "^4.2.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.2.tgz", + "integrity": "sha512-QO4zghLxiQ5W9UZmX2Lo0nta2PuE1sSrXUYDoaB6HMR762C0P7v/HEPHf6ZdglTVssJG1bsrSBxdc3quvDSihw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.5.tgz", + "integrity": "sha512-j+733Um7f1/DXjYhCbvNXABV53NyCRRA54C7bNEIxNPs0YjfRxeMKjjgm2jvTYrciZyCjsicHwQ6Q0ylo+NAUw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz", + "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz", + "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", + "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", + "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz", + "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.0.26", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.26.tgz", + "integrity": "sha512-xgl75aHIS/3rrGp7iTxQAOELYeyiwBu+eEgAk4xfKwJJ0L8VUjhO2shsDpeil54BOFsqmk5xfdesiewbUY5tKQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.5", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.0.26", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.26.tgz", + "integrity": "sha512-z81yyIkGiLLYVDetKTUeCZQ8x20EEzvQjrqJtb/mXnevLq2+w3XCEWTJ2pMp401b6BkEkHVfXb/cROBpVauLMQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.1.5", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/property-provider": "^4.0.5", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.7.tgz", + "integrity": "sha512-klGBP+RpBp6V5JbrY2C/VKnHXn3d5V2YrifZbmMY8os7M6m8wdYFoO6w/fe5VkP+YVwrEktW3IWYaSQVNZJ8oQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", + "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.5.tgz", + "integrity": "sha512-N40PfqsZHRSsByGB81HhSo+uvMxEHT+9e255S53pfBw/wI6WKDI7Jw9oyu5tJTLwZzV5DsMha3ji8jk9dsHmQQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.7.tgz", + "integrity": "sha512-TTO6rt0ppK70alZpkjwy+3nQlTiqNfoXja+qwuAchIEAIoSZW8Qyd76dvBv3I5bCpE38APafG23Y/u270NspiQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.0.7", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.4.tgz", + "integrity": "sha512-vSKnvNZX2BXzl0U2RgCLOwWaAP9x/ddd/XobPK02pCbzRm5s55M53uwb1rl/Ts7RXZvdJZerPkA+en2FDghLuQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/types": "^4.3.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", + "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", + "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@supabase/auth-js": { "version": "2.71.1", "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.71.1.tgz", @@ -2307,6 +3587,16 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/nodemailer": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.1.tgz", + "integrity": "sha512-UfHAghPmGZVzaL8x9y+mKZMWyHC399+iq0MOmya5tIyenWX3lcdSb60vOmp0DocR6gCDTYTozv/ULQnREyyjkg==", + "license": "MIT", + "dependencies": { + "@aws-sdk/client-sesv2": "^3.839.0", + "@types/node": "*" + } + }, "node_modules/@types/phoenix": { "version": "1.6.6", "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", @@ -2333,6 +3623,12 @@ "@types/react": "^19.0.0" } }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -3204,6 +4500,12 @@ "dev": true, "license": "MIT" }, + "node_modules/bowser": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.1.tgz", + "integrity": "sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -4290,6 +5592,24 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -5798,6 +7118,15 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/nodemailer": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.5.tgz", + "integrity": "sha512-nsrh2lO3j4GkLLXoeEksAMgAOqxOv6QumNRVQTJwKH4nuiww6iC2y7GyANs9kRAxCexg3+lTWM3PZ91iLlVjfg==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -6798,6 +8127,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -7238,6 +8579,19 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/package.json b/package.json index 1c79775..0b6adbb 100644 --- a/package.json +++ b/package.json @@ -17,12 +17,15 @@ "@radix-ui/react-progress": "^1.1.7", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@supabase/supabase-js": "^2.56.0", + "@types/nodemailer": "^7.0.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.541.0", "next": "15.5.0", + "nodemailer": "^7.0.5", "react": "19.1.0", "react-dom": "19.1.0", "tailwind-merge": "^3.3.1" diff --git a/src/app/admin/campaigns/[id]/participants/page.tsx b/src/app/admin/campaigns/[id]/participants/page.tsx index 5304256..97c5b6d 100644 --- a/src/app/admin/campaigns/[id]/participants/page.tsx +++ b/src/app/admin/campaigns/[id]/participants/page.tsx @@ -8,6 +8,7 @@ import AddParticipantModal from '@/components/AddParticipantModal'; import EditParticipantModal from '@/components/EditParticipantModal'; import DeleteParticipantModal from '@/components/DeleteParticipantModal'; import ImportCSVModal from '@/components/ImportCSVModal'; +import SendParticipantEmailModal from '@/components/SendParticipantEmailModal'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; @@ -29,6 +30,7 @@ function CampaignParticipantsPageContent() { const [showEditModal, setShowEditModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); const [showImportModal, setShowImportModal] = useState(false); + const [showSendEmailModal, setShowSendEmailModal] = useState(false); const [selectedParticipant, setSelectedParticipant] = useState(null); const [copiedParticipantId, setCopiedParticipantId] = useState(null); @@ -357,6 +359,18 @@ function CampaignParticipantsPageContent() { )} + @@ -402,6 +416,15 @@ function CampaignParticipantsPageContent() { type="participants" campaignTitle={campaign?.title} /> + + {selectedParticipant && campaign && ( + setShowSendEmailModal(false)} + participant={selectedParticipant} + campaign={campaign} + /> + )} ); diff --git a/src/app/admin/campaigns/[id]/stats/page.tsx b/src/app/admin/campaigns/[id]/stats/page.tsx index c1b3d43..69f206d 100644 --- a/src/app/admin/campaigns/[id]/stats/page.tsx +++ b/src/app/admin/campaigns/[id]/stats/page.tsx @@ -9,6 +9,7 @@ import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Progress } from '@/components/ui/progress'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import Navigation from '@/components/Navigation'; import AuthGuard from '@/components/AuthGuard'; import { @@ -20,7 +21,12 @@ import { Award, FileText, Calendar, - ArrowLeft + ArrowLeft, + SortAsc, + TrendingDown, + Users2, + Target as TargetIcon, + Hash } from 'lucide-react'; export const dynamic = 'force-dynamic'; @@ -31,8 +37,29 @@ interface PropositionStats { averageAmount: number; minAmount: number; maxAmount: number; + totalAmount: number; + participationRate: number; + voteDistribution: number; + consensusScore: number; } +type SortOption = + | 'popularity' + | 'total_impact' + | 'consensus' + | 'engagement' + | 'distribution' + | 'alphabetical'; + +const sortOptions = [ + { value: 'popularity', label: 'Popularité', icon: TrendingUp, description: 'Moyenne décroissante puis nombre de votants' }, + { value: 'total_impact', label: 'Impact total', icon: Target, description: 'Somme totale investie décroissante' }, + { value: 'consensus', label: 'Consensus', icon: Users2, description: 'Plus petit écart-type = plus de consensus' }, + { value: 'engagement', label: 'Engagement', icon: Users, description: 'Taux de participation décroissant' }, + { value: 'distribution', label: 'Répartition', icon: BarChart3, description: 'Nombre de votes différents' }, + { value: 'alphabetical', label: 'Alphabétique', icon: Hash, description: 'Ordre alphabétique par titre' } +]; + function CampaignStatsPageContent() { const params = useParams(); const campaignId = params.id as string; @@ -43,6 +70,7 @@ function CampaignStatsPageContent() { const [votes, setVotes] = useState([]); const [loading, setLoading] = useState(true); const [propositionStats, setPropositionStats] = useState([]); + const [sortBy, setSortBy] = useState('popularity'); useEffect(() => { if (campaignId) { @@ -74,13 +102,33 @@ function CampaignStatsPageContent() { const stats = propositionsData.map(proposition => { const propositionVotes = votesData.filter(vote => vote.proposition_id === proposition.id && vote.amount > 0); const amounts = propositionVotes.map(vote => vote.amount); + const totalAmount = amounts.reduce((sum, amount) => sum + amount, 0); + + // Calculer l'écart-type pour le consensus + const mean = amounts.length > 0 ? totalAmount / amounts.length : 0; + const variance = amounts.length > 0 + ? amounts.reduce((sum, amount) => sum + Math.pow(amount - mean, 2), 0) / amounts.length + : 0; + const consensusScore = Math.sqrt(variance); + + // Calculer le taux de participation pour cette proposition + const participationRate = participantsData.length > 0 + ? (propositionVotes.length / participantsData.length) * 100 + : 0; + + // Calculer la répartition des votes (nombre de montants différents) + const uniqueAmounts = new Set(amounts).size; return { proposition, voteCount: propositionVotes.length, - averageAmount: amounts.length > 0 ? Math.round(amounts.reduce((sum, amount) => sum + amount, 0) / amounts.length) : 0, + averageAmount: amounts.length > 0 ? Math.round(totalAmount / amounts.length) : 0, minAmount: amounts.length > 0 ? Math.min(...amounts) : 0, - maxAmount: amounts.length > 0 ? Math.max(...amounts) : 0 + maxAmount: amounts.length > 0 ? Math.max(...amounts) : 0, + totalAmount, + participationRate: Math.round(participationRate * 100) / 100, + voteDistribution: uniqueAmounts, + consensusScore: Math.round(consensusScore * 100) / 100 }; }); @@ -92,6 +140,38 @@ function CampaignStatsPageContent() { } }; + const getSortedStats = () => { + const sorted = [...propositionStats]; + + switch (sortBy) { + case 'popularity': + return sorted.sort((a, b) => { + if (b.averageAmount !== a.averageAmount) { + return b.averageAmount - a.averageAmount; + } + return b.voteCount - a.voteCount; + }); + + case 'total_impact': + return sorted.sort((a, b) => b.totalAmount - a.totalAmount); + + case 'consensus': + return sorted.sort((a, b) => a.consensusScore - b.consensusScore); + + case 'engagement': + return sorted.sort((a, b) => b.participationRate - a.participationRate); + + case 'distribution': + return sorted.sort((a, b) => b.voteDistribution - a.voteDistribution); + + case 'alphabetical': + return sorted.sort((a, b) => a.proposition.title.localeCompare(b.proposition.title)); + + default: + return sorted; + } + }; + const getParticipationRate = () => { if (participants.length === 0) return 0; const votedParticipants = participants.filter(p => { @@ -101,8 +181,6 @@ function CampaignStatsPageContent() { return Math.round((votedParticipants.length / participants.length) * 100); }; - - const getAverageVotesPerProposition = () => { if (propositions.length === 0) return 0; const totalVotes = votes.filter(v => v.amount > 0).length; @@ -230,13 +308,42 @@ function CampaignStatsPageContent() { {/* Propositions Stats */} - - - Préférences par proposition - - - Statistiques des montants exprimés par les participants pour chaque proposition - +
+
+ + + Préférences par proposition + + + Statistiques des montants exprimés par les participants pour chaque proposition + +
+ +
+ Trier par : + +
+
{propositionStats.length === 0 ? ( @@ -251,9 +358,7 @@ function CampaignStatsPageContent() { ) : (
- {propositionStats - .sort((a, b) => b.averageAmount - a.averageAmount) // Trier par moyenne décroissante - .map((stat, index) => ( + {getSortedStats().map((stat, index) => (
@@ -269,15 +374,19 @@ function CampaignStatsPageContent() { {stat.proposition.description}

- {index === 0 && stat.averageAmount > 0 && ( - - - Préférée - - )} + {index === 0 && stat.averageAmount > 0 && ( + + + {sortBy === 'popularity' ? 'Préférée' : + sortBy === 'total_impact' ? 'Plus d\'impact' : + sortBy === 'consensus' ? 'Plus de consensus' : + sortBy === 'engagement' ? 'Plus d\'engagement' : + sortBy === 'distribution' ? 'Plus de répartition' : 'Première'} + + )}
-
+

{stat.voteCount} @@ -292,6 +401,12 @@ function CampaignStatsPageContent() {

Moyenne

+
+

+ {stat.totalAmount}€ +

+

Total

+

{stat.minAmount}€ @@ -304,6 +419,34 @@ function CampaignStatsPageContent() {

Maximum

+
+

+ {stat.participationRate}% +

+

Participation

+
+
+ + {/* Métriques avancées */} +
+
+
+ + Consensus +
+ + Écart-type: {stat.consensusScore}€ + +
+
+
+ + Répartition +
+ + {stat.voteDistribution} montants différents + +
{stat.voteCount > 0 && ( diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 2b8c555..254a88f 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -13,7 +13,7 @@ import { Input } from '@/components/ui/input'; import { Progress } from '@/components/ui/progress'; import Navigation from '@/components/Navigation'; import AuthGuard from '@/components/AuthGuard'; -import { FolderOpen, Users, FileText, CheckCircle, Clock, Plus, BarChart3 } from 'lucide-react'; +import { FolderOpen, Users, FileText, CheckCircle, Clock, Plus, BarChart3, Settings } from 'lucide-react'; export const dynamic = 'force-dynamic'; @@ -123,10 +123,18 @@ function AdminPageContent() {

Administration

Gérez vos campagnes de budget participatif

- +
+ + +
diff --git a/src/app/admin/settings/page.tsx b/src/app/admin/settings/page.tsx new file mode 100644 index 0000000..8094ed3 --- /dev/null +++ b/src/app/admin/settings/page.tsx @@ -0,0 +1,181 @@ +'use client'; +import { useState, useEffect } from 'react'; +import { Setting } from '@/types'; +import { settingsService } from '@/lib/services'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Switch } from '@/components/ui/switch'; +import { Label } from '@/components/ui/label'; +import Navigation from '@/components/Navigation'; +import AuthGuard from '@/components/AuthGuard'; +import SmtpSettingsForm from '@/components/SmtpSettingsForm'; +import { Settings, Monitor, Save, CheckCircle, Mail } from 'lucide-react'; + +export const dynamic = 'force-dynamic'; + +function SettingsPageContent() { + const [settings, setSettings] = useState([]); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [saved, setSaved] = useState(false); + const [randomizePropositions, setRandomizePropositions] = useState(false); + + useEffect(() => { + loadSettings(); + }, []); + + const loadSettings = async () => { + try { + setLoading(true); + const settingsData = await settingsService.getAll(); + setSettings(settingsData); + + // Charger la valeur du paramètre d'ordre aléatoire + const randomizeValue = await settingsService.getBooleanValue('randomize_propositions', false); + setRandomizePropositions(randomizeValue); + } catch (error) { + console.error('Erreur lors du chargement des paramètres:', error); + } finally { + setLoading(false); + } + }; + + const handleRandomizeChange = async (checked: boolean) => { + setRandomizePropositions(checked); + }; + + const handleSave = async () => { + try { + setSaving(true); + await settingsService.setBooleanValue('randomize_propositions', randomizePropositions); + setSaved(true); + setTimeout(() => setSaved(false), 2000); + } catch (error) { + console.error('Erreur lors de la sauvegarde des paramètres:', error); + } finally { + setSaving(false); + } + }; + + if (loading) { + return ( +
+
+ +
+
+
+

Chargement des paramètres...

+
+
+
+
+ ); + } + + return ( +
+
+ + + {/* Header */} +
+
+
+

Paramètres

+

Configurez les paramètres de l'application

+
+ +
+
+ + {/* Settings Categories */} +
+ {/* Affichage Category */} + + +
+
+ +
+
+ Affichage + + Paramètres d'affichage de l'interface utilisateur + +
+
+
+ + {/* Randomize Propositions Setting */} +
+
+ +

+ Lorsque activé, les propositions seront affichées dans un ordre aléatoire pour chaque participant lors du vote. +

+
+ +
+
+
+ + {/* Email Category */} + { + setSaved(true); + setTimeout(() => setSaved(false), 2000); + }} /> + + {/* Future Categories Placeholder */} + + + +

+ Plus de catégories à venir +

+

+ D'autres catégories de paramètres seront ajoutées prochainement. +

+
+
+
+
+
+ ); +} + +export default function SettingsPage() { + return ( + + + + ); +} diff --git a/src/app/api/send-participant-email/route.ts b/src/app/api/send-participant-email/route.ts new file mode 100644 index 0000000..798c749 --- /dev/null +++ b/src/app/api/send-participant-email/route.ts @@ -0,0 +1,151 @@ +import { NextRequest, NextResponse } from 'next/server'; +import * as nodemailer from 'nodemailer'; +import { SmtpSettings } from '@/types'; + +export async function POST(request: NextRequest) { + try { + const { + smtpSettings, + toEmail, + toName, + subject, + message, + campaignTitle, + voteUrl + } = await request.json(); + + // Validation des paramètres + if (!smtpSettings || !toEmail || !toName || !subject || !message) { + return NextResponse.json( + { success: false, error: 'Paramètres manquants' }, + { status: 400 } + ); + } + + // Validation de l'email + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(toEmail)) { + return NextResponse.json( + { success: false, error: 'Adresse email de destination invalide' }, + { status: 400 } + ); + } + + // Validation des paramètres SMTP + if (!smtpSettings.host || !smtpSettings.port || !smtpSettings.username || !smtpSettings.password) { + return NextResponse.json( + { success: false, error: 'Paramètres SMTP incomplets' }, + { status: 400 } + ); + } + + // Créer le transporteur SMTP + const transporter = nodemailer.createTransport({ + host: smtpSettings.host, + port: smtpSettings.port, + secure: smtpSettings.secure, + auth: { + user: smtpSettings.username, + pass: smtpSettings.password, + }, + tls: { + rejectUnauthorized: false, + }, + connectionTimeout: 10000, + greetingTimeout: 10000, + socketTimeout: 10000, + }); + + // Vérifier la connexion + await transporter.verify(); + + // Créer le contenu HTML de l'email + const htmlContent = ` +
+
+

Mes Budgets Participatifs

+
+ +
+

Bonjour ${toName},

+ +
+

Campagne : ${campaignTitle}

+

${message.replace(/\n/g, '
')}

+
+ + + +
+

+ 💡 Conseil : Ce lien est personnel et unique. Ne le partagez pas avec d'autres personnes. +

+
+ +
+ +

+ Si le bouton ne fonctionne pas, vous pouvez copier et coller ce lien dans votre navigateur : +

+

+ ${voteUrl} +

+
+ +
+

+ Cet email a été envoyé automatiquement par Mes Budgets Participatifs.
+ Si vous avez des questions, contactez l'administrateur de la campagne. +

+
+
+ `; + + // Envoyer l'email + const info = await transporter.sendMail({ + from: `"${smtpSettings.from_name}" <${smtpSettings.from_email}>`, + to: `"${toName}" <${toEmail}>`, + subject: subject, + text: message, + html: htmlContent, + }); + + return NextResponse.json({ + success: true, + messageId: info.messageId, + message: 'Email envoyé avec succès' + }); + + } catch (error) { + console.error('Erreur lors de l\'envoi de l\'email au participant:', error); + + let errorMessage = 'Erreur lors de l\'envoi de l\'email'; + + if (error instanceof Error) { + if (error.message.includes('EBADNAME')) { + errorMessage = 'Impossible de résoudre le nom d\'hôte SMTP. Vérifiez que le serveur SMTP est correct.'; + } else if (error.message.includes('ECONNREFUSED')) { + errorMessage = 'Connexion refusée. Vérifiez le serveur et le port SMTP.'; + } else if (error.message.includes('ETIMEDOUT')) { + errorMessage = 'Délai d\'attente dépassé. Vérifiez votre connexion internet et les paramètres SMTP.'; + } else if (error.message.includes('EAUTH')) { + errorMessage = 'Authentification échouée. Vérifiez le nom d\'utilisateur et le mot de passe SMTP.'; + } else { + errorMessage = error.message; + } + } + + return NextResponse.json( + { + success: false, + error: errorMessage + }, + { status: 500 } + ); + } +} diff --git a/src/app/api/test-email/route.ts b/src/app/api/test-email/route.ts new file mode 100644 index 0000000..03dad00 --- /dev/null +++ b/src/app/api/test-email/route.ts @@ -0,0 +1,134 @@ +import { NextRequest, NextResponse } from 'next/server'; +import * as nodemailer from 'nodemailer'; +import { SmtpSettings } from '@/types'; + +export async function POST(request: NextRequest) { + try { + const { smtpSettings, toEmail } = await request.json(); + + // Validation des paramètres + if (!smtpSettings || !toEmail) { + return NextResponse.json( + { success: false, error: 'Paramètres manquants' }, + { status: 400 } + ); + } + + // Validation de l'email + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(toEmail)) { + return NextResponse.json( + { success: false, error: 'Adresse email de destination invalide' }, + { status: 400 } + ); + } + + // Validation des paramètres SMTP + if (!smtpSettings.host || !smtpSettings.port || !smtpSettings.username || !smtpSettings.password) { + return NextResponse.json( + { success: false, error: 'Paramètres SMTP incomplets' }, + { status: 400 } + ); + } + + // Créer le transporteur SMTP avec options de résolution DNS + const transporter = nodemailer.createTransport({ + host: smtpSettings.host, + port: smtpSettings.port, + secure: smtpSettings.secure, // true pour 465, false pour les autres ports + auth: { + user: smtpSettings.username, + pass: smtpSettings.password, + }, + // Options pour résoudre les problèmes DNS + tls: { + rejectUnauthorized: false, // Accepte les certificats auto-signés + }, + // Timeout pour éviter les blocages + connectionTimeout: 10000, // 10 secondes + greetingTimeout: 10000, + socketTimeout: 10000, + }); + + // Vérifier la connexion + await transporter.verify(); + + // Envoyer l'email de test + const info = await transporter.sendMail({ + from: `"${smtpSettings.from_name}" <${smtpSettings.from_email}>`, + to: toEmail, + subject: 'Test de configuration SMTP - Mes Budgets Participatifs', + html: ` +
+

✅ Test de configuration SMTP réussi !

+

Bonjour,

+

Cet email confirme que votre configuration SMTP fonctionne correctement.

+
+

Configuration utilisée :

+
    +
  • Serveur : ${smtpSettings.host}:${smtpSettings.port}
  • +
  • Sécurisé : ${smtpSettings.secure ? 'Oui (SSL/TLS)' : 'Non'}
  • +
  • Utilisateur : ${smtpSettings.username}
  • +
  • Expéditeur : ${smtpSettings.from_name} <${smtpSettings.from_email}>
  • +
+
+

Vous pouvez maintenant utiliser cette configuration pour envoyer des emails automatiques depuis votre application.

+
+

+ Cet email a été envoyé automatiquement par Mes Budgets Participatifs pour tester la configuration SMTP. +

+
+ `, + text: ` +Test de configuration SMTP réussi ! + +Bonjour, + +Cet email confirme que votre configuration SMTP fonctionne correctement. + +Configuration utilisée : +- Serveur : ${smtpSettings.host}:${smtpSettings.port} +- Sécurisé : ${smtpSettings.secure ? 'Oui (SSL/TLS)' : 'Non'} +- Utilisateur : ${smtpSettings.username} +- Expéditeur : ${smtpSettings.from_name} <${smtpSettings.from_email}> + +Vous pouvez maintenant utiliser cette configuration pour envoyer des emails automatiques depuis votre application. + +--- +Cet email a été envoyé automatiquement par Mes Budgets Participatifs pour tester la configuration SMTP. + ` + }); + + return NextResponse.json({ + success: true, + messageId: info.messageId + }); + + } catch (error) { + console.error('Erreur lors de l\'envoi de l\'email de test:', error); + + let errorMessage = 'Erreur lors de l\'envoi de l\'email'; + + if (error instanceof Error) { + if (error.message.includes('EBADNAME')) { + errorMessage = 'Impossible de résoudre le nom d\'hôte SMTP. Vérifiez que le serveur SMTP est correct.'; + } else if (error.message.includes('ECONNREFUSED')) { + errorMessage = 'Connexion refusée. Vérifiez le serveur et le port SMTP.'; + } else if (error.message.includes('ETIMEDOUT')) { + errorMessage = 'Délai d\'attente dépassé. Vérifiez votre connexion internet et les paramètres SMTP.'; + } else if (error.message.includes('EAUTH')) { + errorMessage = 'Authentification échouée. Vérifiez le nom d\'utilisateur et le mot de passe.'; + } else { + errorMessage = error.message; + } + } + + return NextResponse.json( + { + success: false, + error: errorMessage + }, + { status: 500 } + ); + } +} diff --git a/src/app/api/test-smtp/route.ts b/src/app/api/test-smtp/route.ts new file mode 100644 index 0000000..32e684d --- /dev/null +++ b/src/app/api/test-smtp/route.ts @@ -0,0 +1,95 @@ +import { NextRequest, NextResponse } from 'next/server'; +import * as nodemailer from 'nodemailer'; +import { SmtpSettings } from '@/types'; + +export async function POST(request: NextRequest) { + try { + const { smtpSettings } = await request.json(); + + // Validation des paramètres + if (!smtpSettings) { + return NextResponse.json( + { success: false, error: 'Paramètres SMTP manquants' }, + { status: 400 } + ); + } + + // Validation des paramètres SMTP + if (!smtpSettings.host || !smtpSettings.port || !smtpSettings.username || !smtpSettings.password) { + return NextResponse.json( + { success: false, error: 'Paramètres SMTP incomplets' }, + { status: 400 } + ); + } + + // Validation du port + if (smtpSettings.port < 1 || smtpSettings.port > 65535) { + return NextResponse.json( + { success: false, error: 'Port SMTP invalide' }, + { status: 400 } + ); + } + + // Validation de l'email d'expédition + if (!smtpSettings.from_email.includes('@')) { + return NextResponse.json( + { success: false, error: 'Adresse email d\'expédition invalide' }, + { status: 400 } + ); + } + + // Créer le transporteur SMTP avec options de résolution DNS + const transporter = nodemailer.createTransport({ + host: smtpSettings.host, + port: smtpSettings.port, + secure: smtpSettings.secure, // true pour 465, false pour les autres ports + auth: { + user: smtpSettings.username, + pass: smtpSettings.password, + }, + // Options pour résoudre les problèmes DNS + tls: { + rejectUnauthorized: false, // Accepte les certificats auto-signés + }, + // Timeout pour éviter les blocages + connectionTimeout: 10000, // 10 secondes + greetingTimeout: 10000, + socketTimeout: 10000, + }); + + // Vérifier la connexion + await transporter.verify(); + + return NextResponse.json({ + success: true, + message: 'Connexion SMTP réussie' + }); + + } catch (error) { + console.error('Erreur lors du test de connexion SMTP:', error); + + let errorMessage = 'Erreur de connexion SMTP'; + + if (error instanceof Error) { + if (error.message.includes('EBADNAME')) { + errorMessage = 'Impossible de résoudre le nom d\'hôte SMTP. Vérifiez que le serveur SMTP est correct.'; + } else if (error.message.includes('ECONNREFUSED')) { + errorMessage = 'Connexion refusée. Vérifiez le serveur et le port SMTP.'; + } else if (error.message.includes('ETIMEDOUT')) { + errorMessage = 'Délai d\'attente dépassé. Vérifiez votre connexion internet et les paramètres SMTP.'; + } else if (error.message.includes('EAUTH')) { + errorMessage = 'Authentification échouée. Vérifiez le nom d\'utilisateur et le mot de passe.'; + } else { + errorMessage = error.message; + } + } + + return NextResponse.json( + { + success: false, + error: errorMessage + }, + { status: 500 } + ); + } +} diff --git a/src/app/campaigns/[id]/vote/[participantId]/page.tsx b/src/app/campaigns/[id]/vote/[participantId]/page.tsx index 55d6cf3..bc46101 100644 --- a/src/app/campaigns/[id]/vote/[participantId]/page.tsx +++ b/src/app/campaigns/[id]/vote/[participantId]/page.tsx @@ -4,7 +4,7 @@ import { useState, useEffect } from 'react'; import { useParams, useRouter } from 'next/navigation'; import Link from 'next/link'; import { Campaign, Proposition, Participant, Vote, PropositionWithVote } from '@/types'; -import { campaignService, participantService, propositionService, voteService } from '@/lib/services'; +import { campaignService, participantService, propositionService, voteService, settingsService } from '@/lib/services'; // Force dynamic rendering to avoid SSR issues with Supabase export const dynamic = 'force-dynamic'; @@ -26,6 +26,7 @@ export default function PublicVotePage() { // Votes temporaires stockés localement const [localVotes, setLocalVotes] = useState>({}); const [totalVoted, setTotalVoted] = useState(0); + const [isRandomOrder, setIsRandomOrder] = useState(false); useEffect(() => { if (campaignId && participantId) { @@ -73,11 +74,20 @@ export default function PublicVotePage() { const votes = await voteService.getByParticipant(campaignId, participantId); // Combiner les propositions avec leurs votes - const propositionsWithVotes = propositionsData.map(proposition => ({ + let propositionsWithVotes = propositionsData.map(proposition => ({ ...proposition, vote: votes.find(vote => vote.proposition_id === proposition.id) })); + // Vérifier si l'ordre aléatoire est activé + const randomizePropositions = await settingsService.getBooleanValue('randomize_propositions', false); + + if (randomizePropositions) { + // Mélanger les propositions de manière aléatoire + propositionsWithVotes = propositionsWithVotes.sort(() => Math.random() - 0.5); + setIsRandomOrder(true); + } + setPropositions(propositionsWithVotes); // Initialiser les votes locaux avec les votes existants @@ -278,6 +288,14 @@ export default function PublicVotePage() {

Description

{campaign?.description}

+ {isRandomOrder && ( +
+

+ ℹ️ + Les propositions sont affichées dans un ordre aléatoire pour éviter les biais liés à l'ordre de présentation. +

+
+ )}
diff --git a/src/components/SendParticipantEmailModal.tsx b/src/components/SendParticipantEmailModal.tsx new file mode 100644 index 0000000..b0d76de --- /dev/null +++ b/src/components/SendParticipantEmailModal.tsx @@ -0,0 +1,229 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { Participant, Campaign } from '@/types'; +import { settingsService } from '@/lib/services'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Mail, Send, CheckCircle, XCircle } from 'lucide-react'; + +interface SendParticipantEmailModalProps { + isOpen: boolean; + onClose: () => void; + participant: Participant; + campaign: Campaign; +} + +export default function SendParticipantEmailModal({ + isOpen, + onClose, + participant, + campaign +}: SendParticipantEmailModalProps) { + const [subject, setSubject] = useState(''); + const [message, setMessage] = useState(''); + const [sending, setSending] = useState(false); + const [result, setResult] = useState<{ success: boolean; message: string } | null>(null); + + // Générer le lien de vote + const voteUrl = `${typeof window !== 'undefined' ? window.location.origin : ''}/campaigns/${campaign.id}/vote/${participant.id}`; + + // Initialiser le message par défaut quand le modal s'ouvre + useEffect(() => { + if (isOpen && campaign && participant) { + setSubject(`Votez pour la campagne "${campaign.title}"`); + setMessage(`Bonjour ${participant.first_name}, + +Vous êtes invité(e) à participer au vote pour la campagne "${campaign.title}". + +${campaign.description} + +Pour voter, cliquez sur le lien suivant : +${voteUrl} + +Vous disposez d'un budget de ${campaign.budget_per_user}€ à répartir entre les propositions selon vos préférences. + +Merci de votre participation ! + +Cordialement, +L'équipe Mes Budgets Participatifs`); + } + }, [isOpen, campaign, participant]); + + const handleSendEmail = async () => { + if (!subject.trim() || !message.trim()) { + setResult({ success: false, message: 'Veuillez remplir le sujet et le message' }); + return; + } + + try { + setSending(true); + setResult(null); + + // Récupérer les paramètres SMTP + const smtpSettings = await settingsService.getSmtpSettings(); + + if (!smtpSettings.host || !smtpSettings.username || !smtpSettings.password) { + setResult({ success: false, message: 'Configuration SMTP manquante. Veuillez configurer les paramètres email dans les paramètres.' }); + return; + } + + // Envoyer l'email via l'API + const response = await fetch('/api/send-participant-email', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + smtpSettings, + toEmail: participant.email, + toName: `${participant.first_name} ${participant.last_name}`, + subject: subject.trim(), + message: message.trim(), + campaignTitle: campaign.title, + voteUrl + }), + }); + + const result = await response.json(); + + if (result.success) { + setResult({ success: true, message: 'Email envoyé avec succès !' }); + // Vider les champs après succès + setTimeout(() => { + setSubject(''); + setMessage(''); + onClose(); + }, 2000); + } else { + setResult({ success: false, message: result.error || 'Erreur lors de l\'envoi de l\'email' }); + } + } catch (error) { + setResult({ success: false, message: 'Erreur inattendue lors de l\'envoi' }); + } finally { + setSending(false); + } + }; + + const handleClose = () => { + setSubject(''); + setMessage(''); + setResult(null); + onClose(); + }; + + return ( + + + + + + Envoyer un email à {participant.first_name} {participant.last_name} + + + Envoyez un email personnalisé à ce participant avec le lien de vote. + + + +
+ {/* Informations du participant */} +
+

+ Destinataire : +

+
+
Nom : {participant.first_name} {participant.last_name}
+
Email : {participant.email}
+
Campagne : {campaign.title}
+
+
+ + {/* Sujet */} +
+ + setSubject(e.target.value)} + disabled={sending} + /> +
+ + {/* Message */} +
+ +