Compare commits

...
Author SHA1 Message Date
jmartinandCursor d32d956b0e feat(dashboard-admin): connect admin dashboard to real API data (Ticket #92)
- Frontend:
  - Create UserService to handle user-related API calls (gestionnaires, parents, AMs, admins)
  - Update AdminDashboardScreen to use dynamic widgets
  - Implement dynamic management widgets:
    - GestionnaireManagementWidget
    - ParentManagementWidget
    - AssistanteMaternelleManagementWidget
    - AdminManagementWidget
  - Add data models: ParentModel, AssistanteMaternelleModel
  - Update AppUser model
  - Update ApiConfig

- Backend:
  - Update controllers (Parents, AMs, Gestionnaires, Users) to allow ADMINISTRATEUR role to list users
  - Fix: Activate endpoint GET /gestionnaires (import GestionnairesModule in UserModule)

- Docs:
  - Add note about backend fix for Gestionnaires module
  - Update .cursorrules to forbid worktrees

- Seed:
  - Add test data seed script (reset-and-seed-db.sh)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-17 22:17:51 +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
jmartin a5dae7a017 docs: Workflow création de compte + refonte documentation
- Ajout Cahier des Charges v1.3
- Ajout Workflow technique création de compte (v1.0)
- Réorganisation docs avec préfixes numériques (00_, 01_, etc.)
- Ajout données de test CSV
- Modifications principales :
  * Champ téléphone unique (suppression mobile/fixe)
  * Inscription sans mot de passe (Parents + AM)
  * Création MDP par email après validation (7j)
  * Genre enfant obligatoire (H/F)
  * Date agrément obligatoire pour AM
2025-11-25 00:28:35 +01:00
jmartin 48b01ed3fe docs: Ajout de la documentation complète (API, Database, Audit)
- Création du dossier docs/ pour centraliser la documentation
- Ajout de API.md : documentation complète de tous les endpoints
- Ajout de DATABASE.md : schéma complet de la base de données
- Ajout de AUDIT.md : audit du projet YNOV
- Déplacement des README-ARCHITECTURE.md et README-DEPLOYMENT.md vers docs/
- Ajout d'un README.md index dans docs/
2025-11-24 21:39:01 +01:00
jmartin aa61831878 feat: Création de la structure api-contracts
- Contrats d'API Frontend ↔ Backend (OpenAPI 3.0)
- Contrats Backend ↔ Database (Prisma/SQL)
- Documentation complète pour génération de code
- Permet l'interchangeabilité des composants
2025-11-24 15:45:07 +01:00
jmartin ad81a2f4f4 feat: Configuration Docker Compose à 3 services
- Frontend: Flutter web (app.ptits-pas.fr)
- Backend: NestJS API (app.ptits-pas.fr/api)
- Database: PostgreSQL 17 + PgAdmin (app.ptits-pas.fr/pgadmin)
- Réseau: ptitspas_network + proxy_network (Traefik)
- Documentation architecture et déploiement
2025-11-24 15:44:58 +01:00
jmartin bbf73458cb feat: Intégration de la base de données PostgreSQL depuis YNOV
- Structure complète: utilisateurs, parents, assmat, enfants, contrats
- Migrations SQL avec enums et contraintes
- Seed: 1 super_admin (admin@ptits-pas.fr)
- Mot de passe: 4dm1n1strateur (hash bcrypt)
2025-11-24 15:44:39 +01:00
jmartin 9cb4162165 feat: Intégration du frontend Flutter depuis YNOV
- Framework: Flutter web
- Pages: Login, inscription, dashboards
- Services: API client, authentification, gestion d'état
- Intégration avec backend NestJS
- Dockerfile pour déploiement web
2025-11-24 15:44:15 +01:00
jmartin 33d6e7b0c3 feat: Intégration du backend NestJS depuis YNOV
- Framework: NestJS avec TypeORM
- Authentification: JWT (access + refresh tokens)
- Gestion utilisateurs: CRUD complet avec validation
- Routes: auth, users, parents, assistantes maternelles
- Dockerfile pour conteneurisation
2025-11-24 15:44:07 +01:00
jmartin 49f0684ad3 nettoyage: Suppression des dossiers de la maquette initiale 2025-11-24 15:43:53 +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
320 changed files with 41255 additions and 3575 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.

Before

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 MiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 662 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 501 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 563 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 498 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+87
View File
@@ -0,0 +1,87 @@
# 📜 API Contracts - PtitsPas
Ce dossier contient les **contrats d'API** qui définissent les interfaces entre les différentes couches de l'application.
## 🎯 Objectif
Garantir que **Frontend**, **Backend** et **Database** respectent des contrats stricts, permettant de les rendre **interchangeables** sans casser l'application.
---
## 📁 Structure
```
api-contracts/
├── frontend-backend/ # Contrat Frontend ↔ Backend (HTTP REST)
│ ├── openapi.yaml # Spécification OpenAPI 3.0 (source de vérité)
│ └── generated/ # Code généré automatiquement
│ ├── dart/ # Client API pour Flutter
│ └── typescript/ # Types pour NestJS
└── backend-database/ # Contrat Backend ↔ Database (ORM/SQL)
├── schema.prisma # Schéma Prisma (ou TypeORM entities)
└── migrations/ # Migrations SQL versionnées
```
---
## 🔄 Workflow de Génération
### 1. Frontend ↔ Backend
**Source de vérité :** `frontend-backend/openapi.yaml`
**Génération du client Dart (Flutter) :**
```bash
cd api-contracts/frontend-backend
docker run --rm -v "${PWD}:/local" openapitools/openapi-generator-cli generate \
-i /local/openapi.yaml \
-g dart-dio \
-o /local/generated/dart
```
**Génération des types TypeScript (NestJS) :**
```bash
cd api-contracts/frontend-backend
npx openapi-typescript openapi.yaml --output generated/typescript/api.types.ts
```
---
### 2. Backend ↔ Database
**Source de vérité :** `backend-database/schema.prisma`
**Génération du client Prisma :**
```bash
cd api-contracts/backend-database
npx prisma generate
```
**Génération des migrations SQL :**
```bash
cd api-contracts/backend-database
npx prisma migrate dev --name <nom_migration>
```
---
## ✅ Avantages
- **Frontend interchangeable** : React, Vue, Angular → il suffit de régénérer le client API
- **Backend interchangeable** : Python, Go, Java → tant qu'il respecte `openapi.yaml`
- **Database read-only en prod** : User PostgreSQL `app_user` (pas de DDL)
- **Cohérence garantie** : Types générés = pas d'erreur de typage
- **Documentation auto** : OpenAPI = documentation interactive (Swagger UI)
---
## 📚 Documentation
- [OpenAPI 3.0 Spec](https://swagger.io/specification/)
- [Prisma Schema](https://www.prisma.io/docs/concepts/components/prisma-schema)
- [openapi-generator](https://openapi-generator.tech/)
- [openapi-typescript](https://github.com/drwpow/openapi-typescript)
+27
View File
@@ -0,0 +1,27 @@
# 💾 Backend ↔ Database Contract
Ce dossier contient le **contrat de données** entre le Backend et la Base de Données.
## 📋 Contenu
- **`schema.prisma`** : Schéma de base de données (à créer)
- **`migrations/`** : Migrations SQL versionnées (actuellement dans `/database/migrations/`)
## 🔄 Migration Future
À terme, les migrations SQL de `/database/migrations/` seront gérées ici avec Prisma :
```bash
# Générer une migration
npx prisma migrate dev --name add_user_phone
# Appliquer en production
npx prisma migrate deploy
```
## 📚 Référence
- [Prisma Migrate](https://www.prisma.io/docs/concepts/components/prisma-migrate)
+179
View File
@@ -0,0 +1,179 @@
openapi: 3.0.0
info:
title: PtitsPas API
version: 1.0.0
description: |
API REST pour l'application PtitsPas.
Ce contrat définit l'interface entre le Frontend (Flutter) et le Backend (NestJS).
contact:
name: PtitsPas Team
email: admin@ptits-pas.fr
servers:
- url: https://app.ptits-pas.fr/api
description: Production
- url: http://localhost:3000/api
description: Développement local
paths:
/auth/login:
post:
summary: Authentification utilisateur
operationId: loginUser
tags:
- Authentication
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- email
- password
properties:
email:
type: string
format: email
example: admin@ptits-pas.fr
password:
type: string
format: password
example: "4dm1n1strateur"
responses:
'200':
description: Authentification réussie
content:
application/json:
schema:
type: object
properties:
access_token:
type: string
description: Token JWT d'accès
refresh_token:
type: string
description: Token JWT de rafraîchissement
user:
$ref: '#/components/schemas/User'
'401':
description: Identifiants invalides
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/users:
get:
summary: Liste des utilisateurs
operationId: listUsers
tags:
- Users
security:
- bearerAuth: []
parameters:
- name: role
in: query
schema:
$ref: '#/components/schemas/RoleType'
- name: statut
in: query
schema:
$ref: '#/components/schemas/StatutUtilisateurType'
responses:
'200':
description: Liste des utilisateurs
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
'401':
description: Non authentifié
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
User:
type: object
required:
- id
- email
- role
- statut
properties:
id:
type: string
format: uuid
example: "550e8400-e29b-41d4-a716-446655440000"
email:
type: string
format: email
example: "parent@ptits-pas.fr"
prenom:
type: string
example: "Jean"
nom:
type: string
example: "Dupont"
role:
$ref: '#/components/schemas/RoleType'
statut:
$ref: '#/components/schemas/StatutUtilisateurType'
telephone:
type: string
example: "0612345678"
adresse:
type: string
photo_url:
type: string
format: uri
cree_le:
type: string
format: date-time
RoleType:
type: string
enum:
- parent
- assistante_maternelle
- gestionnaire
- administrateur
- super_admin
StatutUtilisateurType:
type: string
enum:
- en_attente
- actif
- suspendu
Error:
type: object
required:
- message
- statusCode
properties:
message:
type: string
example: "Identifiants invalides"
statusCode:
type: integer
example: 401
error:
type: string
example: "Unauthorized"
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: Token JWT obtenu via /auth/login
+26
View File
@@ -0,0 +1,26 @@
# Fichier: .env.example
# Copier ce fichier vers .env et adapter les valeurs selon votre environnement
# Configuration de la base de données PostgreSQL
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
POSTGRES_USER=admin
POSTGRES_PASSWORD=admin123
POSTGRES_DB=ptitpas_db
# Configuration PgAdmin (accessible sur http://localhost:8080)
PGADMIN_DEFAULT_EMAIL=admin@localhost
PGADMIN_DEFAULT_PASSWORD=admin123
# Configuration de l'API
API_PORT=3000
# Secrets pour l'authentification JWT
JWT_SECRET=dev-jwt-secret-key-change-me
JWT_EXPIRATION_TIME=7d
# Environnement
NODE_ENV=development
# Log de chaque appel API (mode debug) — mettre à true pour tracer les requêtes front
# LOG_API_REQUESTS=true
+64
View File
@@ -0,0 +1,64 @@
# compiled output
/dist
/node_modules
/build
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# temp directory
.temp
.tmp
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
.env
# Tests bdd
.vscode/
BDD.sql
migrations/
src/seed/
+4
View File
@@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}
+42
View File
@@ -0,0 +1,42 @@
FROM node:22-alpine AS builder
WORKDIR /app
# Copier les fichiers de configuration
COPY package*.json ./
COPY tsconfig*.json ./
COPY nest-cli.json ./
# Installer TOUTES les dépendances (dev + prod pour le build)
RUN npm install && npm cache clean --force
# Copier le code source
COPY src ./src
# Builder l'application
RUN npm run build
# Stage production
FROM node:22-alpine AS production
WORKDIR /app
# Installer seulement les dépendances de production
COPY package*.json ./
RUN npm install --only=production && npm cache clean --force
# Copier le build depuis le stage builder
COPY --from=builder /app/dist ./dist
# Créer un utilisateur non-root
RUN addgroup -g 1001 -S nodejs
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
EXPOSE 3000
CMD ["node", "dist/main"]
+63
View File
@@ -0,0 +1,63 @@
# 🚀 Guide de développement local
## Prérequis
- Docker et Docker Compose installés
- Git
## 🏃‍♂️ Démarrage rapide
### 1. Cloner le projet
```bash
git clone <url-du-depot-backend>
cd ptitspas-backend
```
### 2. Configuration de l'environnement
```bash
# Copier le fichier d'exemple
cp .env.example .env
# Optionnel : adapter les valeurs dans .env selon vos besoins
```
### 3. Lancer l'application
```bash
# Démarrer tous les services (PostgreSQL + PgAdmin + Backend)
docker compose -f docker-compose.dev.yml up -d
# Voir les logs
docker compose -f docker-compose.dev.yml logs -f
```
## 🌐 Accès aux services
- **Backend API** : http://localhost:3000
- **PgAdmin** : http://localhost:8080
- Email : admin@localhost
- Mot de passe : admin123
- **PostgreSQL** : localhost:5432
- Utilisateur : admin
- Mot de passe : admin123
- Base : ptitpas_db
## 🛠️ Commandes utiles
```bash
# Arrêter les services
docker compose -f docker-compose.dev.yml down
# Rebuild le backend après modification du Dockerfile
docker compose -f docker-compose.dev.yml up --build backend
# Voir l'état des services
docker compose -f docker-compose.dev.yml ps
# Accéder au container backend
docker exec -it ptitspas-backend-dev sh
```
## 📝 Notes de développement
- Les modifications du code source sont automatiquement prises en compte (hot reload)
- Les données PostgreSQL sont persistantes via le volume `postgres_dev_data`
- Le fichier `.env` n'est pas versionné pour des raisons de sécurité
+158
View File
@@ -0,0 +1,158 @@
# P'titsPas API Backend ✨
Ce dépôt contient le code source de l'API backend pour la plateforme **P'titsPas**. L'API est construite avec NestJS et est responsable de toute la logique métier, de la gestion des données et de l'authentification des utilisateurs.
---
## 📚 Table des matières
- [Technologies utilisées](#-technologies-utilisées)
- [Prérequis](#-prérequis)
- [Installation](#-installation)
- [Lancement de l'application](#-lancement-de-lapplication)
- [Scripts principaux](#-scripts-principaux)
- [Tests](#-tests)
- [Gestion des migrations](#-gestion-des-migrations)
- [Documentation de l'API](#-documentation-de-lapi)
---
## 🛠️ Technologies utilisées
- **Framework**: [NestJS](https://nestjs.com/) (TypeScript)
- **Base de données**: [PostgreSQL](https://www.postgresql.org/)
- **ORM**: [TypeORM](https://typeorm.io/)
- **Authentification**: JWT avec [Passport.js](http://www.passportjs.org/)
- **Stockage Fichiers**: [MinIO](https://min.io/) (Compatible S3)
- **Tâches Asynchrones**: [Redis](https://redis.io/) avec [BullMQ](https://bullmq.io/)
- **Conteneurisation**: [Docker](https://www.docker.com/)
---
## 📋 Prérequis
Avant de commencer, assurez-vous d'avoir installé les outils suivants sur votre machine :
- [Node.js](https://nodejs.org/) (v18 ou supérieure)
- [npm](https://www.npmjs.com/) ou [pnpm](https://pnpm.io/)
- [Docker](https://www.docker.com/products/docker-desktop/) et Docker Compose
- [Git](https://git-scm.com/)
---
## 🚀 Installation
1. **Clonez le dépôt :**
```bash
git clone https://github.com/votre-username/ptitspas-backend.git
cd ptitspas-backend
```
2. **Créez le fichier d'environnement :**
Copiez le fichier d'exemple `.env.example` et renommez-le en `.env`. Ce fichier est ignoré par Git et contiendra vos secrets locaux.
```bash
cp .env.example .env
```
➡️ **Important :** Ouvrez le fichier `.env` et remplissez les variables (identifiants de la base de données, secrets JWT, etc.).
3. **Installez les dépendances du projet :**
```bash
npm install
```
---
## ▶️ Lancement de l'application
### Méthode recommandée : avec Docker
Cette méthode lance l'ensemble des services nécessaires (API, base de données, MinIO, Redis) dans des conteneurs isolés.
```bash
docker-compose up --build
```
L'API sera accessible à l'adresse `http://localhost:3000` (ou le port que vous avez configuré dans votre `.env`).
### Méthode locale (pour le développement)
Cette méthode ne lance que le serveur NestJS. Assurez-vous que les autres services (PostgreSQL, Redis, etc.) sont déjà en cours d'exécution (par exemple, via Docker).
```
npm run start:dev
```
Le serveur redémarrera automatiquement à chaque modification de fichier.
---
## ⚙️ Scripts principaux
| Commande | Description |
| :------------------ | :-------------------------------------------------------------------- |
| `npm run start:dev` | Lance le serveur en mode développement avec rechargement automatique. |
| `npm run build` | Compile le projet TypeScript en JavaScript. |
| `npm start` | Lance l'application depuis les fichiers compilés (mode production). |
| `npm run lint` | Analyse le code pour détecter les erreurs de style et de syntaxe. |
---
## 🧪 Tests
Pour lancer les tests, utilisez les commandes suivantes :
| Commande | Description |
| :----------------- | :--------------------------------------------------------------- |
| `npm test` | Lance les tests unitaires. |
| `npm run test:e2e` | Lance les tests de bout en bout (end-to-end). |
| `npm run test:cov` | Lance tous les tests et génère un rapport de couverture de code. |
---
## 🗄️ Gestion des migrations
La structure de la base de données est gérée par des fichiers de migration TypeORM.
1. **Générer une nouvelle migration :**
Après avoir modifié une entité TypeORM, générez automatiquement le fichier de migration correspondant.
```bash
npm run migration:generate -- src/database/migrations/NomDeLaMigration
```
2. **Appliquer les migrations :**
Pour mettre à jour le schéma de votre base de données avec les nouvelles migrations.
```bash
npm run migration:run
```
---
## 📖 Documentation de l'API
Une fois l'application lancée, la documentation de l'API générée avec **Swagger (OpenAPI)** est disponible à l'adresse suivante :
➡️ **[http://localhost:3000/api-docs](http://localhost:3000/api-docs)**
Cette interface vous permet d'explorer et de tester toutes les routes de l'API directement depuis votre navigateur.
Excellente idée. C'est un élément crucial qui définit les droits et les devoirs liés à votre code.
En me basant sur la section `10.5 Propriété intellectuelle et licence de lapplication` de votre cahier des charges, j'ai rédigé une section "Licence" qui reflète précisément le statut propriétaire de votre projet.
Voici le `README.md` complet et mis à jour.
---
## 📜 Licence
Ce projet est distribué sous une **licence propriétaire**.
Le code source, la marque "P'titsPas" et la documentation associée sont la propriété exclusive de l'éditeur, Julien MARTIN.
Toute reproduction, distribution, modification ou utilisation du code source est strictement interdite sans un accord écrit préalable de l'auteur. Les clients et partenaires autorisés disposent d'une licence d'utilisation non-exclusive et non-transférable, conformément aux termes de leur contrat.
Pour toute question relative à l'utilisation ou à l'acquisition d'une licence, veuillez contacter l'auteur.
+72
View File
@@ -0,0 +1,72 @@
# Docker Compose pour développement local
# Usage: docker compose -f docker-compose.dev.yml up -d
services:
# Base de données PostgreSQL
postgres:
image: postgres:17
container_name: ptitspas-postgres-dev
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-admin}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-admin123}
POSTGRES_DB: ${POSTGRES_DB:-ptitpas_db}
ports:
- "5433:5432"
volumes:
# Si le fichier d'init existe dans le dépôt database
- ./migrations/01_init.sql:/docker-entrypoint-initdb.d/01_init.sql
- postgres_dev_data:/var/lib/postgresql/data
networks:
- ptitspas_dev
# Interface d'administration DB
pgadmin:
image: dpage/pgadmin4
container_name: ptitspas-pgadmin-dev
restart: unless-stopped
environment:
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-admin@ptits-pas.fr}
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin123}
ports:
- "8080:80"
depends_on:
- postgres
networks:
- ptitspas_dev
# Backend NestJS
backend:
build:
context: .
dockerfile: Dockerfile
container_name: ptitspas-backend-dev
restart: unless-stopped
environment:
POSTGRES_HOST: ${POSTGRES_HOST:-postgres}
POSTGRES_PORT: ${POSTGRES_PORT:-5432}
POSTGRES_USER: ${POSTGRES_USER:-admin}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-admin123}
POSTGRES_DB: ${POSTGRES_DB:-ptitpas_db}
API_PORT: ${API_PORT:-3000}
JWT_SECRET: ${JWT_SECRET:-dev-jwt-secret-key}
JWT_EXPIRATION_TIME: ${JWT_EXPIRATION_TIME:-7d}
NODE_ENV: ${NODE_ENV:-development}
ports:
- "3000:3000"
depends_on:
- postgres
volumes:
# Pour le hot reload en développement
- ./src:/app/src
- /app/node_modules
networks:
- ptitspas_dev
volumes:
postgres_dev_data:
networks:
ptitspas_dev:
driver: bridge
+34
View File
@@ -0,0 +1,34 @@
// @ts-check
import eslint from '@eslint/js';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import globals from 'globals';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{
ignores: ['eslint.config.mjs'],
},
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
eslintPluginPrettierRecommended,
{
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
sourceType: 'commonjs',
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn'
},
},
);
+8
View File
@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}
+10226 -1341
View File
File diff suppressed because it is too large Load Diff
+85 -26
View File
@@ -1,36 +1,95 @@
{ {
"name": "petitspas-backend", "name": "ptitspas-ynov-back",
"version": "1.0.0", "version": "0.0.1",
"description": "Backend pour l'application P'titsPas", "description": "",
"main": "dist/index.js", "author": "",
"private": true,
"license": "UNLICENSED",
"scripts": { "scripts": {
"start": "node dist/index.js", "typeorm": "typeorm-ts-node-commonjs",
"dev": "ts-node-dev --respawn --transpile-only src/index.ts", "migration:run": "npm run typeorm migration:run",
"build": "tsc", "build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest", "test": "jest",
"init-admin": "ts-node src/scripts/initAdmin.ts" "test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
}, },
"dependencies": { "dependencies": {
"@nestjs/common": "^11.1.0", "@nestjs/common": "^11.1.6",
"@prisma/client": "^6.7.0", "@nestjs/config": "^4.0.2",
"@types/jsonwebtoken": "^9.0.9", "@nestjs/core": "^11.0.1",
"bcrypt": "^5.1.1", "@nestjs/jwt": "^11.0.0",
"cors": "^2.8.5", "@nestjs/mapped-types": "^2.1.0",
"express": "^4.18.2", "@nestjs/platform-express": "^11.0.1",
"helmet": "^7.1.0", "@nestjs/swagger": "^11.2.0",
"jsonwebtoken": "^9.0.2", "@nestjs/typeorm": "^11.0.0",
"morgan": "^1.10.0" "@sentry/nestjs": "^10.10.0",
"bcrypt": "^6.0.0",
"bcryptjs": "^3.0.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"joi": "^18.0.0",
"mapped-types": "^0.0.1",
"multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.16",
"passport-jwt": "^4.0.1",
"pg": "^8.16.3",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"swagger-ui-express": "^5.0.1",
"typeorm": "^0.3.26"
}, },
"devDependencies": { "devDependencies": {
"@types/bcrypt": "^5.0.2", "@eslint/eslintrc": "^3.2.0",
"@types/cors": "^2.8.17", "@eslint/js": "^9.18.0",
"@types/express": "^4.17.21", "@nestjs/cli": "^11.0.10",
"@types/helmet": "^4.0.0", "@nestjs/schematics": "^11.0.0",
"@types/morgan": "^1.9.9", "@nestjs/testing": "^11.0.1",
"@types/node": "^20.11.19", "@types/bcrypt": "^6.0.0",
"prisma": "^6.7.0", "@types/express": "^5.0.0",
"@types/jest": "^30.0.0",
"@types/multer": "^1.4.12",
"@types/node": "^22.10.7",
"@types/nodemailer": "^6.4.16",
"@types/passport-jwt": "^4.0.1",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"globals": "^16.0.0",
"jest": "^30.0.0",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"ts-node-dev": "^2.0.0", "tsconfig-paths": "^4.2.0",
"typescript": "^5.3.3" "typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
} }
} }
+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
}'
-18
View File
@@ -1,18 +0,0 @@
import { Controller, Post, Body, UseGuards, Req } from '@nestjs/common';
import { AdminService } from './admin.service';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
@Controller('admin')
export class AdminController {
constructor(private readonly adminService: AdminService) {}
@Post('change-password')
@UseGuards(JwtAuthGuard)
async changePassword(
@Req() req,
@Body('oldPassword') oldPassword: string,
@Body('newPassword') newPassword: string,
) {
return this.adminService.changePassword(req.user.id, oldPassword, newPassword);
}
}
-18
View File
@@ -1,18 +0,0 @@
import { Module } from '@nestjs/common';
import { AdminController } from './admin.controller';
import { AdminService } from './admin.service';
import { PrismaModule } from '../prisma/prisma.module';
import { JwtModule } from '@nestjs/jwt';
@Module({
imports: [
PrismaModule,
JwtModule.register({
secret: process.env.JWT_SECRET,
signOptions: { expiresIn: '1d' },
}),
],
controllers: [AdminController],
providers: [AdminService],
})
export class AdminModule {}
-40
View File
@@ -1,40 +0,0 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { PrismaService } from '../prisma/prisma.service';
import * as bcrypt from 'bcrypt';
@Injectable()
export class AdminService {
constructor(
private prisma: PrismaService,
private jwtService: JwtService,
) {}
async changePassword(adminId: string, oldPassword: string, newPassword: string) {
// Récupérer l'administrateur
const admin = await this.prisma.admin.findUnique({
where: { id: adminId },
});
if (!admin) {
throw new UnauthorizedException('Administrateur non trouvé');
}
// Vérifier l'ancien mot de passe
const isPasswordValid = await bcrypt.compare(oldPassword, admin.password);
if (!isPasswordValid) {
throw new UnauthorizedException('Ancien mot de passe incorrect');
}
// Hasher le nouveau mot de passe
const hashedPassword = await bcrypt.hash(newPassword, 10);
// Mettre à jour le mot de passe
await this.prisma.admin.update({
where: { id: adminId },
data: { password: hashedPassword },
});
return { message: 'Mot de passe modifié avec succès' };
}
}
+22
View File
@@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});
+17
View File
@@ -0,0 +1,17 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getOverView() {
return this.appService.getOverView();
}
@Get('hello')
getHello(): string {
return this.appService.getHello();
}
}
+60 -6
View File
@@ -1,17 +1,71 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule, ConfigService } from '@nestjs/config';
import { PrismaModule } from './prisma/prisma.module'; import { AppController } from './app.controller';
import { AuthModule } from './auth/auth.module'; import { AppService } from './app.service';
import { AdminModule } from './admin/admin.module'; import appConfig from './config/app.config';
import databaseConfig from './config/database.config';
import jwtConfig from './config/jwt.config';
import { configValidationSchema } from './config/validation.schema';
import { UserModule } from './routes/user/user.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { APP_FILTER } from '@nestjs/core';
import { ParentsModule } from './routes/parents/parents.module';
import { AuthModule } from './routes/auth/auth.module';
import { SentryGlobalFilter } from '@sentry/nestjs/setup';
import { AllExceptionsFilter } from './common/filters/all_exceptions.filters';
import { EnfantsModule } from './routes/enfants/enfants.module';
import { AppConfigModule } from './modules/config/config.module';
import { DocumentsLegauxModule } from './modules/documents-legaux';
@Module({ @Module({
imports: [ imports: [
ConfigModule.forRoot({ ConfigModule.forRoot({
// Gestion dynamique des fichiers .env
envFilePath: [`.env.${process.env.NODE_ENV || 'development'}`, '.env'],
// envFilePath: '.env',
// Chargement de configurations typées
load: [appConfig, databaseConfig, jwtConfig],
isGlobal: true, isGlobal: true,
validationSchema: configValidationSchema,
}), }),
PrismaModule, TypeOrmModule.forRootAsync({
imports: [ConfigModule,
],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
type: 'postgres',
host: config.get('database.host'),
port: config.get<number>('database.port'),
username: config.get('database.username'),
password: config.get('database.password'),
database: config.get('database.database'),
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: false,
migrations: [__dirname + '/migrations/**/*{.ts,.js}'],
logging: true,
}),
}),
UserModule,
ParentsModule,
EnfantsModule,
AuthModule, AuthModule,
AdminModule, AppConfigModule,
DocumentsLegauxModule,
],
controllers: [AppController],
providers: [
AppService,
{
provide: APP_FILTER,
useClass: SentryGlobalFilter
},
{
provide: APP_FILTER,
useClass: AllExceptionsFilter,
}
], ],
}) })
export class AppModule { } export class AppModule { }
+62
View File
@@ -0,0 +1,62 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello Test!!!';
}
getOverView() {
return {
name: "P'titsPas API",
version: "1.0",
description: "Documentation rapide des endpoints disponibles",
authentication: "JWT Bearer Token requis",
endpoints: [
{
method: "GET",
path: "/parents",
description: "Liste tous les parents",
roles: ["SUPER_ADMIN", "GESTIONNAIRE"]
},
{
method: "GET",
path: "/parents/:id",
description: "Récupère un parent par ID utilisateur",
roles: ["SUPER_ADMIN", "GESTIONNAIRE"],
params: ["id (UUID)"]
},
{
method: "POST",
path: "/parents",
description: "Crée un parent",
roles: ["SUPER_ADMIN", "GESTIONNAIRE"],
body: {
user_id: "UUID",
co_parent_id: "UUID (optionnel)"
}
},
{
method: "PATCH",
path: "/parents/:id",
description: "Met à jour un parent",
roles: ["SUPER_ADMIN", "GESTIONNAIRE"],
params: ["id (UUID)"],
body: {
user_id: "UUID (optionnel)",
co_parent_id: "UUID (optionnel)"
}
},
{
method: "DELETE",
path: "/parents/:id",
description: "Supprime un parent",
roles: ["SUPER_ADMIN"],
params: ["id (UUID)"]
}
],
docs: "/api/docs"
};
}
}
@@ -0,0 +1,4 @@
import { SetMetadata } from "@nestjs/common";
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
@@ -0,0 +1,3 @@
import { SetMetadata } from "@nestjs/common";
export const Roles = (...roles: string[]) => SetMetadata("roles", roles);
@@ -0,0 +1,7 @@
import { createParamDecorator, ExecutionContext } from "@nestjs/common";
export const User = createParamDecorator((data: string | undefined, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;
return data ? user?.[data] : user;
});
@@ -0,0 +1,11 @@
import { IsDateString, IsOptional } from "class-validator";
export class DateRangeQueryDto {
@IsOptional()
@IsDateString()
start?: string;
@IsOptional()
@IsDateString()
end?: string;
}
+6
View File
@@ -0,0 +1,6 @@
import { IsUUID } from "class-validator";
export class IdParamDto {
@IsUUID()
id: string;
}
@@ -0,0 +1,11 @@
import { IsOptional, IsPositive } from "class-validator";
export class PaginationQueryDto {
@IsOptional()
@IsPositive()
offset?: number;
@IsOptional()
@IsPositive()
limit?: number;
}
@@ -0,0 +1,8 @@
import { IsOptional, IsString, MinLength } from "class-validator";
export class SearchQueryDto {
@IsOptional()
@IsString()
@MinLength(2)
q?: string;
}
@@ -0,0 +1,27 @@
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from "@nestjs/common";
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message =
exception instanceof HttpException
? exception.getResponse()
: { message: 'Internal server error' };
response.status(status).json({
success: false,
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message,
});
}
}
+49
View File
@@ -0,0 +1,49 @@
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { JwtService } from "@nestjs/jwt";
import { Request } from 'express';
import { IS_PUBLIC_KEY } from "../decorators/public.decorator";
import { ConfigService } from "@nestjs/config";
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private readonly jwtService: JwtService,
private readonly reflector: Reflector,
private readonly configService: ConfigService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) return true;
const request = context.switchToHttp().getRequest<Request>();
if (request.path.startsWith('/api-docs')) {
return true;
}
const authHeader = request.headers['authorization'] as string | undefined;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new UnauthorizedException('Token manquant ou invalide');
}
const token = authHeader.split(' ')[1];
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: this.configService.get<string>('jwt.accessSecret'),
});
request.user = {
...payload,
id: payload.sub,
};
return true;
} catch (error) {
throw new UnauthorizedException('Token invalide ou expiré');
}
}
}
+26
View File
@@ -0,0 +1,26 @@
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { Observable } from "rxjs";
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles || requiredRoles.length === 0) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
if (!user || !user.role) {
return false;
}
return requiredRoles.includes(user.role);
}
}
@@ -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,15 @@
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common";
import { map, Observable, timestamp } from "rxjs";
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => ({
success: true,
timestamp: new Date().toISOString(),
data
})),
);
}
}
+6
View File
@@ -0,0 +1,6 @@
import { registerAs } from '@nestjs/config';
export default registerAs('', () => ({
port: process.env.PORT,
env: process.env.NODE_ENV,
}));
+9
View File
@@ -0,0 +1,9 @@
import { registerAs } from '@nestjs/config';
export default registerAs('database', () => ({
host: process.env.POSTGRES_HOST,
port: process.env.POSTGRES_PORT,
username: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD,
database: process.env.POSTGRES_DB,
}));
+8
View File
@@ -0,0 +1,8 @@
import { registerAs } from '@nestjs/config';
export default registerAs('jwt', () => ({
accessSecret: process.env.JWT_ACCESS_SECRET,
accessExpiresIn: process.env.JWT_ACCESS_EXPIRES,
refreshSecret: process.env.JWT_REFRESH_SECRET,
refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES,
}));
+21
View File
@@ -0,0 +1,21 @@
import * as Joi from 'joi';
export const configValidationSchema = Joi.object({
NODE_ENV: Joi.string()
.valid('development', 'production', 'test')
.default('development'),
PORT: Joi.number().optional(),
// Base de données
POSTGRES_HOST: Joi.string().required(),
POSTGRES_PORT: Joi.number().required(),
POSTGRES_USER: Joi.string().required(),
POSTGRES_PASSWORD: Joi.string().required(),
POSTGRES_DB: Joi.string().required(),
// JWT
JWT_ACCESS_SECRET: Joi.string().required(),
JWT_ACCESS_EXPIRES: Joi.string().required(),
JWT_REFRESH_SECRET: Joi.string().required(),
JWT_REFRESH_EXPIRES: Joi.string().required(),
});
@@ -1,72 +0,0 @@
import { Request, Response } from 'express';
import { ThemeService, ThemeData } from '../services/theme.service';
export class ThemeController {
// Créer un nouveau thème
static async createTheme(req: Request, res: Response) {
try {
const themeData: ThemeData = req.body;
const theme = await ThemeService.createTheme(themeData);
res.status(201).json(theme);
} catch (error) {
res.status(500).json({ error: 'Erreur lors de la création du thème' });
}
}
// Récupérer tous les thèmes
static async getAllThemes(req: Request, res: Response) {
try {
const themes = await ThemeService.getAllThemes();
res.json(themes);
} catch (error) {
res.status(500).json({ error: 'Erreur lors de la récupération des thèmes' });
}
}
// Récupérer le thème actif
static async getActiveTheme(req: Request, res: Response) {
try {
const theme = await ThemeService.getActiveTheme();
if (!theme) {
return res.status(404).json({ error: 'Aucun thème actif trouvé' });
}
res.json(theme);
} catch (error) {
res.status(500).json({ error: 'Erreur lors de la récupération du thème actif' });
}
}
// Activer un thème
static async activateTheme(req: Request, res: Response) {
try {
const { themeId } = req.params;
const theme = await ThemeService.activateTheme(themeId);
res.json(theme);
} catch (error) {
res.status(500).json({ error: 'Erreur lors de l\'activation du thème' });
}
}
// Mettre à jour un thème
static async updateTheme(req: Request, res: Response) {
try {
const { themeId } = req.params;
const themeData: Partial<ThemeData> = req.body;
const theme = await ThemeService.updateTheme(themeId, themeData);
res.json(theme);
} catch (error) {
res.status(500).json({ error: 'Erreur lors de la mise à jour du thème' });
}
}
// Supprimer un thème
static async deleteTheme(req: Request, res: Response) {
try {
const { themeId } = req.params;
await ThemeService.deleteTheme(themeId);
res.status(204).send();
} catch (error) {
res.status(500).json({ error: 'Erreur lors de la suppression du thème' });
}
}
}
@@ -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,51 @@
import { Entity, PrimaryColumn, Column, OneToOne, JoinColumn } from 'typeorm';
import { Users } from './users.entity';
@Entity('assistantes_maternelles')
export class AssistanteMaternelle {
// PK = FK vers utilisateurs.id
@PrimaryColumn('uuid', { name: 'id_utilisateur' })
user_id: string;
@OneToOne(() => Users, (user) => user.assistanteMaternelle, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'id_utilisateur', referencedColumnName: 'id' })
user: Users;
@Column({ name: 'numero_agrement', length: 50, nullable: true })
approval_number?: string;
@Column({ name: 'nir_chiffre', length: 15, nullable: true })
nir?: string;
@Column({ name: 'nb_max_enfants', type: 'int', nullable: true })
max_children?: number;
@Column({ name: 'biographie', type: 'text', nullable: true })
biography?: string;
@Column({
name: 'disponible',
type: 'boolean',
default: true,
nullable: true,
})
available?: boolean;
@Column({ name: 'ville_residence', length: 100, nullable: true })
residence_city?: string;
@Column( { name: 'date_agrement', type: 'date', nullable: true })
agreement_date?: Date;
@Column( { name: 'annee_experience', type: 'smallint', nullable: true })
years_experience?: number;
@Column( { name: 'specialite', length: 100, nullable: true })
specialty?: string;
@Column( { name: 'place_disponible', type: 'integer', nullable: true })
places_available?: number;
}
@@ -0,0 +1,42 @@
import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
import { Contrat } from "./contrats.entity";
import { Users } from "./users.entity";
export enum StatutAvenantType {
PROPOSE = 'propose',
ACCEPTE = 'accepte',
REFUSE = 'refuse',
}
@Entity('avenants_contrats')
export class AvenantContrat {
// Define your columns and relationships here
@PrimaryGeneratedColumn('uuid')
id: string;
@ManyToOne(() => Contrat, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'id_contrat' })
contrat: Contrat;
@Column({ type: 'jsonb', nullable: true, name: 'modifications' })
modifications?: any;
@ManyToOne(() => Users, { nullable: true })
@JoinColumn({ name: 'initie_par', referencedColumnName: 'id' })
initiator?: Users;
@Column({
type: 'enum',
enum: StatutAvenantType,
enumName: 'statut_avenant_type',
default: StatutAvenantType.PROPOSE,
name: 'statut'
})
statut: StatutAvenantType;
@CreateDateColumn({ name: 'cree_le', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'modifie_le', type: 'timestamptz' })
updatedAt: Date;
}
+74
View File
@@ -0,0 +1,74 @@
import {
Entity, PrimaryGeneratedColumn, Column,
OneToMany, ManyToMany, CreateDateColumn, JoinTable
} from 'typeorm';
import { Parents } from './parents.entity';
import { ParentsChildren } from './parents_children.entity';
import { Dossier } from './dossiers.entity';
export enum StatutEnfantType {
A_NAITRE = 'a_naitre',
ACTIF = 'actif',
SCOLARISE = 'scolarise',
}
export enum GenreType {
H = 'H',
F = 'F',
AUTRE = 'Autre',
}
@Entity('enfants')
export class Children {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({
type: 'enum',
enum: StatutEnfantType,
enumName: 'statut_enfant_type',
name: 'statut'
})
status: StatutEnfantType;
@Column({ name: 'prenom', length: 100, nullable: true })
first_name?: string;
@Column({ name: 'nom', length: 100, nullable: true })
last_name?: string;
@Column({
type: 'enum',
enum: GenreType,
enumName: 'genre_type',
nullable: true,
name: 'genre'
})
gender?: GenreType;
@Column({ type: 'date', nullable: true, name: 'date_naissance' })
birth_date?: Date;
@Column({ type: 'date', nullable: true, name: 'date_prevue_naissance' })
due_date?: Date;
@Column({ nullable: true, name: 'photo_url', type: 'text' })
photo_url?: string;
@Column({ default: false, name: 'consentement_photo', type: 'boolean' })
consent_photo: boolean;
@Column({ type: 'timestamptz', nullable: true, name: 'date_consentement_photo' })
consent_photo_at?: Date;
@Column({ default: false, name: 'est_multiple', type: 'boolean' })
is_multiple: boolean;
// Lien via table de jointure enfants_parents
@OneToMany(() => ParentsChildren, pc => pc.child)
parentLinks: ParentsChildren[];
// Relation avec Dossier
@OneToMany(() => Dossier, d => d.child)
dossiers: Dossier[];
}
@@ -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;
}
+57
View File
@@ -0,0 +1,57 @@
import { Column, CreateDateColumn, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
import { Dossier } from "./dossiers.entity";
export enum StatutContratType {
BROUILLON = 'brouillon',
EN_ATTENTE_SIGNATURE = 'en_attente_signature',
VALIDE = 'valide',
RESILIE = 'resilie',
}
@Entity('contrats')
export class Contrat {
// Define your columns and relationships here
@PrimaryGeneratedColumn('uuid')
id: string;
@OneToOne(() => Dossier, {onDelete: 'CASCADE'} )
@JoinColumn({ name: 'id_dossier'})
dossier: Dossier;
@Column({type: 'jsonb', nullable: true, name: 'planning'})
planning?: any;
@Column({type: 'numeric', precision: 6, scale: 2, nullable: true, name: 'tarif_horaire'})
hourly_rate?: string;
@Column({type: 'numeric', precision: 6, scale: 2, nullable: true, name: 'indemnites_repas'})
meal_indemnity?: string;
@Column( { name: 'date_debut', type: 'date', nullable: true })
start_date?: Date;
@Column({
type: 'enum',
enum: StatutContratType,
enumName: 'statut_contrat_type',
default: StatutContratType.BROUILLON,
name: 'statut'
})
statut: StatutContratType;
@Column({type: 'boolean', default: false, name: 'signe_parent'})
signed_by_parent: boolean;
@Column({type: 'boolean', default: false, name: 'signe_am'})
signed_by_am: boolean;
@Column({type: 'timestamptz', nullable: true, name: 'finalise_le'})
finalized_at?: Date;
@CreateDateColumn({ name: 'cree_le', type: 'timestamptz' })
created_at: Date;
@UpdateDateColumn({ name: 'modifie_le', type: 'timestamptz' })
updated_at: Date;
}
@@ -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;
}
+61
View File
@@ -0,0 +1,61 @@
import {
Entity, PrimaryGeneratedColumn, Column,
ManyToOne, OneToMany, CreateDateColumn, UpdateDateColumn, JoinColumn
} from 'typeorm';
import { Parents } from './parents.entity';
import { Children } from './children.entity';
import { Message } from './messages.entity';
export enum StatutDossierType {
ENVOYE = 'envoye',
ACCEPTE = 'accepte',
REFUSE = 'refuse',
CLOTURE = 'cloture',
}
@Entity('dossiers')
export class Dossier {
@PrimaryGeneratedColumn('uuid')
id: string;
@ManyToOne(() => Parents, p => p.dossiers, { onDelete: 'CASCADE', nullable: false })
@JoinColumn({ name: 'id_parent', referencedColumnName: 'user_id' })
parent: Parents;
@ManyToOne(() => Children, c => c.dossiers, { onDelete: 'CASCADE', nullable: false })
@JoinColumn({ name: 'id_enfant', referencedColumnName: 'id' })
child: Children;
@Column({ type: 'text', nullable: true, name: 'presentation' })
presentation?: string;
@Column({ type: 'varchar', length: 50, nullable: true, name: 'type_contrat' })
type_contrat?: string;
@Column({ type: 'boolean', default: false, name: 'repas' })
meals: boolean;
@Column({ type: 'numeric', precision: 10, scale: 2, nullable: true, name: 'budget' })
budget?: number;
@Column({ type: 'jsonb', nullable: true, name: 'planning_souhaite' })
desired_schedule?: any;
@Column({
type: 'enum',
enum: StatutDossierType,
enumName: 'statut_dossier_type',
default: StatutDossierType.ENVOYE,
name: 'statut'
})
status: StatutDossierType;
@CreateDateColumn({ name: 'cree_le', type: 'timestamptz' })
created_at: Date;
@UpdateDateColumn({ name: 'modifie_le', type: 'timestamptz' })
updated_at: Date;
@OneToMany(() => Message, m => m.dossier)
messages: Message[];
}
+79
View File
@@ -0,0 +1,79 @@
import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
import { Children } from "./children.entity";
import { Users } from "./users.entity";
import { Parents } from "./parents.entity";
export enum TypeEvenementType {
ABSENCE_ENFANT = 'absence_enfant',
CONGE_AM = 'conge_am',
CONGE_PARENT = 'conge_parent',
ARRET_MALADIE_AM = 'arret_maladie_am',
EVENEMENT_RPE = 'evenement_rpe',
}
export enum StatutEvenementType {
PROPOSE = 'propose',
VALIDE = 'valide',
REFUSE = 'refuse',
}
@Entity('evenements')
export class Evenement {
// Define your columns and relationships here
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({
type: 'enum',
enum: TypeEvenementType,
enumName: 'type_evenement_type',
name: 'type'
})
type: TypeEvenementType;
@ManyToOne(() => Children, { onDelete: 'CASCADE', nullable: true })
@JoinColumn({ name: 'id_enfant', referencedColumnName: 'id' })
child?: Children;
@ManyToOne(() => Users, { nullable: true })
@JoinColumn({ name: 'id_am', referencedColumnName: 'id' })
assistanteMaternelle?: Users;
@ManyToOne(() => Parents, { nullable: true })
@JoinColumn({ name: 'id_parent', referencedColumnName: 'user_id' })
parent?: Parents;
@ManyToOne(() => Users, { nullable: true })
@JoinColumn({ name: 'cree_par', referencedColumnName: 'id' })
created_by?: Users;
@Column({ type: 'timestamptz', nullable: true, name: 'date_debut' })
start_date?: Date;
@Column({ type: 'timestamptz', nullable: true, name: 'date_fin' })
end_date?: Date;
@Column({ type: 'text', nullable: true, name: 'commentaires' })
comments?: string;
@Column({
type: 'enum',
enum: StatutEvenementType,
enumName: 'statut_evenement_type',
name: 'statut',
default: StatutEvenementType.PROPOSE
})
status: StatutEvenementType;
@Column({type: 'timestamptz', nullable: true, name: 'delai_grace'})
grace_deadline?: Date;
@Column({type: 'boolean', default: false, name: 'urgent'})
urgent: boolean;
@CreateDateColumn({ name: 'cree_le', type: 'timestamptz' })
created_at: Date;
@UpdateDateColumn({ name: 'modifie_le', type: 'timestamptz' })
updated_at: Date;
}
+29
View File
@@ -0,0 +1,29 @@
import {
Entity, PrimaryGeneratedColumn, Column,
ManyToOne, JoinColumn, CreateDateColumn
} from 'typeorm';
import { Dossier } from './dossiers.entity';
import { Users } from './users.entity';
@Entity('messages')
export class Message {
@PrimaryGeneratedColumn('uuid')
id: string;
@ManyToOne(() => Dossier, d => d.messages, { onDelete: 'CASCADE', nullable: false })
@JoinColumn({ name: 'id_dossier' })
dossier: Dossier;
@ManyToOne(() => Users, u => u.messages, { onDelete: 'CASCADE', nullable: false })
@JoinColumn({ name: 'id_expediteur' })
sender: Users;
@Column({ type: 'text', name: 'contenu' })
content: string;
@Column({ type: 'boolean', name: 're_redige_par_ia', default: false })
reRedigeParIA: boolean;
@CreateDateColumn({ name: 'cree_le', type: 'timestamptz' })
created_at: Date;
}
@@ -0,0 +1,23 @@
import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
import { Users } from "./users.entity";
@Entity('notifications')
export class Notification {
// Define your columns and relationships here
@PrimaryGeneratedColumn('uuid')
id: string;
@ManyToOne(() => Users, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'id_utilisateur', referencedColumnName: 'id' })
user: Users;
@Column({ type: 'text', name: 'contenu' })
content: string;
@Column({type: 'boolean', name: 'lu', default: false})
read: boolean;
@CreateDateColumn({ name: 'cree_le', type: 'timestamptz' })
created_at: Date;
}
+31
View File
@@ -0,0 +1,31 @@
import {
Entity, PrimaryColumn, OneToOne, JoinColumn,
ManyToOne, OneToMany
} from 'typeorm';
import { Users } from './users.entity';
import { ParentsChildren } from './parents_children.entity';
import { Dossier } from './dossiers.entity';
@Entity('parents', { schema: 'public' })
export class Parents {
// PK = FK vers utilisateurs.id
@PrimaryColumn('uuid', { name: 'id_utilisateur' })
user_id: string;
@OneToOne(() => Users, user => user.parent, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'id_utilisateur', referencedColumnName: 'id' })
user: Users;
// Co-parent (nullable) → FK vers utilisateurs.id
@ManyToOne(() => Users, { nullable: true })
@JoinColumn({ name: 'id_co_parent', referencedColumnName: 'id' })
co_parent?: Users;
// Lien vers enfants via la table enfants_parents
@OneToMany(() => ParentsChildren, pc => pc.parent)
parentChildren: ParentsChildren[];
// Lien vers les dossiers de ce parent
@OneToMany(() => Dossier, d => d.parent)
dossiers: Dossier[];
}
@@ -0,0 +1,22 @@
import {
Entity, ManyToOne, JoinColumn, PrimaryColumn
} from 'typeorm';
import { Parents } from './parents.entity';
import { Children } from './children.entity';
@Entity('enfants_parents', { schema: 'public' })
export class ParentsChildren {
@PrimaryColumn('uuid', { name: 'id_parent' })
parentId: string;
@PrimaryColumn('uuid', { name: 'id_enfant' })
enfantId: string;
@ManyToOne(() => Parents, p => p.parentChildren, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'id_parent', referencedColumnName: 'user_id' })
parent: Parents;
@ManyToOne(() => Children, c => c.parentLinks, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'id_enfant', referencedColumnName: 'id' })
child: Children;
}
@@ -0,0 +1,19 @@
import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
import { Users } from "./users.entity";
@Entity('signalements_bugs')
export class SignalementBug {
@PrimaryGeneratedColumn('uuid')
id: string;
@ManyToOne(() => Users, {nullable: true})
@JoinColumn({ name: 'id_utilisateur', referencedColumnName: 'id' })
user?: Users;
@Column({ type: 'text', name: 'description'})
description: string;
@CreateDateColumn({ name: 'cree_le', type: 'timestamptz' })
created_at: Date;
}
+21
View File
@@ -0,0 +1,21 @@
import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
import { Users } from "./users.entity";
@Entity('uploads')
export class Upload {
@PrimaryGeneratedColumn('uuid')
id: string;
@ManyToOne(() => Users, { onDelete: 'SET NULL', nullable: true })
@JoinColumn({ name: 'id_utilisateur', referencedColumnName: 'id' })
user?: Users;
@Column({ type: 'text', name: 'fichier_url' })
file_url: string;
@Column({type: 'varchar', length: 50, nullable: true, name: 'type'})
type?: string;
@CreateDateColumn({ name: 'cree_le', type: 'timestamptz' })
created_at: Date;
}
+150
View File
@@ -0,0 +1,150 @@
import {
Entity, PrimaryGeneratedColumn, Column,
CreateDateColumn, UpdateDateColumn,
OneToOne, OneToMany
} from 'typeorm';
import { AssistanteMaternelle } from './assistantes_maternelles.entity';
import { Parents } from './parents.entity';
import { Message } from './messages.entity';
// Enums alignés avec la BDD PostgreSQL
export enum RoleType {
PARENT = 'parent',
GESTIONNAIRE = 'gestionnaire',
SUPER_ADMIN = 'super_admin',
ASSISTANTE_MATERNELLE = 'assistante_maternelle',
ADMINISTRATEUR = 'administrateur',
}
//Enum pour definir le genre
export enum GenreType {
H = 'H',
F = 'F',
AUTRE = 'Autre',
}
//Enum pour definir le statut utilisateur
export enum StatutUtilisateurType {
EN_ATTENTE = 'en_attente',
ACTIF = 'actif',
SUSPENDU = 'suspendu',
}
export enum SituationFamilialeType {
CELIBATAIRE = 'celibataire',
MARIE = 'marie',
DIVORCE = 'divorce',
VEUF = 'veuf',
PACSE = 'pacse',
SEPARE = 'separe',
PARENT_ISOLE = 'parent_isole',
CONCUBINAGE = 'concubinage',
}
//Declaration de l'entite utilisateur
@Entity('utilisateurs', { schema: 'public' })
export class Users {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true, name: 'email' })
email: string;
@Column({ name: 'password', nullable: true })
password?: string;
@Column({ name: 'prenom', nullable: true })
prenom?: string;
@Column({ name: 'nom', nullable: true })
nom?: string;
@Column({
type: 'enum',
enum: GenreType,
enumName: 'genre_type', // correspond à l'enum de la db psql
nullable: true,
name: 'genre'
})
genre?: GenreType;
@Column({
type: 'enum',
enum: RoleType,
enumName: 'role_type', // correspond à l'enum de la db psql
name: 'role'
})
role: RoleType;
@Column({
type: 'enum',
enum: StatutUtilisateurType,
enumName: 'statut_utilisateur_type', // correspond à l'enum de la db psql
default: StatutUtilisateurType.EN_ATTENTE,
name: 'statut'
})
statut: StatutUtilisateurType;
@Column({ type: 'enum',
enum: SituationFamilialeType,
enumName: 'situation_familiale_type',
nullable: true,
name: 'situation_familiale'
})
situation_familiale?: SituationFamilialeType;
@Column({ nullable: true, name: 'telephone' })
telephone?: string;
@Column({ nullable: true, name: 'adresse' })
adresse?: string;
@Column({ nullable: true, name: 'photo_url' })
photo_url?: string;
@Column({ default: false, name: 'consentement_photo' })
consentement_photo: boolean;
@Column({ type: 'timestamptz', nullable: true, name: 'date_consentement_photo' })
date_consentement_photo?: Date;
@Column({ default: false, name: 'changement_mdp_obligatoire' })
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' })
ville?: string;
@Column({ nullable: true, name: 'code_postal' })
code_postal?: string;
@Column({ nullable: true, name: 'profession' })
profession?: string;
@Column({ name: 'date_naissance', type: 'date', nullable: true })
date_naissance?: Date;
@CreateDateColumn({ name: 'cree_le', type: 'timestamptz' })
cree_le: Date;
@UpdateDateColumn({ name: 'modifie_le', type: 'timestamptz' })
modifie_le: Date;
// Relations
@OneToOne(() => AssistanteMaternelle, a => a.user)
assistanteMaternelle?: AssistanteMaternelle;
@OneToOne(() => Parents, p => p.user)
parent?: Parents;
@OneToMany(() => Message, m => m.sender)
messages?: Message[];
@OneToMany(() => Parents, parent => parent.co_parent)
co_parent_in?: Parents[];
}
@@ -0,0 +1,44 @@
import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
import { Users } from "./users.entity";
export enum StatutValidationType {
EN_ATTENTE = 'en_attente',
VALIDE = 'valide',
REFUSE = 'refuse',
}
@Entity('validations')
export class Validation {
@PrimaryGeneratedColumn('uuid')
id: string;
@ManyToOne(() => Users, { nullable: true })
@JoinColumn({ name: 'id_utilisateur', referencedColumnName: 'id' })
user?: Users;
@Column({ type: 'varchar', length: 50, name: 'type', nullable: true })
type: string;
@Column({
type: 'enum',
enum: StatutValidationType,
enumName: 'statut_validation_type',
name: 'statut',
default: StatutValidationType.EN_ATTENTE
})
status: StatutValidationType;
@ManyToOne(() => Users, { nullable: true })
@JoinColumn({ name: 'valide_par', referencedColumnName: 'id' })
validated_by?: Users;
@Column( { name: 'commentaire', type: 'text', nullable: true })
comment?: string;
@CreateDateColumn({ name: 'cree_le', type: 'timestamptz' })
created_at: Date;
@UpdateDateColumn({ name: 'modifie_le', type: 'timestamptz' })
updated_at: Date;
}
-28
View File
@@ -1,28 +0,0 @@
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import morgan from 'morgan';
import themeRoutes from './routes/theme.routes';
const app = express();
const port = process.env.PORT || 3000;
// Middleware
app.use(cors());
app.use(helmet());
app.use(morgan('dev'));
app.use(express.json());
// Routes
app.use('/api/themes', themeRoutes);
// Gestion des erreurs
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
console.error(err.stack);
res.status(500).json({ error: 'Une erreur est survenue' });
});
// Démarrage du serveur
app.listen(port, () => {
console.log(`Serveur démarré sur le port ${port}`);
});
+67
View File
@@ -0,0 +1,67 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ConfigService } from '@nestjs/config';
import { SwaggerModule } from '@nestjs/swagger/dist/swagger-module';
import { DocumentBuilder } from '@nestjs/swagger';
import { ValidationPipe } from '@nestjs/common';
import { LogRequestInterceptor } from './common/interceptors/log-request.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule,
{ logger: ['error', 'warn', 'log', 'debug', 'verbose'] });
// 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(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
})
);
const configService = app.get(ConfigService);
const port = configService.get<number>('app.port', 3000);
app.setGlobalPrefix('api/v1');
const config = new DocumentBuilder()
.setTitle("P'titsPas API")
.setDescription("API pour l'application P'titsPas")
.setVersion('1.0.0')
.addBearerAuth(
{
type: 'http',
scheme: 'Bearer',
bearerFormat: 'JWT',
name: 'Authorization',
description: 'Enter JWT token',
in: 'header',
},
'access-token',
)
//.addServer('/api/v1')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api/v1/swagger', app, document);
await app.listen(port);
console.log(`✅ P'titsPas API is running on: ${await app.getUrl()}`);
}
bootstrap().catch((err) => {
console.error('❌ Error starting the application:', err);
process.exit(1);
});
@@ -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)',
};
}
}

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