Documentation Backend Teadle
📋 Table des matières
- Vue d'ensemble
- Architecture
- Technologies
- Structure du projet
- API Documentation
- Sécurité
- Base de données
- Déploiement
🎯 Vue d'ensemble
Teadle est une plateforme de mise en relation entre formateurs et organisations. Le backend est développé en Symfony 7.3 avec une architecture Domain-Driven Design (DDD) et utilise API Platform pour exposer les APIs REST.
Fonctionnalités principales
- 🔐 Authentification : JWT, OAuth (LinkedIn, Google, Microsoft), codes d'authentification
- 👥 Gestion des utilisateurs : Formateurs et Organisations
- 📋 Onboarding : Processus d'inscription et de configuration
- 💼 Opportunités : Gestion des missions et candidatures
- 📅 Événements : Planning et interventions
- 🔗 Partage de planning : Liens publics avec description facultative (255 car., création et édition)
- 💰 Facturation : Gestion des factures
- 📧 Notifications : Emails via Brevo, centralisés par
EmailService(méthodes métier par type d'email ; templates Brevo mappés dansemail_templates.yaml). Inclut notamment l'email de synthèse organisation envoyé au formateur après validation de ses tarifs (sendTrainerSummaryOrganizationInfoEmail, template #20). - 📊 Bilan mensuel automatique : Email envoyé le 1er du mois à 08h via Symfony Scheduler aux formateurs ayant opté pour l'optin
MONTHLY_SUMMARY. Calcul viaMonthlyReportCalculator, envoi viasendMonthlyReportEmail(template Brevo #23). Protection anti-doublon parstateful($cache)+processOnlyLastMissedRun(true). - 📅 Récap semaine à venir : Email chaque vendredi 17:00 (Europe/Paris) via Symfony Scheduler (
SendUpcomingWeekReports→ transport async). Cible : formateurs onboarding terminé avec au moins une intervention (affectation active) dont le début tombe dans la semaine calendaire suivante (lundi–dimanche contenanttoday + 7 jours, fenêtre calculée en Europe/Paris). Paramètres Brevo viasendUpcomingWeekReportEmail(templaterecap_week_upcoming/ #24). Commande de debug :app:debug:send-upcoming-week-reports. - 🔄 Resync calendrier (lien iCal ou OAuth) : Chaque nuit à 01:00 (Europe/Paris), le scheduler envoie
ScheduledTrainerCalendarRemoteResyncsur le transport async ;DispatchScheduledTrainerCalendarRemoteResyncCommandenchaîne les contrôles puisSyncProcessTrainerCalendarIcalCommand::processpour chaque configurateur ayant une URL iCal ou des jetons OAuth persistés (équivalent au POST/trainer/calendar/configurator/{uuid}/sync). Le traitement async passe parProcessTrainerCalendarConfiguratorSync:TrainerCalendarConfiguratorSynchronizerappelleparseCalendar(OAuth) ouIcsParserInterfacepuis boucle sur les stratégies (supportsIcal/supportsIcalProdId) etparseIcalVo, puisPersistTrainerCalendarConnectorEventsCommand(Application/Command/Trainer/Calendar/). Pour la déduction d’année scolaire côté parseurs iCal, la date de l’événement (DTSTART) est désormais prioritaire ; l’analyse textuelle reste un fallback (champssummary/description/locationselon le parseur), avec normalisation des tirets Unicode avant lecture des plages d’années. Côté OAuth Outlook (OutlookOAuthCalendarEventsParser), l’année scolaire est maintenant toujours renseignée dans cet ordre : date de l’événement, fallback texte (subject/bodyPreview), puis année courante ; garde-fou explicite pour éviter toute valeur0-0. - 📅 Agenda personnel distant (formateur) : sur
TrainerAvailabilityConfiguration— OAuth (jetons +oauth_calendar_id+personal_calendar_provider_type), URL iCal (personal_calendar_ical_url, migration Version20260420140000), ou fichier .ics (personal_calendar_provider_type = ical_file, clé S3…/personal-availability-{id}.icsviaTrainerPersonalCalendarIcalFileStorageKeyResolver, aligné sur le configurateur orga). API agenda personnel distant :TrainerPersonalCalendarOAuthResource— OAuth (/trainer/calendar/personal/oauth/...) et import iCal sans compte :POST /trainer/calendar/personal/ical/add(corps JSON identique au configurateur orga :icalLink,providerTypeoptionnel) etPOST /trainer/calendar/personal/ical/upload(multiparticsfile,providerTypeoptionnel), traités parCreateProcessTrainerPersonalCalendarIcalLinkCommand/CreateProcessTrainerPersonalCalendarIcalFileCommandpuis la même synchro asyncProcessTrainerPersonalCalendarSync→TrainerPersonalCalendarSynchronizer→PersistTrainerPersonalCalendarEventsCommand. Resync planifiée :TrainerAvailabilityConfigurationRepository::findAllWithPersonalCalendarRemoteSyncSource(). GET/trainer/availability/configurationexposepersonalCalendarRemoteSyncConfiguredetpersonalCalendarIcalUrl. DéconnexionDELETE …/personal/oauthefface OAuth et URL iCal. Le flux OAuth SPA réutiliseTrainerCalendarOAuthCallbackView+sessionStorage.teadle_calendar_oauth_personal.
🏗️ Architecture
Architecture Hexagonale (DDD)
Le projet suit une architecture hexagonale avec séparation claire des couches :
┌─────────────────────────────────────────────────────────────┐
│ Infrastructure Layer │
├─────────────────────────────────────────────────────────────┤
│ Application Layer │
├─────────────────────────────────────────────────────────────┤
│ Domain Layer │
└─────────────────────────────────────────────────────────────┘
Couches de l'architecture
1. Domain Layer (src/Domain/)
- Entities : Modèles métier (User, Trainer, Organization, etc.)
- Value Objects : Objets de valeur (PhoneNumber, etc.)
- Repositories : Interfaces des repositories
- Services : Services métier
- Events : Événements du domaine
- Exceptions : Exceptions métier
2. Application Layer (src/Application/)
- Commands : Commandes CQRS
- Handlers : Gestionnaires de commandes
- Services : Services d'application (dont
Service/Email/EmailServiceetEmailTemplateRegistrypour l'envoi d'emails centralisé)
3. Infrastructure Layer (src/Infrastructure/)
- API : Resources et Processors API Platform
- ORM : Implémentations Doctrine
- Security : Authentification et autorisation
- Notification : Services d'envoi d'emails
- Symfony : Intégrations Symfony (dont EventSubscribers écoutant les Domain Events pour déclencher des effets de bord — emails, notifications)
🛠️ Technologies
Framework et composants principaux
- Symfony 7.3 : Framework PHP (inclut Symfony Scheduler pour les tâches planifiées)
- API Platform 3.2 : API REST automatique
- Doctrine ORM 3.3 : Mapping objet-relationnel
- Lexik JWT Bundle : Authentification JWT
- Gesdinet JWT Refresh Token : Refresh tokens
- OAuth2 Client Bundle : Authentification OAuth
Services externes
- Brevo : Service d'envoi d'emails
- Stockage fichiers (S3 / Clever Cloud Cellar) : bucket formateurs via Flysystem (
config/packages/flysystem.yaml). Les ressources exposées par URL directe (logoUrlfacturation, avatar, etc.) passent parFilesystemWriter::writePublic()(ACLpublic-readcôté S3) ; sans cela, l’URL renvoie 403 Access Denied. Les fichiers déjà uploadés en privé : ré-uploader pour corriger. - LinkedIn OAuth : Authentification LinkedIn
- Google OAuth : Authentification Google
- Microsoft OAuth : Authentification Microsoft
Monitoring & Observabilité
- Sentry (
sentry/sentry-symfony5.x) : capture automatique des exceptions, erreurs Messenger, performance tracing (traces_sample_rate: 0.2). Handler Monologsentry(levelerror) en prod. DSN viaSENTRY_DSNdans.env.prod. - New Relic (agent PHP natif, extension
newrelic.so) : APM, slow queries Doctrine, distributed tracing. Installé dans le Dockerfile prod, configuré dynamiquement au démarrage viastart.shsiNEW_RELIC_LICENSE_KEYest défini. Région EU (collector.eu01.nr-data.net), frameworksymfony4.
Outils de développement
- PHPStan : Analyse statique
- PHP CS Fixer : Standards de code
- Alice Bundle : Fixtures de données
- Doctrine Migrations : Gestion des migrations
📁 Structure du projet
backend/
├── src/
│ ├── Domain/ # Couche domaine
│ │ ├── Entity/ # Entités métier
│ │ ├── Repository/ # Interfaces repositories
│ │ ├── Service/ # Services métier
│ │ ├── ValueObject/ # Objets de valeur
│ │ ├── Event/ # Événements
│ │ ├── Exception/ # Exceptions métier
│ │ ├── Enum/ # Énumérations (ex. Onboarding/OnboardingStep ; Optin/OptinType)
│ │ ├── Security/ # Sécurité domaine
│ │ ├── Notification/ # Notifications
│ │ └── Messenger/ # Messages (ex. SendMonthlyReports)
│ ├── Application/ # Couche application
│ │ ├── Command/ # Commandes CQRS
│ │ ├── Handler/ # Gestionnaires
│ │ └── Service/ # Services application
│ └── Infrastructure/ # Couche infrastructure
│ ├── Api/ # API Platform
│ │ ├── Resource/ # Resources API
│ │ └── State/ # Processors API
│ ├── Orm/ # Doctrine ORM
│ ├── Security/ # Sécurité infrastructure
│ ├── Notification/ # Services notification
│ ├── Symfony/ # Intégrations Symfony
│ │ └── EventSubscriber/ # Subscribers de Domain Events (emails, side effects)
│ ├── AuthCode/ # Codes d'authentification
│ ├── PdfParser/ # Parsing PDF
│ ├── IcsParser/ # Parsing ICS
│ └── PhoneNumber/ # Gestion numéros
├── config/ # Configuration
├── migrations/ # Migrations Doctrine
├── fixtures/ # Fixtures de données
└── tests/ # Tests
🔌 API Documentation
Endpoints principaux
Authentification
POST /security/login- Connexion JWTPOST /security/password-reset/request- Demande reset mot de passePUT /security/password-reset/perform- Reset mot de passePOST /security/register- InscriptionPOST /security/auth-code/send- Envoi code d'authentificationPOST /security/auth-code/verify- Vérification codePOST /security/oauth/login- Connexion OAuth
Utilisateurs
GET /me- Informations utilisateur connecté. Pour un formateur : inclutonboardingProgress(état DISC-016 : étapes, compteurs,completedAt,checklistDismissedAt,sampleData:active,expiresAt,dismissedAt,firstRealReservationReceivedAt; fenêtre J+N depuisregistered_at+teadle.sample_data_duration_days).GET /trainer/onboarding/sample-data— Si le sample data est actif : payload illustratif (mêmes DTOs que demandes + dashboard :requests,interventions,dashboard,expiresAt) ; sinon{ "active": false }(200). Formateur authentifié uniquement. Le contenu fictif est défini dansbackend/config/packages/teadle_onboarding_sample_data.yaml(paramètreteadle_onboarding_sample_data, lu viaParameterBagProviderInterface) ; GetTrainerSampleDataHandler renvoieTrainerSampleDataResultDto(VO Domain pour les demandes / interventions, DTO Application pour le bloc dashboard figé YAML) ;TrainerSampleDataResponseMapperproduitSampleDataResponsepour API Platform.- Prévu (sample organisations) : même principe que demandes / dashboard — ajouter côté backend une slice dédiée (organisations ou
assignmentsalignée sur le listing/trainer/organization/dashboardou équivalent consommé par OrganizationsView) lorsque le sample data est actif : données fictives dans le YAML + mapping dansTrainerSampleDataResultDto/SampleDataResponse(et pas de logique « sample » dans le listing frontend, comme pour les autres blocs). À traiter dans une issue API dédiée quand le périmètre métier (nombre de lignes, statuts, UUID fictifs) sera figé. POST /trainer/onboarding/sample-data/dismiss— Masque les données d’exemple (sample_data_dismissed_at), réponse =TrainerOnboardingProgressResponse(snapshotonboardingProgress). Sans corps. Formateur authentifié uniquement.POST /trainer/onboarding/dismiss— Masque la checklist « Démarrer » (checklist_dismissed_at), même réponse TrainerOnboardingProgressResponse. Idempotent. Formateur authentifié uniquement (FEAT-088).POST /trainer/onboarding/restore— Réaffiche la checklist (checklist_dismissed_at = null), même réponse. Idempotent (FEAT-088).POST /trainer/onboarding/step/{step}/complete— Marque une étapeOnboardingStepcomme complétée viaMarkTrainerOnboardingStepCompletedCommand;{step}invalide → 400. Réponse TrainerOnboardingProgressResponse. Idempotent (FEAT-088).PUT /trainer/updatePreferences- Mise à jour préférences formateurPUT /organization/updatePreferences- Mise à jour préférences organisationPUT /organization/updateInfo- Mise à jour infos organisation
Dashboard formateur (/trainer/dashboard/*)
GET /trainer/dashboard/summary- Résumé (revenu mensuel, heures, taux d’utilisation, clients actifs, taux de réponse). Les agrégations « mois courant » et le trimestre clients actifs utilisent le fuseau du formateur (user.timezone, défautEurope/Paris). La réponse inclutperiodMonth(1–12),periodYear,timezone(IANA), plus les enrichissements FEAT-074 :weeklyLoad(interventionCount,totalHours,realizedHours,dailyBreakdown),monthlySessions(total,realized,previousMonthTotal,evolution),schoolBreakdowns(top 3 écoles),monthlyRevenue.realizedAmount/realizedHours,monthlyHours.previousMonthHours/evolution, et FEAT-076 :concentrationCA(percentage,topClient,level),effectiveHourlyRate(current,previousMonth,evolution),annualEvolution(currentYearRevenue,previousYearRevenue,evolutionPercent,comparisonStartMonth,comparisonEndMonth,comparisonYear). Les mois de comparaison sont renvoyés en numérique (1..12) pour traduction côté frontend.GET /trainer/dashboard/revenue-evolution- Évolution du revenu (fenêtre calendaire selon le fuseau formateur). Période par défaut12_months(plus6_monthsetyear). Retourne toujours 200 (plus de gating onboarding).GET /trainer/dashboard/upcoming-interventions- Prochaines interventions. Paramètrescopesupporté (today,week, défautnull= comportement historique upcoming). Chaque item expose désormaisdurationHours. Retourne toujours 200 (plus de gating onboarding).GET /trainer/dashboard/recent-requests- Demandes récentes. Limite par défaut portée à 5 (surchargable vialimit) ; chaque item exposemodule,slotsCount,conflictsCount(initialisé à 0 dans FEAT-074). Retourne toujours 200 (plus de gating onboarding).GET /trainer/dashboard/revenue-by-client- Revenu par client. Paramètreperiodsupporté :quarter(défaut),year,12_months. Chaque item incluttrend(up/down/stable),nextSession(ISO ounull),healthStatus(green/orange/red),organizationStatus(ok/conflict/error/none, même règle que/trainer/organization/dashboard) etorganizationId(UUID pour deeplink facturation). Retourne toujours 200 (plus de gating onboarding).GET /trainer/dashboard/badges— Compteurs pour le polling (sidebar / barre d’état) :notifications(notifications in-app non lues, non expirées — même filtre que/notification/list),requests{pending,urgent,newUnseen} (newUnseen=firstViewAtnull sur les lots NEW/PENDING non expirés), etunreadInbox{total,conversations,notifications,critical} : agrégat inbox unifiée (total= conversations + notifications ;critical= notifications non lues de niveau URGENT). Le front limite déjà la fréquence des appels (ex. 2 min). Pas de 409 onboarding.GET /staff/dashboard/badges— Même principe que le formateur (notifications,requests,unreadInbox).pendingcompte les demandes (groupesReservationBatchGroupavec statut agrégé « en attente », comme l’onglet Mes demandes), pas chaque lot isolément ;urgent/newUnseenrestent par lot NEW/PENDING dans ces demandes.
Notifications in-app (NotificationResource)
GET /notification/list— Liste des notifications actives non lues pour l’utilisateur connecté (sous-types discriminés :reservation,invoice,normal,association,calendar; champs communs incluentuuid,dismissedAt,title,body). 401 si non authentifié (plus de réponse vide anonyme).GET /notifications/search— Recherche paginée (mêmes sous-types). Paramètres query (comme la liste conversations) :page,limitouitemsPerPage,sort[…],filters[…]— ex.filters[uuid]pour cibler une notification par UUID. 401 si non authentifié.POST /notifications/read-all— Marque toutes les notifications actives encore non lues comme lues ; réponse JSON{ "updated": <nombre> }.POST /notifications/{uuid}/read— Marque une notification comme lue (readAt) ; idempotent si déjà lue ; réponse = ressource notification (même forme que les autres endpoints) ; 403 / 404 comme ci-dessous.POST /notifications/{uuid}/dismiss— PersistedismissedAtpour la notification identifiée paruuid(jamais par id entier) ; 403 si la notification appartient à un autre utilisateur ; 404 si absente ou UUID invalide.
Demandes de réservation (formateur)
GET /trainer/reservation/requests— Liste des lots (lecture seule pourfirstViewAt: la liste ne marque plus le lot comme vu ; seulPATCH …/viewmet à jourfirstViewAt) ; chaque élément inclut notammentcreatedAt,firstViewAt,respondedAt,contactRole(rôle du membership organisation si présent). Chaque entrée dereservations[]exposeweekEvents: pour la semaine lun–ven du créneau, fusion des événements du planning du formateur et des créneaux demandés du lot en cours (titres du typeDemande — {module}), triés par heure de début. Chaque entrée deweekEventsinclutisMainEvent(créneau correspondant à la lignereservations[]courante — « celui sur lequel on raisonne ») etisBatchEvent(autre créneau du même lot) ; les événements planning ont les deux àfalse. Vide sans contexte formateur.PATCH /trainer/reservation/batch/{uuid}/view— Marque le lot comme vu (firstViewAt), idempotent ; 204 sans corps ; 404 si inconnu ; 401 si le lot n’appartient pas au formateur connecté.POST /trainer/reservation/batches/mark-all-as-viewed— Marque en masse tous les lots du formateur comme vus (firstViewAt) avec une mise à jour idempotente (firstViewAt IS NULLuniquement), puis renvoie 200 avec le même payload queGET /trainer/reservation/requests(liste + compteurs).-
accept/refuse: idempotents si le lot est déjà ACCEPTED ou REFUSED (réponse DTO sans ré-exécuter les effets de bord — pas de double création d’événements agenda ; un secondrefuseignore les nouveaux motif/message).slotsinchangé. -
GET /trainer/organization/dashboard— Carte organisations : par assignation, métriques planifiées (totalHours,totalRevenue) et réalisées (totalRealizedHours,totalRealizedRevenue,realizedHoursPercent), pluscalendarColor,organizationInitials,lastImportSuccessful,isSyncPossible(aligné surCalendarConfigurator::canSyncFromRemote(): URL iCal, fichier.icsserveur ou jetons OAuth),providerType(id technique du connecteur FEAT-043 :google,edusign,hyperplanning,ical, etc. —nullsi pas de configurateur ; libellé FR dérivé côté front),countUnlinkedAssignments(nombre d’assignations iCal actives sans PMO / « à relier » ; remplace l’ancien booléenhasSynchonizedIcalError),lastImportErrorCode(enum string, présent seulement silastImportSuccessfulest false — libellés côté front), etstatus(enum métierok/conflict/error/nonecalculé côté backend :nonesi aucun configurateur,errorsi dernier import KO,conflictsi au moins une assignation active non reliée, sinonok). La règle est mutualisée dansDomain/Service/Dashboard/OrganizationStatusResolver. Compteur global inchangé :countSynchonizedIcalError.
Dashboard (autres)
GET /dashboard/trainer/income- Revenus formateurGET /dashboard/trainer/favorite-organizations- Organisations favoritesGET /dashboard/trainer/events- Événements formateurGET /dashboard/trainer/opportunity-proposals- Propositions d'opportunités
Calendrier / connecteurs (formateur, FEAT-043)
GET /trainer/calendar/configurator/{uuid}— réponse configurateur inclutproviderType(sélection hub),icalUrletdefaultPrice(tarif horaire par défaut du formateur :trainer_billing_settings.default_pricesi défini, sinon paramètre applicatiftrainer.default_hourly_price, défaut 35). Les imports iCal / réservation utilisent la même résolution viaTrainerDefaultHourlyPriceResolver.POST .../configurator/{organizationUuid}/ical/add— corps JSON :icalLink, optionnelproviderType; enregistre l’URL et le type sur le configurateur (jetons OAuth effacés si présents), renvoie un process, puis import asynchrone viaProcessTrainerCalendarConfiguratorSync(comme le callback OAuth).POST .../configurator/{organizationUuid}/ical/upload— multiparticsfile, optionnelproviderType; écrit le.icssur S3, met à jour le configurateur (ical_file, URL iCal effacée, jetons OAuth effacés), renvoie un process, puisProcessTrainerCalendarConfiguratorSync.POST /trainer/calendar/configurator/{uuid}/sync— relance d’import.POST /trainer/calendar/oauth/{provider}/authorize— API Platform (JWT formateur) : corps JSON{"organizationUuid":"<uuid>"}; réponse JSONauthorizationUrl. Fournisseurs :google,outlook,apple,edusign,hyperplanning,ypareo,ical(URL). Les stubs OAuth répondent 501 ;edusign/hyperplanning/ypareoexposent toutefois l’enrichissement iCal (parseurs PRODID via les stratégies connecteur (parseIcalVo/*IcalCalendarEventParser) sur le VOEvent, champsupportIcaldu hub inchangé côté API). Google / Outlook utilisentGOOGLE_CLIENT_*/MICROSOFT_CLIENT_*+TRAINER_CALENDAR_*_REDIRECT_URI: URL du SPA (ex.https://app…/trainer/calendar/oauth/google/callback) enregistrée chez le fournisseur ; identique à l’URL utilisée pour l’échange ducode.POST /trainer/calendar/oauth/{provider}/calendar/list— API Platform (TrainerCalendarOAuthCalendarListProcessor) : JWT formateur ; corps{ "code": "…", "state": "…" }(ouerror). Échange le code contre des jetons sans les persister ; renvoie la liste des agendas (disabledsi pour ce formateur l’identifiant d’agenda distant est déjà lié à une autre organisation ou déjà utilisé comme agenda personnel OAuth de ce formateur —TrainerOAuthRemoteCalendarListConflictMarker, requêtes filtrées par formateur). Réponse JSONtokens+calendars.POST /trainer/calendar/oauth/{provider}/callback— API Platform (TrainerCalendarOAuthCallbackProcessor) : JWT formateur ; corps JSON{ "accessToken", "refreshToken?", "expiresAt?", "calendarId", "state" }ou{ "error" }. Vérifie le state signé, enregistre jetons +oauth_calendar_idsurcalendar_configurator, asyncProcessTrainerCalendarConfiguratorSync. Réponse 200ProcessResponse(comme iCal add / sync) :uuid,metadata(dontorganization_uuid,calendar_oauth_provider). Erreur ou paramètres invalides : 400.
Company & Facturation (formateur)
GET /trainer/company- Entreprise du formateur (identité + infos bancaires uniquement)PUT /trainer/company- Mise à jour entreprise (identité + bancaire)GET /trainer/billing/settings- Paramètres de facturation (TVA, unité, délai, tarif par défaut, mentions, template email, logo)PUT /trainer/billing/settings- Mise à jour des paramètres de facturationGET /trainer/invoices/config- Config facturation (company + billing séparés, organizations, products ; utilisé pour préremplir la création de facture)GET /trainer/invoices/{uuid}- Détail facture (inclutdefaultVatRate,defaultUnit,paymentTermappliqués, en lecture seule)GET /trainer/invoices/{uuid}/pdf- PDF facture : en brouillon (DRAFT), généré à la volée sans stockage S3 ; dès que la facture n’est plus brouillon (FINALIZED,SENT,PAID, etc.), le backend sert d’abord l’objet S3{trainerUuid}/billing/invoices/{invoiceUuid}(même clé que celle écrite à la finalisation viaInvoiceFinalizedEvent), et ne régénère + réécrit que si l’objet est absent.GET /trainer/activity-reports/{uuid}/pdf- PDF CRA : toujours généré à la volée à chaque requête, sans stockage S3.
Validation factures : l'entité Invoice impose dueDate >= issueDate lorsque les deux dates sont renseignées (Assert Symfony sur le Domain). La commande UpdateInvoiceCommand valide avant persistance ; les Processors Create/Update renvoient 400 en cas de violation.
Format des réponses
Toutes les réponses sont au format JSON avec la structure suivante :
{
"data": {
// Données de la réponse
},
"meta": {
"timestamp": "2025-01-13T10:00:00Z",
"version": "1.0.0"
}
}
🔒 Sécurité
Authentification
JWT (JSON Web Tokens)
- Algorithme : RS256
- Durée de vie : 1 heure
- Refresh token : 30 jours
- Header :
Authorization: Bearer <token>
OAuth 2.0
- Providers supportés : LinkedIn, Google, Microsoft
- Flow : Authorization Code Flow
- Scopes : email, profile
Codes d'authentification
- Format : 6 chiffres
- Durée de vie : 10 minutes
- Envoi : Email via Brevo
Autorisation
Rôles utilisateurs
ROLE_USER: Utilisateur de baseROLE_TRAINER: FormateurROLE_ORGANIZATION: OrganisationROLE_ADMIN: Administrateur
Contrôle d'accès
- Routes publiques :
/security/*,/onboarding/*,/health/* - Routes protégées : Toutes les autres routes
- Validation : Via annotations Symfony Security
🗄️ Base de données
Schéma principal
erDiagram
User {
int id PK
string firstname
string lastname
string email UK
string password
array roles
string oauthProvider
string oauthProviderId UK
boolean acceptCgv
boolean acceptOptIn
boolean verifiedEmail
boolean finishedOnBoarding
string type
}
Trainer {
string SIRET
json onboardingCompletedSteps
}
Organization {
string name
string organizationType
string service
phone_number phoneNumber
}
Event {
int id PK
int trainer_id FK
datetime date_start
datetime date_end
string type
}
InterventionEvent {
int organization_id FK
string name
float amount
boolean is_approve_by_organization
}
Opportunity {
int id PK
int organization_id FK
string name
datetime date_start
datetime date_end
}
OpportunityProposal {
int id PK
int opportunity_id FK
int trainer_id FK
int candidacy_id FK
}
Candidacy {
int id PK
datetime date
}
Invoice {
int id PK
int trainer_id FK
int organization_id FK
int intervention_event_id FK
string amount
}
User ||--|| Trainer : "inherits"
User ||--|| Organization : "inherits"
Trainer ||--o{ Event : "has"
Trainer ||--o{ Candidacy : "has"
Trainer ||--o{ Invoice : "has"
Organization ||--o{ InterventionEvent : "has"
Organization ||--o{ Opportunity : "has"
Organization ||--o{ Invoice : "has"
Opportunity ||--o{ OpportunityProposal : "has"
Trainer ||--o{ OpportunityProposal : "proposes"
Candidacy ||--o{ OpportunityProposal : "linked_to"
Onboarding formateur
Parcours unique DISC-016 :
- Étapes backend :
CREATE_ACCOUNT,VERIFY_AVAILABILITY,SHARE_LINK,IMPORT_CALENDAR,DEFINE_PRICING. - L’avancement est stocké sur le Trainer dans
onboardingCompletedSteps(JSON : liste de valeurs d’enum des étapes validées). À l’inscription (CreateNewTrainerCommand, formulaire ou OAuth),CREATE_ACCOUNTest enregistrée dès la création du formateur. - L’exposition API canonique passe par
onboardingProgress(GetTrainerOnboardingProgressHandler+TrainerOnboardingProgressResponse).
Migrations
Le projet utilise Doctrine Migrations pour la gestion du schéma :
# Créer une nouvelle migration
php bin/console doctrine:migrations:diff
# Exécuter les migrations
php bin/console doctrine:migrations:migrate
# Annuler la dernière migration
php bin/console doctrine:migrations:migrate prev
🚀 Déploiement
Prérequis
- PHP : 8.2+
- Composer : 2.0+
- PostgreSQL : 13+
- Redis : 6.0+ (optionnel, pour le cache)
Variables d'environnement
# Base de données
DATABASE_URL="postgresql://user:password@localhost:5432/teadle"
# Sécurité
APP_SECRET="your-secret-key"
JWT_SECRET_KEY="path/to/private.key"
JWT_PUBLIC_KEY="path/to/public.key"
# OAuth
LINKEDIN_CLIENT_ID="your-linkedin-client-id"
LINKEDIN_CLIENT_SECRET="your-linkedin-client-secret"
LINKEDIN_REDIRECT_URI="https://your-domain.com/oauth/linkedin/callback"
GOOGLE_CLIENT_ID="your-google-client-id"
GOOGLE_CLIENT_SECRET="your-google-client-secret"
GOOGLE_REDIRECT_URI="https://your-domain.com/oauth/google/callback"
MICROSOFT_CLIENT_ID="your-microsoft-client-id"
MICROSOFT_CLIENT_SECRET="your-microsoft-client-secret"
MICROSOFT_REDIRECT_URI="https://your-domain.com/oauth/microsoft/callback"
# OAuth calendrier formateur (callbacks dédiés ; identifiants = GOOGLE_CLIENT_* / MICROSOFT_CLIENT_* ci-dessus)
TRAINER_CALENDAR_GOOGLE_REDIRECT_URI="https://app.example.com/trainer/calendar/oauth/google/callback"
TRAINER_CALENDAR_MICROSOFT_REDIRECT_URI="https://app.example.com/trainer/calendar/oauth/outlook/callback"
TRAINER_CALENDAR_OAUTH_SUCCESS_REDIRECT_URL="https://app.example.com"
# Email (Brevo)
BREVO_SECRET="your-brevo-api-key"
# URLs
PASSWORD_RESET_LINK="https://your-domain.com/password-reset"
Installation
# Installer les dépendances
composer install --no-dev --optimize-autoloader
# Configurer la base de données
php bin/console doctrine:database:create
php bin/console doctrine:migrations:migrate
# Charger les fixtures (développement)
php bin/console hautelook:alice:doctrine:load
# Vider le cache
php bin/console cache:clear --env=prod
# Configurer les permissions
chmod -R 755 var/
chmod -R 755 public/
Commandes utiles
# Analyser le code
php bin/console phpstan:analyse
# Corriger le style de code
php bin/console php-cs-fixer:fix
# Générer les clés JWT
php bin/console lexik:jwt:generate-keypair
# Vider le cache
php bin/console cache:clear
# Vérifier la santé de l'application
curl http://localhost:8000/health
# Le bilan mensuel est déclenché automatiquement par le Symfony Scheduler
# (via le worker supervisor symfony-scheduler consommant scheduler_default)
# Pas de commande console à lancer manuellement