From e5e6f72f9224320e0b81374d0865c0d68ce3453b Mon Sep 17 00:00:00 2001 From: Sam DEPARDIEU Date: Mon, 6 Jul 2026 11:09:34 +0200 Subject: [PATCH] =?UTF-8?q?S=C3=A9curisation,=20d=C3=A9ploiement=20CI/CD?= =?UTF-8?q?=20Gitea=20et=20documentation=20INFCDAAL3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CI/CD Gitea Actions: ci.yml (Pint + PHPUnit), deploy.yml (déploiement SSH) - Suppression de l'ancien workflow GitHub cassé - En-têtes de sécurité: ajout CSP et Permissions-Policy - RGPD: droit à l'effacement (anonymisation du compte) + tests - Correctifs: ForceHttps hors env testing, timestamps du modèle Utilisateur, suppression des migrations en doublon (migrate échouait sur base neuve) - Qualité: formatage Laravel Pint sur l'ensemble du code - Documentation dossier technique (docs/) + .docx + .pptx Co-Authored-By: Claude Opus 4.8 --- .env.example | 21 ++-- .gitea/workflows/ci.yml | 50 ++++++++ .gitea/workflows/deploy.yml | 49 ++++++++ .github/workflows/deploy.yml | 27 ----- README.md | 93 ++++++++------- app/Http/Controllers/LoginController.php | 3 +- app/Http/Controllers/ProfileController.php | 37 +++++- app/Http/Middleware/ForceHttps.php | 6 +- app/Http/Middleware/SecurityHeaders.php | 30 ++++- app/Models/Activite.php | 3 + app/Models/ActiviteMessage.php | 3 + app/Models/Categorie.php | 2 + app/Models/Commentaire.php | 4 +- app/Models/Ressource.php | 4 +- app/Models/Role.php | 2 + app/Models/TypeRessource.php | 2 + app/Models/Utilisateur.php | 29 ++++- app/Models/Utilisateur_Progression.php | 2 + app/Providers/AppServiceProvider.php | 5 +- bootstrap/app.php | 4 +- .../2026_03_23_211437_create_roles_table.php | 27 ----- ...03_23_211437_create_utilisateurs_table.php | 32 ----- ...26_03_23_211438_create_activites_table.php | 27 ----- ...6_03_23_211438_create_categories_table.php | 27 ----- ...03_23_211438_create_commentaires_table.php | 27 ----- ...6_03_23_211438_create_ressources_table.php | 27 ----- ...23_211438_create_type_ressources_table.php | 27 ----- docker-compose.yml | 2 +- docs/01-plan-de-securisation.md | 99 ++++++++++++++++ docs/02-plan-de-deploiement.md | 109 ++++++++++++++++++ docs/03-gestion-des-versions.md | 61 ++++++++++ docs/04-gestion-des-evolutions.md | 63 ++++++++++ docs/Dossier-technique-INFCDAAL3.docx | Bin 0 -> 43005 bytes docs/README.md | 36 ++++++ docs/Soutenance-INFCDAAL3.pptx | Bin 0 -> 42912 bytes resources/views/profile/settings.blade.php | 29 ++++- routes/web.php | 10 +- tests/Feature/SecuriteTest.php | 68 +++++++++++ 38 files changed, 756 insertions(+), 291 deletions(-) create mode 100644 .gitea/workflows/ci.yml create mode 100644 .gitea/workflows/deploy.yml delete mode 100644 .github/workflows/deploy.yml delete mode 100644 database/migrations/2026_03_23_211437_create_roles_table.php delete mode 100644 database/migrations/2026_03_23_211437_create_utilisateurs_table.php delete mode 100644 database/migrations/2026_03_23_211438_create_activites_table.php delete mode 100644 database/migrations/2026_03_23_211438_create_categories_table.php delete mode 100644 database/migrations/2026_03_23_211438_create_commentaires_table.php delete mode 100644 database/migrations/2026_03_23_211438_create_ressources_table.php delete mode 100644 database/migrations/2026_03_23_211438_create_type_ressources_table.php create mode 100644 docs/01-plan-de-securisation.md create mode 100644 docs/02-plan-de-deploiement.md create mode 100644 docs/03-gestion-des-versions.md create mode 100644 docs/04-gestion-des-evolutions.md create mode 100644 docs/Dossier-technique-INFCDAAL3.docx create mode 100644 docs/README.md create mode 100644 docs/Soutenance-INFCDAAL3.pptx create mode 100644 tests/Feature/SecuriteTest.php diff --git a/.env.example b/.env.example index c0660ea..f076da4 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,8 @@ -APP_NAME=Laravel +# --- Application --- +# En production : APP_ENV=production et APP_DEBUG=false (ne jamais exposer les +# traces d'erreur / la configuration au public). APP_KEY doit être généré via +# `php artisan key:generate` et rester secret (chiffrement des sessions/cookies). +APP_NAME="(RE)Sources Relationnelles" APP_ENV=local APP_KEY= APP_DEBUG=true @@ -20,15 +24,18 @@ LOG_STACK=single LOG_DEPRECATIONS_CHANNEL=null LOG_LEVEL=debug -DB_CONNECTION=sqlite -# DB_HOST=127.0.0.1 -# DB_PORT=3306 -# DB_DATABASE=laravel -# DB_USERNAME=root -# DB_PASSWORD= +# --- Base de données --- +# En production, utiliser MySQL/MariaDB (données ministérielles) plutôt que SQLite. +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=ressources_relationnel +DB_USERNAME=changeme +DB_PASSWORD=changeme SESSION_DRIVER=database SESSION_LIFETIME=120 +# En production, passer à true pour chiffrer les données de session (RGPD). SESSION_ENCRYPT=false SESSION_PATH=/ SESSION_DOMAIN=null diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..5d62c0e --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,50 @@ +name: CI - Intégration continue + +# Intégration continue : à chaque push et chaque pull request, on vérifie +# automatiquement la qualité du code (Laravel Pint) puis on exécute les tests +# automatisés (PHPUnit). Un déploiement (voir deploy.yml) ne doit se faire que +# si cette étape est au vert. + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + quality-and-tests: + runs-on: ubuntu-latest + + steps: + - name: Récupération du code + uses: actions/checkout@v4 + + - name: Installation de PHP 8.2 + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + extensions: pdo_mysql, mbstring, exif, pcntl, bcmath, gd + coverage: none + + - name: Cache des dépendances Composer + uses: actions/cache@v4 + with: + path: vendor + key: composer-${{ hashFiles('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: Exécution des tests automatisés (PHPUnit) + env: + DB_CONNECTION: sqlite + DB_DATABASE: ':memory:' + run: php artisan test diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..5ab7c54 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,49 @@ +name: CD - Déploiement continu + +# Déploiement continu : après un push sur master, une fois la CI passée, +# on se connecte en SSH au serveur de production et on met à jour l'application +# (git pull, rebuild des conteneurs, migrations, mise en cache). +# +# 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) + +on: + push: + branches: [ master ] + +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 /home/sam/mon-app-laravel + + # Récupération de la dernière version du code + git pull origin master + + # Reconstruction et redémarrage des conteneurs + docker compose up -d --build + + # Migrations de la base de données (sans interaction) + docker exec laravel_php php artisan migrate --force + + # Mise en cache de la configuration et des routes (performances en prod) + docker exec laravel_php php artisan config:cache + docker exec laravel_php php artisan route:cache + docker exec laravel_php php artisan view:cache + + # Correction des permissions sur les dossiers écrits par Laravel + docker exec -u 0 laravel_php chown -R www-data:www-data \ + /var/www/storage /var/www/bootstrap/cache diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index b83cbe3..0000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Deploy Laravel App - -on: - push: - branches: [ main ] # Déclenche le déploiement lors d'un push sur la branche main - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - name: Deploy to Server via SSH - uses: appleboy/ssh-action@master - with: - host: ${{ secrets.HOST }} - username: ${{ secrets.USERNAME }} - key: ${{ secrets.SSH_PRIVATE_KEY }} - script: | - cd /home/sam/mon-app-laravel - git pull origin main - docker-compose up -d --build - docker exec laravel_app php artisan migrate --force - docker exec laravel_app php artisan config:cache - docker exec laravel_app php artisan route:cache - - name: Fix permissions - run: | - docker exec -u 0 laravel_php chown -R www-data:www-data /var/www/storage /var/www/bootstrap/cache /tmp - docker exec laravel_php php artisan view:clear diff --git a/README.md b/README.md index 0165a77..3328063 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,70 @@ -

