diff --git a/README.md b/README.md index 08a53c1..bd48a0b 100644 --- a/README.md +++ b/README.md @@ -58,10 +58,24 @@ Ouvre ton navigateur sur : http://localhost:8081 ``` -**Email** : `admin@ptits-pas.fr` -**Mot de passe** : `admin123` +Par défaut un administrateur est créé par la migration d'initialisation (`migrations/01_init.sql`). +Pour des raisons de sécurité, le mot de passe n'est pas gardé en clair dans le README. -**Mot de passse pour se connecter au server local** : `admin123` +Si tu veux un mot de passe connu en dev, génère le hash bcrypt puis modifie la migration +ou crée un seed SQL : + +```sql +-- Exemple pour définir le mot de passe en dev : +UPDATE utilisateurs +SET password = crypt('admin123', gen_salt('bf')) +WHERE email = 'admin@ptits-pas.fr'; +``` + +Exécute la commande suivante pour appliquer ce seed sur ta base dev : + +```bash +docker exec -i ptitspas-postgres-standalone psql -U admin -d ptitpas_db -c "UPDATE utilisateurs SET password = crypt('admin123', gen_salt('bf')) WHERE email = 'admin@ptits-pas.fr';" +``` ## Conseils et bonnes pratiques @@ -71,6 +85,30 @@ http://localhost:8081 - Pour ajouter des scripts d'automatisation, crée un dossier `scripts/` - Documente les étapes spécifiques dans le README ou dans `docs/` +### Synchroniser les types ENUM avec les CSV d'import + +Un utilitaire est fourni pour générer une migration sûre qui ajoute les valeurs +manquantes aux types ENUM en base en se basant sur les CSV présents dans +`bdd/data_test/`. + +Générer la migration : + +```bash +make sync-enums +``` + +Le fichier généré est `migrations/00_sync_enums.sql`. Relis ce fichier avant de +l'appliquer (il contient des blocs `DO $$ ... ALTER TYPE ... ADD VALUE` qui +ne suppriment rien mais modifient le type ENUM). Pour appliquer la migration +sur ta base locale : + +```bash +# Après avoir démarré la base (make reset ou make demo) +docker exec -i ptitspas-postgres-standalone psql -U admin -d ptitpas_db -f migrations/00_sync_enums.sql +``` + +Attention : ne pas appliquer directement en production sans vérification. + --- ## Contact diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index cdee3e2..3f5b8ff 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -14,7 +14,8 @@ services: ports: - "5433:5432" volumes: - # Scripts de migration (ordre important → init, indexes, checks, triggers, import…) + # Scripts de migration (ordre important → sync_enums, init, indexes, checks, triggers, import…) + - ./migrations/00_sync_enums.sql:/docker-entrypoint-initdb.d/00_sync_enums.sql - ./migrations/01_init.sql:/docker-entrypoint-initdb.d/01_init.sql - ./migrations/02_indexes.sql:/docker-entrypoint-initdb.d/02_indexes.sql - ./migrations/03_checks.sql:/docker-entrypoint-initdb.d/03_checks.sql diff --git a/makefile b/makefile index 48f7ba3..a3f575b 100644 --- a/makefile +++ b/makefile @@ -69,4 +69,10 @@ psql: stop: docker compose -f docker-compose.dev.yml down -j \ No newline at end of file +stop: + docker compose -f docker-compose.dev.yml down + +sync-enums: + @echo "🔁 Génération de migrations/00_sync_enums.sql depuis les CSV" + @python3 scripts/sync_enums.py > migrations/00_sync_enums.sql + @echo "✅ migrations/00_sync_enums.sql générée. Relis le fichier avant de l'appliquer." \ No newline at end of file diff --git a/migrations/00_sync_enums.sql b/migrations/00_sync_enums.sql new file mode 100644 index 0000000..c0cf18a --- /dev/null +++ b/migrations/00_sync_enums.sql @@ -0,0 +1,183 @@ +-- Generated by scripts/sync_enums.py — review before applying + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_enum e + JOIN pg_type t ON e.enumtypid = t.oid + WHERE t.typname = 'statut_contrat_type' + AND e.enumlabel = 'brouillon' + ) THEN + ALTER TYPE statut_contrat_type ADD VALUE 'brouillon'; + END IF; +END$$; + + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_enum e + JOIN pg_type t ON e.enumtypid = t.oid + WHERE t.typname = 'statut_dossier_type' + AND e.enumlabel = 'envoye' + ) THEN + ALTER TYPE statut_dossier_type ADD VALUE 'envoye'; + END IF; +END$$; + + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_enum e + JOIN pg_type t ON e.enumtypid = t.oid + WHERE t.typname = 'statut_enfant_type' + AND e.enumlabel = 'actif' + ) THEN + ALTER TYPE statut_enfant_type ADD VALUE 'actif'; + END IF; +END$$; + + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_enum e + JOIN pg_type t ON e.enumtypid = t.oid + WHERE t.typname = 'statut_enfant_type' + AND e.enumlabel = 'scolarise' + ) THEN + ALTER TYPE statut_enfant_type ADD VALUE 'scolarise'; + END IF; +END$$; + + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_enum e + JOIN pg_type t ON e.enumtypid = t.oid + WHERE t.typname = 'genre_type' + AND e.enumlabel = 'F' + ) THEN + ALTER TYPE genre_type ADD VALUE 'F'; + END IF; +END$$; + + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_enum e + JOIN pg_type t ON e.enumtypid = t.oid + WHERE t.typname = 'genre_type' + AND e.enumlabel = 'H' + ) THEN + ALTER TYPE genre_type ADD VALUE 'H'; + END IF; +END$$; + + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_enum e + JOIN pg_type t ON e.enumtypid = t.oid + WHERE t.typname = 'type_evenement_type' + AND e.enumlabel = 'absence_enfant' + ) THEN + ALTER TYPE type_evenement_type ADD VALUE 'absence_enfant'; + END IF; +END$$; + + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_enum e + JOIN pg_type t ON e.enumtypid = t.oid + WHERE t.typname = 'statut_evenement_type' + AND e.enumlabel = 'propose' + ) THEN + ALTER TYPE statut_evenement_type ADD VALUE 'propose'; + END IF; +END$$; + + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_enum e + JOIN pg_type t ON e.enumtypid = t.oid + WHERE t.typname = 'role_type' + AND e.enumlabel = 'administrateur' + ) THEN + ALTER TYPE role_type ADD VALUE 'administrateur'; + END IF; +END$$; + + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_enum e + JOIN pg_type t ON e.enumtypid = t.oid + WHERE t.typname = 'role_type' + AND e.enumlabel = 'assistante_maternelle' + ) THEN + ALTER TYPE role_type ADD VALUE 'assistante_maternelle'; + END IF; +END$$; + + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_enum e + JOIN pg_type t ON e.enumtypid = t.oid + WHERE t.typname = 'role_type' + AND e.enumlabel = 'gestionnaire' + ) THEN + ALTER TYPE role_type ADD VALUE 'gestionnaire'; + END IF; +END$$; + + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_enum e + JOIN pg_type t ON e.enumtypid = t.oid + WHERE t.typname = 'role_type' + AND e.enumlabel = 'parent' + ) THEN + ALTER TYPE role_type ADD VALUE 'parent'; + END IF; +END$$; + + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_enum e + JOIN pg_type t ON e.enumtypid = t.oid + WHERE t.typname = 'statut_utilisateur_type' + AND e.enumlabel = 'actif' + ) THEN + ALTER TYPE statut_utilisateur_type ADD VALUE 'actif'; + END IF; +END$$; + + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_enum e + JOIN pg_type t ON e.enumtypid = t.oid + WHERE t.typname = 'statut_validation_type' + AND e.enumlabel = 'valide' + ) THEN + ALTER TYPE statut_validation_type ADD VALUE 'valide'; + END IF; +END$$; + diff --git a/migrations/01_init.sql b/migrations/01_init.sql index e595feb..3eec9f7 100644 --- a/migrations/01_init.sql +++ b/migrations/01_init.sql @@ -262,7 +262,10 @@ INSERT INTO utilisateurs ( VALUES ( gen_random_uuid(), 'admin@ptits-pas.fr', - 'admin123', -- ⚠️ à remplacer par le hash réel du mot de passe + -- Use a bcrypt hash via pgcrypto for the default admin password in dev. + -- In dev you can keep a known password by using: crypt('admin123', gen_salt('bf')) + -- For security, avoid committing plaintext passwords in migrations for production. + crypt('admin123', gen_salt('bf')), 'Admin', 'PtitsPas', 'super_admin', diff --git a/scripts/sync_enums.py b/scripts/sync_enums.py new file mode 100644 index 0000000..cc94d59 --- /dev/null +++ b/scripts/sync_enums.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +""" +scripts/sync_enums.py + +Génère une migration SQL non destructive pour ajouter les valeurs d'ENUM +manquantes en se basant sur les CSV de `bdd/data_test`. + +Usage: + python3 scripts/sync_enums.py > migrations/00_sync_enums.sql + +Relire le SQL généré avant application en production. +""" +from pathlib import Path +import csv +from collections import defaultdict +import sys + +# Mapping colonne CSV -> nom du type enum en base +# NOTE: mapping is now based on filename (more precise) +CSV_DIR = Path('bdd/data_test') + +# Define per-file mappings: filename stem -> {column_name: enum_type} +FILE_COLUMN_ENUM_MAP = { + 'utilisateurs': { + 'role': 'role_type', + 'genre': 'genre_type', + 'statut': 'statut_utilisateur_type', + }, + 'enfants': { + 'statut': 'statut_enfant_type', + 'genre': 'genre_type', + }, + 'contrats': { + 'statut': 'statut_contrat_type', + }, + 'dossiers': { + 'statut': 'statut_dossier_type', + }, + 'evenements': { + 'type': 'type_evenement_type', + 'statut': 'statut_evenement_type', + }, + 'validations': { + 'statut': 'statut_validation_type', + } +} + +def quote_sql_literal(s: str) -> str: + return "'" + s.replace("'", "''") + "'" + +def discover_values(): + # heuristique : map common column names to enums + enum_values = defaultdict(set) + for p in sorted(CSV_DIR.glob('*.csv')): + try: + with p.open(newline='', encoding='utf-8') as fh: + reader = csv.DictReader(fh) + fname = p.stem + per_file_map = FILE_COLUMN_ENUM_MAP.get(fname, {}) + for row in reader: + for col, enum in per_file_map.items(): + if col in row and row[col] is not None: + v = row[col].strip() + if v != '': + enum_values[enum].add(v) + except Exception as e: + print(f"-- Error reading {p}: {e}", file=sys.stderr) + return enum_values + +def emit_sql(enum_values): + lines = [] + lines.append('-- Generated by scripts/sync_enums.py — review before applying') + for enum_type, vals in enum_values.items(): + if not vals: + continue + for v in sorted(vals): + lit = quote_sql_literal(v) + block = f""" +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_enum e + JOIN pg_type t ON e.enumtypid = t.oid + WHERE t.typname = {quote_sql_literal(enum_type)} + AND e.enumlabel = {lit} + ) THEN + ALTER TYPE {enum_type} ADD VALUE {lit}; + END IF; +END$$; +""" + lines.append(block) + return '\n'.join(lines) + +def main(): + enum_values = discover_values() + sql = emit_sql(enum_values) + print(sql) + +if __name__ == '__main__': + main() diff --git a/verify.log b/verify.log index 8ff5aa4..f859211 100644 --- a/verify.log +++ b/verify.log @@ -6,7 +6,7 @@ executed_at ------------------------------- - 2025-09-19 08:16:02.305781+00 + 2025-09-19 09:15:01.473058+00 (1 row) === 1) Comptes & répartition par rôle ========================== @@ -126,49 +126,49 @@ === 13) Performance : EXPLAIN sur requêtes clés =============== QUERY PLAN ---------------------------------------------------------------------------------------------------------------------------------------------------- - Limit (cost=11.31..11.31 rows=3 width=89) (actual time=0.027..0.029 rows=0 loops=1) - -> Sort (cost=11.31..11.31 rows=3 width=89) (actual time=0.026..0.027 rows=0 loops=1) + Limit (cost=11.31..11.31 rows=3 width=89) (actual time=0.035..0.036 rows=0 loops=1) + -> Sort (cost=11.31..11.31 rows=3 width=89) (actual time=0.032..0.033 rows=0 loops=1) Sort Key: cree_le DESC Sort Method: quicksort Memory: 25kB - -> Bitmap Heap Scan on messages m (cost=4.17..11.28 rows=3 width=89) (actual time=0.021..0.021 rows=0 loops=1) + -> Bitmap Heap Scan on messages m (cost=4.17..11.28 rows=3 width=89) (actual time=0.023..0.024 rows=0 loops=1) Recheck Cond: (id_dossier = 'dddddddd-dddd-dddd-dddd-dddddddddddd'::uuid) - -> Bitmap Index Scan on idx_messages_id_dossier_cree_le (cost=0.00..4.17 rows=3 width=0) (actual time=0.011..0.011 rows=0 loops=1) + -> Bitmap Index Scan on idx_messages_id_dossier_cree_le (cost=0.00..4.17 rows=3 width=0) (actual time=0.012..0.013 rows=0 loops=1) Index Cond: (id_dossier = 'dddddddd-dddd-dddd-dddd-dddddddddddd'::uuid) - Planning Time: 0.837 ms - Execution Time: 0.079 ms + Planning Time: 1.468 ms + Execution Time: 0.521 ms (10 rows) QUERY PLAN ----------------------------------------------------------------------------------------------------------------------------------------------------- Index Scan using idx_evenements_id_enfant_date_debut on evenements ev (cost=0.15..8.17 rows=1 width=161) (actual time=0.006..0.007 rows=0 loops=1) Index Cond: ((id_enfant = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'::uuid) AND (date_debut >= '2025-01-01 00:00:00+00'::timestamp with time zone)) - Planning Time: 0.107 ms - Execution Time: 0.099 ms + Planning Time: 0.117 ms + Execution Time: 0.025 ms (4 rows) QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------ - Limit (cost=9.52..9.53 rows=2 width=73) (actual time=0.020..0.021 rows=0 loops=1) - -> Sort (cost=9.52..9.53 rows=2 width=73) (actual time=0.019..0.020 rows=0 loops=1) + Limit (cost=9.52..9.53 rows=2 width=73) (actual time=0.012..0.013 rows=0 loops=1) + -> Sort (cost=9.52..9.53 rows=2 width=73) (actual time=0.011..0.012 rows=0 loops=1) Sort Key: cree_le DESC Sort Method: quicksort Memory: 25kB - -> Bitmap Heap Scan on notifications n (cost=4.17..9.51 rows=2 width=73) (actual time=0.014..0.014 rows=0 loops=1) + -> Bitmap Heap Scan on notifications n (cost=4.17..9.51 rows=2 width=73) (actual time=0.007..0.008 rows=0 loops=1) Recheck Cond: ((id_utilisateur = '33333333-3333-3333-3333-333333333333'::uuid) AND (NOT lu)) - -> Bitmap Index Scan on idx_notifications_user_lu_cree_le (cost=0.00..4.17 rows=2 width=0) (actual time=0.008..0.008 rows=0 loops=1) + -> Bitmap Index Scan on idx_notifications_user_lu_cree_le (cost=0.00..4.17 rows=2 width=0) (actual time=0.004..0.005 rows=0 loops=1) Index Cond: ((id_utilisateur = '33333333-3333-3333-3333-333333333333'::uuid) AND (lu = false)) - Planning Time: 0.122 ms - Execution Time: 0.043 ms + Planning Time: 0.087 ms + Execution Time: 0.027 ms (10 rows) QUERY PLAN ----------------------------------------------------------------------------------------------------------------------------------------------------------------- - Sort (cost=8.18..8.18 rows=1 width=267) (actual time=0.429..0.431 rows=0 loops=1) + Sort (cost=8.18..8.18 rows=1 width=267) (actual time=0.017..0.018 rows=0 loops=1) Sort Key: cree_le DESC Sort Method: quicksort Memory: 25kB - -> Index Scan using idx_dossiers_id_parent_enfant_statut_cree_le on dossiers d (cost=0.15..8.17 rows=1 width=267) (actual time=0.419..0.419 rows=0 loops=1) + -> Index Scan using idx_dossiers_id_parent_enfant_statut_cree_le on dossiers d (cost=0.15..8.17 rows=1 width=267) (actual time=0.012..0.012 rows=0 loops=1) Index Cond: ((id_parent = '33333333-3333-3333-3333-333333333333'::uuid) AND (id_enfant = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'::uuid)) - Planning Time: 1.115 ms - Execution Time: 0.476 ms + Planning Time: 0.062 ms + Execution Time: 0.032 ms (7 rows) === 14) JSONB : exemples de filtrage ===========================