Compare commits

...
Author SHA1 Message Date
jmartinandCursor 7f8104e7e4 fix(auth): correctif login et parsing user (version stable)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-17 15:54:24 +01:00
jmartinandCursor c4d93ee458 Merge develop into master (squash)
- Ticket #92: endpoint GET /gestionnaires, dashboard admin données réelles
- Seed données de test, script reset-and-seed-db.sh
- Modèles frontend (Parent, AM), user_service, widgets admin

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-16 22:55:38 +01:00
jmartin 1fca0cf132 Merge branch 'master' of https://git.ptits-pas.fr/jmartin/petitspas 2026-02-16 16:22:16 +01:00
jmartinandCursor b16dd4b55c merge: resolution conflits develop -> master (ticket #14)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-16 16:22:04 +01:00
jmartinandCursor 8682421453 Merge develop into master (squash)
- feat(#90): API Inscription AM - POST /auth/register/am
- Suppression legacy register/parent/legacy
- BDD assistantes_maternelles alignée entité
- Script test register AM

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-16 16:19:23 +01:00
jmartinandCursor 31bd8c3175 fix(#90): BDD assistantes_maternelles alignée entité + script test curl
- BDD.sql: ville_residence, annee_experience, specialite, date_agrement nullable
- scripts/test-register-am.sh pour tester POST /auth/register/am

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-16 16:18:06 +01:00
jmartinandCursor c94f2cf0d5 feat(#90): API Inscription AM - POST /auth/register/am
- DTO RegisterAMCompletDto (identité, photo, infos pro, CGU)
- Endpoint POST /auth/register/am + inscrireAMComplet() (transaction User + AssistanteMaternelle)
- Photo base64, token création MDP, consentement photo
- Suppression legacy: route register/parent/legacy, registerParent(), RegisterParentDto
- Frontend: ApiConfig.registerAM pour ticket #91

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-16 00:05:23 +01:00
jmartinandCursor dfe7daed14 Merge squash develop into master (incl. #14 première config setup/complete)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-15 23:20:15 +01:00
jmartin 111935e451 Merge branch 'feature/14-premiere-connexion-config' into develop (#14) 2026-02-15 23:20:00 +01:00
jmartinandCursor ae3292a7fc fix(backend): setup/complete accepte userId null pour éviter erreur UUID (#14)
- completeSetup: userId = req.user?.id ?? null (plus de fallback 'system')
- markSetupCompleted(userId: string | null), set(..., userId ?? undefined)
- Corrige 'invalid input syntax for type uuid: "system"' au clic Sauvegarder

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-15 23:19:18 +01:00
jmartinandCursor 8e8c6d79b1 feat(#14): finalisation redirection et nettoyage
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-15 23:08:02 +01:00
jmartinandCursor 6752dc97b4 feat(#14): redirection première connexion config
- Redirection vers /login après première config réussie
- Gestion défensive des réponses API (200/201, bool/string)
- Force l'onglet Paramètres si setup non terminé

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-15 23:02:12 +01:00
jmartinandCursor 31857ec891 docs(#14): note back config/setup + frontend parsing erreurs
- docs/14_NOTE-BACKEND-CONFIG-SETUP.md : modifs à faire côté back (UUID system)
- configuration_service : parsing défensif des réponses d'erreur (évite JSNull)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-13 16:10:04 +01:00
jmartinandCursor ca7ef862da feat(admin): première connexion → panneau Paramètres, reste grisé jusqu'à Sauvegarder (#14)
- Au chargement admin: appel getSetupStatus(), si non terminé → onglet Paramètres par défaut
- Onglet Gestion des utilisateurs grisé et inaccessible tant que setup non complété
- Sauvegarder: updateBulk + completeSetup + déblocage des panneaux
- Tester SMTP: saveBulkOnly puis test (sans completeSetup, panneaux restent verrouillés)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-13 15:54:47 +01:00
jmartinandCursor 11aa66feff Merge squash develop into master (incl. #89 log API requests debug)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-13 15:26:06 +01:00
jmartin 358eefdab3 Merge branch 'feature/89-log-api-requests' into develop (#89) 2026-02-13 15:25:56 +01:00
jmartinandCursor d23f3c9f4f feat(admin): panneau Paramètres - sauvegarde config + test SMTP (#15)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-13 15:23:00 +01:00
jmartinandCursor 1834eb8c79 feat(admin): panneau Paramètres - sauvegarde config + test SMTP
- Onglet Paramètres dans l'admin avec 3 sections (Email, Personnalisation, Avancé)
- Service ConfigurationService (GET config, PATCH bulk, POST test-smtp)
- Bouton Sauvegarder et bouton Tester SMTP (sauvegarde avant test)
- Endpoints api_config pour configuration

Closes #15

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-13 11:16:37 +01:00
jmartinandCursor 0386785f81 feat(backend): log des appels API en mode debug (#89)
- Ajout LogRequestInterceptor (méthode, URL, query, body)
- Activé via LOG_API_REQUESTS=true
- Masquage des champs sensibles (password, smtp_password, token...)
- Enregistrement global dans main.ts, doc dans .env.example

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-13 11:15:41 +01:00
jmartinandCursor c43f55bed6 Merge origin/develop (conflit 23_LISTE-TICKETS résolu, garde numéros = Gitea)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-10 00:20:59 +01:00
jmartinandCursor 0c48a5c06f Merge branch 'develop' (docs concept v1.3, conflit résolu)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-10 00:20:32 +01:00
jmartinandCursor be8b1f23ed docs: concept v1.3 config (panneau Paramètres 3 sections, numéros = Gitea)
- 21_CONFIGURATION-SYSTEME: workflow sans /admin/setup, 3 sections, panneau unique
- 23_LISTE-TICKETS: numéros de section = numéros Gitea, tickets #14/#15 alignés
- 24_DECISIONS-PROJET: config initiale = panneau + navigation bloquée
- BRIEFING-FRONTEND: tickets #12/#13 remplacés par panneau Paramètres
- Suppression login_screen.dart.bak

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-10 00:19:35 +01:00
jmartinandCursor 18b270eaa3 Merge origin/develop: résolution conflits doc tickets
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 23:55:16 +01:00
jmartinandCursor 68e4f54814 Merge develop (squash): correctifs modale MDP (champs lavande/jaune), doc tickets 84/85
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 23:53:58 +01:00
jmartinandCursor 6794190916 chore(login): retrait du lien Test modale MDP
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 23:52:51 +01:00
jmartinandCursor 790761d576 fix(modale): champs MDP actuel lavande, nouveau et confirmation jaune
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 23:52:25 +01:00
jmartinandCursor 930097f87d Merge develop: fix auth connexion admin (#84)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 22:37:48 +01:00
jmartinandCursor 18af5c9034 fix(auth): connexion admin - token snake_case, routes GoRouter, profil (Closes #84)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 22:36:52 +01:00
jmartinandCursor 10bf2553e7 fix(ui): renforcer ombre des boutons Parents/AM sur mobile (écran choix)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 19:15:10 +01:00
jmartinandCursor 678f4219b5 docs: ticket #82 fermé (écran Login mobile)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 18:56:07 +01:00
jmartinandCursor 5295e8ec72 Merge develop (squash): login mobile, formulaire sous slogan par ratio
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 18:54:16 +01:00
jmartinandCursor 480f4a9396 fix(login): position formulaire sous slogan par ratio image (river_logo_mobile)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 18:53:37 +01:00
jmartinandCursor 6bf0932da8 docs: Index, doc API Gitea et script fermeture issue
- docs/00_INDEX.md : entrée pour 26_GITEA-API
- docs/26_GITEA-API.md : procédure API Gitea (auth, issues, PR, branches, dépannage)
- scripts/gitea-close-issue.sh : fermer une issue via l'API (GITEA_TOKEN / .gitea-token)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 16:39:47 +01:00
jmartinandCursor 2f1740b35f docs: Ajouter ticket #83 RegisterChoiceScreen Mobile (terminé)
Mise à jour de la liste des tickets pour documenter le ticket #83 complété :
- Adaptation responsive RegisterChoiceScreen (mobile/desktop)
- Extraction ChoiceCardWidget réutilisable
- Bouton Précédent stylisé avec CustomNavigationButton
- Tailles icônes augmentées (140px/170px)

Total: 65 tickets (~184h)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 12:37:04 +01:00
jmartinandCursor fd97e68dd9 feat: Adapter RegisterChoiceScreen pour mobile (Closes #83)
- Implémentation responsive avec LayoutBuilder pour détecter mobile/desktop
- Mode mobile : titre au-dessus, carte pleine largeur avec ratio 2/3, boutons verticaux
- Mode desktop : chevron en haut à gauche, layout texte/carte côte à côte
- Extraction de la logique de carte dans ChoiceCardWidget réutilisable
- Bouton "Précédent" stylisé avec CustomNavigationButton et HoverReliefWidget
- Tailles d'icônes augmentées (140px mobile, 170px desktop)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 12:34:50 +01:00
jmartinandCursor 813fdb8449 fix(#83): Corriger le style du bouton Précédent et ajuster les tailles d'icônes
Utilise CustomNavigationButton avec HoverReliefWidget pour le bouton Précédent en mode mobile, assurant la cohérence visuelle avec les autres écrans. Augmente également la taille des icônes de choix (140px mobile, 170px desktop).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 12:33:48 +01:00
jmartinandCursor 155c6ca4d5 chore(deps): Update pubspec.lock
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 11:44:47 +01:00
jmartinandCursor 030ef81038 feat(frontend): Adapt RegisterChoiceScreen for mobile (Closes #83)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-07 17:43:17 +01:00
jmartinandCursor 0d88597bb6 test
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-07 17:42:09 +01:00
jmartinandCursor 9b007fe490 docs: Mark tickets #38, #39, #40, #41, #42 as completed (Closes #38, Closes #39, Closes #40, Closes #41, Closes #42)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-07 17:15:15 +01:00
jmartinandCursor 7ecb99963c docs: Update ticket list with #81 status (Closes #81)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-07 17:10:47 +01:00
jmartinandCursor 39814c76b1 docs: Update ticket list and cleanup obsolete files (Closes #79, Closes #80)
- Delete obsolete nanny_registration_data.dart and nanny_register_confirmation_screen.dart
- Update 23_LISTE-TICKETS.md with #79 and #80 status

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-07 17:09:29 +01:00
jmartin b956f94ad2 Merge branch 'develop' 2026-02-07 14:52:47 +01:00
jmartinandCursor b18d5c8a9e feat(frontend): Refonte infrastructure formulaires multi-modes
- Support des modes Desktop/Mobile et Édition/Lecture seule
- Refactoring des widgets de formulaire (PersonalInfo, ProfessionalInfo, Presentation, ChildCard)
- Mise à jour des écrans de récapitulatif (ParentStep5, AmStep4)
- Ajout de navigation (Précédent/Soumettre) sur mobile

Closes #78

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-07 14:51:33 +01:00
jmartinandCursor 6ad88cbbc6 docs: Update ticket list with #78 (Multi-mode forms refactoring)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-07 14:46:34 +01:00
jmartinandCursor dfe91ed772 Add navigation buttons to mobile recap screens and fix child card width
- Add 'Previous' and 'Submit' buttons to mobile recap screens (Parent & AM)
- Fix imports for navigation buttons and widgets
- Adjust ChildCardWidget width to fill available space on mobile editing

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-07 14:44:17 +01:00
jmartinandCursor 08612c455d Fix recap screens layout (desktop/mobile) and widget styles
- Restore horizontal 2:1 layout for desktop readonly cards
- Implement adaptive height for mobile readonly cards
- Fix spacing and margins on mobile recap screens
- Update field styles to use beige background
- Adjust ChildCardWidget width for mobile editing
- Fix compilation errors and duplicate methods

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-07 13:29:11 +01:00
jmartinandCursor 6452706680 fix(#78): Ajustements UI ChildCardWidget et ParentStep3
- Réduction de la taille des polices et des champs dans la carte enfant (Mobile/Desktop) pour éviter l'overflow.
- Restauration de la taille du bouton "+" en mode Desktop (100px).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-04 14:33:04 +01:00
jmartinandCursor eea94769bf feat(#78): Migrer ParentRegisterStep3Screen (Enfants) vers infrastructure multi-modes
Adaptation responsive du formulaire "Informations Enfants" (Parent Step 3) :
- Desktop : Conservation du layout horizontal avec scroll et effets de fondu
- Mobile : Layout vertical avec cartes empilées
  - Header fixe
  - Bouton "+" carré (50px) centré à la fin de la liste
  - Boutons navigation intégrés au scroll
  - Cartes enfants adaptées (scale 0.9, polices réduites)
- Mise à jour DisplayConfig (mode optionnel par défaut)
- Mise à jour AppCustomCheckbox (paramètre fontSize)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-04 11:43:26 +01:00
jmartinandCursor bdecbc2c1d feat(#78): Migrer PresentationFormScreen vers infrastructure multi-modes
Adaptation responsive du formulaire de présentation (AM Step 3) :
- Desktop : Layout horizontal avec scroll global (format 2:1)
- Mobile : Layout plein écran sans scroll global
  - Header fixe (titre + étape)
  - Carte occupe tout l'espace vertical disponible
  - Seul le champ texte interne est scrollable
  - Boutons fixes en bas
- Checkbox CGU adaptée (texte raccourci + scale 0.85 en mobile)
- Chevrons uniquement en mode desktop

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-04 11:19:43 +01:00
jmartinandCursor f8bd911c02 feat(#78): Migrer ProfessionalInfoFormScreen vers infrastructure multi-modes
Adaptation responsive du formulaire d'informations professionnelles (AM Step 2) :
- Desktop : photo gauche (270px) + champs droite
- Mobile : layout vertical avec photo 200px + champs empilés
- Boutons CustomNavigationButton (violet/vert) sous la carte en mobile
- Chevrons uniquement en mode desktop
- Espacement adapté (12px mobile vs 32px desktop)
- Tailles de police adaptées

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-04 11:12:42 +01:00
jmartinandCursor b79f8c7e64 refactor(#78): Renommer assets images pour usage générique
Renommage des assets pour permettre leur utilisation
aussi bien pour les boutons que pour les champs :

**Images renommées :**
- input_field_bg.png → bg_beige.png
- input_field_jaune.png → bg_yellow.png
- input_field_lavande.png → bg_lavender.png
- btn_green.png → bg_green.png

**Fichiers mis à jour (8) :**
- custom_app_text_field.dart (champs de formulaire)
- custom_navigation_button.dart (nouveau widget boutons)
- base_form_screen.dart (structure de page)
- login_screen.dart
- change_password_dialog.dart
- am_register_step4_screen.dart
- parent_register_step5_screen.dart
- summary_screen.dart

**Avantages :**
✅ Noms génériques et cohérents
✅ Réutilisabilité boutons ET champs
✅ Maintenance facilitée

Référence: #78

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-04 11:01:54 +01:00
jmartinandCursor a57993a90f feat(#78): Finaliser layout mobile responsive de PersonalInfoFormScreen
Layout mobile complètement repensé avec séparation desktop/mobile :

**Layout Desktop (_buildDesktopFields) :**
- Champs par paires horizontales (Row avec Expanded)
- Code Postal + Ville avec ratio flex 2:5
- Espacement 32px entre lignes
- Taille police : 22px labels, 20px input

**Layout Mobile (_buildMobileFields) :**
- Tous les champs empilés verticalement (Column pure)
- Chaque champ prend toute la largeur
- Espacement 12px entre champs (compact)
- Taille police : 15px labels, 14px input
- Hauteur champs réduite : 45px

**Nouveau widget CustomNavigationButton :**
- Widget réutilisable pour boutons navigation
- Enum NavigationButtonStyle (green/purple)
- Utilise assets images comme fond
- Bouton "Précédent" : fond lavande, texte violet foncé
- Bouton "Suivant" : fond vert, texte vert foncé

**Boutons mobile :**
- Positionnés sous la carte (dans le scroll)
- Aligned avec les marges de la carte (5% de chaque côté)
- Prennent toute la largeur avec Expanded
- Écart de 16px entre les deux
- Utilisation de CustomNavigationButton

**Optimisations mobile :**
- Padding carte réduit : 20px vertical (vs 40px initial)
- Toggles compacts (Switch scale 0.85)
- Titre : 18px (vs 24px desktop)
- Étape : 13px (vs 16px desktop)

Référence: #78

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-04 11:01:43 +01:00
jmartinandCursor 1d774f29eb feat(#78): Migrer PersonalInfoFormScreen vers infrastructure multi-modes
Migration du widget PersonalInfoFormScreen pour utiliser la nouvelle
infrastructure générique :

**Modifications PersonalInfoFormScreen:**
- Ajout paramètre DisplayMode mode (editable/readonly)
- Utilisation de DisplayConfig pour détecter mobile/desktop
- Utilisation de FormFieldRow pour layout responsive
- Adaptation automatique carte vertical/horizontal
- Boutons navigation adaptés mobile/desktop
- Conservation de toutes les fonctionnalités (toggles, validation, etc.)

**Corrections infrastructure:**
- base_form_screen.dart: Correction paramètres ImageButton (bg, textColor)
- form_field_wrapper.dart: Correction paramètres CustomAppTextField
- Gestion correcte des types nullables (TextInputType)

**Résultat:**
✅ Compilation sans erreurs
✅ Layout responsive fonctionnel
✅ Mode editable opérationnel
✅ Prêt pour mode readonly (récaps)

Référence: #78

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-03 17:38:54 +01:00
jmartinandCursor 890619ff59 feat(#78): Créer infrastructure générique pour formulaires multi-modes
Nouvelle architecture centralisée pour tous les formulaires :

**Configuration centrale (display_config.dart):**
- DisplayMode enum (editable/readonly)
- LayoutType enum (mobile/desktop)
- DisplayConfig class pour configuration complète
- LayoutHelper avec utilitaires (détection, spacing, etc.)
- Breakpoint: 600px (mobile < 600px reste toujours vertical)

**Widgets génériques (form_field_wrapper.dart):**
- FormFieldWrapper: champ auto-adaptatif (TextField ou Text readonly)
- FormFieldRow: ligne responsive (horizontal desktop, vertical mobile)

**Structure de page (base_form_screen.dart):**
- BaseFormScreen: layout complet avec carte, boutons, navigation
- Gestion auto des assets carte (horizontal/vertical selon layout)

**Avantages:**
✅ Code unique pour editable + readonly + mobile + desktop
✅ Logique centralisée (aucune duplication)
✅ Héritage automatique via DisplayConfig propagé
✅ API simple et cohérente

Prochaine étape: Migration des widgets existants

Référence: #78

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-03 17:33:29 +01:00
jmartinandCursor 5d7eb9eb36 fix(#79): Supprimer toutes les références obsolètes à Nanny
Corrections suite au merge master:
- Suppression imports nanny_register_step*.dart (fichiers supprimés)
- Suppression variable nannyRegistrationDataNotifier
- Suppression section Nanny Registration Flow complète
- Ajout import go_router dans login_screen.dart

L'application compile maintenant sans erreurs.

Référence: #79

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-03 16:17:49 +01:00
jmartin 45bd8a9ef1 Sync master with develop 2026-02-03 16:12:58 +01:00
jmartinandCursor acb8e72a7c Merge remote-tracking branch 'origin/master' into master
Résolution conflit: Suppression de frontend/lib/navigation/app_router.dart
(fichier obsolète remplacé par frontend/lib/config/app_router.dart)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-03 16:12:29 +01:00
jmartin b6c70a52ac Merge develop into master
Synchronisation complète de develop vers master.

Inclut:
- Backend: API auth, config, documents légaux (#3-#9, #31, #75-#77)
- Database: Schéma unifié, tables config/documents (#3-#7)
- Documentation: Architecture, déploiement, tickets (#61-#62)
- Frontend: Inscription parents/AM, dashboard (#36-#42)
- Frontend: Renommage Nanny→AM (#79)
- Frontend: Widgets génériques réutilisables (#80)
- Frontend: Corrections post-refactoring (#81)

80 commits mergés - master à jour avec develop
2026-02-03 16:10:47 +01:00
jmartin 96794919a8 refactor(widgets): Extraire ProfessionalInfoFormScreen en widget réutilisable
Nouveau widget professional_info_form_screen.dart :
- Formulaire complet d'infos professionnelles pour AM
- Gestion de la photo avec sélection et consentement
- Champs : ville/pays/date de naissance, NIR, agrément, capacité
- Validations intégrées (NIR 13 chiffres, capacité > 0, etc.)

AM Step 2 refactorisé :
- Utilise le nouveau ProfessionalInfoFormScreen
- Code réduit de ~280 lignes à ~75 lignes
- Logique de génération de données de test préservée
- Préparé pour réutilisation dans les récapitulatifs

Impact : -205 lignes de code
2026-01-28 17:09:41 +01:00
jmartin 271dc713a3 feat(widgets): Créer composants réutilisables pour écrans de récapitulatif
Nouveau fichier summary_screen.dart avec :
- Widget SummaryScreen : Layout générique pour récapitulatif
- Widget SummaryCard : Carte de récapitulatif avec AspectRatio et bouton Edit
- Fonction buildDisplayFieldValue : Champ en lecture seule stylisé

Ces composants permettront de simplifier et unifier les écrans
de récapitulatif parent et AM.
2026-01-28 17:03:10 +01:00
jmartin 13741b0430 chore(auth): Supprimer les fichiers nanny obsolètes
Suppression des 4 fichiers nanny_register_step*.dart qui sont obsolètes
après le renommage complet en "am" (Assistante Maternelle).

Les nouveaux fichiers correspondants sont :
- am_register_step1_screen.dart
- am_register_step2_screen.dart
- am_register_step3_screen.dart
- am_register_step4_screen.dart
2026-01-28 17:00:52 +01:00
jmartin 8e3af711e5 refactor(widgets): Extraire ChildCardWidget dans un fichier séparé
Extraction du widget _ChildCardWidget de parent_register_step3_screen.dart
vers un fichier réutilisable child_card_widget.dart

Améliorations :
- Widget désormais public (ChildCardWidget au lieu de _ChildCardWidget)
- Réutilisable dans d'autres écrans (ex: récapitulatifs détaillés)
- Imports nettoyés et simplifiés
- Meilleure organisation du code

Le widget gère :
- Photo de l'enfant avec sélection d'image
- Toggle "Enfant à naître"
- Champs: Prénom, Nom, Date de naissance
- Checkboxes: Consentement photo, Naissance multiple
- Bouton de suppression (si > 1 enfant)
2026-01-28 17:00:40 +01:00
jmartin e700e50924 fix(widgets): Ajouter 2 toggles côte à côte comme l'ancien design
Structure correcte pour Parent Step 2 :
- Toggle gauche : "Ajouter Parent 2 ?" avec icône person_add_alt_1
- Toggle droit : "Même Adresse ?" avec icône home_work_outlined
- Les 2 toggles sont dans une Row (flex: 12 chacun)
- Toggle "Même Adresse" grisé si Parent 2 désactivé
- Suppression de l'ancienne checkbox en bas

Conforme à l'ancien code testé et validé.
2026-01-28 16:52:04 +01:00
jmartin 36ef0f8d5c fix(widgets): Repositionner le toggle et checkbox dans la carte
- Toggle "Il y a un 2ème parent ?" maintenant DANS la carte (pas au-dessus)
- Checkbox "Même adresse que parent 1" reste dans la carte
- Taille du texte du toggle ajustée à 20px pour cohérence
- Espacement de 25px après le toggle

Position correcte conforme à l'ancien design.
2026-01-28 16:51:02 +01:00
jmartin f09deb5efc fix(auth): Correction des erreurs de compilation
Corrections des appels de méthodes et des types :

1. Parent Steps 1-2 : Passer des objets ParentData complets
   - updateParent1(ParentData(...)) au lieu de paramètres nommés
   - updateParent2(ParentData(...)) ou null pour supprimer

2. AM Step 1 : Utiliser la bonne méthode
   - updateIdentityInfo() au lieu de updatePersonalInfo()

3. personal_info_form_screen : Corrections widgets
   - Accès correct à widget.stepText
   - Gestion du nullable sur onChanged de AppCustomCheckbox

Ces corrections permettent la compilation sans erreur.
2026-01-28 16:47:10 +01:00
jmartin 26a0e31b32 fix(router): Mise à jour du routeur principal
- Ajout des imports pour les nouveaux écrans AM
- Mise à jour des routes /am-register-step1 à step4
- Suppression de la route /am-register-confirmation (obsolète)
- Configuration du Provider AmRegistrationData
- Nettoyage des imports inutilisés

Les routes AM sont maintenant complètes et fonctionnelles.
2026-01-28 16:43:55 +01:00
jmartin 21430dca41 refactor(auth): Refactoring écrans avec widgets génériques
Refactorisation des écrans d'inscription pour utiliser les nouveaux widgets :

Parent Step 1 (227 → 65 lignes, -71%)
- Utilise personal_info_form_screen
- Conserve préremplissage des données de test
- Couleur : peach

Parent Step 2 (273 → 90 lignes, -67%)
- Utilise personal_info_form_screen
- Toggle "Il y a un 2ème parent"
- Checkbox "Même adresse que parent 1"
- Couleur : blue

Parent Step 4 (247 → 42 lignes, -83%)
- Utilise presentation_form_screen
- Formulaire de motivation
- Couleur : green

AM Step 1 (209 → 65 lignes, -69%)
- Utilise personal_info_form_screen
- Conserve préremplissage des données de test
- Couleur : blue

AM Step 3 (195 → 45 lignes, -77%)
- Utilise presentation_form_screen
- Formulaire de présentation
- Couleur : peach

Total : -709 lignes de code maintenable !
2026-01-28 16:43:47 +01:00
jmartin dcb81d3feb feat(widgets): Création de widgets génériques réutilisables
Création de 2 nouveaux widgets génériques pour réduire la duplication :

1. presentation_form_screen.dart
   - Widget pour formulaires de présentation/motivation
   - Paramétrable : titre, couleur, hint, routes
   - Utilisé par Parent Step 4 et AM Step 3
   - Réduction de ~350 lignes de code dupliqué

2. personal_info_form_screen.dart
   - Widget pour formulaires d'informations personnelles
   - Gère nom, prénom, téléphone, email, adresse
   - Options : toggle "2ème parent", checkbox "même adresse"
   - Utilisé par Parent Steps 1-2 et AM Step 1
   - Réduction de ~460 lignes de code dupliqué

Avantages :
- Maintenance simplifiée (1 seul fichier à modifier)
- Cohérence visuelle garantie entre tous les écrans
- Extensibilité facile pour nouveaux types d'utilisateurs
2026-01-28 16:43:36 +01:00
jmartin 7c86feeb78 chore(auth): Suppression des fichiers obsolètes et doublons
- Suppression de l'ancien routeur navigation/app_router.dart
- Suppression du dossier /parent/ (versions dupliquées)
- Suppression du dossier /am/ (versions de travail temporaires)

Ces fichiers sont remplacés par les versions actives dans auth/
2026-01-28 16:43:25 +01:00
jmartin df87abbb85 feat(auth): Renommer "Nanny" en "Assistante Maternelle" (AM)
- Création du modèle am_registration_data.dart
- Création des 4 écrans d'inscription AM (steps 1-4)
- Mise à jour du bouton "Assistante Maternelle" dans register_choice
- Conformité CDC : pas de champs mot de passe dans les formulaires
- Préremplissage des données de test pour faciliter le développement

Ref: Ticket #XX - Renommage workflow inscription AM
2026-01-28 16:43:16 +01:00
jmartin bd81561e41 Merge branch 'master' into develop 2026-01-27 16:51:02 +01:00
jmartin cc96ef20e1 Merge branch 'master' of https://git.ptits-pas.fr/jmartin/petitspas 2026-01-27 16:49:44 +01:00
jmartin a4ac65a5db Merge feature/47: Modale de changement de mot de passe obligatoire 2026-01-27 16:49:37 +01:00
jmartin 3d13eb5b2e [Doc] Ajout tâche amélioration libellé consentement photo (#62)
Ajout d'une tâche dans le suivi des évolutions du CDC :
- Note pour améliorer le libellé de la checkbox de consentement photo
  sur l'écran d'inscription des nounous (étape 2)

Le libellé actuel 'J'accepte l'utilisation de ma photo' devra être
rendu plus explicite et conforme RGPD.

Refs: #62 (Amendement CDC)
2026-01-27 16:44:23 +01:00
jmartin 5b37d09fa9 [Doc] Guide d'architecture technique et déploiement (#61 #16)
Ajout d'une documentation technique complète pour l'infrastructure
et le déploiement de l'application P'titsPas.

Contenu du guide :
- Vue d'ensemble de l'architecture (Flutter frontend + Node.js backend)
- Prérequis serveur (Node.js, PostgreSQL, ressources recommandées)
- Instructions d'installation pas à pas
- Configuration de la base de données PostgreSQL
- Déploiement du backend (NestJS)
- Build et déploiement du frontend Flutter Web
- Configuration NGINX comme reverse proxy
- Sécurisation SSL/TLS avec Let's Encrypt
- Monitoring et maintenance
- Sauvegarde et restauration
- Troubleshooting des problèmes courants

Ce document est essentiel pour le déploiement on-premise de l'application
par les collectivités locales.

Refs: #61 (Guide installation & configuration), #16 (Doc config on-premise)
2026-01-27 16:44:23 +01:00
jmartin 53f3af9794 [Frontend] Ajout nouvelles icônes SVG de l'application
Ajout de deux versions d'icônes SVG pour l'application :
- icon.svg : Icône standard de l'application
- icon_improved.svg : Version améliorée de l'icône

Ces icônes seront utilisées pour le branding de l'application
et les différentes tailles d'affichage.
2026-01-27 16:44:23 +01:00
jmartin 105cf53e7b [Frontend] Parcours complet inscription Assistantes Maternelles (#40 #41 #42)
Implémentation du parcours d'inscription des assistantes maternelles en 4 étapes
+ écran de confirmation, en utilisant Provider pour la gestion d'état.

Fonctionnalités implémentées :
- Étape 1 : Identité (nom, prénom, adresse, email, mot de passe)
- Étape 2 : Infos professionnelles (photo, agrément, NIR, capacité d'accueil)
- Étape 3 : Présentation personnelle et acceptation CGU
- Étape 4 : Récapitulatif et validation finale
- Écran de confirmation post-inscription

Fichiers ajoutés :
- models/nanny_registration_data.dart : Modèle de données avec Provider
- screens/auth/nanny_register_step1_screen.dart : Identité
- screens/auth/nanny_register_step2_screen.dart : Infos pro
- screens/auth/nanny_register_step3_screen.dart : Présentation
- screens/auth/nanny_register_step4_screen.dart : Récapitulatif
- screens/auth/nanny_register_confirmation_screen.dart : Confirmation
- screens/unknown_screen.dart : Écran pour routes inconnues
- config/app_router.dart : Copie du routeur (à intégrer)

Refs: #40 (Panneau 1 Identité), #41 (Panneau 2 Infos pro), #42 (Finalisation)
2026-01-27 16:44:23 +01:00
jmartin 29bee9fa80 [Frontend] Refactorisation inscription Parents avec Provider (#38 #39)
Refactorisation complète du parcours d'inscription des parents pour utiliser
Provider au lieu du passage de données par paramètres de navigation.

Modifications principales :
- Utilisation de Provider pour partager UserRegistrationData entre les étapes
- Simplification du routeur (suppression des paramètres)
- Amélioration de la persistance des données entre les étapes
- Meilleure expérience utilisateur lors de la navigation

Fichiers modifiés :
- models/user_registration_data.dart : Modèle avec ChangeNotifier
- screens/auth/parent_register_step1-5_screen.dart : Intégration Provider
- navigation/app_router.dart : Simplification du routing
- main.dart : Configuration du Provider
- login_screen.dart : Ajout navigation vers inscription
- register_choice_screen.dart : Navigation vers parcours parent/AM
- utils/data_generator.dart : Génération de données de test

Refs: #38 (Étape 3 Enfants), #39 (Étapes 4-6 Finalisation)
2026-01-27 16:44:23 +01:00
Julien Martin dbd56637e1 chore: ignore les fichiers générés par Flutter pour Android et Windows 2026-01-27 16:44:23 +01:00
jmartin 264e0d49ae fix: Configuration CORS explicite pour dev localhost 2026-01-27 16:36:53 +01:00
jmartin fe71fdf28e feat(#47): Ajout de la modale de changement de mot de passe obligatoire
Implémentation complète du ticket #47 :
- Mise à jour de l'URL API vers app.ptits-pas.fr
- Ajout du champ changement_mdp_obligatoire au modèle AppUser
- Ajout des endpoints /auth/me et /auth/change-password-required
- Implémentation de la vraie logique de connexion dans AuthService
- Création de la modale ChangePasswordDialog non-dismissible
- Connexion du bouton de connexion avec gestion de la modale
- Ajout des routes admin-dashboard et parent-dashboard

La modale s'affiche automatiquement après connexion si
changement_mdp_obligatoire = true et bloque l'utilisateur jusqu'au
changement de mot de passe.
2026-01-27 16:30:15 +01:00
jmartin b3ec1b94ea docs: Ajout briefing développement frontend 2026-01-27 16:21:22 +01:00
jmartin 95d1c3741b feat(#31): API Changement MDP obligatoire première connexion (#77)
Co-authored-by: Julien Martin <julien.martin@ptits-pas.fr>
Co-committed-by: Julien Martin <julien.martin@ptits-pas.fr>
2026-01-27 15:13:45 +00:00
jmartin c5028c3b22 feat(#75): Seed Super Administrateur par défaut (#76)
Co-authored-by: Julien Martin <julien.martin@ptits-pas.fr>
Co-committed-by: Julien Martin <julien.martin@ptits-pas.fr>
2026-01-27 15:07:22 +00:00
jmartin cef197d133 Merge master into develop - Synchronisation des branches
Fusion des travaux :
- Backend complet (ConfigService, DocumentsLegaux, Auth, etc.)
- Frontend étapes inscription 1-2
- Infrastructure Docker
- Documentation technique

Résolution des conflits :
- Images déplacées vers frontend/assets/images/
- Dossier Archives supprimé
- Backend : version master conservée
- Frontend : améliorations UI de develop conservées
2026-01-27 14:56:49 +01:00
jmartin c934466e47 Merge pull request 'feat(#37): Inscription Parent - Étape 2 (Parent 2)' (#74) from feature/37-frontend-inscription-parent-step2 into master
feat(#37): Inscription Parent - Étape 2 (Parent 2)
2025-12-01 22:36:31 +00:00
jmartin 90d8fa8669 feat(#37): Inscription Parent - Étape 2 (Parent 2)
Frontend Step2:
- Suppression des champs mot de passe et confirmation
- Correction de l'indicateur d'étape: 2/5 → 2/6
- Améliorations visuelles (mêmes que Step1):
  * Taille des labels: 18 → 22px
  * Taille de police des champs: 18 → 20px
  * Espacement entre champs: 20 → 32px
  * Meilleure répartition verticale avec spaceEvenly

Note: Le champ password est conservé dans le modèle ParentData pour compatibilité
2025-12-01 23:36:02 +01:00
jmartin 90cdf16709 docs: Correction numérotation tickets et ajout statuts terminés
- Correction numérotation pour correspondre à Gitea (#36-#63)
- Ajout tickets #34 et #35 (réservés)
- Marquage tickets terminés avec ✅:
  * #3, #4, #7 (BDD)
  * #18, #19, #20, #21 (Backend API Parent)
  * #36 (Frontend Step1)
- Correction doublons (#38, #39, #41, #42, #47, #48)
- Renumération tickets Frontend et Tests
2025-12-01 23:28:08 +01:00
jmartin bde97c24db Merge pull request 'feat(#36): Inscription Parent - Étape 1 (Parent 1)' (#73) from feature/36-frontend-inscription-parent-step1 into master
feat(#36): Inscription Parent - Étape 1 (Parent 1)
2025-12-01 22:21:02 +00:00
jmartin 9ae6533b4d feat(#36): Inscription Parent - Étape 1 (Parent 1)
Backend:
- Retrait des champs non-CDC: profession, situation_familiale, date_naissance
- Nettoyage des DTOs RegisterParentCompletDto et RegisterParentDto
- Mise à jour de la logique dans auth.service.ts (inscrireParentComplet et legacy)

Frontend Step1:
- Suppression des champs mot de passe et confirmation
- Correction de l'indicateur d'étape: 1/5 → 1/6
- Améliorations visuelles:
  * Taille des labels: 18 → 22px
  * Taille de police des champs: 18 → 20px
  * Espacement entre champs: 20 → 32px
  * Meilleure répartition verticale avec spaceEvenly

Note: Le champ password est conservé dans le modèle ParentData pour compatibilité avec Step2
2025-12-01 23:19:58 +01:00
jmartin 579b6cae90 [Backend] API Inscription Parent - REFONTE Workflow 6 etapes (#72)
Co-authored-by: Julien Martin <julien.martin@ptits-pas.fr>
Co-committed-by: Julien Martin <julien.martin@ptits-pas.fr>
2025-12-01 21:43:36 +00:00
jmartin 9aea26805d Merge pull request '[Backend] Endpoint inscription parent + Nettoyage code #9' (#71) from feature/9-endpoints-inscription-parent into master 2025-11-30 15:10:21 +00:00
jmartin 40c7f40d12 feat(backend): endpoint inscription parent + nettoyage code #9
🧹 NETTOYAGE CODE ÉTUDIANT:
- Suppression console.log/error (4 occurrences)
- Suppression code commenté inutile
- Suppression champs obsolètes (mobile, telephone_fixe)
- Correction password nullable dans Users entity

✨ NOUVELLES FONCTIONNALITÉS:
- Ajout champs token_creation_mdp dans Users entity
- Création RegisterParentDto (validation complète)
- Endpoint POST /auth/register/parent
- Méthode registerParent() avec transaction
- Gestion Parent 1 + Parent 2 (co-parent optionnel)
- Génération tokens UUID pour création MDP
- Lecture durée token depuis AppConfigService
- Création automatique entités Parents
- Statut EN_ATTENTE par défaut
- Intégration AppConfigModule dans AuthModule
- Amélioration méthode login() (vérif statut + password null)

📋 WORKFLOW CDC CONFORME:
- Inscription SANS mot de passe
- Token envoyé par email (TODO)
- Validation gestionnaire requise
- Support co-parent avec même adresse

Réf: docs/20_WORKFLOW-CREATION-COMPTE.md
Ticket: #9 (ou #16)
2025-11-30 16:09:54 +01:00
jmartin 61b45cd830 Merge pull request '[Backend] API Documents Légaux #31' (#70) from feature/31-api-documents-legaux into master 2025-11-30 15:00:52 +00:00
jmartin f53fd903e5 feat(backend): API documents légaux #31
- Création DTOs (UploadDocumentDto, DocumentsActifsResponseDto, DocumentVersionDto)
- Création DocumentsLegauxController avec 6 endpoints:
  * GET /documents-legaux/actifs (public)
  * GET /documents-legaux/:type/versions (admin)
  * POST /documents-legaux (upload, admin)
  * PATCH /documents-legaux/:id/activer (admin)
  * GET /documents-legaux/:id/download (public)
  * GET /documents-legaux/:id/verifier-integrite (admin)
- Support upload multipart/form-data avec FileInterceptor
- Validation des types (cgu/privacy)
- Stream PDF pour téléchargement
- Intégration dans DocumentsLegauxModule
- Compilation OK

TODO: Ajouter guards auth (JwtAuthGuard, RolesGuard)

Réf: docs/22_DOCUMENTS-LEGAUX.md
2025-11-30 16:00:12 +01:00
jmartin 98082187b5 Merge pull request '[Backend] Service gestion documents légaux #8' (#69) from feature/8-service-documents-legaux into master
Merge pull request #69: Service gestion documents légaux

Ticket #8 complété
2025-11-30 14:51:36 +00:00
jmartin 1fb8c33cbf feat(backend): service gestion documents légaux #8
- Création entité DocumentLegal
- Création entité AcceptationDocument
- Création DocumentsLegauxService avec méthodes:
  * getDocumentsActifs()
  * uploadNouvelleVersion() (avec hash SHA-256)
  * activerVersion() (transaction)
  * listerVersions()
  * telechargerDocument()
  * verifierIntegrite()
  * enregistrerAcceptation()
  * getAcceptationsUtilisateur()
- Création DocumentsLegauxModule
- Intégration dans AppModule
- Ajout dépendances multer + @types/multer

Réf: docs/22_DOCUMENTS-LEGAUX.md
2025-11-30 15:50:38 +01:00
jmartin 6ceb0f0ea9 Merge feature/7-tables-documents-legaux into master
Ticket #7: Ajout tables documents légaux versionnés
- Table documents_legaux
- Table acceptations_documents
- Colonnes CGU dans utilisateurs
- Seed documents v1
2025-11-30 15:36:31 +01:00
jmartin bebd3c74da feat(bdd): ajout tables documents_legaux et acceptations_documents #7
- Création table documents_legaux (versioning + hash SHA-256)
- Création table acceptations_documents (traçabilité RGPD)
- Ajout colonnes dans utilisateurs (cgu_version_acceptee, etc.)
- Seed documents génériques v1 (CGU + Privacy)
- Index pour performance

Réf: docs/22_DOCUMENTS-LEGAUX.md
2025-11-30 15:34:28 +01:00
jmartin e1628da9cb Merge pull request '[Backend] API admin configuration avec test SMTP' (#67) from feature/6-admin-configuration into master
Merge pull request #67: [Backend] API admin configuration avec test SMTP

Implémentation de l'API REST pour la gestion de la configuration système avec test SMTP intégré.

Closes #6
2025-11-28 16:24:57 +00:00
jmartin eb1583b35b feat(backend): API admin configuration avec test SMTP (#6)
Implémentation de l'API REST pour la gestion de la configuration
système par les administrateurs.

Nouveaux fichiers :
- modules/config/config.controller.ts : Controller REST
- modules/config/dto/update-config.dto.ts : DTO mise à jour
- modules/config/dto/test-smtp.dto.ts : DTO test SMTP

Endpoints créés :
✅ GET /api/v1/configuration/setup/status
   → Vérifier si la configuration initiale est terminée

✅ POST /api/v1/configuration/setup/complete
   → Marquer la configuration comme terminée

✅ POST /api/v1/configuration/test-smtp
   → Tester la connexion SMTP + envoi email de test

✅ PATCH /api/v1/configuration/bulk
   → Mise à jour multiple des configurations

✅ GET /api/v1/configuration
   → Récupérer toutes les configurations (admin)

✅ GET /api/v1/configuration/:category
   → Récupérer par catégorie (email/app/security)

Fonctionnalités :
- Validation des données avec class-validator
- Test SMTP avec Nodemailer
- Envoi d'email de test HTML
- Gestion d'erreurs complète
- Rechargement automatique du cache
- Traçabilité des modifications

Sécurité :
- Guards commentés (à activer avec JWT)
- Validation des catégories
- Mots de passe masqués dans les réponses

Dépendances ajoutées :
- nodemailer ^6.9.16
- @types/nodemailer ^6.4.16

Tests effectués :
✅ GET /setup/status → {setupCompleted: false}
✅ GET /email → 8 configurations email
✅ Build Docker réussi
✅ Toutes les routes mappées correctement

Ref: #6
2025-11-28 17:00:55 +01:00
jmartin ec485b5a3e Merge pull request '[Backend] Service de configuration avec cache et encryption' (#66) from feature/5-service-configuration into master
Merge pull request #66: [Backend] Service de configuration avec cache et encryption

Implémentation du service de configuration dynamique avec cache en mémoire et chiffrement AES-256-CBC.

Closes #5
2025-11-28 15:33:20 +00:00
jmartin 80afe2fa2f feat(backend): service de configuration avec cache et encryption (#5)
Implémentation du service de configuration dynamique pour
le déploiement on-premise de l'application.

Nouveaux fichiers :
- entities/configuration.entity.ts : Entité TypeORM
- modules/config/config.service.ts : Service avec cache et encryption
- modules/config/config.module.ts : Module NestJS
- modules/config/index.ts : Export centralisé

Fonctionnalités :
✅ Cache en mémoire au démarrage (16 configurations)
✅ Chiffrement AES-256-CBC pour valeurs sensibles
✅ Conversion automatique de types (string/number/boolean/json)
✅ Méthodes get/set avec traçabilité
✅ Récupération par catégorie (email/app/security)
✅ Masquage automatique des mots de passe
✅ Support setup wizard (isSetupCompleted)

Sécurité :
- Clé de chiffrement depuis CONFIG_ENCRYPTION_KEY
- Format iv:encrypted pour AES-256-CBC
- Mots de passe masqués dans les API

Intégration :
- AppConfigModule ajouté à app.module.ts
- Service global exporté pour utilisation dans toute l'app
- Chargement automatique au démarrage (OnModuleInit)

Tests :
✅ Build Docker réussi
✅ 16 configurations chargées en cache
✅ Service démarré sans erreur

Ref: #5
2025-11-28 16:29:33 +01:00
jmartin 4149d0147f Merge pull request '[BDD] Ajout table configuration système' (#65) from feature/4-table-configuration into master
Merge pull request #65: [BDD] Ajout table configuration système

Ajout de la table configuration pour la gestion dynamique de la configuration on-premise.

Closes #4
2025-11-28 15:23:31 +00:00
jmartin 47dbe94b02 feat(bdd): ajout table configuration système (#4)
Ajout de la table configuration pour la gestion dynamique
de la configuration on-premise de l'application.

Structure :
- Table configuration (clé/valeur avec types)
- Index sur cle et categorie pour performance
- Contrainte UNIQUE sur cle
- Référence vers utilisateurs pour traçabilité

Données initiales (seed) :
- Configuration Email (SMTP) : 8 paramètres
- Configuration Application : 4 paramètres
- Configuration Sécurité : 4 paramètres

Types supportés :
- string : chaînes de caractères
- number : nombres entiers/décimaux
- boolean : true/false
- json : objets JSON
- encrypted : valeurs chiffrées AES-256

Catégories :
- email : Configuration SMTP
- app : Paramètres application
- security : Paramètres de sécurité

Base de données recréée et testée ✅
16 configurations insérées par défaut ✅

Ref: #4
2025-11-28 16:19:46 +01:00
jmartin fd4f5e6b12 Merge pull request '[BDD] Conformité CDC v1.3 - Schéma unifié' (#64) from feature/3-ajout-champs-bdd into master
Merge pull request #64: [BDD] Conformité CDC v1.3 - Schéma unifié

Modifications du schéma BDD.sql pour conformité CDC v1.3.

Closes #3
2025-11-28 15:03:43 +00:00
jmartin 40b1eb2192 feat(bdd): conformité CDC v1.3 - schéma unifié (#3)
Modifications du schéma BDD.sql :

Table utilisateurs :
- password devient NULLABLE (créé après validation via token)
- Ajout token_creation_mdp + token_creation_mdp_expire_le
- telephone unifié (suppression mobile/telephone_fixe)
- Ajout index sur token_creation_mdp

Table assistantes_maternelles :
- date_agrement devient NOT NULL (obligatoire)
- Suppression annee_experience
- Suppression specialite

Table enfants :
- genre devient NOT NULL (obligatoire H/F)

Autres modifications :
- docker-compose.yml : pointage vers BDD.sql unifié
- Suppression des anciens fichiers de migration (01-07)
- Base de données recréée et testée ✅

Ref: #3
2025-11-28 16:00:17 +01:00
jmartin 933793aad8 docs: ajout roadmap générale (Phases 1-5) et mise à jour index 2025-11-28 14:54:29 +01:00
jmartin 2285069a52 docs: ajout ligne vide finale + ignore token Gitea 2025-11-28 10:31:11 +01:00
jmartin 2e3139b5fc docs: mise à jour contrats API (backend-database et frontend-backend) 2025-11-26 14:33:46 +01:00
jmartin 93306d287b docs: mise à jour workflow création compte (corrections cohérence CDC v1.3) 2025-11-26 14:33:20 +01:00
jmartin d0827a119e docs: ajout documentation technique complète (configuration, documents légaux, tickets, décisions, backlog Phase 2) 2025-11-26 14:33:04 +01:00
Julien Martin 1bbdab03d0 suppression d'un reliquat 2025-05-12 22:47:30 +02:00
Julien Martin 7f78617561 Merge branch 'develop' of https://github.com/MonkeyJLuffy/petitspas into develop 2025-05-12 16:15:29 +02:00
Julien Martin 7707b99773 feat: harmonisation de la taille de police dans les champs de motivation (étape4 et 5) - Ajout du paramètre fontSize au CustomDecoratedTextField - Taille de police fixée à 18px pour une meilleure lisibilité 2025-05-12 16:11:12 +02:00
Julien Martin 760f4feca3 feat(ui): Amélioration des cartes de récapitulatif parents et enfants\n\n- Labes plus grands et espacement augmenté pour les cartes parents\n- Labels plus grands pour les champs enfants\n- Titre enfant intégré et centré dans la carte\n- Image enfant sans cadre blanc, occupant toute la hauteur\n- Bouton de modification bien positionné\n- Affichage des consentements en lecture seule sous la fiche enfant 2025-05-12 13:04:29 +02:00
Julien Martin 03712bd99b feat(ui): Ajout des icônes de croix rouge et grise pour la suppression des cartes enfants 2025-05-12 12:21:05 +02:00
Julien Martin 1496f7f174 refactor(inscription): Refonte complète du processus d'inscription - Modèles etdonnées: Suppression de placeholder_registration_data.dart, ajout de user_registration_data.dart, data_generator.dart et card_assets.dart - Interface utilisateur: Refonte des écrans d'inscription, amélioration des widgets, ajout de cartes colorées - Assets: Ajout de nouvelles cartes colorées - Configuration: Mise à jour de pubspec.yaml et app_router.dart 2025-05-12 12:00:49 +02:00
Julien Martin acb602643a feat: Avancée majeure parcours inscription parent et refactorisation widgets UI
Ce commit comprend plusieurs améliorations significatives :

Inscription Parent - Étape 5 (Récapitulatif) :
- Initialisation de l'écran pour l'étape 5/5 du parcours d'inscription parent.
- Mise en place de la structure de base de l'écran de récapitulatif (titre, fond, bouton de soumission initial, modale de confirmation).
- Intégration de la navigation vers l'étape 5 depuis l'étape 4, incluant le passage (actuellement factice) des données d'inscription.
- Correction des erreurs de navigation et de typage liées à l'introduction de `PlaceholderRegistrationData` pour cette nouvelle étape.

Refactorisation des Widgets UI :
- `CustomAppTextField` :
    - Évolution majeure pour supporter différents styles de fond (beige, lavande, jaune) via un nouvel enum `CustomAppTextFieldStyle`.
    - Les images de fond pour les styles lavande et jaune (`input_field_lavande.png`, `input_field_jaune.png`) ont été renommées et sont maintenant utilisées.
    - Mise à jour de l'écran de login pour utiliser ce `CustomAppTextField` stylisé, remplaçant l'ancien widget privé `_ImageTextField`.
    - Réintégration des paramètres `isRequired`, `enabled`, `readOnly`, `onTap`, et `suffixIcon` qui avaient été omis lors d'une refactorisation précédente, assurant la compatibilité avec l'étape 3.
- `ImageButton` :
    - Extraction du widget privé `_ImageButton` de l'écran de login en un widget public `ImageButton` (dans `widgets/image_button.dart`) pour une réutilisation globale.
    - Mise à jour de l'écran de login pour utiliser ce nouveau widget public.
    - Utilisation du nouveau `ImageButton` pour le bouton "Soumettre ma demande" sur l'écran de l'étape 5.

Corrections :
- Correction d'une erreur de `RenderFlex overflowed` dans la carte enfant (`_ChildCardWidget`) de l'étape 3 de l'inscription parent, en ajustant les espacements internes.
- Résolution de diverses erreurs de compilation qui sont apparues pendant ces refactorisations.
2025-05-07 17:43:07 +02:00
Julien Martin 0772f83369 feat(auth): amélioration UI et UX étape 4 inscription parent 2025-05-07 17:09:06 +02:00
Julien Martin 42d147c273 feat(auth): Amélioration UI/UX étape 3 inscription enfants
- Corrige le débordement visuel (RenderFlex overflow) dans les cartes enfants.

- Augmente les marges latérales du sélecteur d'enfants pour un meilleur centrage.

- Ajoute un défilement automatique vers la droite lors de l'ajout d'un enfant.

- Intègre une barre de défilement horizontale et un effet de fondu dynamique (fading edges) au sélecteur d'enfants.

- Ajuste le padding vertical dans CustomAppTextField pour un meilleur centrage du hintText.

- Met à jour index.html :

  - Utilise le token {{flutter_service_worker_version}}.

  - Ajoute la balise meta mobile-web-app-capable.

  - Rétablit temporairement loadEntrypoint pour éviter un écran blanc (avertissement de dépréciation en attente de correction).
2025-05-07 10:42:52 +02:00
Julien Martin df56ba11df feat(auth): Amélioration UI et logique inscription parent étape 3
- Ajout du switch "Enfant à naître" et ajustement du champ prénom.

- Amélioration de la gestion de l'affichage des photos (placeholder, kIsWeb).

- Refactorisation des boutons avec HoverReliefWidget.

- Localisation du DatePicker en français.

- Nettoyage de l'intégration (annulée) de image_cropper.

- Mise à jour de EVOLUTIONS_CDC.md.
2025-05-06 23:44:10 +02:00
Julien Martin bbdacd68aa feat(auth): Supprime l'ancien workflow d'inscription parent et ajoute les assets pour le nouveau workflow 2025-05-05 12:51:32 +02:00
Julien Martin 7f831f363e chore: mise à jour du .gitignore et nettoyage du cache 2025-05-02 21:27:29 +02:00
Julien Martin 009d42ece8 chore: mise à jour du .gitignore et nettoyage des fichiers inutiles 2025-05-02 21:24:28 +02:00
Julien Martin e6d3c41ecc refactor: suppression des fichiers de thème non utilisés 2025-05-02 20:41:00 +02:00
Julien Martin c7ac3d9ebe docs: mise à jour des règles et évolutions du CDC 2025-05-02 19:54:18 +02:00
Julien Martin c8b8ad9318 feat(login): ajout du lien 'Mot de passe oublié ?' dans l'interface de connexion\n\n- Ajout du lien dans la page de connexion\n- Mise à jour du document d'évolution avec les spécifications de récupération de compte\n- Ajustements mineurs dans l'interface 2025-05-02 19:44:52 +02:00
Julien Martin 482040ba55 fix: mise à jour des chemins de l'icône pour utiliser icon.png 2025-05-01 16:51:20 +02:00
Julien Martin 2bcb0b1e54 J'ajoute tout ce que cursor a oublié... 2025-05-01 16:43:03 +02:00
Julien Martin 30e72242a8 style(login): � Ajustement de la mise en page du formulaire de connexion - Alignement des labels et des champs - Ajustement de la taille de police 2025-05-01 16:34:23 +02:00
Julien Martin aaf7070757 feat(login): ✨ Ajoutdes champs de formulaire et du bouton de connexion - Images field_email, field_password et btn_green 2025-04-30 18:38:04 +02:00
Julien Martin f4c211e0dd feat(login): � Refote visuelle du login - Fond paper2 et image river_logo_desktop positionnée à 1/4 de la largeur restante - ✨ Séparation desktop/mobile 2025-04-30 18:26:40 +02:00
Julien Martin 9519fafe3a feat: ajout d'un sélecteur de thèmes avec trois options (P'titsPas, Pastel, Sombre) 2025-04-30 11:01:15 +02:00
Julien Martin 9321430818 feat(init): mise en place initiale de P'titsPas - Documentation: CDC complet, sécifications techniques SSS-001, charte graphique, évolutions - Backend: structure NestJS avec controllers/services/routes, config Prisma - Frontend: app Flutter avec structure MVC, thème et Firebase - Changement de nom: SuperNounou devient P'titsPas 2025-04-30 10:38:47 +02:00
164 changed files with 19216 additions and 4768 deletions
+18
View File
@@ -0,0 +1,18 @@
# Fins de ligne : toujours LF dans le dépôt (évite les conflits Linux/Windows)
* text=auto eol=lf
# Fichiers binaires : pas de conversion
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.webp binary
*.pdf binary
*.woff binary
*.woff2 binary
*.ttf binary
*.eot binary
# Scripts shell : toujours LF
*.sh text eol=lf
+5
View File
@@ -37,6 +37,10 @@ yarn-error.log*
.pub-cache/ .pub-cache/
.pub/ .pub/
/build/ /build/
**/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java
**/windows/flutter/generated_plugin_registrant.cc
**/windows/flutter/generated_plugin_registrant.h
**/windows/flutter/generated_plugins.cmake
# Coverage # Coverage
coverage/ coverage/
@@ -52,3 +56,4 @@ Xcf/**
# Release notes # Release notes
CHANGELOG.md CHANGELOG.md
Ressources/ Ressources/
.gitea-token
Binary file not shown.
+2
View File
@@ -83,3 +83,5 @@ npx prisma migrate dev --name <nom_migration>
- [openapi-generator](https://openapi-generator.tech/) - [openapi-generator](https://openapi-generator.tech/)
- [openapi-typescript](https://github.com/drwpow/openapi-typescript) - [openapi-typescript](https://github.com/drwpow/openapi-typescript)
+2
View File
@@ -23,3 +23,5 @@ npx prisma migrate deploy
- [Prisma Migrate](https://www.prisma.io/docs/concepts/components/prisma-migrate) - [Prisma Migrate](https://www.prisma.io/docs/concepts/components/prisma-migrate)
@@ -175,3 +175,5 @@ components:
bearerFormat: JWT bearerFormat: JWT
description: Token JWT obtenu via /auth/login description: Token JWT obtenu via /auth/login
+3
View File
@@ -21,3 +21,6 @@ JWT_EXPIRATION_TIME=7d
# Environnement # Environnement
NODE_ENV=development NODE_ENV=development
# Log de chaque appel API (mode debug) — mettre à true pour tracer les requêtes front
# LOG_API_REQUESTS=true
+3
View File
@@ -32,6 +32,9 @@ COPY --from=builder /app/dist ./dist
RUN addgroup -g 1001 -S nodejs RUN addgroup -g 1001 -S nodejs
RUN adduser -S nestjs -u 1001 RUN adduser -S nestjs -u 1001
# Créer le dossier uploads et donner les permissions
RUN mkdir -p /app/uploads/photos && chown -R nestjs:nodejs /app/uploads
USER nestjs USER nestjs
EXPOSE 3000 EXPOSE 3000
+4
View File
@@ -37,6 +37,8 @@
"class-validator": "^0.14.2", "class-validator": "^0.14.2",
"joi": "^18.0.0", "joi": "^18.0.0",
"mapped-types": "^0.0.1", "mapped-types": "^0.0.1",
"multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.16",
"passport-jwt": "^4.0.1", "passport-jwt": "^4.0.1",
"pg": "^8.16.3", "pg": "^8.16.3",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
@@ -53,7 +55,9 @@
"@types/bcrypt": "^6.0.0", "@types/bcrypt": "^6.0.0",
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/multer": "^1.4.12",
"@types/node": "^22.10.7", "@types/node": "^22.10.7",
"@types/nodemailer": "^6.4.16",
"@types/passport-jwt": "^4.0.1", "@types/passport-jwt": "^4.0.1",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
"eslint": "^9.18.0", "eslint": "^9.18.0",
+108
View File
@@ -0,0 +1,108 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// Modèle pour les parents
model Parent {
id String @id @default(uuid())
email String @unique
password String
firstName String
lastName String
phoneNumber String?
address String?
status AccountStatus @default(PENDING)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
children Child[]
contracts Contract[]
}
// Modèle pour les enfants
model Child {
id String @id @default(uuid())
firstName String
dateOfBirth DateTime
photoUrl String?
photoConsent Boolean @default(false)
isMultiple Boolean @default(false)
isUnborn Boolean @default(false)
parentId String
parent Parent @relation(fields: [parentId], references: [id])
contracts Contract[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// Modèle pour les contrats
model Contract {
id String @id @default(uuid())
parentId String
childId String
startDate DateTime
endDate DateTime?
status ContractStatus @default(ACTIVE)
parent Parent @relation(fields: [parentId], references: [id])
child Child @relation(fields: [childId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// Modèle pour les thèmes
model Theme {
id String @id @default(uuid())
name String @unique
primaryColor String
secondaryColor String
backgroundColor String
textColor String
isActive Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
appSettings AppSettings[]
}
// Modèle pour les paramètres de l'application
model AppSettings {
id String @id @default(uuid())
currentThemeId String
currentTheme Theme @relation(fields: [currentThemeId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([currentThemeId])
}
// Modèle pour les administrateurs
model Admin {
id String @id @default(uuid())
email String @unique
password String
firstName String
lastName String
passwordChanged Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// Enums
enum AccountStatus {
PENDING
VALIDATED
REJECTED
SUSPENDED
}
enum ContractStatus {
ACTIVE
ENDED
CANCELLED
}
+27
View File
@@ -0,0 +1,27 @@
#!/bin/bash
# Test POST /auth/register/am (ticket #90)
# Usage: ./scripts/test-register-am.sh [BASE_URL]
# Exemple: ./scripts/test-register-am.sh https://app.ptits-pas.fr/api/v1
# ./scripts/test-register-am.sh http://localhost:3000/api/v1
BASE_URL="${1:-http://localhost:3000/api/v1}"
echo "Testing POST $BASE_URL/auth/register/am"
echo "---"
curl -s -w "\n\nHTTP %{http_code}\n" -X POST "$BASE_URL/auth/register/am" \
-H "Content-Type: application/json" \
-d '{
"email": "marie.dupont.test@ptits-pas.fr",
"prenom": "Marie",
"nom": "DUPONT",
"telephone": "0612345678",
"adresse": "1 rue Test",
"code_postal": "75001",
"ville": "Paris",
"consentement_photo": true,
"nir": "123456789012345",
"numero_agrement": "AGR-2024-001",
"capacite_accueil": 4,
"acceptation_cgu": true,
"acceptation_privacy": true
}'
+4
View File
@@ -14,6 +14,8 @@ import { AuthModule } from './routes/auth/auth.module';
import { SentryGlobalFilter } from '@sentry/nestjs/setup'; import { SentryGlobalFilter } from '@sentry/nestjs/setup';
import { AllExceptionsFilter } from './common/filters/all_exceptions.filters'; import { AllExceptionsFilter } from './common/filters/all_exceptions.filters';
import { EnfantsModule } from './routes/enfants/enfants.module'; import { EnfantsModule } from './routes/enfants/enfants.module';
import { AppConfigModule } from './modules/config/config.module';
import { DocumentsLegauxModule } from './modules/documents-legaux';
@Module({ @Module({
imports: [ imports: [
@@ -49,6 +51,8 @@ import { EnfantsModule } from './routes/enfants/enfants.module';
ParentsModule, ParentsModule,
EnfantsModule, EnfantsModule,
AuthModule, AuthModule,
AppConfigModule,
DocumentsLegauxModule,
], ],
controllers: [AppController], controllers: [AppController],
providers: [ providers: [
@@ -0,0 +1,69 @@
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { Request } from 'express';
/** Clés à masquer dans les logs (corps de requête) */
const SENSITIVE_KEYS = [
'password',
'smtp_password',
'token',
'accessToken',
'refreshToken',
'secret',
];
function maskBody(body: unknown): unknown {
if (body === null || body === undefined) return body;
if (typeof body !== 'object') return body;
const out: Record<string, unknown> = {};
for (const [key, value] of Object.entries(body)) {
const lower = key.toLowerCase();
const isSensitive = SENSITIVE_KEYS.some((s) => lower.includes(s));
out[key] = isSensitive ? '***' : value;
}
return out;
}
@Injectable()
export class LogRequestInterceptor implements NestInterceptor {
private readonly enabled: boolean;
constructor() {
this.enabled =
process.env.LOG_API_REQUESTS === 'true' ||
process.env.LOG_API_REQUESTS === '1';
}
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
if (!this.enabled) return next.handle();
const http = context.switchToHttp();
const req = http.getRequest<Request>();
const { method, url, body, query } = req;
const hasBody = body && Object.keys(body).length > 0;
const logLine = [
`[API] ${method} ${url}`,
Object.keys(query || {}).length ? `query=${JSON.stringify(query)}` : '',
hasBody ? `body=${JSON.stringify(maskBody(body))}` : '',
]
.filter(Boolean)
.join(' ');
console.log(logLine);
return next.handle().pipe(
tap({
next: () => {
// Optionnel: log du statut en fin de requête (si besoin plus tard)
},
}),
);
}
}
@@ -0,0 +1,40 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
JoinColumn,
CreateDateColumn,
} from 'typeorm';
import { Users } from './users.entity';
import { DocumentLegal } from './document-legal.entity';
@Entity('acceptations_documents')
export class AcceptationDocument {
@PrimaryGeneratedColumn('uuid')
id: string;
@ManyToOne(() => Users, { nullable: false, onDelete: 'CASCADE' })
@JoinColumn({ name: 'id_utilisateur' })
utilisateur: Users;
@ManyToOne(() => DocumentLegal, { nullable: true })
@JoinColumn({ name: 'id_document' })
document: DocumentLegal | null;
@Column({ type: 'varchar', length: 50, nullable: false })
type_document: 'cgu' | 'privacy';
@Column({ type: 'integer', nullable: false })
version_document: number;
@CreateDateColumn({ name: 'accepte_le', type: 'timestamptz' })
accepteLe: Date;
@Column({ type: 'inet', nullable: true })
ip_address: string | null;
@Column({ type: 'text', nullable: true })
user_agent: string | null;
}
@@ -0,0 +1,39 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
JoinColumn,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import { Users } from './users.entity';
@Entity('configuration')
export class Configuration {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'varchar', length: 100, unique: true, nullable: false })
cle: string;
@Column({ type: 'text', nullable: true })
valeur: string | null;
@Column({ type: 'varchar', length: 50, nullable: false })
type: 'string' | 'number' | 'boolean' | 'json' | 'encrypted';
@Column({ type: 'varchar', length: 50, nullable: true })
categorie: 'email' | 'app' | 'security' | null;
@Column({ type: 'text', nullable: true })
description: string | null;
@UpdateDateColumn({ name: 'modifie_le' })
modifieLe: Date;
@ManyToOne(() => Users, { nullable: true })
@JoinColumn({ name: 'modifie_par' })
modifiePar: Users | null;
}
@@ -0,0 +1,44 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
JoinColumn,
CreateDateColumn,
} from 'typeorm';
import { Users } from './users.entity';
@Entity('documents_legaux')
export class DocumentLegal {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'varchar', length: 50, nullable: false })
type: 'cgu' | 'privacy';
@Column({ type: 'integer', nullable: false })
version: number;
@Column({ type: 'varchar', length: 255, nullable: false })
fichier_nom: string;
@Column({ type: 'varchar', length: 500, nullable: false })
fichier_path: string;
@Column({ type: 'varchar', length: 64, nullable: false })
fichier_hash: string;
@Column({ type: 'boolean', default: false })
actif: boolean;
@ManyToOne(() => Users, { nullable: true })
@JoinColumn({ name: 'televerse_par' })
televersePar: Users | null;
@CreateDateColumn({ name: 'televerse_le', type: 'timestamptz' })
televerseLe: Date;
@Column({ name: 'active_le', type: 'timestamptz', nullable: true })
activeLe: Date | null;
}
+8 -8
View File
@@ -50,8 +50,8 @@ export class Users {
@Column({ unique: true, name: 'email' }) @Column({ unique: true, name: 'email' })
email: string; email: string;
@Column({ name: 'password' }) @Column({ name: 'password', nullable: true })
password: string; password?: string;
@Column({ name: 'prenom', nullable: true }) @Column({ name: 'prenom', nullable: true })
prenom?: string; prenom?: string;
@@ -96,12 +96,6 @@ export class Users {
@Column({ nullable: true, name: 'telephone' }) @Column({ nullable: true, name: 'telephone' })
telephone?: string; telephone?: string;
@Column({ name: 'mobile', nullable: true })
mobile?: string;
@Column({ name: 'telephone_fixe', nullable: true })
telephone_fixe?: string;
@Column({ nullable: true, name: 'adresse' }) @Column({ nullable: true, name: 'adresse' })
adresse?: string; adresse?: string;
@@ -117,6 +111,12 @@ export class Users {
@Column({ default: false, name: 'changement_mdp_obligatoire' }) @Column({ default: false, name: 'changement_mdp_obligatoire' })
changement_mdp_obligatoire: boolean; changement_mdp_obligatoire: boolean;
@Column({ nullable: true, name: 'token_creation_mdp', length: 255 })
token_creation_mdp?: string;
@Column({ type: 'timestamptz', nullable: true, name: 'token_creation_mdp_expire_le' })
token_creation_mdp_expire_le?: Date;
@Column({ nullable: true, name: 'ville' }) @Column({ nullable: true, name: 'ville' })
ville?: string; ville?: string;
+13 -5
View File
@@ -1,17 +1,25 @@
import { NestFactory, Reflector } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { SwaggerModule } from '@nestjs/swagger/dist/swagger-module'; import { SwaggerModule } from '@nestjs/swagger/dist/swagger-module';
import { DocumentBuilder } from '@nestjs/swagger'; import { DocumentBuilder } from '@nestjs/swagger';
import { AuthGuard } from './common/guards/auth.guard';
import { JwtService } from '@nestjs/jwt';
import { RolesGuard } from './common/guards/roles.guard';
import { ValidationPipe } from '@nestjs/common'; import { ValidationPipe } from '@nestjs/common';
import { LogRequestInterceptor } from './common/interceptors/log-request.interceptor';
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule, const app = await NestFactory.create(AppModule,
{ logger: ['error', 'warn', 'log', 'debug', 'verbose'] }); { logger: ['error', 'warn', 'log', 'debug', 'verbose'] });
app.enableCors();
// Log de chaque appel API si LOG_API_REQUESTS=true (mode debug)
app.useGlobalInterceptors(new LogRequestInterceptor());
// Configuration CORS pour autoriser les requêtes depuis localhost (dev) et production
app.enableCors({
origin: true, // Autorise toutes les origines (dev) - à restreindre en prod
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'Accept'],
credentials: true,
});
app.useGlobalPipes( app.useGlobalPipes(
new ValidationPipe({ new ValidationPipe({
@@ -0,0 +1,231 @@
import {
Controller,
Get,
Patch,
Post,
Body,
Param,
UseGuards,
Request,
HttpStatus,
HttpException,
} from '@nestjs/common';
import { AppConfigService } from './config.service';
import { UpdateConfigDto } from './dto/update-config.dto';
import { TestSmtpDto } from './dto/test-smtp.dto';
@Controller('configuration')
export class ConfigController {
constructor(private readonly configService: AppConfigService) {}
/**
* Vérifier si la configuration initiale est terminée
* GET /api/v1/configuration/setup/status
*/
@Get('setup/status')
async getSetupStatus() {
try {
const isCompleted = this.configService.isSetupCompleted();
return {
success: true,
data: {
setupCompleted: isCompleted,
},
};
} catch (error) {
throw new HttpException(
{
success: false,
message: 'Erreur lors de la vérification du statut de configuration',
error: error.message,
},
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
/**
* Marquer la configuration initiale comme terminée
* POST /api/v1/configuration/setup/complete
*/
@Post('setup/complete')
// @UseGuards(JwtAuthGuard, RolesGuard)
// @Roles('super_admin')
async completeSetup(@Request() req: any) {
try {
const userId = req.user?.id ?? null;
await this.configService.markSetupCompleted(userId);
return {
success: true,
message: 'Configuration initiale terminée avec succès',
};
} catch (error) {
throw new HttpException(
{
success: false,
message: 'Erreur lors de la finalisation de la configuration',
error: error.message,
},
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
/**
* Test de la connexion SMTP
* POST /api/v1/configuration/test-smtp
*/
@Post('test-smtp')
// @UseGuards(JwtAuthGuard, RolesGuard)
// @Roles('super_admin')
async testSmtp(@Body() testSmtpDto: TestSmtpDto) {
try {
const result = await this.configService.testSmtpConnection(testSmtpDto.testEmail);
if (result.success) {
return {
success: true,
message: 'Connexion SMTP réussie. Email de test envoyé.',
};
} else {
return {
success: false,
message: 'Échec du test SMTP',
error: result.error,
};
}
} catch (error) {
throw new HttpException(
{
success: false,
message: 'Erreur lors du test SMTP',
error: error.message,
},
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
/**
* Mise à jour multiple des configurations
* PATCH /api/v1/configuration/bulk
*/
@Patch('bulk')
// @UseGuards(JwtAuthGuard, RolesGuard)
// @Roles('super_admin')
async updateBulk(@Body() updateConfigDto: UpdateConfigDto, @Request() req: any) {
try {
// TODO: Récupérer l'ID utilisateur depuis le JWT
const userId = req.user?.id || null;
let updated = 0;
const errors: string[] = [];
// Parcourir toutes les clés du DTO
for (const [key, value] of Object.entries(updateConfigDto)) {
if (value !== undefined) {
try {
await this.configService.set(key, value, userId);
updated++;
} catch (error) {
errors.push(`${key}: ${error.message}`);
}
}
}
// Recharger le cache après les modifications
await this.configService.loadCache();
if (errors.length > 0) {
return {
success: false,
message: 'Certaines configurations n\'ont pas pu être mises à jour',
updated,
errors,
};
}
return {
success: true,
message: 'Configuration mise à jour avec succès',
updated,
};
} catch (error) {
throw new HttpException(
{
success: false,
message: 'Erreur lors de la mise à jour des configurations',
error: error.message,
},
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
/**
* Récupérer toutes les configurations (pour l'admin)
* GET /api/v1/configuration
*/
@Get()
// @UseGuards(JwtAuthGuard, RolesGuard)
// @Roles('super_admin')
async getAll() {
try {
const configs = await this.configService.getAll();
return {
success: true,
data: configs,
};
} catch (error) {
throw new HttpException(
{
success: false,
message: 'Erreur lors de la récupération des configurations',
error: error.message,
},
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
/**
* Récupérer les configurations par catégorie
* GET /api/v1/configuration/:category
*/
@Get(':category')
// @UseGuards(JwtAuthGuard, RolesGuard)
// @Roles('super_admin')
async getByCategory(@Param('category') category: string) {
try {
if (!['email', 'app', 'security'].includes(category)) {
throw new HttpException(
{
success: false,
message: 'Catégorie invalide. Valeurs acceptées: email, app, security',
},
HttpStatus.BAD_REQUEST,
);
}
const configs = await this.configService.getByCategory(category);
return {
success: true,
data: configs,
};
} catch (error) {
if (error instanceof HttpException) {
throw error;
}
throw new HttpException(
{
success: false,
message: 'Erreur lors de la récupération des configurations',
error: error.message,
},
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Configuration } from '../../entities/configuration.entity';
import { AppConfigService } from './config.service';
import { ConfigController } from './config.controller';
@Module({
imports: [TypeOrmModule.forFeature([Configuration])],
controllers: [ConfigController],
providers: [AppConfigService],
exports: [AppConfigService],
})
export class AppConfigModule {}
@@ -0,0 +1,338 @@
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Configuration } from '../../entities/configuration.entity';
import * as crypto from 'crypto';
@Injectable()
export class AppConfigService implements OnModuleInit {
private readonly logger = new Logger(AppConfigService.name);
private cache: Map<string, any> = new Map();
private readonly ENCRYPTION_KEY: string;
private readonly ENCRYPTION_ALGORITHM = 'aes-256-cbc';
private readonly IV_LENGTH = 16;
constructor(
@InjectRepository(Configuration)
private configRepo: Repository<Configuration>,
) {
// Clé de chiffrement depuis les variables d'environnement
// En production, cette clé doit être générée de manière sécurisée
this.ENCRYPTION_KEY =
process.env.CONFIG_ENCRYPTION_KEY ||
crypto.randomBytes(32).toString('hex');
if (!process.env.CONFIG_ENCRYPTION_KEY) {
this.logger.warn(
'⚠️ CONFIG_ENCRYPTION_KEY non définie. Utilisation d\'une clé temporaire (NON RECOMMANDÉ EN PRODUCTION)',
);
}
}
/**
* Chargement du cache au démarrage de l'application
*/
async onModuleInit() {
await this.loadCache();
}
/**
* Chargement de toutes les configurations en cache
*/
async loadCache(): Promise<void> {
try {
const configs = await this.configRepo.find();
this.logger.log(`📦 Chargement de ${configs.length} configurations en cache`);
for (const config of configs) {
let value = config.valeur;
// Déchiffrement si nécessaire
if (config.type === 'encrypted' && value) {
try {
value = this.decrypt(value);
} catch (error) {
this.logger.error(
`❌ Erreur de déchiffrement pour la clé '${config.cle}'`,
error,
);
value = null;
}
}
// Conversion de type
const convertedValue = this.convertType(value, config.type);
this.cache.set(config.cle, convertedValue);
}
this.logger.log('✅ Cache de configuration chargé avec succès');
} catch (error) {
this.logger.error('❌ Erreur lors du chargement du cache', error);
throw error;
}
}
/**
* Récupération d'une valeur de configuration
* @param key Clé de configuration
* @param defaultValue Valeur par défaut si la clé n'existe pas
* @returns Valeur de configuration
*/
get<T = any>(key: string, defaultValue?: T): T {
if (this.cache.has(key)) {
return this.cache.get(key) as T;
}
if (defaultValue !== undefined) {
return defaultValue;
}
this.logger.warn(`⚠️ Configuration '${key}' non trouvée et aucune valeur par défaut fournie`);
return undefined as T;
}
/**
* Mise à jour d'une valeur de configuration
* @param key Clé de configuration
* @param value Nouvelle valeur
* @param userId ID de l'utilisateur qui modifie
*/
async set(key: string, value: any, userId?: string): Promise<void> {
const config = await this.configRepo.findOne({ where: { cle: key } });
if (!config) {
throw new Error(`Configuration '${key}' non trouvée`);
}
let valueToStore = value !== null && value !== undefined ? String(value) : null;
// Chiffrement si nécessaire
if (config.type === 'encrypted' && valueToStore) {
valueToStore = this.encrypt(valueToStore);
}
config.valeur = valueToStore;
config.modifieLe = new Date();
if (userId) {
config.modifiePar = { id: userId } as any;
}
await this.configRepo.save(config);
// Mise à jour du cache (avec la valeur déchiffrée)
this.cache.set(key, value);
this.logger.log(`✅ Configuration '${key}' mise à jour`);
}
/**
* Récupération de toutes les configurations par catégorie
* @param category Catégorie de configuration
* @returns Objet clé/valeur des configurations
*/
async getByCategory(category: string): Promise<Record<string, any>> {
const configs = await this.configRepo.find({
where: { categorie: category as any },
});
const result: Record<string, any> = {};
for (const config of configs) {
let value = config.valeur;
// Masquer les mots de passe
if (config.type === 'encrypted') {
value = value ? '***********' : null;
} else {
value = this.convertType(value, config.type);
}
result[config.cle] = {
value,
type: config.type,
description: config.description,
};
}
return result;
}
/**
* Récupération de toutes les configurations (pour l'admin)
* @returns Liste de toutes les configurations
*/
async getAll(): Promise<Configuration[]> {
const configs = await this.configRepo.find({
order: { categorie: 'ASC', cle: 'ASC' },
});
// Masquer les valeurs chiffrées
return configs.map((config) => ({
...config,
valeur: config.type === 'encrypted' && config.valeur ? '***********' : config.valeur,
}));
}
/**
* Test de la configuration SMTP
* @param testEmail Email de destination pour le test
* @returns Objet avec success et error éventuel
*/
async testSmtpConnection(testEmail?: string): Promise<{ success: boolean; error?: string }> {
try {
this.logger.log('🧪 Test de connexion SMTP...');
// Récupération de la configuration SMTP
const smtpHost = this.get<string>('smtp_host');
const smtpPort = this.get<number>('smtp_port');
const smtpSecure = this.get<boolean>('smtp_secure');
const smtpAuthRequired = this.get<boolean>('smtp_auth_required');
const smtpUser = this.get<string>('smtp_user');
const smtpPassword = this.get<string>('smtp_password');
const emailFromName = this.get<string>('email_from_name');
const emailFromAddress = this.get<string>('email_from_address');
// Import dynamique de nodemailer
const nodemailer = await import('nodemailer');
// Configuration du transporteur
const transportConfig: any = {
host: smtpHost,
port: smtpPort,
secure: smtpSecure,
};
if (smtpAuthRequired && smtpUser && smtpPassword) {
transportConfig.auth = {
user: smtpUser,
pass: smtpPassword,
};
}
const transporter = nodemailer.createTransport(transportConfig);
// Vérification de la connexion
await transporter.verify();
this.logger.log('✅ Connexion SMTP vérifiée');
// Si un email de test est fourni, on envoie un email
if (testEmail) {
await transporter.sendMail({
from: `"${emailFromName}" <${emailFromAddress}>`,
to: testEmail,
subject: '🧪 Test de configuration SMTP - P\'titsPas',
text: 'Ceci est un email de test pour vérifier la configuration SMTP de votre application P\'titsPas.',
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #4CAF50;">✅ Test de configuration SMTP réussi !</h2>
<p>Ceci est un email de test pour vérifier la configuration SMTP de votre application <strong>P'titsPas</strong>.</p>
<p>Si vous recevez cet email, cela signifie que votre configuration SMTP fonctionne correctement.</p>
<hr style="border: 1px solid #eee; margin: 20px 0;">
<p style="color: #666; font-size: 12px;">
Cet email a été envoyé automatiquement depuis votre application P'titsPas.<br>
Configuration testée le ${new Date().toLocaleString('fr-FR')}
</p>
</div>
`,
});
this.logger.log(`📧 Email de test envoyé à ${testEmail}`);
}
return { success: true };
} catch (error) {
this.logger.error('❌ Échec du test SMTP', error);
return {
success: false,
error: error.message || 'Erreur inconnue lors du test SMTP',
};
}
}
/**
* Vérification si la configuration initiale est terminée
* @returns true si la configuration est terminée
*/
isSetupCompleted(): boolean {
return this.get<boolean>('setup_completed', false);
}
/**
* Marquer la configuration initiale comme terminée
* @param userId ID de l'utilisateur qui termine la configuration (null si non authentifié)
*/
async markSetupCompleted(userId: string | null): Promise<void> {
await this.set('setup_completed', 'true', userId ?? undefined);
this.logger.log('✅ Configuration initiale marquée comme terminée');
}
/**
* Conversion de type selon le type de configuration
* @param value Valeur à convertir
* @param type Type cible
* @returns Valeur convertie
*/
private convertType(value: string | null, type: string): any {
if (value === null || value === undefined) {
return null;
}
switch (type) {
case 'number':
return parseFloat(value);
case 'boolean':
return value === 'true' || value === '1';
case 'json':
try {
return JSON.parse(value);
} catch {
return null;
}
case 'string':
case 'encrypted':
default:
return value;
}
}
/**
* Chiffrement AES-256-CBC
* @param text Texte à chiffrer
* @returns Texte chiffré (format: iv:encrypted)
*/
private encrypt(text: string): string {
const iv = crypto.randomBytes(this.IV_LENGTH);
const key = Buffer.from(this.ENCRYPTION_KEY, 'hex');
const cipher = crypto.createCipheriv(this.ENCRYPTION_ALGORITHM, key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
// Format: iv:encrypted
return `${iv.toString('hex')}:${encrypted}`;
}
/**
* Déchiffrement AES-256-CBC
* @param encryptedText Texte chiffré (format: iv:encrypted)
* @returns Texte déchiffré
*/
private decrypt(encryptedText: string): string {
const parts = encryptedText.split(':');
if (parts.length !== 2) {
throw new Error('Format de chiffrement invalide');
}
const iv = Buffer.from(parts[0], 'hex');
const encrypted = parts[1];
const key = Buffer.from(this.ENCRYPTION_KEY, 'hex');
const decipher = crypto.createDecipheriv(this.ENCRYPTION_ALGORITHM, key, iv);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
}
@@ -0,0 +1,7 @@
import { IsEmail } from 'class-validator';
export class TestSmtpDto {
@IsEmail()
testEmail: string;
}
@@ -0,0 +1,67 @@
import { IsString, IsOptional, IsNumber, IsBoolean, IsEmail, IsUrl } from 'class-validator';
export class UpdateConfigDto {
// Configuration Email (SMTP)
@IsOptional()
@IsString()
smtp_host?: string;
@IsOptional()
@IsNumber()
smtp_port?: number;
@IsOptional()
@IsBoolean()
smtp_secure?: boolean;
@IsOptional()
@IsBoolean()
smtp_auth_required?: boolean;
@IsOptional()
@IsString()
smtp_user?: string;
@IsOptional()
@IsString()
smtp_password?: string;
@IsOptional()
@IsString()
email_from_name?: string;
@IsOptional()
@IsEmail()
email_from_address?: string;
// Configuration Application
@IsOptional()
@IsString()
app_name?: string;
@IsOptional()
@IsUrl()
app_url?: string;
@IsOptional()
@IsString()
app_logo_url?: string;
// Configuration Sécurité
@IsOptional()
@IsNumber()
password_reset_token_expiry_days?: number;
@IsOptional()
@IsNumber()
jwt_expiry_hours?: number;
@IsOptional()
@IsNumber()
max_upload_size_mb?: number;
@IsOptional()
@IsNumber()
bcrypt_rounds?: number;
}
+3
View File
@@ -0,0 +1,3 @@
export * from './config.module';
export * from './config.service';
@@ -0,0 +1,202 @@
import {
Controller,
Get,
Post,
Patch,
Param,
Body,
UseInterceptors,
UploadedFile,
Res,
HttpStatus,
BadRequestException,
ParseUUIDPipe,
StreamableFile,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import type { Response } from 'express';
import { DocumentsLegauxService } from './documents-legaux.service';
import { UploadDocumentDto } from './dto/upload-document.dto';
import { DocumentsActifsResponseDto } from './dto/documents-actifs.dto';
import { DocumentVersionDto } from './dto/document-version.dto';
@Controller('documents-legaux')
export class DocumentsLegauxController {
constructor(private readonly documentsService: DocumentsLegauxService) {}
/**
* GET /api/v1/documents-legaux/actifs
* Récupérer les documents actifs (CGU + Privacy)
* PUBLIC
*/
@Get('actifs')
async getDocumentsActifs(): Promise<DocumentsActifsResponseDto> {
const { cgu, privacy } = await this.documentsService.getDocumentsActifs();
return {
cgu: {
id: cgu.id,
type: cgu.type,
version: cgu.version,
url: `/api/v1/documents-legaux/${cgu.id}/download`,
activeLe: cgu.activeLe,
},
privacy: {
id: privacy.id,
type: privacy.type,
version: privacy.version,
url: `/api/v1/documents-legaux/${privacy.id}/download`,
activeLe: privacy.activeLe,
},
};
}
/**
* GET /api/v1/documents-legaux/:type/versions
* Lister toutes les versions d'un type de document
* ADMIN ONLY
*/
@Get(':type/versions')
// @UseGuards(JwtAuthGuard, RolesGuard)
// @Roles('super_admin', 'administrateur')
async listerVersions(@Param('type') type: string): Promise<DocumentVersionDto[]> {
if (type !== 'cgu' && type !== 'privacy') {
throw new BadRequestException('Le type doit être "cgu" ou "privacy"');
}
const documents = await this.documentsService.listerVersions(type as 'cgu' | 'privacy');
return documents.map((doc) => ({
id: doc.id,
version: doc.version,
fichier_nom: doc.fichier_nom,
actif: doc.actif,
televersePar: doc.televersePar
? {
id: doc.televersePar.id,
prenom: doc.televersePar.prenom,
nom: doc.televersePar.nom,
}
: null,
televerseLe: doc.televerseLe,
activeLe: doc.activeLe,
}));
}
/**
* POST /api/v1/documents-legaux
* Upload une nouvelle version d'un document
* ADMIN ONLY
*/
@Post()
@UseInterceptors(FileInterceptor('file'))
// @UseGuards(JwtAuthGuard, RolesGuard)
// @Roles('super_admin', 'administrateur')
async uploadDocument(
@Body() uploadDto: UploadDocumentDto,
@UploadedFile() file: Express.Multer.File,
// @CurrentUser() user: any, // TODO: Décommenter quand le guard sera implémenté
) {
if (!file) {
throw new BadRequestException('Aucun fichier fourni');
}
// TODO: Récupérer l'ID utilisateur depuis le guard
const userId = '00000000-0000-0000-0000-000000000000'; // Temporaire
const document = await this.documentsService.uploadNouvelleVersion(
uploadDto.type,
file,
userId,
);
return {
id: document.id,
type: document.type,
version: document.version,
fichier_nom: document.fichier_nom,
actif: document.actif,
televerseLe: document.televerseLe,
};
}
/**
* PATCH /api/v1/documents-legaux/:id/activer
* Activer une version d'un document
* ADMIN ONLY
*/
@Patch(':id/activer')
// @UseGuards(JwtAuthGuard, RolesGuard)
// @Roles('super_admin', 'administrateur')
async activerVersion(@Param('id', ParseUUIDPipe) documentId: string) {
await this.documentsService.activerVersion(documentId);
// Récupérer le document pour retourner les infos
const documents = await this.documentsService.listerVersions('cgu');
const document = documents.find((d) => d.id === documentId);
if (!document) {
const documentsPrivacy = await this.documentsService.listerVersions('privacy');
const docPrivacy = documentsPrivacy.find((d) => d.id === documentId);
if (!docPrivacy) {
throw new BadRequestException('Document non trouvé');
}
return {
message: 'Document activé avec succès',
documentId: docPrivacy.id,
type: docPrivacy.type,
version: docPrivacy.version,
};
}
return {
message: 'Document activé avec succès',
documentId: document.id,
type: document.type,
version: document.version,
};
}
/**
* GET /api/v1/documents-legaux/:id/download
* Télécharger un document
* PUBLIC
*/
@Get(':id/download')
async telechargerDocument(
@Param('id', ParseUUIDPipe) documentId: string,
@Res() res: Response,
) {
const { stream, filename } = await this.documentsService.telechargerDocument(documentId);
res.set({
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="${filename}"`,
});
res.status(HttpStatus.OK).send(stream);
}
/**
* GET /api/v1/documents-legaux/:id/verifier-integrite
* Vérifier l'intégrité d'un document (hash SHA-256)
* ADMIN ONLY
*/
@Get(':id/verifier-integrite')
// @UseGuards(JwtAuthGuard, RolesGuard)
// @Roles('super_admin', 'administrateur')
async verifierIntegrite(@Param('id', ParseUUIDPipe) documentId: string) {
const integre = await this.documentsService.verifierIntegrite(documentId);
return {
documentId,
integre,
message: integre
? 'Le document est intègre (hash valide)'
: 'ALERTE : Le document a été modifié (hash invalide)',
};
}
}
@@ -0,0 +1,15 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DocumentLegal } from '../../entities/document-legal.entity';
import { AcceptationDocument } from '../../entities/acceptation-document.entity';
import { DocumentsLegauxService } from './documents-legaux.service';
import { DocumentsLegauxController } from './documents-legaux.controller';
@Module({
imports: [TypeOrmModule.forFeature([DocumentLegal, AcceptationDocument])],
providers: [DocumentsLegauxService],
controllers: [DocumentsLegauxController],
exports: [DocumentsLegauxService],
})
export class DocumentsLegauxModule {}
@@ -0,0 +1,209 @@
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { DocumentLegal } from '../../entities/document-legal.entity';
import { AcceptationDocument } from '../../entities/acceptation-document.entity';
import * as crypto from 'crypto';
import * as fs from 'fs/promises';
import * as path from 'path';
@Injectable()
export class DocumentsLegauxService {
private readonly UPLOAD_DIR = '/app/documents/legaux';
constructor(
@InjectRepository(DocumentLegal)
private docRepo: Repository<DocumentLegal>,
@InjectRepository(AcceptationDocument)
private acceptationRepo: Repository<AcceptationDocument>,
) {}
/**
* Récupérer les documents actifs (CGU + Privacy)
*/
async getDocumentsActifs(): Promise<{ cgu: DocumentLegal; privacy: DocumentLegal }> {
const cgu = await this.docRepo.findOne({
where: { type: 'cgu', actif: true },
});
const privacy = await this.docRepo.findOne({
where: { type: 'privacy', actif: true },
});
if (!cgu || !privacy) {
throw new NotFoundException('Documents légaux manquants');
}
return { cgu, privacy };
}
/**
* Uploader une nouvelle version d'un document
*/
async uploadNouvelleVersion(
type: 'cgu' | 'privacy',
file: Express.Multer.File,
userId: string,
): Promise<DocumentLegal> {
// Validation du type de fichier
if (file.mimetype !== 'application/pdf') {
throw new BadRequestException('Seuls les fichiers PDF sont acceptés');
}
// Validation de la taille (max 10MB)
const maxSize = 10 * 1024 * 1024; // 10MB
if (file.size > maxSize) {
throw new BadRequestException('Le fichier ne doit pas dépasser 10MB');
}
// 1. Calculer la prochaine version
const lastDoc = await this.docRepo.findOne({
where: { type },
order: { version: 'DESC' },
});
const nouvelleVersion = (lastDoc?.version || 0) + 1;
// 2. Calculer le hash SHA-256 du fichier
const fileBuffer = file.buffer;
const hash = crypto.createHash('sha256').update(fileBuffer).digest('hex');
// 3. Générer le nom de fichier unique
const timestamp = Date.now();
const fileName = `${type}_v${nouvelleVersion}_${timestamp}.pdf`;
const filePath = path.join(this.UPLOAD_DIR, fileName);
// 4. Créer le répertoire si nécessaire et sauvegarder le fichier
await fs.mkdir(this.UPLOAD_DIR, { recursive: true });
await fs.writeFile(filePath, fileBuffer);
// 5. Créer l'entrée en BDD
const document = this.docRepo.create({
type,
version: nouvelleVersion,
fichier_nom: file.originalname,
fichier_path: filePath,
fichier_hash: hash,
actif: false, // Pas actif par défaut
televersePar: { id: userId } as any,
televerseLe: new Date(),
});
return await this.docRepo.save(document);
}
/**
* Activer une version (désactive automatiquement l'ancienne)
*/
async activerVersion(documentId: string): Promise<void> {
const document = await this.docRepo.findOne({ where: { id: documentId } });
if (!document) {
throw new NotFoundException('Document non trouvé');
}
// Transaction : désactiver l'ancienne version, activer la nouvelle
await this.docRepo.manager.transaction(async (manager) => {
// Désactiver toutes les versions de ce type
await manager.update(
DocumentLegal,
{ type: document.type, actif: true },
{ actif: false },
);
// Activer la nouvelle version
await manager.update(
DocumentLegal,
{ id: documentId },
{ actif: true, activeLe: new Date() },
);
});
}
/**
* Lister toutes les versions d'un type de document
*/
async listerVersions(type: 'cgu' | 'privacy'): Promise<DocumentLegal[]> {
return await this.docRepo.find({
where: { type },
order: { version: 'DESC' },
relations: ['televersePar'],
});
}
/**
* Télécharger un document (retourne le buffer et le nom)
*/
async telechargerDocument(documentId: string): Promise<{ stream: Buffer; filename: string }> {
const document = await this.docRepo.findOne({ where: { id: documentId } });
if (!document) {
throw new NotFoundException('Document non trouvé');
}
try {
const fileBuffer = await fs.readFile(document.fichier_path);
return {
stream: fileBuffer,
filename: document.fichier_nom,
};
} catch (error) {
throw new NotFoundException('Fichier introuvable sur le système de fichiers');
}
}
/**
* Vérifier l'intégrité d'un document (hash SHA-256)
*/
async verifierIntegrite(documentId: string): Promise<boolean> {
const document = await this.docRepo.findOne({ where: { id: documentId } });
if (!document) {
throw new NotFoundException('Document non trouvé');
}
try {
const fileBuffer = await fs.readFile(document.fichier_path);
const hash = crypto.createHash('sha256').update(fileBuffer).digest('hex');
return hash === document.fichier_hash;
} catch (error) {
return false;
}
}
/**
* Enregistrer une acceptation de document (lors de l'inscription)
*/
async enregistrerAcceptation(
userId: string,
documentId: string,
typeDocument: 'cgu' | 'privacy',
versionDocument: number,
ipAddress: string | null,
userAgent: string | null,
): Promise<AcceptationDocument> {
const acceptation = this.acceptationRepo.create({
utilisateur: { id: userId } as any,
document: { id: documentId } as any,
type_document: typeDocument,
version_document: versionDocument,
ip_address: ipAddress,
user_agent: userAgent,
});
return await this.acceptationRepo.save(acceptation);
}
/**
* Récupérer l'historique des acceptations d'un utilisateur
*/
async getAcceptationsUtilisateur(userId: string): Promise<AcceptationDocument[]> {
return await this.acceptationRepo.find({
where: { utilisateur: { id: userId } },
order: { accepteLe: 'DESC' },
relations: ['document'],
});
}
}
@@ -0,0 +1,14 @@
export class DocumentVersionDto {
id: string;
version: number;
fichier_nom: string;
actif: boolean;
televersePar: {
id: string;
prenom?: string;
nom?: string;
} | null;
televerseLe: Date;
activeLe: Date | null;
}
@@ -0,0 +1,13 @@
export class DocumentActifDto {
id: string;
type: 'cgu' | 'privacy';
version: number;
url: string;
activeLe: Date | null;
}
export class DocumentsActifsResponseDto {
cgu: DocumentActifDto;
privacy: DocumentActifDto;
}
@@ -0,0 +1,8 @@
import { IsEnum, IsNotEmpty } from 'class-validator';
export class UploadDocumentDto {
@IsEnum(['cgu', 'privacy'], { message: 'Le type doit être "cgu" ou "privacy"' })
@IsNotEmpty({ message: 'Le type est requis' })
type: 'cgu' | 'privacy';
}
@@ -0,0 +1,3 @@
export * from './documents-legaux.module';
export * from './documents-legaux.service';
@@ -35,7 +35,7 @@ export class AssistantesMaternellesController {
return this.assistantesMaternellesService.create(dto); return this.assistantesMaternellesService.create(dto);
} }
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE) @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
@Get() @Get()
@ApiOperation({ summary: 'Récupérer la liste des nounous' }) @ApiOperation({ summary: 'Récupérer la liste des nounous' })
@ApiResponse({ status: 200, description: 'Liste des nounous' }) @ApiResponse({ status: 200, description: 'Liste des nounous' })
+59 -2
View File
@@ -1,8 +1,11 @@
import { Body, Controller, Get, Post, Req, UnauthorizedException, UseGuards } from '@nestjs/common'; import { Body, Controller, Get, Post, Req, UnauthorizedException, BadRequestException, UseGuards } from '@nestjs/common';
import { LoginDto } from './dto/login.dto'; import { LoginDto } from './dto/login.dto';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { Public } from 'src/common/decorators/public.decorator'; import { Public } from 'src/common/decorators/public.decorator';
import { RegisterDto } from './dto/register.dto'; import { RegisterDto } from './dto/register.dto';
import { RegisterParentCompletDto } from './dto/register-parent-complet.dto';
import { RegisterAMCompletDto } from './dto/register-am-complet.dto';
import { ChangePasswordRequiredDto } from './dto/change-password.dto';
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { AuthGuard } from 'src/common/guards/auth.guard'; import { AuthGuard } from 'src/common/guards/auth.guard';
import type { Request } from 'express'; import type { Request } from 'express';
@@ -30,11 +33,38 @@ export class AuthController {
@Public() @Public()
@Post('register') @Post('register')
@ApiOperation({ summary: 'Inscription' }) @ApiOperation({ summary: 'Inscription (OBSOLÈTE - utiliser /register/parent)' })
@ApiResponse({ status: 409, description: 'Email déjà utilisé' })
async register(@Body() dto: RegisterDto) { async register(@Body() dto: RegisterDto) {
return this.authService.register(dto); return this.authService.register(dto);
} }
@Public()
@Post('register/parent')
@ApiOperation({
summary: 'Inscription Parent COMPLÈTE - Workflow 6 étapes',
description: 'Crée Parent 1 + Parent 2 (opt) + Enfants + Présentation + CGU en une transaction'
})
@ApiResponse({ status: 201, description: 'Inscription réussie - Dossier en attente de validation' })
@ApiResponse({ status: 400, description: 'Données invalides ou CGU non acceptées' })
@ApiResponse({ status: 409, description: 'Email déjà utilisé' })
async inscrireParentComplet(@Body() dto: RegisterParentCompletDto) {
return this.authService.inscrireParentComplet(dto);
}
@Public()
@Post('register/am')
@ApiOperation({
summary: 'Inscription Assistante Maternelle COMPLÈTE',
description: 'Crée User AM + entrée assistantes_maternelles (identité + infos pro + photo + CGU) en une transaction',
})
@ApiResponse({ status: 201, description: 'Inscription réussie - Dossier en attente de validation' })
@ApiResponse({ status: 400, description: 'Données invalides ou CGU non acceptées' })
@ApiResponse({ status: 409, description: 'Email déjà utilisé' })
async inscrireAMComplet(@Body() dto: RegisterAMCompletDto) {
return this.authService.inscrireAMComplet(dto);
}
@Public() @Public()
@Post('refresh') @Post('refresh')
@ApiBearerAuth('refresh_token') @ApiBearerAuth('refresh_token')
@@ -62,6 +92,7 @@ export class AuthController {
prenom: user.prenom ?? '', prenom: user.prenom ?? '',
nom: user.nom ?? '', nom: user.nom ?? '',
statut: user.statut, statut: user.statut,
changement_mdp_obligatoire: user.changement_mdp_obligatoire,
}; };
} }
@@ -71,5 +102,31 @@ export class AuthController {
logout(@User() currentUser: Users) { logout(@User() currentUser: Users) {
return this.authService.logout(currentUser.id); return this.authService.logout(currentUser.id);
} }
@Post('change-password-required')
@UseGuards(AuthGuard)
@ApiBearerAuth('access-token')
@ApiOperation({
summary: 'Changement de mot de passe obligatoire',
description: 'Permet de changer le mot de passe lors de la première connexion (flag changement_mdp_obligatoire)'
})
@ApiResponse({ status: 200, description: 'Mot de passe changé avec succès' })
@ApiResponse({ status: 400, description: 'Mot de passe actuel incorrect ou confirmation non correspondante' })
@ApiResponse({ status: 403, description: 'Changement de mot de passe non requis pour cet utilisateur' })
async changePasswordRequired(
@User() currentUser: Users,
@Body() dto: ChangePasswordRequiredDto,
) {
// Vérifier que les mots de passe correspondent
if (dto.nouveau_mot_de_passe !== dto.confirmation_mot_de_passe) {
throw new BadRequestException('Les mots de passe ne correspondent pas');
}
return this.authService.changePasswordRequired(
currentUser.id,
dto.mot_de_passe_actuel,
dto.nouveau_mot_de_passe,
);
}
} }
+8
View File
@@ -1,13 +1,21 @@
import { forwardRef, Module } from '@nestjs/common'; import { forwardRef, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthController } from './auth.controller'; import { AuthController } from './auth.controller';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { UserModule } from '../user/user.module'; import { UserModule } from '../user/user.module';
import { JwtModule } from '@nestjs/jwt'; import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config'; import { ConfigModule, ConfigService } from '@nestjs/config';
import { Users } from 'src/entities/users.entity';
import { Parents } from 'src/entities/parents.entity';
import { Children } from 'src/entities/children.entity';
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
import { AppConfigModule } from 'src/modules/config';
@Module({ @Module({
imports: [ imports: [
TypeOrmModule.forFeature([Users, Parents, Children, AssistanteMaternelle]),
forwardRef(() => UserModule), forwardRef(() => UserModule),
AppConfigModule,
JwtModule.registerAsync({ JwtModule.registerAsync({
imports: [ConfigModule], imports: [ConfigModule],
useFactory: (config: ConfigService) => ({ useFactory: (config: ConfigService) => ({
+359 -23
View File
@@ -2,14 +2,27 @@ import {
ConflictException, ConflictException,
Injectable, Injectable,
UnauthorizedException, UnauthorizedException,
BadRequestException,
} from '@nestjs/common'; } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UserService } from '../user/user.service'; import { UserService } from '../user/user.service';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt'; import * as bcrypt from 'bcrypt';
import * as crypto from 'crypto';
import * as fs from 'fs/promises';
import * as path from 'path';
import { RegisterDto } from './dto/register.dto'; import { RegisterDto } from './dto/register.dto';
import { RegisterParentCompletDto } from './dto/register-parent-complet.dto';
import { RegisterAMCompletDto } from './dto/register-am-complet.dto';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { RoleType, StatutUtilisateurType, Users } from 'src/entities/users.entity'; import { RoleType, StatutUtilisateurType, Users } from 'src/entities/users.entity';
import { Parents } from 'src/entities/parents.entity';
import { Children, StatutEnfantType } from 'src/entities/children.entity';
import { ParentsChildren } from 'src/entities/parents_children.entity';
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
import { LoginDto } from './dto/login.dto'; import { LoginDto } from './dto/login.dto';
import { AppConfigService } from 'src/modules/config/config.service';
@Injectable() @Injectable()
export class AuthService { export class AuthService {
@@ -17,6 +30,13 @@ export class AuthService {
private readonly usersService: UserService, private readonly usersService: UserService,
private readonly jwtService: JwtService, private readonly jwtService: JwtService,
private readonly configService: ConfigService, private readonly configService: ConfigService,
private readonly appConfigService: AppConfigService,
@InjectRepository(Parents)
private readonly parentsRepo: Repository<Parents>,
@InjectRepository(Users)
private readonly usersRepo: Repository<Users>,
@InjectRepository(Children)
private readonly childrenRepo: Repository<Children>,
) { } ) { }
/** /**
@@ -43,29 +63,37 @@ export class AuthService {
* Connexion utilisateur * Connexion utilisateur
*/ */
async login(dto: LoginDto) { async login(dto: LoginDto) {
try { const user = await this.usersService.findByEmailOrNull(dto.email);
const user = await this.usersService.findByEmailOrNull(dto.email);
if (!user) { if (!user) {
throw new UnauthorizedException('Email invalide');
}
console.log("Tentative login:", dto.email, JSON.stringify(dto.password));
console.log("Utilisateur trouvé:", user.email, user.password);
const isMatch = await bcrypt.compare(dto.password, user.password);
console.log("Résultat bcrypt.compare:", isMatch);
if (!isMatch) {
throw new UnauthorizedException('Mot de passe invalide');
}
// if (user.password !== dto.password) {
// throw new UnauthorizedException('Mot de passe invalide');
// }
return this.generateTokens(user.id, user.email, user.role);
} catch (error) {
console.error('Erreur de connexion :', error);
throw new UnauthorizedException('Identifiants invalides'); throw new UnauthorizedException('Identifiants invalides');
} }
// Vérifier que le mot de passe existe (compte activé)
if (!user.password) {
throw new UnauthorizedException(
'Compte non activé. Veuillez créer votre mot de passe via le lien reçu par email.',
);
}
// Vérifier le mot de passe
const isMatch = await bcrypt.compare(dto.password, user.password);
if (!isMatch) {
throw new UnauthorizedException('Identifiants invalides');
}
// Vérifier le statut du compte
if (user.statut === StatutUtilisateurType.EN_ATTENTE) {
throw new UnauthorizedException(
'Votre compte est en attente de validation par un gestionnaire.',
);
}
if (user.statut === StatutUtilisateurType.SUSPENDU) {
throw new UnauthorizedException('Votre compte a été suspendu. Contactez un administrateur.');
}
return this.generateTokens(user.id, user.email, user.role);
} }
/** /**
@@ -89,7 +117,8 @@ export class AuthService {
} }
/** /**
* Inscription utilisateur lambda (parent ou assistante maternelle) * Inscription utilisateur OBSOLÈTE - Utiliser inscrireParentComplet() ou registerAM()
* @deprecated
*/ */
async register(registerDto: RegisterDto) { async register(registerDto: RegisterDto) {
const exists = await this.usersService.findByEmailOrNull(registerDto.email); const exists = await this.usersService.findByEmailOrNull(registerDto.email);
@@ -129,9 +158,316 @@ export class AuthService {
}; };
} }
/**
* Inscription Parent COMPLÈTE - Workflow CDC 6 étapes en 1 transaction
* Gère : Parent 1 + Parent 2 (opt) + Enfants + Présentation + CGU
*/
async inscrireParentComplet(dto: RegisterParentCompletDto) {
if (!dto.acceptation_cgu || !dto.acceptation_privacy) {
throw new BadRequestException('L\'acceptation des CGU et de la politique de confidentialité est obligatoire');
}
if (!dto.enfants || dto.enfants.length === 0) {
throw new BadRequestException('Au moins un enfant est requis');
}
const existe = await this.usersService.findByEmailOrNull(dto.email);
if (existe) {
throw new ConflictException('Un compte avec cet email existe déjà');
}
if (dto.co_parent_email) {
const coParentExiste = await this.usersService.findByEmailOrNull(dto.co_parent_email);
if (coParentExiste) {
throw new ConflictException('L\'email du co-parent est déjà utilisé');
}
}
const joursExpirationToken = await this.appConfigService.get<number>(
'password_reset_token_expiry_days',
7,
);
const tokenCreationMdp = crypto.randomUUID();
const dateExpiration = new Date();
dateExpiration.setDate(dateExpiration.getDate() + joursExpirationToken);
const resultat = await this.usersRepo.manager.transaction(async (manager) => {
const parent1 = manager.create(Users, {
email: dto.email,
prenom: dto.prenom,
nom: dto.nom,
role: RoleType.PARENT,
statut: StatutUtilisateurType.EN_ATTENTE,
telephone: dto.telephone,
adresse: dto.adresse,
code_postal: dto.code_postal,
ville: dto.ville,
token_creation_mdp: tokenCreationMdp,
token_creation_mdp_expire_le: dateExpiration,
});
const parent1Enregistre = await manager.save(Users, parent1);
let parent2Enregistre: Users | null = null;
let tokenCoParent: string | null = null;
if (dto.co_parent_email && dto.co_parent_prenom && dto.co_parent_nom) {
tokenCoParent = crypto.randomUUID();
const dateExpirationCoParent = new Date();
dateExpirationCoParent.setDate(dateExpirationCoParent.getDate() + joursExpirationToken);
const parent2 = manager.create(Users, {
email: dto.co_parent_email,
prenom: dto.co_parent_prenom,
nom: dto.co_parent_nom,
role: RoleType.PARENT,
statut: StatutUtilisateurType.EN_ATTENTE,
telephone: dto.co_parent_telephone,
adresse: dto.co_parent_meme_adresse ? dto.adresse : dto.co_parent_adresse,
code_postal: dto.co_parent_meme_adresse ? dto.code_postal : dto.co_parent_code_postal,
ville: dto.co_parent_meme_adresse ? dto.ville : dto.co_parent_ville,
token_creation_mdp: tokenCoParent,
token_creation_mdp_expire_le: dateExpirationCoParent,
});
parent2Enregistre = await manager.save(Users, parent2);
}
const entiteParent = manager.create(Parents, {
user_id: parent1Enregistre.id,
});
entiteParent.user = parent1Enregistre;
if (parent2Enregistre) {
entiteParent.co_parent = parent2Enregistre;
}
await manager.save(Parents, entiteParent);
if (parent2Enregistre) {
const entiteCoParent = manager.create(Parents, {
user_id: parent2Enregistre.id,
});
entiteCoParent.user = parent2Enregistre;
entiteCoParent.co_parent = parent1Enregistre;
await manager.save(Parents, entiteCoParent);
}
const enfantsEnregistres: Children[] = [];
for (const enfantDto of dto.enfants) {
let urlPhoto: string | null = null;
if (enfantDto.photo_base64 && enfantDto.photo_filename) {
urlPhoto = await this.sauvegarderPhotoDepuisBase64(
enfantDto.photo_base64,
enfantDto.photo_filename,
);
}
const enfant = new Children();
enfant.first_name = enfantDto.prenom;
enfant.last_name = enfantDto.nom || dto.nom;
enfant.gender = enfantDto.genre;
enfant.birth_date = enfantDto.date_naissance ? new Date(enfantDto.date_naissance) : undefined;
enfant.due_date = enfantDto.date_previsionnelle_naissance
? new Date(enfantDto.date_previsionnelle_naissance)
: undefined;
enfant.photo_url = urlPhoto || undefined;
enfant.status = enfantDto.date_naissance ? StatutEnfantType.ACTIF : StatutEnfantType.A_NAITRE;
enfant.consent_photo = false;
enfant.is_multiple = enfantDto.grossesse_multiple || false;
const enfantEnregistre = await manager.save(Children, enfant);
enfantsEnregistres.push(enfantEnregistre);
const lienParentEnfant1 = manager.create(ParentsChildren, {
parentId: parent1Enregistre.id,
enfantId: enfantEnregistre.id,
});
await manager.save(ParentsChildren, lienParentEnfant1);
if (parent2Enregistre) {
const lienParentEnfant2 = manager.create(ParentsChildren, {
parentId: parent2Enregistre.id,
enfantId: enfantEnregistre.id,
});
await manager.save(ParentsChildren, lienParentEnfant2);
}
}
return {
parent1: parent1Enregistre,
parent2: parent2Enregistre,
enfants: enfantsEnregistres,
tokenCreationMdp,
tokenCoParent,
};
});
return {
message: 'Inscription réussie. Votre dossier est en attente de validation par un gestionnaire.',
parent_id: resultat.parent1.id,
co_parent_id: resultat.parent2?.id,
enfants_ids: resultat.enfants.map(e => e.id),
statut: StatutUtilisateurType.EN_ATTENTE,
};
}
/**
* Inscription Assistante Maternelle COMPLÈTE - Un seul endpoint (identité + pro + photo + CGU)
* Crée User (role AM) + entrée assistantes_maternelles, token création MDP
*/
async inscrireAMComplet(dto: RegisterAMCompletDto) {
if (!dto.acceptation_cgu || !dto.acceptation_privacy) {
throw new BadRequestException(
"L'acceptation des CGU et de la politique de confidentialité est obligatoire",
);
}
const existe = await this.usersService.findByEmailOrNull(dto.email);
if (existe) {
throw new ConflictException('Un compte avec cet email existe déjà');
}
const joursExpirationToken = await this.appConfigService.get<number>(
'password_reset_token_expiry_days',
7,
);
const tokenCreationMdp = crypto.randomUUID();
const dateExpiration = new Date();
dateExpiration.setDate(dateExpiration.getDate() + joursExpirationToken);
let urlPhoto: string | null = null;
if (dto.photo_base64 && dto.photo_filename) {
urlPhoto = await this.sauvegarderPhotoDepuisBase64(dto.photo_base64, dto.photo_filename);
}
const dateConsentementPhoto =
dto.consentement_photo ? new Date() : undefined;
const resultat = await this.usersRepo.manager.transaction(async (manager) => {
const user = manager.create(Users, {
email: dto.email,
prenom: dto.prenom,
nom: dto.nom,
role: RoleType.ASSISTANTE_MATERNELLE,
statut: StatutUtilisateurType.EN_ATTENTE,
telephone: dto.telephone,
adresse: dto.adresse,
code_postal: dto.code_postal,
ville: dto.ville,
token_creation_mdp: tokenCreationMdp,
token_creation_mdp_expire_le: dateExpiration,
photo_url: urlPhoto ?? undefined,
consentement_photo: dto.consentement_photo,
date_consentement_photo: dateConsentementPhoto,
date_naissance: dto.date_naissance ? new Date(dto.date_naissance) : undefined,
});
const userEnregistre = await manager.save(Users, user);
const amRepo = manager.getRepository(AssistanteMaternelle);
const am = amRepo.create({
user_id: userEnregistre.id,
approval_number: dto.numero_agrement,
nir: dto.nir,
max_children: dto.capacite_accueil,
biography: dto.biographie,
residence_city: dto.ville ?? undefined,
agreement_date: dto.date_agrement ? new Date(dto.date_agrement) : undefined,
available: true,
});
await amRepo.save(am);
return { user: userEnregistre };
});
return {
message:
'Inscription réussie. Votre dossier est en attente de validation par un gestionnaire.',
user_id: resultat.user.id,
statut: StatutUtilisateurType.EN_ATTENTE,
};
}
/**
* Sauvegarde une photo depuis base64 vers le système de fichiers
*/
private async sauvegarderPhotoDepuisBase64(donneesBase64: string, nomFichier: string): Promise<string> {
const correspondances = donneesBase64.match(/^data:image\/(\w+);base64,(.+)$/);
if (!correspondances) {
throw new BadRequestException('Format de photo invalide (doit être base64)');
}
const extension = correspondances[1];
const tamponImage = Buffer.from(correspondances[2], 'base64');
const dossierUpload = '/app/uploads/photos';
await fs.mkdir(dossierUpload, { recursive: true });
const nomFichierUnique = `${Date.now()}-${crypto.randomUUID()}.${extension}`;
const cheminFichier = path.join(dossierUpload, nomFichierUnique);
await fs.writeFile(cheminFichier, tamponImage);
return `/uploads/photos/${nomFichierUnique}`;
}
/**
* Changement de mot de passe obligatoire (première connexion)
*/
async changePasswordRequired(
userId: string,
motDePasseActuel: string,
nouveauMotDePasse: string,
) {
const user = await this.usersRepo.findOne({ where: { id: userId } });
if (!user) {
throw new UnauthorizedException('Utilisateur introuvable');
}
// Vérifier que le changement est bien obligatoire
if (!user.changement_mdp_obligatoire) {
throw new BadRequestException(
'Le changement de mot de passe n\'est pas requis pour cet utilisateur',
);
}
// Vérifier que l'utilisateur a un mot de passe
if (!user.password) {
throw new BadRequestException('Compte non activé');
}
// Vérifier le mot de passe actuel
const motDePasseValide = await bcrypt.compare(motDePasseActuel, user.password);
if (!motDePasseValide) {
throw new BadRequestException('Mot de passe actuel incorrect');
}
// Vérifier que le nouveau mot de passe est différent de l'ancien
const memeMotDePasse = await bcrypt.compare(nouveauMotDePasse, user.password);
if (memeMotDePasse) {
throw new BadRequestException(
'Le nouveau mot de passe doit être différent de l\'ancien',
);
}
// Hasher et sauvegarder le nouveau mot de passe
const sel = await bcrypt.genSalt(12);
user.password = await bcrypt.hash(nouveauMotDePasse, sel);
user.changement_mdp_obligatoire = false;
user.modifie_le = new Date();
await this.usersRepo.save(user);
return {
success: true,
message: 'Mot de passe changé avec succès',
};
}
async logout(userId: string) { async logout(userId: string) {
// Pour le moment envoyer un message clair
return { success: true, message: 'Deconnexion'} return { success: true, message: 'Deconnexion'}
} }
} }
@@ -0,0 +1,23 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, MinLength, Matches } from 'class-validator';
export class ChangePasswordRequiredDto {
@ApiProperty({ description: 'Mot de passe actuel' })
@IsString()
mot_de_passe_actuel: string;
@ApiProperty({
description: 'Nouveau mot de passe (min 8 caractères, 1 majuscule, 1 chiffre)',
minLength: 8
})
@IsString()
@MinLength(8, { message: 'Le mot de passe doit contenir au moins 8 caractères' })
@Matches(/^(?=.*[A-Z])(?=.*\d)/, {
message: 'Le mot de passe doit contenir au moins une majuscule et un chiffre'
})
nouveau_mot_de_passe: string;
@ApiProperty({ description: 'Confirmation du nouveau mot de passe' })
@IsString()
confirmation_mot_de_passe: string;
}
@@ -0,0 +1,63 @@
import { ApiProperty } from '@nestjs/swagger';
import {
IsString,
IsNotEmpty,
IsOptional,
IsEnum,
IsDateString,
IsBoolean,
MinLength,
MaxLength,
} from 'class-validator';
import { GenreType } from 'src/entities/children.entity';
export class EnfantInscriptionDto {
@ApiProperty({ example: 'Emma', required: false, description: 'Prénom de l\'enfant (obligatoire si déjà né)' })
@IsOptional()
@IsString()
@MinLength(2, { message: 'Le prénom doit contenir au moins 2 caractères' })
@MaxLength(100, { message: 'Le prénom ne peut pas dépasser 100 caractères' })
prenom?: string;
@ApiProperty({ example: 'MARTIN', required: false, description: 'Nom de l\'enfant (hérité des parents si non fourni)' })
@IsOptional()
@IsString()
@MinLength(2, { message: 'Le nom doit contenir au moins 2 caractères' })
@MaxLength(100, { message: 'Le nom ne peut pas dépasser 100 caractères' })
nom?: string;
@ApiProperty({ example: '2023-02-15', required: false, description: 'Date de naissance (si enfant déjà né)' })
@IsOptional()
@IsDateString()
date_naissance?: string;
@ApiProperty({ example: '2025-06-15', required: false, description: 'Date prévisionnelle de naissance (si enfant à naître)' })
@IsOptional()
@IsDateString()
date_previsionnelle_naissance?: string;
@ApiProperty({ enum: GenreType, example: GenreType.F })
@IsEnum(GenreType, { message: 'Le genre doit être H, F ou Autre' })
@IsNotEmpty({ message: 'Le genre est requis' })
genre: GenreType;
@ApiProperty({
example: 'data:image/jpeg;base64,/9j/4AAQSkZJRg...',
required: false,
description: 'Photo de l\'enfant en base64 (obligatoire si déjà né)'
})
@IsOptional()
@IsString()
photo_base64?: string;
@ApiProperty({ example: 'emma_martin.jpg', required: false, description: 'Nom du fichier photo' })
@IsOptional()
@IsString()
photo_filename?: string;
@ApiProperty({ example: false, required: false, description: 'Grossesse multiple (jumeaux, triplés, etc.)' })
@IsOptional()
@IsBoolean()
grossesse_multiple?: boolean;
}
@@ -19,4 +19,7 @@ export class ProfileResponseDto {
@ApiProperty({ enum: StatutUtilisateurType }) @ApiProperty({ enum: StatutUtilisateurType })
statut: StatutUtilisateurType; statut: StatutUtilisateurType;
@ApiProperty({ description: 'Indique si le changement de mot de passe est obligatoire à la première connexion' })
changement_mdp_obligatoire: boolean;
} }
@@ -0,0 +1,156 @@
import { ApiProperty } from '@nestjs/swagger';
import {
IsEmail,
IsNotEmpty,
IsOptional,
IsString,
IsBoolean,
IsInt,
Min,
Max,
MinLength,
MaxLength,
Matches,
IsDateString,
} from 'class-validator';
export class RegisterAMCompletDto {
// ============================================
// ÉTAPE 1 : IDENTITÉ (Obligatoire)
// ============================================
@ApiProperty({ example: 'marie.dupont@ptits-pas.fr' })
@IsEmail({}, { message: 'Email invalide' })
@IsNotEmpty({ message: "L'email est requis" })
email: string;
@ApiProperty({ example: 'Marie' })
@IsString()
@IsNotEmpty({ message: 'Le prénom est requis' })
@MinLength(2, { message: 'Le prénom doit contenir au moins 2 caractères' })
@MaxLength(100)
prenom: string;
@ApiProperty({ example: 'DUPONT' })
@IsString()
@IsNotEmpty({ message: 'Le nom est requis' })
@MinLength(2, { message: 'Le nom doit contenir au moins 2 caractères' })
@MaxLength(100)
nom: string;
@ApiProperty({ example: '0689567890' })
@IsString()
@IsNotEmpty({ message: 'Le téléphone est requis' })
@Matches(/^(\+33|0)[1-9](\d{2}){4}$/, {
message: 'Le numéro de téléphone doit être valide (ex: 0689567890 ou +33689567890)',
})
telephone: string;
@ApiProperty({ example: '5 Avenue du Général de Gaulle', required: false })
@IsOptional()
@IsString()
adresse?: string;
@ApiProperty({ example: '95870', required: false })
@IsOptional()
@IsString()
@MaxLength(10)
code_postal?: string;
@ApiProperty({ example: 'Bezons', required: false })
@IsOptional()
@IsString()
@MaxLength(150)
ville?: string;
// ============================================
// ÉTAPE 2 : PHOTO + INFOS PRO
// ============================================
@ApiProperty({
example: 'data:image/jpeg;base64,/9j/4AAQ...',
required: false,
description: 'Photo de profil en base64',
})
@IsOptional()
@IsString()
photo_base64?: string;
@ApiProperty({ example: 'photo_profil.jpg', required: false })
@IsOptional()
@IsString()
photo_filename?: string;
@ApiProperty({ example: true, description: 'Consentement utilisation photo' })
@IsBoolean()
@IsNotEmpty({ message: 'Le consentement photo est requis' })
consentement_photo: boolean;
@ApiProperty({ example: '2024-01-15', required: false, description: 'Date de naissance' })
@IsOptional()
@IsDateString()
date_naissance?: string;
@ApiProperty({ example: 'Paris', required: false, description: 'Ville de naissance' })
@IsOptional()
@IsString()
@MaxLength(100)
lieu_naissance_ville?: string;
@ApiProperty({ example: 'France', required: false, description: 'Pays de naissance' })
@IsOptional()
@IsString()
@MaxLength(100)
lieu_naissance_pays?: string;
@ApiProperty({ example: '123456789012345', description: 'NIR 15 chiffres' })
@IsString()
@IsNotEmpty({ message: 'Le NIR est requis' })
@Matches(/^\d{15}$/, { message: 'Le NIR doit contenir exactement 15 chiffres' })
nir: string;
@ApiProperty({ example: 'AGR-2024-12345', description: "Numéro d'agrément" })
@IsString()
@IsNotEmpty({ message: "Le numéro d'agrément est requis" })
@MaxLength(50)
numero_agrement: string;
@ApiProperty({ example: '2024-06-01', required: false, description: "Date d'obtention de l'agrément" })
@IsOptional()
@IsDateString()
date_agrement?: string;
@ApiProperty({ example: 4, description: 'Capacité d\'accueil (nombre d\'enfants)', minimum: 1, maximum: 10 })
@IsInt()
@Min(1, { message: 'La capacité doit être au moins 1' })
@Max(10, { message: 'La capacité ne peut pas dépasser 10' })
capacite_accueil: number;
// ============================================
// ÉTAPE 3 : PRÉSENTATION (Optionnel)
// ============================================
@ApiProperty({
example: 'Assistante maternelle expérimentée, accueil bienveillant...',
required: false,
description: 'Présentation / biographie (max 2000 caractères)',
})
@IsOptional()
@IsString()
@MaxLength(2000, { message: 'La présentation ne peut pas dépasser 2000 caractères' })
biographie?: string;
// ============================================
// ÉTAPE 4 : ACCEPTATION CGU (Obligatoire)
// ============================================
@ApiProperty({ example: true, description: "Acceptation des CGU" })
@IsBoolean()
@IsNotEmpty({ message: "L'acceptation des CGU est requise" })
acceptation_cgu: boolean;
@ApiProperty({ example: true, description: 'Acceptation de la Politique de confidentialité' })
@IsBoolean()
@IsNotEmpty({ message: "L'acceptation de la politique de confidentialité est requise" })
acceptation_privacy: boolean;
}
@@ -0,0 +1,166 @@
import { ApiProperty } from '@nestjs/swagger';
import {
IsEmail,
IsNotEmpty,
IsOptional,
IsString,
IsDateString,
IsEnum,
IsBoolean,
IsArray,
ValidateNested,
MinLength,
MaxLength,
Matches,
} from 'class-validator';
import { Type } from 'class-transformer';
import { SituationFamilialeType } from 'src/entities/users.entity';
import { EnfantInscriptionDto } from './enfant-inscription.dto';
export class RegisterParentCompletDto {
// ============================================
// ÉTAPE 1 : PARENT 1 (Obligatoire)
// ============================================
@ApiProperty({ example: 'claire.martin@ptits-pas.fr' })
@IsEmail({}, { message: 'Email invalide' })
@IsNotEmpty({ message: 'L\'email est requis' })
email: string;
@ApiProperty({ example: 'Claire' })
@IsString()
@IsNotEmpty({ message: 'Le prénom est requis' })
@MinLength(2, { message: 'Le prénom doit contenir au moins 2 caractères' })
@MaxLength(100, { message: 'Le prénom ne peut pas dépasser 100 caractères' })
prenom: string;
@ApiProperty({ example: 'MARTIN' })
@IsString()
@IsNotEmpty({ message: 'Le nom est requis' })
@MinLength(2, { message: 'Le nom doit contenir au moins 2 caractères' })
@MaxLength(100, { message: 'Le nom ne peut pas dépasser 100 caractères' })
nom: string;
@ApiProperty({ example: '0689567890' })
@IsString()
@IsNotEmpty({ message: 'Le téléphone est requis' })
@Matches(/^(\+33|0)[1-9](\d{2}){4}$/, {
message: 'Le numéro de téléphone doit être valide (ex: 0689567890 ou +33689567890)',
})
telephone: string;
@ApiProperty({ example: '5 Avenue du Général de Gaulle', required: false })
@IsOptional()
@IsString()
adresse?: string;
@ApiProperty({ example: '95870', required: false })
@IsOptional()
@IsString()
@MaxLength(10)
code_postal?: string;
@ApiProperty({ example: 'Bezons', required: false })
@IsOptional()
@IsString()
@MaxLength(150)
ville?: string;
// ============================================
// ÉTAPE 2 : PARENT 2 / CO-PARENT (Optionnel)
// ============================================
@ApiProperty({ example: 'thomas.martin@ptits-pas.fr', required: false })
@IsOptional()
@IsEmail({}, { message: 'Email du co-parent invalide' })
co_parent_email?: string;
@ApiProperty({ example: 'Thomas', required: false })
@IsOptional()
@IsString()
co_parent_prenom?: string;
@ApiProperty({ example: 'MARTIN', required: false })
@IsOptional()
@IsString()
co_parent_nom?: string;
@ApiProperty({ example: '0678456789', required: false })
@IsOptional()
@IsString()
@Matches(/^(\+33|0)[1-9](\d{2}){4}$/, {
message: 'Le numéro de téléphone du co-parent doit être valide',
})
co_parent_telephone?: string;
@ApiProperty({ example: true, description: 'Le co-parent habite à la même adresse', required: false })
@IsOptional()
@IsBoolean()
co_parent_meme_adresse?: boolean;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
co_parent_adresse?: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
co_parent_code_postal?: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
co_parent_ville?: string;
// ============================================
// ÉTAPE 3 : ENFANT(S) (Au moins 1 requis)
// ============================================
@ApiProperty({
type: [EnfantInscriptionDto],
description: 'Liste des enfants (au moins 1 requis)',
example: [{
prenom: 'Emma',
nom: 'MARTIN',
date_naissance: '2023-02-15',
genre: 'F',
photo_base64: 'data:image/jpeg;base64,...',
photo_filename: 'emma_martin.jpg'
}]
})
@IsArray({ message: 'La liste des enfants doit être un tableau' })
@IsNotEmpty({ message: 'Au moins un enfant est requis' })
@ValidateNested({ each: true })
@Type(() => EnfantInscriptionDto)
enfants: EnfantInscriptionDto[];
// ============================================
// ÉTAPE 4 : PRÉSENTATION DU DOSSIER (Optionnel)
// ============================================
@ApiProperty({
example: 'Nous recherchons une assistante maternelle bienveillante pour nos triplés...',
required: false,
description: 'Présentation du dossier (max 2000 caractères)'
})
@IsOptional()
@IsString()
@MaxLength(2000, { message: 'La présentation ne peut pas dépasser 2000 caractères' })
presentation_dossier?: string;
// ============================================
// ÉTAPE 5 : ACCEPTATION CGU (Obligatoire)
// ============================================
@ApiProperty({ example: true, description: 'Acceptation des Conditions Générales d\'Utilisation' })
@IsBoolean()
@IsNotEmpty({ message: 'L\'acceptation des CGU est requise' })
acceptation_cgu: boolean;
@ApiProperty({ example: true, description: 'Acceptation de la Politique de confidentialité' })
@IsBoolean()
@IsNotEmpty({ message: 'L\'acceptation de la politique de confidentialité est requise' })
acceptation_privacy: boolean;
}
@@ -29,10 +29,10 @@ export class CreateEnfantsDto {
@MaxLength(100) @MaxLength(100)
last_name?: string; last_name?: string;
@ApiProperty({ enum: GenreType, required: false }) @ApiProperty({ enum: GenreType })
@IsOptional()
@IsEnum(GenreType) @IsEnum(GenreType)
gender?: GenreType; @IsNotEmpty()
gender: GenreType;
@ApiProperty({ example: '2018-06-24', required: false }) @ApiProperty({ example: '2018-06-24', required: false })
@ValidateIf(o => o.status !== StatutEnfantType.A_NAITRE) @ValidateIf(o => o.status !== StatutEnfantType.A_NAITRE)
@@ -8,8 +8,13 @@ import {
Patch, Patch,
Post, Post,
UseGuards, UseGuards,
UseInterceptors,
UploadedFile,
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { FileInterceptor } from '@nestjs/platform-express';
import { ApiBearerAuth, ApiTags, ApiConsumes } from '@nestjs/swagger';
import { diskStorage } from 'multer';
import { extname } from 'path';
import { EnfantsService } from './enfants.service'; import { EnfantsService } from './enfants.service';
import { CreateEnfantsDto } from './dto/create_enfants.dto'; import { CreateEnfantsDto } from './dto/create_enfants.dto';
import { UpdateEnfantsDto } from './dto/update_enfants.dto'; import { UpdateEnfantsDto } from './dto/update_enfants.dto';
@@ -28,8 +33,34 @@ export class EnfantsController {
@Roles(RoleType.PARENT) @Roles(RoleType.PARENT)
@Post() @Post()
create(@Body() dto: CreateEnfantsDto, @User() currentUser: Users) { @ApiConsumes('multipart/form-data')
return this.enfantsService.create(dto, currentUser); @UseInterceptors(
FileInterceptor('photo', {
storage: diskStorage({
destination: './uploads/photos',
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
const ext = extname(file.originalname);
cb(null, `enfant-${uniqueSuffix}${ext}`);
},
}),
fileFilter: (req, file, cb) => {
if (!file.mimetype.match(/\/(jpg|jpeg|png|gif)$/)) {
return cb(new Error('Seules les images sont autorisées'), false);
}
cb(null, true);
},
limits: {
fileSize: 5 * 1024 * 1024,
},
}),
)
create(
@Body() dto: CreateEnfantsDto,
@UploadedFile() photo: Express.Multer.File,
@User() currentUser: Users,
) {
return this.enfantsService.create(dto, currentUser, photo);
} }
@Roles(RoleType.ADMINISTRATEUR, RoleType.GESTIONNAIRE, RoleType.SUPER_ADMIN) @Roles(RoleType.ADMINISTRATEUR, RoleType.GESTIONNAIRE, RoleType.SUPER_ADMIN)
+21 -3
View File
@@ -24,10 +24,11 @@ export class EnfantsService {
private readonly parentsChildrenRepository: Repository<ParentsChildren>, private readonly parentsChildrenRepository: Repository<ParentsChildren>,
) { } ) { }
// Création d’un enfant // Création d'un enfant
async create(dto: CreateEnfantsDto, currentUser: Users): Promise<Children> { async create(dto: CreateEnfantsDto, currentUser: Users, photoFile?: Express.Multer.File): Promise<Children> {
const parent = await this.parentsRepository.findOne({ const parent = await this.parentsRepository.findOne({
where: { user_id: currentUser.id }, where: { user_id: currentUser.id },
relations: ['co_parent'],
}); });
if (!parent) throw new NotFoundException('Parent introuvable'); if (!parent) throw new NotFoundException('Parent introuvable');
@@ -46,17 +47,34 @@ export class EnfantsService {
}); });
if (exist) throw new ConflictException('Cet enfant existe déjà'); if (exist) throw new ConflictException('Cet enfant existe déjà');
// Gestion de la photo uploadée
if (photoFile) {
dto.photo_url = `/uploads/photos/${photoFile.filename}`;
if (dto.consent_photo) {
dto.consent_photo_at = new Date().toISOString();
}
}
// Création // Création
const child = this.childrenRepository.create(dto); const child = this.childrenRepository.create(dto);
await this.childrenRepository.save(child); await this.childrenRepository.save(child);
// Lien parent-enfant // Lien parent-enfant (Parent 1)
const parentLink = this.parentsChildrenRepository.create({ const parentLink = this.parentsChildrenRepository.create({
parentId: parent.user_id, parentId: parent.user_id,
enfantId: child.id, enfantId: child.id,
}); });
await this.parentsChildrenRepository.save(parentLink); await this.parentsChildrenRepository.save(parentLink);
// Rattachement automatique au co-parent s'il existe
if (parent.co_parent) {
const coParentLink = this.parentsChildrenRepository.create({
parentId: parent.co_parent.id,
enfantId: child.id,
});
await this.parentsChildrenRepository.save(coParentLink);
}
return this.findOne(child.id, currentUser); return this.findOne(child.id, currentUser);
} }
@@ -20,7 +20,7 @@ import { UpdateParentsDto } from '../user/dto/update_parent.dto';
export class ParentsController { export class ParentsController {
constructor(private readonly parentsService: ParentsService) {} constructor(private readonly parentsService: ParentsService) {}
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE) @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
@Get() @Get()
@ApiResponse({ status: 200, type: [Parents], description: 'Liste des parents' }) @ApiResponse({ status: 200, type: [Parents], description: 'Liste des parents' })
@ApiResponse({ status: 403, description: 'Accès refusé !' }) @ApiResponse({ status: 403, description: 'Accès refusé !' })
@@ -35,7 +35,7 @@ export class GestionnairesController {
return this.gestionnairesService.create(dto); return this.gestionnairesService.create(dto);
} }
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE) @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
@ApiOperation({ summary: 'Liste des gestionnaires' }) @ApiOperation({ summary: 'Liste des gestionnaires' })
@ApiResponse({ status: 200, description: 'Liste des gestionnaires : ', type: [Users] }) @ApiResponse({ status: 200, description: 'Liste des gestionnaires : ', type: [Users] })
@Get() @Get()
@@ -3,9 +3,13 @@ import { GestionnairesService } from './gestionnaires.service';
import { GestionnairesController } from './gestionnaires.controller'; import { GestionnairesController } from './gestionnaires.controller';
import { Users } from 'src/entities/users.entity'; import { Users } from 'src/entities/users.entity';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthModule } from 'src/routes/auth/auth.module';
@Module({ @Module({
imports: [TypeOrmModule.forFeature([Users])], imports: [
TypeOrmModule.forFeature([Users]),
AuthModule,
],
controllers: [GestionnairesController], controllers: [GestionnairesController],
providers: [GestionnairesService], providers: [GestionnairesService],
}) })
+1 -1
View File
@@ -28,7 +28,7 @@ export class UserController {
// Lister tous les utilisateurs (super_admin uniquement) // Lister tous les utilisateurs (super_admin uniquement)
@Get() @Get()
@Roles(RoleType.SUPER_ADMIN) @Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR)
@ApiOperation({ summary: 'Lister tous les utilisateurs' }) @ApiOperation({ summary: 'Lister tous les utilisateurs' })
findAll() { findAll() {
return this.userService.findAll(); return this.userService.findAll();
+2
View File
@@ -9,6 +9,7 @@ import { ParentsModule } from '../parents/parents.module';
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity'; import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
import { AssistantesMaternellesModule } from '../assistantes_maternelles/assistantes_maternelles.module'; import { AssistantesMaternellesModule } from '../assistantes_maternelles/assistantes_maternelles.module';
import { Parents } from 'src/entities/parents.entity'; import { Parents } from 'src/entities/parents.entity';
import { GestionnairesModule } from './gestionnaires/gestionnaires.module';
@Module({ @Module({
imports: [TypeOrmModule.forFeature( imports: [TypeOrmModule.forFeature(
@@ -20,6 +21,7 @@ import { Parents } from 'src/entities/parents.entity';
]), forwardRef(() => AuthModule), ]), forwardRef(() => AuthModule),
ParentsModule, ParentsModule,
AssistantesMaternellesModule, AssistantesMaternellesModule,
GestionnairesModule,
], ],
controllers: [UserController], controllers: [UserController],
providers: [UserService], providers: [UserService],
+139 -10
View File
@@ -46,19 +46,20 @@ CREATE TABLE utilisateurs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) NOT NULL UNIQUE, email VARCHAR(255) NOT NULL UNIQUE,
CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'), CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'),
password TEXT NOT NULL, password TEXT, -- NULL avant création via token
prenom VARCHAR(100), prenom VARCHAR(100),
nom VARCHAR(100), nom VARCHAR(100),
genre genre_type, genre genre_type,
role role_type NOT NULL, role role_type NOT NULL,
statut statut_utilisateur_type DEFAULT 'en_attente', statut statut_utilisateur_type DEFAULT 'en_attente',
mobile VARCHAR(20), telephone VARCHAR(20), -- Unifié (mobile privilégié)
telephone_fixe VARCHAR(20),
adresse TEXT, adresse TEXT,
date_naissance DATE, date_naissance DATE,
photo_url TEXT, photo_url TEXT, -- Obligatoire pour AM, non utilisé pour parents
consentement_photo BOOLEAN DEFAULT false, consentement_photo BOOLEAN DEFAULT false,
date_consentement_photo TIMESTAMPTZ, date_consentement_photo TIMESTAMPTZ,
token_creation_mdp VARCHAR(255), -- Token pour créer MDP après validation
token_creation_mdp_expire_le TIMESTAMPTZ, -- Expiration 7 jours
changement_mdp_obligatoire BOOLEAN DEFAULT false, changement_mdp_obligatoire BOOLEAN DEFAULT false,
cree_le TIMESTAMPTZ DEFAULT now(), cree_le TIMESTAMPTZ DEFAULT now(),
modifie_le TIMESTAMPTZ DEFAULT now(), modifie_le TIMESTAMPTZ DEFAULT now(),
@@ -68,20 +69,26 @@ CREATE TABLE utilisateurs (
situation_familiale situation_familiale_type situation_familiale situation_familiale_type
); );
-- Index pour recherche par token
CREATE INDEX idx_utilisateurs_token_creation_mdp
ON utilisateurs(token_creation_mdp)
WHERE token_creation_mdp IS NOT NULL;
-- ========================================================== -- ==========================================================
-- Table : assistantes_maternelles -- Table : assistantes_maternelles
-- ========================================================== -- ==========================================================
CREATE TABLE assistantes_maternelles ( CREATE TABLE assistantes_maternelles (
id_utilisateur UUID PRIMARY KEY REFERENCES utilisateurs(id) ON DELETE CASCADE, id_utilisateur UUID PRIMARY KEY REFERENCES utilisateurs(id) ON DELETE CASCADE,
numero_agrement VARCHAR(50), numero_agrement VARCHAR(50),
date_agrement DATE,
nir_chiffre CHAR(15), nir_chiffre CHAR(15),
annee_experience SMALLINT,
specialite VARCHAR (100),
nb_max_enfants INT, nb_max_enfants INT,
place_disponible INT,
biographie TEXT, biographie TEXT,
disponible BOOLEAN DEFAULT true disponible BOOLEAN DEFAULT true,
ville_residence VARCHAR(100),
date_agrement DATE,
annee_experience SMALLINT,
specialite VARCHAR(100),
place_disponible INT
); );
-- ========================================================== -- ==========================================================
@@ -100,7 +107,7 @@ CREATE TABLE enfants (
statut statut_enfant_type, statut statut_enfant_type,
prenom VARCHAR(100), prenom VARCHAR(100),
nom VARCHAR(100), nom VARCHAR(100),
genre genre_type, genre genre_type NOT NULL, -- Obligatoire selon CDC
date_naissance DATE, date_naissance DATE,
date_prevue_naissance DATE, date_prevue_naissance DATE,
photo_url TEXT, photo_url TEXT,
@@ -241,3 +248,125 @@ CREATE TABLE validations (
cree_le TIMESTAMPTZ DEFAULT now(), cree_le TIMESTAMPTZ DEFAULT now(),
modifie_le TIMESTAMPTZ DEFAULT now() modifie_le TIMESTAMPTZ DEFAULT now()
); );
-- ==========================================================
-- Table : configuration
-- ==========================================================
CREATE TABLE configuration (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
cle VARCHAR(100) UNIQUE NOT NULL,
valeur TEXT,
type VARCHAR(50) NOT NULL,
categorie VARCHAR(50),
description TEXT,
modifie_le TIMESTAMPTZ DEFAULT now(),
modifie_par UUID REFERENCES utilisateurs(id)
);
-- Index pour performance
CREATE INDEX idx_configuration_cle ON configuration(cle);
CREATE INDEX idx_configuration_categorie ON configuration(categorie);
-- Seed initial de configuration
INSERT INTO configuration (cle, valeur, type, categorie, description) VALUES
-- === Configuration Email (SMTP) ===
('smtp_host', 'localhost', 'string', 'email', 'Serveur SMTP (ex: mail.mairie-bezons.fr, smtp.gmail.com)'),
('smtp_port', '25', 'number', 'email', 'Port SMTP (25, 465, 587)'),
('smtp_secure', 'false', 'boolean', 'email', 'Utiliser SSL/TLS (true pour port 465)'),
('smtp_auth_required', 'false', 'boolean', 'email', 'Authentification SMTP requise'),
('smtp_user', '', 'string', 'email', 'Utilisateur SMTP (si authentification requise)'),
('smtp_password', '', 'encrypted', 'email', 'Mot de passe SMTP (chiffré en AES-256)'),
('email_from_name', 'P''titsPas', 'string', 'email', 'Nom de l''expéditeur affiché dans les emails'),
('email_from_address', 'no-reply@ptits-pas.fr', 'string', 'email', 'Adresse email de l''expéditeur'),
-- === Configuration Application ===
('app_name', 'P''titsPas', 'string', 'app', 'Nom de l''application (affiché dans l''interface)'),
('app_url', 'https://app.ptits-pas.fr', 'string', 'app', 'URL publique de l''application (pour les liens dans emails)'),
('app_logo_url', '/assets/logo.png', 'string', 'app', 'URL du logo de l''application'),
('setup_completed', 'false', 'boolean', 'app', 'Configuration initiale terminée'),
-- === Configuration Sécurité ===
('password_reset_token_expiry_days', '7', 'number', 'security', 'Durée de validité des tokens de création/réinitialisation de mot de passe (en jours)'),
('jwt_expiry_hours', '24', 'number', 'security', 'Durée de validité des sessions JWT (en heures)'),
('max_upload_size_mb', '5', 'number', 'security', 'Taille maximale des fichiers uploadés (en MB)'),
('bcrypt_rounds', '12', 'number', 'security', 'Nombre de rounds bcrypt pour le hachage des mots de passe');
-- ==========================================================
-- Table : documents_legaux
-- ==========================================================
CREATE TABLE documents_legaux (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
type VARCHAR(50) NOT NULL, -- 'cgu' ou 'privacy'
version INTEGER NOT NULL, -- Numéro de version (auto-incrémenté)
fichier_nom VARCHAR(255) NOT NULL, -- Nom original du fichier
fichier_path VARCHAR(500) NOT NULL, -- Chemin de stockage
fichier_hash VARCHAR(64) NOT NULL, -- Hash SHA-256 pour intégrité
actif BOOLEAN DEFAULT false, -- Version actuellement active
televerse_par UUID REFERENCES utilisateurs(id), -- Qui a uploadé
televerse_le TIMESTAMPTZ DEFAULT now(), -- Date d'upload
active_le TIMESTAMPTZ, -- Date d'activation
UNIQUE(type, version) -- Pas de doublon version
);
-- Index pour performance
CREATE INDEX idx_documents_legaux_type_actif ON documents_legaux(type, actif);
CREATE INDEX idx_documents_legaux_version ON documents_legaux(type, version DESC);
-- ==========================================================
-- Table : acceptations_documents
-- ==========================================================
CREATE TABLE acceptations_documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
id_utilisateur UUID REFERENCES utilisateurs(id) ON DELETE CASCADE,
id_document UUID REFERENCES documents_legaux(id),
type_document VARCHAR(50) NOT NULL, -- 'cgu' ou 'privacy'
version_document INTEGER NOT NULL, -- Version acceptée
accepte_le TIMESTAMPTZ DEFAULT now(), -- Date d'acceptation
ip_address INET, -- IP de l'utilisateur (RGPD)
user_agent TEXT -- Navigateur (preuve)
);
-- Index pour performance
CREATE INDEX idx_acceptations_utilisateur ON acceptations_documents(id_utilisateur);
CREATE INDEX idx_acceptations_document ON acceptations_documents(id_document);
-- ==========================================================
-- Modification Table : utilisateurs (ajout colonnes documents)
-- ==========================================================
ALTER TABLE utilisateurs
ADD COLUMN IF NOT EXISTS cgu_version_acceptee INTEGER,
ADD COLUMN IF NOT EXISTS cgu_acceptee_le TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS privacy_version_acceptee INTEGER,
ADD COLUMN IF NOT EXISTS privacy_acceptee_le TIMESTAMPTZ;
-- ==========================================================
-- Seed : Documents légaux génériques v1
-- ==========================================================
INSERT INTO documents_legaux (type, version, fichier_nom, fichier_path, fichier_hash, actif, televerse_le, active_le) VALUES
('cgu', 1, 'cgu_v1_default.pdf', '/documents/legaux/cgu_v1_default.pdf', 'a3f8b2c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2', true, now(), now()),
('privacy', 1, 'privacy_v1_default.pdf', '/documents/legaux/privacy_v1_default.pdf', 'b4f9c3d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4', true, now(), now());
-- ==========================================================
-- Seed : Super Administrateur par défaut
-- ==========================================================
-- Email: admin@ptits-pas.fr
-- Mot de passe: 4dm1n1strateur (hashé bcrypt)
-- IMPORTANT: Changer ce mot de passe en production !
-- ==========================================================
INSERT INTO utilisateurs (
email,
password,
prenom,
nom,
role,
statut,
changement_mdp_obligatoire
) VALUES (
'admin@ptits-pas.fr',
'$2b$12$plOZCW7lzLFkWgDPcE6p6u10EA4yErQt6Xcp5nyH3Sp/2.6EpNW.6',
'Super',
'Administrateur',
'super_admin',
'actif',
true
);
+10
View File
@@ -41,6 +41,16 @@ docker compose -f docker-compose.dev.yml down -v
--- ---
## Réinitialiser la BDD et charger les données de test (dashboard admin)
Depuis la **racine du projet** (ptitspas-app, où se trouve `docker-compose.yml`) :
```bash
./scripts/reset-and-seed-db.sh
```
Ce script : arrête les conteneurs, supprime le volume Postgres, redémarre la base (le schéma est recréé via `BDD.sql`), puis exécute `database/seed/03_seed_test_data.sql`. Tu obtiens un super_admin (`admin@ptits-pas.fr`) plus 9 comptes de test (1 admin, 1 gestionnaire, 2 AM, 5 parents) avec **mot de passe : `password`**. Idéal pour développer le ticket #92 (dashboard admin).
## Importation automatique des données de test ## Importation automatique des données de test
Les données de test (CSV) sont automatiquement importées dans la base au démarrage du conteneur Docker grâce aux scripts présents dans le dossier `migrations/`. Les données de test (CSV) sont automatiquement importées dans la base au démarrage du conteneur Docker grâce aux scripts présents dans le dossier `migrations/`.
-277
View File
@@ -1,277 +0,0 @@
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- ==========================================================
-- ENUMS
-- ==========================================================
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'role_type') THEN
CREATE TYPE role_type AS ENUM ('parent', 'gestionnaire', 'super_admin', 'assistante_maternelle','administrateur');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'genre_type') THEN
CREATE TYPE genre_type AS ENUM ('H', 'F', 'Autre');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_utilisateur_type') THEN
CREATE TYPE statut_utilisateur_type AS ENUM ('en_attente','actif','suspendu');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_enfant_type') THEN
CREATE TYPE statut_enfant_type AS ENUM ('a_naitre','actif','scolarise');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_dossier_type') THEN
CREATE TYPE statut_dossier_type AS ENUM ('envoye','accepte','refuse');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_contrat_type') THEN
CREATE TYPE statut_contrat_type AS ENUM ('brouillon','en_attente_signature','valide','resilie');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_avenant_type') THEN
CREATE TYPE statut_avenant_type AS ENUM ('propose','accepte','refuse');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'type_evenement_type') THEN
CREATE TYPE type_evenement_type AS ENUM ('absence_enfant','conge_am','conge_parent','arret_maladie_am','evenement_rpe');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_evenement_type') THEN
CREATE TYPE statut_evenement_type AS ENUM ('propose','valide','refuse');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_validation_type') THEN
CREATE TYPE statut_validation_type AS ENUM ('en_attente','valide','refuse');
END IF;
END $$;
-- ==========================================================
-- Table : utilisateurs
-- ==========================================================
CREATE TABLE utilisateurs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) NOT NULL UNIQUE,
CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'),
password TEXT NOT NULL,
prenom VARCHAR(100),
nom VARCHAR(100),
genre genre_type,
role role_type NOT NULL,
statut statut_utilisateur_type DEFAULT 'en_attente',
telephone VARCHAR(20),
adresse TEXT,
photo_url TEXT,
consentement_photo BOOLEAN DEFAULT false,
date_consentement_photo TIMESTAMPTZ,
changement_mdp_obligatoire BOOLEAN DEFAULT false,
cree_le TIMESTAMPTZ DEFAULT now(),
modifie_le TIMESTAMPTZ DEFAULT now(),
ville VARCHAR(150),
code_postal VARCHAR(10),
mobile VARCHAR(20),
telephone_fixe VARCHAR(20),
profession VARCHAR(150),
situation_familiale VARCHAR(50),
date_naissance DATE
);
-- ==========================================================
-- Table : assistantes_maternelles
-- ==========================================================
CREATE TABLE assistantes_maternelles (
id_utilisateur UUID PRIMARY KEY REFERENCES utilisateurs(id) ON DELETE CASCADE,
numero_agrement VARCHAR(50),
nir_chiffre CHAR(15),
nb_max_enfants INT,
biographie TEXT,
disponible BOOLEAN DEFAULT true,
ville_residence VARCHAR(100),
date_agrement DATE,
annee_experience SMALLINT,
specialite VARCHAR(100),
place_disponible INT
);
-- ==========================================================
-- Table : parentschange les donnée de init
-- ==========================================================
CREATE TABLE parents (
id_utilisateur UUID PRIMARY KEY REFERENCES utilisateurs(id) ON DELETE CASCADE,
id_co_parent UUID REFERENCES utilisateurs(id)
);
-- ==========================================================
-- Table : enfants
-- ==========================================================
CREATE TABLE enfants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
statut statut_enfant_type,
prenom VARCHAR(100),
nom VARCHAR(100),
genre genre_type,
date_naissance DATE,
date_prevue_naissance DATE,
photo_url TEXT,
consentement_photo BOOLEAN DEFAULT false,
date_consentement_photo TIMESTAMPTZ,
est_multiple BOOLEAN DEFAULT false
);
-- ==========================================================
-- Table : enfants_parents
-- ==========================================================
CREATE TABLE enfants_parents (
id_parent UUID REFERENCES parents(id_utilisateur) ON DELETE CASCADE,
id_enfant UUID REFERENCES enfants(id) ON DELETE CASCADE,
PRIMARY KEY (id_parent, id_enfant)
);
-- ==========================================================
-- Table : dossiers
-- ==========================================================
CREATE TABLE dossiers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
id_parent UUID REFERENCES parents(id_utilisateur) ON DELETE CASCADE,
id_enfant UUID REFERENCES enfants(id) ON DELETE CASCADE,
presentation TEXT,
type_contrat VARCHAR(50),
repas BOOLEAN DEFAULT false,
budget NUMERIC(10,2),
planning_souhaite JSONB,
statut statut_dossier_type DEFAULT 'envoye',
cree_le TIMESTAMPTZ DEFAULT now(),
modifie_le TIMESTAMPTZ DEFAULT now()
);
-- ==========================================================
-- Table : messages
-- ==========================================================
CREATE TABLE messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
id_dossier UUID REFERENCES dossiers(id) ON DELETE CASCADE,
id_expediteur UUID REFERENCES utilisateurs(id) ON DELETE CASCADE,
contenu TEXT,
re_redige_par_ia BOOLEAN DEFAULT false,
cree_le TIMESTAMPTZ DEFAULT now()
);
-- ==========================================================
-- Table : contrats
-- ==========================================================
CREATE TABLE contrats (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
id_dossier UUID UNIQUE REFERENCES dossiers(id) ON DELETE CASCADE,
planning JSONB,
tarif_horaire NUMERIC(6,2),
indemnites_repas NUMERIC(6,2),
date_debut DATE,
statut statut_contrat_type DEFAULT 'brouillon',
signe_parent BOOLEAN DEFAULT false,
signe_am BOOLEAN DEFAULT false,
finalise_le TIMESTAMPTZ,
cree_le TIMESTAMPTZ DEFAULT now(),
modifie_le TIMESTAMPTZ DEFAULT now()
);
-- ==========================================================
-- Table : avenants_contrats
-- ==========================================================
CREATE TABLE avenants_contrats (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
id_contrat UUID REFERENCES contrats(id) ON DELETE CASCADE,
modifications JSONB,
initie_par UUID REFERENCES utilisateurs(id),
statut statut_avenant_type DEFAULT 'propose',
cree_le TIMESTAMPTZ DEFAULT now(),
modifie_le TIMESTAMPTZ DEFAULT now()
);
-- ==========================================================
-- Table : evenements
-- ==========================================================
CREATE TABLE evenements (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
type type_evenement_type,
id_enfant UUID REFERENCES enfants(id) ON DELETE CASCADE,
id_am UUID REFERENCES utilisateurs(id),
id_parent UUID REFERENCES parents(id_utilisateur),
cree_par UUID REFERENCES utilisateurs(id),
date_debut TIMESTAMPTZ,
date_fin TIMESTAMPTZ,
commentaires TEXT,
statut statut_evenement_type DEFAULT 'propose',
delai_grace TIMESTAMPTZ,
urgent BOOLEAN DEFAULT false,
cree_le TIMESTAMPTZ DEFAULT now(),
modifie_le TIMESTAMPTZ DEFAULT now()
);
-- ==========================================================
-- Table : signalements_bugs
-- ==========================================================
CREATE TABLE signalements_bugs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
id_utilisateur UUID REFERENCES utilisateurs(id),
description TEXT,
cree_le TIMESTAMPTZ DEFAULT now()
);
-- ==========================================================
-- Table : uploads
-- ==========================================================
CREATE TABLE uploads (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
id_utilisateur UUID REFERENCES utilisateurs(id) ON DELETE SET NULL,
fichier_url TEXT NOT NULL,
type VARCHAR(50),
cree_le TIMESTAMPTZ DEFAULT now()
);
-- ==========================================================
-- Table : notifications
-- ==========================================================
CREATE TABLE notifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
id_utilisateur UUID REFERENCES utilisateurs(id) ON DELETE CASCADE,
contenu TEXT,
lu BOOLEAN DEFAULT false,
cree_le TIMESTAMPTZ DEFAULT now()
);
-- ==========================================================
-- Table : validations
-- ==========================================================
CREATE TABLE validations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
id_utilisateur UUID REFERENCES utilisateurs(id),
type VARCHAR(50),
statut statut_validation_type DEFAULT 'en_attente',
cree_le TIMESTAMPTZ DEFAULT now(),
modifie_le TIMESTAMPTZ DEFAULT now(),
valide_par UUID REFERENCES utilisateurs(id),
commentaire TEXT
);
-- ==========================================================
-- Initialisation d'un administrateur par défaut
-- ==========================================================
-- ==========================================================
-- SEED: Super Administrateur par défaut
-- ==========================================================
-- Mot de passe: 4dm1n1strateur
INSERT INTO utilisateurs (
id,
email,
password,
prenom,
nom,
role,
statut,
cree_le,
modifie_le
)
VALUES (
gen_random_uuid(),
'admin@ptits-pas.fr',
'$2b$12$Fo5ly1YlTj3O6lXf.IUgoeUqEebBGpmoM5zLbzZx.CueorSE7z2E2',
'Admin',
'Système',
'super_admin',
'actif',
now(),
now()
)
ON CONFLICT (email) DO NOTHING;
-157
View File
@@ -1,157 +0,0 @@
-- =============================================
-- 02_indexes.sql : Index FK + colonnes critiques
-- =============================================
-- Recommandation : exécuter après 01_init.sql
-- ===========
-- UTILISATEURS
-- ===========
-- Recherche par email (insensibilité à la casse pour lookup)
CREATE INDEX IF NOT EXISTS idx_utilisateurs_lower_courriel
ON utilisateurs (LOWER(courriel));
-- ===========
-- ASSISTANTES_MATERNELLES
-- ===========
-- FK -> utilisateurs(id)
CREATE INDEX IF NOT EXISTS idx_am_id_utilisateur
ON assistantes_maternelles (id_utilisateur);
-- =======
-- PARENTS
-- =======
-- FK -> utilisateurs(id)
CREATE INDEX IF NOT EXISTS idx_parents_id_utilisateur
ON parents (id_utilisateur);
-- Co-parent (nullable)
CREATE INDEX IF NOT EXISTS idx_parents_id_co_parent
ON parents (id_co_parent);
-- =======
-- ENFANTS
-- =======
-- (souvent filtrés par statut / date_naissance ? à activer si besoin)
-- CREATE INDEX IF NOT EXISTS idx_enfants_statut ON enfants (statut);
-- CREATE INDEX IF NOT EXISTS idx_enfants_date_naissance ON enfants (date_naissance);
-- ================
-- ENFANTS_PARENTS (N:N)
-- ================
-- PK composite déjà en place (id_parent, id_enfant), ajouter index individuels si jointures unilatérales fréquentes
CREATE INDEX IF NOT EXISTS idx_enfants_parents_id_parent
ON enfants_parents (id_parent);
CREATE INDEX IF NOT EXISTS idx_enfants_parents_id_enfant
ON enfants_parents (id_enfant);
-- ========
-- DOSSIERS
-- ========
-- FK -> parent / enfant
CREATE INDEX IF NOT EXISTS idx_dossiers_id_parent
ON dossiers (id_parent);
CREATE INDEX IF NOT EXISTS idx_dossiers_id_enfant
ON dossiers (id_enfant);
-- Statut (filtrages "à traiter", "envoyé", etc.)
CREATE INDEX IF NOT EXISTS idx_dossiers_statut
ON dossiers (statut);
-- JSONB : si on fait des requêtes @> sur le planning souhaité
-- CREATE INDEX IF NOT EXISTS idx_dossiers_planning_souhaite_gin
-- ON dossiers USING GIN (planning_souhaite);
-- ========
-- MESSAGES
-- ========
-- Filtrage par dossier + tri chronologique
CREATE INDEX IF NOT EXISTS idx_messages_id_dossier_cree_le
ON messages (id_dossier, cree_le);
-- Recherche par expéditeur
CREATE INDEX IF NOT EXISTS idx_messages_id_expediteur_cree_le
ON messages (id_expediteur, cree_le);
-- =========
-- CONTRATS
-- =========
-- UNIQUE(id_dossier) existe déjà -> index implicite
-- Tri / filtres fréquents
CREATE INDEX IF NOT EXISTS idx_contrats_statut
ON contrats (statut);
-- JSONB planning : activer si on requête par clé
-- CREATE INDEX IF NOT EXISTS idx_contrats_planning_gin
-- ON contrats USING GIN (planning);
-- ==================
-- AVENANTS_CONTRATS
-- ==================
CREATE INDEX IF NOT EXISTS idx_avenants_contrats_id_contrat_cree_le
ON avenants_contrats (id_contrat, cree_le);
CREATE INDEX IF NOT EXISTS idx_avenants_contrats_initie_par
ON avenants_contrats (initie_par);
CREATE INDEX IF NOT EXISTS idx_avenants_contrats_statut
ON avenants_contrats (statut);
-- =========
-- EVENEMENTS
-- =========
-- Accès par enfant + période
CREATE INDEX IF NOT EXISTS idx_evenements_id_enfant_date_debut
ON evenements (id_enfant, date_debut);
-- Filtrage par auteur / AM / parent
CREATE INDEX IF NOT EXISTS idx_evenements_cree_par
ON evenements (cree_par);
CREATE INDEX IF NOT EXISTS idx_evenements_id_am
ON evenements (id_am);
CREATE INDEX IF NOT EXISTS idx_evenements_id_parent
ON evenements (id_parent);
CREATE INDEX IF NOT EXISTS idx_evenements_type
ON evenements (type);
CREATE INDEX IF NOT EXISTS idx_evenements_statut
ON evenements (statut);
-- =================
-- SIGNALEMENTS_BUGS
-- =================
CREATE INDEX IF NOT EXISTS idx_signalements_bugs_id_utilisateur_cree_le
ON signalements_bugs (id_utilisateur, cree_le);
-- =======
-- UPLOADS
-- =======
CREATE INDEX IF NOT EXISTS idx_uploads_id_utilisateur_cree_le
ON uploads (id_utilisateur, cree_le);
-- =============
-- NOTIFICATIONS
-- =============
-- Requêtes fréquentes : non lues + ordre chrono
CREATE INDEX IF NOT EXISTS idx_notifications_user_lu_cree_le
ON notifications (id_utilisateur, lu, cree_le);
-- Option : index partiel pour "non lues"
-- CREATE INDEX IF NOT EXISTS idx_notifications_non_lues
-- ON notifications (id_utilisateur, cree_le)
-- WHERE lu = false;
-- ===========
-- VALIDATIONS
-- ===========
-- Requêtes par utilisateur validé, par statut et par date
CREATE INDEX IF NOT EXISTS idx_validations_id_utilisateur_cree_le
ON validations (id_utilisateur, cree_le);
CREATE INDEX IF NOT EXISTS idx_validations_statut
ON validations (statut);
-140
View File
@@ -1,140 +0,0 @@
-- =============================================
-- 03_checks.sql : Contraintes CHECK & NOT NULL
-- A exécuter après 01_init.sql (et 02_indexes.sql)
-- =============================================
-- ==============
-- UTILISATEURS
-- ==============
-- (Regex email déjà présente dans 01_init.sql)
-- Optionnel : forcer prenom/nom non vides si fournis
ALTER TABLE utilisateurs
ADD CONSTRAINT chk_utilisateurs_prenom_non_vide
CHECK (prenom IS NULL OR btrim(prenom) <> ''),
ADD CONSTRAINT chk_utilisateurs_nom_non_vide
CHECK (nom IS NULL OR btrim(nom) <> '');
-- =========================
-- ASSISTANTES_MATERNELLES
-- =========================
-- NIR : aujourd’hui en 15 chiffres (Sprint 2 : chiffrement)
ALTER TABLE assistantes_maternelles
ADD CONSTRAINT chk_am_nir_format
CHECK (nir_chiffre IS NULL OR nir_chiffre ~ '^[0-9]{15}$'),
ADD CONSTRAINT chk_am_nb_max_enfants
CHECK (nb_max_enfants IS NULL OR nb_max_enfants BETWEEN 0 AND 10),
ADD CONSTRAINT chk_am_ville_non_vide
CHECK (ville_residence IS NULL OR btrim(ville_residence) <> '');
-- =========
-- PARENTS
-- =========
-- Interdiction d’être co-parent de soi-même
ALTER TABLE parents
ADD CONSTRAINT chk_parents_co_parent_diff
CHECK (id_co_parent IS NULL OR id_co_parent <> id_utilisateur);
-- =========
-- ENFANTS
-- =========
-- Cohérence statut / dates de naissance
ALTER TABLE enfants
ADD CONSTRAINT chk_enfants_dates_exclusives
CHECK (NOT (date_naissance IS NOT NULL AND date_prevue_naissance IS NOT NULL)),
ADD CONSTRAINT chk_enfants_statut_dates
CHECK (
-- a_naitre => date_prevue_naissance requise
(statut = 'a_naitre' AND date_prevue_naissance IS NOT NULL)
OR
-- actif/scolarise => date_naissance requise
(statut IN ('actif','scolarise') AND date_naissance IS NOT NULL)
OR statut IS NULL -- si statut non encore fixé
),
ADD CONSTRAINT chk_enfants_consentement_coherent
CHECK (
(consentement_photo = true AND date_consentement_photo IS NOT NULL)
OR
(consentement_photo = false AND date_consentement_photo IS NULL)
);
-- =================
-- ENFANTS_PARENTS
-- =================
-- (PK composite déjà en place, rien à ajouter ici)
-- ========
-- DOSSIERS
-- ========
ALTER TABLE dossiers
ADD CONSTRAINT chk_dossiers_budget_nonneg
CHECK (budget IS NULL OR budget >= 0),
ADD CONSTRAINT chk_dossiers_type_contrat_non_vide
CHECK (type_contrat IS NULL OR btrim(type_contrat) <> ''),
ADD CONSTRAINT chk_dossiers_planning_json
CHECK (planning_souhaite IS NULL OR jsonb_typeof(planning_souhaite) = 'object');
-- ========
-- MESSAGES
-- ========
-- Contenu obligatoire, non vide
ALTER TABLE messages
ALTER COLUMN contenu SET NOT NULL;
ALTER TABLE messages
ADD CONSTRAINT chk_messages_contenu_non_vide
CHECK (btrim(contenu) <> '');
-- =========
-- CONTRATS
-- =========
ALTER TABLE contrats
ADD CONSTRAINT chk_contrats_tarif_nonneg
CHECK (tarif_horaire IS NULL OR tarif_horaire >= 0),
ADD CONSTRAINT chk_contrats_indemnites_nonneg
CHECK (indemnites_repas IS NULL OR indemnites_repas >= 0);
-- ==================
-- AVENANTS_CONTRATS
-- ==================
-- Rien de spécifique (statut enum déjà en place)
-- =========
-- EVENEMENTS
-- =========
ALTER TABLE evenements
ADD CONSTRAINT chk_evenements_dates_coherentes
CHECK (date_fin IS NULL OR date_debut IS NULL OR date_fin >= date_debut);
-- =================
-- SIGNALEMENTS_BUGS
-- =================
-- Description obligatoire, non vide
ALTER TABLE signalements_bugs
ALTER COLUMN description SET NOT NULL;
ALTER TABLE signalements_bugs
ADD CONSTRAINT chk_bugs_description_non_vide
CHECK (btrim(description) <> '');
-- =======
-- UPLOADS
-- =======
-- URL obligatoire + format basique (chemin absolu ou http(s))
ALTER TABLE uploads
ALTER COLUMN fichier_url SET NOT NULL;
ALTER TABLE uploads
ADD CONSTRAINT chk_uploads_url_format
CHECK (fichier_url ~ '^(https?://.+|/[^\\s]+)$');
-- =============
-- NOTIFICATIONS
-- =============
-- Contenu obligatoire, non vide
ALTER TABLE notifications
ALTER COLUMN contenu SET NOT NULL;
ALTER TABLE notifications
ADD CONSTRAINT chk_notifications_contenu_non_vide
CHECK (btrim(contenu) <> '');
-- ===========
-- VALIDATIONS
-- ===========
-- Rien de plus ici (Sprint 1 Ticket 8 enrichira la table)
-190
View File
@@ -1,190 +0,0 @@
-- ==========================================================
-- 04_fk_policies.sql : normalisation des politiques ON DELETE
-- A exécuter après 01_init.sql et 03_checks.sql
-- ==========================================================
-- Helper: Drop FK d'une table/colonne si elle existe (par son/leurs noms de colonne)
-- puis recrée la contrainte avec la clause fournie
-- Utilise information_schema pour retrouver le nom de contrainte auto-généré
-- NB: schema = public
-- ========== messages.id_expediteur -> utilisateurs.id : SET NULL (au lieu de CASCADE)
DO $$
DECLARE
conname text;
BEGIN
SELECT tc.constraint_name INTO conname
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
WHERE tc.table_schema='public'
AND tc.table_name='messages'
AND tc.constraint_type='FOREIGN KEY'
AND kcu.column_name='id_expediteur';
IF conname IS NOT NULL THEN
EXECUTE format('ALTER TABLE public.messages DROP CONSTRAINT %I', conname);
END IF;
EXECUTE $sql$
ALTER TABLE public.messages
ADD CONSTRAINT fk_messages_id_expediteur
FOREIGN KEY (id_expediteur) REFERENCES public.utilisateurs(id) ON DELETE SET NULL
$sql$;
END $$;
-- ========== parents.id_co_parent -> utilisateurs.id : SET NULL
DO $$
DECLARE conname text;
BEGIN
SELECT tc.constraint_name INTO conname
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
WHERE tc.table_schema='public'
AND tc.table_name='parents'
AND tc.constraint_type='FOREIGN KEY'
AND kcu.column_name='id_co_parent';
IF conname IS NOT NULL THEN
EXECUTE format('ALTER TABLE public.parents DROP CONSTRAINT %I', conname);
END IF;
EXECUTE $sql$
ALTER TABLE public.parents
ADD CONSTRAINT fk_parents_id_co_parent
FOREIGN KEY (id_co_parent) REFERENCES public.utilisateurs(id) ON DELETE SET NULL
$sql$;
END $$;
-- ========== avenants_contrats.initie_par -> utilisateurs.id : SET NULL
DO $$
DECLARE conname text;
BEGIN
SELECT tc.constraint_name INTO conname
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
WHERE tc.table_schema='public'
AND tc.table_name='avenants_contrats'
AND tc.constraint_type='FOREIGN KEY'
AND kcu.column_name='initie_par';
IF conname IS NOT NULL THEN
EXECUTE format('ALTER TABLE public.avenants_contrats DROP CONSTRAINT %I', conname);
END IF;
EXECUTE $sql$
ALTER TABLE public.avenants_contrats
ADD CONSTRAINT fk_avenants_contrats_initie_par
FOREIGN KEY (initie_par) REFERENCES public.utilisateurs(id) ON DELETE SET NULL
$sql$;
END $$;
-- ========== evenements.id_am -> utilisateurs.id : SET NULL
DO $$
DECLARE conname text;
BEGIN
SELECT tc.constraint_name INTO conname
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
WHERE tc.table_schema='public'
AND tc.table_name='evenements'
AND tc.constraint_type='FOREIGN KEY'
AND kcu.column_name='id_am';
IF conname IS NOT NULL THEN
EXECUTE format('ALTER TABLE public.evenements DROP CONSTRAINT %I', conname);
END IF;
EXECUTE $sql$
ALTER TABLE public.evenements
ADD CONSTRAINT fk_evenements_id_am
FOREIGN KEY (id_am) REFERENCES public.utilisateurs(id) ON DELETE SET NULL
$sql$;
END $$;
-- ========== evenements.id_parent -> parents.id_utilisateur : SET NULL
DO $$
DECLARE conname text;
BEGIN
SELECT tc.constraint_name INTO conname
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
WHERE tc.table_schema='public'
AND tc.table_name='evenements'
AND tc.constraint_type='FOREIGN KEY'
AND kcu.column_name='id_parent';
IF conname IS NOT NULL THEN
EXECUTE format('ALTER TABLE public.evenements DROP CONSTRAINT %I', conname);
END IF;
EXECUTE $sql$
ALTER TABLE public.evenements
ADD CONSTRAINT fk_evenements_id_parent
FOREIGN KEY (id_parent) REFERENCES public.parents(id_utilisateur) ON DELETE SET NULL
$sql$;
END $$;
-- ========== evenements.cree_par -> utilisateurs.id : SET NULL
DO $$
DECLARE conname text;
BEGIN
SELECT tc.constraint_name INTO conname
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
WHERE tc.table_schema='public'
AND tc.table_name='evenements'
AND tc.constraint_type='FOREIGN KEY'
AND kcu.column_name='cree_par';
IF conname IS NOT NULL THEN
EXECUTE format('ALTER TABLE public.evenements DROP CONSTRAINT %I', conname);
END IF;
EXECUTE $sql$
ALTER TABLE public.evenements
ADD CONSTRAINT fk_evenements_cree_par
FOREIGN KEY (cree_par) REFERENCES public.utilisateurs(id) ON DELETE SET NULL
$sql$;
END $$;
-- ========== signalements_bugs.id_utilisateur -> utilisateurs.id : SET NULL
DO $$
DECLARE conname text;
BEGIN
SELECT tc.constraint_name INTO conname
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
WHERE tc.table_schema='public'
AND tc.table_name='signalements_bugs'
AND tc.constraint_type='FOREIGN KEY'
AND kcu.column_name='id_utilisateur';
IF conname IS NOT NULL THEN
EXECUTE format('ALTER TABLE public.signalements_bugs DROP CONSTRAINT %I', conname);
END IF;
EXECUTE $sql$
ALTER TABLE public.signalements_bugs
ADD CONSTRAINT fk_signalements_bugs_id_utilisateur
FOREIGN KEY (id_utilisateur) REFERENCES public.utilisateurs(id) ON DELETE SET NULL
$sql$;
END $$;
-- ========== validations.id_utilisateur -> utilisateurs.id : SET NULL
DO $$
DECLARE conname text;
BEGIN
SELECT tc.constraint_name INTO conname
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
WHERE tc.table_schema='public'
AND tc.table_name='validations'
AND tc.constraint_type='FOREIGN KEY'
AND kcu.column_name='id_utilisateur';
IF conname IS NOT NULL THEN
EXECUTE format('ALTER TABLE public.validations DROP CONSTRAINT %I', conname);
END IF;
EXECUTE $sql$
ALTER TABLE public.validations
ADD CONSTRAINT fk_validations_id_utilisateur
FOREIGN KEY (id_utilisateur) REFERENCES public.utilisateurs(id) ON DELETE SET NULL
$sql$;
END $$;
-- NB:
-- D'autres FK déjà correctes : CASCADE (assistantes_maternelles, parents, enfants_parents, dossiers, messages.id_dossier, contrats, avenants_contrats.id_contrat, evenements.id_enfant), SET NULL (uploads).
-- On laisse ON UPDATE par défaut (NO ACTION), car les UUID ne changent pas.
-150
View File
@@ -1,150 +0,0 @@
-- =============================================
-- 05_triggers.sql : Timestamps automatiques
-- - Ajoute (si absent) cree_le DEFAULT now() et modifie_le DEFAULT now()
-- - Crée un trigger BEFORE UPDATE pour mettre à jour modifie_le
-- - Idempotent (DROP TRIGGER IF EXISTS / IF NOT EXISTS)
-- A exécuter après 01_init.sql, 02_indexes.sql, 03_checks.sql
-- =============================================
-- 1) Fonction unique de mise à jour du timestamp
CREATE OR REPLACE FUNCTION set_modifie_le()
RETURNS TRIGGER AS $$
BEGIN
NEW.modifie_le := NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Helper macro-like: pour chaque table, s'assurer des colonnes & trigger
-- (on ne peut pas faire de macro, donc on répète pour chaque table)
-- Liste des tables concernées :
-- utilisateurs, assistantes_maternelles, parents, enfants, enfants_parents,
-- dossiers, messages, contrats, avenants_contrats, evenements,
-- signalements_bugs, uploads, notifications, validations
-- ========== UTILISATEURS
ALTER TABLE utilisateurs
ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(),
ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW();
DROP TRIGGER IF EXISTS trg_utilisateurs_set_modifie_le ON utilisateurs;
CREATE TRIGGER trg_utilisateurs_set_modifie_le
BEFORE UPDATE ON utilisateurs
FOR EACH ROW EXECUTE FUNCTION set_modifie_le();
-- ========== ASSISTANTES_MATERNELLES
ALTER TABLE assistantes_maternelles
ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(),
ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW();
DROP TRIGGER IF EXISTS trg_am_set_modifie_le ON assistantes_maternelles;
CREATE TRIGGER trg_am_set_modifie_le
BEFORE UPDATE ON assistantes_maternelles
FOR EACH ROW EXECUTE FUNCTION set_modifie_le();
-- ========== PARENTS
ALTER TABLE parents
ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(),
ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW();
DROP TRIGGER IF EXISTS trg_parents_set_modifie_le ON parents;
CREATE TRIGGER trg_parents_set_modifie_le
BEFORE UPDATE ON parents
FOR EACH ROW EXECUTE FUNCTION set_modifie_le();
-- ========== ENFANTS
ALTER TABLE enfants
ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(),
ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW();
DROP TRIGGER IF EXISTS trg_enfants_set_modifie_le ON enfants;
CREATE TRIGGER trg_enfants_set_modifie_le
BEFORE UPDATE ON enfants
FOR EACH ROW EXECUTE FUNCTION set_modifie_le();
-- ========== ENFANTS_PARENTS (table de liaison)
ALTER TABLE enfants_parents
ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(),
ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW();
DROP TRIGGER IF EXISTS trg_enfants_parents_set_modifie_le ON enfants_parents;
CREATE TRIGGER trg_enfants_parents_set_modifie_le
BEFORE UPDATE ON enfants_parents
FOR EACH ROW EXECUTE FUNCTION set_modifie_le();
-- ========== DOSSIERS
ALTER TABLE dossiers
ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(),
ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW();
DROP TRIGGER IF EXISTS trg_dossiers_set_modifie_le ON dossiers;
CREATE TRIGGER trg_dossiers_set_modifie_le
BEFORE UPDATE ON dossiers
FOR EACH ROW EXECUTE FUNCTION set_modifie_le();
-- ========== MESSAGES
ALTER TABLE messages
ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(),
ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW();
DROP TRIGGER IF EXISTS trg_messages_set_modifie_le ON messages;
CREATE TRIGGER trg_messages_set_modifie_le
BEFORE UPDATE ON messages
FOR EACH ROW EXECUTE FUNCTION set_modifie_le();
-- ========== CONTRATS
ALTER TABLE contrats
ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(),
ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW();
DROP TRIGGER IF EXISTS trg_contrats_set_modifie_le ON contrats;
CREATE TRIGGER trg_contrats_set_modifie_le
BEFORE UPDATE ON contrats
FOR EACH ROW EXECUTE FUNCTION set_modifie_le();
-- ========== AVENANTS_CONTRATS
ALTER TABLE avenants_contrats
ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(),
ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW();
DROP TRIGGER IF EXISTS trg_avenants_contrats_set_modifie_le ON avenants_contrats;
CREATE TRIGGER trg_avenants_contrats_set_modifie_le
BEFORE UPDATE ON avenants_contrats
FOR EACH ROW EXECUTE FUNCTION set_modifie_le();
-- ========== EVENEMENTS
ALTER TABLE evenements
ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(),
ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW();
DROP TRIGGER IF EXISTS trg_evenements_set_modifie_le ON evenements;
CREATE TRIGGER trg_evenements_set_modifie_le
BEFORE UPDATE ON evenements
FOR EACH ROW EXECUTE FUNCTION set_modifie_le();
-- ========== SIGNALEMENTS_BUGS
ALTER TABLE signalements_bugs
ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(),
ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW();
DROP TRIGGER IF EXISTS trg_signalements_bugs_set_modifie_le ON signalements_bugs;
CREATE TRIGGER trg_signalements_bugs_set_modifie_le
BEFORE UPDATE ON signalements_bugs
FOR EACH ROW EXECUTE FUNCTION set_modifie_le();
-- ========== UPLOADS
ALTER TABLE uploads
ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(),
ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW();
DROP TRIGGER IF EXISTS trg_uploads_set_modifie_le ON uploads;
CREATE TRIGGER trg_uploads_set_modifie_le
BEFORE UPDATE ON uploads
FOR EACH ROW EXECUTE FUNCTION set_modifie_le();
-- ========== NOTIFICATIONS
ALTER TABLE notifications
ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(),
ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW();
DROP TRIGGER IF EXISTS trg_notifications_set_modifie_le ON notifications;
CREATE TRIGGER trg_notifications_set_modifie_le
BEFORE UPDATE ON notifications
FOR EACH ROW EXECUTE FUNCTION set_modifie_le();
-- ========== VALIDATIONS
ALTER TABLE validations
ADD COLUMN IF NOT EXISTS cree_le TIMESTAMP DEFAULT NOW(),
ADD COLUMN IF NOT EXISTS modifie_le TIMESTAMP DEFAULT NOW();
DROP TRIGGER IF EXISTS trg_validations_set_modifie_le ON validations;
CREATE TRIGGER trg_validations_set_modifie_le
BEFORE UPDATE ON validations
FOR EACH ROW EXECUTE FUNCTION set_modifie_le();
@@ -1,53 +0,0 @@
-- ==========================================================
-- 06_validations_enrich.sql : Traçabilité complète des validations
-- - Ajoute la colonne 'valide_par' (FK -> utilisateurs.id)
-- - ON DELETE SET NULL pour conserver l'historique
-- - Ajoute index utiles pour les requêtes (valideur, statut, date)
-- A exécuter après : 01_init.sql, 02_indexes.sql, 03_checks.sql, 04_fk_policies.sql, 05_triggers.sql
-- ==========================================================
BEGIN;
-- 1) Colonne 'valide_par' si absente
ALTER TABLE validations
ADD COLUMN IF NOT EXISTS valide_par UUID NULL;
-- 2) FK vers utilisateurs(id), ON DELETE SET NULL
DO $$
DECLARE conname text;
BEGIN
SELECT tc.constraint_name INTO conname
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
AND tc.table_schema = kcu.table_schema
WHERE tc.table_schema = 'public'
AND tc.table_name = 'validations'
AND tc.constraint_type= 'FOREIGN KEY'
AND kcu.column_name = 'valide_par';
IF conname IS NOT NULL THEN
EXECUTE format('ALTER TABLE public.validations DROP CONSTRAINT %I', conname);
END IF;
EXECUTE $sql$
ALTER TABLE public.validations
ADD CONSTRAINT fk_validations_valide_par
FOREIGN KEY (valide_par) REFERENCES public.utilisateurs(id) ON DELETE SET NULL
$sql$;
END $$;
-- 3) Index pour accélérer les recherches
-- - qui a validé quoi récemment ?
-- - toutes les validations par statut / par date
CREATE INDEX IF NOT EXISTS idx_validations_valide_par_cree_le
ON validations (valide_par, cree_le);
-- Certains existent peut-être déjà : on sécurise
CREATE INDEX IF NOT EXISTS idx_validations_id_utilisateur_cree_le
ON validations (id_utilisateur, cree_le);
CREATE INDEX IF NOT EXISTS idx_validations_statut
ON validations (statut);
COMMIT;
-42
View File
@@ -1,42 +0,0 @@
-- Script d'importation des données CSV dans la base Postgres du docker dev
-- À exécuter dans le conteneur ou via psql connecté à la base
-- psql -U admin -d ptitpas_db -f /docker-entrypoint-initdb.d/07_import.sql
-- Exemple d'utilisation :
-- Import utilisateurs
\copy utilisateurs FROM 'bdd/data_test/utilisateurs.csv' DELIMITER ',' CSV HEADER;
-- Import assistantes_maternelles
\copy assistantes_maternelles FROM 'bdd/data_test/assistantes_maternelles.csv' DELIMITER ',' CSV HEADER;
-- Import parents
\copy parents FROM 'bdd/data_test/parents.csv' DELIMITER ',' CSV HEADER;
-- Import enfants
\copy enfants FROM 'bdd/data_test/enfants.csv' DELIMITER ',' CSV HEADER;
-- Import enfants_parents
\copy enfants_parents FROM 'bdd/data_test/enfants_parents.csv' DELIMITER ',' CSV HEADER;
-- Import dossiers
\copy dossiers FROM 'bdd/data_test/dossiers.csv' DELIMITER ',' CSV HEADER;
-- Import contrats
\copy contrats FROM 'bdd/data_test/contrats.csv' DELIMITER ',' CSV HEADER;
-- Import validations
\copy validations FROM 'bdd/data_test/validations.csv' DELIMITER ',' CSV HEADER;
-- Import notifications
\copy notifications FROM 'bdd/data_test/notifications.csv' DELIMITER ',' CSV HEADER;
-- Import uploads
\copy uploads FROM 'bdd/data_test/uploads.csv' DELIMITER ',' CSV HEADER;
-- Import evenements
\copy evenements FROM 'bdd/data_test/evenements.csv' DELIMITER ',' CSV HEADER;
-- Remarque :
-- Les chemins doivent être accessibles depuis le conteneur Docker (monter le dossier si besoin)
-- Adapter l'utilisateur, la base et le chemin si nécessaire
+73
View File
@@ -0,0 +1,73 @@
-- ============================================================
-- 03_seed_test_data.sql : Données de test complètes (dashboard admin)
-- Aligné sur utilisateurs-test-complet.json
-- Mot de passe universel : password (bcrypt)
-- À exécuter après BDD.sql (init DB)
-- ============================================================
BEGIN;
-- Hash bcrypt pour "password" (10 rounds)
-- ========== UTILISATEURS (1 admin + 1 gestionnaire + 2 AM + 5 parents) ==========
-- On garde admin@ptits-pas.fr (super_admin) déjà créé par BDD.sql
INSERT INTO utilisateurs (id, email, password, prenom, nom, role, statut, telephone, adresse, ville, code_postal, profession, situation_familiale, date_naissance, consentement_photo)
VALUES
('a0000001-0001-0001-0001-000000000001', 'sophie.bernard@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Sophie', 'BERNARD', 'administrateur', 'actif', '0678123456', '12 Avenue Gabriel Péri', 'Bezons', '95870', 'Responsable administrative', 'marie', '1978-03-15', false),
('a0000002-0002-0002-0002-000000000002', 'lucas.moreau@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Lucas', 'MOREAU', 'gestionnaire', 'actif', '0687234567', '8 Rue Jean Jaurès', 'Bezons', '95870', 'Gestionnaire des placements', 'celibataire', '1985-09-22', false),
('a0000003-0003-0003-0003-000000000003', 'marie.dubois@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Marie', 'DUBOIS', 'assistante_maternelle', 'actif', '0696345678', '25 Rue de la République', 'Bezons', '95870', 'Assistante maternelle', 'marie', '1980-06-08', true),
('a0000004-0004-0004-0004-000000000004', 'fatima.elmansouri@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Fatima', 'EL MANSOURI', 'assistante_maternelle', 'actif', '0675456789', '17 Boulevard Aristide Briand', 'Bezons', '95870', 'Assistante maternelle', 'marie', '1975-11-12', true),
('a0000005-0005-0005-0005-000000000005', 'claire.martin@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Claire', 'MARTIN', 'parent', 'actif', '0689567890', '5 Avenue du Général de Gaulle', 'Bezons', '95870', 'Infirmière', 'marie', '1990-04-03', false),
('a0000006-0006-0006-0006-000000000006', 'thomas.martin@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Thomas', 'MARTIN', 'parent', 'actif', '0678456789', '5 Avenue du Général de Gaulle', 'Bezons', '95870', 'Ingénieur', 'marie', '1988-07-18', false),
('a0000007-0007-0007-0007-000000000007', 'amelie.durand@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Amélie', 'DURAND', 'parent', 'actif', '0667788990', '23 Rue Victor Hugo', 'Bezons', '95870', 'Comptable', 'divorce', '1987-12-14', false),
('a0000008-0008-0008-0008-000000000008', 'julien.rousseau@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Julien', 'ROUSSEAU', 'parent', 'actif', '0656677889', '14 Rue Pasteur', 'Bezons', '95870', 'Commercial', 'divorce', '1985-08-29', false),
('a0000009-0009-0009-0009-000000000009', 'david.lecomte@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'David', 'LECOMTE', 'parent', 'actif', '0645566778', '31 Rue Émile Zola', 'Bezons', '95870', 'Développeur web', 'parent_isole', '1992-10-07', false)
ON CONFLICT (email) DO NOTHING;
-- ========== PARENTS (avec co-parent pour le couple Martin) ==========
INSERT INTO parents (id_utilisateur, id_co_parent)
VALUES
('a0000005-0005-0005-0005-000000000005', 'a0000006-0006-0006-0006-000000000006'),
('a0000006-0006-0006-0006-000000000006', 'a0000005-0005-0005-0005-000000000005'),
('a0000007-0007-0007-0007-000000000007', NULL),
('a0000008-0008-0008-0008-000000000008', NULL),
('a0000009-0009-0009-0009-000000000009', NULL)
ON CONFLICT (id_utilisateur) DO NOTHING;
-- ========== ASSISTANTES MATERNELLES ==========
INSERT INTO assistantes_maternelles (id_utilisateur, numero_agrement, nir_chiffre, nb_max_enfants, biographie, date_agrement, ville_residence, disponible, place_disponible)
VALUES
('a0000003-0003-0003-0003-000000000003', 'AGR-2019-095001', '280069512345671', 4, 'Assistante maternelle agréée depuis 2019. Spécialité bébés 0-18 mois. Accueil bienveillant et cadre sécurisant. 2 places disponibles.', '2019-09-01', 'Bezons', true, 2),
('a0000004-0004-0004-0004-000000000004', 'AGR-2017-095002', '275119512345672', 3, 'Assistante maternelle expérimentée. Spécialité 1-3 ans. Accueil à la journée. 1 place disponible.', '2017-06-15', 'Bezons', true, 1)
ON CONFLICT (id_utilisateur) DO NOTHING;
-- ========== ENFANTS ==========
INSERT INTO enfants (id, prenom, nom, genre, date_naissance, statut, est_multiple)
VALUES
('e0000001-0001-0001-0001-000000000001', 'Emma', 'MARTIN', 'F', '2023-02-15', 'actif', true),
('e0000002-0002-0002-0002-000000000002', 'Noah', 'MARTIN', 'H', '2023-02-15', 'actif', true),
('e0000003-0003-0003-0003-000000000003', 'Léa', 'MARTIN', 'F', '2023-02-15', 'actif', true),
('e0000004-0004-0004-0004-000000000004', 'Chloé', 'ROUSSEAU', 'F', '2022-04-20', 'actif', false),
('e0000005-0005-0005-0005-000000000005', 'Hugo', 'ROUSSEAU', 'H', '2024-03-10', 'actif', false),
('e0000006-0006-0006-0006-000000000006', 'Maxime', 'LECOMTE', 'H', '2023-04-15', 'actif', false)
ON CONFLICT (id) DO NOTHING;
-- ========== ENFANTS_PARENTS (liaison N:N) ==========
-- Martin (Claire + Thomas) -> Emma, Noah, Léa
INSERT INTO enfants_parents (id_parent, id_enfant)
VALUES
('a0000005-0005-0005-0005-000000000005', 'e0000001-0001-0001-0001-000000000001'),
('a0000005-0005-0005-0005-000000000005', 'e0000002-0002-0002-0002-000000000002'),
('a0000005-0005-0005-0005-000000000005', 'e0000003-0003-0003-0003-000000000003'),
('a0000006-0006-0006-0006-000000000006', 'e0000001-0001-0001-0001-000000000001'),
('a0000006-0006-0006-0006-000000000006', 'e0000002-0002-0002-0002-000000000002'),
('a0000006-0006-0006-0006-000000000006', 'e0000003-0003-0003-0003-000000000003'),
('a0000007-0007-0007-0007-000000000007', 'e0000004-0004-0004-0004-000000000004'),
('a0000007-0007-0007-0007-000000000007', 'e0000005-0005-0005-0005-000000000005'),
('a0000008-0008-0008-0008-000000000008', 'e0000004-0004-0004-0004-000000000004'),
('a0000008-0008-0008-0008-000000000008', 'e0000005-0005-0005-0005-000000000005'),
('a0000009-0009-0009-0009-000000000009', 'e0000006-0006-0006-0006-000000000006')
ON CONFLICT DO NOTHING;
COMMIT;
+3 -1
View File
@@ -9,7 +9,7 @@ services:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB} POSTGRES_DB: ${POSTGRES_DB}
volumes: volumes:
- ./database/migrations/01_init.sql:/docker-entrypoint-initdb.d/01_init.sql - ./database/BDD.sql:/docker-entrypoint-initdb.d/01_init.sql
- postgres_data:/var/lib/postgresql/data - postgres_data:/var/lib/postgresql/data
networks: networks:
- ptitspas_network - ptitspas_network
@@ -55,6 +55,8 @@ services:
JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET} JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET}
JWT_REFRESH_EXPIRES: ${JWT_REFRESH_EXPIRES} JWT_REFRESH_EXPIRES: ${JWT_REFRESH_EXPIRES}
NODE_ENV: ${NODE_ENV} NODE_ENV: ${NODE_ENV}
LOG_API_REQUESTS: ${LOG_API_REQUESTS:-false}
CONFIG_ENCRYPTION_KEY: ${CONFIG_ENCRYPTION_KEY}
depends_on: depends_on:
- database - database
labels: labels:
+9
View File
@@ -13,12 +13,21 @@ Ce fichier sert d'index pour naviguer dans toute la documentation du projet.
- [**02 - Architecture**](./02_ARCHITECTURE.md) - Vue d'ensemble de l'architecture mono-repo et multi-conteneurs - [**02 - Architecture**](./02_ARCHITECTURE.md) - Vue d'ensemble de l'architecture mono-repo et multi-conteneurs
- [**03 - Déploiement**](./03_DEPLOYMENT.md) - Guide complet de déploiement et configuration CI/CD - [**03 - Déploiement**](./03_DEPLOYMENT.md) - Guide complet de déploiement et configuration CI/CD
### Planification
- [**04 - Roadmap Générale**](./04_ROADMAP-GENERALE.md) - Roadmap complète du projet (Phases 1 à 5+)
### Développement ### Développement
- [**10 - Database Schema**](./10_DATABASE.md) - Schéma de la base de données et modèles - [**10 - Database Schema**](./10_DATABASE.md) - Schéma de la base de données et modèles
- [**11 - API Documentation**](./11_API.md) - Documentation complète des endpoints REST - [**11 - API Documentation**](./11_API.md) - Documentation complète des endpoints REST
### Workflows Fonctionnels ### Workflows Fonctionnels
- [**20 - Workflow Création de Compte**](./20_WORKFLOW-CREATION-COMPTE.md) - Workflow complet de création et validation des comptes utilisateurs - [**20 - Workflow Création de Compte**](./20_WORKFLOW-CREATION-COMPTE.md) - Workflow complet de création et validation des comptes utilisateurs
- [**21 - Configuration Système**](./21_CONFIGURATION-SYSTEME.md) - Configuration on-premise dynamique
- [**22 - Documents Légaux**](./22_DOCUMENTS-LEGAUX.md) - Gestion CGU/Privacy avec versioning
- [**23 - Liste des Tickets**](./23_LISTE-TICKETS.md) - 61 tickets Phase 1 détaillés
- [**24 - Décisions Projet**](./24_DECISIONS-PROJET.md) - Décisions architecturales et fonctionnelles
- [**25 - Backlog Phase 2**](./25_PHASE-2-BACKLOG.md) - Fonctionnalités techniques reportées
- [**26 - API Gitea**](./26_GITEA-API.md) - Procédure d'utilisation de l'API Gitea (issues, PR, branches, labels)
### Administration (À créer) ### Administration (À créer)
- [**30 - Guide d'administration**](./30_ADMIN.md) - Gestion des utilisateurs, accès PgAdmin, logs - [**30 - Guide d'administration**](./30_ADMIN.md) - Gestion des utilisateurs, accès PgAdmin, logs
+330
View File
@@ -0,0 +1,330 @@
# 🗺️ Roadmap Générale - Projet P'titsPas
**Version** : 1.0
**Date** : 28 Novembre 2025
**Auteur** : Équipe PtitsPas
---
## ⚠️ Avertissement
Les **Phases 2, 3, 4+** sont des **ébauches indicatives** qui seront affinées au fur et à mesure du développement et des retours utilisateurs. Certaines fonctionnalités mentionnées (comme la facturation) ne seront peut-être pas développées ou seront remplacées par d'autres priorités.
**Seule la Phase 1 est détaillée et validée.**
---
## 🎯 Vue d'ensemble
| Phase | Focus | Estimation | Statut |
|-------|-------|------------|--------|
| **Phase 1** | Comptes & Auth | ~173h | ✅ Détaillée (61 tickets) |
| **Phase 2** | Recherche & Contact | ~100h | 📋 Ébauche |
| **Phase 3** | Contrats & Planning | ~120h | 📋 Ébauche |
| **Phase 4** | Suivi & Avancé | ~140h+ | 📋 Ébauche |
| **Phase 5+** | Optimisations | ~200h+ | 📋 Ébauche |
| **TOTAL** | | **~733h+** | |
---
## 📦 Phase 1 (v1.0.0) - 🔐 Création de comptes & Authentification
**Objectif** : MVP fonctionnel avec gestion des utilisateurs
### Fonctionnalités
- ✅ Configuration système (on-premise)
- ✅ Authentification & Sécurité
- ✅ Inscription Parents (workflow 6 étapes)
- ✅ Inscription Assistantes Maternelles (workflow 5 panneaux)
- ✅ Validation par Gestionnaires (dashboard 2 onglets)
- ✅ Documents légaux (CGU/Privacy avec versioning)
- ✅ Upload photos (enfants, AM)
- ✅ Notifications email (validation, refus, création MDP)
- ✅ Logging & Monitoring
- ✅ Tests & Documentation
### Versions incrémentales
| Version | Objectif | Tickets | Estimation |
|---------|----------|---------|------------|
| **0.1.0** | MVP Fonctionnel | ~21 | ~45h |
| **0.2.0** | Sécurité & RGPD | ~10 | ~35h |
| **0.3.0** | Interfaces Complètes | ~17 | ~52h |
| **0.4.0** | Tests & Documentation | ~6 | ~24h |
| **0.5.0** | Monitoring & Optimisations | ~7 | ~17h |
| **1.0.0** | 🎉 **Release Phase 1** | **61** | **~173h** |
### Livrable
Application installable avec création et validation de comptes utilisateurs.
**Référence** : [23_LISTE-TICKETS.md](./23_LISTE-TICKETS.md)
---
## 📦 Phase 2 (v2.0.0) - 🤝 Mise en relation & Communication
**Objectif** : Permettre aux parents de trouver et contacter des assistantes maternelles
### Fonctionnalités (ébauche)
- 🔍 **Recherche d'AM**
- Recherche par critères (ville, capacité, disponibilité, tarifs)
- Filtres avancés
- Géolocalisation (optionnel)
- 👤 **Profils détaillés AM**
- Présentation complète
- Photos du lieu de garde
- Expérience et qualifications
- Avis/Témoignages (optionnel)
- 💬 **Messagerie interne**
- Conversations sécurisées Parent ↔ AM
- Pièces jointes
- Historique des échanges
- 📨 **Demandes de contact**
- Workflow de demande Parent → AM
- Validation/Refus par AM
- Notifications
- ⭐ **Favoris/Shortlist**
- AM sauvegardées par parents
- Comparaison de profils
### Estimation
~100h (à affiner)
### Livrable
Parents peuvent trouver, consulter et contacter des assistantes maternelles.
---
## 📦 Phase 3 (v3.0.0) - 📄 Contrats & Planning
**Objectif** : Formaliser les gardes et gérer les plannings
### Fonctionnalités (ébauche)
- 📄 **Gestion des contrats**
- Création contrats (modèle type personnalisable)
- Signature électronique ou validation
- Stockage documents contractuels (PDF)
- Historique des contrats
- Renouvellement/Modification
- 📅 **Planning & Disponibilités**
- Calendrier AM (disponibilités, absences, congés)
- Réservations/Demandes de garde
- Validation/Refus par AM
- Vue planning Parent (enfants gardés)
- Alertes conflits de planning
- Export calendrier (iCal)
### Estimation
~120h (à affiner)
### Livrable
Contrats formalisés + Planning opérationnel pour gérer les gardes.
---
## 📦 Phase 4 (v4.0.0) - 📊 Suivi & Fonctionnalités avancées
**Objectif** : Suivi quotidien des enfants et fonctionnalités complémentaires
### Fonctionnalités (ébauche)
- 📔 **Suivi des Enfants (Carnet de liaison numérique)**
- Activités quotidiennes (repas, sieste, jeux)
- Photos/Vidéos sécurisées (partage Parent ↔ AM)
- Notes/Observations
- Suivi médical (médicaments, allergies, vaccins)
- Historique complet par enfant
- Export PDF (bilan mensuel)
- 🎯 **Autres fonctionnalités à définir**
- ⚠️ **Pas de facturation** (décision validée)
- Fonctionnalités à déterminer selon retours utilisateurs Phase 2 et 3
### Estimation
~140h+ (à affiner)
### Livrable
Suivi quotidien des enfants + Fonctionnalités complémentaires.
---
## 📦 Phase 5+ (v5.0.0+) - 🚀 Optimisations & Améliorations
**Objectif** : Optimisations, monitoring, et fonctionnalités premium
### Fonctionnalités (ébauche)
#### 📊 Statistiques & Reporting
- Dashboard gestionnaire (stats inscriptions, validations)
- Rapports collectivité (CSV/PDF)
- Graphiques évolution
- Tableaux de bord personnalisés
#### 🔒 RGPD avancé
- Droit à l'oubli (suppression compte)
- Export données personnelles (portabilité)
- Anonymisation automatique comptes inactifs
- Audit trail complet
#### 📈 Monitoring & Infrastructure
- Métriques système (CPU, RAM, BDD)
- Dashboard monitoring admin
- Sauvegarde automatique BDD (cron)
- Procédures de restauration
- Alertes automatiques
#### 📚 Documentation & Formation
- Guides utilisateur (Gestionnaire, Parent, AM)
- Vidéos tutoriels
- FAQ interactive
- Base de connaissances
#### 🎨 Améliorations UX
- Mode sombre
- Notifications push (PWA)
- Accessibilité (WCAG 2.1)
- Multi-langue (i18n)
- Responsive avancé
#### 🌟 Fonctionnalités Premium (optionnel)
- Géolocalisation AM (carte interactive)
- Système d'avis/notation
- Badges/Certifications AM
- Intégrations tierces (CAF, etc.)
- Application mobile native
### Estimation
~200h+ (à affiner)
### Livrable
Application mature, optimisée et riche en fonctionnalités.
**Référence** : [25_PHASE-2-BACKLOG.md](./25_PHASE-2-BACKLOG.md) (anciennes fonctionnalités techniques)
---
## 🎯 Logique de progression
```
Phase 1 : "Je peux créer un compte"
↓
Phase 2 : "Je peux trouver et contacter une AM"
↓
Phase 3 : "Je peux signer un contrat et gérer le planning"
↓
Phase 4 : "Je peux suivre mon enfant au quotidien"
↓
Phase 5+ : "L'application est optimisée et riche en fonctionnalités"
```
---
## 🔢 Schéma de versioning
```
X.Y.Z
X = Phase majeure (0 = dev Phase 1, 1 = Phase 1 livrée, 2 = Phase 2 livrée, etc.)
Y = Version incrémentale dans la phase (0.1, 0.2, 0.3... → 1.0)
Z = Patch/Hotfix (0 par défaut, incrémenté pour corrections)
Exemples :
- 0.1.0 → Phase 1 en dev, Version 1 (MVP)
- 0.1.1 → Phase 1 en dev, Version 1, Patch 1 (correction bug)
- 0.2.0 → Phase 1 en dev, Version 2 (Sécurité)
- 1.0.0 → Livraison finale Phase 1
- 1.0.1 → Patch Phase 1
- 2.0.0 → Livraison finale Phase 2
- 3.0.0 → Livraison finale Phase 3
```
---
## 📅 Critères de passage entre phases
### Phase 1 → Phase 2
- ✅ Phase 1 terminée (61 tickets)
- ✅ Application déployée en production (au moins 1 collectivité)
- ✅ Utilisateurs réels (au moins 10 comptes validés)
- ✅ Feedback terrain collecté
- ✅ Bugs critiques corrigés
### Phase 2 → Phase 3
- ✅ Phase 2 terminée
- ✅ Recherche et messagerie utilisées activement
- ✅ Au moins 5 mises en relation réussies
- ✅ Feedback utilisateurs positif
- ✅ Besoin de formalisation des contrats exprimé
### Phase 3 → Phase 4
- ✅ Phase 3 terminée
- ✅ Contrats et planning utilisés activement
- ✅ Au moins 10 contrats signés
- ✅ Feedback utilisateurs positif
- ✅ Besoin de suivi quotidien exprimé
### Phase 4 → Phase 5+
- ✅ Phase 4 terminée
- ✅ Application stable en production
- ✅ Base utilisateurs significative (50+ comptes actifs)
- ✅ Demandes d'optimisations et fonctionnalités avancées
---
## 📝 Notes importantes
1. **Flexibilité** : Cette roadmap est indicative et sera ajustée en fonction :
- Des retours utilisateurs
- Des priorités des collectivités
- Des contraintes techniques découvertes
- Des évolutions réglementaires
2. **Priorisation** : Les fonctionnalités de chaque phase peuvent être réorganisées selon :
- L'urgence métier
- La valeur ajoutée
- La complexité technique
- Les dépendances
3. **Décisions actées** :
- ❌ Pas de facturation automatique (gestion externe)
- ❌ Pas de SMS (email uniquement)
- ✅ Application on-premise (auto-hébergée)
- ✅ Configuration dynamique (pas de hardcoding)
4. **Documentation** : Chaque phase aura sa propre documentation détaillée avant démarrage.
---
## 📚 Documents de référence
- [00_INDEX.md](./00_INDEX.md) - Index général de la documentation
- [01_CAHIER-DES-CHARGES.md](./01_CAHIER-DES-CHARGES.md) - Cahier des charges v1.3
- [20_WORKFLOW-CREATION-COMPTE.md](./20_WORKFLOW-CREATION-COMPTE.md) - Workflow création de comptes
- [23_LISTE-TICKETS.md](./23_LISTE-TICKETS.md) - Liste des 61 tickets Phase 1
- [24_DECISIONS-PROJET.md](./24_DECISIONS-PROJET.md) - Décisions architecturales
- [25_PHASE-2-BACKLOG.md](./25_PHASE-2-BACKLOG.md) - Anciennes fonctionnalités techniques
---
**Dernière mise à jour** : 28 Novembre 2025
**Version** : 1.0
**Statut** : 📋 Roadmap indicative - Phase 1 détaillée et validée
+37
View File
@@ -0,0 +1,37 @@
# Ticket #14 – Note pour modifications backend
**Contexte :** Première connexion admin → panneau Paramètres, déblocage après clic sur « Sauvegarder ». Le front appelle `POST /api/v1/configuration/setup/complete` au clic sur Sauvegarder.
## Problème
Erreur renvoyée par le back :
`invalid input syntax for type uuid: "system"`
- Le controller fait `const userId = req.user?.id || 'system'` puis `markSetupCompleted(userId)`.
- Le service `set()` fait `config.modifiePar = { id: userId }` ; la colonne `modifie_par` est une FK UUID vers `users`.
- La chaîne `"system"` n’est pas un UUID valide → erreur PostgreSQL.
## Modifications à apporter au backend
**Option A – Accepter l’absence d’utilisateur (recommandé si la route peut être appelée sans JWT)**
1. **`config.controller.ts`** (route `completeSetup`)
- Remplacer :
`const userId = req.user?.id || 'system';`
- Par :
`const userId = req.user?.id ?? null;`
2. **`config.service.ts`** (`markSetupCompleted`)
- Changer la signature :
`async markSetupCompleted(userId: string | null): Promise<void>`
- Et appeler :
`await this.set('setup_completed', 'true', userId ?? undefined);`
- Dans `set()`, ne pas remplir `modifiePar` quand `userId` est absent (déjà le cas si `if (userId)`).
**Option B – Imposer un utilisateur authentifié**
- Activer le guard JWT (et éventuellement RolesGuard) sur `POST /configuration/setup/complete` pour que `req.user` soit toujours défini, et garder `userId = req.user.id` (plus de fallback `'system'`).
---
Une fois le back modifié, le flux « Sauvegarder » → déblocage des panneaux fonctionne sans erreur.
+1 -1
View File
@@ -1,6 +1,6 @@
# 📋 Documentation Technique - Workflow de Création de Compte # 📋 Documentation Technique - Workflow de Création de Compte
**Version** : 1.0 **Version** : 1.0
**Date** : 24 Novembre 2025 **Date** : 24 Novembre 2025
**Auteur** : Équipe PtitsPas **Auteur** : Équipe PtitsPas
**Référence CDC** : Cahier des Charges P'titsPas V1.3 **Référence CDC** : Cahier des Charges P'titsPas V1.3
+667
View File
@@ -0,0 +1,667 @@
# 🔧 Documentation Technique - Configuration Système On-Premise
**Version** : 1.1
**Date** : 9 Février 2026
**Auteur** : Équipe PtitsPas
**Référence** : Architecture On-Premise
---
## 📖 Table des matières
1. [Vue d'ensemble](#vue-densemble)
2. [Architecture de configuration](#architecture-de-configuration)
3. [Table configuration](#table-configuration)
4. [Service Configuration](#service-configuration)
5. [Workflow Setup Initial](#workflow-setup-initial)
6. [APIs Configuration](#apis-configuration)
7. [Interface Admin](#interface-admin)
8. [Exemples de configuration](#exemples-de-configuration)
---
## 🎯 Vue d'ensemble
### Problématique
L'application P'titsPas est déployée **on-premise** chez différentes collectivités. Chaque collectivité a :
- Son propre serveur SMTP
- Ses propres ports et configurations réseau
- Son propre nom de domaine
- Sa propre charte graphique
**Solution** : Configuration dynamique stockée en base de données, modifiable via interface web.
### Principes
1. ✅ **Pas de hardcoding** : Aucune valeur en dur dans le code
2. ✅ **Pas de redéploiement** : Modification sans rebuild Docker
3. ✅ **Sécurité** : Mots de passe chiffrés en AES-256
4. ✅ **Traçabilité** : Qui a modifié quoi et quand
5. ✅ **Setup wizard** : Configuration guidée à la première connexion
6. ✅ **Validation** : Test SMTP avant sauvegarde
---
## 🏗️ Architecture de configuration
### Flux de données
```
┌─────────────────────────────────────────────────────────┐
│ Application │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Frontend │─────▶│ Backend │ │
│ │ (Admin) │ │ ConfigAPI │ │
│ └──────────────┘ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ ConfigService│ │
│ │ (Cache) │ │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ PostgreSQL │ │
│ │ configuration│ │
│ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
```
### Composants
1. **Table `configuration`** : Stockage clé/valeur en BDD
2. **ConfigService** : Cache en mémoire + chiffrement
3. **ConfigAPI** : Endpoints REST pour CRUD
4. **Guard Setup** : Redirection forcée si config incomplète
5. **Interface Admin** : Panneau Paramètres (3 sections) dans le dashboard, première config + accès permanent
---
## 📊 Table configuration
### Schéma SQL
```sql
-- Table de configuration système (clé/valeur)
CREATE TABLE configuration (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
cle VARCHAR(100) UNIQUE NOT NULL, -- Clé unique (ex: 'smtp_host')
valeur TEXT, -- Valeur (peut être NULL)
type VARCHAR(50) NOT NULL, -- Type: 'string', 'number', 'boolean', 'json', 'encrypted'
categorie VARCHAR(50), -- Catégorie: 'email', 'app', 'security'
description TEXT, -- Description pour l'interface admin
modifie_le TIMESTAMPTZ DEFAULT now(), -- Date dernière modification
modifie_par UUID REFERENCES utilisateurs(id) -- Qui a modifié (traçabilité)
);
-- Index pour performance
CREATE INDEX idx_configuration_cle ON configuration(cle);
CREATE INDEX idx_configuration_categorie ON configuration(categorie);
```
### Seed initial
```sql
INSERT INTO configuration (cle, valeur, type, categorie, description) VALUES
-- === Configuration Email (SMTP) ===
('smtp_host', 'localhost', 'string', 'email', 'Serveur SMTP (ex: mail.mairie-bezons.fr, smtp.gmail.com)'),
('smtp_port', '25', 'number', 'email', 'Port SMTP (25, 465, 587)'),
('smtp_secure', 'false', 'boolean', 'email', 'Utiliser SSL/TLS (true pour port 465)'),
('smtp_auth_required', 'false', 'boolean', 'email', 'Authentification SMTP requise'),
('smtp_user', '', 'string', 'email', 'Utilisateur SMTP (si authentification requise)'),
('smtp_password', '', 'encrypted', 'email', 'Mot de passe SMTP (chiffré en AES-256)'),
('email_from_name', 'P''titsPas', 'string', 'email', 'Nom de l''expéditeur affiché dans les emails'),
('email_from_address', 'no-reply@ptits-pas.fr', 'string', 'email', 'Adresse email de l''expéditeur'),
-- === Configuration Application ===
('app_name', 'P''titsPas', 'string', 'app', 'Nom de l''application (affiché dans l''interface)'),
('app_url', 'https://app.ptits-pas.fr', 'string', 'app', 'URL publique de l''application (pour les liens dans emails)'),
('app_logo_url', '/assets/logo.png', 'string', 'app', 'URL du logo de l''application'),
('setup_completed', 'false', 'boolean', 'app', 'Configuration initiale terminée'),
-- === Configuration Sécurité ===
('password_reset_token_expiry_days', '7', 'number', 'security', 'Durée de validité des tokens de création/réinitialisation de mot de passe (en jours)'),
('jwt_expiry_hours', '24', 'number', 'security', 'Durée de validité des sessions JWT (en heures)'),
('max_upload_size_mb', '5', 'number', 'security', 'Taille maximale des fichiers uploadés (en MB)'),
('bcrypt_rounds', '12', 'number', 'security', 'Nombre de rounds bcrypt pour le hachage des mots de passe');
```
### Types de données
| Type | Description | Exemple |
|------|-------------|---------|
| `string` | Chaîne de caractères | `"mail.example.com"` |
| `number` | Nombre entier ou décimal | `587` |
| `boolean` | Booléen | `true` / `false` |
| `json` | Objet JSON | `{"key": "value"}` |
| `encrypted` | Chaîne chiffrée AES-256 | `"a3f8b2..."` (hash) |
---
## 🔧 Service Configuration
### Responsabilités
1. **Cache en mémoire** : Chargement au démarrage
2. **Lecture** : `get(key, defaultValue)`
3. **Écriture** : `set(key, value, userId)`
4. **Chiffrement** : AES-256 pour type `encrypted`
5. **Conversion de types** : string → number/boolean/json
6. **Test SMTP** : Validation connexion
### Implémentation (TypeScript)
```typescript
// backend/src/config/config.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Configuration } from './entities/configuration.entity';
import * as crypto from 'crypto';
@Injectable()
export class ConfigService {
private cache: Map<string, any> = new Map();
private readonly ENCRYPTION_KEY = process.env.CONFIG_ENCRYPTION_KEY;
constructor(
@InjectRepository(Configuration)
private configRepo: Repository<Configuration>,
) {
this.loadCache();
}
// Chargement du cache au démarrage
async loadCache() {
const configs = await this.configRepo.find();
configs.forEach(config => {
let value = config.valeur;
// Déchiffrement si nécessaire
if (config.type === 'encrypted' && value) {
value = this.decrypt(value);
}
// Conversion de type
value = this.convertType(value, config.type);
this.cache.set(config.cle, value);
});
}
// Récupération d'une valeur
get(key: string, defaultValue?: any): any {
return this.cache.has(key) ? this.cache.get(key) : defaultValue;
}
// Mise à jour d'une valeur
async set(key: string, value: any, userId: string): Promise<void> {
const config = await this.configRepo.findOne({ where: { cle: key } });
if (!config) {
throw new Error(`Configuration key '${key}' not found`);
}
let valueToStore = String(value);
// Chiffrement si nécessaire
if (config.type === 'encrypted') {
valueToStore = this.encrypt(valueToStore);
}
config.valeur = valueToStore;
config.modifie_par = userId;
config.modifie_le = new Date();
await this.configRepo.save(config);
// Mise à jour du cache
this.cache.set(key, value);
}
// Récupération de toutes les configs par catégorie
async getByCategory(category: string): Promise<any> {
const configs = await this.configRepo.find({ where: { categorie: category } });
return configs.reduce((acc, config) => {
let value = config.valeur;
if (config.type === 'encrypted') {
value = '***********'; // Masquer les mots de passe
} else {
value = this.convertType(value, config.type);
}
acc[config.cle] = {
value,
description: config.description,
type: config.type,
};
return acc;
}, {});
}
// Test de connexion SMTP
async testSmtpConnection(): Promise<boolean> {
const nodemailer = require('nodemailer');
const transporter = nodemailer.createTransport({
host: this.get('smtp_host'),
port: this.get('smtp_port'),
secure: this.get('smtp_secure'),
auth: this.get('smtp_auth_required') ? {
user: this.get('smtp_user'),
pass: this.get('smtp_password'),
} : undefined,
});
try {
await transporter.verify();
return true;
} catch (error) {
console.error('SMTP test failed:', error);
return false;
}
}
// Utilitaires
private convertType(value: string, type: string): any {
switch (type) {
case 'number': return Number(value);
case 'boolean': return value === 'true';
case 'json': return JSON.parse(value);
default: return value;
}
}
private encrypt(text: string): string {
const cipher = crypto.createCipher('aes-256-cbc', this.ENCRYPTION_KEY);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
return encrypted;
}
private decrypt(text: string): string {
const decipher = crypto.createDecipher('aes-256-cbc', this.ENCRYPTION_KEY);
let decrypted = decipher.update(text, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
}
```
---
## 🔄 Workflow Setup Initial
### Diagramme de séquence
```mermaid
sequenceDiagram
participant Op as Opérateur
participant App as Application (Dashboard)
participant API as ConfigAPI
participant DB as PostgreSQL
participant SMTP as Serveur SMTP
Op->>App: Première connexion (admin)
App->>API: GET /configuration/setup/status
API->>DB: setup_completed ?
DB-->>API: false
API-->>App: setupCompleted: false
App->>App: Affiche panneau Configuration<br/>et bloque les autres onglets
Op->>Op: Remplit les 3 sections<br/>(Email, Personnalisation, Avancé)
Op->>App: Clic "Tester la connexion SMTP"
App->>API: POST /api/v1/configuration/test-smtp
API->>SMTP: Test connexion
alt Test SMTP OK
SMTP-->>API: ✅ Connexion réussie
API->>Op: Envoi email de test
API-->>App: ✅ Test réussi
App-->>Op: Message: "Email de test envoyé"
else Test SMTP KO
SMTP-->>API: ❌ Erreur connexion
API-->>App: ❌ Erreur détaillée
App-->>Op: Message: "Erreur: vérifiez les paramètres"
end
Op->>App: Clic "Sauvegarder et terminer la configuration"
App->>API: PATCH /api/v1/configuration/bulk<br/>{smtp_host, smtp_port, ...}
API->>DB: UPDATE configuration SET valeur=...
API-->>App: OK
App->>API: POST /api/v1/configuration/setup/complete
API->>DB: SET setup_completed = true
API->>API: Recharger cache ConfigService
API-->>App: OK
App->>App: Débloque la navigation<br/>Message succès
Op->>App: Accès complet au dashboard
```
### Étapes détaillées
#### 1. Détection configuration incomplète
**Backend** : Le `SetupGuard` (NestJS) vérifie `setup_completed`. Si false, il autorise l’accès au dashboard et aux APIs configuration (pas de redirection vers une page dédiée). Le **frontend** appelle `GET /configuration/setup/status` au chargement du dashboard admin ; si `setupCompleted === false`, il affiche directement le **panneau Paramètres** et désactive les autres onglets jusqu’à sauvegarde.
**Guard** (exemple) : exemption des routes login + dashboard + APIs configuration.
```typescript
// Exemptions : /auth/login, /api/v1/configuration, routes dashboard admin
// Si setup non complété : pas de redirection HTTP ; le frontend gère l’affichage du panneau Config et le blocage des onglets.
```
#### 2. Panneau Paramètres (Frontend)
**Une seule page avec 3 sections** (blocs successifs, pas d’onglets dans le formulaire) :
##### Section 1 : Configuration Email 📧
| Champ | Type | Valeur par défaut | Obligatoire |
|-------|------|-------------------|-------------|
| Serveur SMTP | Text | `localhost` | ✅ |
| Port SMTP | Number | `25` | ✅ |
| Sécurité | Select | `Aucune` / `STARTTLS` / `SSL/TLS` | ✅ |
| Authentification requise | Checkbox | `false` | - |
| Utilisateur SMTP | Text | - | Si auth |
| Mot de passe SMTP | Password | - | Si auth |
| Nom expéditeur | Text | `P'titsPas` | ✅ |
| Email expéditeur | Email | `no-reply@ptits-pas.fr` | ✅ |
**Bouton** : "🧪 Tester la connexion SMTP"
##### Section 2 : Personnalisation 🎨
| Champ | Type | Valeur par défaut | Obligatoire |
|-------|------|-------------------|-------------|
| Nom de l'application | Text | `P'titsPas` | ✅ |
| URL de l'application | URL | `https://app.ptits-pas.fr` | ✅ |
| Logo | File (PNG/JPG) | Logo par défaut | ❌ |
##### Section 3 : Paramètres avancés ⚙️
| Champ | Type | Valeur par défaut | Obligatoire |
|-------|------|-------------------|-------------|
| Durée validité token MDP (jours) | Number | `7` | ✅ |
| Durée session JWT (heures) | Number | `24` | ✅ |
| Taille max upload (MB) | Number | `5` | ✅ |
**Bouton** : "💾 Sauvegarder et terminer la configuration" (première config) ou "💾 Enregistrer" (accès permanent).
---
## 🔌 APIs Configuration
### Endpoint 1 : Récupérer config par catégorie
```http
GET /api/v1/configuration/:category
Authorization: Bearer <super_admin_token>
```
**Paramètres** :
- `category` : `email` | `app` | `security`
**Réponse 200** :
```json
{
"smtp_host": {
"value": "localhost",
"description": "Serveur SMTP",
"type": "string"
},
"smtp_port": {
"value": 25,
"description": "Port SMTP",
"type": "number"
},
"smtp_password": {
"value": "***********",
"description": "Mot de passe SMTP",
"type": "encrypted"
}
}
```
---
### Endpoint 2 : Mise à jour multiple
```http
PATCH /api/v1/configuration/bulk
Authorization: Bearer <super_admin_token>
Content-Type: application/json
```
**Body** :
```json
{
"smtp_host": "mail.mairie-bezons.fr",
"smtp_port": 587,
"smtp_secure": false,
"smtp_auth_required": true,
"smtp_user": "noreply@mairie-bezons.fr",
"smtp_password": "SecretPassword123",
"email_from_name": "P'titsPas - Mairie de Bezons",
"email_from_address": "noreply@mairie-bezons.fr",
"app_name": "P'titsPas Bezons",
"app_url": "https://ptitspas.mairie-bezons.fr"
}
```
**Réponse 200** :
```json
{
"message": "Configuration mise à jour avec succès",
"updated": 10
}
```
---
### Endpoint 3 : Test connexion SMTP
```http
POST /api/v1/configuration/test-smtp
Authorization: Bearer <super_admin_token>
Content-Type: application/json
```
**Body** :
```json
{
"smtp_host": "mail.mairie-bezons.fr",
"smtp_port": 587,
"smtp_secure": false,
"smtp_auth_required": true,
"smtp_user": "noreply@mairie-bezons.fr",
"smtp_password": "SecretPassword123",
"test_email": "admin@mairie-bezons.fr"
}
```
**Réponse 200** :
```json
{
"success": true,
"message": "Connexion SMTP réussie. Email de test envoyé à admin@mairie-bezons.fr"
}
```
**Réponse 400** :
```json
{
"success": false,
"message": "Erreur de connexion SMTP",
"error": "ECONNREFUSED: Connection refused"
}
```
---
## 💻 Interface Admin
### Panneau Paramètres / Configuration (unique)
Un **seul panneau** dans le dashboard admin, avec **3 sections** affichées sur une même page (défilement si besoin). Pas d’onglets dans le formulaire.
- **Première configuration** (au déploiement, `setup_completed === false`) : l’opérateur arrive sur le dashboard ; le panneau Configuration est affiché par défaut et les **autres onglets sont bloqués** jusqu’à clic sur « Sauvegarder et terminer la configuration » (PATCH bulk + POST setup/complete).
- **Accès permanent** : même panneau accessible via l’onglet « Configuration » / « Paramètres » du dashboard ; pas de blocage, simple modification et enregistrement (PATCH bulk).
```
┌─────────────────────────────────────────────────────────┐
│ Dashboard Admin [ Gestionnaires ] [ Parents ] ... │
│ [ Configuration ] ← onglet actif │
├─────────────────────────────────────────────────────────┤
│ │
│ 📧 Configuration Email (SMTP) │
│ Serveur SMTP * [_________________________________] │
│ Port * [____] Sécurité [▼] ☐ Auth requise │
│ Utilisateur [__________] Mot de passe [__________] │
│ Nom expéditeur * [__________] Email * [__________] │
│ [ 🧪 Tester la connexion SMTP ] │
│ │
│ ───────────────────────────────────────────────── │
│ 🎨 Personnalisation │
│ Nom application * [__________] URL * [__________] │
│ Logo [ Choisir un fichier ] │
│ ───────────────────────────────────────────────── │
│ ⚙️ Paramètres avancés │
│ Durée token MDP (jours) [__] JWT (h) [__] Upload MB [__] │
│ │
│ [ 💾 Sauvegarder et terminer la configuration ] │
│ (ou « Enregistrer » si config déjà complétée) │
└─────────────────────────────────────────────────────────┘
```
---
## 📋 Exemples de configuration
### Configuration 1 : Mairie (serveur local)
```json
{
"smtp_host": "mail.mairie-bezons.fr",
"smtp_port": 25,
"smtp_secure": false,
"smtp_auth_required": false,
"email_from_name": "P'titsPas - Mairie de Bezons",
"email_from_address": "noreply@mairie-bezons.fr",
"app_name": "P'titsPas Bezons",
"app_url": "https://ptitspas.mairie-bezons.fr"
}
```
### Configuration 2 : Gmail (pour tests)
```json
{
"smtp_host": "smtp.gmail.com",
"smtp_port": 587,
"smtp_secure": false,
"smtp_auth_required": true,
"smtp_user": "contact@ptits-pas.fr",
"smtp_password": "abcd efgh ijkl mnop",
"email_from_name": "P'titsPas",
"email_from_address": "contact@ptits-pas.fr",
"app_name": "P'titsPas",
"app_url": "https://app.ptits-pas.fr"
}
```
**Note** : Pour Gmail, utiliser un "Mot de passe d'application" (App Password)
### Configuration 3 : Office 365
```json
{
"smtp_host": "smtp.office365.com",
"smtp_port": 587,
"smtp_secure": false,
"smtp_auth_required": true,
"smtp_user": "noreply@collectivite.fr",
"smtp_password": "MotDePasseSecurise123",
"email_from_name": "P'titsPas - Collectivité",
"email_from_address": "noreply@collectivite.fr",
"app_name": "P'titsPas",
"app_url": "https://ptitspas.collectivite.fr"
}
```
---
## 🔒 Sécurité
### Chiffrement des mots de passe
**Algorithme** : AES-256-CBC
**Clé** : Variable d'environnement `CONFIG_ENCRYPTION_KEY` (32 caractères)
**Génération de la clé** :
```bash
# Linux/Mac
openssl rand -hex 32
# Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
```
**Fichier `.env`** :
```env
CONFIG_ENCRYPTION_KEY=a3f8b2c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2
```
### Variables d'environnement critiques
**Fichier `.env`** (à créer lors de l'installation) :
```env
# Base de données
DATABASE_URL=postgresql://app_user:password@ptitspas-postgres:5432/ptitpas_db
# JWT
JWT_SECRET=VotreSecretJWTTresLongEtAleatoire123456789
JWT_EXPIRY=24h
# Configuration
CONFIG_ENCRYPTION_KEY=VotreCleDeChiffrementAES256TresLongue32Caracteres
# Application
NODE_ENV=production
PORT=3000
```
---
## 📚 Références
### Documentation interne
- [01_CAHIER-DES-CHARGES.md](./01_CAHIER-DES-CHARGES.md)
- [02_ARCHITECTURE.md](./02_ARCHITECTURE.md)
- [03_DEPLOYMENT.md](./03_DEPLOYMENT.md)
- [10_DATABASE.md](./10_DATABASE.md)
- [11_API.md](./11_API.md)
### Documentation externe
- [NestJS Configuration](https://docs.nestjs.com/techniques/configuration)
- [Nodemailer SMTP](https://nodemailer.com/smtp/)
- [Node.js Crypto](https://nodejs.org/api/crypto.html)
---
**Dernière mise à jour** : 9 Février 2026
**Version** : 1.1
**Statut** : ✅ Document validé
+698
View File
@@ -0,0 +1,698 @@
# 📄 Documentation Technique - Gestion Documents Légaux (CGU/Privacy)
**Version** : 1.0
**Date** : 25 Novembre 2025
**Auteur** : Équipe PtitsPas
**Référence** : RGPD & Conformité juridique
---
## 📖 Table des matières
1. [Vue d'ensemble](#vue-densemble)
2. [Architecture](#architecture)
3. [Tables BDD](#tables-bdd)
4. [Service Documents Légaux](#service-documents-légaux)
5. [Workflow Upload & Activation](#workflow-upload--activation)
6. [Workflow Acceptation Utilisateur](#workflow-acceptation-utilisateur)
7. [APIs](#apis)
8. [Interface Admin](#interface-admin)
9. [Conformité RGPD](#conformité-rgpd)
---
## 🎯 Vue d'ensemble
### Problématique
Chaque collectivité déployant P'titsPas on-premise doit pouvoir :
1. ✅ **Personnaliser** les CGU et la Politique de confidentialité
2. ✅ **Versionner** les documents (traçabilité juridique)
3. ✅ **Tracer** qui a accepté quelle version (RGPD)
4. ✅ **Prouver** l'acceptation (IP, User-Agent, horodatage)
5. ✅ **Empêcher** le retour en arrière (sécurité juridique)
### Solution
- **Documents génériques v1** fournis par défaut (rédigés avec juriste)
- **Upload de nouvelles versions** par l'admin (PDF uniquement)
- **Versioning automatique** (incrémentation sans retour arrière)
- **Activation manuelle** (prévisualisation avant mise en prod)
- **Traçabilité complète** (hash SHA-256, IP, User-Agent)
---
## 🏗️ Architecture
### Flux de données
```
┌─────────────────────────────────────────────────────────┐
│ Workflow Documents │
├─────────────────────────────────────────────────────────┤
│ │
│ 1. UPLOAD (Admin) │
│ Admin ──▶ API ──▶ File System ──▶ BDD │
│ /documents/legaux/ │
│ cgu_v4_<timestamp>.pdf │
│ │
│ 2. ACTIVATION (Admin) │
│ Admin ──▶ API ──▶ BDD (actif=true) │
│ │
│ 3. ACCEPTATION (Utilisateur) │
│ User ──▶ Frontend ──▶ API ──▶ BDD │
│ (inscription) (trace IP/UA) │
│ │
└─────────────────────────────────────────────────────────┘
```
### Composants
1. **Table `documents_legaux`** : Stockage versions + métadonnées
2. **Table `acceptations_documents`** : Traçabilité acceptations
3. **Service `DocumentsLegauxService`** : Upload, versioning, activation
4. **API REST** : CRUD documents
5. **Interface Admin** : Upload + activation
6. **Interface Inscription** : Affichage + acceptation
---
## 📊 Tables BDD
### Table 1 : `documents_legaux`
```sql
-- Table pour gérer les versions des documents légaux
CREATE TABLE documents_legaux (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
type VARCHAR(50) NOT NULL, -- 'cgu' ou 'privacy'
version INTEGER NOT NULL, -- Numéro de version (auto-incrémenté)
fichier_nom VARCHAR(255) NOT NULL, -- Nom original du fichier
fichier_path VARCHAR(500) NOT NULL, -- Chemin de stockage
fichier_hash VARCHAR(64) NOT NULL, -- Hash SHA-256 pour intégrité
actif BOOLEAN DEFAULT false, -- Version actuellement active
televerse_par UUID REFERENCES utilisateurs(id), -- Qui a uploadé
televerse_le TIMESTAMPTZ DEFAULT now(), -- Date d'upload
active_le TIMESTAMPTZ, -- Date d'activation
UNIQUE(type, version) -- Pas de doublon version
);
-- Index pour performance
CREATE INDEX idx_documents_legaux_type_actif ON documents_legaux(type, actif);
CREATE INDEX idx_documents_legaux_version ON documents_legaux(type, version DESC);
```
**Contraintes** :
- ✅ Un seul document `actif=true` par type à la fois
- ✅ Versioning auto-incrémenté (pas de gaps)
- ✅ Hash SHA-256 pour vérifier l'intégrité du fichier
---
### Table 2 : `acceptations_documents`
```sql
-- Table de traçabilité des acceptations (RGPD)
CREATE TABLE acceptations_documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
id_utilisateur UUID REFERENCES utilisateurs(id) ON DELETE CASCADE,
id_document UUID REFERENCES documents_legaux(id),
type_document VARCHAR(50) NOT NULL, -- 'cgu' ou 'privacy'
version_document INTEGER NOT NULL, -- Version acceptée
accepte_le TIMESTAMPTZ DEFAULT now(), -- Date d'acceptation
ip_address INET, -- IP de l'utilisateur (RGPD)
user_agent TEXT -- Navigateur (preuve)
);
CREATE INDEX idx_acceptations_utilisateur ON acceptations_documents(id_utilisateur);
CREATE INDEX idx_acceptations_document ON acceptations_documents(id_document);
```
**Données capturées** :
- ✅ **Qui** : `id_utilisateur`
- ✅ **Quoi** : `type_document`, `version_document`
- ✅ **Quand** : `accepte_le`
- ✅ **Où** : `ip_address`
- ✅ **Comment** : `user_agent`
---
### Modification table `utilisateurs`
```sql
-- Ajouter colonnes pour référence rapide (optionnel)
ALTER TABLE utilisateurs
ADD COLUMN cgu_version_acceptee INTEGER,
ADD COLUMN cgu_acceptee_le TIMESTAMPTZ,
ADD COLUMN privacy_version_acceptee INTEGER,
ADD COLUMN privacy_acceptee_le TIMESTAMPTZ;
```
**Note** : Ces colonnes sont **redondantes** avec `acceptations_documents`, mais permettent un accès rapide sans JOIN.
---
### Seed initial
```sql
-- Documents génériques v1 (fournis par défaut)
INSERT INTO documents_legaux (type, version, fichier_nom, fichier_path, fichier_hash, actif, televerse_le, active_le) VALUES
('cgu', 1, 'cgu_v1_default.pdf', '/documents/legaux/cgu_v1_default.pdf', 'a3f8b2c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2', true, now(), now()),
('privacy', 1, 'privacy_v1_default.pdf', '/documents/legaux/privacy_v1_default.pdf', 'b4f9c3d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4', true, now(), now());
```
**Fichiers à fournir** :
- `/documents/legaux/cgu_v1_default.pdf` (rédigé avec juriste)
- `/documents/legaux/privacy_v1_default.pdf` (conforme RGPD)
---
## 🔧 Service Documents Légaux
### Responsabilités
1. **Récupérer documents actifs** : `getDocumentsActifs()`
2. **Uploader nouvelle version** : `uploadNouvelleVersion(type, file, userId)`
3. **Activer une version** : `activerVersion(documentId)`
4. **Lister versions** : `listerVersions(type)`
5. **Télécharger document** : `telechargerDocument(documentId)`
### Implémentation (TypeScript)
```typescript
// backend/src/documents-legaux/documents-legaux.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { DocumentLegal } from './entities/document-legal.entity';
import * as crypto from 'crypto';
import * as fs from 'fs/promises';
import * as path from 'path';
@Injectable()
export class DocumentsLegauxService {
private readonly UPLOAD_DIR = '/app/documents/legaux';
constructor(
@InjectRepository(DocumentLegal)
private docRepo: Repository<DocumentLegal>,
) {}
// Récupérer les documents actifs
async getDocumentsActifs(): Promise<{ cgu: DocumentLegal; privacy: DocumentLegal }> {
const cgu = await this.docRepo.findOne({
where: { type: 'cgu', actif: true },
});
const privacy = await this.docRepo.findOne({
where: { type: 'privacy', actif: true },
});
if (!cgu || !privacy) {
throw new Error('Documents légaux manquants');
}
return { cgu, privacy };
}
// Uploader une nouvelle version
async uploadNouvelleVersion(
type: 'cgu' | 'privacy',
file: Express.Multer.File,
userId: string,
): Promise<DocumentLegal> {
// 1. Calculer la prochaine version
const lastDoc = await this.docRepo.findOne({
where: { type },
order: { version: 'DESC' },
});
const nouvelleVersion = (lastDoc?.version || 0) + 1;
// 2. Calculer le hash du fichier
const fileBuffer = file.buffer;
const hash = crypto.createHash('sha256').update(fileBuffer).digest('hex');
// 3. Générer le nom de fichier unique
const timestamp = Date.now();
const fileName = `${type}_v${nouvelleVersion}_${timestamp}.pdf`;
const filePath = path.join(this.UPLOAD_DIR, fileName);
// 4. Sauvegarder le fichier
await fs.mkdir(this.UPLOAD_DIR, { recursive: true });
await fs.writeFile(filePath, fileBuffer);
// 5. Créer l'entrée en BDD
const document = this.docRepo.create({
type,
version: nouvelleVersion,
fichier_nom: file.originalname,
fichier_path: filePath,
fichier_hash: hash,
actif: false, // Pas actif par défaut
televerse_par: userId,
televerse_le: new Date(),
});
return await this.docRepo.save(document);
}
// Activer une version
async activerVersion(documentId: string): Promise<void> {
const document = await this.docRepo.findOne({ where: { id: documentId } });
if (!document) {
throw new Error('Document non trouvé');
}
// Transaction : désactiver l'ancienne version, activer la nouvelle
await this.docRepo.manager.transaction(async (manager) => {
// Désactiver toutes les versions de ce type
await manager.update(
DocumentLegal,
{ type: document.type, actif: true },
{ actif: false },
);
// Activer la nouvelle version
await manager.update(
DocumentLegal,
{ id: documentId },
{ actif: true, active_le: new Date() },
);
});
}
// Lister toutes les versions (pour l'admin)
async listerVersions(type: 'cgu' | 'privacy'): Promise<DocumentLegal[]> {
return await this.docRepo.find({
where: { type },
order: { version: 'DESC' },
relations: ['televerse_par'],
});
}
// Télécharger un document (stream)
async telechargerDocument(documentId: string): Promise<{ stream: Buffer; filename: string }> {
const document = await this.docRepo.findOne({ where: { id: documentId } });
if (!document) {
throw new Error('Document non trouvé');
}
const fileBuffer = await fs.readFile(document.fichier_path);
return {
stream: fileBuffer,
filename: document.fichier_nom,
};
}
// Vérifier l'intégrité d'un document
async verifierIntegrite(documentId: string): Promise<boolean> {
const document = await this.docRepo.findOne({ where: { id: documentId } });
if (!document) {
throw new Error('Document non trouvé');
}
const fileBuffer = await fs.readFile(document.fichier_path);
const hash = crypto.createHash('sha256').update(fileBuffer).digest('hex');
return hash === document.fichier_hash;
}
}
```
---
## 🔄 Workflow Upload & Activation
### Diagramme de séquence
```mermaid
sequenceDiagram
participant A as Admin
participant API as Backend API
participant FS as File System
participant DB as PostgreSQL
A->>API: POST /api/v1/documents-legaux<br/>{type: 'cgu', file: PDF}
API->>API: Validation fichier<br/>(PDF, max 10MB)
API->>API: Calcul hash SHA-256
API->>DB: SELECT MAX(version)<br/>WHERE type='cgu'
DB-->>API: version = 3
API->>API: Nouvelle version = 4
API->>FS: Enregistrer fichier<br/>/documents/legaux/cgu_v4_<timestamp>.pdf
FS-->>API: ✅ Fichier sauvegardé
API->>DB: INSERT INTO documents_legaux<br/>(type, version=4, actif=false)
DB-->>API: ✅ Document créé
API-->>A: 201 Created<br/>{id, version: 4, actif: false}
A->>A: Prévisualisation PDF
A->>API: PATCH /api/v1/documents-legaux/{id}/activer
API->>DB: BEGIN TRANSACTION
API->>DB: UPDATE documents_legaux<br/>SET actif=false WHERE type='cgu'
API->>DB: UPDATE documents_legaux<br/>SET actif=true, active_le=now()<br/>WHERE id={id}
API->>DB: COMMIT
API-->>A: ✅ CGU v4 activées
```
---
## 📥 Workflow Acceptation Utilisateur
### Diagramme de séquence
```mermaid
sequenceDiagram
participant U as Utilisateur
participant App as Frontend
participant API as Backend
participant DB as PostgreSQL
U->>App: Inscription (étape CGU)
App->>API: GET /api/v1/documents-legaux/actifs
API->>DB: SELECT * FROM documents_legaux<br/>WHERE actif=true
DB-->>API: {cgu: v4, privacy: v2}
API-->>App: {cgu: {version: 4, url: '...'}, privacy: {...}}
App->>App: Afficher liens PDF<br/>"CGU v4" et "Privacy v2"
U->>U: Lit les documents
U->>U: Coche "J'accepte"
App->>API: POST /api/v1/auth/register<br/>{..., cgu_version: 4, privacy_version: 2, ip, user_agent}
API->>DB: BEGIN TRANSACTION
API->>DB: INSERT INTO utilisateurs<br/>(..., cgu_version_acceptee=4, privacy_version_acceptee=2)
DB-->>API: id_utilisateur
API->>DB: INSERT INTO acceptations_documents<br/>(id_utilisateur, type='cgu', version=4, ip, user_agent)
API->>DB: INSERT INTO acceptations_documents<br/>(id_utilisateur, type='privacy', version=2, ip, user_agent)
API->>DB: COMMIT
API-->>App: ✅ Inscription réussie
```
---
## 🔌 APIs
### API 1 : Récupérer documents actifs (Public)
```http
GET /api/v1/documents-legaux/actifs
```
**Réponse 200** :
```json
{
"cgu": {
"id": "uuid-cgu-v4",
"type": "cgu",
"version": 4,
"url": "/api/v1/documents-legaux/uuid-cgu-v4/download",
"active_le": "2025-11-20T14:30:00Z"
},
"privacy": {
"id": "uuid-privacy-v2",
"type": "privacy",
"version": 2,
"url": "/api/v1/documents-legaux/uuid-privacy-v2/download",
"active_le": "2025-10-15T09:15:00Z"
}
}
```
---
### API 2 : Lister versions (Admin)
```http
GET /api/v1/documents-legaux/:type/versions
Authorization: Bearer <super_admin_token>
```
**Paramètres** :
- `type` : `cgu` | `privacy`
**Réponse 200** :
```json
[
{
"id": "uuid-cgu-v4",
"version": 4,
"fichier_nom": "CGU_Mairie_Bezons_2025.pdf",
"actif": true,
"televerse_par": {
"id": "uuid-admin",
"prenom": "Lucas",
"nom": "MOREAU"
},
"televerse_le": "2025-11-20T14:00:00Z",
"active_le": "2025-11-20T14:30:00Z"
},
{
"id": "uuid-cgu-v3",
"version": 3,
"fichier_nom": "CGU_v3.pdf",
"actif": false,
"televerse_par": {
"id": "uuid-admin",
"prenom": "Admin",
"nom": "Système"
},
"televerse_le": "2025-10-15T09:00:00Z",
"active_le": "2025-10-15T09:15:00Z"
}
]
```
---
### API 3 : Upload nouvelle version (Admin)
```http
POST /api/v1/documents-legaux
Authorization: Bearer <super_admin_token>
Content-Type: multipart/form-data
```
**Body** :
```
type: cgu
file: <fichier PDF>
```
**Réponse 201** :
```json
{
"id": "uuid-nouveau-doc",
"type": "cgu",
"version": 5,
"fichier_nom": "CGU_Mairie_Bezons_2025_v2.pdf",
"actif": false,
"televerse_le": "2025-11-25T10:00:00Z"
}
```
**Erreurs** :
- `400 Bad Request` : Fichier non PDF ou trop volumineux (>10MB)
- `401 Unauthorized` : Token manquant ou invalide
- `403 Forbidden` : Rôle insuffisant (pas super_admin)
---
### API 4 : Activer une version (Admin)
```http
PATCH /api/v1/documents-legaux/:id/activer
Authorization: Bearer <super_admin_token>
```
**Réponse 200** :
```json
{
"message": "Document activé avec succès",
"documentId": "uuid-nouveau-doc",
"type": "cgu",
"version": 5
}
```
---
### API 5 : Télécharger document (Public)
```http
GET /api/v1/documents-legaux/:id/download
```
**Réponse 200** :
```
Content-Type: application/pdf
Content-Disposition: attachment; filename="CGU_v5.pdf"
<binary PDF data>
```
---
### API 6 : Historique acceptations utilisateur (Admin)
```http
GET /api/v1/users/:userId/acceptations
Authorization: Bearer <super_admin_token>
```
**Réponse 200** :
```json
[
{
"type_document": "cgu",
"version_document": 4,
"accepte_le": "2025-11-20T15:30:00Z",
"ip_address": "192.168.1.100",
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
},
{
"type_document": "privacy",
"version_document": 2,
"accepte_le": "2025-11-20T15:30:00Z",
"ip_address": "192.168.1.100",
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}
]
```
---
## 💻 Interface Admin
### Écran Gestion Documents Légaux
```
┌─────────────────────────────────────────────────────────┐
│ 📄 Gestion des Documents Légaux │
├─────────────────────────────────────────────────────────┤
│ [ CGU ] [ Politique de confidentialité ] │
├─────────────────────────────────────────────────────────┤
│ │
│ 📋 Conditions Générales d'Utilisation (CGU) │
│ │
│ Version active : v4 │
│ Activée le : 20/11/2025 14:30 │
│ Téléversée par : Lucas MOREAU │
│ │
│ [ 📥 Télécharger ] [ 👁️ Prévisualiser ] │
│ │
│ ───────────────────────────────────────────────── │
│ │
│ 📤 Uploader une nouvelle version │
│ │
│ ⚠️ Attention : L'upload d'une nouvelle version │
│ créera la version v5. Vous pourrez la prévisualiser│
│ avant de l'activer. │
│ │
│ [ Choisir un fichier PDF ] (max 10MB) │
│ │
│ [ 📤 Uploader ] │
│ │
│ ───────────────────────────────────────────────── │
│ │
│ 📜 Historique des versions │
│ │
│ ┌───────────────────────────────────────────────┐ │
│ │ ✅ v4 (Active) │ │
│ │ Activée le : 20/11/2025 14:30 │ │
│ │ Par : Lucas MOREAU │ │
│ │ Hash : a3f8b2c4...e0f1a2 ✓ │ │
│ │ [ 📥 Télécharger ] [ 👁️ Voir ] │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────┐ │
│ │ v3 (Inactive) │ │
│ │ Activée le : 15/10/2025 09:15 │ │
│ │ Par : Admin Système │ │
│ │ Hash : b4f9c3d6...f2a3b4 ✓ │ │
│ │ [ 📥 Télécharger ] [ 👁️ Voir ] [🔄 Réactiver]│ │
│ └───────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
```
---
## 🔒 Conformité RGPD
### Données capturées
| Donnée | Justification RGPD | Durée conservation |
|--------|-------------------|-------------------|
| `id_utilisateur` | Traçabilité acceptation | Durée du compte |
| `version_document` | Preuve version acceptée | Durée du compte |
| `accepte_le` | Horodatage légal | Durée du compte |
| `ip_address` | Preuve origine acceptation | 1 an (recommandé) |
| `user_agent` | Preuve navigateur/appareil | 1 an (recommandé) |
### Droits utilisateur
#### Droit d'accès (Article 15)
L'utilisateur peut demander :
- Quelles versions il a acceptées
- Quand il les a acceptées
- Depuis quelle IP
**API** : `GET /api/v1/users/me/acceptations`
#### Droit à l'oubli (Article 17)
Lors de la suppression du compte :
- Suppression des données personnelles
- Conservation des acceptations anonymisées (obligation légale)
**Implémentation** :
```sql
-- Anonymisation (pas suppression totale)
UPDATE acceptations_documents
SET ip_address = NULL,
user_agent = NULL
WHERE id_utilisateur = '<uuid>';
-- Puis suppression utilisateur
DELETE FROM utilisateurs WHERE id = '<uuid>';
```
---
## 📚 Références
### Documentation interne
- [01_CAHIER-DES-CHARGES.md](./01_CAHIER-DES-CHARGES.md)
- [10_DATABASE.md](./10_DATABASE.md)
- [11_API.md](./11_API.md)
- [21_CONFIGURATION-SYSTEME.md](./21_CONFIGURATION-SYSTEME.md)
### Documentation externe
- [RGPD - Article 7 (Consentement)](https://www.cnil.fr/fr/reglement-europeen-protection-donnees/chapitre2#Article7)
- [RGPD - Article 15 (Droit d'accès)](https://www.cnil.fr/fr/reglement-europeen-protection-donnees/chapitre3#Article15)
- [RGPD - Article 17 (Droit à l'oubli)](https://www.cnil.fr/fr/reglement-europeen-protection-donnees/chapitre3#Article17)
---
**Dernière mise à jour** : 25 Novembre 2025
**Version** : 1.0
**Statut** : ✅ Document validé
File diff suppressed because it is too large Load Diff
+569
View File
@@ -0,0 +1,569 @@
# 📋 Décisions Projet - P'titsPas
**Version** : 1.1
**Date** : 9 Février 2026
**Auteur** : Équipe PtitsPas
---
## 🎯 Objectif de ce document
Ce document trace toutes les **décisions importantes** prises lors de la conception du projet P'titsPas. Il sert de référence pour comprendre les choix techniques et fonctionnels.
---
## 📊 Décisions Architecture
### 1. Mono-repo vs Multi-repo
**Décision** : ✅ **Mono-repo**
**Justification** :
- Code cohérent dans un seul dépôt Git
- Commits atomiques (frontend + backend + BDD en même temps)
- CI/CD simplifié (un seul webhook)
- Facilite le travail collaboratif
**Structure** :
```
ptitspas-app/
├── frontend/ # Application Flutter
├── backend/ # API NestJS
├── database/ # Migrations SQL/Prisma
├── api-contracts/ # Contrats OpenAPI + Prisma
├── docs/ # Documentation
└── docker-compose.yml # Orchestration
```
---
### 2. Déploiement On-Premise
**Décision** : ✅ **Application on-premise avec configuration dynamique**
**Justification** :
- Chaque collectivité a son infrastructure (SMTP, domaines, ports)
- Autonomie totale (pas de dépendance à notre infra)
- Conformité aux politiques IT locales
- Sécurité (données restent dans le SI de la collectivité)
**Solution technique** :
- Table `configuration` en BDD (clé/valeur)
- Panneau Paramètres (3 sections) dans le dashboard à la première connexion ; navigation bloquée jusqu'à sauvegarde
- Configuration SMTP dynamique
- Personnalisation (nom app, logo, URL)
**Référence** : [21_CONFIGURATION-SYSTEME.md](./21_CONFIGURATION-SYSTEME.md)
---
### 3. Gestion des fichiers uploadés
**Décision** : ✅ **Système de fichiers local avec volume Docker**
**Justification** :
- Simplicité (pas de S3/MinIO pour on-premise)
- Performance (accès direct au disque)
- Coût (pas de service externe)
**Solution technique** :
- Stockage dans `/uploads/photos/`
- Volume Docker persistant
- Service Upload avec Multer
- Validation taille (depuis config)
**Alternatives rejetées** :
- ❌ Base de données (BYTEA) : Mauvaise performance
- ❌ S3/MinIO : Overkill pour on-premise
---
### 4. Documents légaux (CGU/Privacy)
**Décision** : ✅ **Versioning automatique avec traçabilité RGPD**
**Justification** :
- Conformité juridique (preuve d'acceptation)
- Flexibilité (chaque collectivité adapte ses CGU)
- Traçabilité (hash SHA-256, IP, User-Agent)
- Sécurité juridique (pas de retour en arrière)
**Solution technique** :
- Table `documents_legaux` (versioning auto-incrémenté)
- Table `acceptations_documents` (traçabilité RGPD)
- Upload PDF par l'admin
- Activation manuelle après prévisualisation
- Documents génériques v1 fournis par défaut
**Référence** : [22_DOCUMENTS-LEGAUX.md](./22_DOCUMENTS-LEGAUX.md)
---
## 🔧 Décisions Fonctionnelles
### 5. Workflow inscription sans mot de passe
**Décision** : ✅ **Pas de mot de passe lors de l'inscription, lien email après validation**
**Justification** :
- Simplifie l'inscription (moins de friction)
- Adapté aux parents séparés/divorcés (communication difficile)
- Sécurité (token UUID avec expiration)
- Validation gestionnaire avant activation
**Workflow** :
1. Parent/AM s'inscrit (sans MDP)
2. Gestionnaire valide
3. Email envoyé avec lien création MDP (valable 7 jours)
4. Utilisateur crée son MDP
5. Compte activé
**Référence** : [20_WORKFLOW-CREATION-COMPTE.md](./20_WORKFLOW-CREATION-COMPTE.md)
---
### 6. Genre enfant obligatoire (H/F)
**Décision** : ✅ **Genre obligatoire (H/F uniquement)**
**Justification** :
- Contexte métier : Enfants à garder (pas de genre neutre)
- Conformité CDC
- Simplicité
**Implémentation** :
- Champ `genre` ENUM('H', 'F') NOT NULL dans table `enfants`
---
### 7. Suppression champs téléphone
**Décision** : ✅ **Un seul champ "Téléphone" (mobile privilégié)**
**Justification** :
- Simplification (plus besoin de "Mobile" et "Téléphone fixe")
- Réalité terrain : Tout le monde a un mobile
- Note dans l'interface : "(mobile privilégié)"
**Implémentation** :
- Supprimer `mobile` et `telephone_fixe`
- Garder uniquement `telephone`
---
### 8. NIR obligatoire pour Assistantes Maternelles
**Décision** : ✅ **NIR (Numéro de Sécurité sociale) obligatoire**
**Justification** :
- Conformité CDC
- Nécessaire pour génération automatique du contrat
- Stockage sécurisé (chiffrement recommandé)
**Implémentation** :
- Champ `nir_chiffre` VARCHAR(15) NOT NULL dans `assistantes_maternelles`
- Validation : 15 chiffres exactement
- Affichage masqué dans l'interface (XXX XX XX XX XXX 123)
---
### 9. Suppression notifications SMS
**Décision** : ✅ **Supprimer les notifications SMS, garder uniquement Email**
**Justification** :
- Pas de plus-value identifiée
- Coût supplémentaire (service SMS)
- Complexité (intégration Twilio/OVH)
- Email suffit largement
**Action** :
- Amender le CDC v1.4
- Remplacer "Email OU SMS" par "Email"
- Supprimer toute mention de SMS
**Référence** : Ticket #57
---
## 🚫 Décisions Phase 2 (reportées)
### 10. Gestion erreurs SMTP (renvoyer email)
**Décision** : ⏸️ **Phase 2 ou cas par cas**
**Justification** :
- Pas prioritaire pour MVP
- Downtime SMTP rare (< 24h)
- Recréation dossier acceptable en phase de test
- Peut être ajouté plus tard si besoin terrain
---
### 11. Sauvegarde & Restauration automatique
**Décision** : ⏸️ **Phase finale (quand tout fonctionne)**
**Justification** :
- Pas bloquant pour développement
- Nécessite application stable
- Script `pg_dump` simple à ajouter
- Documentation pour admins sys
**À faire** :
- Script `backup.sh` avec cron
- Guide sauvegarde/restauration
- Test restauration
---
### 12. RGPD avancé (droit à l'oubli, export données)
**Décision** : ⏸️ **Phase 2**
**Justification** :
- Conformité de base assurée (consentement, traçabilité)
- Droit à l'oubli : Peu de demandes en phase test
- Export données : Peut être fait manuellement en phase test
**À faire** :
- API suppression compte (soft delete)
- API export données personnelles (JSON)
- Anonymisation comptes inactifs
---
### 13. Statistiques dashboard
**Décision** : ⏸️ **Phase 2 (besoin d'utilisateurs d'abord)**
**Justification** :
- Pas de données = pas de statistiques
- Fonctionnalité importante pour vente
- Nécessite application en production
**À faire** :
- API statistiques (comptes, enfants, AM, validations)
- Widgets dashboard (graphiques)
- Export rapports
---
### 14. Migration de données
**Décision** : ❌ **Pas de migration prévue**
**Justification** :
- Pas de système existant à migrer
- Application nouvelle
- Import CSV peut être ajouté cas par cas si besoin
---
### 15. Documentation utilisateur
**Décision** : ⏸️ **Phase 2 (formation présentiel prioritaire)**
**Justification** :
- Premiers utilisateurs : Formation directe par l'équipe
- Documentation nécessaire pour scalabilité
- Vidéos tutoriels utiles mais pas bloquant
**À faire** :
- Guide utilisateur gestionnaire
- Guide utilisateur parent/AM
- FAQ
- Vidéos tutoriels
---
## 🔐 Décisions Sécurité
### 16. Chiffrement mots de passe configuration
**Décision** : ✅ **AES-256-CBC pour mots de passe SMTP**
**Justification** :
- Sécurité (mots de passe SMTP en clair = risque)
- Conformité sécurité
- Déchiffrement uniquement en mémoire
**Implémentation** :
- Type `encrypted` dans table `configuration`
- Clé de chiffrement dans `.env` (32 caractères)
- Service ConfigService gère chiffrement/déchiffrement
---
### 17. Hash mots de passe utilisateurs
**Décision** : ✅ **Bcrypt avec 12 rounds**
**Justification** :
- Standard industrie
- Résistant aux attaques brute-force
- Configurable (rounds dans config)
---
### 18. Tokens création mot de passe
**Décision** : ✅ **UUID avec expiration 7 jours**
**Justification** :
- Sécurité (UUID impossible à deviner)
- Expiration (limite fenêtre d'attaque)
- Durée configurable (dans table `configuration`)
---
### 19. JWT sessions
**Décision** : ✅ **JWT avec expiration 24h**
**Justification** :
- Stateless (pas de session en BDD)
- Performance (pas de lookup BDD à chaque requête)
- Durée configurable
---
## 📊 Décisions Base de Données
### 20. PostgreSQL vs autres SGBD
**Décision** : ✅ **PostgreSQL 17**
**Justification** :
- Open-source
- Robuste et performant
- Support JSON (flexibilité)
- Support UUID natif
- Communauté active
---
### 21. ORM : Prisma vs TypeORM
**Décision** : ✅ **Prisma** (mais TypeORM déjà en place)
**Justification Prisma** :
- Type-safety
- Migrations claires
- Génération client automatique
**Note** : Le projet YNOV utilise TypeORM. À évaluer si migration vers Prisma nécessaire.
---
### 22. Migrations versionnées
**Décision** : ✅ **Migrations SQL versionnées**
**Justification** :
- Traçabilité des changements schéma
- Rollback possible
- Déploiement automatisé
**Implémentation** :
- Fichiers `XX_nom_migration.sql`
- Exécution via `prisma migrate deploy`
- User `app_admin` pour DDL
- User `app_user` pour DML (runtime)
---
## 🧪 Décisions Tests
### 23. Stratégie de tests
**Décision** : ✅ **Tests unitaires + intégration + E2E**
**Justification** :
- Qualité code
- Confiance déploiement
- Détection régression
**Couverture cible** :
- Tests unitaires : > 80%
- Tests intégration : Workflows complets
- Tests E2E : Parcours utilisateurs critiques
---
## 📝 Décisions Documentation
### 24. Structure documentation
**Décision** : ✅ **Documentation centralisée dans `/docs/` avec numérotation**
**Justification** :
- Lisibilité (ordre logique)
- Maintenance (tout au même endroit)
- Versioning (Git)
**Structure** :
```
docs/
├── 00_INDEX.md
├── 01_CAHIER-DES-CHARGES.md
├── 02_ARCHITECTURE.md
├── 03_DEPLOYMENT.md
├── 10_DATABASE.md
├── 11_API.md
├── 20_WORKFLOW-CREATION-COMPTE.md
├── 21_CONFIGURATION-SYSTEME.md
├── 22_DOCUMENTS-LEGAUX.md
├── 23_LISTE-TICKETS.md
├── 24_DECISIONS-PROJET.md (ce document)
├── 90_AUDIT.md
└── test-data/
```
---
## 🔄 Décisions Workflow
### 25. Gitea + Webhook + Déploiement automatique
**Décision** : ✅ **Déploiement automatique sur push master**
**Justification** :
- Productivité (pas de déploiement manuel)
- Fiabilité (script testé)
- Rapidité (feedback immédiat)
**Workflow** :
1. Push sur `master`
2. Webhook Gitea déclenché
3. Script `deploy-ptitspas.sh` exécuté
4. Git pull + Migrations + Docker rebuild + Restart
---
### 26. Branches Git
**Décision** : ✅ **Stratégie simple : master + feature branches**
**Justification** :
- Simplicité (petite équipe)
- Flexibilité
**Branches** :
- `master` : Production
- `archive/*` : Archives (ex: maquette initiale)
- `migration/*` : Migrations (ex: intégration YNOV)
- `feature/*` : Nouvelles fonctionnalités
---
## 🎫 Décisions Ticketing
### 27. Taille des tickets
**Décision** : ✅ **Tickets relativement petits (2-6h)**
**Justification** :
- Granularité (suivi précis)
- Motivation (tickets terminables rapidement)
- Revue de code facilitée
---
### 28. Organisation tickets
**Décision** : ✅ **1 ticket = 1 fonctionnalité complète (Front + Back + BDD si nécessaire)**
**Justification** :
- Cohérence (toute la feature développée ensemble)
- Testable (end-to-end immédiatement)
- Atomique (livraison de valeur fonctionnelle)
**Alternative rejetée** :
- ❌ 1 ticket Front + 1 ticket Back + 1 ticket BDD = Dépendances complexes
---
## 🚀 Décisions Déploiement
### 29. Docker Compose vs Kubernetes
**Décision** : ✅ **Docker Compose**
**Justification** :
- Simplicité (on-premise petite/moyenne collectivité)
- Pas de besoin de scalabilité horizontale
- Maintenance facile
**Alternative rejetée** :
- ❌ Kubernetes : Overkill pour on-premise mono-instance
---
### 30. Traefik comme reverse proxy
**Décision** : ✅ **Traefik**
**Justification** :
- Configuration automatique (labels Docker)
- SSL Let's Encrypt automatique
- Dashboard intégré
---
## 📊 Logs & Monitoring
### 31. Système de logs
**Décision** : ✅ **Winston avec rotation quotidienne**
**Justification** :
- Standard NestJS
- Rotation automatique (pas de disque plein)
- Niveaux de log (info, warn, error)
- Transports multiples (console + fichier)
**Logs à capturer** :
- Logs applicatifs (info, warn, error)
- Logs emails (succès/échec SMTP)
- Logs connexions (qui se connecte quand)
- Logs validations (qui valide quoi)
---
## 📋 Résumé des décisions critiques
| # | Décision | Statut | Impact |
|---|----------|--------|--------|
| 1 | Mono-repo | ✅ Phase 1 | Architecture |
| 2 | On-premise avec config dynamique | ✅ Phase 1 | Déploiement |
| 3 | Système de fichiers pour uploads | ✅ Phase 1 | Stockage |
| 4 | Versioning documents légaux | ✅ Phase 1 | RGPD |
| 5 | Inscription sans MDP | ✅ Phase 1 | UX |
| 6 | Genre enfant obligatoire (H/F) | ✅ Phase 1 | Métier |
| 7 | Un seul champ téléphone | ✅ Phase 1 | Simplification |
| 8 | NIR obligatoire AM | ✅ Phase 1 | Conformité |
| 9 | Suppression SMS | ✅ Phase 1 | Simplification |
| 10 | Renvoyer email | ⏸️ Phase 2 | Nice-to-have |
| 11 | Sauvegarde auto | ⏸️ Phase finale | Ops |
| 12 | RGPD avancé | ⏸️ Phase 2 | Conformité |
| 13 | Statistiques | ⏸️ Phase 2 | Business |
| 14 | Migration données | ❌ Rejeté | N/A |
| 15 | Doc utilisateur | ⏸️ Phase 2 | Formation |
| 31 | Logs Winston | ✅ Phase 1 | Monitoring |
---
## 🔄 Historique des modifications
| Date | Version | Modifications |
|------|---------|---------------|
| 25/11/2025 | 1.0 | Création du document - Toutes les décisions initiales |
| 09/02/2026 | 1.1 | Configuration initiale : un seul panneau Paramètres (3 sections) dans le dashboard, plus de Setup Wizard dédié ; navigation bloquée jusqu'à sauvegarde |
---
**Dernière mise à jour** : 9 Février 2026
**Version** : 1.1
**Statut** : ✅ Document validé
+433
View File
@@ -0,0 +1,433 @@
# 📋 Backlog Phase 2 - P'titsPas
**Version** : 1.0
**Date** : 25 Novembre 2025
**Auteur** : Équipe PtitsPas
---
## 🎯 Objectif de ce document
Ce document liste toutes les **fonctionnalités reportées en Phase 2**. Ces fonctionnalités ne sont pas bloquantes pour le MVP (Phase 1), mais apportent de la valeur ajoutée pour la production intensive.
---
## 📊 Vue d'ensemble
| Catégorie | Nombre de tickets | Estimation |
|-----------|-------------------|------------|
| **RGPD avancé** | 3 tickets | ~8h |
| **Monitoring avancé** | 2 tickets | ~6h |
| **Statistiques & Reporting** | 4 tickets | ~16h |
| **Sauvegarde & Restauration** | 2 tickets | ~6h |
| **Documentation utilisateur** | 3 tickets | ~12h |
| **Améliorations UX** | 3 tickets | ~10h |
| **TOTAL** | **17 tickets** | **~58h** |
---
## 🔒 RGPD avancé
### Ticket P2-01 : [Backend] API Suppression compte (soft delete)
**Estimation** : 3h
**Labels** : `backend`, `phase-2`, `rgpd`
**Description** :
Implémenter le droit à l'oubli (RGPD Article 17) avec suppression logique des comptes.
**Tâches** :
- [ ] Endpoint `DELETE /api/v1/users/:id` (soft delete)
- [ ] Ajout champ `supprime_le` TIMESTAMPTZ dans `utilisateurs`
- [ ] Anonymisation données personnelles (email, téléphone, adresse)
- [ ] Conservation données légales (acceptations CGU)
- [ ] Guards (super_admin only)
- [ ] Tests unitaires
**Justification report Phase 2** :
- Peu de demandes en phase test
- Suppression manuelle possible en attendant
---
### Ticket P2-02 : [Backend] API Export données personnelles (RGPD)
**Estimation** : 3h
**Labels** : `backend`, `phase-2`, `rgpd`
**Description** :
Implémenter le droit à la portabilité (RGPD Article 20) avec export JSON des données personnelles.
**Tâches** :
- [ ] Endpoint `GET /api/v1/users/:id/export` (JSON)
- [ ] Export données utilisateur + enfants + acceptations
- [ ] Format JSON structuré
- [ ] Guards (utilisateur lui-même ou admin)
- [ ] Tests unitaires
**Justification report Phase 2** :
- Export manuel SQL possible en phase test
- Peu de demandes attendues
---
### Ticket P2-03 : [Backend] Cron anonymisation comptes inactifs
**Estimation** : 2h
**Labels** : `backend`, `phase-2`, `rgpd`, `cron`
**Description** :
Anonymiser automatiquement les comptes inactifs après X mois (configurable).
**Tâches** :
- [ ] Cron job quotidien
- [ ] Détection comptes inactifs (dernière connexion > X mois)
- [ ] Anonymisation automatique
- [ ] Notification admin
- [ ] Configuration durée inactivité (table `configuration`)
- [ ] Tests
**Justification report Phase 2** :
- Pas de comptes inactifs en phase test
- Peut être fait manuellement
---
## 📊 Monitoring avancé
### Ticket P2-04 : [Backend] API Métriques système
**Estimation** : 3h
**Labels** : `backend`, `phase-2`, `monitoring`
**Description** :
Exposer des métriques système (CPU, RAM, disque, BDD) pour monitoring.
**Tâches** :
- [ ] Endpoint `GET /api/v1/metrics` (super_admin only)
- [ ] Métriques système (CPU, RAM, disque)
- [ ] Métriques BDD (connexions, taille, requêtes lentes)
- [ ] Métriques application (requêtes/s, temps réponse)
- [ ] Format Prometheus (optionnel)
- [ ] Tests
**Justification report Phase 2** :
- Pas critique pour MVP
- Logs suffisent pour debugging initial
---
### Ticket P2-05 : [Frontend] Dashboard Monitoring
**Estimation** : 3h
**Labels** : `frontend`, `phase-2`, `monitoring`, `admin`
**Description** :
Créer un dashboard de monitoring pour le super admin.
**Tâches** :
- [ ] Page `/admin/monitoring` (super_admin only)
- [ ] Graphiques temps réel (CPU, RAM, disque)
- [ ] Alertes (seuils configurables)
- [ ] Historique métriques (7 jours)
**Justification report Phase 2** :
- Pas critique pour MVP
- Monitoring serveur possible via outils système
---
## 📈 Statistiques & Reporting
### Ticket P2-06 : [Backend] API Statistiques dashboard
**Estimation** : 4h
**Labels** : `backend`, `phase-2`, `statistiques`
**Description** :
Créer les endpoints pour récupérer les statistiques de l'application.
**Tâches** :
- [ ] Endpoint `GET /api/v1/stats/overview`
- [ ] Statistiques globales (comptes, enfants, AM, validations)
- [ ] Statistiques temporelles (inscriptions par mois, validations par semaine)
- [ ] Statistiques géographiques (par ville)
- [ ] Cache (rafraîchissement toutes les heures)
- [ ] Tests
**Justification report Phase 2** :
- Pas de données = pas de statistiques
- Nécessite application en production
---
### Ticket P2-07 : [Frontend] Widgets statistiques dashboard
**Estimation** : 4h
**Labels** : `frontend`, `phase-2`, `statistiques`, `gestionnaire`
**Description** :
Afficher les statistiques dans le dashboard gestionnaire.
**Tâches** :
- [ ] Widget "Comptes en attente" (nombre)
- [ ] Widget "Comptes validés ce mois" (graphique)
- [ ] Widget "Enfants inscrits" (nombre)
- [ ] Widget "AM disponibles" (nombre)
- [ ] Graphique évolution inscriptions (6 derniers mois)
**Justification report Phase 2** :
- Besoin d'utilisateurs réels pour avoir des données
- Fonctionnalité importante pour vente, mais pas bloquante pour MVP
---
### Ticket P2-08 : [Backend] API Export rapports (CSV/PDF)
**Estimation** : 4h
**Labels** : `backend`, `phase-2`, `reporting`
**Description** :
Permettre l'export de rapports pour les gestionnaires.
**Tâches** :
- [ ] Endpoint `GET /api/v1/reports/users` (CSV)
- [ ] Endpoint `GET /api/v1/reports/validations` (CSV)
- [ ] Endpoint `GET /api/v1/reports/summary` (PDF)
- [ ] Génération PDF (librairie PDFKit)
- [ ] Filtres (date, statut, rôle)
- [ ] Tests
**Justification report Phase 2** :
- Export manuel SQL possible en phase test
- Nécessite données réelles
---
### Ticket P2-09 : [Frontend] Écran Rapports
**Estimation** : 4h
**Labels** : `frontend`, `phase-2`, `reporting`, `gestionnaire`
**Description** :
Créer un écran de génération de rapports pour les gestionnaires.
**Tâches** :
- [ ] Page `/admin/rapports` (gestionnaire/admin)
- [ ] Sélection type de rapport (utilisateurs, validations, synthèse)
- [ ] Filtres (date, statut, rôle)
- [ ] Prévisualisation
- [ ] Téléchargement (CSV/PDF)
**Justification report Phase 2** :
- Pas prioritaire pour MVP
- Nécessite données réelles
---
## 💾 Sauvegarde & Restauration
### Ticket P2-10 : [Infra] Script backup PostgreSQL automatique
**Estimation** : 3h
**Labels** : `infra`, `phase-2`, `backup`
**Description** :
Créer un script de sauvegarde automatique de la base de données.
**Tâches** :
- [ ] Script `backup.sh` avec `pg_dump`
- [ ] Compression (gzip)
- [ ] Rotation (garder 30 derniers jours)
- [ ] Cron job quotidien (3h du matin)
- [ ] Notification email en cas d'échec
- [ ] Tests restauration
**Justification report Phase 2** :
- Pas bloquant pour développement
- Backup manuel possible en phase test
- À faire avant mise en production
---
### Ticket P2-11 : [Doc] Guide sauvegarde & restauration
**Estimation** : 3h
**Labels** : `documentation`, `phase-2`, `backup`
**Description** :
Documenter les procédures de sauvegarde et restauration pour les admins sys.
**Tâches** :
- [ ] Procédure sauvegarde manuelle
- [ ] Procédure restauration
- [ ] Configuration cron
- [ ] Troubleshooting
- [ ] Exemples de commandes
- [ ] Checklist pré-restauration
**Justification report Phase 2** :
- Documentation technique, pas bloquante pour MVP
- À faire avant mise en production
---
## 📚 Documentation utilisateur
### Ticket P2-12 : [Doc] Guide utilisateur Gestionnaire
**Estimation** : 4h
**Labels** : `documentation`, `phase-2`, `formation`
**Description** :
Rédiger le guide utilisateur pour les gestionnaires.
**Tâches** :
- [ ] Connexion et première utilisation
- [ ] Consultation des demandes
- [ ] Validation/Refus de comptes
- [ ] Gestion des paramètres
- [ ] FAQ gestionnaire
- [ ] Captures d'écran
**Justification report Phase 2** :
- Formation présentiel prioritaire pour premiers utilisateurs
- Documentation nécessaire pour scalabilité
---
### Ticket P2-13 : [Doc] Guide utilisateur Parent/AM
**Estimation** : 4h
**Labels** : `documentation`, `phase-2`, `formation`
**Description** :
Rédiger le guide utilisateur pour les parents et assistantes maternelles.
**Tâches** :
- [ ] Inscription étape par étape
- [ ] Création du mot de passe
- [ ] Connexion
- [ ] Utilisation de l'application
- [ ] FAQ parent/AM
- [ ] Captures d'écran
**Justification report Phase 2** :
- Formation présentiel prioritaire
- Documentation nécessaire pour scalabilité
---
### Ticket P2-14 : [Doc] Vidéos tutoriels
**Estimation** : 4h
**Labels** : `documentation`, `phase-2`, `formation`, `video`
**Description** :
Créer des vidéos tutoriels pour les utilisateurs.
**Tâches** :
- [ ] Vidéo "Inscription parent" (3-5 min)
- [ ] Vidéo "Inscription AM" (3-5 min)
- [ ] Vidéo "Validation comptes gestionnaire" (3-5 min)
- [ ] Vidéo "Configuration initiale admin" (5-7 min)
- [ ] Hébergement (YouTube privé ou serveur)
**Justification report Phase 2** :
- Pas bloquant pour MVP
- Utile pour scalabilité et autonomie utilisateurs
---
## 🎨 Améliorations UX
### Ticket P2-15 : [Frontend] Mode sombre
**Estimation** : 3h
**Labels** : `frontend`, `phase-2`, `ux`
**Description** :
Ajouter un mode sombre à l'application.
**Tâches** :
- [ ] Thème sombre (couleurs, contrastes)
- [ ] Toggle mode clair/sombre
- [ ] Sauvegarde préférence utilisateur
- [ ] Adaptation tous les écrans
**Justification report Phase 2** :
- Nice-to-have, pas bloquant
- Améliore confort utilisateur
---
### Ticket P2-16 : [Frontend] Notifications push (optionnel)
**Estimation** : 4h
**Labels** : `frontend`, `phase-2`, `ux`, `notifications`
**Description** :
Ajouter des notifications push pour les événements importants.
**Tâches** :
- [ ] Service Worker (PWA)
- [ ] Notification "Compte validé"
- [ ] Notification "Nouveau message" (si messagerie)
- [ ] Gestion permissions
- [ ] Paramètres notifications
**Justification report Phase 2** :
- Nice-to-have, pas bloquant
- Nécessite PWA ou app mobile
---
### Ticket P2-17 : [Frontend] Accessibilité (WCAG 2.1)
**Estimation** : 3h
**Labels** : `frontend`, `phase-2`, `ux`, `a11y`
**Description** :
Améliorer l'accessibilité de l'application (conformité WCAG 2.1).
**Tâches** :
- [ ] Audit accessibilité (Lighthouse, axe)
- [ ] Correction contrastes
- [ ] Attributs ARIA
- [ ] Navigation clavier
- [ ] Lecteur d'écran (test)
**Justification report Phase 2** :
- Amélioration continue
- Pas bloquant pour MVP
- Important pour collectivités (obligation légale)
---
## 📋 Priorisation Phase 2
### Priorité Haute (à faire en premier)
1. **Sauvegarde & Restauration** (Tickets P2-10, P2-11) → Avant mise en production
2. **Statistiques** (Tickets P2-06, P2-07) → Argument de vente
3. **Documentation utilisateur** (Tickets P2-12, P2-13) → Scalabilité
### Priorité Moyenne
4. **RGPD avancé** (Tickets P2-01, P2-02, P2-03) → Conformité
5. **Reporting** (Tickets P2-08, P2-09) → Utile pour gestionnaires
### Priorité Basse
6. **Monitoring avancé** (Tickets P2-04, P2-05) → Nice-to-have
7. **Améliorations UX** (Tickets P2-15, P2-16, P2-17) → Confort
---
## 🚀 Critères de passage en Phase 2
La Phase 2 peut commencer quand :
- ✅ Phase 1 terminée (61 tickets)
- ✅ Application déployée en production (au moins 1 collectivité)
- ✅ Utilisateurs réels (au moins 10 comptes validés)
- ✅ Feedback terrain collecté
- ✅ Bugs critiques corrigés
---
## 📊 Estimation globale Phase 2
**Total** : 17 tickets
**Estimation** : ~58h de développement
**Planning suggéré** :
- Sprint 1 (2 semaines) : Sauvegarde + Statistiques (26h)
- Sprint 2 (2 semaines) : Documentation + RGPD (24h)
- Sprint 3 (1 semaine) : Reporting + Améliorations UX (8h)
---
**Dernière mise à jour** : 25 Novembre 2025
**Version** : 1.0
**Statut** : ✅ Backlog Phase 2 défini
+176
View File
@@ -0,0 +1,176 @@
# Procédure – Utilisation de l'API Gitea
## 1. Contexte
- **Instance** : https://git.ptits-pas.fr
- **API de base** : `https://git.ptits-pas.fr/api/v1`
- **Projet P'titsPas** : dépôt `jmartin/petitspas` (owner = `jmartin`, repo = `petitspas`)
## 2. Authentification
### 2.1 Token
Le token est défini dans l'environnement (ex. `~/.bashrc`) :
```bash
export GITEA_TOKEN="<votre_token>"
```
Pour l'utiliser dans les commandes :
```bash
source ~/.bashrc # ou : . ~/.bashrc
# Puis utiliser $GITEA_TOKEN dans les curl
```
### 2.2 En-tête HTTP
Toutes les requêtes API doivent envoyer le token :
```bash
-H "Authorization: token $GITEA_TOKEN"
```
Exemple :
```bash
curl -s -H "Authorization: token $GITEA_TOKEN" \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas"
```
## 3. Endpoints utiles
### 3.1 Dépôt (repository)
| Action | Méthode | URL |
|---------------|---------|-----|
| Infos dépôt | GET | `/repos/{owner}/{repo}` |
| Liste dépôts | GET | `/repos/search?q=petitspas` |
Exemple – infos du dépôt :
```bash
curl -s -H "Authorization: token $GITEA_TOKEN" \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas" | jq .
```
### 3.2 Issues (tickets)
| Action | Méthode | URL |
|------------------|---------|-----|
| Liste des issues | GET | `/repos/{owner}/{repo}/issues` |
| Détail d'une issue | GET | `/repos/{owner}/{repo}/issues/{index}` |
| Créer une issue | POST | `/repos/{owner}/{repo}/issues` |
| Modifier une issue | PATCH | `/repos/{owner}/{repo}/issues/{index}` |
| Fermer une issue | PATCH | (même URL, `state: "closed"`) |
**Paramètres GET utiles pour la liste :**
- `state` : `open` ou `closed`
- `labels` : filtre par label (ex. `frontend`)
- `page`, `limit` : pagination
Exemples :
```bash
# Toutes les issues ouvertes
curl -s -H "Authorization: token $GITEA_TOKEN" \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues?state=open" | jq .
# Issues ouvertes avec label "frontend"
curl -s -H "Authorization: token $GITEA_TOKEN" \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues?state=open" | \
jq '.[] | select(.labels[].name == "frontend") | {number, title, state}'
# Détail de l'issue #47
curl -s -H "Authorization: token $GITEA_TOKEN" \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues/47" | jq .
# Fermer l'issue #31
curl -s -X PATCH -H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"state":"closed"}' \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues/31"
# Créer une issue
curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"title":"Titre du ticket","body":"Description","labels":[1]}' \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues"
```
### 3.3 Pull requests
| Action | Méthode | URL |
|---------------|---------|-----|
| Liste des PR | GET | `/repos/{owner}/{repo}/pulls` |
| Détail d'une PR | GET | `/repos/{owner}/{repo}/pulls/{index}` |
| Créer une PR | POST | `/repos/{owner}/{repo}/pulls` |
| Fusionner une PR | POST | `/repos/{owner}/{repo}/pulls/{index}/merge` |
Exemples :
```bash
# Liste des PR ouvertes
curl -s -H "Authorization: token $GITEA_TOKEN" \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/pulls?state=open" | jq .
# Créer une PR (head = branche source, base = branche cible)
curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"head":"develop","base":"master","title":"Titre de la PR"}' \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/pulls"
```
### 3.4 Branches
| Action | Méthode | URL |
|---------------|---------|-----|
| Liste des branches | GET | `/repos/{owner}/{repo}/branches` |
```bash
curl -s -H "Authorization: token $GITEA_TOKEN" \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/branches" | jq '.[].name'
```
### 3.5 Webhooks
| Action | Méthode | URL |
|---------------|---------|-----|
| Liste webhooks | GET | `/repos/{owner}/{repo}/hooks` |
| Créer webhook | POST | `/repos/{owner}/{repo}/hooks` |
### 3.6 Labels
| Action | Méthode | URL |
|---------------|---------|-----|
| Liste des labels | GET | `/repos/{owner}/{repo}/labels` |
```bash
curl -s -H "Authorization: token $GITEA_TOKEN" \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/labels" | jq '.[] | {id, name}'
```
## 4. Résumé des URLs pour P'titsPas
Remplacer `{owner}` par `jmartin` et `{repo}` par `petitspas` :
| Ressource | URL |
|------------------|-----|
| Dépôt | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas` |
| Issues | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues` |
| Issue #n | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues/{n}` |
| Pull requests | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/pulls` |
| Branches | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/branches` |
| Labels | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/labels` |
## 5. Documentation officielle
- Swagger / OpenAPI : https://docs.gitea.com/api
- Référence selon la version de Gitea installée (ex. 1.21, 1.25).
## 6. Dépannage
- **401 Unauthorized** : vérifier le token et l'en-tête `Authorization: token <TOKEN>`.
- **404** : vérifier owner/repo et l'URL (sensible à la casse).
- **422 / body invalide** : pour POST/PATCH, envoyer `Content-Type: application/json` et un JSON valide.
+63
View File
@@ -0,0 +1,63 @@
# Note Backend - Activation du module Gestionnaires (Ticket #92)
## Problème
L'endpoint `GET /api/v1/gestionnaires` renvoie une erreur **404 Not Found**.
Cela est dû au fait que le `GestionnairesModule` n'est pas importé dans l'arbre des modules de l'application (via `UserModule` ou `AppModule`).
## Solution de contournement actuelle (Frontend)
Le frontend utilise actuellement l'endpoint générique `/api/v1/users` et filtre les résultats côté client pour ne garder que les utilisateurs ayant le rôle `gestionnaire`.
*Fichier concerné : `frontend/lib/services/user_service.dart`*
## Correctif Backend à appliquer
Pour activer proprement l'endpoint dédié, il faut effectuer les modifications suivantes dans le backend :
### 1. Importer le module dans `UserModule`
Fichier : `backend/src/routes/user/user.module.ts`
Ajouter `GestionnairesModule` dans les imports.
```typescript
import { GestionnairesModule } from './gestionnaires/gestionnaires.module';
@Module({
imports: [
// ... autres imports
GestionnairesModule, // <--- AJOUTER ICI
],
// ...
})
export class UserModule { }
```
### 2. Ajouter AuthModule dans `GestionnairesModule`
Fichier : `backend/src/routes/user/gestionnaires/gestionnaires.module.ts`
Le contrôleur utilise `AuthGuard`, qui dépend de `JwtService` fourni par `AuthModule`.
```typescript
import { AuthModule } from 'src/routes/auth/auth.module';
@Module({
imports: [
TypeOrmModule.forFeature([Users]),
AuthModule // <--- AJOUTER ICI
],
controllers: [GestionnairesController],
providers: [GestionnairesService],
})
export class GestionnairesModule { }
```
## Après application du correctif
Une fois ces modifications backend effectuées :
1. Redémarrer le serveur backend.
2. Modifier le frontend (`frontend/lib/services/user_service.dart`) pour utiliser à nouveau l'endpoint dédié :
```dart
static Future<List<AppUser>> getGestionnaires() async {
final response = await http.get(
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.gestionnaires}'),
headers: await _headers(),
);
// ...
}
```
+344
View File
@@ -0,0 +1,344 @@
# 📐 Règles de Codage - Projet P'titsPas
**Version** : 1.0
**Date** : 1er Décembre 2025
**Statut** : ✅ Actif
---
## 🌍 Langue du Code
### Principe Général
**Tout le code doit être écrit en FRANÇAIS**, sauf les termes techniques qui restent en **ANGLAIS**.
---
## ✅ Ce qui doit être en FRANÇAIS
### 1. Noms de variables
```typescript
// ✅ BON
const utilisateurConnecte = await this.trouverUtilisateur(id);
const enfantsEnregistres = [];
const tokenCreationMotDePasse = crypto.randomUUID();
// ❌ MAUVAIS
const loggedUser = await this.findUser(id);
const savedChildren = [];
const passwordCreationToken = crypto.randomUUID();
```
### 2. Noms de fonctions/méthodes
```typescript
// ✅ BON
async inscrireParentComplet(dto: DtoInscriptionParentComplet) { }
async creerGestionnaire(dto: DtoCreationGestionnaire) { }
async validerCompte(idUtilisateur: string) { }
// ❌ MAUVAIS
async registerParentComplete(dto: RegisterParentCompleteDto) { }
async createManager(dto: CreateManagerDto) { }
async validateAccount(userId: string) { }
```
### 3. Noms de classes/interfaces/types
```typescript
// ✅ BON
export class DtoInscriptionParentComplet { }
export class ServiceAuthentification { }
export interface OptionsConfiguration { }
export type StatutUtilisateur = 'actif' | 'en_attente' | 'suspendu';
// ❌ MAUVAIS
export class RegisterParentCompleteDto { }
export class AuthService { }
export interface ConfigOptions { }
export type UserStatus = 'active' | 'pending' | 'suspended';
```
### 4. Noms de fichiers
```typescript
// ✅ BON
inscription-parent-complet.dto.ts
service-authentification.ts
entite-utilisateurs.ts
controleur-configuration.ts
// ❌ MAUVAIS
register-parent-complete.dto.ts
auth.service.ts
users.entity.ts
config.controller.ts
```
### 5. Propriétés d'entités/DTOs
```typescript
// ✅ BON
export class Enfants {
@Column({ name: 'prenom' })
prenom: string;
@Column({ name: 'date_naissance' })
dateNaissance: Date;
@Column({ name: 'consentement_photo' })
consentementPhoto: boolean;
}
// ❌ MAUVAIS
export class Children {
@Column({ name: 'first_name' })
firstName: string;
@Column({ name: 'birth_date' })
birthDate: Date;
@Column({ name: 'consent_photo' })
consentPhoto: boolean;
}
```
### 6. Commentaires
```typescript
// ✅ BON
// Créer Parent 1 + Parent 2 (si existe) + entités parents
// Vérifier que l'email n'existe pas déjà
// Transaction : Créer utilisateur + entité métier
// ❌ MAUVAIS
// Create Parent 1 + Parent 2 (if exists) + parent entities
// Check if email already exists
// Transaction: Create user + business entity
```
### 7. Messages d'erreur/succès
```typescript
// ✅ BON
throw new ConflictException('Un compte avec cet email existe déjà');
return { message: 'Inscription réussie. Votre dossier est en attente de validation.' };
// ❌ MAUVAIS
throw new ConflictException('An account with this email already exists');
return { message: 'Registration successful. Your application is pending validation.' };
```
### 8. Logs
```typescript
// ✅ BON
this.logger.log('📦 Chargement de 16 configurations en cache');
this.logger.error('Erreur lors de la création du parent');
// ❌ MAUVAIS
this.logger.log('📦 Loading 16 configurations in cache');
this.logger.error('Error creating parent');
```
---
## ✅ Ce qui RESTE en ANGLAIS (Termes Techniques)
### 1. Patterns de conception
- `singleton`
- `factory`
- `repository`
- `observer`
- `decorator`
### 2. Architecture/Framework
- `backend` / `frontend`
- `controller`
- `service`
- `middleware`
- `guard`
- `interceptor`
- `pipe`
- `filter`
- `module`
- `provider`
### 3. Concepts techniques
- `entity` (TypeORM)
- `DTO` (Data Transfer Object)
- `API` / `endpoint`
- `token` (JWT)
- `hash` (bcrypt)
- `cache`
- `query`
- `transaction`
- `migration`
- `seed`
### 4. Bibliothèques/Technologies
- `NestJS`
- `TypeORM`
- `PostgreSQL`
- `Docker`
- `Git`
- `JWT`
- `bcrypt`
- `Multer`
- `Nodemailer`
### 5. Mots-clés TypeScript/JavaScript
- `async` / `await`
- `const` / `let` / `var`
- `function`
- `class`
- `interface`
- `type`
- `enum`
- `import` / `export`
- `return`
- `throw`
---
## 📋 Exemples Complets
### Exemple 1 : Service d'authentification
```typescript
// ✅ BON
@Injectable()
export class ServiceAuthentification {
constructor(
private readonly serviceUtilisateurs: ServiceUtilisateurs,
private readonly serviceJwt: JwtService,
@InjectRepository(Utilisateurs)
private readonly depotUtilisateurs: Repository<Utilisateurs>,
) {}
async inscrireParentComplet(dto: DtoInscriptionParentComplet) {
// Vérifier que l'email n'existe pas
const existe = await this.serviceUtilisateurs.trouverParEmail(dto.email);
if (existe) {
throw new ConflictException('Un compte avec cet email existe déjà');
}
// Générer le token de création de mot de passe
const tokenCreationMdp = crypto.randomUUID();
const dateExpiration = new Date();
dateExpiration.setDate(dateExpiration.getDate() + 7);
// Transaction : Créer parent + enfants
const resultat = await this.depotUtilisateurs.manager.transaction(async (manager) => {
const parent1 = new Utilisateurs();
parent1.email = dto.email;
parent1.prenom = dto.prenom;
parent1.nom = dto.nom;
parent1.tokenCreationMdp = tokenCreationMdp;
const parentEnregistre = await manager.save(Utilisateurs, parent1);
return { parent: parentEnregistre, token: tokenCreationMdp };
});
return {
message: 'Inscription réussie. Votre dossier est en attente de validation.',
idParent: resultat.parent.id,
statut: 'en_attente',
};
}
}
```
### Exemple 2 : Entité Enfants
```typescript
// ✅ BON
@Entity('enfants')
export class Enfants {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'prenom', length: 100 })
prenom: string;
@Column({ name: 'nom', length: 100 })
nom: string;
@Column({
type: 'enum',
enum: TypeGenre,
name: 'genre'
})
genre: TypeGenre;
@Column({ type: 'date', name: 'date_naissance', nullable: true })
dateNaissance?: Date;
@Column({ type: 'date', name: 'date_prevue_naissance', nullable: true })
datePrevueNaissance?: Date;
@Column({ name: 'photo_url', type: 'text', nullable: true })
photoUrl?: string;
@Column({ name: 'consentement_photo', type: 'boolean', default: false })
consentementPhoto: boolean;
@Column({ name: 'est_multiple', type: 'boolean', default: false })
estMultiple: boolean;
@Column({
type: 'enum',
enum: StatutEnfantType,
name: 'statut'
})
statut: StatutEnfantType;
}
```
---
## 🔄 Migration Progressive
### Stratégie
1. ✅ **Nouveau code** : Appliquer la règle immédiatement
2. ⏳ **Code existant** : Migrer progressivement lors des modifications
3. ❌ **Ne PAS refactoriser** tout le code d'un coup
### Priorités de migration
1. **Haute priorité** : Nouveaux fichiers, nouvelles fonctionnalités
2. **Moyenne priorité** : Fichiers modifiés fréquemment
3. **Basse priorité** : Code stable non modifié
### Exemple de migration progressive
```typescript
// Avant (ancien code - OK pour l'instant)
export class Children { }
// Après modification (nouveau code - appliquer la règle)
export class Enfants { }
```
---
## 🚫 Exceptions
### Cas où l'anglais est toléré
1. **Noms de colonnes en BDD** : Si la BDD existe déjà (ex: `first_name` en BDD → `prenom` en TypeScript)
2. **APIs externes** : Noms imposés par des bibliothèques tierces
3. **Standards** : `id`, `uuid`, `url`, `email`, `password` (termes universels)
---
## ✅ Checklist Avant Commit
- [ ] Noms de variables en français
- [ ] Noms de fonctions/méthodes en français
- [ ] Noms de classes/interfaces en français
- [ ] Noms de fichiers en français
- [ ] Propriétés d'entités/DTOs en français
- [ ] Commentaires en français
- [ ] Messages d'erreur/succès en français
- [ ] Termes techniques restent en anglais
- [ ] Pas de `console.log` (utiliser `this.logger`)
- [ ] Pas de code commenté
- [ ] Types TypeScript corrects (pas de `any`)
- [ ] Imports propres (pas d'imports inutilisés)
---
**Dernière mise à jour** : 1er Décembre 2025
**Auteur** : Équipe P'titsPas
+592
View File
@@ -0,0 +1,592 @@
# Architecture Technique - P'titsPas
## Guide d'Infrastructure et de Déploiement
---
## Vue d'ensemble
P'titsPas est une application de gestion de garde d'enfants pour les collectivités locales, basée sur une architecture **client-serveur moderne** :
- **Frontend** : Application web Flutter (Single Page Application)
- **Backend** : API REST Node.js/Express avec TypeScript
- **Base de données** : PostgreSQL avec ORM Prisma
- **Architecture** : Séparation claire frontend/backend avec API REST
---
## Prérequis Serveur
### Environnement d'exécution
#### Backend
- **Node.js** : Version 18+ (LTS recommandée : 18.19.0+)
- **npm** : Version 9+
- **TypeScript** : Inclus dans les dépendances du projet
#### Base de données
- **PostgreSQL** : Version 15+
- **Extensions** : UUID (pour les clés primaires)
#### Frontend
- **Serveur web statique** : nginx, Apache, ou similaire
- **Flutter Web** : Compilation en JavaScript (pas de prérequis runtime)
### Ressources recommandées
#### Environnement de développement
- **RAM** : 4GB minimum
- **CPU** : 2 vCPU
- **Storage** : 10GB
#### Environnement de production
- **RAM** : 8GB recommandé (4GB minimum)
- **CPU** : 4 vCPU recommandé (2 vCPU minimum)
- **Storage** : 50GB minimum (base de données + logs + backups)
- **Réseau** :
- Port 3000 : API Backend (interne)
- Port 80/443 : Web (externe)
- Port 5432 : PostgreSQL (interne uniquement)
---
## Stack Technique Détaillée
### Backend (API)
```json
{
"runtime": "Node.js 18+",
"language": "TypeScript",
"framework": "Express.js 4.18+",
"orm": "Prisma 6.7+",
"database_client": "@prisma/client",
"security": [
"helmet (sécurité headers)",
"cors (CORS policy)",
"bcrypt (hashage mots de passe)",
"jsonwebtoken (JWT auth)"
],
"logging": "morgan",
"validation": "@nestjs/common"
}
```
### Frontend (Web)
```json
{
"framework": "Flutter 3.2.6+",
"language": "Dart 3.0+",
"compilation": "JavaScript (Flutter Web)",
"navigation": "go_router 13.2+",
"state_management": "provider 6.1+",
"ui_framework": "Material Design",
"fonts": "Google Fonts",
"http_client": "http 1.2+"
}
```
### Base de données
```sql
-- Structure PostgreSQL
-- Tables principales :
-- - Parent (utilisateurs parents)
-- - Child (enfants)
-- - Contract (contrats de garde)
-- - Admin (administrateurs)
-- - Theme (thèmes interface)
-- - AppSettings (paramètres app)
-- Types de données :
-- - UUID pour toutes les clés primaires
-- - Timestamps automatiques (createdAt, updatedAt)
-- - Enums pour les statuts
```
---
## Installation et Configuration
### 1. Prérequis système
```bash
# Ubuntu/Debian
sudo apt update
sudo apt install -y nodejs npm postgresql postgresql-contrib nginx
# Vérification versions
node --version # >= 18.0.0
npm --version # >= 9.0.0
psql --version # >= 15.0
```
### 2. Configuration base de données
```sql
-- Se connecter en tant que postgres
sudo -u postgres psql
-- Créer la base de données et l'utilisateur
CREATE DATABASE ptitspas;
CREATE USER ptitspas_user WITH PASSWORD 'secure_password_here';
GRANT ALL PRIVILEGES ON DATABASE ptitspas TO ptitspas_user;
ALTER USER ptitspas_user CREATEDB; -- Pour les migrations
-- Quitter
\q
```
### 3. Installation Backend
```bash
cd backend
# Installation des dépendances
npm install
# Configuration environnement
cp .env.example .env
# Éditer .env avec vos paramètres
# Génération du client Prisma et migrations
npx prisma generate
npx prisma migrate deploy
# Initialisation admin (optionnel)
npm run init-admin
```
### 4. Installation Frontend
```bash
cd frontend
# Installation des dépendances Flutter
flutter pub get
# Build pour production
flutter build web --release
```
---
## Variables d'Environnement
### Backend (.env)
```bash
# Base de données
DATABASE_URL="postgresql://ptitspas_user:secure_password_here@localhost:5432/ptitspas"
# Sécurité
JWT_SECRET="your-super-secret-jwt-key-minimum-32-characters"
JWT_EXPIRES_IN="24h"
# Serveur
PORT=3000
NODE_ENV=production
# Optionnel
CORS_ORIGIN="https://ptitspas.yourdomain.com"
```
---
## Déploiement
### Option 1 : Déploiement classique (recommandé)
#### Backend
```bash
cd backend
# Installation production
npm ci --only=production
# Build TypeScript
npm run build
# Démarrage (avec PM2 recommandé)
npm install -g pm2
pm2 start dist/index.js --name "ptitspas-api"
pm2 startup
pm2 save
```
#### Frontend
```bash
cd frontend
# Build production
flutter build web --release
# Copier vers serveur web
sudo cp -r build/web/* /var/www/ptitspas/
sudo chown -R www-data:www-data /var/www/ptitspas/
```
### Option 2 : Conteneurisation Docker
#### Dockerfile Backend
```dockerfile
FROM node:18-alpine
# Créer répertoire app
WORKDIR /app
# Copier package files
COPY package*.json ./
COPY prisma ./prisma/
# Installer dépendances
RUN npm ci --only=production
# Copier code source
COPY . .
# Build
RUN npm run build
# Générer client Prisma
RUN npx prisma generate
# Exposer port
EXPOSE 3000
# Variables d'environnement
ENV NODE_ENV=production
# Commande démarrage
CMD ["npm", "start"]
```
#### Dockerfile Frontend
```dockerfile
FROM nginx:alpine
# Copier build Flutter
COPY build/web /usr/share/nginx/html
# Configuration nginx
COPY nginx.conf /etc/nginx/nginx.conf
# Exposer port
EXPOSE 80
# Démarrage nginx
CMD ["nginx", "-g", "daemon off;"]
```
#### Docker Compose
```yaml
version: '3.8'
services:
postgres:
image: postgres:15-alpine
environment:
POSTGRES_DB: ptitspas
POSTGRES_USER: ptitspas_user
POSTGRES_PASSWORD: secure_password_here
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
backend:
build: ./backend
environment:
DATABASE_URL: postgresql://ptitspas_user:secure_password_here@postgres:5432/ptitspas
JWT_SECRET: your-super-secret-jwt-key
NODE_ENV: production
ports:
- "3000:3000"
depends_on:
- postgres
frontend:
build: ./frontend
ports:
- "80:80"
depends_on:
- backend
volumes:
postgres_data:
```
---
## Configuration Nginx
### Configuration complète
```nginx
server {
listen 80;
server_name ptitspas.yourdomain.com;
# Redirection HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name ptitspas.yourdomain.com;
# Certificats SSL
ssl_certificate /etc/letsencrypt/live/ptitspas.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ptitspas.yourdomain.com/privkey.pem;
# Configuration SSL sécurisée
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# Frontend statique (Flutter Web)
location / {
root /var/www/ptitspas;
index index.html;
try_files $uri $uri/ /index.html;
# Cache statique
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
# API Backend
location /api/ {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Logs
access_log /var/log/nginx/ptitspas_access.log;
error_log /var/log/nginx/ptitspas_error.log;
}
```
---
## Sécurité
### Obligatoire
1. **HTTPS avec certificat SSL**
```bash
# Installation Certbot
sudo apt install certbot python3-certbot-nginx
# Génération certificat
sudo certbot --nginx -d ptitspas.yourdomain.com
# Renouvellement automatique
sudo crontab -e
# Ajouter : 0 12 * * * /usr/bin/certbot renew --quiet
```
2. **Firewall**
```bash
# UFW (Ubuntu)
sudo ufw allow 22 # SSH
sudo ufw allow 80 # HTTP
sudo ufw allow 443 # HTTPS
sudo ufw enable
```
3. **Base de données sécurisée**
```bash
# PostgreSQL : accès local uniquement
sudo nano /etc/postgresql/15/main/postgresql.conf
# Commenter : #listen_addresses = 'localhost'
sudo nano /etc/postgresql/15/main/pg_hba.conf
# Vérifier que seules les connexions locales sont autorisées
```
4. **Backup automatique**
```bash
# Script backup
#!/bin/bash
BACKUP_DIR="/var/backups/ptitspas"
DATE=$(date +%Y%m%d_%H%M%S)
pg_dump -U ptitspas_user -h localhost ptitspas > $BACKUP_DIR/ptitspas_$DATE.sql
# Nettoyer les backups > 30 jours
find $BACKUP_DIR -name "*.sql" -mtime +30 -delete
# Crontab : tous les jours à 2h
# 0 2 * * * /path/to/backup_script.sh
```
### Recommandé
1. **Fail2Ban** (protection brute force)
```bash
sudo apt install fail2ban
sudo systemctl enable fail2ban
```
2. **Monitoring des logs**
```bash
# Logrotate pour éviter les gros fichiers
sudo nano /etc/logrotate.d/ptitspas
```
3. **Updates automatiques**
```bash
sudo apt install unattended-upgrades
sudo dpkg-reconfigure unattended-upgrades
```
---
## Commandes de Gestion
### Démarrage des services
```bash
# Backend (développement)
cd backend && npm run dev
# Backend (production avec PM2)
pm2 start ptitspas-api
pm2 status
# Base de données
sudo systemctl start postgresql
sudo systemctl status postgresql
# Serveur web
sudo systemctl start nginx
sudo systemctl status nginx
```
### Maintenance
```bash
# Migrations base de données
cd backend
npx prisma migrate deploy
# Logs Backend
pm2 logs ptitspas-api
# Logs Nginx
sudo tail -f /var/log/nginx/ptitspas_access.log
sudo tail -f /var/log/nginx/ptitspas_error.log
# Restart services
pm2 restart ptitspas-api
sudo systemctl restart nginx
```
---
## Monitoring et Healthcheck
### Endpoints de santé
```bash
# API Health (à implémenter)
curl https://ptitspas.yourdomain.com/api/health
# Base de données
psql -h localhost -U ptitspas_user -d ptitspas -c "SELECT 1;"
# Frontend
curl -I https://ptitspas.yourdomain.com/
```
### Logs à surveiller
1. **Backend** : Via PM2 ou logs applicatifs
2. **PostgreSQL** : `/var/log/postgresql/postgresql-15-main.log`
3. **Nginx** : `/var/log/nginx/ptitspas_*.log`
4. **Système** : `/var/log/syslog`
### Métriques importantes
- **CPU/RAM** : Usage serveur
- **Espace disque** : Base de données et logs
- **Connexions DB** : Nombre de connexions actives
- **Temps de réponse** : API et frontend
- **Erreurs 5xx** : Erreurs serveur
---
## Troubleshooting
### Problèmes courants
1. **Backend ne démarre pas**
```bash
# Vérifier variables d'environnement
cd backend && cat .env
# Vérifier connexion DB
npx prisma db pull
# Logs détaillés
npm run dev
```
2. **Frontend ne s'affiche pas**
```bash
# Vérifier build
cd frontend && flutter build web
# Vérifier nginx
sudo nginx -t
sudo systemctl reload nginx
```
3. **Erreurs base de données**
```bash
# Vérifier statut PostgreSQL
sudo systemctl status postgresql
# Vérifier connexions
sudo -u postgres psql -c "SELECT * FROM pg_stat_activity;"
```
---
## Évolutivité
### Optimisations possibles
1. **Cache Redis** : Pour les sessions et cache applicatif
2. **CDN** : Pour les assets statiques
3. **Load Balancer** : Pour haute disponibilité
4. **Clustering** : Multiple instances Node.js
5. **Database replication** : Master/Slave PostgreSQL
### Monitoring avancé
- **Prometheus + Grafana** : Métriques système et applicatif
- **ELK Stack** : Centralisation des logs
- **Uptime monitoring** : Surveillance externe
Cette architecture est conçue pour être **scalable**, **maintenable** et **sécurisée** pour un environnement de production professionnel.
+267
View File
@@ -0,0 +1,267 @@
# 🎯 BRIEFING - Développement Frontend P'titsPas
## Projet
Application de gestion de crèches/assistantes maternelles - Frontend Flutter Web
---
## Accès Git
```bash
# Cloner le repo
git clone https://git.ptits-pas.fr/jmartin/petitspas.git
cd petitspas/frontend
# Identifiants Gitea (si demandé)
# User: jmartin
# Token: giteabu_1796c6aace0e2ef7e4fdb49cdc3bc1bf8ee31fbc
# API Gitea pour consulter les tickets frontend
curl -H "Authorization: token giteabu_1796c6aace0e2ef7e4fdb49cdc3bc1bf8ee31fbc" \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues?state=open" | jq '.[] | select(.labels[].name == "frontend") | {number, title}'
```
---
## Backend API (déjà déployé)
**URL de base** : `https://app.ptits-pas.fr/api/v1/`
### Endpoints clés disponibles
| Endpoint | Méthode | Description |
|----------|---------|-------------|
| `/configuration/setup/status` | GET | Vérifie si setup terminé (`setupCompleted: true/false`) |
| `/auth/login` | POST | Connexion (`{email, password}`) |
| `/auth/me` | GET | Profil utilisateur (inclut `changement_mdp_obligatoire`) |
| `/auth/change-password-required` | POST | Changement MDP obligatoire |
| `/auth/refresh` | POST | Rafraîchir les tokens |
| `/configuration` | GET | Liste toutes les configs (super_admin) |
| `/configuration/:category` | GET | Configs par catégorie (email, app, security) |
| `/configuration/bulk` | PATCH | Mise à jour multiple des configs |
| `/configuration/test-smtp` | POST | Test connexion SMTP |
| `/configuration/setup/complete` | POST | Marquer setup comme terminé |
| `/gestionnaires` | POST | Créer gestionnaire (super_admin only) |
| `/gestionnaires` | GET | Liste des gestionnaires |
| `/documents-legaux/actifs` | GET | CGU et Privacy actifs |
### Compte test super_admin
```
Email: admin@ptits-pas.fr
MDP: 4dm1n1strateur
changement_mdp_obligatoire: true (première connexion)
```
---
## Tickets Frontend prioritaires
### Workflow d'initialisation (P1 - BLOQUANT)
| # | Ticket | Description | Effort |
|---|--------|-------------|--------|
| **#12** | Panneau Paramètres / Configuration | Une page avec 3 sections (Email, Personnalisation, Avancé) ; première config = affichage direct + navigation bloquée jusqu'à sauvegarde | 5h |
| **#13** | Intégration panneau au dashboard | Onglet Configuration, accès permanent, même interface | 1h |
| **#47** | Changement MDP Obligatoire | Modale bloquante après login si flag=true | 1-2h |
| **#35** | Création Gestionnaire | Formulaire création gestionnaire | 2-3h |
### Authentification (P3)
| # | Ticket | Description | Effort |
|---|--------|-------------|--------|
| **#43** | Écran Création MDP | Page `/create-password?token=xxx` | 2h |
| **#48** | Gestion Erreurs & Messages | Snackbars, intercepteur HTTP | 2h |
### Dashboard Gestionnaire (P3)
| # | Ticket | Description | Effort |
|---|--------|-------------|--------|
| **#44** | Structure Dashboard | Layout avec onglets Parents/AM | 2h |
| **#45** | Liste Parents | Liste des parents en attente | 2h |
| **#46** | Liste AM | Liste des AM en attente | 2h |
### Inscription (P3)
| # | Ticket | Description | Effort |
|---|--------|-------------|--------|
| **#38** | Inscription Parent Étape 3 | Enfants | 3h |
| **#39** | Inscription Parent Étapes 4-6 | Finalisation | 4h |
| **#40-42** | Inscription AM | 3 panneaux | 6h |
| **#50** | Affichage CGU dynamique | Version lors inscription | 1h |
### Admin (P3)
| # | Ticket | Description | Effort |
|---|--------|-------------|--------|
| **#49** | Gestion Documents Légaux | Upload/activation CGU/Privacy | 4h |
| **#51** | Écran Logs Admin | Optionnel v1.1 | 4h |
---
## Règles de codage
### Langue
- **Français** pour : commentaires, descriptions, noms métier
- **Anglais** pour : termes techniques (controller, service, widget, singleton, etc.)
### Exemples
```dart
// ✅ BON - Noms métier en français, technique en anglais
class EcranConnexion extends StatefulWidget { }
final utilisateurConnecte = authService.recupererUtilisateur();
final gestionnaire = await gestionnaireService.creer(donnees);
// Widget avec nom métier
class CarteEnfant extends StatelessWidget { }
class FormulaireInscriptionParent extends StatefulWidget { }
// ❌ MAUVAIS - Tout en anglais
class LoginScreen extends StatefulWidget { }
final loggedInUser = authService.getUser();
class ChildCard extends StatelessWidget { }
```
### Termes à garder en anglais
- Widget, StatefulWidget, StatelessWidget
- Controller, Service, Provider
- Singleton, Factory, Builder
- async, await, Future, Stream
- Navigator, Router, Route
- API, HTTP, JSON, DTO
### Termes métier en français
- Utilisateur, Parent, Enfant, Gestionnaire, Administrateur
- AssistanteMaternelle (ou AM)
- Inscription, Connexion, Déconnexion
- Configuration, Paramètres
- Validation, Refus, EnAttente
---
## Structure frontend existante
```
frontend/lib/
├── config/
│ └── env.dart # Configuration environnement
├── controllers/
│ └── parent_dashboard_controller.dart
├── models/
│ ├── user_registration_data.dart
│ ├── parent_user_registration_data.dart
│ └── am_user_registration_data.dart
├── navigation/
│ └── app_router.dart # Routes de l'application
├── screens/
│ ├── auth/
│ │ ├── login_screen.dart ✅ Fait
│ │ ├── register_choice_screen.dart ✅ Fait
│ │ ├── parent_register_step1-5_screen.dart ✅ Fait
│ │ ├── am/ ✅ 4 étapes faites
│ │ └── parent/ ✅ 5 étapes faites
│ ├── administrateurs/
│ │ ├── admin_dashboardScreen.dart
│ │ └── creation/
│ │ └── gestionnaires_create.dart ⚠️ Placeholder
│ ├── home/
│ │ ├── home_screen.dart
│ │ └── parent_screen/
│ └── legal/
├── services/
│ ├── auth_service.dart
│ ├── bug_report_service.dart
│ ├── dashboardService.dart
│ ├── login_navigation_service.dart
│ └── api/
│ ├── api_config.dart
│ └── tokenService.dart
├── utils/
│ └── data_generator.dart
└── widgets/
├── admin/
│ ├── dashboard_admin.dart
│ ├── gestionnaire_card.dart
│ ├── gestionnaire_management_widget.dart
│ └── parent_managmant_widget.dart
├── dashbord_parent/
└── ...
```
---
## Workflow Git
```bash
# 1. Créer une branche feature
git checkout develop
git pull origin develop
git checkout -b feature/XX-nom-ticket
# 2. Développer et commiter
git add .
git commit -m "feat(#XX): Description courte"
# 3. Pousser et créer PR
git push -u origin feature/XX-nom-ticket
# 4. Créer PR vers master via Gitea ou API
curl -X POST \
-H "Authorization: token giteabu_1796c6aace0e2ef7e4fdb49cdc3bc1bf8ee31fbc" \
-H "Content-Type: application/json" \
-d '{"title":"feat(#XX): Titre","body":"Description\n\nCloses #XX","head":"feature/XX-nom-ticket","base":"master"}' \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/pulls"
```
---
## Commandes Flutter
```bash
# Installation des dépendances
cd frontend
flutter pub get
# Lancer en mode développement
flutter run -d chrome
# ou
flutter run -d windows
# ou
flutter run -d macos
# Build web
flutter build web
# Analyser le code
flutter analyze
```
---
## Fichiers de configuration importants
### `lib/config/env.dart`
```dart
class Env {
static const String apiUrl = 'https://app.ptits-pas.fr/api/v1';
}
```
### `lib/services/api/api_config.dart`
Configuration des appels API avec intercepteurs.
---
## Recommandation de démarrage
1. **Ticket #47** - Modale changement MDP obligatoire (simple, rapide)
2. **Tickets #12 + #13** - Panneau Paramètres (3 sections) : première config + onglet Configuration dans le dashboard
3. **Ticket #35** - Formulaire création gestionnaire
Ces tickets complètent le **workflow d'initialisation** !
---
*Dernière mise à jour : 9 février 2026*
+51
View File
@@ -0,0 +1,51 @@
# Charte graphique · **P'titsPas**
*Version 1.0 – avril 2025*
---
## 1. Essentiel de la marque
| Élément | Raison d'être |
|---------|---------------|
| **Nom** | *P'titsPas* · évoque le cheminement serein des 0-3 ans |
| **Signature** | « Grandir pas à pas, sereinement » |
| **Valeurs** | Bienveillance · Transparence · Simplicité · Modernité |
---
## 2. Logos officiels
| Variante | Aperçu | Usage |
|----------|--------|-------|
| **Principal** | ![logo principal](P'tisPas_logo_trans.png) | Headers, back-office, print A4+ |
| **Icône** | ![icône](P'titsPas_icone.png) | Favicon, PWA, app mobile |
| **Monochrome** | ![mono](mono_placeholder.svg) | Sérigraphie, tampon, textile sombre |
> **Zone de protection** : laisser au minimum l'équivalent d'une pierre pastel autour du logotype.
---
## 3. Palette de couleurs
| Nom | Hex | Rôle |
|-----|-----|------|
| Violet Pastel | `#c6a3d8` | Accent / information |
| Turquoise | `#8ad0c8` | Actions primaires |
| Jaune Doux | `#f2d269` | Avertissements légers |
| Corail | `#f4a28c` | États d'erreur ou badges « conflit » |
| Encre | `#2f2f2f` | Texte principal |
| Ivoire BG | `#fffef9` | Fond d'application & documents |
---
## 4. Typographies
| Contexte | Fonte | Chargement |
|----------|-------|------------|
| Titres & accroches | **Merienda 600** | Google Fonts |
| Texte courant | **Merriweather 300/400** | Google Fonts |
| UI compact | **Inter** (fallback système) | CDN |
```css
h1, h2 { font-family: "Merienda", cursive; }
body { font-family: "Merriweather", serif; }
```
+258
View File
@@ -0,0 +1,258 @@
# Évolutions du Cahier des Charges
Ce document liste les modifications à apporter au cahier des charges original pour le rendre conforme à l'application développée.
## 1. Gestion des Enfants
### Modifications à apporter dans la section "Création de compte parent"
#### Situation actuelle dans le CDC :
- Mentionne uniquement la collecte d'informations sur l'enfant
- Ne précise pas la possibilité d'ajouter plusieurs enfants
- Ne mentionne pas la gestion des naissances multiples
- Ne mentionne pas la gestion des enfants à naître
#### Modifications proposées :
Ajouter le paragraphe suivant après la description de la collecte d'informations sur l'enfant :
```
Les parents peuvent ajouter autant d'enfants que nécessaire. Pour chaque enfant, les informations suivantes sont collectées :
- Prénom
- Date de naissance (ou date prévue pour les enfants à naître)
- Photo (optionnelle)
- Consentement pour l'utilisation de la photo
- Indication si l'enfant fait partie d'une naissance multiple (jumeaux, triplés, etc.)
Les parents peuvent :
- Ajouter un nouvel enfant à tout moment
- Supprimer un enfant ajouté
- Modifier les informations d'un enfant existant
- Indiquer si l'enfant est à naître
- Indiquer si l'enfant fait partie d'une naissance multiple
- Donner ou retirer leur consentement pour l'utilisation de la photo de l'enfant
```
### Modifications à apporter dans la section "Workflow de création de compte"
#### Situation actuelle dans le CDC :
- Étape 3 : "Collecte des informations sur l'enfant"
#### Modifications proposées :
Remplacer l'étape 3 par :
```
3. Collecte des informations sur les enfants
- Ajout d'un premier enfant
- Possibilité d'ajouter d'autres enfants
- Pour chaque enfant :
* Saisie du prénom
* Saisie de la date de naissance (ou date prévue)
* Option d'ajout d'une photo
* Option de consentement photo
* Indication si naissance multiple
* Indication si enfant à naître
- Possibilité de modifier ou supprimer un enfant
```
## 2. Workflow de Création de Compte
### Modifications à apporter dans la section "Workflow de création de compte"
#### Situation actuelle dans le CDC :
- Ne précise pas l'ordre exact des étapes
- Ne mentionne pas le statut du compte après création
- Ne détaille pas le processus de validation
#### Modifications proposées :
Ajouter les précisions suivantes au workflow :
```
Le processus de création de compte suit l'ordre suivant :
1. Collecte des informations du premier parent
2. Option d'ajout d'un second parent
3. Collecte des informations sur les enfants
4. Description de la situation familiale
5. Acceptation des conditions générales
6. Résumé et validation finale
Après la validation :
- Le compte est créé avec le statut "en attente"
- Un gestionnaire doit valider le compte avant son activation
- Les parents reçoivent une notification de la création de leur compte
- Une notification est envoyée aux gestionnaires pour validation
```
## 3. Informations Supplémentaires
### Modifications à apporter dans la section "Création de compte parent"
#### Situation actuelle dans le CDC :
- Ne mentionne pas la possibilité de présentation personnelle
- Ne mentionne pas la gestion des photos
- Ne précise pas les statuts possibles du compte
#### Modifications proposées :
Ajouter les sections suivantes :
```
### Informations complémentaires
Le premier parent peut optionnellement ajouter une présentation personnelle pour décrire sa situation et ses attentes.
### Gestion des photos
Pour chaque enfant, les parents peuvent :
- Ajouter une photo
- Donner ou retirer leur consentement pour l'utilisation de la photo
- La photo est stockée de manière sécurisée
- Le consentement est enregistré avec date et heure
### Statut du compte
Les statuts possibles du compte sont :
- En attente : compte créé, en attente de validation
- Validé : compte activé par un gestionnaire
- Rejeté : compte refusé par un gestionnaire
- Suspendu : compte temporairement désactivé
```
## 4. Validation et Sécurité
### Modifications à apporter dans la section "Validation"
#### Situation actuelle dans le CDC :
- Mentionne la validation par un gestionnaire
- Ne précise pas le processus de validation
- Ne mentionne pas les notifications
#### Modifications proposées :
Ajouter la section suivante :
```
### Processus de validation
1. Création du compte avec statut "en attente"
2. Notification automatique aux gestionnaires
3. Revue des informations par un gestionnaire
4. Décision de validation ou rejet
5. Notification aux parents de la décision
6. Activation ou rejet du compte selon la décision
### Notifications
- Les parents reçoivent une notification à chaque changement de statut
- Les gestionnaires reçoivent une notification pour chaque nouveau compte
- Un historique des validations est conservé
```
## 5. Initialisation de l'Application
### Ajout de l'administrateur par défaut
#### Situation actuelle dans le CDC :
- Ne mentionne pas l'existence d'un administrateur par défaut
- Ne précise pas les identifiants de connexion par défaut
#### Modifications proposées :
Ajouter la section suivante :
```
### Administrateur par défaut
Lors du premier démarrage de l'application, un compte administrateur est automatiquement créé avec les identifiants suivants :
- Email : administrateur@ptitspas.fr
- Mot de passe : password
Ce compte permet d'accéder à toutes les fonctionnalités administratives de l'application.
Le changement de mot de passe est obligatoire lors de la première connexion.
L'application doit forcer ce changement avant d'autoriser l'accès aux fonctionnalités administratives.
```
## 6. Changement de Nom de l'Application
### Situation actuelle dans le CDC :
- L'application est nommée "SuperNounou" dans tout le document
- Les références à l'application utilisent ce nom
### Modifications proposées :
Ajouter la section suivante :
```
### Changement de nom
L'application est renommée "P'titsPas" dans toute la documentation et l'interface utilisateur.
Ce changement implique :
- Mise à jour de toutes les références à "SuperNounou" dans le CDC
- Mise à jour des mentions légales
- Mise à jour de la documentation technique
- Mise à jour des interfaces utilisateur
- Mise à jour des messages système et notifications
- Mise à jour des adresses email (ex: support@ptitspas.fr)
```
### Impact sur l'application :
- Mise à jour de tous les textes statiques dans le code
- Mise à jour des templates d'email
- Mise à jour des messages de notification
- Mise à jour de la documentation utilisateur
- Mise à jour des mentions légales et CGU
## Format de présentation
Pour chaque évolution identifiée, ce document suivra la structure suivante :
1. Section concernée dans le CDC
2. Situation actuelle
3. Modifications proposées
4. Impact sur l'application
## Prochaines évolutions à documenter
- [x] Ajouter d'autres évolutions identifiées
- [ ] Mettre à jour le CDC original
- [ ] Valider les modifications avec les parties prenantes
- [ ] Modifier le texte de la checkbox de consentement photo (libellé actuel : 'J\'accepte l\'utilisation de ma photo.') sur l'écran d'inscription Nounou Étape 2 (`nanny_register_step2_screen.dart`).
# Évolutions proposées au cahier des charges
## 1. Workflow de création de compte
### 1.1 Récupération de compte
#### 1.1.1 Fonctionnalités
- Ajout d'un lien "Mot de passe oublié" sur la page de connexion
- Processus de récupération en 3 étapes :
1. Saisie de l'adresse email
2. Envoi d'un lien unique de réinitialisation (valide 24h)
3. Création d'un nouveau mot de passe
#### 1.1.2 Sécurité
- Le lien de réinitialisation doit être unique et à usage unique
- Le lien expire après 24 heures
- Le nouveau mot de passe doit respecter les mêmes critères que lors de la création de compte
- Notification par email lors de la réinitialisation du mot de passe
#### 1.1.3 Interface
- Page dédiée pour la saisie de l'email
- Page de confirmation d'envoi du lien
- Formulaire de réinitialisation du mot de passe
- Messages d'erreur clairs en cas de :
- Email non trouvé
- Lien expiré
- Mot de passe non conforme
## X. Amélioration de la Gestion des Photos Utilisateurs (Proposition)
### X.1 Recadrage et Redimensionnement des Photos
#### X.1.1 Fonctionnalités
- **Contexte :** Lors du téléchargement de photos par les utilisateurs (photos de profil, photos d'enfants).
- **Besoin :** Permettre à l'utilisateur de recadrer l'image (notamment en format carré pour les avatars) et potentiellement de la faire pivoter ou de zoomer avant son enregistrement final.
- **Objectif :** Améliorer l'expérience utilisateur, assurer une meilleure qualité et cohérence visuelle des images stockées et affichées dans l'application.
#### X.1.2 Solution Technique Envisagée (pour discussion)
- L'intégration d'une librairie Flutter tierce dédiée au recadrage d'image (par exemple, `image_cropper` ou `crop_image`) sera nécessaire après la sélection initiale de l'image via `image_picker`.
- La tentative initiale avec `image_cropper` (version 5.0.1) a rencontré des difficultés techniques d'intégration (erreur "Too many positional arguments" persistante avec `AndroidUiSettings`) et a été mise en attente. Une investigation plus approfondie ou l'évaluation d'alternatives sera requise.
#### X.1.3 Impact sur l'application
- Modification du flux de sélection d'image dans les écrans concernés (ex: `parent_register_step3_screen.dart`).
- Ajout potentiel de nouvelles dépendances et configurations spécifiques aux plateformes.
- Mise à jour de la documentation utilisateur si cette fonctionnalité est implémentée.
+35
View File
@@ -0,0 +1,35 @@
Point tickets frontend (API Gitea) - 27/01/2026
================================================
Issues avec label "frontend" : 20 (ouvertes: 12, fermees: 8)
Num | Etat | Titre
----+--------+--------------------------------------------------------
35 | open | [Frontend] Écran Création Gestionnaire
36 | closed | [Frontend] Inscription Parent - Étape 1 (Parent 1)
37 | closed | [Frontend] Inscription Parent - Étape 2 (Parent 2)
38 | closed | [Frontend] Inscription Parent - Étape 3 (Enfants)
39 | closed | [Frontend] Inscription Parent - Étapes 4-6 (Finalisatio
40 | closed | [Frontend] Inscription AM - Panneau 1 (Identité)
41 | closed | [Frontend] Inscription AM - Panneau 2 (Infos pro)
42 | closed | [Frontend] Inscription AM - Finalisation
43 | open | [Frontend] Écran Création Mot de Passe
44 | open | [Frontend] Dashboard Gestionnaire - Structure
45 | open | [Frontend] Dashboard Gestionnaire - Liste Parents
46 | open | [Frontend] Dashboard Gestionnaire - Liste AM
47 | open | [Frontend] Écran Changement MDP Obligatoire
48 | open | [Frontend] Gestion Erreurs & Messages
49 | open | [Frontend] Écran Gestion Documents Légaux (Admin)
50 | open | [Frontend] Affichage dynamique CGU lors inscription
51 | open | [Frontend] Écran Logs Admin (optionnel v1.1)
54 | open | [Tests] Tests E2E Frontend
82 | closed | [Frontend] Adapter �cran Login pour mobile
83 | closed | [Frontend] Adapter �cran Choix Inscription pour mobile
Suivi doc 23_LISTE-TICKETS (Gitea #73,78,79,81,82,83):
#73 closed labels=[]
#78 closed labels=[]
#79 closed labels=[]
#81 closed labels=[]
#82 closed (écran Login mobile)
#83 closed labels=['frontend', 'p3', 'phase-1', 'ux']
+176
View File
@@ -0,0 +1,176 @@
# Procédure – Utilisation de l’API Gitea
## 1. Contexte
- **Instance** : https://git.ptits-pas.fr
- **API de base** : `https://git.ptits-pas.fr/api/v1`
- **Projet P'titsPas** : dépôt `jmartin/petitspas` (owner = `jmartin`, repo = `petitspas`)
## 2. Authentification
### 2.1 Token
Le token est défini dans l’environnement (ex. `~/.bashrc`) :
```bash
export GITEA_TOKEN="<votre_token>"
```
Pour l’utiliser dans les commandes :
```bash
source ~/.bashrc # ou : . ~/.bashrc
# Puis utiliser $GITEA_TOKEN dans les curl
```
### 2.2 En-tête HTTP
Toutes les requêtes API doivent envoyer le token :
```bash
-H "Authorization: token $GITEA_TOKEN"
```
Exemple :
```bash
curl -s -H "Authorization: token $GITEA_TOKEN" \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas"
```
## 3. Endpoints utiles
### 3.1 Dépôt (repository)
| Action | Méthode | URL |
|---------------|---------|-----|
| Infos dépôt | GET | `/repos/{owner}/{repo}` |
| Liste dépôts | GET | `/repos/search?q=petitspas` |
Exemple – infos du dépôt :
```bash
curl -s -H "Authorization: token $GITEA_TOKEN" \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas" | jq .
```
### 3.2 Issues (tickets)
| Action | Méthode | URL |
|------------------|---------|-----|
| Liste des issues | GET | `/repos/{owner}/{repo}/issues` |
| Détail d’une issue | GET | `/repos/{owner}/{repo}/issues/{index}` |
| Créer une issue | POST | `/repos/{owner}/{repo}/issues` |
| Modifier une issue | PATCH | `/repos/{owner}/{repo}/issues/{index}` |
| Fermer une issue | PATCH | (même URL, `state: "closed"`) |
**Paramètres GET utiles pour la liste :**
- `state` : `open` ou `closed`
- `labels` : filtre par label (ex. `frontend`)
- `page`, `limit` : pagination
Exemples :
```bash
# Toutes les issues ouvertes
curl -s -H "Authorization: token $GITEA_TOKEN" \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues?state=open" | jq .
# Issues ouvertes avec label "frontend"
curl -s -H "Authorization: token $GITEA_TOKEN" \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues?state=open" | \
jq '.[] | select(.labels[].name == "frontend") | {number, title, state}'
# Détail de l’issue #47
curl -s -H "Authorization: token $GITEA_TOKEN" \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues/47" | jq .
# Fermer l’issue #31
curl -s -X PATCH -H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"state":"closed"}' \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues/31"
# Créer une issue
curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"title":"Titre du ticket","body":"Description","labels":[1]}' \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues"
```
### 3.3 Pull requests
| Action | Méthode | URL |
|---------------|---------|-----|
| Liste des PR | GET | `/repos/{owner}/{repo}/pulls` |
| Détail d’une PR | GET | `/repos/{owner}/{repo}/pulls/{index}` |
| Créer une PR | POST | `/repos/{owner}/{repo}/pulls` |
| Fusionner une PR | POST | `/repos/{owner}/{repo}/pulls/{index}/merge` |
Exemples :
```bash
# Liste des PR ouvertes
curl -s -H "Authorization: token $GITEA_TOKEN" \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/pulls?state=open" | jq .
# Créer une PR (head = branche source, base = branche cible)
curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"head":"develop","base":"master","title":"Titre de la PR"}' \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/pulls"
```
### 3.4 Branches
| Action | Méthode | URL |
|---------------|---------|-----|
| Liste des branches | GET | `/repos/{owner}/{repo}/branches` |
```bash
curl -s -H "Authorization: token $GITEA_TOKEN" \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/branches" | jq '.[].name'
```
### 3.5 Webhooks
| Action | Méthode | URL |
|---------------|---------|-----|
| Liste webhooks | GET | `/repos/{owner}/{repo}/hooks` |
| Créer webhook | POST | `/repos/{owner}/{repo}/hooks` |
### 3.6 Labels
| Action | Méthode | URL |
|---------------|---------|-----|
| Liste des labels | GET | `/repos/{owner}/{repo}/labels` |
```bash
curl -s -H "Authorization: token $GITEA_TOKEN" \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/labels" | jq '.[] | {id, name}'
```
## 4. Résumé des URLs pour P'titsPas
Remplacer `{owner}` par `jmartin` et `{repo}` par `petitspas` :
| Ressource | URL |
|------------------|-----|
| Dépôt | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas` |
| Issues | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues` |
| Issue #n | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues/{n}` |
| Pull requests | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/pulls` |
| Branches | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/branches` |
| Labels | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/labels` |
## 5. Documentation officielle
- Swagger / OpenAPI : https://docs.gitea.com/api
- Référence selon la version de Gitea installée (ex. 1.21, 1.25).
## 6. Dépannage
- **401 Unauthorized** : vérifier le token et l’en-tête `Authorization: token <TOKEN>`.
- **404** : vérifier owner/repo et l’URL (sensible à la casse).
- **422 / body invalide** : pour POST/PATCH, envoyer `Content-Type: application/json` et un JSON valide.
+115
View File
@@ -0,0 +1,115 @@
# Statut de l'application P'titsPas
**Date du point** : 8 février 2026
---
## 1. Environnement de production
| Élément | Statut | Détail |
|--------|--------|--------|
| **URL** | OK | https://app.ptits-pas.fr |
| **Frontend** | 200 | Flutter Web, Nginx |
| **API** | 200 | NestJS, préfixe `/api/v1` |
| **Base de données** | OK | PostgreSQL 17 |
| **PgAdmin** | OK | https://app.ptits-pas.fr/pgadmin |
### Conteneurs Docker
| Service | Image | État |
|---------|--------|------|
| ptitspas-frontend | ptitspas-app-frontend | Up (recréé récemment) |
| ptitspas-backend | ptitspas-app-backend | Up ~26h |
| ptitspas-postgres | postgres:17 | Up ~28h |
| ptitspas-pgadmin | dpage/pgadmin4 | Up ~28h |
---
## 2. Dépôt Git
- **Branche déployée** : `master`
- **Derniers commits** :
- `10bf255` – fix(ui): renforcer ombre boutons Parents/AM sur mobile
- `678f421` – docs: ticket #82 fermé (écran Login mobile)
- `5295e8e` – Merge develop: login mobile, formulaire sous slogan par ratio
- `6bf0932` – docs: Index, doc API Gitea, script fermeture issue
- `2f1740b` – docs: ticket #83 RegisterChoiceScreen Mobile (terminé)
- **Branches actives** : `master`, `develop`, diverses `feature/*` (inscription, config, documents légaux, etc.)
---
## 3. Déploiement (hook Gitea)
| Élément | Statut |
|--------|--------|
| **Webhook** | Opérationnel (`hooks.ptits-pas.fr/hooks/petitspas-deploy`) |
| **Déclencheur** | Push sur `master`, dépôt `petitspas` |
| **Script** | Monté depuis l’hôte (verrou + sans Prisma) |
| **Dernier déploiement** | 08/02/2026 18:18:26 – Succès |
Un seul déploiement à la fois (verrou) ; plus d’étape Prisma dans le script.
---
## 4. Fonctionnalités livrées
### Backend (API)
- Auth : login, refresh, profil, **changement MDP obligatoire** (first login)
- Configuration : setup status, bulk, test SMTP, catégories
- Documents légaux : actifs, versions, upload, activation, téléchargement
- Inscription : parents (workflow complet), enfants (CRUD)
- Compte super_admin par défaut (seed BDD) : `admin@ptits-pas.fr` / `4dm1n1strateur`
### Frontend
- **Formulaires d’inscription** : compatibles **desktop et mobile**
- Choix d’inscription (Parents / Assistante maternelle) – responsive
- Inscription Parent : étapes 1 à 5 (infos parent 1 & 2, enfants, présentation, CGU, récap)
- Inscription AM : étapes 1 à 4 (identité, pro, présentation, récap)
- **Login** : écran adapté mobile (formulaire sous slogan selon ratio)
- Modale **changement de mot de passe obligatoire** après première connexion si `changement_mdp_obligatoire`
- CORS configuré (localhost + prod)
### Base de données
- Schéma database-first (BDD.sql)
- Tables : utilisateurs, configuration, documents_legaux, acceptations_documents, enfants, etc.
- Champs tokens création MDP, genre enfants, configuration système
---
## 5. Tickets / Priorités (résumé)
- **Liste détaillée** : `docs/23_LISTE-TICKETS.md`
- **Récent fermé** : #82 (Login mobile), #83 (RegisterChoiceScreen mobile), #73, #78, #79, #81
- **P0 (BDD)** : quelques amendements ouverts (champs CDC, présentation dossier, etc.)
- **P1** : configuration système (panneau Paramètres, 3 sections, première config + accès permanent)
- **P2/P3** : backend métier et frontend (dashboards, écrans création MDP, etc.)
---
## 6. Documentation utile
| Fichier | Usage |
|---------|--------|
| `00_INDEX.md` | Index de la doc |
| `01_CAHIER-DES-CHARGES.md` | CDC v1.3 |
| `11_API.md` | Endpoints API |
| `20_WORKFLOW-CREATION-COMPTE.md` | Workflow création compte |
| `23_LISTE-TICKETS.md` | Liste des tickets |
| `BRIEFING-FRONTEND.md` | Brief frontend, accès Git, tickets prioritaires |
| `PROCEDURE-API-GITEA.md` | Utilisation API Gitea (issues, PR, token) |
---
## 7. Synthèse
L’application est **en production** sur https://app.ptits-pas.fr avec :
- Frontend et API accessibles et répondant en 200.
- Déploiement automatique sur push `master` avec script à jour (verrou, sans Prisma).
- Formulaires d’inscription (Parents et AM) **responsive desktop et mobile**.
- Login et changement de mot de passe obligatoire opérationnels.
- Prochaines priorités : P0 BDD si besoin, P1 panneau Paramètres / Configuration (tickets #12, #13), puis dashboards et workflows métier (P2/P3).
File diff suppressed because it is too large Load Diff
+108
View File
@@ -0,0 +1,108 @@
# SuperNounou – SSS-001
## Spécification technique & opérationnelle unifiée
_Version 0.2 – 24 avril 2025_
---
## 1. Objet
Centraliser tous les aspects **techniques et opérationnels** de la plateforme SuperNounou :
- Sauvegarde & Plan de Reprise d’Activité (PRA)
- Spécifications des API & intégrations
- Directives de déploiement, d’observabilité, de CI/CD
## 2. Portée
Instances de production, pré-production et recette (Frontend, Backend, PostgreSQL, stockage objets), scripts d’installation, pipelines CI/CD, journaux et métriques.
## 3. Références
- CDC SuperNounou V1.1
- ISO 27001 / ISO 22301 bonnes pratiques
- Politique sécurité DSI Enedis #SEC-POL-2024
- RGPD (2016/679)
---
# A – Sauvegarde & Plan de Reprise d’Activité
### A.1 Architecture de sauvegarde
Schéma bloc, chiffrement AES-256 (KMS), réplication hors-site « Object Storage B ».
### A.2 Stratégie de sauvegarde
| Type | Fréquence | Rétention | Support |
|-------------------|-------------|-----------|--------------------|
| Incrémentale | Quotidienne | 30 j | Object Storage A |
| Complète | Hebdomadaire| 6 mois | Object Storage B |
| Export logique DB | Mensuelle | 5 ans | Stockage Glacier |
### A.3 PRA
RPO 24 h / RTO 4 h – scénarios : panne VM, corruption DB, sinistre DC – procédure détaillée + escalade.
### A.4 Tests de restauration
Intégrale semestrielle, partielle trimestrielle – rapport d’audit et actions correctives.
### A.5 Monitoring & alertes
Endpoint Prometheus `/metrics`, tableau Grafana « Backup status », alerte > 26 h sans backup.
### A.6 Rôles
DevOps Lead (implémentation), DBA (tests restore), RSSI (audit).
---
# B – API & Intégrations
### B.1 Conventions
OpenAPI 3 livré (`openapi.yaml`), version URL `/api/v1`, ISO 8601 dates.
### B.2 Sécurité API
JWT Bearer (ou OAuth 2), TLS 1.3, rate-limit 100 req/min/IP, signature HMAC pour webhooks.
### B.3 Exemples
Collection Postman, scripts cURL, guide « Appeler l’API ».
### B.4 Intégrations futures
SSO LDAP/SAML, webhook `contract.validated`, export statistiques CSV.
---
# C – Déploiement, CI/CD et Observabilité *(nouveau)*
### C.1 Déploiement communal (on-premise)
- **Objectif** : installation complète sur un serveur Linux ou VM en moins d’1 h.
- **Livrable** : solution de packaging **au choix** (Docker Compose, image VM, paquet .deb/.rpm).
- **Script update / rollback** : `update.sh` ou équivalent (backup ➜ pull ➜ migrate ➜ vérif ; rollback ≤ 5 min).
- **Config** : fichier `.env.sample` décrivant toutes les variables.
### C.2 Environnements
- Developpement local (Docker Compose).
- Recette & Production (serveur communal).
- Les étudiants doivent décrire la procédure de bascule Recette → Prod.
### C.3 Pipeline CI/CD
- Pipeline automatisé (tests unitaires + build image + scan CVE) déclenché à chaque merge.
- L’école fournit son propre dépôt Git/runner.
- Artifacts : images taguées, notes de version (`CHANGELOG.md`).
### C.4 Observabilité & logs
- Journaux applicatifs JSON (timestamp UTC, level, traceId).
- Rotation/retention : 7 jours sur disque, 30 jours sur archive compressée.
- Export métriques Prometheus (`/metrics`) : latence API, nombre de sessions, files d’attente hors-ligne.
- Tableaux Grafana d’exemple inclus (`grafana_dashboard.json`).
### C.5 SLA et performances (indicatifs)
- Disponibilité mensuelle cible : **≥ 98 %**.
- Temps de réponse P95 des opérations courantes : **< 500 ms**.
- Capacité test : **≈ 50 sessions simultanées** sans dégradation (> 1 s).
---
# D – Glossaire
AES-256, JWT, KMS, OpenAPI, RPO, RTO, rate-limit, HMAC, Compose, CI/CD…
---
# E – Historique des versions
| Version | Date | Auteur | Commentaire |
|---------|------------|------------------|---------------------------------|
| 0.1-draft | 2025-04-24 | Équipe projet | Création du SSS unifié |
| 0.2 | 2025-04-24 | ChatGPT & Julien | Ajout déploiement / CI/CD / logs |
@@ -1,44 +0,0 @@
package io.flutter.plugins;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import io.flutter.Log;
import io.flutter.embedding.engine.FlutterEngine;
/**
* Generated file. Do not edit.
* This file is generated by the Flutter tool based on the
* plugins that support the Android platform.
*/
@Keep
public final class GeneratedPluginRegistrant {
private static final String TAG = "GeneratedPluginRegistrant";
public static void registerWith(@NonNull FlutterEngine flutterEngine) {
try {
flutterEngine.getPlugins().add(new io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin());
} catch (Exception e) {
Log.e(TAG, "Error registering plugin flutter_plugin_android_lifecycle, io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin", e);
}
try {
flutterEngine.getPlugins().add(new io.flutter.plugins.imagepicker.ImagePickerPlugin());
} catch (Exception e) {
Log.e(TAG, "Error registering plugin image_picker_android, io.flutter.plugins.imagepicker.ImagePickerPlugin", e);
}
try {
flutterEngine.getPlugins().add(new io.flutter.plugins.pathprovider.PathProviderPlugin());
} catch (Exception e) {
Log.e(TAG, "Error registering plugin path_provider_android, io.flutter.plugins.pathprovider.PathProviderPlugin", e);
}
try {
flutterEngine.getPlugins().add(new io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin());
} catch (Exception e) {
Log.e(TAG, "Error registering plugin shared_preferences_android, io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin", e);
}
try {
flutterEngine.getPlugins().add(new io.flutter.plugins.urllauncher.UrlLauncherPlugin());
} catch (Exception e) {
Log.e(TAG, "Error registering plugin url_launcher_android, io.flutter.plugins.urllauncher.UrlLauncherPlugin", e);
}
}
}
+1 -1
View File
@@ -1,2 +1,2 @@
flutter.sdk=C:\\Users\\myhan\\flutter flutter.sdk=C:\\Users\\marti\\dev\\flutter
sdk.dir=C:\\Users\\myhan\\AppData\\Local\\Android\\Sdk sdk.dir=C:\\Users\\myhan\\AppData\\Local\\Android\\Sdk

Before

Width:  |  Height:  |  Size: 510 KiB

After

Width:  |  Height:  |  Size: 510 KiB

Before

Width:  |  Height:  |  Size: 181 KiB

After

Width:  |  Height:  |  Size: 181 KiB

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<rect x="271" y="17" width="193" height="96" rx="19.2" fill="#f9bc8e" />
<rect x="168" y="108" width="206" height="115" rx="23.0" fill="#f7db75" />
<rect x="131" y="229" width="223" height="132" rx="26.4" fill="#aadac2" />
<rect x="47" y="349" width="238" height="144" rx="28.8" fill="#dfc2cf" />
</svg>

After

Width:  |  Height:  |  Size: 374 B

+91
View File
@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- Water‑color like noise -->
<filter id="wcTexture" x="-20%" y="-20%" width="140%" height="140%">
<feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="4" seed="12" result="noise"/>
<feBlend in="SourceGraphic" in2="noise" mode="multiply"/>
</filter>
<!-- Gradients -->
<radialGradient id="gradCoral" cx="50%" cy="40%" r="70%">
<stop offset="0%" stop-color="#ffddc9"/>
<stop offset="100%" stop-color="#f49c6e"/>
</radialGradient>
<radialGradient id="gradYellow" cx="45%" cy="35%" r="70%">
<stop offset="0%" stop-color="#fff6c9"/>
<stop offset="100%" stop-color="#e9c833"/>
</radialGradient>
<radialGradient id="gradMint" cx="40%" cy="30%" r="70%">
<stop offset="0%" stop-color="#d4f4ec"/>
<stop offset="100%" stop-color="#6bbda3"/>
</radialGradient>
<radialGradient id="gradLavender" cx="35%" cy="25%" r="70%">
<stop offset="0%" stop-color="#f5e6ff"/>
<stop offset="100%" stop-color="#b289c9"/>
</radialGradient>
</defs>
<!-- CORAL -->
<path filter="url(#wcTexture)" fill="url(#gradCoral)" d="
M 360 40
Q 380 15 420 22
L 450 28
Q 480 35 490 60
L 495 80
Q 500 105 470 120
L 440 135
Q 410 150 370 135
L 345 120
Q 315 105 325 75
L 330 55
Q 335 50 360 40 Z"/>
<!-- YELLOW -->
<path filter="url(#wcTexture)" fill="url(#gradYellow)" d="
M 280 190
Q 300 170 340 175
L 370 180
Q 405 185 410 210
L 415 230
Q 420 255 390 270
L 355 285
Q 320 300 290 285
L 265 270
Q 235 255 245 225
L 250 205
Q 255 200 280 190 Z"/>
<!-- MINT -->
<path filter="url(#wcTexture)" fill="url(#gradMint)" d="
M 180 330
Q 205 310 255 315
L 285 320
Q 325 325 330 350
L 335 370
Q 340 395 305 410
L 275 425
Q 235 440 200 425
L 175 410
Q 145 395 155 365
L 160 345
Q 165 340 180 330 Z"/>
<!-- LAVENDER -->
<path filter="url(#wcTexture)" fill="url(#gradLavender)" d="
M 80 460
Q 100 440 160 445
L 190 450
Q 235 455 240 480
L 245 500
Q 250 525 210 540
L 170 555
Q 130 570 95 555
L 65 540
Q 35 525 45 495
L 50 475
Q 55 470 80 460 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

+126
View File
@@ -0,0 +1,126 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
// Models
import '../models/user_registration_data.dart';
import '../models/am_registration_data.dart';
// Screens
import '../screens/auth/login_screen.dart';
import '../screens/auth/register_choice_screen.dart';
import '../screens/auth/parent_register_step1_screen.dart';
import '../screens/auth/parent_register_step2_screen.dart';
import '../screens/auth/parent_register_step3_screen.dart';
import '../screens/auth/parent_register_step4_screen.dart';
import '../screens/auth/parent_register_step5_screen.dart';
import '../screens/auth/am_register_step1_screen.dart';
import '../screens/auth/am_register_step2_screen.dart';
import '../screens/auth/am_register_step3_screen.dart';
import '../screens/auth/am_register_step4_screen.dart';
import '../screens/home/home_screen.dart';
import '../screens/administrateurs/admin_dashboardScreen.dart';
import '../screens/home/parent_screen/ParentDashboardScreen.dart';
import '../screens/unknown_screen.dart';
// --- Provider Instances ---
// It's generally better to provide these higher up the widget tree if possible,
// or ensure they are created only once.
// For ShellRoute, creating them here and passing via .value is common.
final userRegistrationDataNotifier = UserRegistrationData();
final amRegistrationDataNotifier = AmRegistrationData();
class AppRouter {
static final GoRouter router = GoRouter(
initialLocation: '/login',
errorBuilder: (context, state) => const UnknownScreen(),
debugLogDiagnostics: true,
routes: <RouteBase>[
GoRoute(
path: '/login',
builder: (BuildContext context, GoRouterState state) => const LoginScreen(),
),
GoRoute(
path: '/register-choice',
builder: (BuildContext context, GoRouterState state) => const RegisterChoiceScreen(),
),
GoRoute(
path: '/home',
builder: (BuildContext context, GoRouterState state) => const HomeScreen(),
),
GoRoute(
path: '/admin-dashboard',
builder: (BuildContext context, GoRouterState state) => const AdminDashboardScreen(),
),
GoRoute(
path: '/parent-dashboard',
builder: (BuildContext context, GoRouterState state) => const ParentDashboardScreen(),
),
GoRoute(
path: '/am-dashboard',
builder: (BuildContext context, GoRouterState state) => const HomeScreen(),
),
// --- Parent Registration Flow ---
ShellRoute(
builder: (context, state, child) {
return ChangeNotifierProvider<UserRegistrationData>.value(
value: userRegistrationDataNotifier,
child: child,
);
},
routes: <RouteBase>[
GoRoute(
path: '/parent-register-step1',
builder: (BuildContext context, GoRouterState state) => const ParentRegisterStep1Screen(),
),
GoRoute(
path: '/parent-register-step2',
builder: (BuildContext context, GoRouterState state) => const ParentRegisterStep2Screen(),
),
GoRoute(
path: '/parent-register-step3',
builder: (BuildContext context, GoRouterState state) => const ParentRegisterStep3Screen(),
),
GoRoute(
path: '/parent-register-step4',
builder: (BuildContext context, GoRouterState state) => const ParentRegisterStep4Screen(),
),
GoRoute(
path: '/parent-register-step5',
builder: (BuildContext context, GoRouterState state) => const ParentRegisterStep5Screen(),
),
],
),
// --- AM (Assistante Maternelle) Registration Flow ---
ShellRoute(
builder: (context, state, child) {
return ChangeNotifierProvider<AmRegistrationData>.value(
value: amRegistrationDataNotifier,
child: child,
);
},
routes: <RouteBase>[
GoRoute(
path: '/am-register-step1',
builder: (BuildContext context, GoRouterState state) => const AmRegisterStep1Screen(),
),
GoRoute(
path: '/am-register-step2',
builder: (BuildContext context, GoRouterState state) => const AmRegisterStep2Screen(),
),
GoRoute(
path: '/am-register-step3',
builder: (BuildContext context, GoRouterState state) => const AmRegisterStep3Screen(),
),
GoRoute(
path: '/am-register-step4',
builder: (BuildContext context, GoRouterState state) => const AmRegisterStep4Screen(),
),
],
),
],
);
}
+106
View File
@@ -0,0 +1,106 @@
import 'package:flutter/material.dart';
/// Mode d'affichage d'un formulaire
enum DisplayMode {
/// Mode éditable (formulaire d'inscription)
editable,
/// Mode lecture seule (récapitulatif)
readonly,
}
/// Configuration d'affichage pour les widgets de formulaire
class DisplayConfig {
/// Mode d'affichage (editable/readonly)
final DisplayMode mode;
/// Type de layout détecté (mobile/desktop)
final LayoutType layoutType;
const DisplayConfig({
required this.mode,
required this.layoutType,
});
/// Crée une config à partir du contexte
factory DisplayConfig.fromContext(
BuildContext context, {
DisplayMode mode = DisplayMode.editable,
}) {
return DisplayConfig(
mode: mode,
layoutType: LayoutHelper.getLayoutType(context),
);
}
/// Est en mode éditable
bool get isEditable => mode == DisplayMode.editable;
/// Est en mode lecture seule
bool get isReadonly => mode == DisplayMode.readonly;
/// Est en layout mobile
bool get isMobile => layoutType == LayoutType.mobile;
/// Est en layout desktop
bool get isDesktop => layoutType == LayoutType.desktop;
/// Layout vertical (mobile)
bool get isVerticalLayout => isMobile;
/// Layout horizontal (desktop)
bool get isHorizontalLayout => isDesktop;
}
/// Type de layout
enum LayoutType {
/// Mobile (< 600px) - toujours vertical
mobile,
/// Desktop/Tablette (≥ 600px) - horizontal
desktop,
}
/// Utilitaires pour la détection de layout
class LayoutHelper {
/// Seuil de largeur pour mobile/desktop (en pixels)
static const double mobileBreakpoint = 600.0;
/// Détermine le type de layout selon la largeur d'écran
static LayoutType getLayoutType(BuildContext context) {
final width = MediaQuery.of(context).size.width;
return width < mobileBreakpoint
? LayoutType.mobile
: LayoutType.desktop;
}
/// Vérifie si on est sur mobile
static bool isMobile(BuildContext context) {
return getLayoutType(context) == LayoutType.mobile;
}
/// Vérifie si on est sur desktop
static bool isDesktop(BuildContext context) {
return getLayoutType(context) == LayoutType.desktop;
}
/// Retourne un espacement adapté au layout
static double getSpacing(BuildContext context, {
double mobileSpacing = 12.0,
double desktopSpacing = 20.0,
}) {
return isMobile(context) ? mobileSpacing : desktopSpacing;
}
/// Retourne une largeur max adaptée au layout
static double getMaxWidth(BuildContext context, {
double? mobileMaxWidth,
double? desktopMaxWidth,
}) {
if (isMobile(context)) {
return mobileMaxWidth ?? double.infinity;
} else {
return desktopMaxWidth ?? 1200.0;
}
}
}
+2 -2
View File
@@ -2,11 +2,11 @@ class Env {
// Base URL de l'API, surchargeable à la compilation via --dart-define=API_BASE_URL // Base URL de l'API, surchargeable à la compilation via --dart-define=API_BASE_URL
static const String apiBaseUrl = String.fromEnvironment( static const String apiBaseUrl = String.fromEnvironment(
'API_BASE_URL', 'API_BASE_URL',
defaultValue: 'https://ynov.ptits-pas.fr', defaultValue: 'https://app.ptits-pas.fr',
); );
// Construit une URL vers l'API v1 à partir d'un chemin (commençant par '/') // Construit une URL vers l'API v1 à partir d'un chemin (commençant par '/')
static String apiV1(String path) => "${apiBaseUrl}/api/v1$path"; static String apiV1(String path) => '$apiBaseUrl/api/v1$path';
} }
+7 -5
View File
@@ -1,7 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; // Import pour la localisation
import 'navigation/app_router.dart'; // import 'package:provider/provider.dart'; // Supprimer Provider
import 'config/app_router.dart'; // <-- Importer le bon routeur (GoRouter)
// import 'theme/app_theme.dart'; // Supprimer AppTheme
// import 'theme/theme_provider.dart'; // Supprimer ThemeProvider
void main() { void main() {
runApp(const MyApp()); // Exécution simple runApp(const MyApp()); // Exécution simple
@@ -14,7 +17,7 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Pas besoin de Provider.of ici // Pas besoin de Provider.of ici
return MaterialApp( return MaterialApp.router( // <-- Utilisation de MaterialApp.router
title: 'P\'titsPas', title: 'P\'titsPas',
theme: ThemeData.light().copyWith( // Utiliser un thème simple par défaut theme: ThemeData.light().copyWith( // Utiliser un thème simple par défaut
textTheme: GoogleFonts.meriendaTextTheme( textTheme: GoogleFonts.meriendaTextTheme(
@@ -32,8 +35,7 @@ class MyApp extends StatelessWidget {
// Locale('en', 'US'), // Anglais, si besoin // Locale('en', 'US'), // Anglais, si besoin
], ],
locale: const Locale('fr', 'FR'), // Forcer la locale française par défaut locale: const Locale('fr', 'FR'), // Forcer la locale française par défaut
initialRoute: AppRouter.login, routerConfig: AppRouter.router, // <-- Passer la configuration du GoRouter
onGenerateRoute: AppRouter.generateRoute,
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
); );
} }
@@ -0,0 +1,136 @@
import 'package:flutter/foundation.dart';
class AmRegistrationData extends ChangeNotifier {
// Step 1: Identity Info
String firstName = '';
String lastName = '';
String streetAddress = ''; // Nouveau pour N° et Rue
String postalCode = ''; // Nouveau
String city = ''; // Nouveau
String phone = '';
String email = '';
String password = '';
// String? photoPath; // Déplacé ou géré à l'étape 2
// bool photoConsent = false; // Déplacé ou géré à l'étape 2
// Step 2: Professional Info
String? photoPath; // Ajouté pour l'étape 2
bool photoConsent = false; // Ajouté pour l'étape 2
DateTime? dateOfBirth;
String birthCity = ''; // Nouveau
String birthCountry = ''; // Nouveau
// String placeOfBirth = ''; // Remplacé par birthCity et birthCountry
String nir = ''; // Numéro de Sécurité Sociale
String agrementNumber = ''; // Numéro d'agrément
int? capacity; // Number of children the AM can look after
// Step 3: Presentation & CGU
String presentationText = '';
bool cguAccepted = false;
// --- Methods to update data and notify listeners ---
void updateIdentityInfo({
String? firstName,
String? lastName,
String? streetAddress, // Modifié
String? postalCode, // Nouveau
String? city, // Nouveau
String? phone,
String? email,
String? password,
}) {
this.firstName = firstName ?? this.firstName;
this.lastName = lastName ?? this.lastName;
this.streetAddress = streetAddress ?? this.streetAddress; // Modifié
this.postalCode = postalCode ?? this.postalCode; // Nouveau
this.city = city ?? this.city; // Nouveau
this.phone = phone ?? this.phone;
this.email = email ?? this.email;
this.password = password ?? this.password;
// if (photoPath != null || this.photoPath != null) { // Supprimé de l'étape 1
// this.photoPath = photoPath;
// }
// this.photoConsent = photoConsent ?? this.photoConsent; // Supprimé de l'étape 1
notifyListeners();
}
void updateProfessionalInfo({
String? photoPath,
bool? photoConsent,
DateTime? dateOfBirth,
String? birthCity, // Nouveau
String? birthCountry, // Nouveau
// String? placeOfBirth, // Remplacé
String? nir,
String? agrementNumber,
int? capacity,
}) {
// Allow setting photoPath to null explicitly
if (photoPath != null || this.photoPath != null) {
this.photoPath = photoPath;
}
this.photoConsent = photoConsent ?? this.photoConsent;
this.dateOfBirth = dateOfBirth ?? this.dateOfBirth;
this.birthCity = birthCity ?? this.birthCity; // Nouveau
this.birthCountry = birthCountry ?? this.birthCountry; // Nouveau
// this.placeOfBirth = placeOfBirth ?? this.placeOfBirth; // Remplacé
this.nir = nir ?? this.nir;
this.agrementNumber = agrementNumber ?? this.agrementNumber;
this.capacity = capacity ?? this.capacity;
notifyListeners();
}
void updatePresentationAndCgu({
String? presentationText,
bool? cguAccepted,
}) {
this.presentationText = presentationText ?? this.presentationText;
this.cguAccepted = cguAccepted ?? this.cguAccepted;
notifyListeners();
}
// --- Getters for validation or display ---
bool get isStep1Complete =>
firstName.isNotEmpty &&
lastName.isNotEmpty &&
streetAddress.isNotEmpty && // Modifié
postalCode.isNotEmpty && // Nouveau
city.isNotEmpty && // Nouveau
phone.isNotEmpty &&
email.isNotEmpty;
// password n'est pas requis à l'inscription (défini après validation par lien email)
bool get isStep2Complete =>
// photoConsent is mandatory if a photo is system-required, otherwise optional.
// For now, let's assume if photoPath is present, consent should ideally be true.
// Or, make consent always mandatory if photo section exists.
// Based on new mockup, photo is present, so consent might be implicitly or explicitly needed.
(photoPath != null ? photoConsent == true : true) && // Ajuster selon la logique de consentement désirée
dateOfBirth != null &&
birthCity.isNotEmpty &&
birthCountry.isNotEmpty &&
nir.isNotEmpty && // Basic check, could add validation
agrementNumber.isNotEmpty &&
capacity != null && capacity! > 0;
bool get isStep3Complete =>
// presentationText is optional as per CDC (message au gestionnaire)
cguAccepted;
bool get isRegistrationComplete =>
isStep1Complete && isStep2Complete && isStep3Complete;
@override
String toString() {
return 'AmRegistrationData('
'firstName: $firstName, lastName: $lastName, '
'streetAddress: $streetAddress, postalCode: $postalCode, city: $city, '
'phone: $phone, email: $email, '
// 'photoPath: $photoPath, photoConsent: $photoConsent, ' // Commenté car déplacé/modifié
'dateOfBirth: $dateOfBirth, birthCity: $birthCity, birthCountry: $birthCountry, '
'nir: $nir, agrementNumber: $agrementNumber, capacity: $capacity, '
'photoPath (step2): $photoPath, photoConsent (step2): $photoConsent, '
'presentationText: $presentationText, cguAccepted: $cguAccepted)';
}
}
@@ -0,0 +1,30 @@
import 'package:p_tits_pas/models/user.dart';
class AssistanteMaternelleModel {
final AppUser user;
final String? approvalNumber;
final String? residenceCity;
final int? maxChildren;
final int? placesAvailable;
AssistanteMaternelleModel({
required this.user,
this.approvalNumber,
this.residenceCity,
this.maxChildren,
this.placesAvailable,
});
factory AssistanteMaternelleModel.fromJson(Map<String, dynamic> json) {
final userJson = json['user'] ?? json;
final user = AppUser.fromJson(userJson);
return AssistanteMaternelleModel(
user: user,
approvalNumber: json['numero_agrement'] as String?,
residenceCity: json['ville_residence'] as String?,
maxChildren: json['nb_max_enfants'] as int?,
placesAvailable: json['place_disponible'] as int?,
);
}
}
+18
View File
@@ -0,0 +1,18 @@
import 'package:p_tits_pas/models/user.dart';
class ParentModel {
final AppUser user;
final int childrenCount;
ParentModel({required this.user, this.childrenCount = 0});
factory ParentModel.fromJson(Map<String, dynamic> json) {
final userJson = json['user'] ?? json;
final user = AppUser.fromJson(userJson);
final children = json['parentChildren'] as List?;
return ParentModel(
user: user,
childrenCount: children?.length ?? 0,
);
}
}
+65 -6
View File
@@ -4,6 +4,15 @@ class AppUser {
final String role; final String role;
final DateTime createdAt; final DateTime createdAt;
final DateTime updatedAt; final DateTime updatedAt;
final bool changementMdpObligatoire;
final String? nom;
final String? prenom;
final String? statut;
final String? telephone;
final String? photoUrl;
final String? adresse;
final String? ville;
final String? codePostal;
AppUser({ AppUser({
required this.id, required this.id,
@@ -11,15 +20,54 @@ class AppUser {
required this.role, required this.role,
required this.createdAt, required this.createdAt,
required this.updatedAt, required this.updatedAt,
this.changementMdpObligatoire = false,
this.nom,
this.prenom,
this.statut,
this.telephone,
this.photoUrl,
this.adresse,
this.ville,
this.codePostal,
}); });
factory AppUser.fromJson(Map<String, dynamic> json) { factory AppUser.fromJson(Map<String, dynamic> json) {
final id = json['id']?.toString();
final email = json['email']?.toString();
final role = json['role']?.toString();
if (id == null || id.isEmpty) {
throw Exception('Profil invalide: id manquant');
}
if (email == null || email.isEmpty) {
throw Exception('Profil invalide: email manquant');
}
if (role == null || role.isEmpty) {
throw Exception('Profil invalide: rôle manquant');
}
return AppUser( return AppUser(
id: json['id'] as String, id: id,
email: json['email'] as String, email: email,
role: json['role'] as String, role: role,
createdAt: DateTime.parse(json['createdAt'] as String), createdAt: json['cree_le'] != null
updatedAt: DateTime.parse(json['updatedAt'] as String), ? DateTime.tryParse(json['cree_le'].toString()) ?? DateTime.now()
: (json['createdAt'] != null
? DateTime.tryParse(json['createdAt'].toString()) ?? DateTime.now()
: DateTime.now()),
updatedAt: json['modifie_le'] != null
? DateTime.tryParse(json['modifie_le'].toString()) ?? DateTime.now()
: (json['updatedAt'] != null
? DateTime.tryParse(json['updatedAt'].toString()) ?? DateTime.now()
: DateTime.now()),
changementMdpObligatoire:
json['changement_mdp_obligatoire'] == true,
nom: json['nom']?.toString(),
prenom: json['prenom']?.toString(),
statut: json['statut']?.toString(),
telephone: json['telephone']?.toString(),
photoUrl: json['photo_url']?.toString(),
adresse: json['adresse']?.toString(),
ville: json['ville']?.toString(),
codePostal: json['code_postal']?.toString(),
); );
} }
@@ -30,6 +78,17 @@ class AppUser {
'role': role, 'role': role,
'createdAt': createdAt.toIso8601String(), 'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(), 'updatedAt': updatedAt.toIso8601String(),
'changement_mdp_obligatoire': changementMdpObligatoire,
'nom': nom,
'prenom': prenom,
'statut': statut,
'telephone': telephone,
'photo_url': photoUrl,
'adresse': adresse,
'ville': ville,
'code_postal': codePostal,
}; };
} }
}
String get fullName => '${prenom ?? ''} ${nom ?? ''}'.trim();
}
@@ -0,0 +1,155 @@
import 'dart:io'; // Pour File
import '../models/card_assets.dart'; // Import de l'enum CardColorVertical
import 'package:flutter/foundation.dart';
import 'package:intl/intl.dart';
// import 'package:p_tits_pas/models/child.dart'; // Commenté car fichier non trouvé
class ParentData {
String firstName;
String lastName;
String address; // Rue et numéro
String postalCode; // Ajout
String city; // Ajout
String phone;
String email;
String password; // Peut-être pas nécessaire pour le récap, mais pour la création initiale si
File? profilePicture; // Chemin ou objet File
ParentData({
this.firstName = '',
this.lastName = '',
this.address = '', // Rue
this.postalCode = '', // Ajout
this.city = '', // Ajout
this.phone = '',
this.email = '',
this.password = '',
this.profilePicture,
});
}
class ChildData {
String firstName;
String lastName;
String dob; // Date de naissance ou prévisionnelle
bool photoConsent;
bool multipleBirth;
bool isUnbornChild;
File? imageFile;
CardColorVertical cardColor; // Nouveau champ pour la couleur de la carte
ChildData({
this.firstName = '',
this.lastName = '',
this.dob = '',
this.photoConsent = false,
this.multipleBirth = false,
this.isUnbornChild = false,
this.imageFile,
required this.cardColor, // Rendre requis dans le constructeur
});
}
// Nouvelle classe pour les détails bancaires
class BankDetails {
String bankName;
String iban;
String bic;
BankDetails({
this.bankName = '',
this.iban = '',
this.bic = '',
});
}
class UserRegistrationData extends ChangeNotifier {
ParentData parent1;
ParentData? parent2; // Optionnel
List<ChildData> children;
String motivationText;
bool cguAccepted;
BankDetails? bankDetails; // Ajouté
String attestationCafNumber; // Ajouté
bool consentQuotientFamilial; // Ajouté
UserRegistrationData({
ParentData? parent1Data,
this.parent2,
List<ChildData>? childrenData,
this.motivationText = '',
this.cguAccepted = false,
this.bankDetails, // Ajouté
this.attestationCafNumber = '', // Ajouté
this.consentQuotientFamilial = false, // Ajouté
}) : parent1 = parent1Data ?? ParentData(),
children = childrenData ?? [];
// Méthode pour ajouter/mettre à jour le parent 1
void updateParent1(ParentData data) {
parent1 = data;
notifyListeners(); // Notifier les changements
}
// Méthode pour ajouter/mettre à jour le parent 2
void updateParent2(ParentData? data) {
parent2 = data;
notifyListeners();
}
// Méthode pour ajouter un enfant
void addChild(ChildData child) {
children.add(child);
notifyListeners();
}
// Méthode pour mettre à jour un enfant (si nécessaire plus tard)
void updateChild(int index, ChildData child) {
if (index >= 0 && index < children.length) {
children[index] = child;
notifyListeners();
}
}
// Méthode pour supprimer un enfant
void removeChild(int index) {
if (index >= 0 && index < children.length) {
children.removeAt(index);
notifyListeners();
}
}
// Mettre à jour la motivation
void updateMotivation(String text) {
motivationText = text;
notifyListeners();
}
// Mettre à jour les informations bancaires et CAF
void updateFinancialInfo({
BankDetails? bankDetails,
String? attestationCafNumber,
bool? consentQuotientFamilial,
}) {
if (bankDetails != null) this.bankDetails = bankDetails;
if (attestationCafNumber != null) this.attestationCafNumber = attestationCafNumber;
if (consentQuotientFamilial != null) this.consentQuotientFamilial = consentQuotientFamilial;
notifyListeners();
}
// Accepter les CGU
void acceptCGU(bool accepted) { // Prend un booléen
cguAccepted = accepted;
notifyListeners();
}
// Méthode pour vérifier si toutes les données requises sont là (simplifié)
bool isRegistrationComplete() {
// Ajouter ici les validations nécessaires
// Exemple : parent1 doit avoir des champs remplis, au moins un enfant, CGU acceptées
return parent1.firstName.isNotEmpty &&
parent1.lastName.isNotEmpty &&
children.isNotEmpty &&
cguAccepted;
}
}
-164
View File
@@ -1,164 +0,0 @@
import 'package:flutter/material.dart';
import 'package:p_tits_pas/models/am_user_registration_data.dart';
import 'package:p_tits_pas/screens/administrateurs/admin_dashboardScreen.dart';
import 'package:p_tits_pas/screens/auth/am/am_register_step1_sceen.dart';
import 'package:p_tits_pas/screens/auth/am/am_register_step2_sceen.dart';
import 'package:p_tits_pas/screens/auth/am/am_register_step3_sceen.dart';
import 'package:p_tits_pas/screens/auth/am/am_register_step4_sceen.dart';
import 'package:p_tits_pas/screens/home/parent_screen/ParentDashboardScreen.dart';
import 'package:p_tits_pas/screens/home/parent_screen/find_nanny.dart';
import 'package:p_tits_pas/screens/legal/legal_page.dart';
import 'package:p_tits_pas/screens/legal/privacy_page.dart';
import '../screens/auth/login_screen.dart';
import '../screens/auth/register_choice_screen.dart';
import '../screens/auth/parent/parent_register_step1_screen.dart';
import '../screens/auth/parent/parent_register_step2_screen.dart';
import '../screens/auth/parent/parent_register_step3_screen.dart';
import '../screens/auth/parent/parent_register_step4_screen.dart';
import '../screens/auth/parent/parent_register_step5_screen.dart';
import '../models/parent_user_registration_data.dart';
class AppRouter {
static const String login = '/login';
static const String registerChoice = '/register-choice';
static const String legal = '/legal';
static const String privacy = '/privacy';
static const String parentRegisterStep1 = '/parent-register/step1';
static const String parentRegisterStep2 = '/parent-register/step2';
static const String parentRegisterStep3 = '/parent-register/step3';
static const String parentRegisterStep4 = '/parent-register/step4';
static const String parentRegisterStep5 = '/parent-register/step5';
static const String amRegisterStep1 = '/am-register/step1';
static const String amRegisterStep2 = '/am-register/step2';
static const String amRegisterStep3 = '/am-register/step3';
static const String amRegisterStep4 = '/am-register/step4';
static const String parentDashboard = '/parent-dashboard';
static const String admin_dashboard = '/admin_dashboard';
static const String findNanny = '/find-nanny';
static Route<dynamic> generateRoute(RouteSettings settings) {
Widget screen;
bool slideTransition = false;
Object? args = settings.arguments;
Widget buildErrorScreen(String step) {
print("Erreur: Données UserRegistrationData manquantes ou de mauvais type pour l'étape $step");
return const ParentRegisterStep1Screen();
}
switch (settings.name) {
case login:
screen = const LoginPage();
break;
case registerChoice:
screen = const RegisterChoiceScreen();
slideTransition = true;
break;
case legal:
screen = const LegalPage();
slideTransition = true;
break;
case privacy:
screen = const PrivacyPage();
slideTransition = true;
break;
case parentRegisterStep1:
screen = ParentRegisterStep1Screen();
slideTransition = true;
break;
case parentRegisterStep2:
if (args is UserRegistrationData) {
screen = ParentRegisterStep2Screen(registrationData: args);
} else {
screen = buildErrorScreen('2');
}
slideTransition = true;
break;
case parentRegisterStep3:
if (args is UserRegistrationData) {
screen = ParentRegisterStep3Screen(registrationData: args);
} else {
screen = buildErrorScreen('3');
}
slideTransition = true;
break;
case parentRegisterStep4:
if (args is UserRegistrationData) {
screen = ParentRegisterStep4Screen(registrationData: args);
} else {
screen = buildErrorScreen('4');
}
slideTransition = true;
break;
case parentRegisterStep5:
if (args is UserRegistrationData) {
screen = ParentRegisterStep5Screen(registrationData: args);
} else {
screen = buildErrorScreen('5');
}
slideTransition = true;
break;
case amRegisterStep1:
screen = const AmRegisterStep1Screen();
slideTransition = true;
break;
case amRegisterStep2:
if (args is ChildminderRegistrationData) {
screen = AmRegisterStep2Screen(registrationData: args);
} else {
screen = AmRegisterStep2Screen(registrationData: ChildminderRegistrationData());
}
slideTransition = true;
break;
case amRegisterStep3:
if (args is ChildminderRegistrationData) {
screen = AmRegisterStep3Screen(registrationData: args);
} else {
screen = AmRegisterStep3Screen(registrationData: ChildminderRegistrationData());
}
slideTransition = true;
break;
case amRegisterStep4:
if (args is ChildminderRegistrationData) {
screen = AmRegisterStep4Screen(registrationData: args);
} else {
screen = AmRegisterStep4Screen(registrationData: ChildminderRegistrationData());
}
slideTransition = true;
break;
case parentDashboard:
screen = const ParentDashboardScreen();
break;
case admin_dashboard:
screen = const AdminDashboardScreen();
break;
case findNanny:
screen = const FindNannyScreen();
break;
default:
screen = Scaffold(
body: Center(
child: Text('Route non définie : ${settings.name}'),
),
);
}
if (slideTransition) {
return PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => screen,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
const begin = Offset(1.0, 0.0);
const end = Offset.zero;
const curve = Curves.easeInOut;
var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
var offsetAnimation = animation.drive(tween);
return SlideTransition(position: offsetAnimation, child: child);
},
transitionDuration: const Duration(milliseconds: 400),
);
} else {
return MaterialPageRoute(builder: (_) => screen);
}
}
}
@@ -1,7 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:p_tits_pas/services/configuration_service.dart';
import 'package:p_tits_pas/widgets/admin/assistante_maternelle_management_widget.dart'; import 'package:p_tits_pas/widgets/admin/assistante_maternelle_management_widget.dart';
import 'package:p_tits_pas/widgets/admin/gestionnaire_management_widget.dart'; import 'package:p_tits_pas/widgets/admin/gestionnaire_management_widget.dart';
import 'package:p_tits_pas/widgets/admin/parent_managmant_widget.dart'; import 'package:p_tits_pas/widgets/admin/parent_managmant_widget.dart';
import 'package:p_tits_pas/widgets/admin/admin_management_widget.dart';
import 'package:p_tits_pas/widgets/admin/parametres_panel.dart';
import 'package:p_tits_pas/widgets/app_footer.dart'; import 'package:p_tits_pas/widgets/app_footer.dart';
import 'package:p_tits_pas/widgets/admin/dashboard_admin.dart'; import 'package:p_tits_pas/widgets/admin/dashboard_admin.dart';
@@ -9,20 +12,55 @@ class AdminDashboardScreen extends StatefulWidget {
const AdminDashboardScreen({super.key}); const AdminDashboardScreen({super.key});
@override @override
_AdminDashboardScreenState createState() => _AdminDashboardScreenState(); State<AdminDashboardScreen> createState() => _AdminDashboardScreenState();
} }
class _AdminDashboardScreenState extends State<AdminDashboardScreen> { class _AdminDashboardScreenState extends State<AdminDashboardScreen> {
int selectedIndex = 0; bool? _setupCompleted;
int mainTabIndex = 0;
int subIndex = 0;
void onTabChange(int index) { @override
void initState() {
super.initState();
_loadSetupStatus();
}
Future<void> _loadSetupStatus() async {
try {
final completed = await ConfigurationService.getSetupStatus();
if (!mounted) return;
setState(() {
_setupCompleted = completed;
if (!completed) mainTabIndex = 1;
});
} catch (e) {
if (mounted) setState(() {
_setupCompleted = false;
mainTabIndex = 1;
});
}
}
void onMainTabChange(int index) {
setState(() { setState(() {
selectedIndex = index; mainTabIndex = index;
});
}
void onSubTabChange(int index) {
setState(() {
subIndex = index;
}); });
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_setupCompleted == null) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
return Scaffold( return Scaffold(
appBar: PreferredSize( appBar: PreferredSize(
preferredSize: const Size.fromHeight(60.0), preferredSize: const Size.fromHeight(60.0),
@@ -33,13 +71,19 @@ class _AdminDashboardScreenState extends State<AdminDashboardScreen> {
), ),
), ),
child: DashboardAppBarAdmin( child: DashboardAppBarAdmin(
selectedIndex: selectedIndex, selectedIndex: mainTabIndex,
onTabChange: onTabChange, onTabChange: onMainTabChange,
setupCompleted: _setupCompleted!,
), ),
), ),
), ),
body: Column( body: Column(
children: [ children: [
if (mainTabIndex == 0)
DashboardUserManagementSubBar(
selectedSubIndex: subIndex,
onSubTabChange: onSubTabChange,
),
Expanded( Expanded(
child: _getBody(), child: _getBody(),
), ),
@@ -50,17 +94,20 @@ class _AdminDashboardScreenState extends State<AdminDashboardScreen> {
} }
Widget _getBody() { Widget _getBody() {
switch (selectedIndex) { if (mainTabIndex == 1) {
return ParametresPanel(redirectToLoginAfterSave: !_setupCompleted!);
}
switch (subIndex) {
case 0: case 0:
return const GestionnaireManagementWidget(); return const GestionnaireManagementWidget();
case 1: case 1:
return const ParentManagementWidget(); return const ParentManagementWidget();
case 2: case 2:
return const AssistanteMaternelleManagementWidget(); return const AssistanteMaternelleManagementWidget();
case 3: case 3:
return const Center(child: Text("👨‍💼 Administrateurs")); return const AdminManagementWidget();
default: default:
return const Center(child: Text("Page non trouvée")); return const Center(child: Text('Page non trouvée'));
} }
} }
} }

Some files were not shown because too many files have changed in this diff Show More