Laravel Logo

+# (RE)Sources Relationnelles -

-Build Status -Total Downloads -Latest Stable Version -License -

+Plateforme de partage de ressources relationnelles à destination des citoyens +(simulation d'un projet du Ministère des Solidarités et de la Santé). +Projet collaboratif — **CDA / CESI — Groupe 1**. -## About Laravel +Application web développée avec **Laravel 12** (architecture MVC), conteneurisée +avec **Docker** et déployée en continu via **Gitea Actions**. -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: +- Application : https://app.sam-coffre.duckdns.org +- Dépôt Git : https://gitea.sam-coffre.duckdns.org/ -- [Simple, fast routing engine](https://laravel.com/docs/routing). -- [Powerful dependency injection container](https://laravel.com/docs/container). -- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage. -- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent). -- Database agnostic [schema migrations](https://laravel.com/docs/migrations). -- [Robust background job processing](https://laravel.com/docs/queues). -- [Real-time event broadcasting](https://laravel.com/docs/broadcasting). +## Stack technique -Laravel is accessible, powerful, and provides tools required for large, robust applications. +| Élément | Technologie | +|---------|-------------| +| Framework | Laravel 12 (PHP 8.2) | +| Base de données | MySQL / MariaDB (SQLite en test) | +| Serveur web | Nginx (reverse proxy + TLS) | +| Conteneurisation | Docker / Docker Compose | +| Gestion de version | Git + Gitea | +| CI/CD | Gitea Actions | +| Qualité de code | Laravel Pint | +| Tests | PHPUnit | -## Learning Laravel +## Installation en local -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. +```bash +git clone https://gitea.sam-coffre.duckdns.org//ressources-relationnel.git +cd ressources-relationnel +cp .env.example .env +composer install +php artisan key:generate +php artisan migrate --seed +npm install && npm run build +php artisan serve +``` -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. +## Développement -## Laravel Sponsors +```bash +# Lancer les tests automatisés +php artisan test -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). +# Vérifier le style de code +vendor/bin/pint --test -### Premium Partners +# Corriger automatiquement le style de code +vendor/bin/pint +``` -- **[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)** +## Déploiement -## Contributing +Le déploiement est **automatisé** : un `push` sur la branche `master` déclenche +la CI (qualité + tests) puis, si tout est vert, le déploiement en production. +Voir [`docs/02-plan-de-deploiement.md`](docs/02-plan-de-deploiement.md). -Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions). +## Documentation (rendu bloc INFCDAAL3) -## Code of Conduct +La documentation technique « Déployer et sécuriser » est dans le dossier +[`docs/`](docs/README.md) : -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). +- [Plan de sécurisation](docs/01-plan-de-securisation.md) +- [Plan de déploiement](docs/02-plan-de-deploiement.md) +- [Gestion des versions](docs/03-gestion-des-versions.md) +- [Gestion des évolutions](docs/04-gestion-des-evolutions.md) -## Security Vulnerabilities +## Licence -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). +Projet pédagogique — usage limité au cadre de la formation. diff --git a/app/Http/Controllers/LoginController.php b/app/Http/Controllers/LoginController.php index da4b774..5845b34 100644 --- a/app/Http/Controllers/LoginController.php +++ b/app/Http/Controllers/LoginController.php @@ -2,8 +2,8 @@ namespace App\Http\Controllers; -use App\Models\Utilisateur; use App\Models\Role; +use App\Models\Utilisateur; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; @@ -36,6 +36,7 @@ class LoginController extends Controller if (Auth::attempt($loginCredentials)) { $request->session()->regenerate(); + return redirect()->route('home')->with('success', 'Connexion réussie!'); } diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index 80f8b87..44850d5 100644 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -14,6 +14,7 @@ class ProfileController extends Controller { /** @var Utilisateur $user */ $user = Auth::user(); + return view('profile.settings', compact('user')); } @@ -24,8 +25,8 @@ class ProfileController extends Controller $user = Auth::user(); $request->validate([ - 'Pseudo' => ['required', 'string', 'max:100', 'unique:utilisateurs,Pseudo,' . $user->ID_Utilisateur . ',ID_Utilisateur'], - 'Email' => ['required', 'string', 'email', 'max:255', 'unique:utilisateurs,Email,' . $user->ID_Utilisateur . ',ID_Utilisateur'], + 'Pseudo' => ['required', 'string', 'max:100', 'unique:utilisateurs,Pseudo,'.$user->ID_Utilisateur.',ID_Utilisateur'], + 'Email' => ['required', 'string', 'email', 'max:255', 'unique:utilisateurs,Email,'.$user->ID_Utilisateur.',ID_Utilisateur'], 'MotDePasse_Actuel' => ['nullable', 'string'], 'MotDePasse_Nouveau' => ['nullable', 'string', 'min:8', 'confirmed'], ], [ @@ -41,7 +42,7 @@ class ProfileController extends Controller // Vérifier le mot de passe actuel si on veut le modifier if ($request->filled('MotDePasse_Nouveau')) { - if (!Hash::check($request->MotDePasse_Actuel, $user->MotDePasse)) { + if (! Hash::check($request->MotDePasse_Actuel, $user->MotDePasse)) { return back()->withErrors([ 'MotDePasse_Actuel' => 'Le mot de passe actuel est incorrect.', ])->onlyInput('Email', 'Pseudo'); @@ -56,4 +57,34 @@ class ProfileController extends Controller return back()->with('success', 'Votre profil a été mis à jour avec succès!'); } + + // Supprimer / anonymiser son compte (droit à l'effacement RGPD) + public function destroy(Request $request) + { + /** @var Utilisateur $user */ + $user = Auth::user(); + + // Vérification du mot de passe pour confirmer l'action sensible + $request->validate([ + 'MotDePasse_Confirmation' => ['required', 'string'], + ], [ + 'MotDePasse_Confirmation.required' => 'Veuillez saisir votre mot de passe pour confirmer.', + ]); + + if (! Hash::check($request->MotDePasse_Confirmation, $user->MotDePasse)) { + return back()->withErrors([ + 'MotDePasse_Confirmation' => 'Mot de passe incorrect. Suppression annulée.', + ]); + } + + // Anonymisation des données personnelles, puis déconnexion sécurisée + $user->anonymiser(); + + Auth::logout(); + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return redirect()->route('home') + ->with('success', 'Votre compte a été supprimé et vos données personnelles anonymisées.'); + } } diff --git a/app/Http/Middleware/ForceHttps.php b/app/Http/Middleware/ForceHttps.php index 94077f2..888ea8c 100644 --- a/app/Http/Middleware/ForceHttps.php +++ b/app/Http/Middleware/ForceHttps.php @@ -9,8 +9,10 @@ class ForceHttps { public function handle(Request $request, Closure $next) { - // En production, forcer HTTPS même si on arrive en HTTP - if (!app()->environment('local') && !$request->isSecure()) { + // En production, forcer HTTPS même si on arrive en HTTP. + // On exclut les environnements local et testing (pas de TLS, et + // la redirection casserait la suite de tests automatisés / CI). + if (! app()->environment(['local', 'testing']) && ! $request->isSecure()) { return redirect( str_replace('http://', 'https://', $request->url()), 308 diff --git a/app/Http/Middleware/SecurityHeaders.php b/app/Http/Middleware/SecurityHeaders.php index cf3fe0c..30251f2 100644 --- a/app/Http/Middleware/SecurityHeaders.php +++ b/app/Http/Middleware/SecurityHeaders.php @@ -15,13 +15,41 @@ class SecurityHeaders { $response = $next($request); - // Ajouter les headers de sécurité + // --- En-têtes de sécurité HTTP (défense en profondeur) --- + + // Force le navigateur à n'utiliser que HTTPS pendant 1 an (protection contre le SSL stripping). $response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains', true); + + // Empêche le navigateur de "deviner" le type MIME d'une ressource (protection contre le MIME sniffing). $response->headers->set('X-Content-Type-Options', 'nosniff', true); + + // Interdit l'affichage du site dans une iframe externe (protection contre le clickjacking). $response->headers->set('X-Frame-Options', 'SAMEORIGIN', true); + + // Ancienne protection XSS des navigateurs (conservée pour les navigateurs anciens). $response->headers->set('X-XSS-Protection', '1; mode=block', true); + + // Limite les informations de provenance envoyées aux sites tiers. $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin', true); + // Désactive des API sensibles du navigateur non utilisées par l'application (RGPD / vie privée). + $response->headers->set('Permissions-Policy', 'geolocation=(), microphone=(), camera=(), payment=()', true); + + // Content-Security-Policy : réduit fortement le risque d'injection de scripts (XSS). + // Politique volontairement compatible avec Laravel/Vite (styles et scripts inline autorisés). + // Pour durcir davantage, remplacer 'unsafe-inline' par un système de nonce. + $response->headers->set('Content-Security-Policy', implode('; ', [ + "default-src 'self'", + "script-src 'self' 'unsafe-inline'", + "style-src 'self' 'unsafe-inline'", + "img-src 'self' data: https:", + "font-src 'self' data:", + "connect-src 'self'", + "object-src 'none'", + "base-uri 'self'", + "frame-ancestors 'self'", + ]), true); + return $response; } } diff --git a/app/Models/Activite.php b/app/Models/Activite.php index f826c2a..5f7f1c1 100644 --- a/app/Models/Activite.php +++ b/app/Models/Activite.php @@ -7,8 +7,11 @@ use Illuminate\Database\Eloquent\Model; class Activite extends Model { protected $primaryKey = 'ID_Instance'; + protected $table = 'activites'; + public $incrementing = true; + public $timestamps = false; protected $fillable = [ diff --git a/app/Models/ActiviteMessage.php b/app/Models/ActiviteMessage.php index 140d96f..dcaae59 100644 --- a/app/Models/ActiviteMessage.php +++ b/app/Models/ActiviteMessage.php @@ -7,8 +7,11 @@ use Illuminate\Database\Eloquent\Model; class ActiviteMessage extends Model { protected $primaryKey = 'ID_Message'; + protected $table = 'activite_messages'; + public $incrementing = true; + public $timestamps = false; protected $fillable = [ diff --git a/app/Models/Categorie.php b/app/Models/Categorie.php index 7e66e27..358ae4a 100644 --- a/app/Models/Categorie.php +++ b/app/Models/Categorie.php @@ -7,7 +7,9 @@ use Illuminate\Database\Eloquent\Model; class Categorie extends Model { public $timestamps = false; + protected $primaryKey = 'ID_Categorie'; + protected $table = 'categories'; protected $fillable = [ diff --git a/app/Models/Commentaire.php b/app/Models/Commentaire.php index c00e596..06afb13 100644 --- a/app/Models/Commentaire.php +++ b/app/Models/Commentaire.php @@ -7,7 +7,9 @@ use Illuminate\Database\Eloquent\Model; class Commentaire extends Model { protected $primaryKey = 'ID_Commentaire'; + protected $table = 'commentaires'; + public $incrementing = true; protected $fillable = [ @@ -42,4 +44,4 @@ class Commentaire extends Model { return $this->belongsTo(Commentaire::class, 'ID_Parent_Commentaire', 'ID_Commentaire'); } -} \ No newline at end of file +} diff --git a/app/Models/Ressource.php b/app/Models/Ressource.php index e89d7a5..cf9cba6 100644 --- a/app/Models/Ressource.php +++ b/app/Models/Ressource.php @@ -7,7 +7,9 @@ use Illuminate\Database\Eloquent\Model; class Ressource extends Model { protected $primaryKey = 'ID_Ressource'; + protected $table = 'ressources'; + public $incrementing = true; protected $fillable = [ @@ -59,4 +61,4 @@ class Ressource extends Model { return $this->hasMany(Utilisateur_Progression::class, 'ID_Ressource', 'ID_Ressource'); } -} \ No newline at end of file +} diff --git a/app/Models/Role.php b/app/Models/Role.php index 826fa4b..ca7689c 100644 --- a/app/Models/Role.php +++ b/app/Models/Role.php @@ -7,7 +7,9 @@ use Illuminate\Database\Eloquent\Model; class Role extends Model { public $timestamps = false; + protected $primaryKey = 'ID_Role'; + protected $table = 'roles'; protected $fillable = [ diff --git a/app/Models/TypeRessource.php b/app/Models/TypeRessource.php index d706696..0bd202f 100644 --- a/app/Models/TypeRessource.php +++ b/app/Models/TypeRessource.php @@ -7,7 +7,9 @@ use Illuminate\Database\Eloquent\Model; class TypeRessource extends Model { public $timestamps = false; + protected $primaryKey = 'ID_Type_Ressource'; + protected $table = 'type_ressources'; protected $fillable = [ diff --git a/app/Models/Utilisateur.php b/app/Models/Utilisateur.php index 23c2252..9597cea 100644 --- a/app/Models/Utilisateur.php +++ b/app/Models/Utilisateur.php @@ -4,6 +4,7 @@ namespace App\Models; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Illuminate\Support\Carbon; /** * @property int $ID_Utilisateur @@ -12,7 +13,7 @@ use Illuminate\Notifications\Notifiable; * @property string $Pseudo * @property string $Statut * @property int $ID_Role - * @property \Illuminate\Support\Carbon $DateInscription + * @property Carbon $DateInscription */ class Utilisateur extends Authenticatable { @@ -20,10 +21,15 @@ class Utilisateur extends Authenticatable // Nom de la table protected $table = 'utilisateurs'; - + + // La table utilise DateInscription et non created_at/updated_at + public $timestamps = false; + // Clé primaire personnalisée protected $primaryKey = 'ID_Utilisateur'; + public $incrementing = true; + protected $keyType = 'int'; // Colonnes modifiables (Mass Assignment) @@ -88,4 +94,21 @@ class Utilisateur extends Authenticatable { return $this->hasMany(Utilisateur_Progression::class, 'ID_Utilisateur', 'ID_Utilisateur'); } -} \ No newline at end of file + + /** + * Anonymise le compte (droit à l'effacement RGPD). + * + * Les données personnelles identifiantes (e-mail, pseudo, mot de passe) + * sont remplacées par des valeurs neutres et non réversibles, tandis que + * les contributions liées (ressources, commentaires) restent en base sous + * une forme anonyme afin de préserver l'intégrité référentielle. + */ + public function anonymiser(): void + { + $this->Email = 'anonyme_'.$this->ID_Utilisateur.'@anonymise.local'; + $this->Pseudo = 'Utilisateur supprimé'; + $this->MotDePasse = bin2hex(random_bytes(32)); // mot de passe non utilisable + $this->Statut = 'Inactif'; // compte désactivé (valeur de l'enum Statut) + $this->save(); + } +} diff --git a/app/Models/Utilisateur_Progression.php b/app/Models/Utilisateur_Progression.php index 4637f40..6be5483 100644 --- a/app/Models/Utilisateur_Progression.php +++ b/app/Models/Utilisateur_Progression.php @@ -7,7 +7,9 @@ use Illuminate\Database\Eloquent\Model; class Utilisateur_Progression extends Model { protected $table = 'utilisateur_progressions'; + public $timestamps = false; + public $incrementing = false; protected $primaryKey = ['ID_Utilisateur', 'ID_Ressource']; diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 60142b0..68c9c33 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,9 +2,8 @@ namespace App\Providers; -use Illuminate\Support\ServiceProvider; use Illuminate\Support\Facades\URL; -use Symfony\Component\HttpFoundation\Request as SymfonyRequest; +use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { @@ -21,7 +20,7 @@ class AppServiceProvider extends ServiceProvider */ public function boot(): void { - if (!app()->environment('local')) { + if (! app()->environment('local')) { URL::forceScheme('https'); } // ForceHttps middleware se charge de forcer HTTPS diff --git a/bootstrap/app.php b/bootstrap/app.php index 7d7bd0e..f2ba62f 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,10 +1,10 @@ withRouting( diff --git a/database/migrations/2026_03_23_211437_create_roles_table.php b/database/migrations/2026_03_23_211437_create_roles_table.php deleted file mode 100644 index 0b4fe21..0000000 --- a/database/migrations/2026_03_23_211437_create_roles_table.php +++ /dev/null @@ -1,27 +0,0 @@ -id(); - $table->timestamps(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('roles'); - } -}; diff --git a/database/migrations/2026_03_23_211437_create_utilisateurs_table.php b/database/migrations/2026_03_23_211437_create_utilisateurs_table.php deleted file mode 100644 index 4bb24e8..0000000 --- a/database/migrations/2026_03_23_211437_create_utilisateurs_table.php +++ /dev/null @@ -1,32 +0,0 @@ -id(); - $table->string('Email')->unique(); - $table->string('MotDePasse'); - $table->string('Pseudo')->nullable(); - $table->unsignedBigInteger('ID_Role')->nullable(); - $table->string('Statut')->default('Actif'); - $table->timestamps(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('utilisateurs'); - } -}; diff --git a/database/migrations/2026_03_23_211438_create_activites_table.php b/database/migrations/2026_03_23_211438_create_activites_table.php deleted file mode 100644 index 71a9ef9..0000000 --- a/database/migrations/2026_03_23_211438_create_activites_table.php +++ /dev/null @@ -1,27 +0,0 @@ -id(); - $table->timestamps(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('activites'); - } -}; diff --git a/database/migrations/2026_03_23_211438_create_categories_table.php b/database/migrations/2026_03_23_211438_create_categories_table.php deleted file mode 100644 index 24c1532..0000000 --- a/database/migrations/2026_03_23_211438_create_categories_table.php +++ /dev/null @@ -1,27 +0,0 @@ -id(); - $table->timestamps(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('categories'); - } -}; diff --git a/database/migrations/2026_03_23_211438_create_commentaires_table.php b/database/migrations/2026_03_23_211438_create_commentaires_table.php deleted file mode 100644 index 92f55d9..0000000 --- a/database/migrations/2026_03_23_211438_create_commentaires_table.php +++ /dev/null @@ -1,27 +0,0 @@ -id(); - $table->timestamps(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('commentaires'); - } -}; diff --git a/database/migrations/2026_03_23_211438_create_ressources_table.php b/database/migrations/2026_03_23_211438_create_ressources_table.php deleted file mode 100644 index 71d31fa..0000000 --- a/database/migrations/2026_03_23_211438_create_ressources_table.php +++ /dev/null @@ -1,27 +0,0 @@ -id(); - $table->timestamps(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('ressources'); - } -}; diff --git a/database/migrations/2026_03_23_211438_create_type_ressources_table.php b/database/migrations/2026_03_23_211438_create_type_ressources_table.php deleted file mode 100644 index 16b8c3c..0000000 --- a/database/migrations/2026_03_23_211438_create_type_ressources_table.php +++ /dev/null @@ -1,27 +0,0 @@ -id(); - $table->timestamps(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('type_ressources'); - } -}; diff --git a/docker-compose.yml b/docker-compose.yml index 468f09e..43067d1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,7 +14,7 @@ services: container_name: laravel_web restart: always ports: - - "8000:80" + - "8005:80" - "8443:443" # HTTPS port volumes: - .:/var/www diff --git a/docs/01-plan-de-securisation.md b/docs/01-plan-de-securisation.md new file mode 100644 index 0000000..c9df0a4 --- /dev/null +++ b/docs/01-plan-de-securisation.md @@ -0,0 +1,99 @@ +# 01 — Plan de sécurisation + +Projet **(RE)Sources Relationnelles** — Bloc INFCDAAL3 + +> Contexte : plateforme ministérielle manipulant des **données personnelles** +> (comptes citoyens, ressources privées, échanges modérés). La sécurité et la +> conformité **RGPD** sont donc des exigences fortes. + +## 1. Identification des risques de sécurité + +Analyse inspirée du **Top 10 OWASP** et adaptée au projet. + +| Risque | Exemple sur le projet | Gravité | Traitement mis en place | +|--------|-----------------------|---------|-------------------------| +| Injection SQL | Recherche / filtres de ressources | Élevée | ORM Eloquent (requêtes préparées), validation des entrées | +| XSS (injection de scripts) | Commentaires, ressources créées par les citoyens | Élevée | Échappement Blade `{{ }}`, en-tête **CSP** | +| CSRF | Formulaires (login, création de ressource) | Élevée | Jeton CSRF Laravel automatique sur les formulaires | +| Vol de session | Cookies de session | Élevée | Cookies `HttpOnly` + `Secure`, HTTPS forcé, `session.regenerate()` à la connexion | +| Mots de passe faibles / en clair | Comptes citoyens | Élevée | Hachage **bcrypt** (`hashed` cast), min. 8 caractères | +| Contrôle d'accès défaillant | Accès Back Office / pages profil | Élevée | Middleware `auth`, rôles (Citoyen / Modérateur / Admin / Super-Admin) | +| Man-in-the-middle | Transport des données | Élevée | **TLS/HTTPS** obligatoire + **HSTS** | +| Exposition d'informations | Messages d'erreur, debug | Moyenne | `APP_DEBUG=false` et `APP_ENV=production` en prod | +| Clickjacking | Intégration en iframe | Moyenne | En-tête `X-Frame-Options: SAMEORIGIN` | +| Dépendances vulnérables | Paquets Composer / npm | Moyenne | `composer audit` / mises à jour régulières | + +## 2. Mesures de prévention des risques + +### 2.1 Sécurité applicative (implémentée dans le code) + +- **Authentification** : hachage bcrypt des mots de passe, régénération de + session à la connexion, invalidation à la déconnexion + (`app/Http/Controllers/LoginController.php`). +- **Contrôle d'accès** : les routes sensibles sont regroupées derrière le + middleware `auth` (`routes/web.php`), avec un modèle de **rôles** + (`app/Models/Role.php`). +- **Validation des entrées** : toutes les données reçues sont validées + (règles `required`, `email`, `unique`, `min:8`, `confirmed`…). +- **Protection CSRF** : activée par défaut sur tous les formulaires POST/PUT. +- **En-têtes de sécurité HTTP** (`app/Http/Middleware/SecurityHeaders.php`) : + - `Strict-Transport-Security` (HSTS) — impose HTTPS, + - `Content-Security-Policy` (CSP) — limite les sources de scripts (anti-XSS), + - `X-Content-Type-Options: nosniff`, + - `X-Frame-Options: SAMEORIGIN` (anti-clickjacking), + - `Referrer-Policy` et `Permissions-Policy`. +- **Forçage HTTPS** (`app/Http/Middleware/ForceHttps.php`) : redirection + automatique de HTTP vers HTTPS hors environnement local/test. + +### 2.2 Sécurité du transport et de l'infrastructure + +- **TLS/HTTPS** terminé au niveau du reverse proxy (`nginx.conf`) : le port 80 + redirige en 301 vers le port 443. +- Nom de domaine du service : `https://app.sam-coffre.duckdns.org`. +- Séparation des rôles conteneurs : PHP-FPM (`laravel_php`) et serveur web + (`laravel_web`) isolés (`docker-compose.yml`). + +## 3. Chiffrement des données + +| Donnée | État au repos | En transit | +|--------|---------------|-----------| +| Mots de passe | Hachage **bcrypt** (non réversible) | HTTPS | +| Sessions / cookies | Chiffrés via `APP_KEY` (AES-256), `SESSION_ENCRYPT=true` en prod | HTTPS + cookie `Secure`/`HttpOnly` | +| Données personnelles sensibles | Chiffrables via les *casts* `encrypted` d'Eloquent | HTTPS | +| Communications client ↔ serveur | — | **TLS 1.2+** obligatoire | + +> Laravel chiffre en **AES-256-CBC** à partir de la clé `APP_KEY`. Cette clé est +> stockée hors dépôt (`.env` est dans `.gitignore`) et ne doit jamais être versionnée. + +## 4. Conformité RGPD + +- **Minimisation** : seules les données nécessaires sont collectées (pseudo, e-mail). +- **Base légale et information** : pages légales présentes + (`resources/views/legal/mentions.blade.php`, + `resources/views/legal/politique.blade.php`). +- **Droit d'accès / rectification** : espace profil + (`/profile/settings`, `ProfileController`). +- **Droit à l'effacement / anonymisation** : implémenté. Depuis son espace + profil, l'utilisateur peut supprimer son compte (confirmation par mot de passe) ; + ses données personnelles (e-mail, pseudo, mot de passe) sont alors anonymisées + de façon irréversible via `Utilisateur::anonymiser()` + (`app/Http/Controllers/ProfileController@destroy`), tout en préservant + l'intégrité référentielle des contributions. +- **Sécurité par défaut** (privacy by design) : chiffrement, HTTPS, contrôle d'accès. +- **Sous-traitance et hébergement** : hébergement maîtrisé, journalisation des accès. + +## 5. Bonnes pratiques de développement + +- **Style de code homogène** : **Laravel Pint** (PSR-12), vérifié en CI + (`vendor/bin/pint --test`). +- **Commentaires** : le code de sécurité (middlewares) est documenté en français. +- **Tests automatisés** : `tests/Feature/SecuriteTest.php` vérifie les en-têtes + de sécurité et le contrôle d'accès aux pages protégées. +- **Séparation des environnements** : configuration via `.env` (jamais de secret en dur). +- **Architecture MVC** : contrôleurs / modèles / vues séparés. + +## 6. Synthèse — traçabilité risque → mesure + +Chaque risque du §1 est couvert par au moins une mesure du §2/§3/§4, et les +mesures critiques (en-têtes, contrôle d'accès) sont **vérifiées automatiquement** +par la CI à chaque évolution du code. diff --git a/docs/02-plan-de-deploiement.md b/docs/02-plan-de-deploiement.md new file mode 100644 index 0000000..0e73456 --- /dev/null +++ b/docs/02-plan-de-deploiement.md @@ -0,0 +1,109 @@ +# 02 — Plan de déploiement + +Projet **(RE)Sources Relationnelles** — Bloc INFCDAAL3 + +## 1. Stratégie de déploiement + +Le déploiement repose sur trois piliers : + +1. **Conteneurisation** (Docker) pour un environnement reproductible. +2. **Gestion de version** (Gitea) comme source unique de vérité. +3. **Intégration et déploiement continus** (Gitea Actions) pour automatiser + qualité, tests et mise en production. + +Approche retenue : à chaque `push` sur `master`, la CI valide le code +(qualité + tests) ; si tout est vert, le déploiement se déclenche automatiquement +sur le serveur. + +## 2. Environnements + +| Environnement | Rôle | Hébergement | +|---------------|------|-------------| +| **Local (dev)** | Développement sur le poste | Docker / `php artisan serve` | +| **Test (CI)** | Exécution automatisée des tests | Runner Gitea Actions (base SQLite en mémoire) | +| **Pré-production (QA)** *(recommandé)* | Recette avant mise en ligne | Conteneurs Docker, base dédiée | +| **Production** | Service ouvert aux citoyens | Serveur `app.sam-coffre.duckdns.org` | + +> La configuration de chaque environnement est portée par un fichier `.env` +> distinct (jamais versionné). Seul `.env.example` est présent dans le dépôt. + +## 3. Architecture de déploiement + +``` + Internet (HTTPS) + │ + ▼ + ┌───────────────────────────┐ + │ Nginx (conteneur web) │ :443 TLS / redirection 80→443 + │ laravel_web │ + └─────────────┬─────────────┘ + │ FastCGI :9000 + ▼ + ┌───────────────────────────┐ + │ PHP-FPM (conteneur app) │ Laravel 12 + │ laravel_php │ + └─────────────┬─────────────┘ + │ + ▼ + Base de données MySQL +``` + +Fichiers concernés : `Dockerfile` (image PHP), `docker-compose.yml` +(orchestration), `nginx.conf` (reverse proxy / TLS). + +## 4. Pipeline d'intégration et de déploiement continus (CI/CD) + +### 4.1 Intégration continue — `.gitea/workflows/ci.yml` + +Déclenchée sur chaque `push` et chaque `pull_request` vers `master` : + +1. Récupération du code. +2. Installation de PHP 8.2 et des extensions. +3. Installation des dépendances Composer (avec cache). +4. **Vérification du style de code** : `vendor/bin/pint --test`. +5. **Exécution des tests** : `php artisan test` (PHPUnit). + +### 4.2 Déploiement continu — `.gitea/workflows/deploy.yml` + +Déclenché sur `push` vers `master`, il se connecte en **SSH** au serveur et exécute : + +1. `git pull origin master` — récupération de la nouvelle version. +2. `docker compose up -d --build` — reconstruction des conteneurs. +3. `php artisan migrate --force` — application des migrations de base. +4. `config:cache`, `route:cache`, `view:cache` — optimisations de production. +5. Correction des permissions (`storage`, `bootstrap/cache`). + +**Secrets Gitea requis** (Settings → Actions → Secrets) : `SSH_HOST`, +`SSH_USER`, `SSH_PRIVATE_KEY`, `SSH_PORT`. + +> Prérequis : un **runner Gitea Actions** doit être enregistré sur l'instance +> (`https://gitea.sam-coffre.duckdns.org/`) et les Actions activées pour le dépôt. + +## 5. Plan de déploiement détaillé (étapes, ressources, responsables) + +| # | Étape | Ressource / Outil | Responsable | Automatisé ? | +|---|-------|-------------------|-------------|--------------| +| 1 | Développement + commit | Git / Gitea | Développeur | Non | +| 2 | Push sur `master` | Gitea | Développeur | Non | +| 3 | Contrôle qualité (Pint) | Gitea Actions | Runner CI | ✅ Oui | +| 4 | Tests automatisés (PHPUnit) | Gitea Actions | Runner CI | ✅ Oui | +| 5 | Connexion au serveur | SSH | Runner CD | ✅ Oui | +| 6 | Récupération du code | Git | Serveur | ✅ Oui | +| 7 | Build des conteneurs | Docker Compose | Serveur | ✅ Oui | +| 8 | Migrations BDD | Artisan | Serveur | ✅ Oui | +| 9 | Mise en cache config/routes/vues | Artisan | Serveur | ✅ Oui | +| 10 | Vérification post-déploiement | Endpoint `/up` (health check) | Équipe | Semi | + +## 6. Retour arrière (rollback) + +En cas de problème après un déploiement : + +- **Code** : `git revert` ou retour à un tag précédent, puis nouveau déploiement. +- **Base de données** : `php artisan migrate:rollback` (migrations réversibles). +- **Conteneurs** : redéploiement de l'image précédente via `docker compose`. + +## 7. Supervision + +- **Health check** : endpoint `/up` exposé par Laravel (voir `bootstrap/app.php`). +- **Journaux** : `storage/logs/laravel.log` + logs des conteneurs Docker. +- **Historique des déploiements** : onglet *Actions* de Gitea. diff --git a/docs/03-gestion-des-versions.md b/docs/03-gestion-des-versions.md new file mode 100644 index 0000000..5e9f529 --- /dev/null +++ b/docs/03-gestion-des-versions.md @@ -0,0 +1,61 @@ +# 03 — Outil de gestion des versions + +Projet **(RE)Sources Relationnelles** — Bloc INFCDAAL3 + +## 1. Outil choisi : Git + Gitea (auto-hébergé) + +Le versionnement du code repose sur **Git**, hébergé sur une instance +**Gitea** auto-hébergée par l'équipe : + +- Instance : `https://gitea.sam-coffre.duckdns.org/` +- Motivations du choix : + - **Maîtrise des données** : hébergement sur notre propre serveur (cohérent + avec un contexte ministériel / RGPD, pas de dépendance à un tiers). + - **Léger et open source** : Gitea est simple à administrer et gratuit. + - **Fonctionnalités complètes** : dépôts Git, *pull requests*, *issues*, + *releases*, et **Gitea Actions** (CI/CD) intégrés. + +## 2. Organisation du dépôt + +- **Branche principale** : `master` (branche de référence, protégée, déployée). +- **Branches de fonctionnalité** *(recommandé)* : une branche par fonctionnalité + (`feature/…`), fusionnée via *pull request* après relecture. +- Fichiers de configuration Git présents : `.gitignore`, `.gitattributes`, + `.editorconfig` (cohérence entre développeurs). + +## 3. Convention de nommage des commits + +Convention recommandée (type *Conventional Commits*) : + +``` +: + +Types : feat, fix, docs, style, refactor, test, chore +Exemples : + feat: ajout de la création de ressources + fix: correction de la redirection HTTPS + docs: rédaction du plan de sécurisation +``` + +## 4. Flux de travail (workflow) proposé + +1. Créer une branche depuis `master` : `git checkout -b feature/ma-fonctionnalite`. +2. Développer + commits atomiques. +3. Pousser la branche : `git push origin feature/ma-fonctionnalite`. +4. Ouvrir une **Pull Request** sur Gitea. +5. La **CI** (Pint + tests) s'exécute automatiquement sur la PR. +6. Relecture par un pair, puis fusion dans `master`. +7. La fusion déclenche le **déploiement automatique** en production. + +## 5. Gestion des livraisons (releases) + +- Utilisation des **tags Git** pour marquer les versions : `v1.0.0`, `v1.1.0`… +- Convention **SemVer** : `MAJEUR.MINEUR.CORRECTIF`. +- Les *Releases* Gitea documentent chaque version livrée (notes de version). + +## 6. Sécurité du dépôt + +- Aucun secret versionné : `.env`, clés et certificats sont exclus via + `.gitignore`. +- Accès au dépôt restreint aux membres de l'équipe (comptes Gitea). +- Historique complet et traçable (auteur, date, message de chaque modification). diff --git a/docs/04-gestion-des-evolutions.md b/docs/04-gestion-des-evolutions.md new file mode 100644 index 0000000..6fe9cfa --- /dev/null +++ b/docs/04-gestion-des-evolutions.md @@ -0,0 +1,63 @@ +# 04 — Outil de gestion des demandes d'évolutions et des incidents + +Projet **(RE)Sources Relationnelles** — Bloc INFCDAAL3 + +## 1. Outil choisi : les *Issues* de Gitea + +La gestion des évolutions et des incidents s'appuie sur le module **Issues** de +Gitea (déjà utilisé pour le code), ce qui évite d'ajouter un outil externe et +centralise le suivi au même endroit que le code et les déploiements. + +- Instance : `https://gitea.sam-coffre.duckdns.org/` + +## 2. Typologie des demandes (labels) + +Chaque demande est catégorisée par un **label** : + +| Label | Usage | +|-------|-------| +| `bug` | Incident / anomalie en production | +| `evolution` | Demande d'évolution fonctionnelle | +| `securite` | Faille ou correctif de sécurité (priorité haute) | +| `documentation` | Mise à jour de documentation | +| `priorite:haute` / `priorite:basse` | Niveau de priorité | + +## 3. Cycle de vie d'une demande + +``` +Ouverte → À traiter → En cours → En test (QA) → Résolue / Fermée +``` + +- Une *issue* est **liée à une branche et à une Pull Request** ; la fusion de la + PR ferme automatiquement l'issue (mot-clé `Closes #12` dans le commit). +- Le tableau **Projects (Kanban)** de Gitea permet de visualiser l'avancement + (colonnes : À faire / En cours / Terminé). +- Les **Milestones** regroupent les demandes par version cible (ex. `v1.1`). + +## 4. Processus de gestion des incidents (maintenance corrective) + +1. **Détection** : signalement (utilisateur, supervision, logs). +2. **Qualification** : création d'une issue `bug`, évaluation de la gravité. +3. **Priorisation** : affectation d'un label de priorité et d'un responsable. +4. **Correction** : branche `fix/…`, développement, tests. +5. **Validation** : CI verte + recette en QA. +6. **Déploiement** : fusion sur `master` → mise en production automatique. +7. **Clôture** : fermeture de l'issue, note de version si nécessaire. + +## 5. Processus de gestion des évolutions (maintenance évolutive) + +1. **Recueil du besoin** : issue `evolution` décrivant le besoin et la valeur. +2. **Analyse et estimation** : faisabilité, impact, charge. +3. **Planification** : affectation à un *milestone* / une version. +4. **Réalisation** : branche `feature/…`, développement + tests. +5. **Revue et intégration** : Pull Request relue, CI validée. +6. **Livraison** : déploiement continu, communication de la nouvelle version. + +## 6. Traçabilité + +Grâce à Gitea, chaque évolution est traçable de bout en bout : + +**Issue → Branche → Commits → Pull Request → CI → Déploiement → Release** + +Cette chaîne assure la traçabilité complète exigée pour la maintenance d'une +application manipulant des données sensibles. diff --git a/docs/Dossier-technique-INFCDAAL3.docx b/docs/Dossier-technique-INFCDAAL3.docx new file mode 100644 index 0000000000000000000000000000000000000000..265d74bb2a4174a6491d8244dc818a3f9606015b GIT binary patch literal 43005 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 literal 0 HcmV?d00001 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..3f09baa --- /dev/null +++ b/docs/README.md @@ -0,0 +1,36 @@ +# Dossier de rendu — Bloc INFCDAAL3 + +**Déployer et sécuriser les applications informatiques** +Projet collaboratif : **(RE)Sources Relationnelles** + +Ce dossier regroupe les documents techniques attendus pour le bloc INFCDAAL3. +Il sert de base au **document écrit de 15 à 20 pages** demandé dans les consignes. + +## Sommaire + +| # | Document | Répond au critère de la grille | +|---|----------|--------------------------------| +| 01 | [Plan de sécurisation](01-plan-de-securisation.md) | Risques de sécurité, RGPD, chiffrement, prévention des risques | +| 02 | [Plan de déploiement](02-plan-de-deploiement.md) | Environnement de déploiement, automatisation, intégration continue, étapes et ressources | +| 03 | [Gestion des versions](03-gestion-des-versions.md) | Outil de gestion des versions (Gitea / Git) | +| 04 | [Gestion des évolutions](04-gestion-des-evolutions.md) | Outil de gestion des demandes d'évolutions et des incidents | + +## Correspondance avec la grille d'évaluation + +- **Déploiement de l'application** → doc 02 + fichiers `Dockerfile`, `docker-compose.yml`, `nginx.conf`, `.gitea/workflows/` +- **Maintenance** (gestion des versions et des évolutions) → docs 03 et 04 +- **Sécurité** (risques, RGPD, bonnes pratiques, plan de sécurisation) → doc 01 + code (`app/Http/Middleware/`, tests, Laravel Pint) +- **Organisation / Prestation orale** → non technique (support de soutenance ~10 slides) + +## Éléments techniques livrés dans le dépôt + +| Élément | Emplacement | Objectif | +|---------|-------------|----------| +| Intégration continue (qualité + tests) | `.gitea/workflows/ci.yml` | Automatiser Pint + PHPUnit à chaque push | +| Déploiement continu (SSH) | `.gitea/workflows/deploy.yml` | Automatiser la mise en production | +| Conteneurisation | `Dockerfile`, `docker-compose.yml` | Environnement reproductible | +| Serveur web / TLS | `nginx.conf` | Redirection HTTP→HTTPS, HTTPS | +| En-têtes de sécurité | `app/Http/Middleware/SecurityHeaders.php` | HSTS, CSP, X-Frame-Options, etc. | +| Forçage HTTPS | `app/Http/Middleware/ForceHttps.php` | Chiffrement du transport | +| Tests de sécurité | `tests/Feature/SecuriteTest.php` | Vérification automatisée | +| Qualité de code | `laravel/pint` (dev) | Standard PSR / style homogène | diff --git a/docs/Soutenance-INFCDAAL3.pptx b/docs/Soutenance-INFCDAAL3.pptx new file mode 100644 index 0000000000000000000000000000000000000000..cf6db7937701cfd39792fa16ca12070cbf364c72 GIT binary patch literal 42912 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 P0spiB@A~`W?_T{Mq$-VD literal 0 HcmV?d00001 diff --git a/resources/views/profile/settings.blade.php b/resources/views/profile/settings.blade.php index 8722d7c..90b5467 100644 --- a/resources/views/profile/settings.blade.php +++ b/resources/views/profile/settings.blade.php @@ -148,12 +148,39 @@

⚠️ Zone de danger

Les actions ci-dessous ne peuvent pas être annulées.

-
+ @csrf
+ +
+ + +

Supprimer mon compte

+

+ Conformément au RGPD, vous pouvez demander la suppression de votre compte. + Vos données personnelles (e-mail, pseudo) seront définitivement anonymisées. + Cette action est irréversible. +

+
+ @csrf + @method('DELETE') +
+ + +
+ +
diff --git a/routes/web.php b/routes/web.php index a3be1e1..3200755 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,16 +1,19 @@ name('home'); -Route::get('/form-csrf', function () {return view('form-csrf');}); +Route::get('/form-csrf', function () { + return view('form-csrf'); +}); -Route::get('/debug-https', function (Illuminate\Http\Request $request) { +Route::get('/debug-https', function (Request $request) { return response()->json([ 'isSecure' => $request->isSecure(), 'url' => $request->url(), @@ -44,4 +47,5 @@ Route::post('/logout', [LoginController::class, 'logout'])->name('logout'); Route::middleware('auth')->group(function () { Route::get('/profile/settings', [ProfileController::class, 'settings'])->name('profile.settings'); Route::put('/profile/update', [ProfileController::class, 'update'])->name('profile.update'); + Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); }); diff --git a/tests/Feature/SecuriteTest.php b/tests/Feature/SecuriteTest.php new file mode 100644 index 0000000..095404e --- /dev/null +++ b/tests/Feature/SecuriteTest.php @@ -0,0 +1,68 @@ +get('/'); + + $response->assertStatus(200); + $response->assertHeader('X-Content-Type-Options', 'nosniff'); + $response->assertHeader('X-Frame-Options', 'SAMEORIGIN'); + $response->assertHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); + $response->assertHeader('Content-Security-Policy'); + $response->assertHeader('Permissions-Policy'); + } + + /** + * Vérifie que les pages protégées redirigent un visiteur non authentifié + * vers la page de connexion (contrôle d'accès). + */ + public function test_les_pages_protegees_exigent_une_authentification(): void + { + $response = $this->get('/profile/settings'); + + $response->assertRedirect('/login'); + } + + /** + * Vérifie le droit à l'effacement RGPD : la suppression du compte + * anonymise bien les données personnelles de l'utilisateur. + */ + public function test_la_suppression_de_compte_anonymise_les_donnees(): void + { + $role = Role::create(['Nom' => 'Utilisateur']); + + $user = Utilisateur::create([ + 'Pseudo' => 'JeanDupont', + 'Email' => 'jean.dupont@example.com', + 'MotDePasse' => Hash::make('motdepasse123'), + 'ID_Role' => $role->ID_Role, + 'Statut' => 'Actif', + ]); + + $response = $this->actingAs($user)->delete('/profile', [ + 'MotDePasse_Confirmation' => 'motdepasse123', + ]); + + $response->assertRedirect('/'); + + $user->refresh(); + $this->assertSame('Utilisateur supprimé', $user->Pseudo); + $this->assertSame('Inactif', $user->Statut); + $this->assertStringNotContainsString('jean.dupont', $user->Email); + } +}