diff --git a/.gitea/issue_template/evolution.yaml b/.gitea/issue_template/evolution.yaml new file mode 100644 index 0000000..a61ce6f --- /dev/null +++ b/.gitea/issue_template/evolution.yaml @@ -0,0 +1,56 @@ +name: "✨ Demande d'évolution" +about: "Proposer une nouvelle fonctionnalité ou une amélioration (maintenance évolutive)" +title: "[ÉVOLUTION] " +labels: + - "type/évolution" +body: + - type: markdown + attributes: + value: | + Toute demande d'évolution fait l'objet d'une analyse par le prestataire + (documentation, délai de mise en œuvre, coût associé) avant planification. + - type: dropdown + id: module + attributes: + label: Module concerné + options: + - "Comptes utilisateurs" + - "Informations" + - "Tracker d'émotions" + - "Administration" + - "Nouveau module (Diagnostic, Cohérence cardiaque, Activités de détente)" + - "Transverse (sécurité, RGPD, performance)" + validations: + required: true + - type: textarea + id: besoin + attributes: + label: Besoin / problème à résoudre + description: "En tant que … je souhaite … afin de …" + validations: + required: true + - type: textarea + id: solution + attributes: + label: Solution proposée + validations: + required: false + - type: dropdown + id: priorite + attributes: + label: Priorité souhaitée + options: + - "Haute" + - "Moyenne" + - "Basse" + validations: + required: true + - type: textarea + id: criteres + attributes: + label: Critères d'acceptation + value: | + - [ ] + - [ ] + validations: + required: false diff --git a/.gitea/issue_template/incident.yaml b/.gitea/issue_template/incident.yaml new file mode 100644 index 0000000..1ed2731 --- /dev/null +++ b/.gitea/issue_template/incident.yaml @@ -0,0 +1,74 @@ +name: "🐞 Signalement d'incident / anomalie" +about: "Déclarer un dysfonctionnement de l'application (maintenance corrective)" +title: "[INCIDENT] " +labels: + - "type/incident" +body: + - type: markdown + attributes: + value: | + Merci de renseigner ce formulaire le plus précisément possible. + Le délai de prise en compte dépend de la **sévérité** déclarée (voir SLA du contrat). + - type: dropdown + id: severite + attributes: + label: Sévérité + description: "Bloquant = service inutilisable · Majeur = service dégradé avec contournement · Mineur = altération mineure" + options: + - "Bloquant — priorité critique (prise en compte 1 h / correction 3 h)" + - "Bloquant — priorité forte (prise en compte 2 h / correction 6 h)" + - "Majeur (prise en compte 7 h / correction 16 h)" + - "Mineur (prise en compte 1 j / correction 40 h)" + validations: + required: true + - type: dropdown + id: module + attributes: + label: Module concerné + options: + - "Comptes utilisateurs / Authentification" + - "Informations (contenus)" + - "Tracker d'émotions" + - "Administration" + - "Infrastructure / Déploiement" + - "Autre" + validations: + required: true + - type: input + id: environnement + attributes: + label: Environnement + placeholder: "Production (cesizen.cleanows.fr) · web ou mobile · navigateur/OS + version" + validations: + required: true + - type: textarea + id: description + attributes: + label: Description du problème + description: "Ce qui ne fonctionne pas." + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Étapes de reproduction + value: | + 1. + 2. + 3. + validations: + required: true + - type: textarea + id: attendu + attributes: + label: Résultat attendu vs. obtenu + placeholder: "Attendu : … / Obtenu : …" + validations: + required: true + - type: textarea + id: contexte + attributes: + label: Captures d'écran / logs + description: "Capture, message d'erreur, extrait de journal (sans donnée personnelle)." + validations: + required: false diff --git a/.gitea/pull_request_template.md b/.gitea/pull_request_template.md new file mode 100644 index 0000000..479bf34 --- /dev/null +++ b/.gitea/pull_request_template.md @@ -0,0 +1,25 @@ + + +## Objet + + +## Ticket lié + +Closes # + +## Type de changement +- [ ] 🐞 Correctif (maintenance corrective) +- [ ] ✨ Évolution (maintenance évolutive) +- [ ] ♻️ Refactorisation / dette technique +- [ ] 🔒 Sécurité +- [ ] 📖 Documentation + +## Vérifications +- [ ] Les tests automatisés passent en local (`php artisan test`, `npm test`) +- [ ] Le style de code est respecté (`vendor/bin/pint`, `npm run lint`) +- [ ] La CI Gitea Actions est verte +- [ ] Pas de secret ni de donnée personnelle commitée +- [ ] Documentation mise à jour si nécessaire + +## Impact sécurité / données personnelles + diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..8628c3c --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,85 @@ +# ───────────────────────────────────────────────────────────────────────── +# Intégration continue CESIZen — Gitea Actions +# Déclenchée à chaque push et pull request. Lance les 29 tests automatisés +# (backend PHPUnit + frontend Vitest + mobile Jest) et le lint. +# Un déploiement Coolify est déclenché par webhook après un merge sur main +# (voir docker-compose.prod.yaml + MIGRATION_GITEA.md). +# ───────────────────────────────────────────────────────────────────────── +name: CI + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + # ── Backend Laravel : PHPUnit sur base SQLite en mémoire (cf. backend/phpunit.xml) ── + backend: + name: Backend · PHPUnit + Pint + runs-on: ubuntu-latest + defaults: + run: + working-directory: backend + steps: + - uses: actions/checkout@v4 + + - name: Installer PHP 8.4 + uses: shivammathur/setup-php@v2 + with: + php-version: "8.4" + extensions: pdo, pdo_sqlite, bcmath, mbstring + coverage: none + + - name: Cache des dépendances Composer + uses: actions/cache@v4 + with: + path: backend/vendor + key: composer-${{ hashFiles('backend/composer.lock') }} + + - name: Installer les dépendances + run: composer install --no-interaction --prefer-dist --no-progress + + - name: Vérifier le style (Laravel Pint, PSR-12) + run: vendor/bin/pint --test + + - name: Lancer les tests (Unit + Feature) + run: php artisan test + + # ── Frontend web Next.js : ESLint + Vitest ── + frontend: + name: Frontend · ESLint + Vitest + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + cache-dependency-path: frontend/package-lock.json + - run: npm ci + - name: Lint + run: npm run lint + - name: Tests unitaires + run: npm test + + # ── Mobile Expo / React Native : Jest ── + mobile: + name: Mobile · Jest + runs-on: ubuntu-latest + defaults: + run: + working-directory: mobile + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + cache-dependency-path: mobile/package-lock.json + - run: npm ci + - name: Tests unitaires + run: npm test diff --git a/CESIZen_Soutenance_Bloc3.pptx b/CESIZen_Soutenance_Bloc3.pptx new file mode 100644 index 0000000..ae38a24 Binary files /dev/null and b/CESIZen_Soutenance_Bloc3.pptx differ diff --git a/Dossier_Bloc3_CESIZen.docx b/Dossier_Bloc3_CESIZen.docx new file mode 100644 index 0000000..42db6e9 Binary files /dev/null and b/Dossier_Bloc3_CESIZen.docx differ diff --git a/MIGRATION_GITEA.md b/MIGRATION_GITEA.md new file mode 100644 index 0000000..e8f70f1 --- /dev/null +++ b/MIGRATION_GITEA.md @@ -0,0 +1,97 @@ +# Migration vers Gitea & mise en place des outils (Bloc 3) + +Ce guide décrit la mise en place de **Gitea** comme plateforme unique de **versioning**, de +**gestion des tickets** (maintenance) et d'**intégration continue** (Gitea Actions), ainsi que le +raccordement au déploiement continu **Coolify**. Il sert de support à la démonstration de soutenance. + +> Instance utilisée : Gitea auto-hébergée. Dépôt cible : `https:///adam/cesizen`. + +--- + +## 1. Migrer le dépôt GitHub → Gitea + +### Option A — Migration assistée (recommandée, conserve issues & historique) +1. Se connecter à Gitea → **+ → Nouvelle migration → GitHub**. +2. URL source : `https://github.com/AzmogEx/CesiZen.git`. +3. Cocher : *Éléments à migrer* → dépôt Git, **Issues**, **Pull requests**, **Releases**, **Labels**, **Milestones**. +4. Renseigner un jeton d'accès GitHub (lecture) si le dépôt est privé. Lancer. + +### Option B — Push manuel (miroir du code seul) +```bash +cd /chemin/vers/CesiZen +git remote add gitea https:///adam/cesizen.git +git push gitea --all # toutes les branches +git push gitea --tags # tous les tags/releases +# Faire de Gitea le dépôt de référence : +git remote set-url origin https:///adam/cesizen.git +``` + +Vérifier ensuite que `.gitea/` (workflows + gabarits d'issues) est bien présent à la racine. + +--- + +## 2. Activer l'intégration continue (Gitea Actions) + +1. **Côté serveur Gitea** : Actions doit être activé (`app.ini` → `[actions] ENABLED = true`). +2. **Enregistrer un runner** (une fois) : + ```bash + # Récupérer le jeton dans Gitea → Administration → Actions → Runners + act_runner register --instance https:// --token \ + --name cesizen-runner --labels ubuntu-latest:docker://node:20-bookworm + act_runner daemon + ``` +3. Le workflow **`.gitea/workflows/ci.yml`** se déclenche alors à chaque `push` / `pull request` et + exécute les 3 jobs : **backend** (PHPUnit + Pint), **frontend** (ESLint + Vitest), **mobile** (Jest). +4. Une pastille verte/rouge s'affiche sur chaque commit et chaque PR (démo). + +--- + +## 3. Configurer le ticketing (maintenance) + +### Labels à créer (Issues → Labels → « Créer un label ») +| Label | Couleur | Usage | +|---|---|---| +| `type/incident` | rouge | Anomalie (maintenance corrective) — posé automatiquement par le gabarit | +| `type/évolution` | bleu | Nouvelle fonctionnalité (maintenance évolutive) | +| `gravité/bloquant` | #C8102E | Service inutilisable | +| `gravité/majeur` | #E4801C | Service dégradé, contournement possible | +| `gravité/mineur` | #F0C808 | Altération mineure | +| `priorité/critique` · `priorité/forte` · `priorité/normale` | dégradé | Ordonnancement | +| `statut/à-qualifier` · `statut/en-cours` · `statut/en-revue` · `statut/résolu` | gris→vert | Suivi Kanban | +| `module/comptes` · `module/informations` · `module/tracker` · `module/infra` | vert | Domaine | + +Les **gabarits d'issues** sont déjà versionnés dans `.gitea/issue_template/` (`incident.yaml`, +`evolution.yaml`) : ils apparaissent automatiquement au clic sur **« Nouvelle issue »**. + +### Milestones (= versions) +Créer un jalon par version livrée : `v1.0`, `v1.1`, … Chaque issue/PR est rattachée à un jalon → suivi +de l'avancement par version. + +### Tableau de bord Kanban (pilotage) +Onglet **Projects → Nouveau projet** (type *Board*) avec les colonnes : +`À qualifier` → `À faire` → `En cours` → `En revue (PR)` → `Résolu`. +Les issues glissent d'une colonne à l'autre ; les labels de statut sont synchronisés. + +--- + +## 4. Raccorder le déploiement continu (Coolify) + +Coolify build les images depuis le dépôt Git et redéploie à chaque nouveau commit sur `main` : + +1. Dans Coolify → application CESIZen → **Source** : pointer sur le dépôt Gitea (`main`). +2. Activer le **webhook de déploiement** : Coolify fournit une URL de type + `https:///api/v1/deploy?uuid=…&force=false`. +3. Dans Gitea → dépôt → **Paramètres → Webhooks → Ajouter** : coller l'URL, événement *Push*, branche `main`. +4. Résultat : `merge sur main` → CI verte → webhook → Coolify build + déploie en production + (`cesizen.cleanows.fr` / `api.cesizen.cleanows.fr`), avec migrations, seed et génération des + secrets automatisés par `backend/docker/entrypoint.sh`. + +--- + +## 5. Script de démonstration (soutenance) + +1. **Versioning** : montrer l'arbre des commits Gitea, les branches, un tag de version. +2. **CI** : ouvrir une PR triviale → montrer les 3 jobs qui tournent → pastille verte. +3. **Ticketing** : créer une issue via le gabarit *incident*, la qualifier (labels + jalon), + la déplacer sur le board Kanban. +4. **CD** : merger la PR → montrer le déploiement Coolify déclenché → rafraîchir le site en ligne. diff --git a/Plan_Oral_Bloc3_CESIZen.md b/Plan_Oral_Bloc3_CESIZen.md new file mode 100644 index 0000000..6417863 --- /dev/null +++ b/Plan_Oral_Bloc3_CESIZen.md @@ -0,0 +1,62 @@ +# Plan d'oral — Soutenance CESIZen · Activité 3 + +**Apprenant :** Adam Marzuk · **Bloc :** INFCDAAL3 — Déployer et sécuriser les applications informatiques +**Format :** 20 min de présentation + ~10 min de questions/réponses +**Support :** `CESIZen_Soutenance_Bloc3.pptx` (14 slides) + **démo live des outils** (Gitea + Coolify) +**Dossier :** `Dossier_Bloc3_CESIZen.docx` · **Guide outils :** `MIGRATION_GITEA.md` +**En ligne :** https://cesizen.cleanows.fr · API https://api.cesizen.cleanows.fr + +> Objectif du jury : évaluer le **plan de déploiement**, le **plan de maintenance** et le **plan de sécurisation**, ainsi que la **démonstration** des outils de versioning et de ticketing réellement configurés. + +--- + +## Déroulé minuté (20 min) + +| Temps | Slide(s) | Sujet | Messages clés à dire | +|---|---|---|---| +| **0:00 – 1:30** | 1 — 2 | **Introduction** | Se présenter. Rappeler CESIZen (plateforme de santé mentale, Ministère). Annoncer que l'Activité 3 porte sur le **déploiement** et la **sécurisation**, avec 3 livrables + une démo d'outils. Présenter le sommaire. | +| **1:30 – 3:00** | 3 | **Contexte & périmètre** | Situer après Act. 1 (conception) et Act. 2 (dev/test). Prototype = API Laravel + web Next.js + mobile Expo + PostgreSQL. Les 3 livrables : plan de déploiement, de maintenance, de sécurisation. | +| **3:00 – 4:30** | 4 | **Architecture technique** | Architecture **découplée** : 1 API REST, 2 clients. Une seule logique métier → testabilité, évolutivité. Conteneurisation Docker = environnements reproductibles. | +| **4:30 – 7:00** | 5 — 6 | **Plan de déploiement + CI/CD** | Les **3 environnements** (dev local / test = CI / prod = Coolify). Détailler le schéma. Puis **Gitea** (versioning souverain) + **Gitea Actions** (29 tests à chaque push) + **Coolify** (déploiement continu sur merge, HTTPS, migrations auto). Insister : « la prod est **réellement en ligne**. » | +| **7:00 – 9:30** | 7 — 8 | **Plan de maintenance** | Ticketing **Gitea Issues** : gabarits incident/évolution, labels, jalons, Kanban. Cycle de vie d'un ticket + **SLA du contrat** (bloquant/majeur/mineur). Maintenance évolutive (analyse : doc, délai, coût). **Veille** : sécurité (CVE/CERT-FR), dépendances (audit en CI), réglementation (CNIL). | +| **9:30 – 13:00** | 9 — 10 — 11 | **Plan de sécurisation** ⭐ | (Partie la plus notée — 8 pts.) Vulnérabilités **OWASP** avec mesures en place. **Matrice de criticité** (gravité × probabilité) → R1 fuite de données, R7 chiffrement au repos, R2 injection prioritaires. Actions **préventives / correctives** + chiffrement (TLS, bcrypt, AES, JWT). Assumer les axes d'amélioration (chiffrement colonne, headers de sécurité). | +| **13:00 – 14:30** | 12 — 13 | **RGPD & gestion de crise** | Données de santé = **sensibles (art. 9)**. Consentement, droits (export/anonymisation), soft delete, audit immuable, hébergement UE/HDS. **Escalade** N1→N4, notification **CNIL < 72 h**. | +| **14:30 – 18:30** | démo | **🎬 Démo des outils** | Sur Gitea : commits/branches/tags → ouvrir une **PR** → 3 jobs de CI **verts** → créer une **issue** (gabarit incident) + labels + Kanban → **merge** → montrer **Coolify** qui déploie → rafraîchir le site en ligne. 🛟 Si un outil plante : montrer les captures du dossier / `MIGRATION_GITEA.md`. | +| **18:30 – 20:00** | 14 | **Conclusion** | Synthèse : chaîne de déploiement complète et en prod, maintenance cadrée, sécurité par les risques + RGPD. Remercier, ouvrir les questions. | + +**Repère de rythme :** checkpoint à **14:30** (début démo). Si retard : raccourcir la maintenance (garder SLA + veille) et garder 4 min de démo + 1 min de conclusion. ⚠️ **Prépare les onglets AVANT d'entrer** : Gitea (dépôt + une PR prête), Coolify (tableau de bord), le site en ligne. + +--- + +## Préparation aux questions / réponses (~10 min) + +### Déploiement +**Q. Pourquoi trois environnements alors qu'un seul est réellement déployé ?** +> Le sujet demande de décrire dev/test/prod et d'en déployer **un** réellement : c'est la production, en ligne sur Coolify. Le dev tourne en local (docker-compose), le test correspond au runner de CI (base SQLite en mémoire). Chacun a un rôle distinct dans le cycle de vie. + +**Q. Qu'est-ce qui est automatisé exactement ?** +> Deux choses : l'**intégration continue** (Gitea Actions lance les 29 tests + le lint à chaque push) et le **déploiement continu** (un merge sur main déclenche, via webhook, le build des images et le redéploiement Coolify — migrations, seed et génération des secrets étant faits par l'entrypoint au démarrage). + +**Q. Comment gérez-vous les secrets et le rollback ?** +> Les secrets de prod (APP_KEY, JWT_SECRET) ne sont **pas versionnés** : ils sont générés au premier démarrage par l'entrypoint. Le rollback consiste à redéployer le tag de la version précédente depuis Coolify. + +### Maintenance +**Q. Pourquoi Gitea plutôt que GitHub ou Jira ?** +> Gitea réunit dépôt Git, ticketing et CI/CD dans un outil **souverain et auto-hébergé**, cohérent avec un projet ministériel (données maîtrisées, pas de dépendance SaaS hors UE), léger et gratuit. + +**Q. Comment respectez-vous les délais de correction ?** +> La sévérité déclarée dans le gabarit d'issue déclenche le SLA correspondant (bloquant critique = prise en compte 1 h / correction 3 h, etc.). Le Kanban et les jalons donnent au Ministère une visibilité temps réel. + +### Sécurisation +**Q. Quelles sont les vraies faiblesses de sécurité aujourd'hui ?** +> Honnêtement : pas encore de chiffrement applicatif au repos des notes de santé, pas d'en-têtes HSTS/CSP, et une politique de mot de passe limitée à 8 caractères. Ce sont mes actions correctives prioritaires — elles figurent dans le plan (R7 notamment). + +**Q. Comment calculez-vous la criticité ?** +> Criticité = gravité × probabilité, sur une échelle de 1 à 25. Ça hiérarchise les actions : R7 (chiffrement au repos, 12) et R2 (injection, 9) passent avant R8 (erreur de déploiement, 2). + +**Q. Que se passe-t-il en cas de fuite de données de santé ?** +> Escalade N3 : le chef de projet et le DPO sont mobilisés, notification à la **CNIL sous 72 h** (art. 33 RGPD), information des personnes concernées, isolation et préservation des preuves, puis retour d'expérience. + +### Général +**Q. Qu'est-ce qui prouve que ce n'est pas que de la théorie ?** +> La production est **en ligne** (cesizen.cleanows.fr), les outils sont configurés (je les montre en démo), les 29 tests tournent en CI, et l'audit de sécurité est réellement alimenté dans l'application. diff --git a/assets_bloc3/archi_deploiement.png b/assets_bloc3/archi_deploiement.png new file mode 100644 index 0000000..85e63b1 Binary files /dev/null and b/assets_bloc3/archi_deploiement.png differ diff --git a/assets_bloc3/cycle_ticket.png b/assets_bloc3/cycle_ticket.png new file mode 100644 index 0000000..a099828 Binary files /dev/null and b/assets_bloc3/cycle_ticket.png differ diff --git a/assets_bloc3/matrice_risques.png b/assets_bloc3/matrice_risques.png new file mode 100644 index 0000000..c187577 Binary files /dev/null and b/assets_bloc3/matrice_risques.png differ diff --git a/generate_diagrams_bloc3.py b/generate_diagrams_bloc3.py new file mode 100644 index 0000000..aaa29df --- /dev/null +++ b/generate_diagrams_bloc3.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +""" +Génère les schémas du dossier Bloc 3 (Déployer et sécuriser) — CESIZen. +Sortie : ./assets_bloc3/*.png (réutilisés dans le .docx et le .pptx) +""" +import os +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt +from matplotlib.patches import FancyBboxPatch, FancyArrowPatch +import matplotlib.font_manager as fm + +# ─── Charte CESIZen ─────────────────────────────────────────────── +GREEN = "#06C656" +GREEN_D = "#049641" +BLUE = "#004682" +BLUE_L = "#E6F0FA" +GREY_D = "#232323" +GREY = "#6E6E6E" +WHITE = "#FFFFFF" +INK = "#23232D" +RED = "#C8102E" +ORANGE = "#E4801C" +YELLOW_A = "#F0C808" + +HERE = os.path.dirname(os.path.abspath(__file__)) +OUT = os.path.join(HERE, "assets_bloc3") +os.makedirs(OUT, exist_ok=True) + +plt.rcParams["font.family"] = "DejaVu Sans" + + +def box(ax, x, y, w, h, text, fc, ec=None, tc="white", fs=10, bold=True, radius=0.02): + ec = ec or fc + p = FancyBboxPatch((x, y), w, h, boxstyle=f"round,pad=0.005,rounding_size={radius}", + fc=fc, ec=ec, lw=1.4, mutation_aspect=1) + ax.add_patch(p) + ax.text(x + w / 2, y + h / 2, text, ha="center", va="center", + color=tc, fontsize=fs, fontweight="bold" if bold else "normal", zorder=5) + + +def arrow(ax, x1, y1, x2, y2, color=GREY_D, style="-|>", lw=1.8, ls="-"): + a = FancyArrowPatch((x1, y1), (x2, y2), arrowstyle=style, mutation_scale=16, + color=color, lw=lw, linestyle=ls, zorder=1) + ax.add_patch(a) + + +# ══════════════════════════════════════════════════════════════════ +# 1. ARCHITECTURE DE DÉPLOIEMENT + CHAÎNE CI/CD +# ══════════════════════════════════════════════════════════════════ +def diagram_deploiement(): + fig, ax = plt.subplots(figsize=(12, 6.6)) + ax.set_xlim(0, 12); ax.set_ylim(0, 6.6); ax.axis("off") + + ax.text(6, 6.35, "Chaîne de déploiement CESIZen — du poste développeur à la production", + ha="center", fontsize=13, fontweight="bold", color=BLUE) + + # --- Bandeau des 3 environnements --- + envs = [ + (0.3, "DÉVELOPPEMENT", "Poste local", GREEN, + ["Docker Compose (dev)", "PostgreSQL 16 + Redis 7", "Hot-reload back & front", "APP_DEBUG=true"]), + (4.15, "TEST / INTÉGRATION", "Gitea Actions (CI)", ORANGE, + ["Runner conteneurisé", "Base SQLite en mémoire", "29 tests automatisés", "Lint (Pint / ESLint)"]), + (8.0, "PRODUCTION", "Coolify (PaaS)", BLUE, + ["Docker Compose (prod)", "HTTPS / TLS (reverse proxy)", "Secrets générés hors dépôt", "Health checks + restart"]), + ] + for x, titre, sous, col, items in envs: + box(ax, x, 3.4, 3.55, 2.2, "", BLUE_L, ec=col, tc=INK) + box(ax, x, 5.0, 3.55, 0.6, titre, col, fs=11) + ax.text(x + 1.775, 4.75, sous, ha="center", fontsize=9.5, style="italic", color=GREY_D) + for i, it in enumerate(items): + ax.text(x + 0.15, 4.45 - i * 0.27, "• " + it, ha="left", fontsize=8.5, color=INK) + + arrow(ax, 3.95, 4.5, 4.1, 4.5, color=GREY_D) + arrow(ax, 7.8, 4.5, 7.95, 4.5, color=GREY_D) + + # --- Chaîne CI/CD (bas) --- + ax.text(6, 2.9, "Flux d'intégration & de livraison continues", ha="center", + fontsize=11, fontweight="bold", color=GREEN_D) + steps = [ + (0.3, "git push\n(Gitea)", GREY_D), + (2.55, "Gitea Actions\ndéclenché", ORANGE), + (4.8, "Build + Tests\n(29 tests)", ORANGE), + (7.05, "Webhook\nCoolify", BLUE), + (9.3, "Build images\n+ déploiement", BLUE), + ] + for x, t, col in steps: + box(ax, x, 1.3, 2.05, 1.05, t, col, fs=9.5) + for i in range(len(steps) - 1): + arrow(ax, steps[i][0] + 2.05, 1.82, steps[i + 1][0], 1.82, color=GREEN_D, lw=2) + + box(ax, 9.3, 0.15, 2.05, 0.8, "cesizen.cleanows.fr\napi.cesizen.cleanows.fr", GREEN, fs=8) + arrow(ax, 10.32, 1.3, 10.32, 0.95, color=GREEN_D, lw=2) + ax.text(0.3, 0.55, "Rollback : redéploiement de la version\nGit précédente (tag) en 1 clic Coolify.", + ha="left", fontsize=8, color=GREY, style="italic") + + plt.tight_layout() + fig.savefig(os.path.join(OUT, "archi_deploiement.png"), dpi=170, bbox_inches="tight") + plt.close(fig) + + +# ══════════════════════════════════════════════════════════════════ +# 2. MATRICE DE RISQUES (criticité = gravité × probabilité) +# ══════════════════════════════════════════════════════════════════ +def diagram_matrice_risques(): + fig, ax = plt.subplots(figsize=(11, 6.8)) + n = 5 + for g in range(n): # gravité (y) + for p in range(n): # probabilité (x) + crit = (g + 1) * (p + 1) + if crit <= 4: + c = "#2E9E4F" + elif crit <= 9: + c = "#F0C808" + elif crit <= 14: + c = "#E4801C" + else: + c = "#C8102E" + ax.add_patch(plt.Rectangle((p, g), 1, 1, fc=c, ec="white", lw=2, alpha=0.9)) + ax.text(p + 0.5, g + 0.5, str(crit), ha="center", va="center", + fontsize=9, color="white", fontweight="bold", alpha=0.6) + + # Positionnement des risques réels du projet (P sur 1..5, G sur 1..5) + risques = [ + ("R1", 2, 4), # Fuite données de santé (Art.9) + ("R2", 3, 3), # Injection / XSS + ("R3", 2, 3), # Compromission compte admin + ("R4", 3, 2), # DDoS / indisponibilité + ("R5", 2, 2), # Perte de données (sauvegarde) + ("R6", 4, 2), # Dépendances vulnérables + ("R7", 3, 4), # Absence de chiffrement au repos + ("R8", 2, 1), # Erreur humaine déploiement + ] + for nom, p, g in risques: + ax.plot(p - 0.5, g - 0.5, "o", ms=22, mfc="white", mec=INK, mew=1.8, zorder=6) + ax.text(p - 0.5, g - 0.5, nom, ha="center", va="center", fontsize=8.5, + fontweight="bold", color=INK, zorder=7) + + ax.set_xlim(0, n); ax.set_ylim(0, n) + ax.set_xticks([i + 0.5 for i in range(n)]) + ax.set_yticks([i + 0.5 for i in range(n)]) + ax.set_xticklabels(["Très faible", "Faible", "Moyenne", "Forte", "Très forte"], fontsize=9) + ax.set_yticklabels(["Mineure", "Modérée", "Grave", "Critique", "Catastroph."], fontsize=9) + ax.set_xlabel("Probabilité →", fontsize=11, fontweight="bold", color=BLUE) + ax.set_ylabel("Gravité →", fontsize=11, fontweight="bold", color=BLUE) + ax.set_title("Matrice de criticité des risques CESIZen (criticité = gravité × probabilité)", + fontsize=12.5, fontweight="bold", color=BLUE, pad=12) + for s in ax.spines.values(): + s.set_visible(False) + ax.tick_params(length=0) + + legende = ("R1 Fuite de données de santé R2 Injection SQL/XSS R3 Compromission compte admin " + "R4 DDoS / indisponibilité\nR5 Perte de données R6 Dépendances vulnérables " + "R7 Absence de chiffrement au repos R8 Erreur humaine de déploiement") + fig.text(0.5, -0.02, legende, ha="center", fontsize=8.3, color=GREY_D) + + plt.tight_layout() + fig.savefig(os.path.join(OUT, "matrice_risques.png"), dpi=170, bbox_inches="tight") + plt.close(fig) + + +# ══════════════════════════════════════════════════════════════════ +# 3. CYCLE DE VIE D'UN TICKET (maintenance corrective) +# ══════════════════════════════════════════════════════════════════ +def diagram_ticket(): + fig, ax = plt.subplots(figsize=(12, 5.2)) + ax.set_xlim(0, 12); ax.set_ylim(0, 5.2); ax.axis("off") + ax.text(6, 4.95, "Cycle de vie d'un ticket sur Gitea (prestataire ↔ Ministère)", + ha="center", fontsize=13, fontweight="bold", color=BLUE) + + etapes = [ + ("1. Signalement\nIssue créée\n(gabarit incident)", GREY_D), + ("2. Qualification\nLabels : gravité,\npriorité, module", BLUE), + ("3. Priorisation\nSLA appliqué\n(bloquant/majeur/mineur)", ORANGE), + ("4. Correction\nbranche fix/*\n+ tests", GREEN_D), + ("5. Revue & CI\nPull Request\n29 tests verts", GREEN_D), + ("6. Livraison\nmerge → Coolify\ndéploie", GREEN), + ("7. Clôture\nvalidation client\nIssue fermée", GREEN), + ] + x = 0.15 + w = 1.58 + gap = 0.11 + y = 2.5 + for i, (t, col) in enumerate(etapes): + box(ax, x, y, w, 1.3, t, col, fs=8.3) + if i < len(etapes) - 1: + arrow(ax, x + w, y + 0.65, x + w + gap, y + 0.65, color=GREY_D, lw=1.6) + x += w + gap + + # Bandeau SLA + ax.text(6, 1.9, "Délais contractuels (jours ouvrés) — repris du cahier des charges", + ha="center", fontsize=10, fontweight="bold", color=GREEN_D) + sla = [ + ("Bloquant critique", "Prise en compte 1 h", "Correction 3 h", RED), + ("Bloquant fort", "Prise en compte 2 h", "Correction 6 h", ORANGE), + ("Majeur", "Prise en compte 7 h", "Correction 16 h", YELLOW_A), + ("Mineur (par lots)", "Prise en compte 1 j", "Correction 40 h", GREEN), + ] + bx = 0.3 + bw = 2.85 + for titre, d1, d2, col in sla: + box(ax, bx, 0.35, bw, 1.15, "", BLUE_L, ec=col, tc=INK) + box(ax, bx, 1.12, bw, 0.38, titre, col, fs=9, tc="white") + ax.text(bx + bw / 2, 0.92, d1, ha="center", fontsize=8.3, color=INK) + ax.text(bx + bw / 2, 0.62, d2, ha="center", fontsize=8.3, color=INK) + bx += bw + 0.1 + + plt.tight_layout() + fig.savefig(os.path.join(OUT, "cycle_ticket.png"), dpi=170, bbox_inches="tight") + plt.close(fig) + + +if __name__ == "__main__": + diagram_deploiement() + diagram_matrice_risques() + diagram_ticket() + print("Schémas Bloc 3 générés dans :", OUT) + for f in sorted(os.listdir(OUT)): + print(" -", f) diff --git a/generate_dossier_bloc3.py b/generate_dossier_bloc3.py new file mode 100644 index 0000000..f1083ff --- /dev/null +++ b/generate_dossier_bloc3.py @@ -0,0 +1,613 @@ +#!/usr/bin/env python3 +""" +Génère le dossier écrit du Bloc 3 CESIZen — « Déployer et sécuriser les applications +informatiques » (INFCDAAL3) au format Word éditable. + +Sortie : ./Dossier_Bloc3_CESIZen.docx +Schémas : ./assets_bloc3/*.png (générés par generate_diagrams_bloc3.py) +""" +import os +from docx import Document +from docx.shared import Pt, Inches, RGBColor +from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING +from docx.enum.table import WD_TABLE_ALIGNMENT +from docx.enum.section import WD_SECTION +from docx.oxml.ns import qn +from docx.oxml import OxmlElement + +# ─── Charte CESIZen ─────────────────────────────────────────────── +BLUE = RGBColor(0x00, 0x46, 0x82) +GREEN = RGBColor(0x04, 0x96, 0x41) +GREEN_V = RGBColor(0x06, 0xC6, 0x56) +INK = RGBColor(0x23, 0x23, 0x2D) +GREY = RGBColor(0x6E, 0x6E, 0x6E) +RED = RGBColor(0xC8, 0x10, 0x2E) +ORANGE = RGBColor(0xE4, 0x80, 0x1C) +WHITE = RGBColor(0xFF, 0xFF, 0xFF) +BLUE_L = "E6F0FA" +GREY_L = "F2F2F2" + +HERE = os.path.dirname(os.path.abspath(__file__)) +ASSETS = os.path.join(HERE, "assets_bloc3") +OUT = os.path.join(HERE, "Dossier_Bloc3_CESIZen.docx") + +doc = Document() + +# ─── Styles de base ─────────────────────────────────────────────── +normal = doc.styles["Normal"] +normal.font.name = "Calibri" +normal.font.size = Pt(10.5) +normal.font.color.rgb = INK +normal.paragraph_format.space_after = Pt(6) +normal.paragraph_format.line_spacing = 1.12 + +for name, size, color in [("Heading 1", 17, BLUE), ("Heading 2", 13.5, GREEN), + ("Heading 3", 11.5, BLUE)]: + st = doc.styles[name] + st.font.name = "Calibri" + st.font.size = Pt(size) + st.font.color.rgb = color + st.font.bold = True + st.paragraph_format.space_before = Pt(12) + st.paragraph_format.space_after = Pt(4) + st.paragraph_format.keep_with_next = True + + +def _shade(cell, hexcolor): + tcPr = cell._tc.get_or_add_tcPr() + shd = OxmlElement("w:shd") + shd.set(qn("w:val"), "clear") + shd.set(qn("w:fill"), hexcolor) + tcPr.append(shd) + + +def _set_cell_text(cell, text, bold=False, color=INK, size=9.5, white=False, + align=WD_ALIGN_PARAGRAPH.LEFT): + cell.text = "" + p = cell.paragraphs[0] + p.alignment = align + p.paragraph_format.space_after = Pt(1) + p.paragraph_format.space_before = Pt(1) + r = p.add_run(text) + r.font.size = Pt(size) + r.font.bold = bold + r.font.color.rgb = WHITE if white else color + r.font.name = "Calibri" + + +def h1(text): + doc.add_paragraph(text, style="Heading 1") + + +def h2(text): + doc.add_paragraph(text, style="Heading 2") + + +def h3(text): + doc.add_paragraph(text, style="Heading 3") + + +def para(text, size=10.5, italic=False, color=INK, align=WD_ALIGN_PARAGRAPH.JUSTIFY, + bold=False, after=6): + p = doc.add_paragraph() + p.alignment = align + p.paragraph_format.space_after = Pt(after) + r = p.add_run(text) + r.font.size = Pt(size) + r.italic = italic + r.bold = bold + r.font.color.rgb = color + return p + + +def rich(segments, align=WD_ALIGN_PARAGRAPH.JUSTIFY, after=6, size=10.5): + """segments = list of (text, bold)""" + p = doc.add_paragraph() + p.alignment = align + p.paragraph_format.space_after = Pt(after) + for text, bold in segments: + r = p.add_run(text) + r.font.size = Pt(size) + r.bold = bold + r.font.color.rgb = INK + return p + + +def bullet(text, level=0, bold_prefix=None): + p = doc.add_paragraph(style="List Bullet" if level == 0 else "List Bullet 2") + p.paragraph_format.space_after = Pt(3) + if bold_prefix: + r = p.add_run(bold_prefix) + r.font.bold = True + r.font.size = Pt(10.5) + r.font.color.rgb = INK + r = p.add_run(text) + r.font.size = Pt(10.5) + r.font.color.rgb = INK + return p + + +def image(name, width=6.3, caption=None): + path = os.path.join(ASSETS, name) + doc.add_picture(path, width=Inches(width)) + doc.paragraphs[-1].alignment = WD_ALIGN_PARAGRAPH.CENTER + if caption: + p = doc.add_paragraph() + p.alignment = WD_ALIGN_PARAGRAPH.CENTER + r = p.add_run("Figure — " + caption) + r.font.size = Pt(9) + r.italic = True + r.font.color.rgb = GREY + p.paragraph_format.space_after = Pt(8) + + +def callout(title, text, color=BLUE, fill=BLUE_L): + """Encadré une-cellule.""" + t = doc.add_table(rows=1, cols=1) + t.alignment = WD_TABLE_ALIGNMENT.CENTER + cell = t.rows[0].cells[0] + _shade(cell, fill) + cell.width = Inches(6.4) + p = cell.paragraphs[0] + p.paragraph_format.space_after = Pt(2) + r = p.add_run(title + " ") + r.font.bold = True + r.font.size = Pt(10) + r.font.color.rgb = color + r2 = p.add_run(text) + r2.font.size = Pt(9.5) + r2.font.color.rgb = INK + doc.add_paragraph().paragraph_format.space_after = Pt(4) + return t + + +def table(headers, rows, widths=None, header_fill="004682", zebra=True, size=9.3, + fonts_bold_col0=False): + t = doc.add_table(rows=1, cols=len(headers)) + t.style = "Table Grid" + t.alignment = WD_TABLE_ALIGNMENT.CENTER + hdr = t.rows[0].cells + for i, htxt in enumerate(headers): + _set_cell_text(hdr[i], htxt, bold=True, white=True, size=size, + align=WD_ALIGN_PARAGRAPH.CENTER) + _shade(hdr[i], header_fill) + for ri, row in enumerate(rows): + cells = t.add_row().cells + for ci, val in enumerate(row): + _set_cell_text(cells[ci], str(val), size=size, + bold=(fonts_bold_col0 and ci == 0)) + if zebra and ri % 2 == 1: + _shade(cells[ci], GREY_L) + if widths: + for row in t.rows: + for i, w in enumerate(widths): + row.cells[i].width = Inches(w) + doc.add_paragraph().paragraph_format.space_after = Pt(2) + return t + + +def page_break(): + doc.add_page_break() + + +def add_toc(): + p = doc.add_paragraph() + run = p.add_run() + fldSimple = OxmlElement("w:fldSimple") + fldSimple.set(qn("w:instr"), r'TOC \o "1-3" \h \z \u') + r = OxmlElement("w:r") + t = OxmlElement("w:t") + t.text = "Faites un clic droit ici puis « Mettre à jour les champs » pour générer le sommaire." + r.append(t) + fldSimple.append(r) + p._p.append(fldSimple) + + +# ══════════════════════════════════════════════════════════════════ +# PAGE DE GARDE +# ══════════════════════════════════════════════════════════════════ +for _ in range(3): + doc.add_paragraph() +p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.CENTER +r = p.add_run("CESIZen"); r.font.size = Pt(46); r.font.bold = True; r.font.color.rgb = GREEN_V +p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.CENTER +r = p.add_run("L'application de votre santé mentale"); r.font.size = Pt(16); r.font.color.rgb = GREY +doc.add_paragraph() +p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.CENTER +r = p.add_run("Dossier — Activité 3"); r.font.size = Pt(22); r.font.bold = True; r.font.color.rgb = BLUE +p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.CENTER +r = p.add_run("Déployer et sécuriser les applications informatiques") +r.font.size = Pt(15); r.italic = True; r.font.color.rgb = INK +for _ in range(6): + doc.add_paragraph() +for line, sz, col, bold in [ + ("Plan de déploiement · Plan de maintenance · Plan de sécurisation", 12, GREEN, True), + ("", 6, INK, False), + ("Adam Marzuk — CESI École d'Ingénieurs", 12, INK, False), + ("Titre Concepteur Développeur d'Applications (CDA)", 11, GREY, False), + ("Bloc INFCDAAL3 — Commanditaire : Ministère des Solidarités et de la Santé", 11, GREY, False), +]: + p = doc.add_paragraph(); p.alignment = WD_ALIGN_PARAGRAPH.CENTER + if line: + r = p.add_run(line); r.font.size = Pt(sz); r.font.color.rgb = col; r.font.bold = bold +page_break() + +# ══════════════════════════════════════════════════════════════════ +# SOMMAIRE +# ══════════════════════════════════════════════════════════════════ +h1("Sommaire") +add_toc() +page_break() + +# ══════════════════════════════════════════════════════════════════ +# 1. INTRODUCTION +# ══════════════════════════════════════════════════════════════════ +h1("1. Introduction") +para("CESIZen est une plateforme grand public de santé mentale simulée pour le Ministère des " + "Solidarités et de la Santé. Elle aide chacun à mieux comprendre sa santé mentale et à agir sur " + "son stress à travers trois modules : la gestion des comptes utilisateurs, la diffusion " + "d'informations et un tracker d'émotions. La solution repose sur une API REST Laravel, un site " + "web Next.js et une application mobile React Native, adossés à une base PostgreSQL.") +para("Après la conception (Activité 1) et le développement/test du prototype (Activité 2), la présente " + "Activité 3 traite du déploiement et de la sécurisation de l'application. Ce dossier présente les " + "trois livrables attendus :") +bullet("l'architecture des environnements, le versioning et les automatisations de déploiement ;", + bold_prefix="Un plan de déploiement — ") +bullet("un outil de ticketing configuré et la méthodologie de traitement des incidents et évolutions ;", + bold_prefix="Un plan de maintenance — ") +bullet("l'analyse des vulnérabilités, les actions préventives/correctives, la protection des données " + "personnelles et la gestion de crise.", bold_prefix="Un plan de sécurisation — ") +para("Les outils de versioning, d'intégration continue et de ticketing retenus (Gitea et Coolify) sont " + "réellement configurés et font l'objet d'une démonstration lors de la soutenance.") + +callout("Périmètre.", "Un environnement de déploiement (la production) est réellement mis en place et " + "accessible en ligne sur cesizen.cleanows.fr. Les trois modules du prototype (Comptes, " + "Informations, Tracker d'émotions) y sont déployés.") + +# ══════════════════════════════════════════════════════════════════ +# 2. PLAN DE DÉPLOIEMENT +# ══════════════════════════════════════════════════════════════════ +h1("2. Plan de déploiement") + +h2("2.1 Architecture technique de la solution") +para("L'application suit une architecture découplée : une API REST centrale expose la logique métier et " + "les données, consommée par deux clients distincts (web et mobile). Ce découpage garantit la " + "non-duplication de la logique et facilite l'ajout de nouveaux clients.") +table( + ["Composant", "Technologie", "Rôle"], + [ + ["API / Back-end", "Laravel 12 (PHP 8.4)", "API REST JSON, authentification JWT, logique métier"], + ["Base de données", "PostgreSQL 16", "Persistance ACID des données"], + ["Cache / file (dev)", "Redis 7", "Cache, sessions, files d'attente (environnement de dev)"], + ["Front-end web", "Next.js 16 / React 19", "Site public et back-office, design system DSFR de l'État"], + ["Application mobile", "React Native / Expo 54", "Application iOS & Android (Mobile First)"], + ["Conteneurisation", "Docker / Docker Compose", "Isolation et reproductibilité des environnements"], + ["Plateforme de déploiement", "Coolify (PaaS auto-hébergé)", "Build, orchestration, HTTPS, supervision"], + ], + widths=[1.7, 1.9, 2.8], +) +para("En production, le cache, les sessions et les files d'attente basculent sur la base de données " + "(pilote « database »), ce qui évite d'exploiter Redis et simplifie l'infrastructure.", size=9.7, + italic=True, color=GREY) + +h2("2.2 Les trois environnements") +para("Le cycle de vie du logiciel s'appuie sur trois environnements distincts, du poste du développeur " + "jusqu'à la production exposée aux utilisateurs.") +table( + ["", "Développement", "Test / Intégration", "Production"], + [ + ["Objectif", "Coder et tester en local", "Valider automatiquement chaque modification", "Servir les utilisateurs finaux"], + ["Hébergement", "Poste du développeur", "Runner Gitea Actions (CI)", "Serveur Coolify (VPS)"], + ["Orchestration", "docker-compose.yml", "Conteneur de CI éphémère", "docker-compose.prod.yaml"], + ["Base de données", "PostgreSQL 16 + Redis", "SQLite en mémoire", "PostgreSQL 16 (réseau interne)"], + ["Configuration", "APP_DEBUG=true, secrets en dur (non sensibles)", "APP_ENV=testing", "APP_DEBUG=false, secrets générés hors dépôt"], + ["Accès", "localhost:3000 / :8000", "Journaux de la CI", "HTTPS : cesizen.cleanows.fr"], + ], + widths=[1.3, 1.85, 1.7, 1.9], size=8.8, +) + +h2("2.3 Conteneurisation et images Docker") +para("Chaque service est livré sous forme d'image Docker, garantissant un fonctionnement identique " + "d'un environnement à l'autre. Les images de développement privilégient le rechargement à chaud ; " + "les images de production sont optimisées.") +bullet("l'image back-end (php:8.4-cli) installe les extensions PostgreSQL/bcmath et n'embarque que les " + "dépendances de production (composer install --no-dev) ;", bold_prefix="Back-end — ") +bullet("l'image front-end utilise un build multi-étapes (builder → runner) et la sortie « standalone » " + "de Next.js, produisant une image d'exécution minimale ;", bold_prefix="Front-end — ") +bullet("un script d'entrée (entrypoint.sh) automatise au démarrage la génération des clés (APP_KEY, " + "JWT_SECRET), les migrations, le seed idempotent et la mise en cache des routes/vues.", + bold_prefix="Démarrage — ") + +h2("2.4 Automatisations et intégration continue") +para("Deux automatisations complémentaires structurent la chaîne de livraison :") +bullet("à chaque push ou pull request, le pipeline .gitea/workflows/ci.yml exécute les 29 tests " + "automatisés (19 back-end PHPUnit, 6 web Vitest, 4 mobile Jest) et les contrôles de style " + "(Laravel Pint, ESLint). Un changement dont les tests échouent ne peut pas être livré.", + bold_prefix="Intégration continue (Gitea Actions) — ") +bullet("un merge sur la branche main déclenche, via un webhook, la reconstruction des images et le " + "redéploiement par Coolify, avec migrations et seed joués automatiquement, health checks et " + "redémarrage automatique des conteneurs.", bold_prefix="Déploiement continu (Coolify) — ") +image("archi_deploiement.png", width=6.4, + caption="Chaîne de déploiement : trois environnements et flux d'intégration/livraison continues.") + +h2("2.5 Étapes de déploiement et ressources") +para("Le déploiement en production se déroule selon les étapes suivantes, entièrement automatisées " + "après le déclenchement :") +table( + ["Étape", "Action", "Automatisation"], + [ + ["1. Récupération", "Coolify clone la révision de main", "Webhook Gitea → Coolify"], + ["2. Build", "Construction des images back & front", "Dockerfile.prod (multi-étapes)"], + ["3. Secrets", "Génération APP_KEY / JWT_SECRET si absents", "entrypoint.sh"], + ["4. Base de données", "Migrations + seed idempotent", "artisan migrate --force / db:seed"], + ["5. Démarrage", "Lancement des conteneurs + health checks", "docker-compose.prod.yaml"], + ["6. Exposition", "Routage HTTPS/TLS et certificat", "Reverse proxy Coolify"], + ["7. Supervision", "Vérification /up et redémarrage auto", "healthcheck + restart: unless-stopped"], + ], + widths=[1.4, 2.6, 2.4], size=9, +) +para("Ressources et dimensionnement — l'application grand public est dimensionnée pour un VPS unique " + "(2 vCPU, 4 Go de RAM, 40 Go SSD) hébergeant les trois conteneurs, suffisant pour la phase de " + "test de 12 mois. La montée en charge se fait horizontalement (réplication des conteneurs back " + "et front derrière le reverse proxy, base de données managée) sans refonte, l'API étant stateless.") +page_break() + +# ══════════════════════════════════════════════════════════════════ +# 3. VERSIONING +# ══════════════════════════════════════════════════════════════════ +h1("3. Gestion des sources et versioning") +h2("3.1 Outil retenu : Gitea") +para("Le versioning s'appuie sur Git, hébergé sur une instance Gitea auto-hébergée. Gitea a été retenu " + "car il réunit dans un seul outil, souverain et gratuit, le dépôt Git, le suivi des tickets, " + "l'intégration continue (Gitea Actions) et la gestion des versions — cohérent avec un projet porté " + "par un ministère (hébergement maîtrisé, pas de dépendance à un service tiers hors UE).") +table( + ["Critère", "Gitea", "GitHub", "GitLab"], + [ + ["Souveraineté / auto-hébergement", "Oui (léger)", "Non (SaaS US)", "Oui (lourd)"], + ["Ticketing intégré", "Oui", "Oui", "Oui"], + ["CI/CD intégrée", "Oui (Actions)", "Oui", "Oui"], + ["Coût", "Gratuit", "Gratuit/payant", "Gratuit/payant"], + ["Ressources serveur", "Faibles", "—", "Élevées"], + ], + widths=[2.4, 1.4, 1.4, 1.4], size=9, +) + +h2("3.2 Stratégie de branches et conventions") +bullet("main : code en production, toujours déployable et protégé (merge via pull request + CI verte) ;") +bullet("develop : intégration des développements en cours ;") +bullet("feature/*, fix/*, hotfix/* : une branche par fonctionnalité ou correctif, fusionnée par PR.") +para("Les messages de commit suivent la convention Conventional Commits déjà appliquée sur le projet " + "(feat, fix, docs, chore, refactor…) avec un scope (back, web, mobile, deploy), ce qui rend " + "l'historique lisible et permet de générer automatiquement les notes de version. Les versions " + "livrées sont marquées par des tags (v1.0, v1.1…) reliés aux jalons Gitea.") + +# ══════════════════════════════════════════════════════════════════ +# 4. PLAN DE MAINTENANCE +# ══════════════════════════════════════════════════════════════════ +h1("4. Plan de maintenance") +h2("4.1 Outil de gestion des corrections et évolutions") +para("Le suivi des demandes s'appuie sur le module Issues de Gitea, complété par un tableau de bord " + "Kanban (Projects) et des jalons (Milestones). Deux gabarits d'issues versionnés dans le dépôt " + "structurent la saisie et posent automatiquement les bons libellés :") +bullet("gabarit incident (maintenance corrective) : sévérité, module, environnement, étapes de " + "reproduction, résultat attendu/obtenu ;", bold_prefix="🐞 ") +bullet("gabarit demande d'évolution (maintenance évolutive) : besoin, solution proposée, priorité, " + "critères d'acceptation.", bold_prefix="✨ ") +para("Les libellés couvrent le type, la gravité, la priorité, le statut et le module ; les jalons " + "correspondent aux versions. Le tableau Kanban (À qualifier → À faire → En cours → En revue → " + "Résolu) offre une vision temps réel de l'avancement, partagée entre le prestataire et le client.") + +h2("4.2 Maintenance corrective : méthodologie et engagements de service") +para("La maintenance corrective démarre dès la prise en compte d'un ticket d'incident. La sévérité " + "conditionne les délais contractuels de prise en compte et de correction, distinguant incident " + "bloquant, majeur et mineur.") +table( + ["Sévérité", "Définition", "Prise en compte", "Correction"], + [ + ["Bloquant — critique", "Service inutilisable, aucun contournement", "1 h ouvrée", "3 h ouvrées"], + ["Bloquant — fort", "Service inutilisable, contournement partiel", "2 h ouvrées", "6 h ouvrées"], + ["Majeur", "Service dégradé / intermittent avec contournement", "7 h ouvrées", "16 h ouvrées"], + ["Mineur (par lots)", "Altération mineure, service opérationnel", "1 j ouvré", "40 h ouvrées"], + ], + widths=[1.5, 2.5, 1.2, 1.2], size=9, +) +para("Le traitement suit un cycle standardisé et traçable, du signalement à la clôture validée par le " + "client, en passant par la correction sur une branche dédiée et la revue par pull request " + "(fusion conditionnée à une CI verte).") +image("cycle_ticket.png", width=6.4, + caption="Cycle de vie d'un ticket sur Gitea et rappel des engagements de service (SLA).") + +h2("4.3 Maintenance évolutive") +para("Chaque demande d'évolution fait l'objet d'un ticket puis d'une analyse par le prestataire " + "proposant : la documentation fonctionnelle et technique, le délai de mise en œuvre et le coût " + "associé. Après validation du client, l'évolution est planifiée dans un jalon de version. " + "L'architecture découplée et le référentiel de données extensible permettent d'activer les " + "modules écartés (Diagnostic de stress, Cohérence cardiaque, Activités de détente) sans réécrire " + "le modèle existant.") + +h2("4.4 Pilotage") +para("Le pilotage de la maintenance s'appuie sur les tableaux de bord natifs de Gitea : filtres par " + "libellé/jalon/assigné, vue Kanban et suivi d'avancement par version. Ces indicateurs (nombre de " + "tickets ouverts/fermés, respect des délais, répartition par module) permettent au Ministère de " + "suivre la qualité de service.") + +h2("4.5 Veille technologique et pérennité") +para("La pérennité de l'application dépend du suivi des évolutions de son écosystème. Une méthodologie " + "de veille structurée est mise en place :") +table( + ["Axe de veille", "Sources", "Rythme", "Action"], + [ + ["Sécurité / vulnérabilités", "CVE, bulletins CERT-FR, GitHub Advisories, OWASP", + "Continu (automatisé)", "Alertes de dépendances + correctifs prioritaires"], + ["Frameworks (Laravel, Next.js, Expo)", "Blogs officiels, notes de version, roadmaps", + "Mensuel", "Planifier les montées de version LTS"], + ["Dépendances", "npm audit, composer audit, Renovate/Dependabot", + "Hebdomadaire", "PR automatiques de mise à jour, testées par la CI"], + ["Réglementation (RGPD, HDS)", "CNIL, ANSSI, Legifrance", + "Trimestriel", "Adapter les mesures de conformité"], + ], + widths=[1.9, 2.3, 1.1, 1.9], size=8.7, +) +para("La veille sécurité est en partie automatisée : les outils d'analyse de dépendances (composer " + "audit, npm audit) sont intégrables à la CI et signalent immédiatement toute dépendance " + "vulnérable, tandis qu'un robot de mise à jour propose des pull requests validées par les tests.") +page_break() + +# ══════════════════════════════════════════════════════════════════ +# 5. PLAN DE SÉCURISATION +# ══════════════════════════════════════════════════════════════════ +h1("5. Plan de sécurisation") +para("S'agissant d'une application portée par un ministère et manipulant des données de santé mentale " + "(données sensibles au sens de l'article 9 du RGPD), la sécurité est traitée comme une exigence de " + "premier ordre. Ce plan identifie les vulnérabilités et risques, en évalue la criticité, puis " + "présente les mesures déjà en place et les actions correctives et préventives planifiées.") + +h2("5.1 Analyse des vulnérabilités") +para("L'analyse s'appuie sur le référentiel OWASP Top 10. Le tableau ci-dessous confronte chaque risque " + "aux mesures réellement implémentées dans le prototype et aux axes de renforcement.") +table( + ["Vulnérabilité (OWASP)", "Mesure en place", "Renforcement prévu"], + [ + ["A01 Contrôle d'accès défaillant", "RBAC par middleware (role:administrateur), garde-fous (un admin ne peut se désactiver/supprimer)", "Revue périodique des droits"], + ["A02 Défaillances cryptographiques", "HTTPS/TLS, bcrypt (coût 12), mots de passe masqués et exclus des logs", "Chiffrement applicatif au repos des notes de santé (cast chiffré)"], + ["A03 Injection", "ORM Eloquent (requêtes paramétrées), Form Requests de validation", "Purification/échappement HTML côté API"], + ["A05 Mauvaise configuration", "APP_DEBUG=false en prod, secrets hors dépôt, CORS en liste blanche", "En-têtes de sécurité HSTS/CSP/X-Frame-Options"], + ["A07 Authentification défaillante", "JWT signé (HS256), TTL 60 min, blacklist au logout, rate limiting", "Politique de mot de passe renforcée (complexité), vérification d'e-mail, MFA admin"], + ["A09 Journalisation insuffisante", "Piste d'audit immuable (IP, ancienne/nouvelle valeur)", "Centralisation des logs + alerting (supervision)"], + ], + widths=[2.1, 2.6, 1.7], size=8.6, +) + +h2("5.2 Matrice de criticité des risques") +para("Chaque risque est positionné selon sa gravité et sa probabilité ; la criticité est le produit des " + "deux (échelle de 1 à 25). Cette hiérarchisation oriente la priorité des actions.") +image("matrice_risques.png", width=6.2, + caption="Matrice de criticité des risques du projet CESIZen.") +table( + ["Réf.", "Risque", "Gravité", "Prob.", "Criticité", "Priorité"], + [ + ["R1", "Fuite de données de santé (art. 9)", "Critique (4)", "Faible (2)", "8", "Élevée"], + ["R7", "Absence de chiffrement au repos", "Critique (4)", "Moyenne (3)", "12", "Élevée"], + ["R2", "Injection SQL / XSS", "Grave (3)", "Moyenne (3)", "9", "Élevée"], + ["R3", "Compromission d'un compte admin", "Grave (3)", "Faible (2)", "6", "Moyenne"], + ["R6", "Dépendances vulnérables", "Modérée (2)", "Forte (4)", "8", "Moyenne"], + ["R4", "DDoS / indisponibilité", "Modérée (2)", "Moyenne (3)", "6", "Moyenne"], + ["R5", "Perte de données (sauvegarde)", "Modérée (2)", "Faible (2)", "4", "Faible"], + ["R8", "Erreur humaine de déploiement", "Mineure (1)", "Faible (2)", "2", "Faible"], + ], + widths=[0.5, 2.5, 1.1, 1.0, 0.9, 0.9], size=8.7, +) + +h2("5.3 Actions préventives et correctives") +para("Pour chaque risque, des actions préventives (réduire la probabilité) et correctives (limiter " + "l'impact) sont définies, couvrant l'ensemble des risques critiques et majeurs identifiés.") +table( + ["Réf.", "Actions préventives", "Actions correctives"], + [ + ["R1", "Minimisation des données, chiffrement en transit, RBAC, audit des accès, hébergement UE/HDS", + "Procédure de notification CNIL sous 72 h, isolation, révocation des accès, information des personnes"], + ["R7", "Chiffrement applicatif des champs sensibles (note), chiffrement du volume PostgreSQL", + "Rotation des clés, restauration depuis sauvegarde chiffrée"], + ["R2", "Validation systématique (Form Requests), ORM paramétré, échappement des sorties", + "Correctif prioritaire, revue de code, test de non-régression"], + ["R3", "MFA administrateur, mots de passe robustes, rate limiting, journalisation des actions", + "Blacklist du token, réinitialisation, analyse de la piste d'audit"], + ["R6", "composer/npm audit en CI, robot de mise à jour, veille CVE", + "Application du correctif, redéploiement automatisé"], + ["R4", "Reverse proxy, rate limiting, health checks + redémarrage auto", + "Mise à l'échelle, activation d'une protection anti-DDoS (CDN/WAF)"], + ["R5", "Sauvegardes quotidiennes automatisées et testées, réplication du volume", + "Restauration point-in-time, plan de reprise d'activité"], + ], + widths=[0.5, 3.0, 3.0], size=8.4, +) + +h2("5.4 Chiffrement et protection des échanges") +bullet("l'ensemble des échanges entre les clients et l'API passe par HTTPS/TLS (terminaison au niveau " + "du reverse proxy Coolify), interdisant l'interception en clair ;", bold_prefix="En transit — ") +bullet("les mots de passe sont hachés avec bcrypt (coût 12) et ne sont jamais renvoyés ni journalisés ; " + "la clé applicative (APP_KEY, AES-256) protège cookies et données chiffrées. Le chiffrement " + "applicatif des notes de santé (champ le plus sensible) est planifié via le cast chiffré de " + "Laravel ;", bold_prefix="Au repos — ") +bullet("l'authentification repose sur des jetons JWT signés (HS256), d'une durée de vie de 60 minutes, " + "invalidés à la déconnexion (liste noire).", bold_prefix="Jetons — ") + +h2("5.5 Données personnelles et conformité RGPD") +para("Les données de santé mentale (émotions, intensité, notes) constituent des données sensibles " + "(art. 9 RGPD). La conformité est prise en compte par conception :") +table( + ["Exigence RGPD", "Mise en œuvre dans CESIZen"], + [ + ["Base légale / consentement", "Consentement explicite obligatoire à l'inscription (non pré-coché), tracé en base"], + ["Minimisation", "Seules les données nécessaires au service sont collectées"], + ["Droit d'accès et portabilité (art. 15/20)", "Export JSON téléchargeable de toutes les données de l'utilisateur"], + ["Droit à l'effacement (art. 17)", "Anonymisation (dépersonnalisation des notes de santé) et suppression logique (soft delete)"], + ["Traçabilité / responsabilité", "Piste d'audit immuable (action, IP, anciennes/nouvelles valeurs, hors mot de passe)"], + ["Localisation", "Hébergement dans l'Union européenne, aucun transfert hors UE ; cible HDS pour la prod"], + ["Sécurité (art. 32)", "Chiffrement en transit, hachage, contrôle d'accès, journalisation"], + ], + widths=[2.3, 4.1], size=9, +) + +h2("5.6 Gestion de crise et escalade d'information") +para("En cas d'incident de sécurité avéré (intrusion, fuite de données, indisponibilité majeure), une " + "procédure de gestion de crise organise la réaction et la remontée d'information selon des niveaux " + "d'escalade définis.") +table( + ["Niveau", "Déclencheur", "Responsable", "Actions & communication"], + [ + ["N1 — Détection", "Alerte supervision / signalement", "Astreinte technique", "Qualifier, ouvrir un ticket incident de sécurité, contenir"], + ["N2 — Confirmation", "Incident de sécurité confirmé", "Responsable technique", "Isoler, préserver les preuves (logs), évaluer l'impact"], + ["N3 — Crise", "Fuite de données personnelles avérée", "Chef de projet + DPO", "Notification CNIL sous 72 h, information des personnes concernées"], + ["N4 — Direction", "Impact majeur / médiatique", "Direction + Ministère", "Communication officielle, coordination ANSSI si nécessaire"], + ], + widths=[1.3, 1.9, 1.4, 1.8], size=8.6, +) +para("La communication distingue la remontée interne (technique → chef de projet → direction) et la " + "communication externe (personnes concernées, CNIL sous 72 h conformément à l'art. 33 RGPD, " + "éventuellement ANSSI). Chaque crise donne lieu à un retour d'expérience et à des actions " + "préventives pour éviter la récurrence.") +page_break() + +# ══════════════════════════════════════════════════════════════════ +# 6. BONNES PRATIQUES +# ══════════════════════════════════════════════════════════════════ +h1("6. Bonnes pratiques de développement") +para("La maintenabilité et la sécurité de l'application reposent sur des pratiques de développement " + "structurées, appliquées tout au long du projet :") +bullet("architecture MVC côté back-end (Modèles, Contrôleurs fins, Form Requests de validation, " + "couche Service pour les rapports) ; App Router, hooks personnalisés et store côté front.", + bold_prefix="Structure du code — ") +bullet("respect du PSR-12 vérifié par Laravel Pint ; TypeScript strict et ESLint côté web et mobile ; " + "contrôles intégrés à la CI.", bold_prefix="Normes et style — ") +bullet("29 tests automatisés sur les trois couches (PHPUnit, Vitest, Jest), exécutés à chaque push, " + "empêchant toute régression de partir en production.", bold_prefix="Tests — ") +bullet("Conventional Commits, revue systématique par pull request, historique lisible.", + bold_prefix="Gestion de versions — ") +bullet("README, guide de lancement, documentation d'API commentée et guide de migration/outillage " + "(MIGRATION_GITEA.md).", bold_prefix="Documentation — ") + +h1("7. Conclusion") +para("Le projet CESIZen dispose d'une chaîne de déploiement complète et outillée : trois environnements " + "clairement séparés, un versioning souverain sous Gitea, une intégration continue exécutant " + "29 tests à chaque modification et un déploiement continu automatisé vers une production réellement " + "en ligne et sécurisée par HTTPS. La maintenance est cadrée par un outil de ticketing configuré, " + "des engagements de service précis et une veille technologique méthodique. La sécurisation, enfin, " + "repose sur une analyse de risques hiérarchisée, des mesures déjà implémentées (authentification " + "JWT, RBAC, audit, RGPD) et un plan d'actions correctives et préventives, complété par une " + "procédure de gestion de crise. L'application est ainsi prête à être exploitée et à évoluer dans " + "la durée au service de la santé mentale du grand public.") + +# ─── Pied de page ───────────────────────────────────────────────── +section = doc.sections[0] +footer = section.footer +fp = footer.paragraphs[0] +fp.alignment = WD_ALIGN_PARAGRAPH.CENTER +fr = fp.add_run("CESIZen · Bloc INFCDAAL3 — Déployer et sécuriser les applications informatiques · Adam Marzuk") +fr.font.size = Pt(8) +fr.font.color.rgb = GREY + +doc.save(OUT) +print("Dossier généré :", OUT) +print("Paragraphes :", len(doc.paragraphs), "· Tableaux :", len(doc.tables)) diff --git a/generate_slides_bloc3.py b/generate_slides_bloc3.py new file mode 100644 index 0000000..eaa8311 --- /dev/null +++ b/generate_slides_bloc3.py @@ -0,0 +1,417 @@ +#!/usr/bin/env python3 +""" +Génère le support de soutenance CESIZen — Activité 3 « Déployer et sécuriser » +(PowerPoint 16:9 éditable). Réutilise la charte du support du Bloc 1. +Sortie : ./CESIZen_Soutenance_Bloc3.pptx +Schémas : ./assets_bloc3/*.png +""" +import os +from PIL import Image +from pptx import Presentation +from pptx.util import Inches, Pt +from pptx.dml.color import RGBColor +from pptx.enum.text import PP_ALIGN, MSO_ANCHOR +from pptx.enum.shapes import MSO_SHAPE + +# ─── Charte CESIZen ──────────────────────────────────────────────── +GREEN = RGBColor(6, 198, 86) +GREEN_D = RGBColor(4, 150, 65) +BLUE = RGBColor(0, 70, 130) +BLUE_L = RGBColor(230, 240, 250) +YELLOW = RGBColor(252, 225, 23) +ORANGE = RGBColor(228, 128, 28) +RED = RGBColor(200, 16, 46) +GREY_D = RGBColor(50, 50, 50) +GREY = RGBColor(110, 110, 110) +WHITE = RGBColor(255, 255, 255) +INK = RGBColor(35, 35, 45) + +HERE = os.path.dirname(os.path.abspath(__file__)) +ASSETS = os.path.join(HERE, 'assets_bloc3') +OUT = os.path.join(HERE, 'CESIZen_Soutenance_Bloc3.pptx') + +prs = Presentation() +prs.slide_width = Inches(13.333) +prs.slide_height = Inches(7.5) +SW, SH = prs.slide_width, prs.slide_height +BLANK = prs.slide_layouts[6] + + +def _no_line(shape): + shape.line.fill.background() + + +def rect(slide, x, y, w, h, color, line=None): + s = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, x, y, w, h) + s.fill.solid() + s.fill.fore_color.rgb = color + if line is None: + _no_line(s) + else: + s.line.color.rgb = line + s.line.width = Pt(1) + s.shadow.inherit = False + return s + + +def textbox(slide, x, y, w, h, anchor=MSO_ANCHOR.TOP): + tb = slide.shapes.add_textbox(x, y, w, h) + tf = tb.text_frame + tf.word_wrap = True + tf.vertical_anchor = anchor + tf.margin_left = Inches(0.05) + tf.margin_right = Inches(0.05) + tf.margin_top = Inches(0.02) + tf.margin_bottom = Inches(0.02) + return tb, tf + + +def set_par(p, text, size, color, bold=False, align=PP_ALIGN.LEFT, + font='Arial', space_after=8): + p.text = text + p.alignment = align + p.space_after = Pt(space_after) + for r in p.runs: + r.font.size = Pt(size) + r.font.color.rgb = color + r.font.bold = bold + r.font.name = font + + +def footer(slide): + tb, tf = textbox(slide, Inches(0.4), Inches(7.05), Inches(12.5), Inches(0.35)) + set_par(tf.paragraphs[0], + "CESIZen · Bloc INFCDAAL3 — Déployer et sécuriser les applications informatiques · Adam Marzuk", + 9, GREY, align=PP_ALIGN.CENTER, space_after=0) + + +def band_title(slide, text, num=None): + rect(slide, 0, 0, SW, Inches(1.05), BLUE) + rect(slide, 0, Inches(1.05), SW, Inches(0.06), GREEN) + label = f"{num}. {text}" if num else text + tb, tf = textbox(slide, Inches(0.5), Inches(0.12), Inches(12.3), Inches(0.85), + anchor=MSO_ANCHOR.MIDDLE) + set_par(tf.paragraphs[0], label, 28, WHITE, bold=True, space_after=0) + + +def bullets(slide, items, x, y, w, h, size=18, gap=10): + tb, tf = textbox(slide, x, y, w, h) + for i, it in enumerate(items): + lvl = 0 + txt = it + if isinstance(it, tuple): + txt, lvl = it + p = tf.paragraphs[0] if i == 0 else tf.add_paragraph() + prefix = "• " if lvl == 0 else "– " + set_par(p, prefix + txt, size if lvl == 0 else size - 2, + INK if lvl == 0 else GREY_D, bold=False, space_after=gap) + p.level = lvl + if lvl == 1: + p.runs[0].font.color.rgb = GREY + return tb + + +def image_fit(slide, name, x, y, max_w, max_h, caption=None): + path = os.path.join(ASSETS, name) + iw, ih = Image.open(path).size + ar = iw / ih + box_ar = max_w / max_h + if box_ar > ar: + h = max_h + w = int(h * ar) + else: + w = max_w + h = int(w / ar) + left = x + (max_w - w) // 2 + top = y + (max_h - h) // 2 + slide.shapes.add_picture(path, left, top, width=w, height=h) + if caption: + tb, tf = textbox(slide, x, y + max_h + Inches(0.02), max_w, Inches(0.35)) + set_par(tf.paragraphs[0], caption, 11, GREY, align=PP_ALIGN.CENTER, space_after=0) + + +def cards(slide, items, y, h=1.8, top=0.55): + """items = list of (titre, description, couleur)""" + n = len(items) + total_w = 12.0 + gap = 0.25 + cw = (total_w - gap * (n - 1)) / n + for i, (t, d, col) in enumerate(items): + x = Inches(0.7 + i * (cw + gap)) + rect(slide, x, Inches(y), Inches(cw), Inches(h), BLUE_L) + rect(slide, x, Inches(y), Inches(cw), Inches(top), col) + tb, tf = textbox(slide, x + Inches(0.1), Inches(y + 0.03), Inches(cw - 0.2), + Inches(top), MSO_ANCHOR.MIDDLE) + set_par(tf.paragraphs[0], t, 13, WHITE, bold=True, space_after=0, align=PP_ALIGN.CENTER) + tb, tf = textbox(slide, x + Inches(0.15), Inches(y + top + 0.1), Inches(cw - 0.3), + Inches(h - top - 0.15)) + for j, line in enumerate(d): + p = tf.paragraphs[0] if j == 0 else tf.add_paragraph() + set_par(p, "• " + line, 11.5, INK, space_after=5) + + +# ════════════════════════════════════════════════════════════════════ +# Slide 1 — Titre +# ════════════════════════════════════════════════════════════════════ +s = prs.slides.add_slide(BLANK) +rect(s, 0, 0, SW, SH, WHITE) +rect(s, 0, Inches(2.5), SW, Inches(0.08), GREEN) +tb, tf = textbox(s, Inches(1), Inches(0.9), Inches(11.3), Inches(1.5)) +set_par(tf.paragraphs[0], "CESIZen", 60, GREEN, bold=True, align=PP_ALIGN.CENTER, space_after=0) +tb, tf = textbox(s, Inches(1), Inches(2.62), Inches(11.3), Inches(0.9)) +set_par(tf.paragraphs[0], "L'application de votre santé mentale", 24, GREY_D, + align=PP_ALIGN.CENTER, space_after=0) +tb, tf = textbox(s, Inches(1), Inches(3.65), Inches(11.3), Inches(0.9)) +set_par(tf.paragraphs[0], "Activité 3 — Déployer et sécuriser les applications informatiques", + 18, BLUE, align=PP_ALIGN.CENTER, space_after=0) +tb, tf = textbox(s, Inches(1), Inches(5.4), Inches(11.3), Inches(1.4)) +for i, line in enumerate([ + "Adam Marzuk — CESI École d'Ingénieurs", + "Bloc INFCDAAL3 — Déployer et sécuriser les applications informatiques", + "Commanditaire : Ministère des Solidarités et de la Santé"]): + p = tf.paragraphs[0] if i == 0 else tf.add_paragraph() + set_par(p, line, 15, GREY, align=PP_ALIGN.CENTER, space_after=4) + +# ════════════════════════════════════════════════════════════════════ +# Slide 2 — Sommaire +# ════════════════════════════════════════════════════════════════════ +s = prs.slides.add_slide(BLANK) +band_title(s, "Sommaire") +bullets(s, [ + "Contexte et périmètre de l'Activité 3", + "Plan de déploiement : architecture et environnements", + "Versioning et intégration/livraison continues (Gitea · Coolify)", + "Plan de maintenance : ticketing, engagements de service, veille", + "Plan de sécurisation : vulnérabilités, risques, actions", + "Données personnelles, RGPD et gestion de crise", + "Démonstration des outils et conclusion", +], Inches(1.2), Inches(1.5), Inches(11), Inches(5.2), size=22, gap=13) +footer(s) + +# ════════════════════════════════════════════════════════════════════ +# Slide 3 — Contexte & périmètre +# ════════════════════════════════════════════════════════════════════ +s = prs.slides.add_slide(BLANK) +band_title(s, "Contexte et périmètre de l'Activité 3", 1) +bullets(s, [ + "CESIZen : plateforme grand public de santé mentale (Ministère).", + "Après la conception (Act. 1) et le développement/test (Act. 2), l'Activité 3 traite du déploiement et de la sécurisation.", + "Prototype déployé : API Laravel + web Next.js + mobile Expo + PostgreSQL.", +], Inches(0.8), Inches(1.4), Inches(11.7), Inches(2.0), size=18) +tb, tf = textbox(s, Inches(0.8), Inches(3.7), Inches(11.7), Inches(0.5)) +set_par(tf.paragraphs[0], "3 livrables", 16, GREEN_D, bold=True, space_after=0) +cards(s, [ + ("Plan de déploiement", ["Environnements", "Versioning", "Automatisations"], GREEN), + ("Plan de maintenance", ["Ticketing Gitea", "SLA & méthodologie", "Veille techno"], BLUE), + ("Plan de sécurisation", ["Risques & criticité", "Actions correctives", "RGPD & crise"], GREEN_D), +], y=4.3, h=2.0) +footer(s) + +# ════════════════════════════════════════════════════════════════════ +# Slide 4 — Architecture technique +# ════════════════════════════════════════════════════════════════════ +s = prs.slides.add_slide(BLANK) +band_title(s, "Architecture technique de la solution", 2) +bullets(s, [ + "Architecture découplée : une API REST centrale, deux clients (web + mobile).", + ("Back-end : Laravel 12 (PHP 8.4) — API REST JSON, authentification JWT.", 1), + ("Base de données : PostgreSQL 16 (ACID) ; Redis en développement.", 1), + ("Front-end web : Next.js 16 / React 19, design system de l'État (DSFR).", 1), + ("Mobile : React Native / Expo 54 (iOS + Android, Mobile First).", 1), + "Conteneurisation Docker : environnements reproductibles et isolés.", + "Une seule logique métier → non-duplication, testabilité, évolutivité.", +], Inches(0.8), Inches(1.5), Inches(11.7), Inches(5.0), size=17, gap=9) +footer(s) + +# ════════════════════════════════════════════════════════════════════ +# Slide 5 — Plan de déploiement : 3 environnements + chaîne +# ════════════════════════════════════════════════════════════════════ +s = prs.slides.add_slide(BLANK) +band_title(s, "Plan de déploiement — environnements et chaîne CI/CD", 2) +image_fit(s, "archi_deploiement.png", Inches(0.5), Inches(1.25), Inches(12.3), Inches(5.5), + caption="Développement (local) → Test/Intégration (Gitea Actions) → Production (Coolify, HTTPS)") +footer(s) + +# ════════════════════════════════════════════════════════════════════ +# Slide 6 — Versioning & CI/CD +# ════════════════════════════════════════════════════════════════════ +s = prs.slides.add_slide(BLANK) +band_title(s, "Versioning et intégration/livraison continues", 3) +bullets(s, [ + "Gitea : dépôt Git, ticketing et CI/CD réunis dans un outil souverain auto-hébergé.", + "Stratégie de branches : main (prod, protégée) · develop · feature/fix/hotfix.", + "Conventions : Conventional Commits, revue par pull request, tags de version.", +], Inches(0.8), Inches(1.4), Inches(11.7), Inches(1.9), size=17) +tb, tf = textbox(s, Inches(0.8), Inches(3.55), Inches(11.7), Inches(0.5)) +set_par(tf.paragraphs[0], "Deux automatisations complémentaires", 16, GREEN_D, bold=True, space_after=0) +cards(s, [ + ("Intégration continue — Gitea Actions", [ + "29 tests à chaque push", + "Lint (Pint, ESLint)", + "Merge bloqué si CI rouge"], ORANGE), + ("Déploiement continu — Coolify", [ + "Webhook sur merge main", + "Build images + migrations", + "HTTPS, health checks, restart"], BLUE), +], y=4.15, h=2.1) +footer(s) + +# ════════════════════════════════════════════════════════════════════ +# Slide 7 — Maintenance : ticketing +# ════════════════════════════════════════════════════════════════════ +s = prs.slides.add_slide(BLANK) +band_title(s, "Plan de maintenance — ticketing et cycle de vie", 4) +image_fit(s, "cycle_ticket.png", Inches(0.5), Inches(1.25), Inches(12.3), Inches(5.5), + caption="Gitea Issues : gabarits incident/évolution, labels, jalons, Kanban · SLA du contrat") +footer(s) + +# ════════════════════════════════════════════════════════════════════ +# Slide 8 — Maintenance : méthodo + veille +# ════════════════════════════════════════════════════════════════════ +s = prs.slides.add_slide(BLANK) +band_title(s, "Maintenance corrective, évolutive et veille", 4) +tb, tf = textbox(s, Inches(0.8), Inches(1.3), Inches(11.7), Inches(0.5)) +set_par(tf.paragraphs[0], "Méthodologie", 16, GREEN_D, bold=True, space_after=0) +bullets(s, [ + "Corrective : ticket → qualification (gravité) → SLA → correction → revue (PR + CI) → clôture validée.", + "Évolutive : analyse (documentation, délai, coût) → validation client → planification par version.", + "Pilotage : tableaux de bord Gitea (Kanban, jalons, indicateurs de délais).", +], Inches(0.8), Inches(1.8), Inches(11.7), Inches(2.3), size=16, gap=9) +tb, tf = textbox(s, Inches(0.8), Inches(4.2), Inches(11.7), Inches(0.5)) +set_par(tf.paragraphs[0], "Veille technologique (pérennité)", 16, GREEN_D, bold=True, space_after=0) +bullets(s, [ + "Sécurité : CVE, CERT-FR, OWASP, alertes de dépendances (composer/npm audit en CI).", + "Frameworks & dépendances : notes de version, robot de mises à jour testées par la CI.", + "Réglementation : CNIL, ANSSI (RGPD, HDS).", +], Inches(0.8), Inches(4.7), Inches(11.7), Inches(1.9), size=16, gap=9) +footer(s) + +# ════════════════════════════════════════════════════════════════════ +# Slide 9 — Sécurisation : vulnérabilités OWASP +# ════════════════════════════════════════════════════════════════════ +s = prs.slides.add_slide(BLANK) +band_title(s, "Plan de sécurisation — vulnérabilités (OWASP)", 5) +bullets(s, [ + "Contrôle d'accès : RBAC par middleware, garde-fous admin.", + "Cryptographie : HTTPS/TLS, bcrypt (coût 12), mots de passe masqués et hors logs.", + "Injection : ORM paramétré (Eloquent) + validation par Form Requests.", + "Authentification : JWT signé, TTL 60 min, blacklist, rate limiting différencié.", + "Journalisation : piste d'audit immuable (IP, anciennes/nouvelles valeurs).", + "Configuration : APP_DEBUG=false, secrets hors dépôt, CORS en liste blanche.", +], Inches(0.8), Inches(1.5), Inches(11.7), Inches(5.0), size=17, gap=11) +footer(s) + +# ════════════════════════════════════════════════════════════════════ +# Slide 10 — Matrice de risques +# ════════════════════════════════════════════════════════════════════ +s = prs.slides.add_slide(BLANK) +band_title(s, "Sécurisation — matrice de criticité des risques", 5) +image_fit(s, "matrice_risques.png", Inches(2.4), Inches(1.25), Inches(8.5), Inches(5.5), + caption="Criticité = gravité × probabilité · priorisation des actions (R1, R7, R2 prioritaires)") +footer(s) + +# ════════════════════════════════════════════════════════════════════ +# Slide 11 — Actions préventives / correctives +# ════════════════════════════════════════════════════════════════════ +s = prs.slides.add_slide(BLANK) +band_title(s, "Sécurisation — actions préventives et correctives", 5) +cards(s, [ + ("Prévention", [ + "Validation & ORM (injection)", + "MFA admin, mots de passe robustes", + "audit dépendances en CI", + "Sauvegardes automatisées"], GREEN_D), + ("Chiffrement", [ + "HTTPS/TLS en transit", + "bcrypt + AES (APP_KEY)", + "Notes de santé chiffrées (prévu)", + "JWT signés"], BLUE), + ("Correction", [ + "Correctif prioritaire + test", + "Révocation / blacklist token", + "Redéploiement automatisé", + "Plan de reprise (PRA)"], ORANGE), +], y=1.6, h=4.6, top=0.6) +footer(s) + +# ════════════════════════════════════════════════════════════════════ +# Slide 12 — RGPD +# ════════════════════════════════════════════════════════════════════ +s = prs.slides.add_slide(BLANK) +band_title(s, "Données personnelles et RGPD", 6) +bullets(s, [ + "Données de santé mentale = données sensibles (RGPD art. 9).", + "Consentement explicite obligatoire à l'inscription, tracé en base.", + "Droits implémentés : accès et portabilité (export JSON), effacement (anonymisation + soft delete).", + "Minimisation des données ; hébergement UE, aucun transfert hors UE ; cible HDS en production.", +], Inches(0.8), Inches(1.45), Inches(11.7), Inches(2.4), size=17, gap=10) +rect(s, Inches(0.8), Inches(4.25), Inches(11.7), Inches(2.3), BLUE_L) +rect(s, Inches(0.8), Inches(4.25), Inches(3.6), Inches(0.55), GREEN_D) +tb, tf = textbox(s, Inches(0.95), Inches(4.28), Inches(3.4), Inches(0.5), MSO_ANCHOR.MIDDLE) +set_par(tf.paragraphs[0], "Traçabilité (art. 32)", 15, WHITE, bold=True, space_after=0) +bullets(s, [ + "Piste d'audit immuable : action, IP, anciennes/nouvelles valeurs — le mot de passe n'est jamais journalisé.", + "Chiffrement en transit, hachage bcrypt, contrôle d'accès par rôle.", + "Export et anonymisation testés automatiquement (tests RGPD).", +], Inches(1.0), Inches(4.95), Inches(11.3), Inches(1.6), size=14, gap=6) +footer(s) + +# ════════════════════════════════════════════════════════════════════ +# Slide 13 — Gestion de crise & escalade +# ════════════════════════════════════════════════════════════════════ +s = prs.slides.add_slide(BLANK) +band_title(s, "Gestion de crise et escalade d'information", 6) +levels = [ + ("N1 — Détection", "Astreinte technique", "Qualifier, contenir, ouvrir un ticket sécurité", GREEN), + ("N2 — Confirmation", "Responsable technique", "Isoler, préserver les preuves, évaluer l'impact", YELLOW), + ("N3 — Crise", "Chef de projet + DPO", "Notification CNIL < 72 h, information des personnes", ORANGE), + ("N4 — Direction", "Direction + Ministère", "Communication officielle, coordination ANSSI", RED), +] +y = 1.55 +for titre, resp, act, col in levels: + rect(s, Inches(0.8), Inches(y), Inches(11.7), Inches(1.15), BLUE_L) + rect(s, Inches(0.8), Inches(y), Inches(0.16), Inches(1.15), col) + tb, tf = textbox(s, Inches(1.1), Inches(y + 0.05), Inches(3.0), Inches(1.05), MSO_ANCHOR.MIDDLE) + set_par(tf.paragraphs[0], titre, 15, col if col != YELLOW else GREY_D, bold=True, space_after=2) + p = tf.add_paragraph(); set_par(p, resp, 12, GREY_D, space_after=0) + tb, tf = textbox(s, Inches(4.3), Inches(y + 0.05), Inches(8.0), Inches(1.05), MSO_ANCHOR.MIDDLE) + set_par(tf.paragraphs[0], act, 14, INK, space_after=0) + y += 1.32 +footer(s) + +# ════════════════════════════════════════════════════════════════════ +# Slide 14 — Démo & conclusion +# ════════════════════════════════════════════════════════════════════ +s = prs.slides.add_slide(BLANK) +band_title(s, "Démonstration et conclusion", 7) +tb, tf = textbox(s, Inches(0.8), Inches(1.35), Inches(11.7), Inches(0.5)) +set_par(tf.paragraphs[0], "Démonstration des outils (en direct)", 17, GREEN_D, bold=True, space_after=0) +bullets(s, [ + "Versioning : arbre des commits, branches et tags sur Gitea.", + "Intégration continue : pull request → 29 tests verts (Gitea Actions).", + "Ticketing : création d'une issue (gabarit incident), labels, jalon, Kanban.", + "Déploiement : merge → Coolify → site en ligne (cesizen.cleanows.fr).", +], Inches(0.8), Inches(1.85), Inches(11.7), Inches(2.4), size=16, gap=9) +tb, tf = textbox(s, Inches(0.8), Inches(4.35), Inches(11.7), Inches(0.5)) +set_par(tf.paragraphs[0], "En synthèse", 17, GREEN_D, bold=True, space_after=0) +bullets(s, [ + "Chaîne de déploiement complète, outillée et réellement en production.", + "Maintenance cadrée (ticketing, SLA, veille) et sécurité analysée par les risques + RGPD.", +], Inches(0.8), Inches(4.85), Inches(11.7), Inches(1.4), size=16, gap=8) +rect(s, 0, Inches(6.55), SW, Inches(0.95), BLUE) +tb, tf = textbox(s, Inches(0.8), Inches(6.57), Inches(11.7), Inches(0.9), MSO_ANCHOR.MIDDLE) +set_par(tf.paragraphs[0], "Merci de votre attention — Questions / Réponses", 20, WHITE, + bold=True, align=PP_ALIGN.CENTER, space_after=0) + +# ─── Numéros de page (toutes les slides sauf la page de titre) ───── +for idx, slide in enumerate(prs.slides): + if idx == 0: + continue # pas de numéro sur la slide de titre + tb, tf = textbox(slide, Inches(12.4), Inches(7.05), Inches(0.7), Inches(0.35), + anchor=MSO_ANCHOR.MIDDLE) + set_par(tf.paragraphs[0], str(idx + 1), 10, GREY, bold=True, + align=PP_ALIGN.RIGHT, space_after=0) + +prs.save(OUT) +print(f"Support de soutenance généré : {OUT}") +print(f"Nombre de slides : {len(prs.slides._sldIdLst)}")