Compare commits
6 Commits
6515d8348d
...
89b4107266
| Author | SHA1 | Date | |
|---|---|---|---|
| 89b4107266 | |||
| 70d4767890 | |||
| c51f17a74c | |||
| ebe1836f7e | |||
| d75afe0aac | |||
| c3e1c95f1b |
@@ -0,0 +1,21 @@
|
|||||||
|
<!-- Merci de remplir ce modèle pour faciliter la revue de code -->
|
||||||
|
|
||||||
|
## Description
|
||||||
|
<!-- Que fait cette Pull Request ? -->
|
||||||
|
|
||||||
|
## Ticket lié
|
||||||
|
<!-- Ex : Closes #12 -->
|
||||||
|
|
||||||
|
## 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é
|
||||||
@@ -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"
|
||||||
@@ -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: |
|
||||||
|
- [ ] ...
|
||||||
|
- [ ] ...
|
||||||
@@ -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
|
||||||
@@ -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."
|
||||||
+22
@@ -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/
|
||||||
Generated
-3
@@ -1,3 +0,0 @@
|
|||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
Generated
-1
@@ -1 +0,0 @@
|
|||||||
\\192.168.1.50\cesizen\
|
|
||||||
Generated
-52
@@ -1,52 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module type="JAVA_MODULE" version="4">
|
|
||||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
|
||||||
<exclude-output />
|
|
||||||
<content url="file://$MODULE_DIR$">
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/.dart_tool" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/.pub" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/build" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/linux/flutter/ephemeral/.plugin_symlinks/flutter_secure_storage_linux/.dart_tool" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/linux/flutter/ephemeral/.plugin_symlinks/flutter_secure_storage_linux/.pub" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/linux/flutter/ephemeral/.plugin_symlinks/flutter_secure_storage_linux/build" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/linux/flutter/ephemeral/.plugin_symlinks/jni/.dart_tool" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/linux/flutter/ephemeral/.plugin_symlinks/jni/.pub" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/linux/flutter/ephemeral/.plugin_symlinks/jni/build" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/linux/flutter/ephemeral/.plugin_symlinks/jni/example/.dart_tool" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/linux/flutter/ephemeral/.plugin_symlinks/jni/example/.pub" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/linux/flutter/ephemeral/.plugin_symlinks/jni/example/build" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux/.dart_tool" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux/.pub" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux/build" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux/example/.dart_tool" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux/example/.pub" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux/example/build" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/windows/flutter/ephemeral/.plugin_symlinks/flutter_secure_storage_windows/.dart_tool" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/windows/flutter/ephemeral/.plugin_symlinks/flutter_secure_storage_windows/.pub" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/windows/flutter/ephemeral/.plugin_symlinks/flutter_secure_storage_windows/build" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/windows/flutter/ephemeral/.plugin_symlinks/flutter_secure_storage_windows/example/.dart_tool" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/windows/flutter/ephemeral/.plugin_symlinks/flutter_secure_storage_windows/example/.pub" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/windows/flutter/ephemeral/.plugin_symlinks/flutter_secure_storage_windows/example/build" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/windows/flutter/ephemeral/.plugin_symlinks/jni/.dart_tool" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/windows/flutter/ephemeral/.plugin_symlinks/jni/.pub" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/windows/flutter/ephemeral/.plugin_symlinks/jni/build" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/windows/flutter/ephemeral/.plugin_symlinks/jni/example/.dart_tool" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/windows/flutter/ephemeral/.plugin_symlinks/jni/example/.pub" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/windows/flutter/ephemeral/.plugin_symlinks/jni/example/build" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/windows/flutter/ephemeral/.plugin_symlinks/path_provider_windows/.dart_tool" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/windows/flutter/ephemeral/.plugin_symlinks/path_provider_windows/.pub" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/windows/flutter/ephemeral/.plugin_symlinks/path_provider_windows/build" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/windows/flutter/ephemeral/.plugin_symlinks/path_provider_windows/example/.dart_tool" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/windows/flutter/ephemeral/.plugin_symlinks/path_provider_windows/example/.pub" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/windows/flutter/ephemeral/.plugin_symlinks/path_provider_windows/example/build" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/windows/flutter/ephemeral/.plugin_symlinks/permission_handler_windows/.dart_tool" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/windows/flutter/ephemeral/.plugin_symlinks/permission_handler_windows/.pub" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/windows/flutter/ephemeral/.plugin_symlinks/permission_handler_windows/build" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/windows/flutter/ephemeral/.plugin_symlinks/permission_handler_windows/example/.dart_tool" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/windows/flutter/ephemeral/.plugin_symlinks/permission_handler_windows/example/.pub" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/mobile/windows/flutter/ephemeral/.plugin_symlinks/permission_handler_windows/example/build" />
|
|
||||||
</content>
|
|
||||||
<orderEntry type="inheritedJdk" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
||||||
Generated
-1808
File diff suppressed because it is too large
Load Diff
Generated
-11
@@ -1,11 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="deploymentTargetSelector">
|
|
||||||
<selectionStates>
|
|
||||||
<SelectionState runConfigName="Wear App">
|
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
|
||||||
<DialogSelection />
|
|
||||||
</SelectionState>
|
|
||||||
</selectionStates>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
Generated
-8
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="MarkdownSettings">
|
|
||||||
<option name="previewPanelProviderInfo">
|
|
||||||
<ProviderInfo name="Compose (experimental)" className="com.intellij.markdown.compose.preview.ComposePanelProvider" />
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
Generated
-5
@@ -1,5 +0,0 @@
|
|||||||
<project version="4">
|
|
||||||
<component name="ProjectRootManager" version="2">
|
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
Generated
-8
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectModuleManager">
|
|
||||||
<modules>
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/CESIZen.iml" filepath="$PROJECT_DIR$/.idea/CESIZen.iml" />
|
|
||||||
</modules>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration default="false" name="Launch All (Mobile & Wear)" type="CompoundRunConfigurationType">
|
|
||||||
<toRun name="Mobile App" type="FlutterRunConfigurationType" />
|
|
||||||
<toRun name="Wear App" type="AndroidRunConfigurationType" />
|
|
||||||
<method v="2" />
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
||||||
Generated
-7
@@ -1,7 +0,0 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration default="false" name="Mobile App" type="FlutterRunConfigurationType" factoryName="Flutter">
|
|
||||||
<option name="filePath" value="$PROJECT_DIR$/mobile/lib/main.dart" />
|
|
||||||
<option name="additionalArgs" value="-d PV5HRKVCJVKZ9DOB" />
|
|
||||||
<method v="2" />
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
||||||
Generated
-68
@@ -1,68 +0,0 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration default="false" name="Wear App" type="AndroidRunConfigurationType" factoryName="Android App">
|
|
||||||
<module name="CESIZen.Wear.app.main" />
|
|
||||||
<option name="DEPLOY" value="true" />
|
|
||||||
<option name="DEPLOY_APK_FROM_BUNDLE" value="false" />
|
|
||||||
<option name="DEPLOY_AS_INSTANT" value="false" />
|
|
||||||
<option name="ARTIFACT_NAME" value="" />
|
|
||||||
<option name="PM_INSTALL_OPTIONS" value="" />
|
|
||||||
<option name="ALL_USERS" value="false" />
|
|
||||||
<option name="ALWAYS_INSTALL_WITH_PM" value="false" />
|
|
||||||
<option name="CLEAR_APP_STORAGE" value="false" />
|
|
||||||
<option name="DYNAMIC_FEATURES_DISABLED_LIST" value="" />
|
|
||||||
<option name="ACTIVITY_EXTRA_FLAGS" value="" />
|
|
||||||
<option name="MODE" value="default_activity" />
|
|
||||||
<option name="CLEAR_LOGCAT" value="false" />
|
|
||||||
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
|
|
||||||
<option name="INSPECTION_WITHOUT_ACTIVITY_RESTART" value="false" />
|
|
||||||
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
|
|
||||||
<option name="PREFERRED_AVD" value="" />
|
|
||||||
<option name="SELECTED_DEVICE_ID" value="adb-d76a0274-X63ZH9 (2)._adb-tls-connect._tcp" />
|
|
||||||
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="-1" />
|
|
||||||
<option name="SELECTED_CLOUD_MATRIX_PROJECT_ID" value="" />
|
|
||||||
<option name="DEBUGGER_TYPE" value="Auto" />
|
|
||||||
<Auto>
|
|
||||||
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
|
||||||
<option name="SHOW_STATIC_VARS" value="true" />
|
|
||||||
<option name="WORKING_DIR" value="" />
|
|
||||||
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
|
||||||
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
|
||||||
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
|
|
||||||
<option name="DEBUG_SANDBOX_SDK" value="false" />
|
|
||||||
</Auto>
|
|
||||||
<Hybrid>
|
|
||||||
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
|
||||||
<option name="SHOW_STATIC_VARS" value="true" />
|
|
||||||
<option name="WORKING_DIR" value="" />
|
|
||||||
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
|
||||||
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
|
||||||
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
|
|
||||||
<option name="DEBUG_SANDBOX_SDK" value="false" />
|
|
||||||
</Hybrid>
|
|
||||||
<Java>
|
|
||||||
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
|
|
||||||
<option name="DEBUG_SANDBOX_SDK" value="false" />
|
|
||||||
</Java>
|
|
||||||
<Native>
|
|
||||||
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
|
||||||
<option name="SHOW_STATIC_VARS" value="true" />
|
|
||||||
<option name="WORKING_DIR" value="" />
|
|
||||||
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
|
||||||
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
|
||||||
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
|
|
||||||
<option name="DEBUG_SANDBOX_SDK" value="false" />
|
|
||||||
</Native>
|
|
||||||
<Profilers>
|
|
||||||
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
|
|
||||||
<option name="STARTUP_PROFILING_ENABLED" value="false" />
|
|
||||||
<option name="DETAILED_REPORT_APP_STARTUP_PROFILING_ENABLED" value="false" />
|
|
||||||
</Profilers>
|
|
||||||
<option name="DEEP_LINK" value="" />
|
|
||||||
<option name="ACTIVITY_CLASS" value="" />
|
|
||||||
<option name="SEARCH_ACTIVITY_IN_GLOBAL_SCOPE" value="false" />
|
|
||||||
<option name="SKIP_ACTIVITY_VALIDATION" value="false" />
|
|
||||||
<method v="2">
|
|
||||||
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
|
|
||||||
</method>
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
||||||
Generated
-6
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -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).
|
||||||
+41
@@ -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.
|
||||||
@@ -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}"
|
||||||
@@ -22,3 +22,4 @@
|
|||||||
Homestead.json
|
Homestead.json
|
||||||
Homestead.yaml
|
Homestead.yaml
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
.idea
|
||||||
@@ -13,10 +13,11 @@ use Illuminate\Validation\Rule;
|
|||||||
class AuthController extends Controller
|
class AuthController extends Controller
|
||||||
{
|
{
|
||||||
// AuthController.php
|
// AuthController.php
|
||||||
public function login(Request $request) {
|
public function login(Request $request)
|
||||||
|
{
|
||||||
$fields = $request->validate([
|
$fields = $request->validate([
|
||||||
'email' => 'required|string|email',
|
'email' => 'required|string|email',
|
||||||
'password' => 'required|string'
|
'password' => 'required|string',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Log::info('Tentative de connexion', $request->all());
|
// Log::info('Tentative de connexion', $request->all());
|
||||||
@@ -24,18 +25,18 @@ class AuthController extends Controller
|
|||||||
$user = User::where('email', $fields['email'])->first();
|
$user = User::where('email', $fields['email'])->first();
|
||||||
|
|
||||||
// Vérification du mot de passe haché (Sécurité)
|
// 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([
|
return response([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Identifiants invalides'
|
'message' => 'Identifiants invalides',
|
||||||
], 401);
|
], 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérification si le compte est actif
|
// Vérification si le compte est actif
|
||||||
if (!$user->is_active) {
|
if (! $user->is_active) {
|
||||||
return response([
|
return response([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Votre compte a été désactivé par un administrateur.'
|
'message' => 'Votre compte a été désactivé par un administrateur.',
|
||||||
], 403);
|
], 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +50,7 @@ class AuthController extends Controller
|
|||||||
return response([
|
return response([
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'user' => $user,
|
'user' => $user,
|
||||||
'token' => $token
|
'token' => $token,
|
||||||
], 200);
|
], 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,14 +63,14 @@ class AuthController extends Controller
|
|||||||
'password' => 'required|string|min:8|confirmed',
|
'password' => 'required|string|min:8|confirmed',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
||||||
Log::info('Tentative de création', $request->all());
|
Log::info('Tentative de création', $request->all());
|
||||||
|
|
||||||
if ($validator->fails()) {
|
if ($validator->fails()) {
|
||||||
Log::info( $validator->errors());
|
Log::info($validator->errors());
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'errors' => $validator->errors()
|
'errors' => $validator->errors(),
|
||||||
], 422);
|
], 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +88,7 @@ class AuthController extends Controller
|
|||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'message' => 'Compte créé avec succès',
|
'message' => 'Compte créé avec succès',
|
||||||
'user' => $user,
|
'user' => $user,
|
||||||
'token' => $token
|
'token' => $token,
|
||||||
], 201);
|
], 201);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,15 +101,15 @@ class AuthController extends Controller
|
|||||||
'email' => [
|
'email' => [
|
||||||
'sometimes',
|
'sometimes',
|
||||||
'email',
|
'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é)
|
// 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([
|
return response()->json([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Le mot de passe actuel est incorrect'
|
'message' => 'Le mot de passe actuel est incorrect',
|
||||||
], 403);
|
], 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +120,7 @@ class AuthController extends Controller
|
|||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'user' => $user
|
'user' => $user,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ namespace App\Http\Controllers\Api;
|
|||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use App\Models\EmotionRecord;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
class EmotionController extends Controller
|
class EmotionController extends Controller
|
||||||
@@ -18,7 +17,7 @@ class EmotionController extends Controller
|
|||||||
{
|
{
|
||||||
Log::info('Tentative d\'enregistrement d\'émotion', [
|
Log::info('Tentative d\'enregistrement d\'émotion', [
|
||||||
'user' => $request->user()->id,
|
'user' => $request->user()->id,
|
||||||
'data' => $request->all()
|
'data' => $request->all(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -35,7 +34,8 @@ class EmotionController extends Controller
|
|||||||
|
|
||||||
return response()->json($record, 201);
|
return response()->json($record, 201);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::error('Erreur enregistrement émotion : ' . $e->getMessage());
|
Log::error('Erreur enregistrement émotion : '.$e->getMessage());
|
||||||
|
|
||||||
return response()->json(['error' => 'Erreur serveur'], 500);
|
return response()->json(['error' => 'Erreur serveur'], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ namespace App\Http\Controllers\Api;
|
|||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Information;
|
use App\Models\Information;
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
class InformationController extends Controller
|
class InformationController extends Controller
|
||||||
{
|
{
|
||||||
@@ -16,7 +15,7 @@ class InformationController extends Controller
|
|||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'data' => $informations
|
'data' => $informations,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,16 +23,16 @@ class InformationController extends Controller
|
|||||||
{
|
{
|
||||||
$information = Information::where('is_published', true)->find($id);
|
$information = Information::where('is_published', true)->find($id);
|
||||||
|
|
||||||
if (!$information) {
|
if (! $information) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Information non trouvée'
|
'message' => 'Information non trouvée',
|
||||||
], 404);
|
], 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'data' => $information
|
'data' => $information,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ namespace App\Http\Controllers\Api;
|
|||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\StressEvent;
|
use App\Models\StressEvent;
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
class StressDiagnosticController extends Controller
|
class StressDiagnosticController extends Controller
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
class DashboardController extends Controller
|
class DashboardController extends Controller
|
||||||
@@ -17,7 +15,7 @@ class DashboardController extends Controller
|
|||||||
$stats = [
|
$stats = [
|
||||||
'relaxation_time' => '45 min',
|
'relaxation_time' => '45 min',
|
||||||
'stress_status' => 'Stable',
|
'stress_status' => 'Stable',
|
||||||
'last_diagnostic' => 'il y a 3 jours'
|
'last_diagnostic' => 'il y a 3 jours',
|
||||||
];
|
];
|
||||||
|
|
||||||
return view('welcome', compact('user', 'stats'));
|
return view('welcome', compact('user', 'stats'));
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class AuthController extends Controller
|
|||||||
|
|
||||||
if (Auth::attempt($credentials)) {
|
if (Auth::attempt($credentials)) {
|
||||||
$request->session()->regenerate();
|
$request->session()->regenerate();
|
||||||
|
|
||||||
return redirect()->intended('/');
|
return redirect()->intended('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,6 +62,7 @@ class AuthController extends Controller
|
|||||||
Auth::logout();
|
Auth::logout();
|
||||||
$request->session()->invalidate();
|
$request->session()->invalidate();
|
||||||
$request->session()->regenerateToken();
|
$request->session()->regenerateToken();
|
||||||
|
|
||||||
return redirect('/login');
|
return redirect('/login');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
namespace App\Http\Controllers\Web;
|
namespace App\Http\Controllers\Web;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\ResultatDiag;
|
||||||
use App\Models\StressEvent;
|
use App\Models\StressEvent;
|
||||||
|
use App\Services\StressCalculator;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
@@ -14,23 +16,25 @@ class DiagnosticController extends Controller
|
|||||||
$events = StressEvent::all();
|
$events = StressEvent::all();
|
||||||
$history = [];
|
$history = [];
|
||||||
if (Auth::check()) {
|
if (Auth::check()) {
|
||||||
$history = \App\Models\ResultatDiag::where('id_user', Auth::id())
|
$history = ResultatDiag::where('id_user', Auth::id())
|
||||||
->orderBy('date_passage', 'desc')
|
->orderBy('date_passage', 'desc')
|
||||||
->take(5)
|
->take(5)
|
||||||
->get();
|
->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
return view('diagnostics.index', compact('events', 'history'));
|
return view('diagnostics.index', compact('events', 'history'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function history()
|
public function history()
|
||||||
{
|
{
|
||||||
$history = \App\Models\ResultatDiag::where('id_user', Auth::id())
|
$history = ResultatDiag::where('id_user', Auth::id())
|
||||||
->orderBy('date_passage', 'desc')
|
->orderBy('date_passage', 'desc')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
return view('diagnostics.history', compact('history'));
|
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
|
// Logique pour calculer le score Holmes-Rahe
|
||||||
$totalPoints = 0;
|
$totalPoints = 0;
|
||||||
@@ -43,7 +47,7 @@ class DiagnosticController extends Controller
|
|||||||
|
|
||||||
// Enregistrement si l'utilisateur est connecté
|
// Enregistrement si l'utilisateur est connecté
|
||||||
if (Auth::check()) {
|
if (Auth::check()) {
|
||||||
\App\Models\ResultatDiag::create([
|
ResultatDiag::create([
|
||||||
'id_user' => Auth::id(),
|
'id_user' => Auth::id(),
|
||||||
'score_total' => $totalPoints,
|
'score_total' => $totalPoints,
|
||||||
'niveau_stress' => $niveauStress,
|
'niveau_stress' => $niveauStress,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class EmotionController extends Controller
|
|||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$records = EmotionRecord::where('user_id', Auth::id())->latest()->get();
|
$records = EmotionRecord::where('user_id', Auth::id())->latest()->get();
|
||||||
|
|
||||||
return view('emotions.index', compact('records'));
|
return view('emotions.index', compact('records'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,14 +21,14 @@ class EmotionController extends Controller
|
|||||||
$request->validate([
|
$request->validate([
|
||||||
'emotion' => 'required|string',
|
'emotion' => 'required|string',
|
||||||
'intensity' => 'required|integer|min:1|max:5',
|
'intensity' => 'required|integer|min:1|max:5',
|
||||||
'note' => 'nullable|string'
|
'note' => 'nullable|string',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
EmotionRecord::create([
|
EmotionRecord::create([
|
||||||
'user_id' => Auth::id(),
|
'user_id' => Auth::id(),
|
||||||
'emotion' => $request->emotion,
|
'emotion' => $request->emotion,
|
||||||
'intensity' => $request->intensity,
|
'intensity' => $request->intensity,
|
||||||
'note' => $request->note
|
'note' => $request->note,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return redirect()->back()->with('success', 'Émotion enregistrée');
|
return redirect()->back()->with('success', 'Émotion enregistrée');
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class ProfileController extends Controller
|
|||||||
|
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'name' => ['required', 'string', 'max:255'],
|
'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);
|
$user->fill($validated);
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ namespace App\Http\Controllers\Web;
|
|||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\RelaxationActivity;
|
use App\Models\RelaxationActivity;
|
||||||
use App\Models\Favorite;
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
|
|
||||||
class RelaxationController extends Controller
|
class RelaxationController extends Controller
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SecurityHeaders
|
||||||
|
* ---------------
|
||||||
|
* Ajoute les en-têtes HTTP de sécurité recommandés (OWASP Secure Headers)
|
||||||
|
* à toutes les réponses de l'application.
|
||||||
|
*
|
||||||
|
* Ces en-têtes renforcent la protection contre :
|
||||||
|
* - le clickjacking (X-Frame-Options)
|
||||||
|
* - le MIME-sniffing (X-Content-Type-Options)
|
||||||
|
* - les fuites de référent (Referrer-Policy)
|
||||||
|
* - le vol de session sur HTTP (Strict-Transport-Security / HSTS)
|
||||||
|
* - certaines injections de contenu (Content-Security-Policy)
|
||||||
|
*/
|
||||||
|
class SecurityHeaders
|
||||||
|
{
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
/** @var Response $response */
|
||||||
|
$response = $next($request);
|
||||||
|
|
||||||
|
// Empêche l'affichage du site dans une iframe (anti-clickjacking)
|
||||||
|
$response->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
|
|
||||||
class EvenementStress extends Model
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
@@ -16,7 +16,7 @@ class ResultatDiag extends Model
|
|||||||
'date_passage',
|
'date_passage',
|
||||||
'score_total',
|
'score_total',
|
||||||
'niveau_stress',
|
'niveau_stress',
|
||||||
'event_ids'
|
'event_ids',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function user()
|
public function user()
|
||||||
@@ -26,8 +26,11 @@ class ResultatDiag extends Model
|
|||||||
|
|
||||||
public function getEventsAttribute()
|
public function getEventsAttribute()
|
||||||
{
|
{
|
||||||
if (!$this->event_ids) return collect();
|
if (! $this->event_ids) {
|
||||||
|
return collect();
|
||||||
|
}
|
||||||
$ids = explode(';', $this->event_ids);
|
$ids = explode(';', $this->event_ids);
|
||||||
|
|
||||||
return StressEvent::whereIn('id', $ids)->get();
|
return StressEvent::whereIn('id', $ids)->get();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
class Role extends Model
|
class Role extends Model
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\URL;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
@@ -20,7 +21,7 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
if (config('app.env') === 'production' || config('app.env') === 'local') {
|
if (config('app.env') === 'production' || config('app.env') === 'local') {
|
||||||
\Illuminate\Support\Facades\URL::forceScheme('https');
|
URL::forceScheme('https');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ namespace App\Services;
|
|||||||
class StressCalculator
|
class StressCalculator
|
||||||
{
|
{
|
||||||
public const LEVEL_HIGH = 'Élevé';
|
public const LEVEL_HIGH = 'Élevé';
|
||||||
|
|
||||||
public const LEVEL_MODERATE = 'Modéré';
|
public const LEVEL_MODERATE = 'Modéré';
|
||||||
|
|
||||||
public const LEVEL_LOW = 'Faible';
|
public const LEVEL_LOW = 'Faible';
|
||||||
|
|
||||||
public function calculateScore(array $points): int
|
public function calculateScore(array $points): int
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Middleware\SecurityHeaders;
|
||||||
use Illuminate\Foundation\Application;
|
use Illuminate\Foundation\Application;
|
||||||
use Illuminate\Foundation\Configuration\Exceptions;
|
use Illuminate\Foundation\Configuration\Exceptions;
|
||||||
use Illuminate\Foundation\Configuration\Middleware;
|
use Illuminate\Foundation\Configuration\Middleware;
|
||||||
@@ -12,7 +13,13 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
->withMiddleware(function (Middleware $middleware): void {
|
->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 {
|
->withExceptions(function (Exceptions $exceptions): void {
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Providers\AppServiceProvider;
|
||||||
|
use App\Providers\Filament\AdminPanelProvider;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
App\Providers\AppServiceProvider::class,
|
AppServiceProvider::class,
|
||||||
App\Providers\Filament\AdminPanelProvider::class,
|
AdminPanelProvider::class,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Cookie\Middleware\EncryptCookies;
|
||||||
|
use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;
|
||||||
|
use Laravel\Sanctum\Http\Middleware\AuthenticateSession;
|
||||||
use Laravel\Sanctum\Sanctum;
|
use Laravel\Sanctum\Sanctum;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -76,9 +79,9 @@ return [
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
'middleware' => [
|
'middleware' => [
|
||||||
'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
|
'authenticate_session' => AuthenticateSession::class,
|
||||||
'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class,
|
'encrypt_cookies' => EncryptCookies::class,
|
||||||
'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
|
'validate_csrf_token' => ValidateCsrfToken::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Database\Factories;
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Role;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
@@ -30,7 +31,7 @@ class UserFactory extends Factory
|
|||||||
'email_verified_at' => now(),
|
'email_verified_at' => now(),
|
||||||
'password' => static::$password ??= Hash::make('password'),
|
'password' => static::$password ??= Hash::make('password'),
|
||||||
'remember_token' => Str::random(10),
|
'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']),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,19 @@ use Illuminate\Database\Migrations\Migration;
|
|||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
return new class extends Migration {
|
return new class extends Migration
|
||||||
public function up(): void {
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
Schema::create('roles', function (Blueprint $table) {
|
Schema::create('roles', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->string('libelle');
|
$table->string('libelle');
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
public function down(): void { Schema::dropIfExists('roles'); }
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('roles');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ use Illuminate\Database\Migrations\Migration;
|
|||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
return new class extends Migration {
|
return new class extends Migration
|
||||||
public function up(): void {
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
Schema::create('users', function (Blueprint $table) {
|
Schema::create('users', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->string('name');
|
$table->string('name');
|
||||||
@@ -17,5 +19,9 @@ return new class extends Migration {
|
|||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
public function down(): void { Schema::dropIfExists('users'); }
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('users');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ use Illuminate\Database\Migrations\Migration;
|
|||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
return new class extends Migration {
|
return new class extends Migration
|
||||||
public function up(): void {
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
Schema::create('stress_events', function (Blueprint $table) {
|
Schema::create('stress_events', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->string('event_name');
|
$table->string('event_name');
|
||||||
@@ -13,5 +15,9 @@ return new class extends Migration {
|
|||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
public function down(): void { Schema::dropIfExists('stress_events'); }
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('stress_events');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ use Illuminate\Database\Migrations\Migration;
|
|||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
return new class extends Migration {
|
return new class extends Migration
|
||||||
public function up(): void {
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
Schema::create('relaxation_activities', function (Blueprint $table) {
|
Schema::create('relaxation_activities', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->string('title');
|
$table->string('title');
|
||||||
@@ -17,5 +19,9 @@ return new class extends Migration {
|
|||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
public function down(): void { Schema::dropIfExists('relaxation_activities'); }
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('relaxation_activities');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ use Illuminate\Database\Migrations\Migration;
|
|||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
return new class extends Migration {
|
return new class extends Migration
|
||||||
public function up(): void {
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
Schema::create('resultat_diags', function (Blueprint $table) {
|
Schema::create('resultat_diags', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->foreignId('id_user')->constrained('users')->onDelete('cascade');
|
$table->foreignId('id_user')->constrained('users')->onDelete('cascade');
|
||||||
@@ -16,5 +18,9 @@ return new class extends Migration {
|
|||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
public function down(): void { Schema::dropIfExists('resultat_diags'); }
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('resultat_diags');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ use Illuminate\Database\Migrations\Migration;
|
|||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
return new class extends Migration {
|
return new class extends Migration
|
||||||
public function up(): void {
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
Schema::create('emotion_records', function (Blueprint $table) {
|
Schema::create('emotion_records', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
|
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
|
||||||
@@ -15,5 +17,9 @@ return new class extends Migration {
|
|||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
public function down(): void { Schema::dropIfExists('emotion_records'); }
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('emotion_records');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Models\Role;
|
use App\Models\Role;
|
||||||
|
use App\Models\User;
|
||||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class RelaxationActivitySeeder extends Seeder
|
|||||||
'type' => 'audio',
|
'type' => 'audio',
|
||||||
'category' => 'Méditation',
|
'category' => 'Méditation',
|
||||||
'url' => 'https://www.youtube.com/watch?v=sz7cpV7ERsY',
|
'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',
|
'title' => 'Yoga Doux pour le Soir',
|
||||||
@@ -24,7 +24,7 @@ class RelaxationActivitySeeder extends Seeder
|
|||||||
'type' => 'video',
|
'type' => 'video',
|
||||||
'category' => 'Sport',
|
'category' => 'Sport',
|
||||||
'url' => 'https://www.youtube.com/watch?v=v7AYKMP6rOE',
|
'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',
|
'title' => 'Bruits de Plage et Vagues',
|
||||||
@@ -32,7 +32,7 @@ class RelaxationActivitySeeder extends Seeder
|
|||||||
'type' => 'audio',
|
'type' => 'audio',
|
||||||
'category' => 'Musique',
|
'category' => 'Musique',
|
||||||
'url' => 'https://www.youtube.com/watch?v=0_fS_pS_B0U',
|
'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',
|
'title' => 'Comprendre le Cycle du Stress',
|
||||||
@@ -40,7 +40,7 @@ class RelaxationActivitySeeder extends Seeder
|
|||||||
'type' => 'article',
|
'type' => 'article',
|
||||||
'category' => 'Lecture',
|
'category' => 'Lecture',
|
||||||
'url' => 'https://www.santepubliquefrance.fr',
|
'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',
|
'title' => 'Musique Classique pour la Concentration',
|
||||||
@@ -48,7 +48,7 @@ class RelaxationActivitySeeder extends Seeder
|
|||||||
'type' => 'audio',
|
'type' => 'audio',
|
||||||
'category' => 'Musique',
|
'category' => 'Musique',
|
||||||
'url' => 'https://www.youtube.com/watch?v=5Qq3m9G4_4A',
|
'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',
|
'title' => 'Techniques de Respiration Wim Hof',
|
||||||
@@ -56,8 +56,8 @@ class RelaxationActivitySeeder extends Seeder
|
|||||||
'type' => 'video',
|
'type' => 'video',
|
||||||
'category' => 'Respiration',
|
'category' => 'Respiration',
|
||||||
'url' => 'https://www.youtube.com/watch?v=tybOi4hjZFQ',
|
'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) {
|
foreach ($activities as $activity) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
use App\Models\Role;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
class RoleSeeder extends Seeder
|
class RoleSeeder extends Seeder
|
||||||
@@ -18,7 +18,7 @@ class RoleSeeder extends Seeder
|
|||||||
];
|
];
|
||||||
|
|
||||||
foreach ($roles as $role) {
|
foreach ($roles as $role) {
|
||||||
\App\Models\Role::create($role);
|
Role::create($role);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
use App\Models\StressEvent;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
class StressEventSeeder extends Seeder
|
class StressEventSeeder extends Seeder
|
||||||
@@ -56,7 +56,7 @@ class StressEventSeeder extends Seeder
|
|||||||
];
|
];
|
||||||
|
|
||||||
foreach ($events as $event) {
|
foreach ($events as $event) {
|
||||||
\App\Models\StressEvent::create($event);
|
StressEvent::create($event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-6
@@ -1,9 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\Api\AuthController;
|
||||||
|
use App\Http\Controllers\Api\EmotionController;
|
||||||
|
use App\Http\Controllers\Api\InformationController;
|
||||||
|
use App\Http\Controllers\Api\StressDiagnosticController;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use App\Http\Controllers\Api\AuthController;
|
|
||||||
use App\Http\Controllers\Api\StressDiagnosticController;
|
|
||||||
|
|
||||||
// Route pour le login (accessible par le visiteur anonyme)
|
// Route pour le login (accessible par le visiteur anonyme)
|
||||||
Route::post('/login', [AuthController::class, 'login']);
|
Route::post('/login', [AuthController::class, 'login']);
|
||||||
@@ -23,10 +25,10 @@ Route::middleware('auth:sanctum')->group(function () {
|
|||||||
// Route pour enregistrer un diagnostic effectué sur mobile
|
// Route pour enregistrer un diagnostic effectué sur mobile
|
||||||
Route::post('/stress-diagnostics', [StressDiagnosticController::class, 'store']);
|
Route::post('/stress-diagnostics', [StressDiagnosticController::class, 'store']);
|
||||||
|
|
||||||
Route::get('/emotions', [App\Http\Controllers\Api\EmotionController::class, 'index']);
|
Route::get('/emotions', [EmotionController::class, 'index']);
|
||||||
Route::post('/emotions', [App\Http\Controllers\Api\EmotionController::class, 'store']);
|
Route::post('/emotions', [EmotionController::class, 'store']);
|
||||||
|
|
||||||
// Routes pour les informations (accessibles aux utilisateurs connectés)
|
// Routes pour les informations (accessibles aux utilisateurs connectés)
|
||||||
Route::get('/informations', [App\Http\Controllers\Api\InformationController::class, 'index']);
|
Route::get('/informations', [InformationController::class, 'index']);
|
||||||
Route::get('/informations/{id}', [App\Http\Controllers\Api\InformationController::class, 'show']);
|
Route::get('/informations/{id}', [InformationController::class, 'show']);
|
||||||
});
|
});
|
||||||
|
|||||||
+5
-7
@@ -1,15 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Route;
|
|
||||||
|
|
||||||
use App\Http\Controllers\DashboardController;
|
use App\Http\Controllers\DashboardController;
|
||||||
use App\Http\Controllers\Web\EmotionController;
|
|
||||||
use App\Http\Controllers\Web\DiagnosticController;
|
|
||||||
use App\Http\Controllers\Web\RelaxationController;
|
|
||||||
use App\Http\Controllers\Web\AuthController;
|
use App\Http\Controllers\Web\AuthController;
|
||||||
use App\Http\Controllers\Web\ProfileController;
|
use App\Http\Controllers\Web\DiagnosticController;
|
||||||
|
use App\Http\Controllers\Web\EmotionController;
|
||||||
use App\Http\Controllers\Web\InformationController;
|
use App\Http\Controllers\Web\InformationController;
|
||||||
|
use App\Http\Controllers\Web\ProfileController;
|
||||||
|
use App\Http\Controllers\Web\RelaxationController;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
// Public Routes
|
// Public Routes
|
||||||
Route::get('/', [DashboardController::class, 'index'])->name('home');
|
Route::get('/', [DashboardController::class, 'index'])->name('home');
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
namespace Tests\Feature;
|
namespace Tests\Feature;
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Models\Role;
|
use App\Models\Role;
|
||||||
|
use App\Models\User;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
@@ -31,7 +31,12 @@ class AdminAccessTest extends TestCase
|
|||||||
public function test_admin_user_can_access_admin_panel(): void
|
public function test_admin_user_can_access_admin_panel(): void
|
||||||
{
|
{
|
||||||
$role = Role::create(['libelle' => 'Admin']);
|
$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');
|
$response = $this->actingAs($user)->get('/admin');
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
namespace Tests\Feature;
|
namespace Tests\Feature;
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Models\Role;
|
use App\Models\Role;
|
||||||
|
use App\Models\User;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Tests\TestCase;
|
|
||||||
use PHPUnit\Framework\Attributes\Test;
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
class AuthSecurityTest extends TestCase
|
class AuthSecurityTest extends TestCase
|
||||||
{
|
{
|
||||||
@@ -30,7 +30,7 @@ class AuthSecurityTest extends TestCase
|
|||||||
|
|
||||||
// 2. Action : Simuler une connexion avec Sanctum
|
// 2. Action : Simuler une connexion avec Sanctum
|
||||||
$response = $this->actingAs($user, '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)
|
// 3. Assertion : Vérifier que l'accès est accordé (200)
|
||||||
$response->assertStatus(200);
|
$response->assertStatus(200);
|
||||||
|
|||||||
@@ -2,23 +2,30 @@
|
|||||||
|
|
||||||
namespace Tests\Feature;
|
namespace Tests\Feature;
|
||||||
|
|
||||||
use App\Models\EvenementStress;
|
use App\Models\StressEvent;
|
||||||
|
use App\Models\User;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Foundation\Testing\WithFaker;
|
use Laravel\Sanctum\Sanctum;
|
||||||
use Tests\TestCase;
|
|
||||||
use PHPUnit\Framework\Attributes\Test;
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
class BusinessLogicTest extends TestCase
|
class BusinessLogicTest extends TestCase
|
||||||
{
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function la_liste_des_evenements_de_stress_est_accessible()
|
public function la_liste_des_evenements_de_stress_est_accessible()
|
||||||
{
|
{
|
||||||
// On crée un événement simulé
|
// La route /api/stress-events est protégée par Sanctum :
|
||||||
EvenementStress::create(['libelle' => 'Mariage', 'points' => 50]);
|
// 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 = $this->getJson('/api/stress-events');
|
||||||
|
|
||||||
$response->assertStatus(200)
|
$response->assertStatus(200)
|
||||||
->assertJsonFragment(['libelle' => 'Mariage']);
|
->assertJsonFragment(['event_name' => 'Mariage']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,10 @@
|
|||||||
|
|
||||||
namespace Tests\Feature;
|
namespace Tests\Feature;
|
||||||
|
|
||||||
use Tests\TestCase;
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Models\StressEvent;
|
use App\Models\StressEvent;
|
||||||
use App\Models\ResultatDiag;
|
use App\Models\User;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
class DiagnosticTest extends TestCase
|
class DiagnosticTest extends TestCase
|
||||||
{
|
{
|
||||||
@@ -26,7 +25,7 @@ class DiagnosticTest extends TestCase
|
|||||||
// Simuler une requête de diagnostic
|
// Simuler une requête de diagnostic
|
||||||
$response = $this->actingAs($user)
|
$response = $this->actingAs($user)
|
||||||
->post(route('diagnostics.store'), [
|
->post(route('diagnostics.store'), [
|
||||||
'events' => [$event1->id, $event2->id]
|
'events' => [$event1->id, $event2->id],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Vérifier la redirection et le score en session
|
// Vérifier la redirection et le score en session
|
||||||
@@ -37,7 +36,7 @@ class DiagnosticTest extends TestCase
|
|||||||
$this->assertDatabaseHas('resultat_diags', [
|
$this->assertDatabaseHas('resultat_diags', [
|
||||||
'id_user' => $user->id,
|
'id_user' => $user->id,
|
||||||
'score_total' => 160,
|
'score_total' => 160,
|
||||||
'niveau_stress' => 'Modéré'
|
'niveau_stress' => 'Modéré',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie que les en-têtes HTTP de sécurité (middleware SecurityHeaders)
|
||||||
|
* sont bien présents sur les réponses de l'application.
|
||||||
|
*/
|
||||||
|
class SecurityHeadersTest extends TestCase
|
||||||
|
{
|
||||||
|
#[Test]
|
||||||
|
public function les_entetes_de_securite_sont_presents()
|
||||||
|
{
|
||||||
|
$response = $this->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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ class StressCalculationTest extends TestCase
|
|||||||
$totalScore = array_sum($eventPoints);
|
$totalScore = array_sum($eventPoints);
|
||||||
|
|
||||||
// 3. Assertion (La vérification)
|
// 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
|
public function test_stress_level_label(): void
|
||||||
{
|
{
|
||||||
$score = 250;
|
$score = 250;
|
||||||
$level = "";
|
$level = '';
|
||||||
|
|
||||||
if ($score >= 300) {
|
if ($score >= 300) {
|
||||||
$level = "Risque élevé";
|
$level = 'Risque élevé';
|
||||||
} elseif ($score >= 150) {
|
} elseif ($score >= 150) {
|
||||||
$level = "Risque modéré";
|
$level = 'Risque modéré';
|
||||||
} else {
|
} else {
|
||||||
$level = "Risque léger";
|
$level = 'Risque léger';
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->assertEquals("Risque modéré", $level);
|
$this->assertEquals('Risque modéré', $level);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class StressCalculatorTest extends TestCase
|
|||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
$this->calculator = new StressCalculator();
|
$this->calculator = new StressCalculator;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_calculate_score_sums_points_correctly(): void
|
public function test_calculate_score_sums_points_correctly(): void
|
||||||
|
|||||||
@@ -13,3 +13,4 @@
|
|||||||
.externalNativeBuild
|
.externalNativeBuild
|
||||||
.cxx
|
.cxx
|
||||||
local.properties
|
local.properties
|
||||||
|
.idea
|
||||||
@@ -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
|
||||||
Binary file not shown.
@@ -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.
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
@@ -43,3 +43,5 @@ app.*.map.json
|
|||||||
/android/app/debug
|
/android/app/debug
|
||||||
/android/app/profile
|
/android/app/profile
|
||||||
/android/app/release
|
/android/app/release
|
||||||
|
|
||||||
|
.idea
|
||||||
Generated
+168
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"pptxgenjs": "^4.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
+862
@@ -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));
|
||||||
@@ -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));
|
||||||
Reference in New Issue
Block a user