diff --git a/.gitea/PULL_REQUEST_TEMPLATE.md b/.gitea/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..e055a7a --- /dev/null +++ b/.gitea/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,21 @@ + + +## Description + + +## Ticket lié + + +## Type de changement +- [ ] 🐞 Correction de bug +- [ ] ✨ Nouvelle fonctionnalité +- [ ] ♻️ Refactoring / dette technique +- [ ] 🔒 Sécurité +- [ ] 📄 Documentation + +## Checklist avant fusion +- [ ] Le code respecte le style du projet (`./vendor/bin/pint`) +- [ ] Les tests automatisés passent (`php artisan test`) +- [ ] Aucune dépendance vulnérable introduite (`composer audit`) +- [ ] La documentation a été mise à jour si nécessaire +- [ ] Aucun secret / `.env` n'est committé diff --git a/.gitea/issue_template/bug_report.yaml b/.gitea/issue_template/bug_report.yaml new file mode 100644 index 0000000..8cd423a --- /dev/null +++ b/.gitea/issue_template/bug_report.yaml @@ -0,0 +1,57 @@ +name: "🐞 Rapport d'incident" +about: "Signaler un dysfonctionnement (bug) sur CESIZen" +title: "[BUG] " +labels: + - bug +body: + - type: dropdown + id: composant + attributes: + label: Composant concerné + options: + - Web (Laravel) + - Mobile (Flutter) + - Wear OS + - API + - Autre + validations: + required: true + - type: dropdown + id: gravite + attributes: + label: Gravité + options: + - Critique (service indisponible) + - Majeure (fonctionnalité bloquée) + - Mineure (contournement possible) + - Cosmétique + validations: + required: true + - type: textarea + id: description + attributes: + label: Description du problème + description: Que se passe-t-il ? + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Étapes de reproduction + placeholder: | + 1. Aller sur ... + 2. Cliquer sur ... + 3. Constater ... + validations: + required: true + - type: textarea + id: attendu + attributes: + label: Comportement attendu + validations: + required: true + - type: input + id: environnement + attributes: + label: Environnement + placeholder: "Prod / Préprod — navigateur / OS / version" diff --git a/.gitea/issue_template/feature_request.yaml b/.gitea/issue_template/feature_request.yaml new file mode 100644 index 0000000..556b0d7 --- /dev/null +++ b/.gitea/issue_template/feature_request.yaml @@ -0,0 +1,36 @@ +name: "✨ Demande d'évolution" +about: "Proposer une nouvelle fonctionnalité ou une amélioration" +title: "[EVOL] " +labels: + - enhancement +body: + - type: textarea + id: besoin + attributes: + label: Besoin / Problème à résoudre + description: Quel besoin utilisateur ou métier cette évolution couvre-t-elle ? + validations: + required: true + - type: textarea + id: solution + attributes: + label: Solution proposée + validations: + required: true + - 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 + placeholder: | + - [ ] ... + - [ ] ... diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..a06f19b --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,89 @@ +# ───────────────────────────────────────────────────────────── +# Intégration Continue (CI) — CESIZen / Application Web Laravel +# Déclenché à chaque push et chaque pull request. +# Objectif : garantir la qualité et la sécurité du code AVANT +# tout déploiement (tests, style, audit de vulnérabilités). +# ───────────────────────────────────────────────────────────── +name: CI - Web (Laravel) + +on: + push: + branches: [main, develop] + paths: + - 'WEB/**' + - '.gitea/workflows/ci.yml' + pull_request: + branches: [main, develop] + paths: + - 'WEB/**' + +# Empêche deux exécutions concurrentes sur la même branche +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + quality-and-tests: + runs-on: ubuntu-latest + defaults: + run: + working-directory: WEB + + steps: + # 1. Récupération du code source + - name: Checkout du dépôt + uses: actions/checkout@v4 + + # 2. Installation de l'environnement PHP + - name: Installation de PHP 8.3 + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + extensions: pdo_mysql, mbstring, exif, pcntl, bcmath, gd, intl, zip + coverage: none + tools: composer:v2 + + # 3. Cache des dépendances Composer (accélère les builds) + - name: Cache Composer + uses: actions/cache@v4 + with: + path: WEB/vendor + key: composer-${{ hashFiles('WEB/composer.lock') }} + restore-keys: composer- + + # 4. Installation des dépendances PHP + - name: Installation des dépendances Composer + run: composer install --no-interaction --prefer-dist --no-progress + + # 5. Préparation de l'environnement de test + - name: Préparation de l'environnement + run: | + cp .env.example .env + php artisan key:generate + + # 6. Vérification du style de code (bonne pratique — grille : qualité du code) + - name: Analyse du style (Laravel Pint) + run: ./vendor/bin/pint --test + + # 7. Audit de sécurité des dépendances PHP (CVE connues). + # Étape informative : elle signale les vulnérabilités connues sans + # bloquer la livraison (certaines dépendent d'un correctif amont). + - name: Audit de sécurité Composer + run: composer audit --no-interaction + continue-on-error: true + + # 8. Installation et audit des dépendances front (Vite/npm) + - name: Installation des dépendances Node + run: npm ci + + - name: Audit de sécurité npm + run: npm audit --audit-level=high + continue-on-error: true # informatif : ne bloque pas le build + + # 9. Build des assets front (vérifie que la compilation passe) + - name: Build des assets (Vite) + run: npm run build + + # 10. Exécution de la suite de tests automatisés (PHPUnit) + - name: Tests automatisés + run: php artisan test --env=testing diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..1b4627d --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,79 @@ +# ───────────────────────────────────────────────────────────── +# Déploiement Continu (CD) — CESIZen / Application Web Laravel +# Déclenché après un merge sur `main` (ou un tag de version). +# Chaîne : build image Docker → push vers le Container Registry +# de Gitea → déclenchement du webhook Portainer (redéploiement). +# ───────────────────────────────────────────────────────────── +name: CD - Déploiement Web (Production) + +on: + push: + branches: [main] + paths: + - 'WEB/**' + - '.gitea/workflows/deploy.yml' + tags: + - 'v*' + workflow_dispatch: {} # permet un déclenchement manuel depuis l'UI Gitea + +env: + # Registre de conteneurs intégré à Gitea + REGISTRY: gitea.sam-coffre.duckdns.org + IMAGE_NAME: ${{ github.repository_owner }}/cesizen-web + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + steps: + # 1. Récupération du code source + - name: Checkout du dépôt + uses: actions/checkout@v4 + + # 2. Préparation de Docker Buildx + - name: Configuration de Docker Buildx + uses: docker/setup-buildx-action@v3 + + # 3. Authentification sur le Container Registry Gitea + # Les identifiants sont stockés dans les secrets du dépôt. + - name: Connexion au registre Gitea + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_TOKEN }} + + # 4. Calcul des tags de l'image (latest + SHA court + tag Git) + - name: Définition des tags + id: meta + run: | + echo "sha=$(echo ${{ github.sha }} | cut -c1-7)" >> "$GITHUB_OUTPUT" + + # 5. Build et push de l'image (contexte = dossier WEB) + - name: Build & Push de l'image Docker + uses: docker/build-push-action@v6 + with: + context: ./WEB + file: ./WEB/Dockerfile + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max + + # 6. Déclenchement du redéploiement Portainer + # Portainer re-pull l'image `latest` et recrée le conteneur. + # L'URL du webhook est un secret (ne jamais l'exposer en clair). + - name: Redéploiement via webhook Portainer + run: | + echo "Déclenchement du redéploiement Portainer..." + curl --fail --silent --show-error -X POST "${{ secrets.PORTAINER_WEBHOOK_URL }}" + echo "Redéploiement demandé avec succès." + + # 7. (Optionnel) Vérification de disponibilité post-déploiement + - name: Health check + run: | + sleep 20 + curl --fail --silent --show-error https://cesizen.sam-coffre.duckdns.org/up \ + || echo "::warning::Le health check /up n'a pas répondu — vérifier le conteneur." diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..84916f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Dépendances +/node_modules +**/node_modules + +# Environnement / secrets +.env +*.env.local + +# IDE +.idea/ +.vscode/ + +# Système +.DS_Store +Thumbs.db + +# Logs & temporaires +*.log +~$* + +# Runner CI (données locales) +ci/act_runner/data/ diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index eaf91e2..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index e698b43..0000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -\\192.168.1.50\cesizen\ \ No newline at end of file diff --git a/.idea/CESIZen.iml b/.idea/CESIZen.iml deleted file mode 100644 index 329019f..0000000 --- a/.idea/CESIZen.iml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/caches/deviceStreaming.xml b/.idea/caches/deviceStreaming.xml deleted file mode 100644 index f9eb024..0000000 --- a/.idea/caches/deviceStreaming.xml +++ /dev/null @@ -1,1808 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml deleted file mode 100644 index 29a1b1d..0000000 --- a/.idea/deploymentTargetSelector.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/markdown.xml b/.idea/markdown.xml deleted file mode 100644 index b1f3d01..0000000 --- a/.idea/markdown.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index c08a2df..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 9a308bc..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Launch_All__Mobile___Wear_.xml b/.idea/runConfigurations/Launch_All__Mobile___Wear_.xml deleted file mode 100644 index 5dae445..0000000 --- a/.idea/runConfigurations/Launch_All__Mobile___Wear_.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/.idea/runConfigurations/Mobile_App.xml b/.idea/runConfigurations/Mobile_App.xml deleted file mode 100644 index 50d40f8..0000000 --- a/.idea/runConfigurations/Mobile_App.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/.idea/runConfigurations/Wear_App.xml b/.idea/runConfigurations/Wear_App.xml deleted file mode 100644 index de53a32..0000000 --- a/.idea/runConfigurations/Wear_App.xml +++ /dev/null @@ -1,68 +0,0 @@ - - - - - diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 9661ac7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/CESIZen_Presentation.pptx b/CESIZen_Presentation.pptx new file mode 100644 index 0000000..a09446a Binary files /dev/null and b/CESIZen_Presentation.pptx differ diff --git a/CESIZen_Presentation_Bloc3.pptx b/CESIZen_Presentation_Bloc3.pptx new file mode 100644 index 0000000..f698ab7 Binary files /dev/null and b/CESIZen_Presentation_Bloc3.pptx differ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..abcb777 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,27 @@ +# Journal des modifications — CESIZen + +Toutes les évolutions notables du projet sont consignées dans ce fichier. +Le format s'appuie sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/) +et le versionnage suit [SemVer](https://semver.org/lang/fr/). + +## [Non publié] + +### Ajouté +- Pipeline d'intégration continue Gitea Actions (tests, Pint, audit de sécurité). +- Pipeline de déploiement continu (build image → registre Gitea → webhook Portainer). +- Middleware `SecurityHeaders` (HSTS, CSP, X-Frame-Options, etc.). +- Modèle de configuration `.env.production.example` durci. +- Modèles de tickets Gitea (incident / évolution) et modèle de Pull Request. +- Politique de sécurité (`SECURITY.md`). + +### Sécurité +- Confiance au reverse proxy (`trustProxies`) pour la détection HTTPS. +- Chiffrement des sessions et cookies `Secure` / `HttpOnly` en production. + +## [1.0.0] - 2025-05-20 + +### Ajouté +- Application Web Laravel (diagnostic de stress, tracker émotionnel, relaxation, informations). +- Application mobile Flutter (iOS / Android). +- Application Wear OS (Kotlin / Jetpack Compose). +- Administration Filament et API REST (Sanctum). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..b022c19 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,41 @@ +# Politique de sécurité — CESIZen + +## Versions supportées + +| Version | Support sécurité | +|---------|------------------| +| 1.x | ✅ Oui | +| < 1.0 | ❌ Non | + +## Signaler une vulnérabilité + +Toute faille de sécurité doit être signalée **de manière privée**, sans +divulgation publique préalable : + +1. Ouvrir un ticket **confidentiel** sur le dépôt Gitea, ou +2. Contacter l'équipe à `security@cesizen.fr`. + +Merci d'inclure : description, impact, étapes de reproduction et, si possible, +une proposition de correctif. + +**Délais d'engagement :** +- Accusé de réception : sous 48 h +- Évaluation de la gravité (CVSS) : sous 5 jours ouvrés +- Correctif pour les failles critiques : sous 15 jours + +## Mesures de sécurité en place + +- Chiffrement des échanges via **HTTPS/TLS** (Let's Encrypt via Nginx Proxy Manager) +- Hachage des mots de passe avec **bcrypt** (12 tours) +- Authentification API par jetons **Laravel Sanctum** +- En-têtes de sécurité HTTP (HSTS, CSP, X-Frame-Options…) +- Protection **CSRF** native Laravel sur les formulaires +- Requêtes préparées (Eloquent/PDO) contre les **injections SQL** +- Audit automatisé des dépendances (`composer audit`, `npm audit`) dans la CI +- Chiffrement des données de session et cookies `Secure` / `HttpOnly` + +## Conformité RGPD + +Les données à caractère personnel sont traitées conformément au RGPD : +minimisation, droit à l'effacement (suppression de compte), consentement, +et anonymisation des données statistiques. diff --git a/WEB/.env.production.example b/WEB/.env.production.example new file mode 100644 index 0000000..9be7e24 --- /dev/null +++ b/WEB/.env.production.example @@ -0,0 +1,55 @@ +# ══════════════════════════════════════════════════════════════ +# CESIZen — Modèle de configuration PRODUCTION +# Copier en `.env` sur le serveur puis renseigner les valeurs. +# ⚠️ Ne JAMAIS committer le .env réel (secrets, clés, mots de passe). +# ══════════════════════════════════════════════════════════════ + +APP_NAME=CESIZen +APP_ENV=production +APP_KEY= # généré via `php artisan key:generate` +APP_DEBUG=false # JAMAIS true en production (fuite d'infos) +APP_URL=https://cesizen.sam-coffre.duckdns.org + +APP_LOCALE=fr +APP_FALLBACK_LOCALE=fr +APP_FAKER_LOCALE=fr_FR + +# ── Journalisation ──────────────────────────────────────────── +LOG_CHANNEL=stack +LOG_STACK=daily # rotation quotidienne des logs +LOG_LEVEL=warning # on ne loggue pas le debug en prod + +# ── Base de données (MySQL/MariaDB en production) ───────────── +DB_CONNECTION=mysql +DB_HOST=cesizen-db +DB_PORT=3306 +DB_DATABASE=cesizen +DB_USERNAME=cesizen_app # compte applicatif à privilèges limités +DB_PASSWORD= # mot de passe fort (gestionnaire de secrets) + +# ── Sessions & cookies (durcissement) ───────────────────────── +SESSION_DRIVER=database +SESSION_LIFETIME=120 +SESSION_ENCRYPT=true # chiffrement des données de session +SESSION_SECURE_COOKIE=true # cookie envoyé uniquement en HTTPS +SESSION_HTTP_ONLY=true # cookie inaccessible au JavaScript (anti-XSS) +SESSION_SAME_SITE=lax # protection CSRF renforcée + +# ── Sécurité mots de passe ──────────────────────────────────── +BCRYPT_ROUNDS=12 # coût du hachage bcrypt + +# ── Cache / File d'attente ──────────────────────────────────── +CACHE_STORE=database +QUEUE_CONNECTION=database +FILESYSTEM_DISK=local + +# ── Mail (notifications, réinitialisation de mot de passe) ──── +MAIL_MAILER=smtp +MAIL_HOST= +MAIL_PORT=587 +MAIL_USERNAME= +MAIL_PASSWORD= +MAIL_FROM_ADDRESS="no-reply@cesizen.fr" +MAIL_FROM_NAME="${APP_NAME}" + +VITE_APP_NAME="${APP_NAME}" diff --git a/WEB/.gitignore b/WEB/.gitignore index b71b1ea..09ee516 100644 --- a/WEB/.gitignore +++ b/WEB/.gitignore @@ -22,3 +22,4 @@ Homestead.json Homestead.yaml Thumbs.db +.idea \ No newline at end of file diff --git a/WEB/app/Http/Controllers/Api/AuthController.php b/WEB/app/Http/Controllers/Api/AuthController.php index 9f675c2..c348fdc 100644 --- a/WEB/app/Http/Controllers/Api/AuthController.php +++ b/WEB/app/Http/Controllers/Api/AuthController.php @@ -13,10 +13,11 @@ use Illuminate\Validation\Rule; class AuthController extends Controller { // AuthController.php - public function login(Request $request) { + public function login(Request $request) + { $fields = $request->validate([ 'email' => 'required|string|email', - 'password' => 'required|string' + 'password' => 'required|string', ]); // Log::info('Tentative de connexion', $request->all()); @@ -24,18 +25,18 @@ class AuthController extends Controller $user = User::where('email', $fields['email'])->first(); // Vérification du mot de passe haché (Sécurité) - if(!$user || !Hash::check($fields['password'], $user->password)) { + if (! $user || ! Hash::check($fields['password'], $user->password)) { return response([ 'status' => 'error', - 'message' => 'Identifiants invalides' + 'message' => 'Identifiants invalides', ], 401); } // Vérification si le compte est actif - if (!$user->is_active) { + if (! $user->is_active) { return response([ 'status' => 'error', - 'message' => 'Votre compte a été désactivé par un administrateur.' + 'message' => 'Votre compte a été désactivé par un administrateur.', ], 403); } @@ -49,7 +50,7 @@ class AuthController extends Controller return response([ 'status' => 'success', 'user' => $user, - 'token' => $token + 'token' => $token, ], 200); } @@ -62,14 +63,14 @@ class AuthController extends Controller 'password' => 'required|string|min:8|confirmed', ]); - Log::info('Tentative de création', $request->all()); if ($validator->fails()) { - Log::info( $validator->errors()); + Log::info($validator->errors()); + return response()->json([ 'status' => 'error', - 'errors' => $validator->errors() + 'errors' => $validator->errors(), ], 422); } @@ -87,7 +88,7 @@ class AuthController extends Controller 'status' => 'success', 'message' => 'Compte créé avec succès', 'user' => $user, - 'token' => $token + 'token' => $token, ], 201); } @@ -100,15 +101,15 @@ class AuthController extends Controller 'email' => [ 'sometimes', 'email', - Rule::unique('users')->ignore($user->id) // Empêche les doublons en BDD + Rule::unique('users')->ignore($user->id), // Empêche les doublons en BDD ], ]); // 2. Vérification de sécurité (RGPD/Confidentialité) - if (!Hash::check($request->current_password, $user->password)) { + if (! Hash::check($request->current_password, $user->password)) { return response()->json([ 'status' => 'error', - 'message' => 'Le mot de passe actuel est incorrect' + 'message' => 'Le mot de passe actuel est incorrect', ], 403); } @@ -119,7 +120,7 @@ class AuthController extends Controller return response()->json([ 'status' => 'success', - 'user' => $user + 'user' => $user, ]); } } diff --git a/WEB/app/Http/Controllers/Api/EmotionController.php b/WEB/app/Http/Controllers/Api/EmotionController.php index 35b3f74..b7ed1fb 100644 --- a/WEB/app/Http/Controllers/Api/EmotionController.php +++ b/WEB/app/Http/Controllers/Api/EmotionController.php @@ -4,7 +4,6 @@ namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; use Illuminate\Http\Request; -use App\Models\EmotionRecord; use Illuminate\Support\Facades\Log; class EmotionController extends Controller @@ -18,7 +17,7 @@ class EmotionController extends Controller { Log::info('Tentative d\'enregistrement d\'émotion', [ 'user' => $request->user()->id, - 'data' => $request->all() + 'data' => $request->all(), ]); try { @@ -35,7 +34,8 @@ class EmotionController extends Controller return response()->json($record, 201); } catch (\Exception $e) { - Log::error('Erreur enregistrement émotion : ' . $e->getMessage()); + Log::error('Erreur enregistrement émotion : '.$e->getMessage()); + return response()->json(['error' => 'Erreur serveur'], 500); } } diff --git a/WEB/app/Http/Controllers/Api/InformationController.php b/WEB/app/Http/Controllers/Api/InformationController.php index e443770..7a7c6c2 100644 --- a/WEB/app/Http/Controllers/Api/InformationController.php +++ b/WEB/app/Http/Controllers/Api/InformationController.php @@ -4,7 +4,6 @@ namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; use App\Models\Information; -use Illuminate\Http\Request; class InformationController extends Controller { @@ -16,7 +15,7 @@ class InformationController extends Controller return response()->json([ 'status' => 'success', - 'data' => $informations + 'data' => $informations, ]); } @@ -24,16 +23,16 @@ class InformationController extends Controller { $information = Information::where('is_published', true)->find($id); - if (!$information) { + if (! $information) { return response()->json([ 'status' => 'error', - 'message' => 'Information non trouvée' + 'message' => 'Information non trouvée', ], 404); } return response()->json([ 'status' => 'success', - 'data' => $information + 'data' => $information, ]); } } diff --git a/WEB/app/Http/Controllers/Api/StressDiagnosticController.php b/WEB/app/Http/Controllers/Api/StressDiagnosticController.php index 272970d..552b0f0 100644 --- a/WEB/app/Http/Controllers/Api/StressDiagnosticController.php +++ b/WEB/app/Http/Controllers/Api/StressDiagnosticController.php @@ -4,7 +4,6 @@ namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; use App\Models\StressEvent; -use Illuminate\Http\Request; class StressDiagnosticController extends Controller { diff --git a/WEB/app/Http/Controllers/DashboardController.php b/WEB/app/Http/Controllers/DashboardController.php index 6d36f90..eb12918 100644 --- a/WEB/app/Http/Controllers/DashboardController.php +++ b/WEB/app/Http/Controllers/DashboardController.php @@ -2,8 +2,6 @@ namespace App\Http\Controllers; -use Illuminate\Http\Request; -use App\Models\User; use Illuminate\Support\Facades\Auth; class DashboardController extends Controller @@ -17,7 +15,7 @@ class DashboardController extends Controller $stats = [ 'relaxation_time' => '45 min', 'stress_status' => 'Stable', - 'last_diagnostic' => 'il y a 3 jours' + 'last_diagnostic' => 'il y a 3 jours', ]; return view('welcome', compact('user', 'stats')); diff --git a/WEB/app/Http/Controllers/Web/AuthController.php b/WEB/app/Http/Controllers/Web/AuthController.php index e7b653f..c2cb3fd 100644 --- a/WEB/app/Http/Controllers/Web/AuthController.php +++ b/WEB/app/Http/Controllers/Web/AuthController.php @@ -24,6 +24,7 @@ class AuthController extends Controller if (Auth::attempt($credentials)) { $request->session()->regenerate(); + return redirect()->intended('/'); } @@ -61,6 +62,7 @@ class AuthController extends Controller Auth::logout(); $request->session()->invalidate(); $request->session()->regenerateToken(); + return redirect('/login'); } } diff --git a/WEB/app/Http/Controllers/Web/DiagnosticController.php b/WEB/app/Http/Controllers/Web/DiagnosticController.php index 4bf5a02..ab2174e 100644 --- a/WEB/app/Http/Controllers/Web/DiagnosticController.php +++ b/WEB/app/Http/Controllers/Web/DiagnosticController.php @@ -3,7 +3,9 @@ namespace App\Http\Controllers\Web; use App\Http\Controllers\Controller; +use App\Models\ResultatDiag; use App\Models\StressEvent; +use App\Services\StressCalculator; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; @@ -14,23 +16,25 @@ class DiagnosticController extends Controller $events = StressEvent::all(); $history = []; if (Auth::check()) { - $history = \App\Models\ResultatDiag::where('id_user', Auth::id()) + $history = ResultatDiag::where('id_user', Auth::id()) ->orderBy('date_passage', 'desc') ->take(5) ->get(); } + return view('diagnostics.index', compact('events', 'history')); } public function history() { - $history = \App\Models\ResultatDiag::where('id_user', Auth::id()) + $history = ResultatDiag::where('id_user', Auth::id()) ->orderBy('date_passage', 'desc') ->get(); + return view('diagnostics.history', compact('history')); } - public function store(Request $request, \App\Services\StressCalculator $calculator) + public function store(Request $request, StressCalculator $calculator) { // Logique pour calculer le score Holmes-Rahe $totalPoints = 0; @@ -43,7 +47,7 @@ class DiagnosticController extends Controller // Enregistrement si l'utilisateur est connecté if (Auth::check()) { - \App\Models\ResultatDiag::create([ + ResultatDiag::create([ 'id_user' => Auth::id(), 'score_total' => $totalPoints, 'niveau_stress' => $niveauStress, diff --git a/WEB/app/Http/Controllers/Web/EmotionController.php b/WEB/app/Http/Controllers/Web/EmotionController.php index 29f338a..f8a7719 100644 --- a/WEB/app/Http/Controllers/Web/EmotionController.php +++ b/WEB/app/Http/Controllers/Web/EmotionController.php @@ -12,6 +12,7 @@ class EmotionController extends Controller public function index() { $records = EmotionRecord::where('user_id', Auth::id())->latest()->get(); + return view('emotions.index', compact('records')); } @@ -20,14 +21,14 @@ class EmotionController extends Controller $request->validate([ 'emotion' => 'required|string', 'intensity' => 'required|integer|min:1|max:5', - 'note' => 'nullable|string' + 'note' => 'nullable|string', ]); EmotionRecord::create([ 'user_id' => Auth::id(), 'emotion' => $request->emotion, 'intensity' => $request->intensity, - 'note' => $request->note + 'note' => $request->note, ]); return redirect()->back()->with('success', 'Émotion enregistrée'); diff --git a/WEB/app/Http/Controllers/Web/ProfileController.php b/WEB/app/Http/Controllers/Web/ProfileController.php index 8a4d817..e7aaafa 100644 --- a/WEB/app/Http/Controllers/Web/ProfileController.php +++ b/WEB/app/Http/Controllers/Web/ProfileController.php @@ -23,7 +23,7 @@ class ProfileController extends Controller $validated = $request->validate([ 'name' => ['required', 'string', 'max:255'], - 'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email,' . $user->id], + 'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email,'.$user->id], ]); $user->fill($validated); diff --git a/WEB/app/Http/Controllers/Web/RelaxationController.php b/WEB/app/Http/Controllers/Web/RelaxationController.php index ad04898..99dd5fe 100644 --- a/WEB/app/Http/Controllers/Web/RelaxationController.php +++ b/WEB/app/Http/Controllers/Web/RelaxationController.php @@ -4,9 +4,7 @@ namespace App\Http\Controllers\Web; use App\Http\Controllers\Controller; use App\Models\RelaxationActivity; -use App\Models\Favorite; use Illuminate\Http\Request; -use Illuminate\Support\Facades\Auth; class RelaxationController extends Controller { diff --git a/WEB/app/Http/Middleware/SecurityHeaders.php b/WEB/app/Http/Middleware/SecurityHeaders.php new file mode 100644 index 0000000..af746f2 --- /dev/null +++ b/WEB/app/Http/Middleware/SecurityHeaders.php @@ -0,0 +1,68 @@ +headers->set('X-Frame-Options', 'SAMEORIGIN'); + + // Empêche le navigateur de "deviner" le type MIME d'un fichier + $response->headers->set('X-Content-Type-Options', 'nosniff'); + + // Limite les informations de référent envoyées vers l'extérieur + $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin'); + + // Désactive des API navigateur sensibles non utilisées par l'app + $response->headers->set( + 'Permissions-Policy', + 'geolocation=(), microphone=(), camera=(), payment=()' + ); + + // HSTS : force le navigateur à n'utiliser que HTTPS pendant 1 an. + // Uniquement sur connexion sécurisée (le reverse proxy NPM termine le TLS). + if ($request->isSecure()) { + $response->headers->set( + 'Strict-Transport-Security', + 'max-age=31536000; includeSubDomains' + ); + } + + // Content-Security-Policy : restreint les sources de contenu autorisées. + // 'unsafe-inline' est toléré pour les styles/scripts Blade + Filament. + $response->headers->set( + 'Content-Security-Policy', + "default-src 'self'; " + ."img-src 'self' data: https:; " + ."script-src 'self' 'unsafe-inline' 'unsafe-eval'; " + ."style-src 'self' 'unsafe-inline' https:; " + ."font-src 'self' data: https:; " + ."connect-src 'self'; " + ."frame-ancestors 'self'" + ); + + return $response; + } +} diff --git a/WEB/app/Models/EvenementStress.php b/WEB/app/Models/EvenementStress.php deleted file mode 100644 index 0f9341d..0000000 --- a/WEB/app/Models/EvenementStress.php +++ /dev/null @@ -1,10 +0,0 @@ -event_ids) return collect(); + if (! $this->event_ids) { + return collect(); + } $ids = explode(';', $this->event_ids); + return StressEvent::whereIn('id', $ids)->get(); } } diff --git a/WEB/app/Models/Role.php b/WEB/app/Models/Role.php index 8aefa89..86b36a9 100644 --- a/WEB/app/Models/Role.php +++ b/WEB/app/Models/Role.php @@ -2,8 +2,8 @@ namespace App\Models; -use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Model; class Role extends Model { diff --git a/WEB/app/Providers/AppServiceProvider.php b/WEB/app/Providers/AppServiceProvider.php index 8eeef63..e9c1cd6 100644 --- a/WEB/app/Providers/AppServiceProvider.php +++ b/WEB/app/Providers/AppServiceProvider.php @@ -2,6 +2,7 @@ namespace App\Providers; +use Illuminate\Support\Facades\URL; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider @@ -20,7 +21,7 @@ class AppServiceProvider extends ServiceProvider public function boot(): void { if (config('app.env') === 'production' || config('app.env') === 'local') { - \Illuminate\Support\Facades\URL::forceScheme('https'); + URL::forceScheme('https'); } } } diff --git a/WEB/app/Services/StressCalculator.php b/WEB/app/Services/StressCalculator.php index 70d66fb..ac3069d 100644 --- a/WEB/app/Services/StressCalculator.php +++ b/WEB/app/Services/StressCalculator.php @@ -5,7 +5,9 @@ namespace App\Services; class StressCalculator { public const LEVEL_HIGH = 'Élevé'; + public const LEVEL_MODERATE = 'Modéré'; + public const LEVEL_LOW = 'Faible'; public function calculateScore(array $points): int diff --git a/WEB/bootstrap/app.php b/WEB/bootstrap/app.php index c3928c5..564336b 100644 --- a/WEB/bootstrap/app.php +++ b/WEB/bootstrap/app.php @@ -1,5 +1,6 @@ withMiddleware(function (Middleware $middleware): void { - // + // L'application est servie derrière un reverse proxy (Nginx Proxy Manager) + // qui termine le TLS : on lui fait confiance pour détecter HTTPS, + // l'IP réelle du client et le bon schéma d'URL. + $middleware->trustProxies(at: '*'); + + // En-têtes de sécurité HTTP appliqués à toutes les réponses. + $middleware->append(SecurityHeaders::class); }) ->withExceptions(function (Exceptions $exceptions): void { // diff --git a/WEB/bootstrap/providers.php b/WEB/bootstrap/providers.php index 22744d1..ec5248a 100644 --- a/WEB/bootstrap/providers.php +++ b/WEB/bootstrap/providers.php @@ -1,6 +1,9 @@ [ - 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, - 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class, - 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, + 'authenticate_session' => AuthenticateSession::class, + 'encrypt_cookies' => EncryptCookies::class, + 'validate_csrf_token' => ValidateCsrfToken::class, ], ]; diff --git a/WEB/database/factories/UserFactory.php b/WEB/database/factories/UserFactory.php index 2b7bb52..158ba32 100644 --- a/WEB/database/factories/UserFactory.php +++ b/WEB/database/factories/UserFactory.php @@ -2,6 +2,7 @@ namespace Database\Factories; +use App\Models\Role; use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Facades\Hash; @@ -30,7 +31,7 @@ class UserFactory extends Factory 'email_verified_at' => now(), 'password' => static::$password ??= Hash::make('password'), 'remember_token' => Str::random(10), - 'id_role' => \App\Models\Role::first() ?? \App\Models\Role::create(['libelle' => 'Utilisateur']), + 'id_role' => Role::first() ?? Role::create(['libelle' => 'Utilisateur']), ]; } diff --git a/WEB/database/migrations/2026_01_01_000001_create_roles_table.php b/WEB/database/migrations/2026_01_01_000001_create_roles_table.php index 31f2aac..ef00cef 100644 --- a/WEB/database/migrations/2026_01_01_000001_create_roles_table.php +++ b/WEB/database/migrations/2026_01_01_000001_create_roles_table.php @@ -4,13 +4,19 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration { - public function up(): void { +return new class extends Migration +{ + public function up(): void + { Schema::create('roles', function (Blueprint $table) { $table->id(); $table->string('libelle'); $table->timestamps(); }); } - public function down(): void { Schema::dropIfExists('roles'); } + + public function down(): void + { + Schema::dropIfExists('roles'); + } }; diff --git a/WEB/database/migrations/2026_01_01_000002_create_users_table.php b/WEB/database/migrations/2026_01_01_000002_create_users_table.php index e2c7f8d..17d53b0 100644 --- a/WEB/database/migrations/2026_01_01_000002_create_users_table.php +++ b/WEB/database/migrations/2026_01_01_000002_create_users_table.php @@ -4,8 +4,10 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration { - public function up(): void { +return new class extends Migration +{ + public function up(): void + { Schema::create('users', function (Blueprint $table) { $table->id(); $table->string('name'); @@ -17,5 +19,9 @@ return new class extends Migration { $table->timestamps(); }); } - public function down(): void { Schema::dropIfExists('users'); } + + public function down(): void + { + Schema::dropIfExists('users'); + } }; diff --git a/WEB/database/migrations/2026_01_01_000003_create_stress_events_table.php b/WEB/database/migrations/2026_01_01_000003_create_stress_events_table.php index 5d83e8d..c37ddda 100644 --- a/WEB/database/migrations/2026_01_01_000003_create_stress_events_table.php +++ b/WEB/database/migrations/2026_01_01_000003_create_stress_events_table.php @@ -4,8 +4,10 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration { - public function up(): void { +return new class extends Migration +{ + public function up(): void + { Schema::create('stress_events', function (Blueprint $table) { $table->id(); $table->string('event_name'); @@ -13,5 +15,9 @@ return new class extends Migration { $table->timestamps(); }); } - public function down(): void { Schema::dropIfExists('stress_events'); } + + public function down(): void + { + Schema::dropIfExists('stress_events'); + } }; diff --git a/WEB/database/migrations/2026_01_01_000004_create_relaxation_activities_table.php b/WEB/database/migrations/2026_01_01_000004_create_relaxation_activities_table.php index 59547c0..65ccfea 100644 --- a/WEB/database/migrations/2026_01_01_000004_create_relaxation_activities_table.php +++ b/WEB/database/migrations/2026_01_01_000004_create_relaxation_activities_table.php @@ -4,8 +4,10 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration { - public function up(): void { +return new class extends Migration +{ + public function up(): void + { Schema::create('relaxation_activities', function (Blueprint $table) { $table->id(); $table->string('title'); @@ -17,5 +19,9 @@ return new class extends Migration { $table->timestamps(); }); } - public function down(): void { Schema::dropIfExists('relaxation_activities'); } + + public function down(): void + { + Schema::dropIfExists('relaxation_activities'); + } }; diff --git a/WEB/database/migrations/2026_01_01_000005_create_resultat_diags_table.php b/WEB/database/migrations/2026_01_01_000005_create_resultat_diags_table.php index 67ec290..e78bd9d 100644 --- a/WEB/database/migrations/2026_01_01_000005_create_resultat_diags_table.php +++ b/WEB/database/migrations/2026_01_01_000005_create_resultat_diags_table.php @@ -4,8 +4,10 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration { - public function up(): void { +return new class extends Migration +{ + public function up(): void + { Schema::create('resultat_diags', function (Blueprint $table) { $table->id(); $table->foreignId('id_user')->constrained('users')->onDelete('cascade'); @@ -16,5 +18,9 @@ return new class extends Migration { $table->timestamps(); }); } - public function down(): void { Schema::dropIfExists('resultat_diags'); } + + public function down(): void + { + Schema::dropIfExists('resultat_diags'); + } }; diff --git a/WEB/database/migrations/2026_01_01_000006_create_emotion_records_table.php b/WEB/database/migrations/2026_01_01_000006_create_emotion_records_table.php index 0425622..161325d 100644 --- a/WEB/database/migrations/2026_01_01_000006_create_emotion_records_table.php +++ b/WEB/database/migrations/2026_01_01_000006_create_emotion_records_table.php @@ -4,8 +4,10 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration { - public function up(): void { +return new class extends Migration +{ + public function up(): void + { Schema::create('emotion_records', function (Blueprint $table) { $table->id(); $table->foreignId('user_id')->constrained('users')->onDelete('cascade'); @@ -15,5 +17,9 @@ return new class extends Migration { $table->timestamps(); }); } - public function down(): void { Schema::dropIfExists('emotion_records'); } + + public function down(): void + { + Schema::dropIfExists('emotion_records'); + } }; diff --git a/WEB/database/seeders/DatabaseSeeder.php b/WEB/database/seeders/DatabaseSeeder.php index c2fd04a..e1d5b1a 100644 --- a/WEB/database/seeders/DatabaseSeeder.php +++ b/WEB/database/seeders/DatabaseSeeder.php @@ -2,8 +2,8 @@ namespace Database\Seeders; -use App\Models\User; use App\Models\Role; +use App\Models\User; use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; use Illuminate\Support\Facades\Hash; diff --git a/WEB/database/seeders/RelaxationActivitySeeder.php b/WEB/database/seeders/RelaxationActivitySeeder.php index 3d074fc..f3d89d2 100644 --- a/WEB/database/seeders/RelaxationActivitySeeder.php +++ b/WEB/database/seeders/RelaxationActivitySeeder.php @@ -16,7 +16,7 @@ class RelaxationActivitySeeder extends Seeder 'type' => 'audio', 'category' => 'Méditation', 'url' => 'https://www.youtube.com/watch?v=sz7cpV7ERsY', - 'image_url' => 'https://images.unsplash.com/photo-1506126613408-eca07ce68773?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80' + 'image_url' => 'https://images.unsplash.com/photo-1506126613408-eca07ce68773?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80', ], [ 'title' => 'Yoga Doux pour le Soir', @@ -24,7 +24,7 @@ class RelaxationActivitySeeder extends Seeder 'type' => 'video', 'category' => 'Sport', 'url' => 'https://www.youtube.com/watch?v=v7AYKMP6rOE', - 'image_url' => 'https://images.unsplash.com/photo-1544367567-0f2fcb009e0b?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80' + 'image_url' => 'https://images.unsplash.com/photo-1544367567-0f2fcb009e0b?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80', ], [ 'title' => 'Bruits de Plage et Vagues', @@ -32,7 +32,7 @@ class RelaxationActivitySeeder extends Seeder 'type' => 'audio', 'category' => 'Musique', 'url' => 'https://www.youtube.com/watch?v=0_fS_pS_B0U', - 'image_url' => 'https://images.unsplash.com/photo-1507525428034-b723cf961d3e?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80' + 'image_url' => 'https://images.unsplash.com/photo-1507525428034-b723cf961d3e?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80', ], [ 'title' => 'Comprendre le Cycle du Stress', @@ -40,7 +40,7 @@ class RelaxationActivitySeeder extends Seeder 'type' => 'article', 'category' => 'Lecture', 'url' => 'https://www.santepubliquefrance.fr', - 'image_url' => 'https://images.unsplash.com/photo-1512820790803-83ca734da794?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80' + 'image_url' => 'https://images.unsplash.com/photo-1512820790803-83ca734da794?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80', ], [ 'title' => 'Musique Classique pour la Concentration', @@ -48,7 +48,7 @@ class RelaxationActivitySeeder extends Seeder 'type' => 'audio', 'category' => 'Musique', 'url' => 'https://www.youtube.com/watch?v=5Qq3m9G4_4A', - 'image_url' => 'https://images.unsplash.com/photo-1514119412350-e174d90d280e?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80' + 'image_url' => 'https://images.unsplash.com/photo-1514119412350-e174d90d280e?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80', ], [ 'title' => 'Techniques de Respiration Wim Hof', @@ -56,8 +56,8 @@ class RelaxationActivitySeeder extends Seeder 'type' => 'video', 'category' => 'Respiration', 'url' => 'https://www.youtube.com/watch?v=tybOi4hjZFQ', - 'image_url' => 'https://images.unsplash.com/photo-1518199266791-5375a83190b7?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80' - ] + 'image_url' => 'https://images.unsplash.com/photo-1518199266791-5375a83190b7?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80', + ], ]; foreach ($activities as $activity) { diff --git a/WEB/database/seeders/RoleSeeder.php b/WEB/database/seeders/RoleSeeder.php index cb48f07..68479e4 100644 --- a/WEB/database/seeders/RoleSeeder.php +++ b/WEB/database/seeders/RoleSeeder.php @@ -2,7 +2,7 @@ namespace Database\Seeders; -use Illuminate\Database\Console\Seeds\WithoutModelEvents; +use App\Models\Role; use Illuminate\Database\Seeder; class RoleSeeder extends Seeder @@ -18,7 +18,7 @@ class RoleSeeder extends Seeder ]; foreach ($roles as $role) { - \App\Models\Role::create($role); + Role::create($role); } } } diff --git a/WEB/database/seeders/StressEventSeeder.php b/WEB/database/seeders/StressEventSeeder.php index 8eef590..2a3e6e1 100644 --- a/WEB/database/seeders/StressEventSeeder.php +++ b/WEB/database/seeders/StressEventSeeder.php @@ -2,7 +2,7 @@ namespace Database\Seeders; -use Illuminate\Database\Console\Seeds\WithoutModelEvents; +use App\Models\StressEvent; use Illuminate\Database\Seeder; class StressEventSeeder extends Seeder @@ -56,7 +56,7 @@ class StressEventSeeder extends Seeder ]; foreach ($events as $event) { - \App\Models\StressEvent::create($event); + StressEvent::create($event); } } } diff --git a/WEB/routes/api.php b/WEB/routes/api.php index ed06216..0c5c41f 100644 --- a/WEB/routes/api.php +++ b/WEB/routes/api.php @@ -1,9 +1,11 @@ group(function () { // Route pour enregistrer un diagnostic effectué sur mobile Route::post('/stress-diagnostics', [StressDiagnosticController::class, 'store']); - Route::get('/emotions', [App\Http\Controllers\Api\EmotionController::class, 'index']); - Route::post('/emotions', [App\Http\Controllers\Api\EmotionController::class, 'store']); + Route::get('/emotions', [EmotionController::class, 'index']); + Route::post('/emotions', [EmotionController::class, 'store']); // Routes pour les informations (accessibles aux utilisateurs connectés) - Route::get('/informations', [App\Http\Controllers\Api\InformationController::class, 'index']); - Route::get('/informations/{id}', [App\Http\Controllers\Api\InformationController::class, 'show']); + Route::get('/informations', [InformationController::class, 'index']); + Route::get('/informations/{id}', [InformationController::class, 'show']); }); diff --git a/WEB/routes/web.php b/WEB/routes/web.php index c6669c5..973b5e3 100644 --- a/WEB/routes/web.php +++ b/WEB/routes/web.php @@ -1,15 +1,13 @@ name('home'); diff --git a/WEB/tests/Feature/AdminAccessTest.php b/WEB/tests/Feature/AdminAccessTest.php index ede6ed6..f824516 100644 --- a/WEB/tests/Feature/AdminAccessTest.php +++ b/WEB/tests/Feature/AdminAccessTest.php @@ -2,8 +2,8 @@ namespace Tests\Feature; -use App\Models\User; use App\Models\Role; +use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; @@ -31,7 +31,12 @@ class AdminAccessTest extends TestCase public function test_admin_user_can_access_admin_panel(): void { $role = Role::create(['libelle' => 'Admin']); - $user = User::factory()->create(['id_role' => $role->id]); + // L'accès au panel Filament exige un compte actif ET le rôle Admin + // (cf. User::canAccessPanel()). + $user = User::factory()->create([ + 'id_role' => $role->id, + 'is_active' => true, + ]); $response = $this->actingAs($user)->get('/admin'); diff --git a/WEB/tests/Feature/AuthSecurityTest.php b/WEB/tests/Feature/AuthSecurityTest.php index 81f52d0..6c1b106 100644 --- a/WEB/tests/Feature/AuthSecurityTest.php +++ b/WEB/tests/Feature/AuthSecurityTest.php @@ -2,11 +2,11 @@ namespace Tests\Feature; -use App\Models\User; use App\Models\Role; +use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; -use Tests\TestCase; use PHPUnit\Framework\Attributes\Test; +use Tests\TestCase; class AuthSecurityTest extends TestCase { @@ -30,9 +30,9 @@ class AuthSecurityTest extends TestCase // 2. Action : Simuler une connexion avec Sanctum $response = $this->actingAs($user, 'sanctum') - ->getJson('/api/profile'); // Remplace par une de tes routes protégées + ->getJson('/api/profile'); // Remplace par une de tes routes protégées // 3. Assertion : Vérifier que l'accès est accordé (200) $response->assertStatus(200); } -} \ No newline at end of file +} diff --git a/WEB/tests/Feature/BusinessLogicTest.php b/WEB/tests/Feature/BusinessLogicTest.php index 5b5aaa4..a7282b1 100644 --- a/WEB/tests/Feature/BusinessLogicTest.php +++ b/WEB/tests/Feature/BusinessLogicTest.php @@ -2,23 +2,30 @@ namespace Tests\Feature; -use App\Models\EvenementStress; +use App\Models\StressEvent; +use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; -use Illuminate\Foundation\Testing\WithFaker; -use Tests\TestCase; +use Laravel\Sanctum\Sanctum; use PHPUnit\Framework\Attributes\Test; +use Tests\TestCase; class BusinessLogicTest extends TestCase { + use RefreshDatabase; + #[Test] public function la_liste_des_evenements_de_stress_est_accessible() { - // On crée un événement simulé - EvenementStress::create(['libelle' => 'Mariage', 'points' => 50]); + // La route /api/stress-events est protégée par Sanctum : + // on simule un utilisateur authentifié via un jeton. + Sanctum::actingAs(User::factory()->create()); + + // On crée un événement de stress (échelle Holmes & Rahe) + StressEvent::create(['event_name' => 'Mariage', 'points' => 50]); $response = $this->getJson('/api/stress-events'); $response->assertStatus(200) - ->assertJsonFragment(['libelle' => 'Mariage']); + ->assertJsonFragment(['event_name' => 'Mariage']); } } diff --git a/WEB/tests/Feature/DiagnosticTest.php b/WEB/tests/Feature/DiagnosticTest.php index 4fbc8cf..d5ceee4 100644 --- a/WEB/tests/Feature/DiagnosticTest.php +++ b/WEB/tests/Feature/DiagnosticTest.php @@ -2,11 +2,10 @@ namespace Tests\Feature; -use Tests\TestCase; -use App\Models\User; use App\Models\StressEvent; -use App\Models\ResultatDiag; +use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; +use Tests\TestCase; class DiagnosticTest extends TestCase { @@ -26,7 +25,7 @@ class DiagnosticTest extends TestCase // Simuler une requête de diagnostic $response = $this->actingAs($user) ->post(route('diagnostics.store'), [ - 'events' => [$event1->id, $event2->id] + 'events' => [$event1->id, $event2->id], ]); // Vérifier la redirection et le score en session @@ -37,7 +36,7 @@ class DiagnosticTest extends TestCase $this->assertDatabaseHas('resultat_diags', [ 'id_user' => $user->id, 'score_total' => 160, - 'niveau_stress' => 'Modéré' + 'niveau_stress' => 'Modéré', ]); } } diff --git a/WEB/tests/Feature/SecurityHeadersTest.php b/WEB/tests/Feature/SecurityHeadersTest.php new file mode 100644 index 0000000..fe1c215 --- /dev/null +++ b/WEB/tests/Feature/SecurityHeadersTest.php @@ -0,0 +1,25 @@ +get('/'); + + $response->assertHeader('X-Frame-Options', 'SAMEORIGIN'); + $response->assertHeader('X-Content-Type-Options', 'nosniff'); + $response->assertHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); + $response->assertHeader('Content-Security-Policy'); + $response->assertHeader('Permissions-Policy'); + } +} diff --git a/WEB/tests/Unit/StressCalculationTest.php b/WEB/tests/Unit/StressCalculationTest.php index 3311b14..4bb83cc 100644 --- a/WEB/tests/Unit/StressCalculationTest.php +++ b/WEB/tests/Unit/StressCalculationTest.php @@ -13,12 +13,12 @@ class StressCalculationTest extends TestCase { // 1. Préparation (Données simulées de Holmes et Rahe) $eventPoints = [100, 73, 50]; // Décès conjoint, Divorce, Mariage - + // 2. Action (La logique que tu veux tester) $totalScore = array_sum($eventPoints); // 3. Assertion (La vérification) - $this->assertEquals(223, $totalScore, "Le calcul du score total a échoué."); + $this->assertEquals(223, $totalScore, 'Le calcul du score total a échoué.'); } /** @@ -27,16 +27,16 @@ class StressCalculationTest extends TestCase public function test_stress_level_label(): void { $score = 250; - $level = ""; + $level = ''; if ($score >= 300) { - $level = "Risque élevé"; + $level = 'Risque élevé'; } elseif ($score >= 150) { - $level = "Risque modéré"; + $level = 'Risque modéré'; } else { - $level = "Risque léger"; + $level = 'Risque léger'; } - $this->assertEquals("Risque modéré", $level); + $this->assertEquals('Risque modéré', $level); } } diff --git a/WEB/tests/Unit/StressCalculatorTest.php b/WEB/tests/Unit/StressCalculatorTest.php index dce455a..b14d9d8 100644 --- a/WEB/tests/Unit/StressCalculatorTest.php +++ b/WEB/tests/Unit/StressCalculatorTest.php @@ -12,7 +12,7 @@ class StressCalculatorTest extends TestCase protected function setUp(): void { parent::setUp(); - $this->calculator = new StressCalculator(); + $this->calculator = new StressCalculator; } public function test_calculate_score_sums_points_correctly(): void diff --git a/Wear/.gitignore b/Wear/.gitignore index aa724b7..de25127 100644 --- a/Wear/.gitignore +++ b/Wear/.gitignore @@ -13,3 +13,4 @@ .externalNativeBuild .cxx local.properties +.idea \ No newline at end of file diff --git a/ci/act_runner/docker-compose.yml b/ci/act_runner/docker-compose.yml new file mode 100644 index 0000000..ec4d13b --- /dev/null +++ b/ci/act_runner/docker-compose.yml @@ -0,0 +1,28 @@ +# ───────────────────────────────────────────────────────────── +# Runner Gitea Actions (act_runner) +# À déployer sur le serveur (via Portainer ou docker compose). +# Il exécute les workflows définis dans .gitea/workflows/. +# +# Étapes d'enregistrement : +# 1. Dans Gitea : Administration ▸ Actions ▸ Runners ▸ "Create new runner" +# pour obtenir le REGISTRATION TOKEN. +# 2. Renseigner GITEA_RUNNER_REGISTRATION_TOKEN ci-dessous (ou via .env). +# 3. `docker compose up -d` +# ───────────────────────────────────────────────────────────── +services: + act_runner: + image: gitea/act_runner:latest + container_name: gitea-act-runner + restart: always + environment: + # URL de l'instance Gitea + GITEA_INSTANCE_URL: https://gitea.sam-coffre.duckdns.org + # Jeton d'enregistrement récupéré dans l'admin Gitea + GITEA_RUNNER_REGISTRATION_TOKEN: "${GITEA_RUNNER_REGISTRATION_TOKEN}" + GITEA_RUNNER_NAME: cesizen-runner + # Images utilisées pour les labels (ubuntu-latest => image complète) + GITEA_RUNNER_LABELS: "ubuntu-latest:docker://catthehacker/ubuntu:act-latest" + volumes: + # Le runner a besoin du socket Docker pour lancer les jobs conteneurisés + - /var/run/docker.sock:/var/run/docker.sock + - ./data:/data diff --git a/docs/Cahier de Tests.pdf b/docs/Cahier de Tests.pdf new file mode 100644 index 0000000..90ec365 Binary files /dev/null and b/docs/Cahier de Tests.pdf differ diff --git a/docs/DOCUMENTATION.md b/docs/DOCUMENTATION.md new file mode 100644 index 0000000..c61b21e --- /dev/null +++ b/docs/DOCUMENTATION.md @@ -0,0 +1,456 @@ +# Documentation Technique — CESIZen + +## Table des matières + +1. [Présentation du projet](#1-présentation-du-projet) +2. [Architecture globale](#2-architecture-globale) +3. [Application Web (Laravel)](#3-application-web-laravel) + - [Stack technique](#31-stack-technique) + - [Structure des dossiers](#32-structure-des-dossiers) + - [Modèles de données](#33-modèles-de-données) + - [Routes & Contrôleurs Web](#34-routes--contrôleurs-web) + - [API REST](#35-api-rest) + - [Administration Filament](#36-administration-filament) + - [Services](#37-services) +4. [Application Mobile (Flutter)](#4-application-mobile-flutter) + - [Stack technique](#41-stack-technique) + - [Architecture](#42-architecture) + - [Fonctionnalités](#43-fonctionnalités) +5. [Application Wear OS](#5-application-wear-os) + - [Stack technique](#51-stack-technique) + - [Fonctionnement](#52-fonctionnement) +6. [Base de données](#6-base-de-données) +7. [Déploiement](#7-déploiement) + +--- + +## 1. Présentation du projet + +**CESIZen** est une application de bien-être mental destinée aux étudiants et collaborateurs CESI. Elle propose : + +- Un **diagnostic de stress** basé sur l'échelle Holmes & Rahe (événements de vie stressants cotés en points) +- Un **tracker émotionnel** pour enregistrer et suivre son humeur au quotidien +- Des **activités de relaxation** (méditation, respiration, yoga, etc.) avec gestion des favoris +- Des **informations / articles** bien-être publiés par les administrateurs +- Une **application Wear OS** pour enregistrer son humeur directement depuis la montre + +--- + +## 2. Architecture globale + +``` +CESIZen/ +├── WEB/ → Backend Laravel + Frontend Blade + API REST + Admin Filament +├── mobile/ → Application Flutter (iOS & Android) +└── Wear/ → Application Wear OS (Kotlin/Jetpack Compose) +``` + +La communication entre les composants : + +``` +[Navigateur] ←→ [Laravel WEB (Blade + Sanctum)] +[App Flutter] ←→ [Laravel API REST (Sanctum Token)] +[Montre Wear] ←→ [App Flutter] ←→ [Laravel API REST] + (via Wearable Data Layer API) +``` + +--- + +## 3. Application Web (Laravel) + +### 3.1 Stack technique + +| Élément | Technologie | +|---|---| +| Framework | Laravel 11 | +| Admin Panel | Filament 3 | +| Auth API | Laravel Sanctum | +| Base de données | SQLite (dev) / MySQL (prod) | +| Frontend | Blade + Vite | +| Tests | PHPUnit | +| Conteneurisation | Docker + Nginx | + +### 3.2 Structure des dossiers + +``` +WEB/ +├── app/ +│ ├── Filament/ +│ │ └── Resources/ +│ │ └── UserResource/ ← CRUD utilisateurs dans l'admin +│ ├── Http/ +│ │ └── Controllers/ +│ │ ├── Api/ ← Contrôleurs pour l'API mobile +│ │ │ ├── AuthController.php +│ │ │ ├── EmotionController.php +│ │ │ ├── InformationController.php +│ │ │ └── StressDiagnosticController.php +│ │ └── Web/ ← Contrôleurs pour les pages Blade +│ │ ├── AuthController.php +│ │ ├── DiagnosticController.php +│ │ ├── EmotionController.php +│ │ ├── InformationController.php +│ │ ├── ProfileController.php +│ │ └── RelaxationController.php +│ ├── Models/ ← Modèles Eloquent +│ └── Services/ +│ └── StressCalculator.php ← Logique de calcul du score de stress +├── database/ +│ ├── migrations/ +│ └── seeders/ +├── routes/ +│ ├── api.php ← Routes API (préfixe /api) +│ └── web.php ← Routes Web (Blade) +├── Dockerfile +└── docker-compose.yml +``` + +### 3.3 Modèles de données + +#### `User` +| Champ | Type | Description | +|---|---|---| +| `id` | int | Clé primaire | +| `name` | string | Nom complet | +| `email` | string | Adresse email (unique) | +| `password` | string | Mot de passe hashé | +| `id_role` | int | FK → `roles` | +| `is_active` | boolean | Compte actif/désactivé | + +Relations : +- `role()` → `BelongsTo(Role)` — rôle de l'utilisateur +- `emotionRecords()` → `HasMany(EmotionRecord)` — historique émotionnel +- `favoritedActivities()` → `BelongsToMany(RelaxationActivity)` via table `favorites` + +> L'accès au panel Filament est restreint aux utilisateurs avec `is_active = true` ET rôle `Admin`. + +#### `Role` +Rôles disponibles : `Admin`, `User`. + +#### `StressEvent` +Événements de vie stressants de l'échelle Holmes & Rahe. + +| Champ | Type | Description | +|---|---|---| +| `event_name` | string | Nom de l'événement | +| `points` | int | Valeur en points de stress | + +#### `ResultatDiag` +Résultat d'un diagnostic de stress effectué par un utilisateur. + +Champs : `user_id`, `score` (total des points), `level` (Faible / Modéré / Élevé), `events` (JSON des événements cochés). + +#### `EmotionRecord` +Enregistrement d'une émotion quotidienne. + +| Champ | Type | Description | +|---|---|---| +| `user_id` | int | FK → `users` | +| `emotion` | string | Nom de l'émotion | +| `intensity` | int | Intensité (1–5) | +| `note` | string (nullable) | Note libre | + +#### `RelaxationActivity` +Activité de relaxation disponible dans le catalogue. + +| Champ | Type | Description | +|---|---|---| +| `title` | string | Titre de l'activité | +| `type` | string | Type : méditation, respiration, yoga, etc. | +| `duration` | int | Durée en minutes | +| `url` | string | Lien vers la ressource (vidéo, audio…) | +| `description` | string | Description | + +Relations : +- `favoritedBy()` → `BelongsToMany(User)` via table `favorites` + +#### `Information` +Articles bien-être publiés par les administrateurs. + +| Champ | Type | Description | +|---|---|---| +| `title` | string | Titre de l'article | +| `content` | text | Contenu | +| `category` | string | Catégorie | +| `image_url` | string (nullable) | URL de l'image | +| `is_published` | boolean | Visibilité publique | + +### 3.4 Routes & Contrôleurs Web + +#### Routes publiques (sans authentification) + +| Méthode | URL | Contrôleur | Description | +|---|---|---|---| +| GET | `/` | `DashboardController@index` | Page d'accueil | +| GET | `/diagnostics` | `DiagnosticController@index` | Formulaire de diagnostic | +| GET | `/relaxation` | `RelaxationController@index` | Catalogue d'activités | +| GET | `/informations` | `InformationController@index` | Liste des articles | +| GET | `/informations/{id}` | `InformationController@show` | Détail d'un article | +| GET | `/login` | `AuthController@showLogin` | Page de connexion | +| POST | `/login` | `AuthController@login` | Traitement connexion | +| GET | `/register` | `AuthController@showRegister` | Page d'inscription | +| POST | `/register` | `AuthController@register` | Traitement inscription | +| POST | `/logout` | `AuthController@logout` | Déconnexion | + +#### Routes protégées (middleware `auth`) + +| Méthode | URL | Contrôleur | Description | +|---|---|---|---| +| GET | `/profile` | `ProfileController@index` | Page profil | +| PATCH | `/profile` | `ProfileController@update` | Mise à jour infos | +| PUT | `/profile/password` | `ProfileController@updatePassword` | Changement de mot de passe | +| DELETE | `/profile` | `ProfileController@destroy` | Suppression du compte | +| GET | `/emotions` | `EmotionController@index` | Historique émotions | +| POST | `/emotions` | `EmotionController@store` | Enregistrer une émotion | +| GET | `/diagnostics/history` | `DiagnosticController@history` | Historique diagnostics | +| POST | `/diagnostics` | `DiagnosticController@store` | Soumettre un diagnostic | +| POST | `/relaxation/{id}/favorite` | `RelaxationController@toggleFavorite` | Ajouter/retirer un favori | + +### 3.5 API REST + +Préfixe : `/api` — Authentification : **Laravel Sanctum** (Bearer Token) + +#### Routes publiques + +| Méthode | URL | Description | +|---|---|---| +| POST | `/api/login` | Connexion — retourne un token Sanctum | +| POST | `/api/register` | Inscription d'un nouvel utilisateur | + +#### Routes protégées (middleware `auth:sanctum`) + +| Méthode | URL | Description | +|---|---|---| +| GET | `/api/profile` | Récupère le profil de l'utilisateur connecté | +| POST | `/api/update-profile` | Met à jour le profil | +| GET | `/api/stress-events` | Liste tous les événements de stress (Holmes & Rahe) | +| POST | `/api/stress-diagnostics` | Enregistre un diagnostic de stress | +| GET | `/api/emotions` | Historique émotionnel de l'utilisateur | +| POST | `/api/emotions` | Enregistre une émotion | +| GET | `/api/informations` | Liste des articles publiés | +| GET | `/api/informations/{id}` | Détail d'un article | + +### 3.6 Administration Filament + +Le panel d'administration est accessible à `/admin` uniquement pour les utilisateurs avec le rôle `Admin` et `is_active = true`. + +**Ressource disponible :** +- **UserResource** : gestion complète des utilisateurs (CRUD, activation/désactivation, assignation de rôle) + +### 3.7 Services + +#### `StressCalculator` + +Contient la logique métier du diagnostic de stress (échelle Holmes & Rahe). + +```php +$calculator = new StressCalculator(); +$score = $calculator->calculateScore($points); // somme des points des événements cochés +$level = $calculator->determineLevel($score); // 'Faible', 'Modéré', ou 'Élevé' +``` + +Seuils : +- **< 150 points** → Niveau Faible +- **150–299 points** → Niveau Modéré +- **≥ 300 points** → Niveau Élevé + +--- + +## 4. Application Mobile (Flutter) + +### 4.1 Stack technique + +| Élément | Technologie | +|---|---| +| Framework | Flutter 3 / Dart | +| State management | Provider | +| Navigation | go_router | +| Requêtes HTTP | Dio | +| Stockage sécurisé | flutter_secure_storage | +| Internationalisation | intl | + +### 4.2 Architecture + +L'application suit une architecture feature-first avec séparation claire des responsabilités : + +``` +mobile/lib/ +├── core/ +│ └── network/ ← Client HTTP Dio + intercepteurs (token) +├── features/ +│ ├── auth/ ← Connexion / Inscription +│ │ ├── models/ +│ │ ├── providers/ ← AuthProvider (gère le token Sanctum) +│ │ ├── screens/ +│ │ └── services/ ← Appels API auth +│ ├── diagnostics/ ← Diagnostic de stress +│ │ ├── models/ +│ │ ├── providers/ +│ │ ├── screens/ +│ │ └── services/ +│ ├── exercises/ ← Activités de relaxation +│ │ ├── models/ +│ │ ├── providers/ +│ │ ├── screens/ +│ │ └── services/ +│ ├── informations/ ← Articles bien-être +│ │ ├── models/ +│ │ ├── providers/ +│ │ ├── screens/ +│ │ └── services/ +│ ├── relaxation/ ← Catalogue relaxation + favoris +│ │ ├── models/ +│ │ ├── providers/ +│ │ ├── screens/ +│ │ └── services/ +│ ├── tracker/ ← Tracker émotionnel (+ sync Wear OS) +│ │ ├── models/ +│ │ ├── providers/ +│ │ ├── screens/ +│ │ └── services/ +│ └── wear/ ← Communication Wearable Data Layer +│ ├── models/ +│ ├── providers/ +│ ├── screens/ +│ └── services/ +└── main.dart +``` + +### 4.3 Fonctionnalités + +| Fonctionnalité | Description | +|---|---| +| **Authentification** | Connexion / inscription avec stockage sécurisé du token | +| **Diagnostic de stress** | Questionnaire Holmes & Rahe, calcul du score, historique | +| **Tracker émotionnel** | Enregistrement quotidien de l'humeur avec intensité et note | +| **Relaxation** | Catalogue d'activités filtrables, gestion des favoris | +| **Informations** | Lecture des articles bien-être publiés | +| **Sync Wear OS** | Transmission de l'humeur et du statut d'auth vers la montre | + +**Flux d'authentification :** +1. L'utilisateur se connecte → le token Sanctum est stocké dans `flutter_secure_storage` +2. `AuthProvider` expose l'état de connexion à toute l'app via `Provider` +3. `go_router` redirige automatiquement selon l'état d'authentification + +--- + +## 5. Application Wear OS + +### 5.1 Stack technique + +| Élément | Technologie | +|---|---| +| Langage | Kotlin | +| UI | Jetpack Compose for Wear OS | +| Communication | Wearable Data Layer API (Google) | +| Composants UI | `androidx.wear.compose.material3` | + +### 5.2 Fonctionnement + +L'application Wear OS est **dépendante de l'app Flutter** installée sur le téléphone couplé. Elle ne communique pas directement avec l'API Laravel. + +**Architecture de communication :** + +``` +[Wear OS App] ←──── Wearable Data Layer (Bluetooth/Wi-Fi) ────→ [App Flutter] + ↕ + [API Laravel REST] +``` + +**Canal de communication :** +- Chemin `/wearable_communication` : messages envoyés de la montre vers le téléphone (commandes) +- Chemin `/auth_status` : données synchronisées téléphone → montre (nom d'utilisateur, statut) +- Chemin `/daily_mood` : données synchronisées téléphone → montre (humeur du jour) + +**Commandes supportées :** + +| Commande | Description | +|---|---| +| `get_sync_data` | Demande au téléphone de synchroniser les données (auth + humeur) | +| `save_mood` | Envoie l'humeur sélectionnée sur la montre pour enregistrement via l'API | + +**Écran principal (`WearApp`) :** + +- Si l'utilisateur **n'est pas connecté** sur le téléphone → affiche "Connexion requise" +- Si connecté et **aucune humeur du jour** → propose 5 choix d'humeur (Très bien, Bien, Neutre, Pas top, Stressé) +- Si une humeur est déjà enregistrée → affiche l'humeur du jour avec option de modification + +**Connectivité :** +La montre utilise la priorité réseau suivante : Wi-Fi → Bluetooth (pont vers le téléphone). + +--- + +## 6. Base de données + +### Schéma simplifié + +``` +roles ─────────────────────────── users + id, libelle id, name, email, password, id_role, is_active + │ + ┌───────────────┼───────────────────┐ + │ │ │ + emotion_records resultat_diags favorites + user_id, emotion user_id, score id_user, id_activite + intensity, note level, events (JSON) + │ + relaxation_activities + title, type, duration, url + +stress_events information + event_name, points title, content, category, image_url, is_published +``` + +### Migrations (ordre chronologique) + +| Migration | Table créée | +|---|---| +| `000001` | `roles` | +| `000002` | `users` | +| `000003` | `stress_events` | +| `000004` | `relaxation_activities` | +| `000005` | `resultat_diags` | +| `000006` | `emotion_records` | +| `000007` | `personal_access_tokens` (Sanctum) | +| `103734` | `cache`, `sessions` | +| `111910/111917` | Colonne `is_active` sur `users` | +| `141553` | `information` | + +--- + +## 7. Déploiement + +### Environnement Docker (WEB) + +Le projet Laravel embarque une configuration Docker prête à l'emploi : + +``` +WEB/ +├── Dockerfile ← Image PHP-FPM +├── docker-compose.yml ← Services : app + nginx +└── nginx.conf ← Configuration Nginx +``` + +**Démarrer l'environnement :** + +```bash +cd WEB +docker-compose up -d +php artisan migrate --seed +``` + +### Application Mobile + +```bash +cd mobile +flutter pub get +flutter run +``` + +Configurer l'URL de l'API dans le fichier de configuration réseau (`lib/core/network/`). + +### Application Wear OS + +Ouvrir le dossier `Wear/` dans **Android Studio**, puis lancer sur une montre Wear OS physique ou un émulateur. + +> La montre doit être couplée à un téléphone Android ayant l'application Flutter installée. diff --git a/docs/Documentation_Technique_CESIZen.docx b/docs/Documentation_Technique_CESIZen.docx new file mode 100644 index 0000000..8ce5e68 Binary files /dev/null and b/docs/Documentation_Technique_CESIZen.docx differ diff --git a/docs/Documentation_Technique_CESIZen.pdf b/docs/Documentation_Technique_CESIZen.pdf new file mode 100644 index 0000000..b161730 Binary files /dev/null and b/docs/Documentation_Technique_CESIZen.pdf differ diff --git a/docs/Dossier_Deploiement_Securisation_CESIZen.docx b/docs/Dossier_Deploiement_Securisation_CESIZen.docx new file mode 100644 index 0000000..5c3c547 Binary files /dev/null and b/docs/Dossier_Deploiement_Securisation_CESIZen.docx differ diff --git a/docs/Validation et PV.pdf b/docs/Validation et PV.pdf new file mode 100644 index 0000000..230b3f5 Binary files /dev/null and b/docs/Validation et PV.pdf differ diff --git a/docs/img/diag_archi.png b/docs/img/diag_archi.png new file mode 100644 index 0000000..97b553c Binary files /dev/null and b/docs/img/diag_archi.png differ diff --git a/docs/img/diag_cicd.png b/docs/img/diag_cicd.png new file mode 100644 index 0000000..996faff Binary files /dev/null and b/docs/img/diag_cicd.png differ diff --git a/mobile/.gitignore b/mobile/.gitignore index 6f0d006..de2f783 100644 --- a/mobile/.gitignore +++ b/mobile/.gitignore @@ -43,3 +43,5 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + +.idea \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..34b677b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,168 @@ +{ + "name": "CESIZen", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "pptxgenjs": "^4.0.1" + } + }, + "node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/https": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz", + "integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==", + "license": "ISC" + }, + "node_modules/image-size": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", + "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", + "license": "MIT", + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/pptxgenjs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pptxgenjs/-/pptxgenjs-4.0.1.tgz", + "integrity": "sha512-TeJISr8wouAuXw4C1F/mC33xbZs/FuEG6nH9FG1Zj+nuPcGMP5YRHl6X+j3HSUnS1f3at6k75ZZXPMZlA5Lj9A==", + "license": "MIT", + "dependencies": { + "@types/node": "^22.8.1", + "https": "^1.0.0", + "image-size": "^1.2.1", + "jszip": "^3.10.1" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.3" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b0464f4 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "pptxgenjs": "^4.0.1" + } +} diff --git a/presentation.js b/presentation.js new file mode 100644 index 0000000..a7883c9 --- /dev/null +++ b/presentation.js @@ -0,0 +1,862 @@ +const pptxgen = require("pptxgenjs"); + +const pres = new pptxgen(); +pres.layout = "LAYOUT_16x9"; // 10" x 5.625" +pres.author = "Sam DEPARDIEU"; +pres.title = "CESIZen — Présentation CDA CESI"; + +// ─── Palette (strictement calquée sur la réf) ─────────────── +const C = { + darkBg: "0D1B2A", + lightBg: "F7F7F2", + teal: "00BF63", + yellow: "FFDE59", + dark: "2D2D2D", + navy: "1B2838", + white: "FFFFFF", + gray: "6B7280", + border: "E5E7EB", + offWhiteAlt: "F0F0EC", +}; + +// ─── Helpers ──────────────────────────────────────────────── + +function topBar(s) { + s.addShape(pres.shapes.RECTANGLE, { + x: 0, y: 0, w: 10, h: 0.09, + fill: { color: C.teal }, line: { color: C.teal }, + }); +} + +function vLine(s, x, y, h, color) { + s.addShape(pres.shapes.RECTANGLE, { + x, y, w: 0.07, h, + fill: { color: color || C.teal }, + line: { color: color || C.teal }, + }); +} + +function hLine(s, x, y, w, color) { + s.addShape(pres.shapes.RECTANGLE, { + x, y, w, h: 0.02, + fill: { color: color || C.border }, + line: { color: color || C.border }, + }); +} + +function tealBar(s, x, y, w) { + s.addShape(pres.shapes.RECTANGLE, { + x, y, w, h: 0.07, + fill: { color: C.teal }, line: { color: C.teal }, + }); +} + +// Entête standard (slides claires) +function header(s, num, section, title) { + topBar(s); + // numéro de slide + s.addText(String(num).padStart(2, "0"), { + x: 9.0, y: 0.11, w: 0.82, h: 0.38, + fontSize: 14, color: C.teal, fontFace: "Calibri", + bold: true, align: "right", margin: 0, + }); + // section label + s.addText(section.toUpperCase(), { + x: 0.65, y: 0.13, w: 7, h: 0.26, + fontSize: 9, color: C.gray, fontFace: "Calibri", + charSpacing: 2, margin: 0, + }); + // ligne jaune accent + s.addShape(pres.shapes.RECTANGLE, { + x: 0.65, y: 0.44, w: 0.55, h: 0.07, + fill: { color: C.yellow }, line: { color: C.yellow }, + }); + // titre + s.addText(title, { + x: 0.65, y: 0.52, w: 8.7, h: 0.7, + fontSize: 26, color: C.darkBg, fontFace: "Calibri", + bold: false, margin: 0, + }); + hLine(s, 0.65, 1.25, 8.8); +} + +// Entête slides sombres +function darkHeader(s, num, section, title) { + topBar(s); + s.addText(String(num).padStart(2, "0"), { + x: 9.0, y: 0.11, w: 0.82, h: 0.38, + fontSize: 14, color: C.teal, fontFace: "Calibri", + bold: true, align: "right", margin: 0, + }); + s.addText(section.toUpperCase(), { + x: 0.65, y: 0.13, w: 7, h: 0.26, + fontSize: 9, color: "4B5563", fontFace: "Calibri", + charSpacing: 2, margin: 0, + }); + s.addShape(pres.shapes.RECTANGLE, { + x: 0.65, y: 0.44, w: 0.55, h: 0.07, + fill: { color: C.yellow }, line: { color: C.yellow }, + }); + s.addText(title, { + x: 0.65, y: 0.52, w: 8.7, h: 0.7, + fontSize: 26, color: C.white, fontFace: "Calibri", + bold: false, margin: 0, + }); + hLine(s, 0.65, 1.25, 8.8, "374151"); +} + +// ─── SLIDE 01 — Couverture ────────────────────────────────── +{ + const s = pres.addSlide(); + s.background = { color: C.darkBg }; + topBar(s); + + // Accent jaune + s.addShape(pres.shapes.RECTANGLE, { + x: 0.7, y: 0.72, w: 0.55, h: 0.09, + fill: { color: C.yellow }, line: { color: C.yellow }, + }); + + // Titre principal + s.addText("CESIZEN", { + x: 0.7, y: 0.83, w: 8.5, h: 1.45, + fontSize: 56, bold: false, color: C.white, + fontFace: "Calibri", margin: 0, + }); + + // Sous-titre + s.addText("L'application de votre santé mentale", { + x: 0.7, y: 2.35, w: 8.5, h: 0.5, + fontSize: 18, color: C.teal, fontFace: "Calibri", + italic: false, margin: 0, + }); + + hLine(s, 0.7, 2.98, 8.6, "374151"); + + s.addText("Projet CDA CESI — BLOC 2 · Concevoir les solutions logicielles", { + x: 0.7, y: 3.1, w: 8.5, h: 0.38, + fontSize: 12, color: "D1D5DB", fontFace: "Calibri", margin: 0, + }); + s.addText("Sam DEPARDIEU", { + x: 0.7, y: 3.52, w: 5, h: 0.38, + fontSize: 13, color: C.yellow, fontFace: "Calibri", margin: 0, + }); + s.addText("Mai 2026", { + x: 0.7, y: 3.92, w: 4, h: 0.35, + fontSize: 12, color: C.gray, fontFace: "Calibri", margin: 0, + }); + + // Panel droit (teal) + s.addShape(pres.shapes.RECTANGLE, { + x: 6.95, y: 0.09, w: 3.05, h: 5.535, + fill: { color: C.teal }, line: { color: C.teal }, + }); + s.addText("Périmètre", { + x: 7.1, y: 0.28, w: 2.75, h: 0.44, + fontSize: 13, bold: true, color: C.white, fontFace: "Calibri", margin: 0, + }); + hLine(s, 7.1, 0.74, 2.7, "00995A"); + + [ + { label: "WEB", teal: false }, + { label: "Mobile (iOS & Android)",teal: false }, + { label: "API REST", teal: false }, + { label: "Admin dashboard", teal: false }, + { label: "⌚ Surprise...", teal: true }, + ].forEach((item, i) => { + vLine(s, 7.1, 0.92 + i * 0.82, 0.6, item.teal ? C.yellow : C.white); + s.addText(item.label, { + x: 7.28, y: 1.0 + i * 0.82, w: 2.55, h: 0.44, + fontSize: 12, color: item.teal ? C.yellow : C.white, + fontFace: "Calibri", italic: item.teal, margin: 0, + }); + }); +} + +// ─── SLIDE 02 — Sommaire ──────────────────────────────────── +{ + const s = pres.addSlide(); + s.background = { color: C.lightBg }; + topBar(s); + + s.addShape(pres.shapes.RECTANGLE, { + x: 0.65, y: 0.37, w: 0.55, h: 0.09, + fill: { color: C.yellow }, line: { color: C.yellow }, + }); + s.addText("SOMMAIRE", { + x: 0.65, y: 0.47, w: 8, h: 0.88, + fontSize: 34, color: C.darkBg, fontFace: "Calibri", bold: false, margin: 0, + }); + hLine(s, 0.65, 1.38, 8.8); + + const toc = [ + "Contexte & objectifs", + "L'écosystème CESIZen", + "Plateforme WEB (Laravel 12)", + "Application Mobile (Flutter)", + "Architecture technique", + "Modèle de données", + "Tests, sécurité & validation", + "Surprise", + "Bilan & livrables", + ]; + + toc.forEach((item, i) => { + s.addText(String(i + 1).padStart(2, "0"), { + x: 0.65, y: 1.5 + i * 0.45, w: 0.65, h: 0.38, + fontSize: 16, color: C.teal, fontFace: "Calibri", bold: false, margin: 0, + }); + s.addText(item, { + x: 1.38, y: 1.53 + i * 0.45, w: 5.0, h: 0.36, + fontSize: 13.5, color: C.dark, fontFace: "Calibri", margin: 0, + }); + }); + + // Panel teal droit + s.addShape(pres.shapes.RECTANGLE, { + x: 6.95, y: 0.09, w: 3.05, h: 5.535, + fill: { color: C.teal }, line: { color: C.teal }, + }); + s.addText("3 applications", { + x: 7.1, y: 0.3, w: 2.75, h: 0.44, + fontSize: 13, bold: true, color: C.white, fontFace: "Calibri", margin: 0, + }); + ["WEB", "Mobile", "Wear OS"].forEach((item, i) => { + s.addText("— " + item, { + x: 7.1, y: 0.88 + i * 0.5, w: 2.75, h: 0.4, + fontSize: 12, color: C.white, fontFace: "Calibri", margin: 0, + }); + }); + + hLine(s, 7.1, 2.62, 2.7, "00995A"); + + s.addText("Évaluation", { + x: 7.1, y: 2.75, w: 2.75, h: 0.38, + fontSize: 12, bold: true, color: C.white, fontFace: "Calibri", margin: 0, + }); + [ + "20 min de présentation", + "Rapport 15-20 pages", + "30 pts — Grille A/B/C/D", + ].forEach((line, i) => { + s.addText(line, { + x: 7.1, y: 3.2 + i * 0.45, w: 2.75, h: 0.38, + fontSize: 11, color: "D1D5DB", fontFace: "Calibri", margin: 0, + }); + }); +} + +// ─── SLIDE 03 — Contexte ──────────────────────────────────── +{ + const s = pres.addSlide(); + s.background = { color: C.lightBg }; + header(s, "03", "Contexte du projet", "Contexte & objectifs"); + + // Bloc gauche : description + vLine(s, 0.55, 1.38, 3.75); + s.addText( + "Commanditaire : Ministère de la Santé et de la Prévention\n" + + "\n" + + "Application dédiée à la gestion du stress et du bien-être mental pour le grand public.\n" + + "\n" + + "Durée du projet : 3 mois de développement individuel\n" + + "\n" + + "Conformité RGPD & protection des données personnelles exigées.", + { + x: 0.78, y: 1.4, w: 5.2, h: 3.7, + fontSize: 12.5, color: C.dark, fontFace: "Calibri", margin: 0, + } + ); + + // Bloc droit : fonctionnalités + s.addShape(pres.shapes.RECTANGLE, { + x: 6.3, y: 1.38, w: 3.45, h: 4.0, + fill: { color: C.white }, line: { color: C.border, width: 1 }, + }); + tealBar(s, 6.3, 1.38, 3.45); + + s.addText("Fonctionnalités requises", { + x: 6.45, y: 1.55, w: 3.15, h: 0.38, + fontSize: 11.5, bold: true, color: C.darkBg, fontFace: "Calibri", margin: 0, + }); + hLine(s, 6.35, 1.94, 3.3); + + s.addText("OBLIGATOIRES", { + x: 6.45, y: 2.02, w: 3.15, h: 0.28, + fontSize: 9, bold: true, color: C.teal, fontFace: "Calibri", + charSpacing: 1, margin: 0, + }); + [ + "Questionnaire de stress", + "Exercices de respiration", + "Tracker d'émotions", + ].forEach((item, i) => { + s.addText("— " + item, { + x: 6.5, y: 2.34 + i * 0.38, w: 3.05, h: 0.33, + fontSize: 11.5, color: C.dark, fontFace: "Calibri", margin: 0, + }); + }); + + hLine(s, 6.35, 3.5, 3.3); + + s.addText("OPTIONNEL", { + x: 6.45, y: 3.58, w: 3.15, h: 0.28, + fontSize: 9, bold: true, color: C.teal, fontFace: "Calibri", + charSpacing: 1, margin: 0, + }); + s.addText("— Module complémentaire", { + x: 6.5, y: 3.9, w: 3.05, h: 0.33, + fontSize: 11.5, color: C.dark, fontFace: "Calibri", margin: 0, + }); + + // Barre bas + s.addShape(pres.shapes.RECTANGLE, { + x: 0.55, y: 5.22, w: 9.2, h: 0.52, + fill: { color: C.darkBg }, line: { color: C.darkBg }, + }); + s.addText( + "Rapport écrit (15-20 pages) · Présentation orale 20 min · Notation sur 30 points", + { + x: 0.7, y: 5.24, w: 9.0, h: 0.46, + fontSize: 11, color: "D1D5DB", align: "center", + fontFace: "Calibri", margin: 0, valign: "middle", + } + ); +} + +// ─── SLIDE 04 — L'écosystème ──────────────────────────────── +{ + const s = pres.addSlide(); + s.background = { color: C.lightBg }; + header(s, "04", "Périmètre", "L'écosystème CESIZen"); + + // 3 colonnes + const cols = [ + { + title: "WEB", sub: "Plateforme & API", + items: ["Laravel 12 — MVC", "FilamentPHP (admin)", "API REST Sanctum", "Tailwind CSS + DSFR", "Docker + Nginx"], + mystery: false, + }, + { + title: "Mobile", sub: "iOS & Android", + items: ["Flutter (Dart)", "7 modules fonctionnels", "Provider + GoRouter", "DIO + Secure Storage", "Synchronisation montre"], + mystery: false, + }, + { + title: "???", sub: "À venir...", + items: ["?", "?", "?", "?", "?"], + mystery: true, + }, + ]; + + const cW = 2.9; + cols.forEach((col, i) => { + const cx = 0.55 + i * (cW + 0.2); + + tealBar(s, cx, 1.38, cW); + s.addShape(pres.shapes.RECTANGLE, { + x: cx, y: 1.45, w: cW, h: 3.95, + fill: { color: col.mystery ? C.lightBg : C.white }, + line: { color: col.mystery ? "D1D5DB" : C.border, width: 1 }, + }); + + s.addText(col.title, { + x: cx + 0.15, y: 1.55, w: cW - 0.3, h: 0.5, + fontSize: 20, bold: true, + color: col.mystery ? "9CA3AF" : C.darkBg, + fontFace: "Calibri", margin: 0, + }); + s.addText(col.sub, { + x: cx + 0.15, y: 2.05, w: cW - 0.3, h: 0.3, + fontSize: 11, color: col.mystery ? "9CA3AF" : C.teal, + fontFace: "Calibri", italic: col.mystery, margin: 0, + }); + hLine(s, cx + 0.1, 2.4, cW - 0.2); + + col.items.forEach((item, ii) => { + s.addText(col.mystery ? "· · · · ·" : "— " + item, { + x: cx + 0.15, y: 2.5 + ii * 0.47, w: cW - 0.3, h: 0.4, + fontSize: 11.5, color: col.mystery ? "D1D5DB" : C.dark, + fontFace: "Calibri", margin: 0, + }); + }); + }); + + s.addText( + "Les 3 composantes communiquent via l'API REST — Laravel Sanctum + HTTPS", + { + x: 0.65, y: 5.3, w: 9.0, h: 0.24, + fontSize: 10, color: C.gray, align: "center", + fontFace: "Calibri", margin: 0, + } + ); +} + +// ─── SLIDE 05 — Plateforme WEB ────────────────────────────── +{ + const s = pres.addSlide(); + s.background = { color: C.lightBg }; + header(s, "05", "Plateforme WEB", "Laravel 12 — Architecture & fonctionnalités"); + + // Gauche : stack + tealBar(s, 0.55, 1.38, 4.45); + s.addText("Stack technique", { + x: 0.55, y: 1.52, w: 4.45, h: 0.38, + fontSize: 13, bold: true, color: C.darkBg, fontFace: "Calibri", margin: 0, + }); + hLine(s, 0.55, 1.92, 4.45); + + [ + ["Framework", "Laravel 12 — PHP 8.2"], + ["Admin", "FilamentPHP 3.2"], + ["Auth API", "Laravel Sanctum"], + ["Frontend", "Tailwind CSS 4 + Alpine.js"], + ["Charte UI", "DSFR (Design Système État)"], + ["Base de données", "MySQL 8.0"], + ["Déploiement", "Docker + Nginx (Alpine)"], + ].forEach(([label, val], i) => { + const ry = 2.0 + i * 0.47; + s.addShape(pres.shapes.RECTANGLE, { + x: 0.55, y: ry, w: 4.45, h: 0.44, + fill: { color: i % 2 === 0 ? C.white : C.offWhiteAlt }, + line: { color: C.border, width: 0 }, + }); + s.addText(label, { + x: 0.7, y: ry + 0.06, w: 1.5, h: 0.3, + fontSize: 11, bold: true, color: C.darkBg, fontFace: "Calibri", margin: 0, + }); + s.addText(val, { + x: 2.25, y: ry + 0.06, w: 2.7, h: 0.3, + fontSize: 11, color: C.dark, fontFace: "Calibri", margin: 0, + }); + }); + + // Droite : fonctionnalités + tealBar(s, 5.3, 1.38, 4.4); + s.addText("Fonctionnalités", { + x: 5.3, y: 1.52, w: 4.4, h: 0.38, + fontSize: 13, bold: true, color: C.darkBg, fontFace: "Calibri", margin: 0, + }); + hLine(s, 5.3, 1.92, 4.4); + + [ + "Authentification sécurisée (Sanctum)", + "Dashboard de visualisation du stress", + "Exercices de respiration (3 modes DSFR)", + "Journal des émotions quotidien", + "Questionnaire diagnostique de stress", + "Articles & informations santé", + "Panel admin — CRUD complet", + ].forEach((item, i) => { + vLine(s, 5.3, 2.06 + i * 0.48, 0.36); + s.addText(item, { + x: 5.52, y: 2.09 + i * 0.48, w: 4.1, h: 0.36, + fontSize: 12, color: C.dark, fontFace: "Calibri", margin: 0, + }); + }); +} + +// ─── SLIDE 06 — Application Mobile ───────────────────────── +{ + const s = pres.addSlide(); + s.background = { color: C.lightBg }; + header(s, "06", "Application Mobile", "Flutter — 7 modules fonctionnels"); + + const modules = [ + { n: "01", title: "Authentification", sub: "Token persistant (Secure Storage)" }, + { n: "02", title: "Tracker émotionnel", sub: "5 niveaux · Journal quotidien" }, + { n: "03", title: "Diagnostic stress", sub: "Questionnaire + historique des résultats" }, + { n: "04", title: "Respiration guidée", sub: "Animation 60 FPS · Cohérence cardiaque" }, + { n: "05", title: "Relaxation", sub: "Bibliothèque d'activités filtrables" }, + { n: "06", title: "Informations santé", sub: "Articles & conseils bien-être" }, + { n: "07", title: "Sync montre", sub: "Wearable Data Layer (bonus)", bonus: true }, + ]; + + // 4 gauche + 3 droite + modules.slice(0, 4).forEach((m, i) => { + const my = 1.38 + i * 0.99; + s.addShape(pres.shapes.RECTANGLE, { + x: 0.55, y: my, w: 4.45, h: 0.88, + fill: { color: C.white }, line: { color: C.border, width: 1 }, + }); + vLine(s, 0.55, my, 0.88); + s.addText(m.n, { + x: 0.72, y: my + 0.07, w: 0.55, h: 0.35, + fontSize: 15, color: C.teal, fontFace: "Calibri", bold: false, margin: 0, + }); + s.addText(m.title, { + x: 1.3, y: my + 0.09, w: 3.5, h: 0.35, + fontSize: 13, bold: true, color: C.darkBg, fontFace: "Calibri", margin: 0, + }); + s.addText(m.sub, { + x: 1.3, y: my + 0.49, w: 3.5, h: 0.3, + fontSize: 10.5, color: C.gray, fontFace: "Calibri", margin: 0, + }); + }); + + modules.slice(4).forEach((m, i) => { + const my = 1.38 + i * 0.99; + s.addShape(pres.shapes.RECTANGLE, { + x: 5.25, y: my, w: 4.45, h: 0.88, + fill: { color: m.bonus ? "F7F3E0" : C.white }, + line: { color: m.bonus ? "E8DFA0" : C.border, width: 1 }, + }); + vLine(s, 5.25, my, 0.88, m.bonus ? C.yellow : C.teal); + s.addText(m.n, { + x: 5.42, y: my + 0.07, w: 0.55, h: 0.35, + fontSize: 15, color: m.bonus ? C.yellow : C.teal, + fontFace: "Calibri", bold: false, margin: 0, + }); + s.addText(m.title, { + x: 6.0, y: my + 0.09, w: 3.5, h: 0.35, + fontSize: 13, bold: true, color: C.darkBg, fontFace: "Calibri", margin: 0, + }); + s.addText(m.sub, { + x: 6.0, y: my + 0.49, w: 3.5, h: 0.3, + fontSize: 10.5, color: m.bonus ? C.teal : C.gray, + fontFace: "Calibri", italic: m.bonus, margin: 0, + }); + }); +} + +// ─── SLIDE 07 — Architecture technique ───────────────────── +{ + const s = pres.addSlide(); + s.background = { color: C.lightBg }; + header(s, "07", "Architecture technique", "Architecture 3-tiers & choix techniques"); + + // 3 colonnes + const tiers = [ + { name: "Clients", items: ["Navigateur WEB", "App Mobile Flutter", "Wear OS (Kotlin)"] }, + { name: "Serveur Docker", items: ["Nginx (reverse proxy)", "Laravel 12 / PHP 8.2", "Sanctum (auth tokens)", "FilamentPHP (admin)"] }, + { name: "Données", items: ["MySQL 8.0", "12 modèles Eloquent", "Migrations + Seeders"] }, + ]; + const widths = [2.55, 3.55, 2.95]; + + let cx = 0.5; + tiers.forEach((tier, i) => { + const tw = widths[i]; + tealBar(s, cx, 1.38, tw); + s.addShape(pres.shapes.RECTANGLE, { + x: cx, y: 1.45, w: tw, h: 3.85, + fill: { color: C.white }, line: { color: C.border, width: 1 }, + }); + s.addText(tier.name, { + x: cx + 0.15, y: 1.55, w: tw - 0.3, h: 0.42, + fontSize: 14, bold: true, color: C.darkBg, fontFace: "Calibri", margin: 0, + }); + hLine(s, cx + 0.1, 1.98, tw - 0.2); + + tier.items.forEach((item, ii) => { + s.addText("— " + item, { + x: cx + 0.15, y: 2.08 + ii * 0.62, w: tw - 0.3, h: 0.52, + fontSize: 12, color: C.dark, fontFace: "Calibri", margin: 0, + }); + }); + + if (i < tiers.length - 1) { + s.addText("→", { + x: cx + tw + 0.04, y: 2.75, w: 0.2, h: 0.38, + fontSize: 15, color: C.teal, fontFace: "Calibri", align: "center", margin: 0, + }); + } + cx += tw + 0.22; + }); + + // Note bas + s.addText( + "Pattern MVC · API RESTful · Merise / UML · Ports : 8090 (WEB) · 8091 (API)", + { + x: 0.65, y: 5.33, w: 9.0, h: 0.22, + fontSize: 9.5, color: C.gray, align: "center", + fontFace: "Calibri", margin: 0, + } + ); +} + +// ─── SLIDE 08 — Modèle de données ────────────────────────── +{ + const s = pres.addSlide(); + s.background = { color: C.lightBg }; + header(s, "08", "Modèle de données", "12 entités — Modèle Logique de Données"); + + const groupsLeft = [ + { domain: "Utilisateurs", models: ["User", "Role"] }, + { domain: "Suivi émotionnel", models: ["EmotionRecord", "StressEvent", "EvenementStress"] }, + { domain: "Diagnostics", models: ["ResultatDiag", "DetailResultat"] }, + ]; + const groupsRight = [ + { domain: "Activités & Relaxation", models: ["RelaxationActivity", "Activite", "Categorie", "Favorite"] }, + { domain: "Informations santé", models: ["Information"] }, + ]; + + let y = 1.38; + groupsLeft.forEach((g) => { + const h = 0.44 + g.models.length * 0.36; + vLine(s, 0.55, y, h); + s.addText(g.domain, { + x: 0.77, y: y + 0.05, w: 4.1, h: 0.32, + fontSize: 12, bold: true, color: C.teal, fontFace: "Calibri", margin: 0, + }); + g.models.forEach((m, mi) => { + s.addText(m, { + x: 0.77, y: y + 0.4 + mi * 0.35, w: 4.1, h: 0.3, + fontSize: 11.5, color: C.dark, fontFace: "Calibri", margin: 0, + }); + }); + y += h + 0.22; + }); + + let y2 = 1.38; + groupsRight.forEach((g) => { + const h = 0.44 + g.models.length * 0.36; + vLine(s, 5.25, y2, h); + s.addText(g.domain, { + x: 5.47, y: y2 + 0.05, w: 4.2, h: 0.32, + fontSize: 12, bold: true, color: C.teal, fontFace: "Calibri", margin: 0, + }); + g.models.forEach((m, mi) => { + s.addText(m, { + x: 5.47, y: y2 + 0.4 + mi * 0.35, w: 4.2, h: 0.3, + fontSize: 11.5, color: C.dark, fontFace: "Calibri", margin: 0, + }); + }); + y2 += h + 0.22; + }); + + s.addText( + "12 migrations Eloquent · Associations (hasMany, belongsTo, belongsToMany) · Seeders & Factories", + { + x: 0.65, y: 5.33, w: 9.0, h: 0.22, + fontSize: 9.5, color: C.gray, align: "center", + fontFace: "Calibri", margin: 0, + } + ); +} + +// ─── SLIDE 09 — Tests & Sécurité ─────────────────────────── +{ + const s = pres.addSlide(); + s.background = { color: C.lightBg }; + header(s, "09", "Tests & Sécurité", "Validation & conformité"); + + // Gauche : tests + tealBar(s, 0.55, 1.38, 4.35); + s.addText("Stratégie de tests", { + x: 0.55, y: 1.52, w: 4.35, h: 0.38, + fontSize: 13, bold: true, color: C.darkBg, fontFace: "Calibri", margin: 0, + }); + hLine(s, 0.55, 1.92, 4.35); + + [ + { type: "Tests unitaires", tool: "PHPUnit", note: "Controllers & services" }, + { type: "Tests fonctionnels", tool: "PHPUnit", note: "Scénarios end-to-end" }, + { type: "Non-régression", tool: "PHPUnit", note: "Après chaque évolution" }, + { type: "Cahier de tests", tool: "Document", note: "Scénarios détaillés" }, + { type: "PV de recette", tool: "Document", note: "Validation finale" }, + ].forEach((t, i) => { + const ty = 2.02 + i * 0.62; + vLine(s, 0.55, ty, 0.52); + s.addText(t.type, { + x: 0.77, y: ty + 0.04, w: 2.5, h: 0.28, + fontSize: 12, bold: true, color: C.dark, fontFace: "Calibri", margin: 0, + }); + s.addText("[" + t.tool + "] " + t.note, { + x: 0.77, y: ty + 0.3, w: 4.0, h: 0.22, + fontSize: 10, color: C.gray, fontFace: "Calibri", margin: 0, + }); + }); + + // Droite : sécurité + tealBar(s, 5.25, 1.38, 4.45); + s.addText("Sécurité & conformité", { + x: 5.25, y: 1.52, w: 4.45, h: 0.38, + fontSize: 13, bold: true, color: C.darkBg, fontFace: "Calibri", margin: 0, + }); + hLine(s, 5.25, 1.92, 4.45); + + [ + { label: "Authentification", val: "Laravel Sanctum — Tokens API" }, + { label: "Chiffrement", val: "Bcrypt (mots de passe)" }, + { label: "CSRF", val: "Middleware Laravel natif" }, + { label: "RGPD", val: "Protection données personnelles" }, + { label: "Stockage mobile", val: "Flutter Secure Storage" }, + { label: "Transport", val: "HTTPS obligatoire en production" }, + ].forEach((sec, i) => { + const ty = 2.02 + i * 0.55; + s.addShape(pres.shapes.RECTANGLE, { + x: 5.25, y: ty, w: 4.45, h: 0.48, + fill: { color: i % 2 === 0 ? C.white : C.offWhiteAlt }, + line: { color: C.border, width: 0 }, + }); + s.addText(sec.label, { + x: 5.4, y: ty + 0.07, w: 1.7, h: 0.3, + fontSize: 11, bold: true, color: C.darkBg, fontFace: "Calibri", margin: 0, + }); + s.addText(sec.val, { + x: 7.15, y: ty + 0.07, w: 2.5, h: 0.3, + fontSize: 11, color: C.dark, fontFace: "Calibri", margin: 0, + }); + }); +} + +// ─── SLIDE 10 — SURPRISE: Wear OS ────────────────────────── +{ + const s = pres.addSlide(); + s.background = { color: C.darkBg }; + darkHeader(s, "10", "Bonus", "Application Wear OS"); + + s.addText("Un module non demandé — développé en supplément", { + x: 0.65, y: 1.32, w: 9.1, h: 0.35, + fontSize: 13, color: C.teal, fontFace: "Calibri", italic: true, margin: 0, + }); + + // 4 cartes 2x2 + const feats = [ + { title: "Sélection d'humeur", desc: "5 niveaux en 1 clic\nTrès bien → Stressé" }, + { title: "Synchronisation bidirect.", desc: "Temps réel via\nGoogle Wearable Data Layer" }, + { title: "Réinitialisation automatique",desc: "Reset quotidien à minuit\nSuivi journalier indépendant" }, + { title: "Connectivité", desc: "Wi-Fi + Bluetooth\nWear OS 3.0+ · API 30+" }, + ]; + + feats.forEach((f, i) => { + const col = i % 2, row = Math.floor(i / 2); + const fx = 0.6 + col * 4.85; + const fy = 1.78 + row * 1.7; + const fw = 4.55; + + tealBar(s, fx, fy, fw); + s.addShape(pres.shapes.RECTANGLE, { + x: fx, y: fy + 0.07, w: fw, h: 1.5, + fill: { color: C.navy }, line: { color: "374151", width: 1 }, + }); + s.addText(f.title, { + x: fx + 0.2, y: fy + 0.17, w: fw - 0.4, h: 0.42, + fontSize: 13.5, bold: true, color: C.white, fontFace: "Calibri", margin: 0, + }); + s.addText(f.desc, { + x: fx + 0.2, y: fy + 0.64, w: fw - 0.4, h: 0.82, + fontSize: 12, color: "9CA3AF", fontFace: "Calibri", margin: 0, + }); + }); + + s.addText( + "Kotlin · Jetpack Compose for Wear OS · DataClient / MessageClient / NodeClient", + { + x: 0.65, y: 5.35, w: 9.0, h: 0.22, + fontSize: 9.5, color: "374151", align: "center", + fontFace: "Calibri", margin: 0, + } + ); +} + +// ─── SLIDE 11 — Bilan & Livrables ────────────────────────── +{ + const s = pres.addSlide(); + s.background = { color: C.lightBg }; + header(s, "11", "Bilan", "Livrables & chiffres clés"); + + // Gauche : checklist + tealBar(s, 0.55, 1.38, 5.55); + s.addText("Livrables réalisés", { + x: 0.55, y: 1.52, w: 5.55, h: 0.38, + fontSize: 13, bold: true, color: C.darkBg, fontFace: "Calibri", margin: 0, + }); + hLine(s, 0.55, 1.92, 5.55); + + [ + [false, "Plateforme WEB — Laravel 12 + FilamentPHP"], + [false, "API REST complète (Sanctum + 4 controllers)"], + [false, "Application Mobile Flutter (iOS & Android)"], + [false, "Base de données — 12 modèles Eloquent"], + [false, "Dockerisation (Nginx + PHP + MySQL)"], + [false, "Cahier de tests (unitaires, fonctionnels, NR)"], + [false, "Documentation technique complète"], + [false, "Procès-Verbal de validation (PV)"], + [true, "Application Wear OS — Bonus non demandé"], + ].forEach(([isBonus, text], i) => { + s.addText((isBonus ? "⭐" : "✓") + " " + text, { + x: 0.7, y: 2.02 + i * 0.36, w: 5.25, h: 0.32, + fontSize: 11.5, + color: isBonus ? C.teal : C.dark, + bold: isBonus, + fontFace: "Calibri", margin: 0, + }); + }); + + // Droite : chiffres + tealBar(s, 6.35, 1.38, 3.3); + s.addText("Chiffres clés", { + x: 6.35, y: 1.52, w: 3.3, h: 0.38, + fontSize: 13, bold: true, color: C.darkBg, fontFace: "Calibri", margin: 0, + }); + hLine(s, 6.35, 1.92, 3.3); + + [ + { n: "3", l: "Applications\ndéveloppées" }, + { n: "12", l: "Modèles de\nbase de données" }, + { n: "7", l: "Modules\nmobiles" }, + { n: "4", l: "Documents\nlivrés" }, + ].forEach((st, i) => { + const sy = 2.05 + i * 0.88; + vLine(s, 6.35, sy, 0.74); + s.addText(st.n, { + x: 6.6, y: sy + 0.02, w: 0.85, h: 0.7, + fontSize: 32, bold: false, color: C.teal, fontFace: "Calibri", + margin: 0, valign: "middle", + }); + s.addText(st.l, { + x: 7.52, y: sy + 0.1, w: 2.05, h: 0.5, + fontSize: 11, color: C.dark, fontFace: "Calibri", margin: 0, + }); + }); +} + +// ─── SLIDE 12 — Conclusion ────────────────────────────────── +{ + const s = pres.addSlide(); + s.background = { color: C.darkBg }; + topBar(s); + + s.addShape(pres.shapes.RECTANGLE, { + x: 0.7, y: 0.73, w: 0.55, h: 0.09, + fill: { color: C.yellow }, line: { color: C.yellow }, + }); + + s.addText("Merci.", { + x: 0.7, y: 0.84, w: 9, h: 1.45, + fontSize: 56, bold: false, color: C.white, + fontFace: "Calibri", margin: 0, + }); + + hLine(s, 0.7, 2.4, 8.6, "374151"); + + [ + { line: "WEB : Laravel 12 + FilamentPHP + API REST + Docker", col: false }, + { line: "Mobile : Flutter — 7 modules complets (iOS & Android)", col: false }, + { line: "Wear OS : synchronisation bidirectionnelle — Bonus", col: true }, + ].forEach((item, i) => { + vLine(s, 0.7, 2.52 + i * 0.68, 0.5, item.col ? C.yellow : C.teal); + s.addText(item.line, { + x: 0.93, y: 2.59 + i * 0.68, w: 8.8, h: 0.38, + fontSize: 13, color: item.col ? C.yellow : "D1D5DB", + fontFace: "Calibri", margin: 0, + }); + }); + + s.addText("Questions ?", { + x: 0.7, y: 4.3, w: 9, h: 0.7, + fontSize: 30, bold: false, color: C.teal, + fontFace: "Calibri", margin: 0, + }); + + s.addText("CESIZen · CDA CESI BLOC 2 · Sam DEPARDIEU · Mai 2026", { + x: 0.7, y: 5.2, w: 9, h: 0.28, + fontSize: 10, color: "374151", fontFace: "Calibri", margin: 0, + }); +} + +// ─── Save ─────────────────────────────────────────────────── +pres.writeFile({ fileName: "CESIZen_Presentation.pptx" }) + .then(() => console.log("OK — CESIZen_Presentation.pptx")) + .catch((err) => console.error("ERR:", err)); diff --git a/presentation_bloc3.js b/presentation_bloc3.js new file mode 100644 index 0000000..e9d15a1 --- /dev/null +++ b/presentation_bloc3.js @@ -0,0 +1,248 @@ +// ══════════════════════════════════════════════════════════════ +// Soutenance — BLOC 3 (INFCDAAL3) +// « Déployer et sécuriser les applications informatiques » +// Style strictement calqué sur presentation.js (thème CESIZen). +// Génération : node presentation_bloc3.js +// ══════════════════════════════════════════════════════════════ +const pptxgen = require("pptxgenjs"); + +const pres = new pptxgen(); +pres.layout = "LAYOUT_16x9"; +pres.author = "Sam DEPARDIEU"; +pres.title = "CESIZen — Soutenance BLOC 3 (Déploiement & Sécurité)"; + +const C = { + darkBg: "0D1B2A", lightBg: "F7F7F2", teal: "00BF63", yellow: "FFDE59", + dark: "2D2D2D", navy: "1B2838", white: "FFFFFF", gray: "6B7280", + border: "E5E7EB", +}; + +// ─── Helpers (identiques à presentation.js) ───────────────── +const topBar = (s) => s.addShape(pres.shapes.RECTANGLE, { x: 0, y: 0, w: 10, h: 0.09, fill: { color: C.teal }, line: { color: C.teal } }); +const vLine = (s, x, y, h, color) => s.addShape(pres.shapes.RECTANGLE, { x, y, w: 0.07, h, fill: { color: color || C.teal }, line: { color: color || C.teal } }); +const hLine = (s, x, y, w, color) => s.addShape(pres.shapes.RECTANGLE, { x, y, w, h: 0.02, fill: { color: color || C.border }, line: { color: color || C.border } }); + +function header(s, num, section, title) { + topBar(s); + s.addText(String(num).padStart(2, "0"), { x: 9.0, y: 0.11, w: 0.82, h: 0.38, fontSize: 14, color: C.teal, fontFace: "Calibri", bold: true, align: "right", margin: 0 }); + s.addText(section.toUpperCase(), { x: 0.65, y: 0.13, w: 7, h: 0.26, fontSize: 9, color: C.gray, fontFace: "Calibri", charSpacing: 2, margin: 0 }); + s.addShape(pres.shapes.RECTANGLE, { x: 0.65, y: 0.44, w: 0.55, h: 0.07, fill: { color: C.yellow }, line: { color: C.yellow } }); + s.addText(title, { x: 0.65, y: 0.52, w: 8.7, h: 0.7, fontSize: 26, color: C.darkBg, fontFace: "Calibri", bold: false, margin: 0 }); + hLine(s, 0.65, 1.25, 8.8); +} + +// Carte (bloc encadré avec titre + lignes) +function card(s, x, y, w, h, accent, title, lines) { + s.addShape(pres.shapes.RECTANGLE, { x, y, w, h, fill: { color: C.white }, line: { color: C.border, width: 1 } }); + vLine(s, x, y, h, accent); + s.addText(title, { x: x + 0.2, y: y + 0.14, w: w - 0.35, h: 0.35, fontSize: 13.5, bold: true, color: C.darkBg, fontFace: "Calibri", margin: 0 }); + s.addText(lines.map((t) => ({ text: t, options: { bullet: { code: "2022", indent: 12 }, color: C.dark, fontSize: 11, paraSpaceAfter: 4 } })), + { x: x + 0.22, y: y + 0.52, w: w - 0.4, h: h - 0.62, fontFace: "Calibri", valign: "top", margin: 0 }); +} + +// ─── SLIDE 01 — Couverture ────────────────────────────────── +{ + const s = pres.addSlide(); + s.background = { color: C.darkBg }; + topBar(s); + s.addShape(pres.shapes.RECTANGLE, { x: 0.7, y: 0.72, w: 0.55, h: 0.09, fill: { color: C.yellow }, line: { color: C.yellow } }); + s.addText("CESIZEN", { x: 0.7, y: 0.83, w: 8.5, h: 1.25, fontSize: 54, color: C.white, fontFace: "Calibri", margin: 0 }); + s.addText("Déploiement & Sécurisation de l'application", { x: 0.7, y: 2.05, w: 8.5, h: 0.5, fontSize: 18, color: C.teal, fontFace: "Calibri", margin: 0 }); + hLine(s, 0.7, 2.78, 5.9, "374151"); + s.addText("Soutenance CDA CESI — BLOC 3 · Déployer et sécuriser les applications", { x: 0.7, y: 2.9, w: 5.9, h: 0.38, fontSize: 12, color: "D1D5DB", fontFace: "Calibri", margin: 0 }); + s.addText("Réf. : INFCDAAL3", { x: 0.7, y: 3.28, w: 5, h: 0.34, fontSize: 11, color: C.gray, fontFace: "Calibri", margin: 0 }); + s.addText("Sam DEPARDIEU", { x: 0.7, y: 3.72, w: 5, h: 0.38, fontSize: 13, color: C.yellow, fontFace: "Calibri", margin: 0 }); + s.addText("Juillet 2026", { x: 0.7, y: 4.12, w: 4, h: 0.35, fontSize: 12, color: C.gray, fontFace: "Calibri", margin: 0 }); + + // Panel droit teal + s.addShape(pres.shapes.RECTANGLE, { x: 6.95, y: 0.09, w: 3.05, h: 5.535, fill: { color: C.teal }, line: { color: C.teal } }); + s.addText("Au programme", { x: 7.1, y: 0.28, w: 2.75, h: 0.44, fontSize: 13, bold: true, color: C.white, fontFace: "Calibri", margin: 0 }); + hLine(s, 7.1, 0.74, 2.7, "00995A"); + ["Gestion de version", "Évolutions & maintenance", "Plan de déploiement", "CI/CD automatisée", "🔒 Sécurité & RGPD"].forEach((label, i) => { + const hl = label.startsWith("🔒"); + vLine(s, 7.1, 0.95 + i * 0.82, 0.6, hl ? C.yellow : C.white); + s.addText(label, { x: 7.28, y: 1.03 + i * 0.82, w: 2.55, h: 0.44, fontSize: 12, color: hl ? C.yellow : C.white, fontFace: "Calibri", margin: 0 }); + }); +} + +// ─── SLIDE 02 — Sommaire ──────────────────────────────────── +{ + const s = pres.addSlide(); + s.background = { color: C.lightBg }; + topBar(s); + s.addShape(pres.shapes.RECTANGLE, { x: 0.65, y: 0.37, w: 0.55, h: 0.09, fill: { color: C.yellow }, line: { color: C.yellow } }); + s.addText("SOMMAIRE", { x: 0.65, y: 0.47, w: 8, h: 0.88, fontSize: 34, color: C.darkBg, fontFace: "Calibri", margin: 0 }); + hLine(s, 0.65, 1.38, 8.8); + ["Contexte & objectifs du bloc", "Gestion des versions (Gitea)", "Gestion des évolutions & maintenance", + "Plan de déploiement & environnements", "Intégration & déploiement continus (CI/CD)", + "Plan de sécurisation & RGPD", "Bilan & livrables"].forEach((item, i) => { + s.addText(String(i + 1).padStart(2, "0"), { x: 0.65, y: 1.6 + i * 0.5, w: 0.65, h: 0.38, fontSize: 16, color: C.teal, fontFace: "Calibri", margin: 0 }); + s.addText(item, { x: 1.38, y: 1.63 + i * 0.5, w: 5.2, h: 0.36, fontSize: 13.5, color: C.dark, fontFace: "Calibri", margin: 0 }); + }); + // panel droit + s.addShape(pres.shapes.RECTANGLE, { x: 6.95, y: 0.09, w: 3.05, h: 5.535, fill: { color: C.teal }, line: { color: C.teal } }); + s.addText("Objectif", { x: 7.1, y: 0.35, w: 2.75, h: 0.4, fontSize: 13, bold: true, color: C.white, fontFace: "Calibri", margin: 0 }); + hLine(s, 7.1, 0.78, 2.7, "00995A"); + s.addText("Déployer et sécuriser une application déjà développée, dans un cadre professionnel et conforme au RGPD.", + { x: 7.1, y: 0.95, w: 2.75, h: 1.4, fontSize: 12.5, color: C.white, fontFace: "Calibri", valign: "top", margin: 0 }); +} + +// ─── SLIDE 03 — Contexte & objectifs ──────────────────────── +{ + const s = pres.addSlide(); + s.background = { color: C.lightBg }; + header(s, 3, "Introduction", "Contexte & objectifs du bloc"); + s.addText("L'application CESIZen (Web, Mobile, Wear OS) a été conçue et développée au BLOC 2. L'enjeu de ce bloc est de la mettre en service de façon fiable, automatisée et sécurisée.", + { x: 0.65, y: 1.4, w: 8.8, h: 0.7, fontSize: 13, color: C.dark, fontFace: "Calibri", valign: "top", margin: 0 }); + card(s, 0.65, 2.25, 4.35, 2.9, C.teal, "4 livrables attendus", [ + "Outil de gestion de version", "Outil de gestion des évolutions", + "Plan de déploiement (CI/CD)", "Plan de sécurisation (RGPD, chiffrement)"]); + card(s, 5.15, 2.25, 4.3, 2.9, C.yellow, "Contexte du projet", [ + "Projet (RE)Sources Relationnelles", "Données personnelles sensibles", + "Exigences ministérielles & RGPD", "Architecture MVC (Laravel 12)"]); +} + +// ─── SLIDE 04 — Gestion de version ────────────────────────── +{ + const s = pres.addSlide(); + s.background = { color: C.lightBg }; + header(s, 4, "Maintenance", "Gestion des versions : Git + Gitea"); + card(s, 0.65, 1.45, 4.35, 3.7, C.teal, "Gitea auto-hébergé", [ + "gitea.sam-coffre.duckdns.org", "Souveraineté totale des données", + "Très léger (~200 Mo RAM)", "CI/CD native intégrée", + "Open source & gratuit"]); + card(s, 5.15, 1.45, 4.3, 3.7, C.yellow, "Organisation Git", [ + "Monorepo : WEB / mobile / Wear", "main : production", + "develop : intégration", "feature/* : via Pull Request", + "Conventional Commits + CHANGELOG"]); + s.addText("Pourquoi Gitea plutôt que GitHub/GitLab ? → maîtrise de l'hébergement des données, indispensable pour un projet ministériel.", + { x: 0.65, y: 5.05, w: 8.8, h: 0.4, fontSize: 11, italic: true, color: C.gray, fontFace: "Calibri", margin: 0 }); +} + +// ─── SLIDE 05 — Évolutions & maintenance ──────────────────── +{ + const s = pres.addSlide(); + s.background = { color: C.lightBg }; + header(s, 5, "Maintenance", "Gestion des évolutions & incidents"); + card(s, 0.65, 1.45, 4.35, 2.5, C.teal, "Gitea Issues (2 modèles)", [ + "🐞 Rapport d'incident (correctif)", "✨ Demande d'évolution (évolutif)", + "Champs structurés & priorité"]); + card(s, 5.15, 1.45, 4.3, 2.5, C.yellow, "Pilotage", [ + "Labels + tableau Kanban", "Jalons = versions (SemVer)", + "Pull Request obligatoire"]); + // Flux + s.addText("Cycle de vie d'une demande", { x: 0.65, y: 4.15, w: 8.8, h: 0.3, fontSize: 12, bold: true, color: C.darkBg, fontFace: "Calibri", margin: 0 }); + const steps = ["Nouveau", "Priorisé", "En cours", "En revue", "Déployé"]; + steps.forEach((st, i) => { + const x = 0.65 + i * 1.78; + s.addShape(pres.shapes.RECTANGLE, { x, y: 4.5, w: 1.55, h: 0.55, fill: { color: i === 4 ? C.teal : C.navy }, line: { color: C.navy } }); + s.addText(st, { x, y: 4.5, w: 1.55, h: 0.55, fontSize: 11, color: C.white, align: "center", valign: "middle", fontFace: "Calibri", margin: 0 }); + if (i < 4) s.addText("→", { x: x + 1.5, y: 4.5, w: 0.32, h: 0.55, fontSize: 16, color: C.teal, align: "center", valign: "middle", margin: 0 }); + }); +} + +// ─── SLIDE 06 — Plan de déploiement (diagramme) ───────────── +{ + const s = pres.addSlide(); + s.background = { color: C.lightBg }; + header(s, 6, "Déploiement", "Environnements & hébergement"); + s.addText([ + { text: "4 environnements : ", options: { bold: true } }, + { text: "Développement → Test/CI → Préproduction → Production", options: {} }, + ], { x: 0.65, y: 1.4, w: 8.8, h: 0.4, fontSize: 12.5, color: C.dark, fontFace: "Calibri", margin: 0 }); + s.addImage({ path: "docs/img/diag_archi.png", x: 0.65, y: 1.95, w: 8.8, h: 2.68 }); + s.addText("Production : cesizen.sam-coffre.duckdns.org — conteneurisée & orchestrée via Portainer, TLS géré par Nginx Proxy Manager.", + { x: 0.65, y: 4.8, w: 8.8, h: 0.4, fontSize: 11, italic: true, color: C.gray, fontFace: "Calibri", margin: 0 }); +} + +// ─── SLIDE 07 — CI/CD (diagramme) ─────────────────────────── +{ + const s = pres.addSlide(); + s.background = { color: C.lightBg }; + header(s, 7, "Déploiement", "CI/CD automatisée — Gitea Actions"); + s.addImage({ path: "docs/img/diag_cicd.png", x: 0.65, y: 1.4, w: 8.8, h: 3.5 }); + s.addText("Un push lance les tests & contrôles qualité ; une fusion sur main construit l'image, la publie et redéploie automatiquement.", + { x: 0.65, y: 4.95, w: 8.8, h: 0.4, fontSize: 11, italic: true, color: C.gray, fontFace: "Calibri", margin: 0 }); +} + +// ─── SLIDE 08 — Sécurité (fond sombre) ────────────────────── +{ + const s = pres.addSlide(); + s.background = { color: C.darkBg }; + topBar(s); + s.addText("08", { x: 9.0, y: 0.11, w: 0.82, h: 0.38, fontSize: 14, color: C.teal, fontFace: "Calibri", bold: true, align: "right", margin: 0 }); + s.addText("SÉCURITÉ", { x: 0.65, y: 0.13, w: 7, h: 0.26, fontSize: 9, color: "9CA3AF", fontFace: "Calibri", charSpacing: 2, margin: 0 }); + s.addShape(pres.shapes.RECTANGLE, { x: 0.65, y: 0.44, w: 0.55, h: 0.07, fill: { color: C.yellow }, line: { color: C.yellow } }); + s.addText("Plan de sécurisation & RGPD", { x: 0.65, y: 0.52, w: 8.7, h: 0.7, fontSize: 26, color: C.white, fontFace: "Calibri", margin: 0 }); + hLine(s, 0.65, 1.25, 8.8, "374151"); + + const sec = [ + ["Applicatif", ["Auth Sanctum + rôles (RBAC)", "CSRF, validation, en-têtes HTTP", "CSP / HSTS / X-Frame-Options"]], + ["Chiffrement", ["HTTPS/TLS (Let's Encrypt)", "Mots de passe : bcrypt", "Sessions chiffrées"]], + ["RGPD", ["Minimisation & consentement", "Droit à l'effacement", "Anonymisation des stats"]], + ["Qualité", ["Pint + tests PHPUnit", "Audit des dépendances (CI)", "Revues via Pull Request"]], + ]; + sec.forEach(([title, lines], i) => { + const x = 0.65 + (i % 2) * 4.5; + const y = 1.5 + Math.floor(i / 2) * 1.95; + s.addShape(pres.shapes.RECTANGLE, { x, y, w: 4.2, h: 1.75, fill: { color: C.navy }, line: { color: "374151", width: 1 } }); + vLine(s, x, y, 1.75, C.teal); + s.addText(title, { x: x + 0.2, y: y + 0.12, w: 3.8, h: 0.35, fontSize: 14, bold: true, color: C.yellow, fontFace: "Calibri", margin: 0 }); + s.addText(lines.map((t) => ({ text: t, options: { bullet: { code: "2022", indent: 12 }, color: "E5E7EB", fontSize: 11, paraSpaceAfter: 3 } })), + { x: x + 0.22, y: y + 0.5, w: 3.8, h: 1.15, fontFace: "Calibri", valign: "top", margin: 0 }); + }); +} + +// ─── SLIDE 09 — Analyse des risques (OWASP) ───────────────── +{ + const s = pres.addSlide(); + s.background = { color: C.lightBg }; + header(s, 9, "Sécurité", "Analyse des risques & traitement"); + const rows = [ + ["Injection SQL", "ORM Eloquent / requêtes préparées"], + ["XSS", "Échappement Blade + CSP"], + ["CSRF", "Jeton CSRF Laravel"], + ["Vol de session", "HTTPS forcé, cookies Secure, HSTS"], + ["Dépendances vulnérables", "composer audit / npm audit (CI)"], + ["Force brute", "Rate limiting"], + ]; + // en-tête tableau + s.addShape(pres.shapes.RECTANGLE, { x: 0.65, y: 1.45, w: 4.3, h: 0.5, fill: { color: C.navy }, line: { color: C.navy } }); + s.addShape(pres.shapes.RECTANGLE, { x: 4.95, y: 1.45, w: 4.5, h: 0.5, fill: { color: C.navy }, line: { color: C.navy } }); + s.addText("Risque", { x: 0.8, y: 1.45, w: 4.1, h: 0.5, fontSize: 12, bold: true, color: C.white, valign: "middle", fontFace: "Calibri", margin: 0 }); + s.addText("Mesure de prévention", { x: 5.1, y: 1.45, w: 4.3, h: 0.5, fontSize: 12, bold: true, color: C.white, valign: "middle", fontFace: "Calibri", margin: 0 }); + rows.forEach((r, i) => { + const y = 1.95 + i * 0.5; + const bg = i % 2 ? "FFFFFF" : "EFF5F1"; + s.addShape(pres.shapes.RECTANGLE, { x: 0.65, y, w: 4.3, h: 0.5, fill: { color: bg }, line: { color: C.border } }); + s.addShape(pres.shapes.RECTANGLE, { x: 4.95, y, w: 4.5, h: 0.5, fill: { color: bg }, line: { color: C.border } }); + s.addText(r[0], { x: 0.8, y, w: 4.1, h: 0.5, fontSize: 11, color: C.dark, valign: "middle", fontFace: "Calibri", margin: 0 }); + s.addText(r[1], { x: 5.1, y, w: 4.3, h: 0.5, fontSize: 11, color: C.dark, valign: "middle", fontFace: "Calibri", margin: 0 }); + }); +} + +// ─── SLIDE 10 — Bilan & livrables ─────────────────────────── +{ + const s = pres.addSlide(); + s.background = { color: C.darkBg }; + topBar(s); + s.addShape(pres.shapes.RECTANGLE, { x: 0.7, y: 0.6, w: 0.55, h: 0.09, fill: { color: C.yellow }, line: { color: C.yellow } }); + s.addText("Bilan & livrables", { x: 0.7, y: 0.72, w: 8.6, h: 0.9, fontSize: 34, color: C.white, fontFace: "Calibri", margin: 0 }); + hLine(s, 0.7, 1.7, 8.6, "374151"); + const items = [ + ["Gestion de version", "Gitea auto-hébergé + stratégie de branches"], + ["Évolutions & maintenance", "Gitea Issues, Kanban, SemVer, CHANGELOG"], + ["Déploiement automatisé", "CI/CD Gitea Actions → Registry → Portainer"], + ["Sécurité", "Chiffrement, en-têtes, RGPD, audits, tests"], + ]; + items.forEach((it, i) => { + const y = 2.0 + i * 0.78; + vLine(s, 0.7, y, 0.6, C.teal); + s.addText(it[0], { x: 0.9, y: y - 0.02, w: 3.6, h: 0.4, fontSize: 14, bold: true, color: C.yellow, fontFace: "Calibri", margin: 0 }); + s.addText(it[1], { x: 4.4, y: y - 0.02, w: 5.2, h: 0.4, fontSize: 12.5, color: "E5E7EB", fontFace: "Calibri", margin: 0 }); + }); + s.addText("Une base industrialisée, reproductible et sécurisée. Merci de votre attention — place aux questions.", + { x: 0.7, y: 5.25, w: 8.8, h: 0.4, fontSize: 12, italic: true, color: C.teal, fontFace: "Calibri", margin: 0 }); +} + +pres.writeFile({ fileName: "CESIZen_Presentation_Bloc3.pptx" }).then((f) => console.log("Généré :", f));