diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..05f9b45 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Mes Budgets Participatifs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index bb43e14..50e536d 100644 --- a/README.md +++ b/README.md @@ -270,7 +270,7 @@ src/ ### Vercel (recommandé) #### Configuration automatique -1. Connectez votre repo GitHub à Vercel +1. Connectez votre repo Git à Vercel 2. Configurez les variables d'environnement dans Vercel 3. Déployez automatiquement @@ -391,7 +391,7 @@ Ce projet est sous licence MIT. Voir le fichier `LICENSE` pour plus de détails. Pour toute question ou problème : 1. Vérifiez la documentation Supabase -2. Consultez les issues GitHub +2. Consultez les issues Git 3. Créez une nouvelle issue si nécessaire --- diff --git a/docs/PROJECT-STRUCTURE.md b/docs/PROJECT-STRUCTURE.md index 4ed2ffd..ef07cbc 100644 --- a/docs/PROJECT-STRUCTURE.md +++ b/docs/PROJECT-STRUCTURE.md @@ -42,8 +42,7 @@ mes-budgets-participatifs/ ├── database/ │ └── supabase-schema.sql # Schéma de base de données ├── scripts/ -│ ├── test-security.js # Tests de sécurité -│ └── migrate-short-links.js # Migration des liens courts +│ └── test-security.js # Tests de sécurité └── docs/ # Documentation ``` @@ -149,21 +148,21 @@ Génère automatiquement un identifiant court unique pour les participants. - `create(vote)` - Crée un nouveau vote - `deleteByParticipant(campaignId, participantId)` - Supprime tous les votes d'un participant -## 🚀 Scripts de migration +## 🚀 Scripts utilitaires -### `scripts/migrate-short-links.js` -Script pour migrer les données existantes et générer les slugs et short_ids manquants. +### `scripts/test-security.js` +Script pour tester la sécurité de l'application et vérifier les politiques RLS. **Usage :** ```bash -node scripts/migrate-short-links.js +npm run test:security ``` **Fonctionnalités :** -- Génère automatiquement les slugs pour les campagnes existantes -- Génère automatiquement les short_ids pour les participants existants -- Gère les conflits et génère des identifiants uniques -- Affiche un rapport détaillé de la migration +- Vérifie que les tables existent et sont accessibles +- Teste les politiques RLS (Row Level Security) +- Valide les permissions d'accès +- Génère un rapport de sécurité détaillé ## 🔒 Sécurité diff --git a/scripts/apply-vote-function.sql b/scripts/apply-vote-function.sql deleted file mode 100644 index 6b94f3c..0000000 --- a/scripts/apply-vote-function.sql +++ /dev/null @@ -1,41 +0,0 @@ --- Script pour appliquer la fonction replace_participant_votes --- À exécuter dans votre base de données Supabase - --- Fonction pour remplacer tous les votes d'un participant de manière atomique -CREATE OR REPLACE FUNCTION replace_participant_votes( - p_campaign_id UUID, - p_participant_id UUID, - p_votes JSONB -) -RETURNS VOID AS $$ -DECLARE - vote_record RECORD; -BEGIN - -- Commencer une transaction - BEGIN - -- Supprimer tous les votes existants pour ce participant dans cette campagne - DELETE FROM votes - WHERE campaign_id = p_campaign_id - AND participant_id = p_participant_id; - - -- Insérer les nouveaux votes - FOR vote_record IN - SELECT * FROM jsonb_array_elements(p_votes) - LOOP - INSERT INTO votes (campaign_id, participant_id, proposition_id, amount) - VALUES ( - p_campaign_id, - p_participant_id, - (vote_record.value->>'proposition_id')::UUID, - (vote_record.value->>'amount')::INTEGER - ); - END LOOP; - - -- La transaction sera automatiquement commitée si tout va bien - EXCEPTION - WHEN OTHERS THEN - -- En cas d'erreur, la transaction sera automatiquement rollbackée - RAISE EXCEPTION 'Erreur lors du remplacement des votes: %', SQLERRM; - END; -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; diff --git a/scripts/check-database-state.sql b/scripts/check-database-state.sql deleted file mode 100644 index 9e541af..0000000 --- a/scripts/check-database-state.sql +++ /dev/null @@ -1,329 +0,0 @@ --- Script de vérification de l'état de la base de données --- À exécuter AVANT la migration pour diagnostiquer l'état actuel - --- ======================================== --- VÉRIFICATION DES COLONNES --- ======================================== - --- Vérifier si les colonnes de liens courts existent -SELECT - 'campaigns.slug' as column_name, - CASE - WHEN EXISTS ( - SELECT 1 FROM information_schema.columns - WHERE table_name = 'campaigns' AND column_name = 'slug' - ) THEN '✅ Existe' - ELSE '❌ Manquante' - END as status -UNION ALL -SELECT - 'participants.short_id' as column_name, - CASE - WHEN EXISTS ( - SELECT 1 FROM information_schema.columns - WHERE table_name = 'participants' AND column_name = 'short_id' - ) THEN '✅ Existe' - ELSE '❌ Manquante' - END as status; - --- ======================================== --- VÉRIFICATION DES FONCTIONS --- ======================================== - --- Vérifier si les fonctions utilitaires existent -SELECT - 'generate_slug' as function_name, - CASE - WHEN EXISTS ( - SELECT 1 FROM pg_proc p - JOIN pg_namespace n ON p.pronamespace = n.oid - WHERE p.proname = 'generate_slug' AND n.nspname = 'public' - ) THEN '✅ Existe' - ELSE '❌ Manquante' - END as status -UNION ALL -SELECT - 'generate_short_id' as function_name, - CASE - WHEN EXISTS ( - SELECT 1 FROM pg_proc p - JOIN pg_namespace n ON p.pronamespace = n.oid - WHERE p.proname = 'generate_short_id' AND n.nspname = 'public' - ) THEN '✅ Existe' - ELSE '❌ Manquante' - END as status -UNION ALL -SELECT - 'replace_participant_votes' as function_name, - CASE - WHEN EXISTS ( - SELECT 1 FROM pg_proc p - JOIN pg_namespace n ON p.pronamespace = n.oid - WHERE p.proname = 'replace_participant_votes' AND n.nspname = 'public' - ) THEN '✅ Existe' - ELSE '❌ Manquante' - END as status; - --- ======================================== --- VÉRIFICATION DES INDEX --- ======================================== - --- Vérifier si les index de performance existent -SELECT - 'idx_campaigns_slug' as index_name, - CASE - WHEN EXISTS ( - SELECT 1 FROM pg_indexes - WHERE indexname = 'idx_campaigns_slug' - ) THEN '✅ Existe' - ELSE '❌ Manquant' - END as status -UNION ALL -SELECT - 'idx_participants_short_id' as index_name, - CASE - WHEN EXISTS ( - SELECT 1 FROM pg_indexes - WHERE indexname = 'idx_participants_short_id' - ) THEN '✅ Existe' - ELSE '❌ Manquant' - END as status; - --- ======================================== --- ANALYSE DES DONNÉES EXISTANTES --- ======================================== - --- Compter les campagnes et leur état (version sécurisée) -DO $$ -DECLARE - campaigns_total INTEGER; - campaigns_with_slug INTEGER := 0; - participants_total INTEGER; - participants_with_short_id INTEGER := 0; - slug_exists BOOLEAN; - short_id_exists BOOLEAN; -BEGIN - -- Vérifier si les colonnes existent - SELECT EXISTS ( - SELECT 1 FROM information_schema.columns - WHERE table_name = 'campaigns' AND column_name = 'slug' - ) INTO slug_exists; - - SELECT EXISTS ( - SELECT 1 FROM information_schema.columns - WHERE table_name = 'participants' AND column_name = 'short_id' - ) INTO short_id_exists; - - -- Compter les campagnes - SELECT COUNT(*) INTO campaigns_total FROM campaigns; - - -- Compter les participants - SELECT COUNT(*) INTO participants_total FROM participants; - - -- Compter les campagnes avec slug si la colonne existe - IF slug_exists THEN - SELECT COUNT(*) INTO campaigns_with_slug FROM campaigns WHERE slug IS NOT NULL; - END IF; - - -- Compter les participants avec short_id si la colonne existe - IF short_id_exists THEN - SELECT COUNT(*) INTO participants_with_short_id FROM participants WHERE short_id IS NOT NULL; - END IF; - - -- Afficher les résultats - RAISE NOTICE '=== ANALYSE DES DONNÉES ==='; - RAISE NOTICE 'Campagnes totales: %', campaigns_total; - IF slug_exists THEN - RAISE NOTICE 'Campagnes avec slug: %', campaigns_with_slug; - RAISE NOTICE 'Campagnes sans slug: %', campaigns_total - campaigns_with_slug; - ELSE - RAISE NOTICE 'Colonne slug: ❌ N''existe pas encore'; - END IF; - - RAISE NOTICE 'Participants totaux: %', participants_total; - IF short_id_exists THEN - RAISE NOTICE 'Participants avec short_id: %', participants_with_short_id; - RAISE NOTICE 'Participants sans short_id: %', participants_total - participants_with_short_id; - ELSE - RAISE NOTICE 'Colonne short_id: ❌ N''existe pas encore'; - END IF; -END $$; - --- ======================================== --- EXEMPLES DE DONNÉES EXISTANTES --- ======================================== - --- Afficher quelques exemples de campagnes (version sécurisée) -DO $$ -DECLARE - slug_exists BOOLEAN; - r RECORD; -BEGIN - -- Vérifier si la colonne slug existe - SELECT EXISTS ( - SELECT 1 FROM information_schema.columns - WHERE table_name = 'campaigns' AND column_name = 'slug' - ) INTO slug_exists; - - IF slug_exists THEN - RAISE NOTICE '=== EXEMPLES DE CAMPAGNES ==='; - FOR r IN - SELECT - id, - title, - slug, - CASE - WHEN slug IS NULL THEN '❌ Besoin de migration' - ELSE '✅ OK' - END as status - FROM campaigns - ORDER BY created_at DESC - LIMIT 5 - LOOP - RAISE NOTICE 'ID: %, Titre: %, Slug: %, Status: %', r.id, r.title, r.slug, r.status; - END LOOP; - ELSE - RAISE NOTICE '=== EXEMPLES DE CAMPAGNES ==='; - FOR r IN - SELECT - id, - title - FROM campaigns - ORDER BY created_at DESC - LIMIT 5 - LOOP - RAISE NOTICE 'ID: %, Titre: %, Slug: ❌ Colonne inexistante', r.id, r.title; - END LOOP; - END IF; -END $$; - --- Afficher quelques exemples de participants (version sécurisée) -DO $$ -DECLARE - short_id_exists BOOLEAN; - r RECORD; -BEGIN - -- Vérifier si la colonne short_id existe - SELECT EXISTS ( - SELECT 1 FROM information_schema.columns - WHERE table_name = 'participants' AND column_name = 'short_id' - ) INTO short_id_exists; - - IF short_id_exists THEN - RAISE NOTICE '=== EXEMPLES DE PARTICIPANTS ==='; - FOR r IN - SELECT - id, - first_name, - last_name, - short_id, - CASE - WHEN short_id IS NULL THEN '❌ Besoin de migration' - ELSE '✅ OK' - END as status - FROM participants - ORDER BY created_at DESC - LIMIT 5 - LOOP - RAISE NOTICE 'ID: %, Nom: % %, Short ID: %, Status: %', r.id, r.first_name, r.last_name, r.short_id, r.status; - END LOOP; - ELSE - RAISE NOTICE '=== EXEMPLES DE PARTICIPANTS ==='; - FOR r IN - SELECT - id, - first_name, - last_name - FROM participants - ORDER BY created_at DESC - LIMIT 5 - LOOP - RAISE NOTICE 'ID: %, Nom: % %, Short ID: ❌ Colonne inexistante', r.id, r.first_name, r.last_name; - END LOOP; - END IF; -END $$; - --- ======================================== --- RECOMMANDATIONS --- ======================================== - -DO $$ -DECLARE - missing_slug_count INTEGER := 0; - missing_short_id_count INTEGER := 0; - missing_functions INTEGER; - missing_indexes INTEGER; - slug_exists BOOLEAN; - short_id_exists BOOLEAN; -BEGIN - -- Vérifier si les colonnes existent - SELECT EXISTS ( - SELECT 1 FROM information_schema.columns - WHERE table_name = 'campaigns' AND column_name = 'slug' - ) INTO slug_exists; - - SELECT EXISTS ( - SELECT 1 FROM information_schema.columns - WHERE table_name = 'participants' AND column_name = 'short_id' - ) INTO short_id_exists; - - -- Compter les éléments manquants seulement si les colonnes existent - IF slug_exists THEN - SELECT COUNT(*) INTO missing_slug_count FROM campaigns WHERE slug IS NULL; - ELSE - SELECT COUNT(*) INTO missing_slug_count FROM campaigns; - END IF; - - IF short_id_exists THEN - SELECT COUNT(*) INTO missing_short_id_count FROM participants WHERE short_id IS NULL; - ELSE - SELECT COUNT(*) INTO missing_short_id_count FROM participants; - END IF; - - SELECT COUNT(*) INTO missing_functions - FROM ( - SELECT 'generate_slug' as func UNION ALL SELECT 'generate_short_id' UNION ALL SELECT 'replace_participant_votes' - ) f - WHERE NOT EXISTS ( - SELECT 1 FROM pg_proc p - JOIN pg_namespace n ON p.pronamespace = n.oid - WHERE p.proname = f.func AND n.nspname = 'public' - ); - - SELECT COUNT(*) INTO missing_indexes - FROM ( - SELECT 'idx_campaigns_slug' as idx UNION ALL SELECT 'idx_participants_short_id' - ) i - WHERE NOT EXISTS ( - SELECT 1 FROM pg_indexes WHERE indexname = i.idx - ); - - RAISE NOTICE '=== RECOMMANDATIONS ==='; - - IF missing_slug_count > 0 OR missing_short_id_count > 0 OR missing_functions > 0 OR missing_indexes > 0 THEN - RAISE NOTICE '🔄 Migration nécessaire !'; - IF missing_slug_count > 0 THEN - IF slug_exists THEN - RAISE NOTICE ' - % campagnes ont besoin d''un slug', missing_slug_count; - ELSE - RAISE NOTICE ' - % campagnes ont besoin de la colonne slug + génération', missing_slug_count; - END IF; - END IF; - IF missing_short_id_count > 0 THEN - IF short_id_exists THEN - RAISE NOTICE ' - % participants ont besoin d''un short_id', missing_short_id_count; - ELSE - RAISE NOTICE ' - % participants ont besoin de la colonne short_id + génération', missing_short_id_count; - END IF; - END IF; - IF missing_functions > 0 THEN - RAISE NOTICE ' - % fonctions utilitaires manquantes', missing_functions; - END IF; - IF missing_indexes > 0 THEN - RAISE NOTICE ' - % index de performance manquants', missing_indexes; - END IF; - RAISE NOTICE ' → Exécutez le script migration-to-latest-schema.sql'; - ELSE - RAISE NOTICE '✅ Base de données à jour ! Aucune migration nécessaire.'; - END IF; -END $$; diff --git a/scripts/migrate-short-links.js b/scripts/migrate-short-links.js deleted file mode 100644 index 468b10b..0000000 --- a/scripts/migrate-short-links.js +++ /dev/null @@ -1,196 +0,0 @@ -#!/usr/bin/env node - -/** - * Script de migration pour générer les slugs et short_ids pour les données existantes - * - * Usage: node scripts/migrate-short-links.js - */ - -const { createClient } = require('@supabase/supabase-js'); -require('dotenv').config({ path: '.env.local' }); - -const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; -const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY; - -if (!supabaseUrl || !supabaseServiceKey) { - console.error('❌ Variables d\'environnement manquantes'); - console.error('NEXT_PUBLIC_SUPABASE_URL et SUPABASE_SERVICE_ROLE_KEY sont requis'); - process.exit(1); -} - -const supabase = createClient(supabaseUrl, supabaseServiceKey); - -async function generateSlug(title) { - // Convertir en minuscules et remplacer les caractères spéciaux - let slug = title.toLowerCase() - .replace(/[^a-z0-9\s]/g, '') - .replace(/\s+/g, '-') - .trim(); - - // Si le slug est vide, utiliser 'campagne' - if (!slug) { - slug = 'campagne'; - } - - // Vérifier si le slug existe déjà et ajouter un numéro si nécessaire - let counter = 0; - let finalSlug = slug; - - while (true) { - const { data, error } = await supabase - .from('campaigns') - .select('id') - .eq('slug', finalSlug) - .single(); - - if (error && error.code === 'PGRST116') { - // Aucune campagne trouvée avec ce slug, on peut l'utiliser - break; - } - - if (error) { - throw error; - } - - // Le slug existe déjà, ajouter un numéro - counter++; - finalSlug = `${slug}-${counter}`; - } - - return finalSlug; -} - -async function generateShortId() { - const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; - let counter = 0; - - while (counter < 100) { - // Générer un identifiant de 6 caractères - let result = ''; - for (let i = 0; i < 6; i++) { - result += chars.charAt(Math.floor(Math.random() * chars.length)); - } - - // Vérifier si le short_id existe déjà - const { data, error } = await supabase - .from('participants') - .select('id') - .eq('short_id', result) - .single(); - - if (error && error.code === 'PGRST116') { - // Aucun participant trouvé avec ce short_id, on peut l'utiliser - return result; - } - - if (error) { - throw error; - } - - counter++; - } - - throw new Error('Impossible de générer un short_id unique après 100 tentatives'); -} - -async function migrateCampaigns() { - console.log('🔄 Migration des campagnes...'); - - // Récupérer toutes les campagnes sans slug - const { data: campaigns, error } = await supabase - .from('campaigns') - .select('id, title') - .is('slug', null); - - if (error) { - console.error('❌ Erreur lors de la récupération des campagnes:', error); - return; - } - - console.log(`📋 ${campaigns.length} campagnes à migrer`); - - for (const campaign of campaigns) { - try { - const slug = await generateSlug(campaign.title); - - const { error: updateError } = await supabase - .from('campaigns') - .update({ slug }) - .eq('id', campaign.id); - - if (updateError) { - console.error(`❌ Erreur lors de la mise à jour de la campagne ${campaign.id}:`, updateError); - } else { - console.log(`✅ Campagne "${campaign.title}" -> slug: ${slug}`); - } - } catch (error) { - console.error(`❌ Erreur lors de la génération du slug pour "${campaign.title}":`, error); - } - } - - console.log('✅ Migration des campagnes terminée'); -} - -async function migrateParticipants() { - console.log('🔄 Migration des participants...'); - - // Récupérer tous les participants sans short_id - const { data: participants, error } = await supabase - .from('participants') - .select('id, first_name, last_name') - .is('short_id', null); - - if (error) { - console.error('❌ Erreur lors de la récupération des participants:', error); - return; - } - - console.log(`📋 ${participants.length} participants à migrer`); - - for (const participant of participants) { - try { - const shortId = await generateShortId(); - - const { error: updateError } = await supabase - .from('participants') - .update({ short_id: shortId }) - .eq('id', participant.id); - - if (updateError) { - console.error(`❌ Erreur lors de la mise à jour du participant ${participant.id}:`, updateError); - } else { - console.log(`✅ Participant "${participant.first_name} ${participant.last_name}" -> short_id: ${shortId}`); - } - } catch (error) { - console.error(`❌ Erreur lors de la génération du short_id pour "${participant.first_name} ${participant.last_name}":`, error); - } - } - - console.log('✅ Migration des participants terminée'); -} - -async function main() { - console.log('🚀 Début de la migration des liens courts...\n'); - - try { - await migrateCampaigns(); - console.log(''); - await migrateParticipants(); - - console.log('\n🎉 Migration terminée avec succès !'); - console.log('\n📝 Résumé des nouvelles routes :'); - console.log('- Dépôt de propositions : /p/[slug]'); - console.log('- Vote : /v/[shortId]'); - console.log('- Les anciennes routes restent fonctionnelles pour la compatibilité'); - - } catch (error) { - console.error('❌ Erreur lors de la migration:', error); - process.exit(1); - } -} - -if (require.main === module) { - main(); -} - -module.exports = { migrateCampaigns, migrateParticipants }; diff --git a/scripts/migration-to-latest-schema.sql b/scripts/migration-to-latest-schema.sql deleted file mode 100644 index 34de7b6..0000000 --- a/scripts/migration-to-latest-schema.sql +++ /dev/null @@ -1,223 +0,0 @@ --- Script de migration vers le schéma le plus récent avec liens courts --- À exécuter dans votre base de données Supabase - --- ======================================== --- ÉTAPE 1: Ajout des colonnes manquantes --- ======================================== - --- Ajouter la colonne slug aux campagnes si elle n'existe pas -DO $$ -BEGIN - IF NOT EXISTS ( - SELECT 1 FROM information_schema.columns - WHERE table_name = 'campaigns' AND column_name = 'slug' - ) THEN - ALTER TABLE campaigns ADD COLUMN slug TEXT UNIQUE; - RAISE NOTICE 'Colonne slug ajoutée à la table campaigns'; - ELSE - RAISE NOTICE 'Colonne slug existe déjà dans la table campaigns'; - END IF; -END $$; - --- Ajouter la colonne short_id aux participants si elle n'existe pas -DO $$ -BEGIN - IF NOT EXISTS ( - SELECT 1 FROM information_schema.columns - WHERE table_name = 'participants' AND column_name = 'short_id' - ) THEN - ALTER TABLE participants ADD COLUMN short_id TEXT UNIQUE; - RAISE NOTICE 'Colonne short_id ajoutée à la table participants'; - ELSE - RAISE NOTICE 'Colonne short_id existe déjà dans la table participants'; - END IF; -END $$; - --- ======================================== --- ÉTAPE 2: Création des fonctions utilitaires --- ======================================== - --- Fonction pour générer un slug à partir d'un titre -CREATE OR REPLACE FUNCTION generate_slug(title TEXT) -RETURNS TEXT AS $$ -DECLARE - generated_slug TEXT; - counter INTEGER := 0; - base_slug TEXT; -BEGIN - -- Convertir en minuscules et remplacer les caractères spéciaux - base_slug := lower(regexp_replace(title, '[^a-zA-Z0-9\s]', '', 'g')); - base_slug := regexp_replace(base_slug, '\s+', '-', 'g'); - base_slug := trim(both '-' from base_slug); - - -- Si le slug est vide, utiliser 'campagne' - IF base_slug = '' THEN - base_slug := 'campagne'; - END IF; - - generated_slug := base_slug; - - -- Vérifier si le slug existe déjà et ajouter un numéro si nécessaire - WHILE EXISTS (SELECT 1 FROM campaigns WHERE campaigns.slug = generated_slug) LOOP - counter := counter + 1; - generated_slug := base_slug || '-' || counter; - END LOOP; - - RETURN generated_slug; -END; -$$ LANGUAGE plpgsql; - --- Fonction pour générer un short_id unique -CREATE OR REPLACE FUNCTION generate_short_id() -RETURNS TEXT AS $$ -DECLARE - chars TEXT := 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; - result TEXT := ''; - i INTEGER; - generated_short_id TEXT; - counter INTEGER := 0; -BEGIN - LOOP - -- Générer un identifiant de 6 caractères - result := ''; - FOR i IN 1..6 LOOP - result := result || substr(chars, floor(random() * length(chars))::integer + 1, 1); - END LOOP; - - generated_short_id := result; - - -- Vérifier si le short_id existe déjà - IF NOT EXISTS (SELECT 1 FROM participants WHERE participants.short_id = generated_short_id) THEN - RETURN generated_short_id; - END IF; - - -- Éviter les boucles infinies - counter := counter + 1; - IF counter > 100 THEN - RAISE EXCEPTION 'Impossible de générer un short_id unique après 100 tentatives'; - END IF; - END LOOP; -END; -$$ LANGUAGE plpgsql; - --- ======================================== --- ÉTAPE 3: Mise à jour des données existantes --- ======================================== - --- Générer des slugs pour les campagnes qui n'en ont pas -UPDATE campaigns -SET slug = generate_slug(title) -WHERE slug IS NULL; - --- Générer des short_ids pour les participants qui n'en ont pas -UPDATE participants -SET short_id = generate_short_id() -WHERE short_id IS NULL; - --- ======================================== --- ÉTAPE 4: Création des index manquants --- ======================================== - --- Index pour les slugs de campagnes -DO $$ -BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_indexes - WHERE indexname = 'idx_campaigns_slug' - ) THEN - CREATE INDEX idx_campaigns_slug ON campaigns(slug); - RAISE NOTICE 'Index idx_campaigns_slug créé'; - ELSE - RAISE NOTICE 'Index idx_campaigns_slug existe déjà'; - END IF; -END $$; - --- Index pour les short_ids de participants -DO $$ -BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_indexes - WHERE indexname = 'idx_participants_short_id' - ) THEN - CREATE INDEX idx_participants_short_id ON participants(short_id); - RAISE NOTICE 'Index idx_participants_short_id créé'; - ELSE - RAISE NOTICE 'Index idx_participants_short_id existe déjà'; - END IF; -END $$; - --- ======================================== --- ÉTAPE 5: Fonction pour remplacer les votes --- ======================================== - --- Fonction pour remplacer tous les votes d'un participant de manière atomique -CREATE OR REPLACE FUNCTION replace_participant_votes( - p_campaign_id UUID, - p_participant_id UUID, - p_votes JSONB -) -RETURNS VOID AS $$ -DECLARE - vote_record RECORD; -BEGIN - -- Commencer une transaction - BEGIN - -- Supprimer tous les votes existants pour ce participant dans cette campagne - DELETE FROM votes - WHERE campaign_id = p_campaign_id - AND participant_id = p_participant_id; - - -- Insérer les nouveaux votes - FOR vote_record IN - SELECT * FROM jsonb_array_elements(p_votes) - LOOP - INSERT INTO votes (campaign_id, participant_id, proposition_id, amount) - VALUES ( - p_campaign_id, - p_participant_id, - (vote_record.value->>'proposition_id')::UUID, - (vote_record.value->>'amount')::INTEGER - ); - END LOOP; - - -- La transaction sera automatiquement commitée si tout va bien - EXCEPTION - WHEN OTHERS THEN - -- En cas d'erreur, la transaction sera automatiquement rollbackée - RAISE EXCEPTION 'Erreur lors du remplacement des votes: %', SQLERRM; - END; -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - --- ======================================== --- ÉTAPE 6: Vérification et rapport --- ======================================== - --- Afficher un rapport de la migration -DO $$ -DECLARE - campaign_count INTEGER; - participant_count INTEGER; - campaign_with_slug INTEGER; - participant_with_short_id INTEGER; -BEGIN - -- Compter les campagnes - SELECT COUNT(*) INTO campaign_count FROM campaigns; - SELECT COUNT(*) INTO campaign_with_slug FROM campaigns WHERE slug IS NOT NULL; - - -- Compter les participants - SELECT COUNT(*) INTO participant_count FROM participants; - SELECT COUNT(*) INTO participant_with_short_id FROM participants WHERE short_id IS NOT NULL; - - RAISE NOTICE '=== RAPPORT DE MIGRATION ==='; - RAISE NOTICE 'Campagnes totales: %', campaign_count; - RAISE NOTICE 'Campagnes avec slug: %', campaign_with_slug; - RAISE NOTICE 'Participants totaux: %', participant_count; - RAISE NOTICE 'Participants avec short_id: %', participant_with_short_id; - - IF campaign_count = campaign_with_slug AND participant_count = participant_with_short_id THEN - RAISE NOTICE '✅ Migration réussie ! Toutes les données ont été migrées.'; - ELSE - RAISE NOTICE '⚠️ Attention: Certaines données n''ont pas été migrées.'; - END IF; -END $$;