Compare commits

..

28 Commits

Author SHA1 Message Date
sam 29e6fec64d feat: page de telechargement des applications mobiles (Android / iOS)
CI - Intégration continue / quality-and-tests (pull_request) Successful in 3m32s
Page publique /telecharger reprenant le modele de Ressources-relationnel :
- config/mobile.php (lien de store prioritaire, sinon binaire local, sinon 'bientot')
- DownloadController + vue au style DSFR, lien dans la sidebar
- variables MOBILE_* et depot public/downloads/ (binaires gitignores)
- 3 tests (page publique, lien store, bouton desactive)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 00:37:27 +02:00
sam df8af066fc fix(front): heberge Alpine.js via Vite au lieu du CDN (bloque par la CSP)
CI - Intégration continue / quality-and-tests (pull_request) Successful in 3m29s
La Content-Security-Policy stricte bloquait le CDN jsdelivr, donc Alpine ne
se chargeait pas : menu profil toujours ouvert et app de respiration KO.
Alpine est desormais bundle par Vite (servi depuis 'self') → compatible CSP,
sans dependance externe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 19:19:54 +02:00
sam 7d52c1e440 feat(tickets): suivi bidirectionnel des tickets utilisateur via Gitea
CI - Intégration continue / quality-and-tests (pull_request) Successful in 3m27s
Le formulaire 'fire-and-forget' devient un vrai systeme de suivi :
- table locale tickets (user <-> numero d'issue Gitea)
- liste 'Mes tickets' avec etat (En cours / Resolu) et jalon
- page de suivi : etat, labels, jalon, fil de discussion (commentaires)
- reponse depuis le web (ajoute un commentaire sur l'issue Gitea)
- controle d'acces : un utilisateur ne voit que ses propres tickets
- GiteaIssueService etendu (getIssue, getComments, addComment)
- remplace l'ancien SuggestionController ; 5 tests de bout en bout

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 23:38:42 +02:00
sam 69da55b292 docs(env): DB_HOST generique (serveur MySQL du reseau) dans le template prod
CI - Intégration continue / quality-and-tests (pull_request) Successful in 3m30s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 22:43:41 +02:00
sam d0eb1252d5 ci(deploy): genere vendor et assets sur l'hote (le bind-mount masque ceux de l'image)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 22:11:41 +02:00
sam 9a0015f205 test: neutralise Vite dans le TestCase de base (CI sans assets compiles)
CI - Intégration continue / quality-and-tests (pull_request) Successful in 3m30s
Corrige l'echec ExampleTest (ViteManifestNotFoundException) en CI :
withoutVite() est desormais applique globalement, le rendu des vues ne
depend plus du manifeste public/build/manifest.json.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 21:20:16 +02:00
sam 94b4218ba5 docs: section systeme de tickets + synchronisation CI/CD (PHPStan, SonarQube, deploiement SSH)
- dossier : 3.2 remontee des demandes depuis l'app (formulaire -> issue Gitea)
- dossier : CI/CD reecrit (PHPStan, couverture, SonarQube, deploiement SSH) + diagramme + annexes
- slides : mention du formulaire in-app + legende CD SSH

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 20:56:00 +02:00
sam f1825434ed feat: systeme de tickets utilisateur connecte a Gitea
CI - Intégration continue / quality-and-tests (pull_request) Failing after 3m10s
Formulaire de suggestions/signalements (utilisateur connecte) qui cree une
issue dans le depot Gitea via son API v1 :
- GiteaIssueService (appel API), SuggestionController (validation + creation)
- vue suggestions/create au style DSFR, lien dans la sidebar (@auth)
- config services.gitea + variables GITEA_URL/TOKEN/REPO
- tests de bout en bout (auth, validation, appel API mocke)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 20:43:49 +02:00
sam d11a297fd2 chore(ci): runner act_runner sur volume nomme (compatible Portainer)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 20:22:58 +02:00
sam 6680003b4d fix(securite): mise a jour des dependances pour corriger 27 CVE (Filament, Laravel, Symfony, Guzzle)
- composer update : filament 3.3.54, laravel/symfony/guzzle patches
- composer audit ne remonte plus aucune vulnerabilite
- ajout d'une etape composer audit (informative) dans la CI

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 20:22:32 +02:00
sam 9ec95111a0 ci: alignement sur le standard qualite (Larastan, couverture, SonarQube) et deploiement SSH
CI - Intégration continue / quality-and-tests (pull_request) Failing after 2m35s
- ci.yml: ajout analyse statique PHPStan/Larastan, couverture PHPUnit (clover+junit) et analyse SonarQube conditionnelle
- deploy.yml: deploiement continu par SSH (git reset + docker compose up --build + migrations + cache), conteneur cesizen-app
- phpstan.neon (niveau 5), grumphp.yml (hooks qualite locaux), sonar-project.properties, docker-compose.sonar.yml
- ajout larastan/larastan et phpro/grumphp en require-dev

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 18:59:02 +02:00
sam 4ffce393de chore(models): annotations @property (Role, User) pour l'analyse statique
Permet a Larastan (PHPStan niveau 5) de resoudre $user->role->libelle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 18:59:02 +02:00
sam 89b4107266 docs: dossier de deploiement/securisation, presentation BLOC 3 et CHANGELOG
CI - Web (Laravel) / quality-and-tests (pull_request) Failing after 4m4s
- docs/Dossier_Deploiement_Securisation_CESIZen.docx (15-20 pages)
- presentation_bloc3.js -> CESIZen_Presentation_Bloc3.pptx (10 slides)
- diagrammes architecture et CI/CD (docs/img)
- CHANGELOG.md (versionnage semantique)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 18:44:13 +02:00
sam 70d4767890 chore(app): mises a jour applicatives et normalisation du code (Laravel Pint)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 18:44:13 +02:00
sam c51f17a74c test: fiabilisation de la suite de tests
- BusinessLogicTest: utilise le vrai modele StressEvent + auth Sanctum
- AdminAccessTest: compte actif requis pour l'acces au panel
- SecurityHeadersTest: verifie les en-tetes de securite
- suppression du modele orphelin EvenementStress

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 18:44:13 +02:00
sam ebe1836f7e feat(securite): en-tetes HTTP, trust proxies et configuration de production durcie
- middleware SecurityHeaders (HSTS, CSP, X-Frame-Options, etc.)
- trustProxies pour le reverse proxy Nginx Proxy Manager
- .env.production.example (APP_DEBUG=false, sessions chiffrees, cookies Secure)
- politique de securite SECURITY.md

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 18:43:17 +02:00
sam d75afe0aac ci: pipelines Gitea Actions (CI/CD) + modeles de tickets et runner
- ci.yml: tests PHPUnit, Pint, audit securite, build Vite
- deploy.yml: build image -> registre Gitea -> webhook Portainer -> health check
- modeles d'issues (incident / evolution) et de Pull Request
- docker-compose du runner act_runner

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 18:43:17 +02:00
sam c3e1c95f1b chore(repo): ajout du .gitignore racine et arret du suivi de node_modules/.idea
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 18:43:17 +02:00
sam 6515d8348d Correction fonctionnalité rear display 2026-05-20 22:17:01 +02:00
sam d0045a7d61 Ajout fonctionnalité écran arriere 2026-05-20 21:50:54 +02:00
sam 0074c28dcd Ajout doc 2026-05-18 23:11:49 +02:00
sam 4cba9ce74b Correction ajout image pour les informations 2026-05-14 19:29:48 +02:00
sam 89610b807c ajout de filtre de guides et informations 2026-05-14 19:09:28 +02:00
sam 4633805dbd save 2026-05-14 19:02:29 +02:00
sam 7f95b8a2ef save 2026-05-14 19:00:55 +02:00
sam 8eb5d93514 Save 2026-05-14 16:59:59 +00:00
sam 15fcbcc9eb Save 2026-05-14 16:54:32 +00:00
sam 85ad36880c Multiples corrections 2026-05-14 18:52:40 +02:00
290 changed files with 22930 additions and 10733 deletions
+21
View File
@@ -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é
+57
View File
@@ -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: |
- [ ] ...
- [ ] ...
+100
View File
@@ -0,0 +1,100 @@
name: CI - Intégration continue
# Intégration continue : à chaque push et chaque pull request, on vérifie
# automatiquement la qualité du code (Laravel Pint, analyse statique Larastan)
# puis on exécute les tests automatisés (PHPUnit) avec couverture. Un
# déploiement (voir deploy.yml) ne doit se faire que si cette étape est au vert.
#
# NB : l'application Laravel vit dans le sous-dossier WEB/ (monorepo).
on:
push:
branches: [main]
paths:
- 'WEB/**'
- '.gitea/workflows/ci.yml'
pull_request:
branches: [main]
paths:
- 'WEB/**'
jobs:
quality-and-tests:
runs-on: ubuntu-latest
# Toutes les commandes s'exécutent dans le dossier de l'app Laravel.
defaults:
run:
working-directory: WEB
# Jetons SonarQube, définis dans les secrets du dépôt Gitea.
# Exposés au niveau du job pour pouvoir conditionner l'étape d'analyse
# (elle ne s'exécute que si un jeton est configuré).
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
steps:
- name: Récupération du code
uses: actions/checkout@v4
with:
# Historique complet : SonarQube en a besoin pour dater le code
# et calculer correctement le "nouveau code".
fetch-depth: 0
- name: Installation de PHP 8.2
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: pdo_mysql, pdo_sqlite, sqlite3, mbstring, exif, pcntl, bcmath, gd, intl, zip
# pcov : moteur de couverture rapide, requis pour le rapport SonarQube.
coverage: pcov
- name: Cache des dépendances Composer
uses: actions/cache@v4
with:
path: WEB/vendor
key: composer-${{ hashFiles('WEB/composer.lock') }}
- name: Installation des dépendances
run: composer install --no-interaction --prefer-dist --optimize-autoloader
- name: Préparation de l'environnement de test
run: |
cp .env.example .env
php artisan key:generate
- name: Vérification du style de code (Laravel Pint)
run: vendor/bin/pint --test
- name: Analyse statique (Larastan / PHPStan)
run: vendor/bin/phpstan analyse --configuration=phpstan.neon --memory-limit=512M --no-progress
- name: Audit de sécurité des dépendances (composer audit)
# Signale les vulnérabilités connues (CVE) des dépendances.
# Informatif : n'échoue pas le build, car un correctif amont peut ne
# pas être encore disponible ; les dépendances sont tenues à jour.
run: composer audit --no-interaction
continue-on-error: true
- name: Exécution des tests + couverture (PHPUnit)
# phpunit.xml définit déjà l'environnement de test (sqlite en mémoire).
# On génère le rapport de couverture (clover) et le rapport de tests
# (junit) consommés ensuite par SonarQube.
run: |
mkdir -p coverage
vendor/bin/phpunit \
--coverage-clover=coverage/clover.xml \
--log-junit=coverage/junit.xml
- name: Analyse SonarQube
# Ne s'exécute que si le secret SONAR_TOKEN est configuré dans Gitea,
# sinon l'étape est ignorée (la CI reste verte tant que le serveur
# SonarQube n'est pas branché).
if: ${{ env.SONAR_TOKEN != '' }}
uses: sonarsource/sonarqube-scan-action@v5
with:
# L'app (et sonar-project.properties) se trouve dans WEB/.
projectBaseDir: WEB
# Le scanner lit sonar-project.properties et utilise SONAR_HOST_URL /
# SONAR_TOKEN exposés au niveau du job.
+77
View File
@@ -0,0 +1,77 @@
name: CD - Déploiement continu
# Déploiement continu : après un push sur main, une fois la CI passée,
# on se connecte en SSH au serveur de production et on met à jour l'application
# (git reset, rebuild des conteneurs, migrations, mise en cache).
#
# L'application Laravel vit dans le sous-dossier WEB/ (monorepo) ; le conteneur
# PHP de production s'appelle « cesizen-app » (cf. WEB/docker-compose.yml).
#
# Secrets à définir dans Gitea (Settings > Actions > Secrets du dépôt) :
# - SSH_HOST : IP ou domaine du serveur
# - SSH_USER : utilisateur SSH
# - SSH_PRIVATE_KEY : clé privée SSH autorisée sur le serveur
# - SSH_PORT : port SSH (optionnel, 22 par défaut)
# - DEPLOY_PATH : chemin du dépôt cloné sur le serveur (ex: /home/sam/CESIZen)
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Déploiement sur le serveur via SSH
uses: appleboy/ssh-action@v1.2.0
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
port: ${{ secrets.SSH_PORT }}
script: |
set -e
cd "${{ secrets.DEPLOY_PATH }}"
DEPLOY_UID=$(id -u)
# L'utilisateur de déploiement doit posséder storage/ & bootstrap/cache
# AVANT toute opération git (un déploiement précédent a pu les passer à
# www-data, ce qui bloquerait git). chown via le conteneur (= root).
docker exec -u 0 cesizen-app chown -R "${DEPLOY_UID}:www-data" \
/var/www/storage /var/www/bootstrap/cache || true
# Récupération propre de la dernière version : on ÉCRASE toute modif
# locale parasite (le serveur est une copie de déploiement, aucune
# édition manuelle n'est censée y vivre). Les fichiers non suivis
# (.env, cert.pem…) ne sont PAS touchés par reset --hard.
git fetch origin main
git reset --hard origin/main
# Reconstruction et redémarrage des conteneurs (compose dans WEB/)
cd WEB
docker compose up -d --build
# Le bind-mount ./:/var/www masque le vendor/ et public/build/
# construits dans l'image : on les (re)génère sur l'hôte, dans le
# conteneur, pour qu'ils soient présents via le montage.
docker exec cesizen-app composer install --no-interaction --no-dev --optimize-autoloader
docker exec cesizen-app npm ci
docker exec cesizen-app npm run build
# Migrations de la base de données (sans interaction)
docker exec cesizen-app php artisan migrate --force
# Mise en cache de la configuration et des routes (performances en prod)
docker exec cesizen-app php artisan config:cache
docker exec cesizen-app php artisan route:cache
docker exec cesizen-app php artisan view:cache
# Correction des permissions sur les dossiers écrits par Laravel.
# Propriétaire = utilisateur de déploiement (pour les futurs git reset),
# groupe = www-data avec écriture de groupe + setgid (héritage du groupe).
docker exec -u 0 cesizen-app sh -c "\
chown -R ${DEPLOY_UID}:www-data /var/www/storage /var/www/bootstrap/cache && \
chmod -R ug+rwX /var/www/storage /var/www/bootstrap/cache && \
find /var/www/storage /var/www/bootstrap/cache -type d -exec chmod g+s {} +"
+22
View File
@@ -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/
-3
View File
@@ -1,3 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
-52
View File
@@ -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>
-1670
View File
File diff suppressed because it is too large Load Diff
-11
View File
@@ -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>
-8
View File
@@ -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>
-5
View File
@@ -1,5 +0,0 @@
<project version="4">
<component name="ProjectRootManager" version="2">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
-8
View File
@@ -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>
-7
View File
@@ -1,7 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Launch All (Mobile &amp; Wear)" type="CompoundRunConfigurationType">
<toRun name="Mobile App" type="FlutterRunConfigurationType" />
<toRun name="Wear App" type="AndroidRunConfigurationType" />
<method v="2" />
</configuration>
</component>
-7
View File
@@ -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>
-68
View File
@@ -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
View File
@@ -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.
+29
View File
@@ -0,0 +1,29 @@
# 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é
- Page publique de téléchargement des applications mobiles Android / iOS (`/telecharger`).
- Système de tickets connecté à Gitea : création, suivi de l'avancement (état, jalon, labels), fil de discussion et réponses depuis l'interface web.
- 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
View File
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+5
View File
@@ -0,0 +1,5 @@
node_modules
vendor
public/build
.git
.env
+12
View File
@@ -62,4 +62,16 @@ AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET= AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false AWS_USE_PATH_STYLE_ENDPOINT=false
# --- Gitea (tickets créés depuis le formulaire de suggestions du site) ---
# Token d'accès Gitea avec le scope "write:issue" (Settings > Applications > Generate Token).
GITEA_URL=https://gitea.sam-coffre.duckdns.org
GITEA_TOKEN=
GITEA_REPO=sam/cesizen
# --- Applications mobiles (page /telecharger) ---
MOBILE_ANDROID_URL=
MOBILE_IOS_URL=
MOBILE_ANDROID_VERSION=1.0.0
MOBILE_IOS_VERSION=1.0.0
VITE_APP_NAME="${APP_NAME}" VITE_APP_NAME="${APP_NAME}"
+69
View File
@@ -0,0 +1,69 @@
# ══════════════════════════════════════════════════════════════
# 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 (serveur MySQL/MariaDB du réseau) ─────────
# DB_HOST = IP ou nom d'hôte du serveur MySQL (ex : 192.168.x.x sur le LAN,
# ou le nom d'un conteneur si la base est dans la même stack Docker).
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=cesizen
DB_USERNAME=cesizen_app
DB_PASSWORD=
# ── 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}"
# ── Gitea (tickets créés depuis le formulaire de suggestions) ─
GITEA_URL=https://gitea.sam-coffre.duckdns.org
GITEA_TOKEN= # token Gitea avec le scope write:issue
GITEA_REPO=sam/cesizen
# ── Applications mobiles (page /telecharger) ─────────────────
# Liens de store (prioritaires) ou dépôt des binaires dans public/downloads/.
MOBILE_ANDROID_URL=
MOBILE_IOS_URL=
MOBILE_ANDROID_VERSION=1.0.0
MOBILE_IOS_VERSION=1.0.0
VITE_APP_NAME="${APP_NAME}"
+3
View File
@@ -18,7 +18,10 @@
/public/storage /public/storage
/storage/*.key /storage/*.key
/storage/pail /storage/pail
/public/downloads/*.apk
/public/downloads/*.ipa
/vendor /vendor
Homestead.json Homestead.json
Homestead.yaml Homestead.yaml
Thumbs.db Thumbs.db
.idea
+51
View File
@@ -0,0 +1,51 @@
FROM php:8.3-fpm
# 1. Installer les dépendances système
RUN apt-get update && apt-get install -y \
git \
curl \
libpng-dev \
libonig-dev \
libxml2-dev \
libicu-dev \
libzip-dev \
zip \
unzip \
gnupg
# 2. Installer Node.js (pour Vite)
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
apt-get install -y nodejs
# 3. Nettoyer le cache apt
RUN apt-get clean && rm -rf /var/lib/apt/lists/*
# 4. Installer les extensions PHP
# On ajoute 'zip' et on configure 'intl' correctement
RUN docker-php-ext-configure intl && \
docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd intl zip
# 5. Installer Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# 6. Définir le répertoire de travail
WORKDIR /var/www
# 7. Copier le projet
COPY . .
# 8. Installer les dépendances PHP et Node
RUN composer install --no-interaction --optimize-autoloader --no-dev
RUN npm install
# 9. Forcer les permissions sur les binaires node_modules
RUN chmod -R +x node_modules/.bin/
# 10. Lancer le build de Vite
RUN npm run build
# 11. Fixer les permissions pour Laravel
RUN chown -R www-data:www-data /var/www/storage /var/www/bootstrap/cache
EXPOSE 9000
CMD ["php-fpm"]
+37 -53
View File
@@ -1,59 +1,43 @@
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p> # 🖥️ CESIZen - Plateforme Web & API
<p align="center"> Ce répertoire contient le backend Laravel, l'API REST sécurisée par Sanctum, et le tableau de bord d'administration utilisant le Design System de l'État Français (DSFR).
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>
## About Laravel ## 🛠️ Guide d'installation
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as: ### Prérequis
- Docker et Docker Compose
- Composer (optionnel si installation via Docker)
- Node.js & NPM
- [Simple, fast routing engine](https://laravel.com/docs/routing). ### Étapes d'installation
- [Powerful dependency injection container](https://laravel.com/docs/container). 1. **Cloner le projet** et entrer dans le dossier `WEB`.
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage. 2. **Configuration de l'environnement** :
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent). ```bash
- Database agnostic [schema migrations](https://laravel.com/docs/migrations). cp .env.example .env
- [Robust background job processing](https://laravel.com/docs/queues). ```
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting). 3. **Lancer les conteneurs Docker** :
```bash
docker-compose up -d
```
4. **Installer les dépendances PHP & JS** :
```bash
docker exec cesizen-app composer install
docker exec cesizen-app npm install
docker exec cesizen-app npm run build
```
5. **Initialiser la base de données** :
```bash
docker exec cesizen-app php artisan key:generate
docker exec cesizen-app php artisan migrate --seed
```
6. **Accès** :
- Web : `http://localhost:8090`
- API : `http://localhost:8091`
Laravel is accessible, powerful, and provides tools required for large, robust applications. ---
## Learning Laravel ## 🏗️ Architecture Technique (API)
- **Framework** : Laravel 10 (PHP 8.2)
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. You can also check out [Laravel Learn](https://laravel.com/learn), where you will be guided through building a modern Laravel application. - **Auth** : Laravel Sanctum (Tokens Bearer)
- **Admin** : FilamentPHP / DSFR
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library. - **Base de données** : MySQL 8.0
## Laravel Sponsors
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
### Premium Partners
- **[Vehikl](https://vehikl.com)**
- **[Tighten Co.](https://tighten.co)**
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
- **[64 Robots](https://64robots.com)**
- **[Curotec](https://www.curotec.com/services/technologies/laravel)**
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
- **[Redberry](https://redberry.international/laravel-development)**
- **[Active Logic](https://activelogic.com)**
## Contributing
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
## Code of Conduct
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
## Security Vulnerabilities
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
## License
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
@@ -0,0 +1,100 @@
<?php
namespace App\Filament\Admin\Resources;
use App\Filament\Admin\Resources\InformationResource\Pages;
use App\Models\Information;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
class InformationResource extends Resource
{
protected static ?string $model = Information::class;
protected static ?string $navigationIcon = 'heroicon-o-information-circle';
protected static ?string $navigationLabel = 'Informations';
protected static ?string $modelLabel = 'Information';
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('title')
->label('Titre')
->required()
->maxLength(255),
Forms\Components\TextInput::make('category')
->label('Catégorie')
->required()
->maxLength(255)
->default('Général'),
Forms\Components\RichEditor::make('content')
->label('Contenu')
->required()
->columnSpanFull(),
Forms\Components\FileUpload::make('image_url')
->label('Image')
->image()
->disk('public') // Ajoutez ceci pour forcer l'utilisation du disque public
->directory('informations')
->visibility('public'),
Forms\Components\Toggle::make('is_published')
->label('Publié')
->default(true)
->required(),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('title')
->label('Titre')
->searchable(),
Tables\Columns\TextColumn::make('category')
->label('Catégorie')
->badge()
->searchable(),
Tables\Columns\IconColumn::make('is_published')
->label('Publié')
->boolean(),
Tables\Columns\TextColumn::make('created_at')
->label('Créé le')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
Tables\Filters\TernaryFilter::make('is_published')
->label('Statut de publication')
->boolean()
->trueLabel('Publiés')
->falseLabel('Brouillons')
->native(false),
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getPages(): array
{
return [
'index' => Pages\ListInformation::route('/'),
'create' => Pages\CreateInformation::route('/create'),
'edit' => Pages\EditInformation::route('/{record}/edit'),
];
}
}
@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Admin\Resources\InformationResource\Pages;
use App\Filament\Admin\Resources\InformationResource;
use Filament\Resources\Pages\CreateRecord;
class CreateInformation extends CreateRecord
{
protected static string $resource = InformationResource::class;
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Admin\Resources\InformationResource\Pages;
use App\Filament\Admin\Resources\InformationResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditInformation extends EditRecord
{
protected static string $resource = InformationResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Admin\Resources\InformationResource\Pages;
use App\Filament\Admin\Resources\InformationResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListInformation extends ListRecords
{
protected static string $resource = InformationResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}
@@ -0,0 +1,103 @@
<?php
namespace App\Filament\Admin\Resources;
use App\Filament\Admin\Resources\RelaxationActivityResource\Pages;
use App\Models\RelaxationActivity;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
class RelaxationActivityResource extends Resource
{
protected static ?string $model = RelaxationActivity::class;
protected static ?string $navigationIcon = 'heroicon-o-sparkles';
protected static ?string $navigationLabel = 'Activités de détente';
protected static ?string $modelLabel = 'Activité de détente';
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('title')
->label('Titre')
->required()
->maxLength(255),
Forms\Components\Select::make('type')
->options([
'video' => 'Vidéo',
'audio' => 'Audio',
'article' => 'Article',
'exercice' => 'Exercice',
])
->required(),
Forms\Components\TextInput::make('duration')
->label('Durée (min)')
->numeric(),
Forms\Components\TextInput::make('url')
->label('Lien / URL')
->url()
->maxLength(255),
Forms\Components\Textarea::make('description')
->columnSpanFull(),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('title')
->label('Titre')
->searchable(),
Tables\Columns\TextColumn::make('type')
->badge(),
Tables\Columns\TextColumn::make('duration')
->label('Durée')
->suffix(' min'),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
Tables\Filters\SelectFilter::make('type')
->options([
'video' => 'Vidéo',
'audio' => 'Audio',
'article' => 'Article',
'exercice' => 'Exercice',
]),
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListRelaxationActivities::route('/'),
'create' => Pages\CreateRelaxationActivity::route('/create'),
'edit' => Pages\EditRelaxationActivity::route('/{record}/edit'),
];
}
}
@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Admin\Resources\RelaxationActivityResource\Pages;
use App\Filament\Admin\Resources\RelaxationActivityResource;
use Filament\Resources\Pages\CreateRecord;
class CreateRelaxationActivity extends CreateRecord
{
protected static string $resource = RelaxationActivityResource::class;
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Admin\Resources\RelaxationActivityResource\Pages;
use App\Filament\Admin\Resources\RelaxationActivityResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditRelaxationActivity extends EditRecord
{
protected static string $resource = RelaxationActivityResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Admin\Resources\RelaxationActivityResource\Pages;
use App\Filament\Admin\Resources\RelaxationActivityResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListRelaxationActivities extends ListRecords
{
protected static string $resource = RelaxationActivityResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}
@@ -0,0 +1,89 @@
<?php
namespace App\Filament\Admin\Resources;
use App\Filament\Admin\Resources\StressEventResource\Pages;
use App\Models\StressEvent;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
class StressEventResource extends Resource
{
protected static ?string $model = StressEvent::class;
protected static ?string $navigationIcon = 'heroicon-o-bolt';
protected static ?string $navigationLabel = 'Événements de stress';
protected static ?string $modelLabel = 'Événement de stress';
protected static ?string $pluralModelLabel = 'Événements de stress';
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('event_name')
->label('Nom de l\'événement')
->required()
->maxLength(255),
Forms\Components\TextInput::make('points')
->label('Points de stress')
->required()
->numeric()
->minValue(0)
->maxValue(100),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('event_name')
->label('Événement')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('points')
->label('Points')
->numeric()
->sortable(),
Tables\Columns\TextColumn::make('updated_at')
->label('Dernière modification')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListStressEvents::route('/'),
'create' => Pages\CreateStressEvent::route('/create'),
'edit' => Pages\EditStressEvent::route('/{record}/edit'),
];
}
}
@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Admin\Resources\StressEventResource\Pages;
use App\Filament\Admin\Resources\StressEventResource;
use Filament\Resources\Pages\CreateRecord;
class CreateStressEvent extends CreateRecord
{
protected static string $resource = StressEventResource::class;
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Admin\Resources\StressEventResource\Pages;
use App\Filament\Admin\Resources\StressEventResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditStressEvent extends EditRecord
{
protected static string $resource = StressEventResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Admin\Resources\StressEventResource\Pages;
use App\Filament\Admin\Resources\StressEventResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListStressEvents extends ListRecords
{
protected static string $resource = StressEventResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}
@@ -0,0 +1,104 @@
<?php
namespace App\Filament\Admin\Resources;
use App\Filament\Admin\Resources\UserResource\Pages;
use App\Models\User;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
class UserResource extends Resource
{
protected static ?string $model = User::class;
protected static ?string $navigationIcon = 'heroicon-o-users';
protected static ?string $navigationLabel = 'Utilisateurs';
protected static ?string $modelLabel = 'Utilisateur';
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('name')
->required()
->maxLength(255),
Forms\Components\TextInput::make('email')
->email()
->required()
->maxLength(255),
Forms\Components\Select::make('id_role')
->relationship('role', 'libelle')
->required(),
Forms\Components\TextInput::make('password')
->password()
->dehydrated(fn ($state) => filled($state))
->required(fn (string $context): bool => $context === 'create'),
Forms\Components\Toggle::make('is_active')
->label('Compte actif')
->default(true)
->required(),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')
->searchable(),
Tables\Columns\TextColumn::make('email')
->searchable(),
Tables\Columns\TextColumn::make('role.libelle')
->label('Rôle')
->badge()
->color(fn (string $state): string => match ($state) {
'Admin' => 'danger',
'Utilisateur' => 'info',
default => 'gray',
}),
Tables\Columns\ToggleColumn::make('is_active')
->label('Actif'),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
Tables\Filters\TernaryFilter::make('is_active')
->label('Statut')
->boolean()
->trueLabel('Utilisateurs actifs')
->falseLabel('Utilisateurs désactivés')
->native(false),
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListUsers::route('/'),
'create' => Pages\CreateUser::route('/create'),
'edit' => Pages\EditUser::route('/{record}/edit'),
];
}
}
@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Admin\Resources\UserResource\Pages;
use App\Filament\Admin\Resources\UserResource;
use Filament\Resources\Pages\CreateRecord;
class CreateUser extends CreateRecord
{
protected static string $resource = UserResource::class;
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Admin\Resources\UserResource\Pages;
use App\Filament\Admin\Resources\UserResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditUser extends EditRecord
{
protected static string $resource = UserResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Admin\Resources\UserResource\Pages;
use App\Filament\Admin\Resources\UserResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListUsers extends ListRecords
{
protected static string $resource = UserResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}
+22 -13
View File
@@ -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,13 +25,21 @@ 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
if (! $user->is_active) {
return response([
'status' => 'error',
'message' => 'Votre compte a été désactivé par un administrateur.',
], 403);
}
// Log::info('Email reçu: ' . $request->email); // Log::info('Email reçu: ' . $request->email);
// Log::info('Password saisi: ' . $request->password); // Log::info('Password saisi: ' . $request->password);
// Log::info('Hachage en BDD: ' . $user->password); // Log::info('Hachage en BDD: ' . $user->password);
@@ -41,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);
} }
@@ -54,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);
} }
@@ -79,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);
} }
@@ -92,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);
} }
@@ -111,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);
} }
} }
@@ -0,0 +1,38 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Information;
class InformationController extends Controller
{
public function index()
{
$informations = Information::where('is_published', true)
->orderBy('created_at', 'desc')
->get();
return response()->json([
'status' => 'success',
'data' => $informations,
]);
}
public function show($id)
{
$information = Information::where('is_published', true)->find($id);
if (! $information) {
return response()->json([
'status' => 'error',
'message' => 'Information non trouvée',
], 404);
}
return response()->json([
'status' => 'success',
'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,40 +16,38 @@ 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) 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;
if ($request->has('events')) { if ($request->has('events')) {
$totalPoints = StressEvent::whereIn('id', $request->events)->sum('points'); $points = StressEvent::whereIn('id', $request->events)->pluck('points')->toArray();
$totalPoints = $calculator->calculateScore($points);
} }
$niveauStress = 'Faible'; $niveauStress = $calculator->determineLevel($totalPoints);
if ($totalPoints >= 300) {
$niveauStress = 'Élevé';
} elseif ($totalPoints >= 150) {
$niveauStress = 'Modéré';
}
// 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,
@@ -0,0 +1,45 @@
<?php
namespace App\Http\Controllers\Web;
use App\Http\Controllers\Controller;
class DownloadController extends Controller
{
// Page publique de téléchargement des applications mobiles
public function index()
{
return view('telecharger', [
'android' => $this->resoudre('android'),
'ios' => $this->resoudre('ios'),
]);
}
/**
* Détermine, pour une plateforme, le lien de téléchargement et sa disponibilité.
*
* @return array{version: string, url: string|null, disponible: bool}
*/
private function resoudre(string $plateforme): array
{
$config = config("mobile.{$plateforme}");
if (! is_array($config)) {
$config = [];
}
$version = is_string($config['version'] ?? null) ? $config['version'] : '1.0.0';
$url = (is_string($config['url'] ?? null) && $config['url'] !== '') ? $config['url'] : null;
$file = is_string($config['file'] ?? null) ? $config['file'] : null;
// À défaut d'URL explicite, on propose le fichier précompilé s'il existe.
if ($url === null && $file !== null && file_exists(public_path($file))) {
$url = asset($file);
}
return [
'version' => $version,
'url' => $url,
'disponible' => $url !== null,
];
}
}
@@ -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');
@@ -0,0 +1,36 @@
<?php
namespace App\Http\Controllers\Web;
use App\Http\Controllers\Controller;
use App\Models\Information;
use Illuminate\Http\Request;
class InformationController extends Controller
{
public function index(Request $request)
{
$query = Information::where('is_published', true);
if ($request->has('category') && $request->category != '') {
$query->where('category', $request->category);
}
$informations = $query->orderBy('created_at', 'desc')
->paginate(6)
->withQueryString();
$categories = Information::where('is_published', true)
->distinct()
->pluck('category');
return view('informations.index', compact('informations', 'categories'));
}
public function show($id)
{
$information = Information::where('is_published', true)->findOrFail($id);
return view('informations.show', compact('information'));
}
}
@@ -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,17 +4,25 @@ 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
{ {
public function index() public function index(Request $request)
{ {
$activities = RelaxationActivity::all(); $query = RelaxationActivity::query();
$favorites = [];
return view('relaxation.index', compact('activities', 'favorites')); if ($request->has('category') && $request->category != '') {
$query->where('category', $request->category);
}
$activities = $query->get();
$categories = RelaxationActivity::distinct()->pluck('category');
$favorites = []; // TODO: Implémenter la logique des favoris si nécessaire
return view('relaxation.index', compact('activities', 'favorites', 'categories'));
} }
public function toggleFavorite($id) public function toggleFavorite($id)
@@ -0,0 +1,131 @@
<?php
namespace App\Http\Controllers\Web;
use App\Http\Controllers\Controller;
use App\Models\Ticket;
use App\Models\User;
use App\Services\GiteaIssueService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\Rule;
class TicketController extends Controller
{
public function __construct(private GiteaIssueService $gitea) {}
/**
* Liste des tickets de l'utilisateur, avec leur état courant récupéré
* depuis Gitea (ouvert / résolu, jalon).
*/
public function index()
{
/** @var User $user */
$user = Auth::user();
$tickets = $user->tickets()->latest()->get()->map(function (Ticket $ticket) {
$issue = $this->gitea->getIssue($ticket->gitea_number);
$ticket->setAttribute('state', $issue['state'] ?? null);
$ticket->setAttribute('milestone', $issue['milestone']['title'] ?? null);
return $ticket;
});
return view('tickets.index', compact('tickets'));
}
// Formulaire de création
public function create()
{
return view('tickets.create');
}
// Création d'un ticket -> issue Gitea + enregistrement local
public function store(Request $request)
{
$data = $request->validate([
'type' => ['required', Rule::in(['Modification', 'Ajout', 'Correction'])],
'titre' => ['required', 'string', 'min:5', 'max:120'],
'description' => ['required', 'string', 'min:10', 'max:5000'],
]);
/** @var User $user */
$user = Auth::user();
$auteur = $user->name ?? 'Utilisateur';
$title = "[{$data['type']}] {$data['titre']}";
$body = "**Type de demande :** {$data['type']}\n"
."**Proposé par :** {$auteur} (via le site CESIZen)\n\n"
."### Description\n"
.$data['description']."\n\n"
."---\n"
.'_Ticket créé automatiquement depuis CESIZen._';
$issue = $this->gitea->createIssue($title, $body);
if (! $issue || ! isset($issue['number'])) {
return back()->withInput()->withErrors([
'gitea' => "L'envoi de votre demande a échoué. Merci de réessayer plus tard.",
]);
}
// On mémorise le lien utilisateur <-> issue pour le suivi.
$user->tickets()->create([
'gitea_number' => (int) $issue['number'],
'title' => $data['titre'],
'type' => $data['type'],
]);
return redirect()->route('tickets.show', $issue['number'])
->with('success', 'Votre demande a bien été transmise à l\'équipe.');
}
/**
* Détail d'un ticket : état, jalon, labels et fil de discussion.
*/
public function show(int $number)
{
$ticket = $this->findOwnedTicket($number);
$issue = $this->gitea->getIssue($number);
$comments = $this->gitea->getComments($number);
return view('tickets.show', compact('ticket', 'issue', 'comments'));
}
/**
* Réponse de l'utilisateur : ajoute un commentaire sur l'issue Gitea.
*/
public function reply(Request $request, int $number)
{
$this->findOwnedTicket($number);
$data = $request->validate([
'message' => ['required', 'string', 'min:2', 'max:5000'],
]);
/** @var User $user */
$user = Auth::user();
$auteur = $user->name ?? 'Utilisateur';
$body = "**{$auteur}** (via CESIZen) :\n\n".$data['message'];
if (! $this->gitea->addComment($number, $body)) {
return back()->withErrors(['gitea' => "L'envoi de votre réponse a échoué."]);
}
return redirect()->route('tickets.show', $number)
->with('success', 'Votre réponse a été ajoutée au ticket.');
}
/**
* Récupère le ticket s'il appartient bien à l'utilisateur connecté,
* sinon renvoie une 404 (empêche de consulter les tickets d'autrui).
*/
private function findOwnedTicket(int $number): Ticket
{
/** @var User $user */
$user = Auth::user();
return $user->tickets()->where('gitea_number', $number)->firstOrFail();
}
}
@@ -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;
}
}
-10
View File
@@ -1,10 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class EvenementStress extends Model
{
//
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Information extends Model
{
use HasFactory;
protected $table = 'information';
protected $fillable = [
'title',
'content',
'category',
'image_url',
'is_published',
];
protected $casts = [
'is_published' => 'boolean',
];
}
+5 -2
View File
@@ -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();
} }
} }
+5 -1
View File
@@ -2,9 +2,13 @@
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;
/**
* @property int $id
* @property string $libelle
*/
class Role extends Model class Role extends Model
{ {
use HasFactory; use HasFactory;
+7 -1
View File
@@ -2,9 +2,15 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
class StressEvent extends Model class StressEvent extends Model
{ {
// use HasFactory;
protected $fillable = [
'event_name',
'points',
];
} }
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property int $id
* @property int $user_id
* @property int $gitea_number
* @property string $title
* @property string $type
*/
class Ticket extends Model
{
protected $fillable = [
'user_id',
'gitea_number',
'title',
'type',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
+34 -23
View File
@@ -2,61 +2,72 @@
namespace App\Models; namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail; use Filament\Models\Contracts\FilamentUser;
use Database\Factories\UserFactory; use Filament\Panel;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens; use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable /**
* @property bool $is_active
* @property-read Role|null $role
*/
class User extends Authenticatable implements FilamentUser
{ {
/** @use HasFactory<UserFactory> */
use HasFactory, Notifiable;
use HasApiTokens, HasFactory, Notifiable; use HasApiTokens, HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [ protected $fillable = [
'name', 'name',
'email', 'email',
'password', 'password',
'id_role', 'id_role',
'is_active',
]; ];
/**
* The attributes that should be hidden for serialization.
*
* @var list<string>
*/
protected $hidden = [ protected $hidden = [
'password', 'password',
'remember_token', 'remember_token',
]; ];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array protected function casts(): array
{ {
return [ return [
'email_verified_at' => 'datetime', 'email_verified_at' => 'datetime',
'password' => 'hashed', 'password' => 'hashed',
'is_active' => 'boolean',
]; ];
} }
public function emotionRecords() public function role(): BelongsTo
{ {
return $this->hasMany(EmotionRecord::class); return $this->belongsTo(Role::class, 'id_role');
} }
public function favoritedBy() public function emotionRecords(): HasMany
{
return $this->hasMany(EmotionRecord::class, 'user_id');
}
/**
* @return HasMany<Ticket, $this>
*/
public function tickets(): HasMany
{
return $this->hasMany(Ticket::class);
}
public function favoritedActivities(): BelongsToMany
{ {
return $this->belongsToMany(RelaxationActivity::class, 'favorites', 'id_user', 'id_activite'); return $this->belongsToMany(RelaxationActivity::class, 'favorites', 'id_user', 'id_activite');
} }
public function canAccessPanel(Panel $panel): bool
{
// Autorise l'accès si l'utilisateur est actif et a le rôle 'Admin'
return $this->is_active && $this->role && $this->role->libelle === 'Admin';
}
} }
+4 -1
View File
@@ -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
@@ -19,6 +20,8 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function boot(): void public function boot(): void
{ {
// if (config('app.env') === 'production' || config('app.env') === 'local') {
URL::forceScheme('https');
}
} }
} }
@@ -0,0 +1,58 @@
<?php
namespace App\Providers\Filament;
use Filament\Http\Middleware\Authenticate;
use Filament\Http\Middleware\AuthenticateSession;
use Filament\Http\Middleware\DisableBladeIconComponents;
use Filament\Http\Middleware\DispatchServingFilamentEvent;
use Filament\Pages;
use Filament\Panel;
use Filament\PanelProvider;
use Filament\Support\Colors\Color;
use Filament\Widgets;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\View\Middleware\ShareErrorsFromSession;
class AdminPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->id('admin')
->path('admin')
->brandName('CESIZen')
->homeUrl('/')
->colors([
'primary' => Color::Amber,
])
->discoverResources(in: app_path('Filament/Admin/Resources'), for: 'App\\Filament\\Admin\\Resources')
->discoverPages(in: app_path('Filament/Admin/Pages'), for: 'App\\Filament\\Admin\\Pages')
->pages([
Pages\Dashboard::class,
])
->discoverWidgets(in: app_path('Filament/Admin/Widgets'), for: 'App\\Filament\\Admin\\Widgets')
->widgets([
Widgets\AccountWidget::class,
Widgets\FilamentInfoWidget::class,
])
->middleware([
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
AuthenticateSession::class,
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
SubstituteBindings::class,
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
])
->authMiddleware([
Authenticate::class,
]);
}
}
+133
View File
@@ -0,0 +1,133 @@
<?php
namespace App\Services;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
/**
* Petit client de l'API Gitea (issues) utilisé par le système de tickets :
* création, consultation, commentaires.
*/
class GiteaIssueService
{
/**
* Prépare un client HTTP authentifié, ou null si Gitea n'est pas configuré.
*/
private function client(): ?PendingRequest
{
$url = rtrim((string) config('services.gitea.url'), '/');
$repo = trim((string) config('services.gitea.repo'), '/');
$token = (string) config('services.gitea.token');
if ($url === '' || $repo === '' || $token === '') {
Log::warning('Gitea non configuré (URL / token / dépôt manquant).');
return null;
}
// Gitea attend l'en-tête "Authorization: token <TOKEN>".
return Http::withToken($token, 'token')
->acceptJson()
->timeout(10)
->baseUrl("{$url}/api/v1/repos/{$repo}");
}
/**
* Crée un ticket (issue). Retourne les données de l'issue créée
* (dont "number") ou null en cas d'échec.
*
* @return array<string, mixed>|null
*/
public function createIssue(string $title, string $body): ?array
{
$client = $this->client();
if (! $client) {
return null;
}
try {
$response = $client->post('/issues', ['title' => $title, 'body' => $body]);
} catch (\Throwable $e) {
Log::error('Appel API Gitea impossible : '.$e->getMessage());
return null;
}
if ($response->failed()) {
Log::error('Création du ticket Gitea échouée', ['status' => $response->status()]);
return null;
}
return (array) $response->json();
}
/**
* Récupère une issue (état, labels, jalon…).
*
* @return array<string, mixed>|null
*/
public function getIssue(int $number): ?array
{
$client = $this->client();
if (! $client) {
return null;
}
try {
$response = $client->get("/issues/{$number}");
} catch (\Throwable $e) {
Log::error('Lecture du ticket Gitea impossible : '.$e->getMessage());
return null;
}
return $response->successful() ? (array) $response->json() : null;
}
/**
* Récupère les commentaires d'une issue (fil de discussion).
*
* @return array<int, mixed>
*/
public function getComments(int $number): array
{
$client = $this->client();
if (! $client) {
return [];
}
try {
$response = $client->get("/issues/{$number}/comments");
} catch (\Throwable $e) {
Log::error('Lecture des commentaires Gitea impossible : '.$e->getMessage());
return [];
}
return $response->successful() ? (array) $response->json() : [];
}
/**
* Ajoute un commentaire (réponse) à une issue.
*/
public function addComment(int $number, string $body): bool
{
$client = $this->client();
if (! $client) {
return false;
}
try {
$response = $client->post("/issues/{$number}/comments", ['body' => $body]);
} catch (\Throwable $e) {
Log::error('Ajout de commentaire Gitea impossible : '.$e->getMessage());
return false;
}
return $response->successful();
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace App\Services;
class StressCalculator
{
public const LEVEL_HIGH = 'Élevé';
public const LEVEL_MODERATE = 'Modéré';
public const LEVEL_LOW = 'Faible';
public function calculateScore(array $points): int
{
return array_sum($points);
}
public function determineLevel(int $score): string
{
if ($score >= 300) {
return self::LEVEL_HIGH;
}
if ($score >= 150) {
return self::LEVEL_MODERATE;
}
return self::LEVEL_LOW;
}
}
+8 -1
View File
@@ -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 {
// //
+2
View File
@@ -1,7 +1,9 @@
<?php <?php
use App\Providers\AppServiceProvider; use App\Providers\AppServiceProvider;
use App\Providers\Filament\AdminPanelProvider;
return [ return [
AppServiceProvider::class, AppServiceProvider::class,
AdminPanelProvider::class,
]; ];
+11 -2
View File
@@ -8,6 +8,7 @@
"require": { "require": {
"php": "^8.2", "php": "^8.2",
"blade-ui-kit/blade-heroicons": "^2.7", "blade-ui-kit/blade-heroicons": "^2.7",
"filament/filament": "^3.2",
"guzzlehttp/guzzle": "^7.10", "guzzlehttp/guzzle": "^7.10",
"laravel/framework": "^12.0", "laravel/framework": "^12.0",
"laravel/sanctum": "^4.0", "laravel/sanctum": "^4.0",
@@ -15,11 +16,13 @@
}, },
"require-dev": { "require-dev": {
"fakerphp/faker": "^1.23", "fakerphp/faker": "^1.23",
"larastan/larastan": "^3.0",
"laravel/pail": "^1.2.2", "laravel/pail": "^1.2.2",
"laravel/pint": "^1.24", "laravel/pint": "^1.24",
"laravel/sail": "^1.41", "laravel/sail": "^1.41",
"mockery/mockery": "^1.6", "mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6", "nunomaduro/collision": "^8.6",
"phpro/grumphp": "^2.5",
"phpunit/phpunit": "^11.5.50" "phpunit/phpunit": "^11.5.50"
}, },
"autoload": { "autoload": {
@@ -53,7 +56,8 @@
], ],
"post-autoload-dump": [ "post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi" "@php artisan package:discover --ansi",
"@php artisan filament:upgrade"
], ],
"post-update-cmd": [ "post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force" "@php artisan vendor:publish --tag=laravel-assets --ansi --force"
@@ -73,15 +77,20 @@
"extra": { "extra": {
"laravel": { "laravel": {
"dont-discover": [] "dont-discover": []
},
"grumphp": {
"disable-plugin": true
} }
}, },
"config": { "config": {
"optimize-autoloader": true, "optimize-autoloader": true,
"preferred-install": "dist", "preferred-install": "dist",
"sort-packages": true, "sort-packages": true,
"platform-check": false,
"allow-plugins": { "allow-plugins": {
"pestphp/pest-plugin": true, "pestphp/pest-plugin": true,
"php-http/discovery": true "php-http/discovery": true,
"phpro/grumphp": true
} }
}, },
"minimum-stability": "stable", "minimum-stability": "stable",
+4198 -355
View File
File diff suppressed because it is too large Load Diff
+29
View File
@@ -0,0 +1,29 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Applications mobiles (téléchargement)
|--------------------------------------------------------------------------
|
| Pour chaque plateforme, la page de téléchargement utilise en priorité
| l'URL fournie (lien de store OU URL directe). À défaut, elle propose le
| fichier précompilé déposé dans public/downloads/. Si aucun des deux n'est
| disponible, le bouton affiche « bientôt disponible ».
|
*/
'android' => [
'version' => env('MOBILE_ANDROID_VERSION', '1.0.0'),
'url' => env('MOBILE_ANDROID_URL'),
'file' => 'downloads/cesizen.apk',
],
'ios' => [
'version' => env('MOBILE_IOS_VERSION', '1.0.0'),
'url' => env('MOBILE_IOS_URL'),
'file' => 'downloads/cesizen.ipa',
],
];
+6 -3
View File
@@ -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,
], ],
]; ];
+7
View File
@@ -35,4 +35,11 @@ return [
], ],
], ],
// Gitea : création de tickets (issues) depuis le formulaire de suggestions.
'gitea' => [
'url' => env('GITEA_URL', 'https://gitea.sam-coffre.duckdns.org'),
'token' => env('GITEA_TOKEN'),
'repo' => env('GITEA_REPO', 'sam/cesizen'),
],
]; ];
+2
View File
@@ -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,6 +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' => 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');
}
}; };
@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('personal_access_tokens', function (Blueprint $table) {
$table->id();
$table->morphs('tokenable');
$table->string('name');
$table->string('token', 64)->unique();
$table->text('abilities')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamp('expires_at')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('personal_access_tokens');
}
};
@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('cache', function (Blueprint $table) {
$table->string('key')->primary();
$table->mediumText('value');
$table->bigInteger('expiration')->index();
});
Schema::create('cache_locks', function (Blueprint $table) {
$table->string('key')->primary();
$table->string('owner');
$table->bigInteger('expiration')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('cache');
Schema::dropIfExists('cache_locks');
}
};
@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');
$table->integer('last_activity')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('sessions');
}
};
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
//
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
//
});
}
};
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->boolean('is_active')->default(true)->after('id_role');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('is_active');
});
}
};
@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('information', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content');
$table->string('category')->default('Général'); // ex: Prévention, Stress, Sommeil
$table->string('image_url')->nullable();
$table->boolean('is_published')->default(true);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('information');
}
};
@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* Table de liaison entre un utilisateur CESIZen et le ticket (issue) qu'il a
* ouvert dans Gitea. Elle permet de retrouver « ses » tickets alors que
* l'application les crée toutes sous un même compte de service.
*/
return new class extends Migration
{
public function up(): void
{
Schema::create('tickets', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->unsignedInteger('gitea_number'); // numéro de l'issue Gitea
$table->string('title');
$table->string('type');
$table->timestamps();
$table->index(['user_id', 'gitea_number']);
});
}
public function down(): void
{
Schema::dropIfExists('tickets');
}
};
+9 -7
View File
@@ -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;
@@ -26,11 +26,13 @@ class DatabaseSeeder extends Seeder
$adminRole = Role::where('libelle', 'Admin')->first(); $adminRole = Role::where('libelle', 'Admin')->first();
User::create([ User::firstOrCreate(
'name' => 'AdminCESI', ['email' => 'admin@cesizen.fr'],
'email' => 'admin@cesizen.fr', [
'password' => Hash::make('okokokok'), // Toujours hacher ! 'name' => 'AdminCESI',
'id_role' => $adminRole->id, 'password' => Hash::make('okokokok'),
]); 'id_role' => $adminRole->id,
]
);
} }
} }
@@ -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 -2
View File
@@ -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 -2
View File
@@ -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);
} }
} }
} }
+43
View File
@@ -0,0 +1,43 @@
# Serveur SonarQube (auto-hébergé) pour l'analyse de qualité du code.
#
# Démarrage : docker compose -f docker-compose.sonar.yml up -d
# Interface : http://localhost:9000 (identifiants par défaut : admin / admin)
# Arrêt : docker compose -f docker-compose.sonar.yml down
#
# ⚠️ Prérequis hôte (Elasticsearch embarqué dans SonarQube) :
# Linux : sudo sysctl -w vm.max_map_count=262144
# (à rendre permanent dans /etc/sysctl.conf)
# Prévoir ~2-3 Go de RAM disponibles pour le conteneur.
services:
sonarqube:
image: sonarqube:community
container_name: sonarqube
depends_on:
- sonar-db
ports:
- "9000:9000"
environment:
SONAR_JDBC_URL: jdbc:postgresql://sonar-db:5432/sonar
SONAR_JDBC_USERNAME: sonar
SONAR_JDBC_PASSWORD: sonar
volumes:
- sonarqube_data:/opt/sonarqube/data
- sonarqube_extensions:/opt/sonarqube/extensions
- sonarqube_logs:/opt/sonarqube/logs
sonar-db:
image: postgres:15
container_name: sonar-db
environment:
POSTGRES_USER: sonar
POSTGRES_PASSWORD: sonar
POSTGRES_DB: sonar
volumes:
- sonar_postgresql:/var/lib/postgresql/data
volumes:
sonarqube_data:
sonarqube_extensions:
sonarqube_logs:
sonar_postgresql:
+36
View File
@@ -0,0 +1,36 @@
services:
# PHP Application
app:
build:
context: .
dockerfile: Dockerfile
container_name: cesizen-app
restart: unless-stopped
tty: true
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- ./:/var/www
- /var/www/node_modules
- ./storage:/var/www/storage
networks:
- cesizen-network
# Nginx Service
webserver:
image: nginx:alpine
container_name: cesizen-webserver
restart: unless-stopped
tty: true
ports:
- "8090:80" # WEB
- "8091:81" # API
volumes:
- ./:/var/www
- ./nginx.conf:/etc/nginx/conf.d/default.conf
networks:
- cesizen-network
networks:
cesizen-network:
driver: bridge
+28
View File
@@ -0,0 +1,28 @@
# Configuration GrumPHP : vérifications automatiques de qualité du code.
# Lancement manuel : vendor/bin/grumphp run
#
# NB : dans ce dépôt (monorepo), l'installation automatique du hook Git est
# désactivée (extra.grumphp.disable-plugin dans composer.json) pour ne pas
# interférer avec le dépôt racine. Les mêmes contrôles sont exécutés en CI.
grumphp:
# On laisse le temps aux analyses (Larastan peut être un peu long).
process_timeout: 120
# On n'arrête pas au premier échec : on veut voir TOUS les problèmes
# (Pint ET PHPStan) en une seule fois.
stop_on_failure: false
tasks:
# 1) Style de code (Laravel Pint) — mêmes règles qu'en CI.
shell:
scripts:
- ['-c', 'vendor/bin/pint --test']
# 2) Analyse statique (Larastan / PHPStan) — détecte les vrais bugs.
phpstan:
configuration: phpstan.neon
# false => on analyse les chemins définis dans phpstan.neon (app/)
# plutôt que seulement les fichiers modifiés : analyse plus fiable.
use_grumphp_paths: false
# Larastan charge tout le framework : 128M par défaut ne suffit pas.
memory_limit: '512M'
+51
View File
@@ -0,0 +1,51 @@
# Configuration pour le Web (Port 80 interne, mappé sur 8090)
server {
listen 80;
server_name _; # Accepte tout car NPM a déjà filtré le domaine
index index.php index.html;
root /var/www/public;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass app:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
# Optimisation pour le proxy
fastcgi_param HTTP_X_FORWARDED_FOR $http_x_forwarded_for;
fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto;
}
}
# Configuration pour l'API (Port 81 interne, mappé sur 8091)
server {
listen 81;
server_name _; # Accepte tout car NPM a déjà filtré le domaine
index index.php index.html;
root /var/www/public;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass app:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
# Optimisation pour le proxy
fastcgi_param HTTP_X_FORWARDED_FOR $http_x_forwarded_for;
fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto;
}
}

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