clean
rajout licence
This commit is contained in:
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -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.
|
||||||
@@ -270,7 +270,7 @@ src/
|
|||||||
### Vercel (recommandé)
|
### Vercel (recommandé)
|
||||||
|
|
||||||
#### Configuration automatique
|
#### Configuration automatique
|
||||||
1. Connectez votre repo GitHub à Vercel
|
1. Connectez votre repo Git à Vercel
|
||||||
2. Configurez les variables d'environnement dans Vercel
|
2. Configurez les variables d'environnement dans Vercel
|
||||||
3. Déployez automatiquement
|
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 :
|
Pour toute question ou problème :
|
||||||
1. Vérifiez la documentation Supabase
|
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
|
3. Créez une nouvelle issue si nécessaire
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -42,8 +42,7 @@ mes-budgets-participatifs/
|
|||||||
├── database/
|
├── database/
|
||||||
│ └── supabase-schema.sql # Schéma de base de données
|
│ └── supabase-schema.sql # Schéma de base de données
|
||||||
├── scripts/
|
├── scripts/
|
||||||
│ ├── test-security.js # Tests de sécurité
|
│ └── test-security.js # Tests de sécurité
|
||||||
│ └── migrate-short-links.js # Migration des liens courts
|
|
||||||
└── docs/ # Documentation
|
└── docs/ # Documentation
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -149,21 +148,21 @@ Génère automatiquement un identifiant court unique pour les participants.
|
|||||||
- `create(vote)` - Crée un nouveau vote
|
- `create(vote)` - Crée un nouveau vote
|
||||||
- `deleteByParticipant(campaignId, participantId)` - Supprime tous les votes d'un participant
|
- `deleteByParticipant(campaignId, participantId)` - Supprime tous les votes d'un participant
|
||||||
|
|
||||||
## 🚀 Scripts de migration
|
## 🚀 Scripts utilitaires
|
||||||
|
|
||||||
### `scripts/migrate-short-links.js`
|
### `scripts/test-security.js`
|
||||||
Script pour migrer les données existantes et générer les slugs et short_ids manquants.
|
Script pour tester la sécurité de l'application et vérifier les politiques RLS.
|
||||||
|
|
||||||
**Usage :**
|
**Usage :**
|
||||||
```bash
|
```bash
|
||||||
node scripts/migrate-short-links.js
|
npm run test:security
|
||||||
```
|
```
|
||||||
|
|
||||||
**Fonctionnalités :**
|
**Fonctionnalités :**
|
||||||
- Génère automatiquement les slugs pour les campagnes existantes
|
- Vérifie que les tables existent et sont accessibles
|
||||||
- Génère automatiquement les short_ids pour les participants existants
|
- Teste les politiques RLS (Row Level Security)
|
||||||
- Gère les conflits et génère des identifiants uniques
|
- Valide les permissions d'accès
|
||||||
- Affiche un rapport détaillé de la migration
|
- Génère un rapport de sécurité détaillé
|
||||||
|
|
||||||
## 🔒 Sécurité
|
## 🔒 Sécurité
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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 $$;
|
|
||||||
@@ -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 };
|
|
||||||
@@ -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 $$;
|
|
||||||
Reference in New Issue
Block a user