diff --git a/Semaine_16_Exam_01/Jour_01/atelier-web/ENONCES-ATELIER-WEB.md b/Semaine_16_Exam_01/Jour_01/atelier-web/ENONCES-ATELIER-WEB.md new file mode 100644 index 0000000..11dc414 --- /dev/null +++ b/Semaine_16_Exam_01/Jour_01/atelier-web/ENONCES-ATELIER-WEB.md @@ -0,0 +1,509 @@ +# Atelier — Développer une application web sécurisée en Python + +**Fil rouge : Bloc&Co, salle d'escalade.** +La salle veut gérer ses adhérents en ligne : inscription, connexion, espace +personnel, et un accès réservé au personnel. + +À la fin de l'atelier, vous saurez construire **seul** un module de gestion +d'utilisateurs sécurisé : c'est la brique de base de presque toutes les +applications web, et elle se transpose à n'importe quel domaine métier. + +--- + +## Ce qui est fourni + +| Fichier | Contenu | +|---|---| +| `starter/app.py` | La plomberie de la base, et les fonctions à compléter | +| `starter/schema.sql` | La table `adherents`. Ne pas la modifier. | +| `starter/templates/` | Tout le HTML, déjà écrit | +| `starter/static/style.css` | La feuille de style | +| `test_app.py` | Les tests. **Un exercice est réussi quand ses tests passent.** | + +## Installation (10 minutes, à faire en premier) + +``` +py -m venv .venv +.venv\Scripts\activate +pip install flask pytest +python app.py +``` + +Sur macOS ou Linux, remplacez les deux premières lignes par : +`python3 -m venv .venv` puis `source .venv/bin/activate` + +Ouvrez http://127.0.0.1:5000 — la page d'accueil doit s'afficher. +Un compte administrateur existe déjà : `admin@blocandco.fr` / `Admin2026!` + +Pour lancer les tests : `pytest -v` +Au départ, la plupart échouent. C'est normal : c'est votre feuille de route. + +--- + +# Partie A — Les bases du web en Python + +## Exercice 0 — Monter un projet de zéro + +**Notions** : environnement virtuel, dépendances, arborescence d'un projet Flask. + +> Le jour de l'épreuve, vous ne saurez qu'en lisant le sujet si un dépôt vous +> est fourni ou si vous partez de rien. **Entraînez-vous aux deux.** + +**A. Page blanche.** Sans rien copier, dans un dossier neuf, créez un projet +Flask qui démarre : environnement virtuel, installation, `app.py`, une route +qui affiche un texte. Chronométrez-vous. + +``` +Objectif : moins de 10 minutes, sans consulter de documentation. +Recommencez demain dans un autre dossier, sans regarder vos notes. +``` + +**B. Projet existant.** Ouvrez le dossier `starter/` et, **avant de coder +quoi que ce soit**, remplissez ce tableau : + +| Question | Votre réponse | +|---|---| +| Combien de routes sont déclarées ? | | +| Où le mot de passe est-il haché ? | | +| Quelles fonctions sont vides ? | | +| Que fait `@app.teardown_appcontext` ? | | +| L'application démarre-t-elle en l'état ? | | + +**Pourquoi cet exercice** : reprendre du code écrit par quelqu'un d'autre est +la situation normale en entreprise, et la moitié des candidats perdent une +heure parce qu'ils ont commencé à taper avant d'avoir lu. + +La fiche `MEMO-JOUR-J.md` récapitule les deux démarches. Gardez-la ouverte +pendant l'épreuve. + +--- + +## Exercice 1 — Votre première page + +**Notions** : route, fonction de vue, serveur de développement. + +Créez un fichier `essai.py` séparé contenant une application Flask minimale +avec une route `/bonjour` qui renvoie le texte `Bonjour Bloc&Co`. +Lancez-la, ouvrez-la dans le navigateur. + +Ajoutez ensuite une route `/bonjour/` qui affiche `Bonjour `. + +``` +http://127.0.0.1:5000/bonjour → Bonjour Bloc&Co +http://127.0.0.1:5000/bonjour/Lea → Bonjour Lea +``` + +**Erreurs attendues** : oublier `app.run()` · deux routes avec le même nom de +fonction → `AssertionError` · modifier le code sans recharger (pensez à +`debug=True`). + +--- + +## Exercice 2 — Séparer le HTML du Python + +**Notions** : templates Jinja, `render_template`, héritage de gabarit. + +Ouvrez `templates/base.html` et `templates/accueil.html`. Répondez par écrit, +en une phrase chacune : + +1. Que fait `{% extends "base.html" %}` ? +2. Où atterrit le contenu de `{% block contenu %}` ? +3. Pourquoi écrit-on `{{ url_for('profil') }}` plutôt que `/profil` ? +4. Dans `base.html`, pourquoi le lien « Administration » n'apparaît-il pas + pour tout le monde ? + +**Pourquoi c'est important** : le jury vous demandera de justifier votre +architecture. « J'ai séparé la présentation de la logique » est une réponse, +encore faut-il pouvoir la montrer dans le code. + +--- + +## Exercice 3 — Un formulaire qui parle au serveur + +**Notions** : méthodes GET et POST, `request.form`. + +Dans `essai.py`, créez une route `/echo` qui accepte GET et POST : + +- en GET, elle affiche un formulaire avec un champ `message` +- en POST, elle réaffiche la page avec le message reçu + +``` +Saisie : "salut" +Affiché : Vous avez écrit : salut +``` + +Puis essayez de saisir `salut`. Que s'affiche-t-il à l'écran ? +Gardez la réponse de côté : on y revient à l'exercice 12. + +**Erreurs attendues** : oublier `methods=["GET", "POST"]` → erreur 405 · +`request.form["message"]` sur un champ absent → `KeyError` (préférez +`request.form.get("message", "")`). + +--- + +# Partie B — Enregistrer les données + +## Exercice 4 — Parler à la base + +**Notions** : SQLite, `SELECT`, `INSERT`, requêtes paramétrées. + +Dans un fichier d'essai, ouvrez `blocandco.db` et : + +1. affichez tous les adhérents +2. affichez uniquement ceux dont le rôle est `admin` +3. comptez le nombre total d'adhérents +4. insérez un adhérent de test, puis supprimez-le + +**Règle non négociable, à appliquer dès maintenant :** une valeur variable ne +s'écrit **jamais** dans le texte de la requête. + +```python +# NON — faille d'injection SQL +db.execute("SELECT * FROM adherents WHERE email = '" + email + "'") + +# OUI — requête paramétrée +db.execute("SELECT * FROM adherents WHERE email = ?", (email,)) +``` + +**Erreurs attendues** : oublier la virgule dans `(email,)` → le tuple n'en est +pas un · oublier `commit()` après un `INSERT` → rien n'est enregistré · +`fetchone()` renvoie `None` quand il n'y a pas de résultat, et le code plante +trois lignes plus loin. + +--- + +## Exercice 5 — L'inscription + +**Notions** : validation des entrées, hachage de mot de passe, contrainte +d'unicité. + +Complétez `valider_inscription()` puis la route `/inscription`. + +**Règles de validation** : + +| Champ | Règle | +|---|---| +| nom, prénom | non vides | +| email | contient `@` et un point après le `@` | +| mot de passe | 12 caractères minimum, une majuscule, un chiffre | +| confirmation | identique au mot de passe | +| email | pas déjà présent en base | + +**Le mot de passe ne doit jamais arriver en clair dans la base.** Utilisez +`generate_password_hash()` de Werkzeug. + +``` +Entrée : Léa / Dupont / lea@test.fr / "Court1A" +Sortie : « Le mot de passe doit faire au moins 12 caractères. » + +Entrée : Léa / Dupont / lea@test.fr / "Grimpe2026ABC" +Sortie : « Compte créé. Vous pouvez vous connecter. » + En base : $scrypt$... (et non "Grimpe2026ABC") +``` + +**Vérification** : `pytest -k inscription` et `pytest -k mot_de_passe` + +**Erreurs attendues** : enregistrer le mot de passe en clair · hacher la +confirmation au lieu du mot de passe · renvoyer le mot de passe dans le +formulaire réaffiché · comparer les e-mails sans normaliser la casse, si bien +que `Lea@Test.fr` et `lea@test.fr` créent deux comptes. + +--- + +# Partie C — Authentification et autorisations + +## Exercice 6 — La connexion + +**Notions** : session, vérification d'empreinte. + +Complétez `utilisateur_courant()` puis la route `/connexion`. + +Le principe : on ne déchiffre **jamais** un mot de passe. On rehache celui qui +est saisi et on compare les deux empreintes, avec `check_password_hash()`. + +**Point de sécurité, et il compte** : que l'adresse existe ou non, que le mot +de passe soit faux ou non, affichez **exactement le même message**. Sinon un +attaquant découvre quelles adresses sont inscrites chez vous — c'est une +énumération de comptes. + +``` +Entrée : lea@test.fr / mauvais → « Identifiants incorrects. » +Entrée : inconnu@test.fr / n'importe → « Identifiants incorrects. » +``` + +**Vérification** : `pytest -k connexion` + +**Erreurs attendues** : messages différents selon le cas · stocker tout +l'utilisateur dans la session au lieu de son seul identifiant · oublier +`session.clear()` avant de connecter quelqu'un (fixation de session) · oublier +`app.secret_key` → Flask refuse d'écrire dans la session. + +--- + +## Exercice 7 — Déconnexion et pages protégées + +**Notions** : décorateur, redirection. + +1. Complétez `/deconnexion` : elle vide la session. +2. Complétez le décorateur `login_requis`. + +Un décorateur est une fonction qui en enveloppe une autre pour ajouter un +contrôle avant son exécution. Ici : « si personne n'est connecté, redirige +vers la connexion ; sinon, laisse passer ». + +``` +Visiteur anonyme sur /profil → redirigé vers /connexion + « Connectez-vous pour accéder à cette page. » +``` + +**Vérification** : `pytest -k protege` et `pytest -k deconnexion` + +**Erreurs attendues** : oublier `@wraps(vue)` → Flask voit deux routes nommées +`wrapper` et refuse de démarrer · placer `@login_requis` **au-dessus** de +`@app.route` (l'ordre compte : la route en premier) · oublier de renvoyer le +résultat de `vue(...)`. + +--- + +## Exercice 8 — L'espace personnel + +**Notions** : `UPDATE`, revalidation côté serveur. + +Complétez `/profil` : afficher les informations, et permettre de modifier nom, +prénom et niveau. + +**Le piège, et c'est le plus important de l'atelier** : le `` · oublier le `WHERE` dans +l'`UPDATE` → tous les adhérents sont modifiés d'un coup · laisser l'utilisateur +changer son propre rôle par ce formulaire. + +--- + +## Exercice 9 — Les rôles + +**Notions** : contrôle d'autorisation, séparation authentification / +autorisation. + +Complétez le décorateur `admin_requis`, la route `/admin` et `/admin/promouvoir`. + +Retenez la distinction, le jury la pose souvent : + +- **authentification** : qui êtes-vous ? (connexion) +- **autorisation** : avez-vous le droit de faire ceci ? (rôle) + +Un utilisateur connecté n'est pas un utilisateur autorisé. + +``` +Visiteur anonyme sur /admin → « Connectez-vous… » +Adhérent connecté sur /admin → « Accès réservé aux administrateurs. » +Administrateur sur /admin → la liste des adhérents +``` + +**Vérification** : `pytest -k admin` et `pytest -k promotion` + +**Erreurs attendues** : masquer le lien dans le menu **sans** protéger la route +(l'URL reste accessible en la tapant) · vérifier le rôle avec `=` au lieu de +`==` · protéger l'affichage mais pas l'action `/admin/promouvoir` — c'est la +faille la plus fréquente et la plus grave. + +--- + +## Exercice 10 — La statistique d'accueil + +**Notions** : `COUNT`, passage de variable au template. + +Affichez sur la page d'accueil le nombre réel d'adhérents inscrits. + +``` +Base vide + compte admin → 1 adhérent inscrit +Après une inscription → 2 adhérents inscrits +``` + +**Vérification** : `pytest -k nombre` + +**Erreurs attendues** : compter en Python avec `len(fetchall())` au lieu d'un +`COUNT(*)` en SQL — correct ici, inacceptable sur un million de lignes · +oublier d'accorder le pluriel. + +--- + +# Partie D — La sécurité, ce que le jury regarde vraiment + +> Les quatre exercices suivants se font **sur votre propre application**. +> C'est légal, c'est le but, et c'est exactement ce qu'on vous demandera de +> démontrer. + +## Exercice 11 — Injection SQL + +**Notions** : OWASP A03, requêtes paramétrées. + +1. Dans une copie de votre code, remplacez volontairement la requête de + connexion par une concaténation : + + ```python + db.execute("SELECT * FROM adherents WHERE email = '" + email + "'") + ``` + +2. Connectez-vous avec l'adresse `lea@test.fr' --` et n'importe quel mot de + passe. Observez. +3. Essayez `' OR '1'='1`. Observez. +4. Revenez à la version paramétrée. Rejouez les deux charges. +5. **Expliquez par écrit, en trois phrases, pourquoi le `?` règle le problème.** + +**Vérification** : `pytest -k injection` + +La réponse attendue : avec `?`, la requête et les données voyagent séparément. +Le moteur a déjà analysé la structure de la requête quand il reçoit la valeur ; +celle-ci ne peut donc plus être lue comme du code SQL. + +--- + +## Exercice 12 — Injection de script (XSS) + +**Notions** : OWASP A03, échappement automatique. + +1. Modifiez votre prénom en `` puis affichez + votre profil. Que se passe-t-il ? Regardez le code source de la page. +2. Dans `profil.html`, remplacez `{{ utilisateur.prenom }}` par + `{{ utilisateur.prenom|safe }}`. Rechargez. +3. Expliquez ce que fait Jinja par défaut, et ce que `|safe` désactive. +4. **Remettez le code dans son état initial.** + +**Vérification** : `pytest -k xss` + +**À retenir** : Jinja échappe automatiquement. Ce filtre `|safe`, vous le +croiserez dans du vrai code — c'est souvent là qu'est la faille. + +--- + +## Exercice 13 — Contrôle d'accès défaillant + +**Notions** : OWASP A01, le risque numéro 1 du classement. + +Trois attaques à mener contre votre propre application : + +1. **Accès direct par l'URL** — déconnectez-vous, tapez `/admin` à la main. + Êtes-vous bloqué ? +2. **Élévation de privilège** — connectez-vous comme simple adhérent, puis + envoyez une requête POST vers `/admin/promouvoir/1`. Votre rôle change-t-il ? + (Le test `test_promotion_interdite_a_un_adherent` fait exactement cela.) +3. **Accès aux données d'autrui** — si vous aviez écrit `/profil/` au lieu + de `/profil`, que se passerait-il en changeant le numéro dans l'URL ? + Expliquez pourquoi la version sans identifiant est plus sûre. + +**Règle** : cacher un bouton n'est pas une mesure de sécurité. Seul le contrôle +exécuté sur le serveur en est une. + +--- + +## Exercice 14 — Les quatre critères, appliqués à votre code + +**Notions** : disponibilité, intégrité, confidentialité, traçabilité. + +Pour chacun des quatre critères, citez **une ligne précise de votre code** qui +y contribue, ou constatez qu'il manque et dites ce que vous ajouteriez. + +| Critère | Dans votre application | +|---|---| +| Confidentialité | ? | +| Intégrité | ? | +| Disponibilité | ? | +| Traçabilité | ? | + +Indice pour le dernier : votre application n'enregistre aujourd'hui **aucune** +trace des connexions. Proposez une table `journal` et dites ce que vous y +mettriez — et surtout ce que vous n'y mettriez **jamais**. + +--- + +# Partie E — L'épreuve + +## Exercice 15 — Projet de synthèse, en autonomie (4 heures) + +**Nouveau cahier des charges, à traiter seul.** + +> **Rouletabosse**, association de covoiturage domicile-travail, veut une +> application web. Elle doit permettre à un salarié de créer un compte +> (nom, prénom, e-mail, mot de passe, entreprise), de se connecter, de +> consulter et modifier son profil. Deux rôles : `membre` et `referent`. +> Seul un référent peut voir la liste des membres et valider un compte. +> La page d'accueil affiche le nombre de membres inscrits. +> Front : HTML / CSS. Back : Python. Mots de passe hachés, sessions +> sécurisées. + +Vous partez d'une page blanche. Vous avez quatre heures. + +**Livrable** : l'application qui tourne en local, plus la présentation de +l'exercice 16. + +**Conseil de méthode** : passez les 20 premières minutes à écrire le schéma de +la base et la liste de vos routes, **avant** la première ligne de code. Les +candidats qui codent immédiatement sont ceux qui ne finissent pas. + +--- + +## Exercice 16 — La présentation au jury + +Une application qui marche mais qu'on ne sait pas présenter est une +application à moitié rendue. Préparez un support de 12 à 15 diapositives : + +1. **Introduction** — le besoin métier, en deux phrases +2. **Schéma de la base de données** — les tables, leurs champs, leurs liens +3. **Modélisation de l'application** — les routes, et le parcours d'un + utilisateur de l'inscription au profil +4. **Choix technologiques** — et leur **justification**. « Flask parce que + c'est léger et que le besoin ne demande pas d'ORM » est une justification. + « Flask parce qu'on l'a vu en cours » n'en est pas une. +5. **Sécurité** — hachage, sessions, requêtes paramétrées, contrôle des rôles, + validation des entrées. Une diapositive par mesure, avec l'extrait de code. +6. **Tests** — votre stratégie, le nombre de tests, une capture de `pytest` +7. **Démonstration** — captures d'écran du parcours complet +8. **Conclusion** — ce qui manque, et ce que vous feriez avec deux jours de plus + +**Préparez une réponse à ces trois questions**, elles tombent presque toujours : + +- Pourquoi hacher plutôt que chiffrer un mot de passe ? +- Que se passe-t-il si je tape l'URL d'administration sans être connecté ? +- Comment savez-vous que votre application fonctionne ? + +--- + +# Grille d'évaluation + +| Compétence du référentiel | Vérifiée par | +|---|---| +| **C15** — procédures d'authentification et d'autorisation intégrées | Exercices 6, 7, 9 | +| **C15** — activités autorisées et interdites définies | Exercices 9, 13 | +| **C15** — gestion sécurisée des données sur tout leur cycle de vie | Exercices 5, 14 | +| **C16** — langages web et logiciel mobilisés (Python, JS…) | Exercices 1 à 10 | +| **C16** — standards de développement respectés | `ruff check .` sur le rendu | +| **C16** — les 4 critères de sécurité respectés | Exercice 14 | +| **C17** — risques identifiés, robustesse aux attaques | Exercices 11, 12, 13 | +| **C17** — composants externes connus et éprouvés | Exercice 16, diapositive 4 | + +| Barème indicatif | Points | +|---|---| +| Partie A — bases web (ex. 1-3) | 2 | +| Partie B — données et inscription (ex. 4-5) | 4 | +| Partie C — authentification et rôles (ex. 6-10) | 6 | +| Partie D — sécurité (ex. 11-14) | 4 | +| Partie E — présentation (ex. 16) | 4 | + +**Retraits transversaux** : mot de passe en clair en base (**−5, éliminatoire**) +· requête SQL construite par concaténation (−3) · route protégée uniquement par +le masquage d'un lien (−3) · `except:` nu (−1) · `app.secret_key` codée en dur +dans un rendu présenté comme « prêt pour la production » (−1). diff --git a/Semaine_16_Exam_01/Jour_01/atelier-web/FICHE-INSTALLER-PYTHON.md b/Semaine_16_Exam_01/Jour_01/atelier-web/FICHE-INSTALLER-PYTHON.md new file mode 100644 index 0000000..dc4194d --- /dev/null +++ b/Semaine_16_Exam_01/Jour_01/atelier-web/FICHE-INSTALLER-PYTHON.md @@ -0,0 +1,213 @@ +# Fiche — installer Python et Flask + +À garder de côté. Vous n'en avez besoin que si `verif_poste.py` affiche une +ligne `[ KO ]`, ou si votre terminal ne reconnaît pas la commande `python`. + +**Durée : 10 minutes.** Suivez les étapes dans l'ordre, sans en sauter. + +--- + +# Windows + +## Étape 1 — Vérifier ce que vous avez déjà + +Ouvrez un terminal : touche **Windows**, tapez `cmd`, **Entrée**. + +Tapez : + +``` +py --list +``` + +| Ce que vous voyez | Ce que ça veut dire | +|---|---| +| Une liste avec `-V:3.9` ou mieux | **Python est déjà installé.** Passez directement à l'étape 4. | +| `'py' n'est pas reconnu…` | Python n'est pas installé, ou mal installé. Étape 2. | +| Uniquement des versions `3.8` ou moins | Trop ancien pour Flask 3. Étape 2. | + +--- + +## Étape 2 — Télécharger + +Allez sur **https://www.python.org/downloads/** + +Le gros bouton jaune propose la dernière version pour votre système. +Cliquez, puis lancez le fichier téléchargé. + +> N'installez pas Python depuis le Microsoft Store. Il fonctionne, mais il +> isole les fichiers d'une façon qui pose des problèmes avec les +> environnements virtuels — exactement ce qu'on va utiliser. + +--- + +## Étape 3 — Installer : l'écran qui compte + +Le premier écran de l'installateur contient **une case à cocher en bas** : + +``` + ☑ Add python.exe to PATH ← COCHEZ-LA +``` + +**C'est l'étape la plus importante de toute la fiche.** Si vous l'oubliez, +Windows ne saura pas où trouver Python et toutes les commandes échoueront. + +Cochez la case, puis cliquez sur **Install Now**. + +Si vous avez déjà cliqué sans cocher : relancez l'installateur, choisissez +**Modify**, puis **Repair** — ou désinstallez et recommencez. + +À la fin, s'il propose **« Disable path length limit »**, acceptez. + +--- + +## Étape 4 — Vérifier + +**Fermez complètement votre terminal et rouvrez-en un neuf.** Le PATH n'est lu +qu'à l'ouverture : sans ça, vous testerez l'ancienne configuration. + +``` +py --version +``` + +Vous devez voir un numéro de version, par exemple `Python 3.13.2`. + +--- + +## Étape 5 — Installer Flask + +``` +py -m pip install flask pytest +``` + +Puis relancez la vérification : + +``` +py verif_poste.py +``` + +Vous devez obtenir « Tout est prêt. Rien à faire. » + +--- + +# macOS + +## Étape 1 — Vérifier + +Ouvrez **Terminal** (Spotlight : `Cmd + Espace`, tapez `terminal`). + +``` +python3 --version +``` + +Si vous obtenez 3.9 ou mieux, passez à l'étape 3. + +## Étape 2 — Installer + +Téléchargez l'installateur macOS sur **https://www.python.org/downloads/** +et suivez-le. Aucune case particulière à cocher : sur macOS le PATH est +configuré automatiquement. + +## Étape 3 — Installer Flask + +``` +python3 -m pip install flask pytest +``` + +Sur macOS et Linux, remplacez partout `py` par `python3` dans les commandes +de l'atelier. + +--- + +# Dépannage + +## « 'python' n'est pas reconnu en tant que commande interne » + +La case **Add python.exe to PATH** n'a pas été cochée. + +Solution immédiate : utilisez `py` à la place de `python`. Le lanceur `py` +est installé séparément et fonctionne presque toujours. + +Solution durable : relancez l'installateur → **Modify** → **Repair**. + +--- + +## Taper `python` ouvre le Microsoft Store + +Windows intercepte la commande avec un raccourci publicitaire. + +**Paramètres** → **Applications** → **Paramètres avancés des applications** +→ **Alias d'exécution d'application** → désactivez les deux lignes +`python.exe` et `python3.exe`. + +En attendant, utilisez `py` : il n'est pas concerné. + +--- + +## J'ai plusieurs versions de Python et je ne sais pas laquelle tourne + +``` +py --list +``` + +Vous choisissez explicitement laquelle utiliser : + +``` +py -3.13 verif_poste.py # remplacez 3.13 par VOTRE version +py -3.13 -m pip install flask +``` + +Le numéro après `-` est la version. Utilisez **toujours le même** pendant +tout l'atelier, sinon vous installerez Flask dans une version et lancerez le +code avec une autre — c'est la cause numéro un des `ModuleNotFoundError`. + +--- + +## `pip` échoue avec une erreur de certificat ou de proxy + +Vous êtes probablement sur un réseau d'entreprise ou d'école qui filtre. + +Essayez d'abord : + +``` +py -m pip install --user flask pytest +``` + +Si cela échoue encore, utilisez votre connexion personnelle (partage de +connexion du téléphone) le temps de l'installation. Une fois Flask installé, +le réseau n'a plus d'importance. + +--- + +## `ModuleNotFoundError: No module named 'flask'` + +Flask est installé, mais pas pour l'interpréteur qui exécute votre code. + +Vérifiez avec quel Python vous travaillez : + +``` +py verif_poste.py +``` + +La ligne **Executable** vous donne le chemin exact. Installez Flask pour +celui-là précisément : + +``` +py -m pip install flask +``` + +Si vous utilisez un environnement virtuel, vérifiez qu'il est **activé** : +votre ligne de commande doit commencer par `(.venv)`. + +--- + +## Rien ne fonctionne et le cours a commencé + +**Ne restez pas bloqué seul.** Dans le chat du cours, envoyez : + +1. Le résultat complet de `py verif_poste.py` +2. La commande que vous avez tapée +3. Le message d'erreur **en entier** + +En attendant la réponse, suivez sur l'écran partagé et binômez avec un +camarade. Vous rattraperez le code à la pause : ce qui compte maintenant, +c'est de ne pas perdre le fil du raisonnement. diff --git a/Semaine_16_Exam_01/Jour_01/atelier-web/MEMO-JOUR-J.md b/Semaine_16_Exam_01/Jour_01/atelier-web/MEMO-JOUR-J.md new file mode 100644 index 0000000..403dfe7 --- /dev/null +++ b/Semaine_16_Exam_01/Jour_01/atelier-web/MEMO-JOUR-J.md @@ -0,0 +1,139 @@ +# Mémo — les 30 premières minutes de l'épreuve + +> À garder sous les yeux pendant l'examen. +> Vous avez 4 heures. Les 30 premières décident des 3 h 30 suivantes. + +Deux situations possibles. Lisez le sujet : on vous donne un dépôt, ou pas. + +--- + +## Situation A — on vous fournit un dépôt + +**Ne codez rien avant d'avoir fait ces six étapes.** + +1. **Clonez**, puis **lisez le README en entier.** Cinq minutes de lecture + valent une heure de tâtonnement. Il dit comment lancer le projet. + +2. **Cherchez le fichier de dépendances** : `requirements.txt`, + `pyproject.toml` ou `Pipfile`. Il vous dit quel framework est imposé. + +3. **Installez et lancez l'application telle quelle**, avant toute + modification. Si elle ne démarre pas, c'est votre premier problème à + résoudre — et vous devez le savoir maintenant, pas à 15 h. + +4. **Cartographiez le projet en cinq minutes**, par écrit : + + | Question | Réponse | + |---|---| + | Où sont déclarées les routes ? | | + | Où sont les templates ? | | + | Y a-t-il déjà une base de données ? un schéma ? | | + | Qu'est-ce qui marche déjà ? | | + | Qu'est-ce qui est vide ou marqué « à faire » ? | | + +5. **Repérez les conventions existantes** : langue des noms de variables, + structure des fonctions, façon d'accéder à la base. Votre code doit + ressembler à celui qui est déjà là. Un jury le remarque. + +6. **Faites un premier commit** avant de commencer. Vous aurez un point de + retour si vous cassez tout. + +--- + +## Situation B — page blanche + +**L'arborescence à créer, dans cet ordre :** + +``` +monprojet/ + app.py + schema.sql + requirements.txt + templates/ + base.html + static/ + style.css +``` + +**Les commandes (Windows)** : + +``` +mkdir monprojet +cd monprojet +py -m venv .venv +.venv\Scripts\activate +pip install flask +pip freeze > requirements.txt +``` + +Sur macOS ou Linux, remplacez les deux premières lignes par : +`python3 -m venv .venv` puis `source .venv/bin/activate` + +**Le `app.py` minimal, à taper de mémoire :** + +```python +from flask import Flask + +app = Flask(__name__) +app.secret_key = "a-changer" + +@app.route("/") +def accueil(): + return "ça tourne" + +if __name__ == "__main__": + app.run(debug=True) +``` + +Lancez-le. **Vous devez voir « ça tourne » dans le navigateur avant d'écrire +la moindre fonctionnalité.** Tant que ce n'est pas le cas, rien d'autre +n'a d'importance. + +--- + +## Dans les deux cas : l'ordre de construction + +Ne construisez jamais dans un autre ordre. Chaque étape s'appuie sur la +précédente, et chacune doit marcher avant de passer à la suivante. + +| # | Étape | Fini quand… | +|---|---|---| +| 1 | L'application démarre | une page s'affiche | +| 2 | La base et la table existent | vous lisez une ligne en Python | +| 3 | L'inscription | un compte apparaît en base, mot de passe **haché** | +| 4 | La connexion | la session retient qui vous êtes | +| 5 | Le profil | vous voyez et modifiez vos infos | +| 6 | Les rôles | un non-admin est refusé sur la page admin | +| 7 | La statistique | le compteur s'affiche sur l'accueil | +| 8 | **Les captures d'écran** | vous avez de quoi faire la démo | + +**À 14 h, arrêtez de coder.** La présentation vaut autant que le code. Une +application à 80 % bien présentée passe mieux qu'une application à 100 % +sans support. + +--- + +## Les pièges qui coûtent des heures + +| Symptôme | Cause réelle | +|---|---| +| `ModuleNotFoundError: flask` | environnement virtuel non activé — regardez si `(.venv)` est au début de la ligne | +| `Address already in use` | une ancienne instance tourne encore : fermez-la, ou `app.run(port=5001)` | +| Les modifications n'apparaissent pas | `debug=True` oublié, ou page en cache (Ctrl+F5) | +| `jinja2.TemplateNotFound` | le dossier doit s'appeler exactement `templates` | +| `RuntimeError: The session is unavailable` | `app.secret_key` manquante | +| Les accents s'affichent en `?` | `encoding="utf-8"` oublié à la lecture du fichier | +| L'erreur 405 | la route n'accepte pas POST : `methods=["GET", "POST"]` | + +--- + +## Ce qui est éliminatoire + +Quoi qu'il arrive, même à court de temps : + +- **Jamais** de mot de passe en clair dans la base +- **Jamais** de requête SQL construite avec `+` ou une f-string +- **Toujours** le contrôle de rôle sur le serveur, pas seulement un lien caché + +Si vous devez sacrifier une fonctionnalité, sacrifiez-la. Ne sacrifiez +jamais une de ces trois règles. diff --git a/Semaine_16_Exam_01/Jour_01/atelier-web/requirements.txt b/Semaine_16_Exam_01/Jour_01/atelier-web/requirements.txt new file mode 100644 index 0000000..258ff8f --- /dev/null +++ b/Semaine_16_Exam_01/Jour_01/atelier-web/requirements.txt @@ -0,0 +1,2 @@ +Flask>=3.0 +pytest>=8.0 diff --git a/Semaine_16_Exam_01/Jour_01/atelier-web/ruff.toml b/Semaine_16_Exam_01/Jour_01/atelier-web/ruff.toml new file mode 100644 index 0000000..d4ff93a --- /dev/null +++ b/Semaine_16_Exam_01/Jour_01/atelier-web/ruff.toml @@ -0,0 +1,18 @@ +# Règles de style de l'atelier. +# Les étudiants lancent ruff check . avant de rendre. +# Ce fichier garantit que tout le monde obtient le même résultat. + +line-length = 88 + +[lint] +select = ["E", "F", "W", "I", "UP", "B", "RET"] + +ignore = [ + # On enseigne date.today(), les fuseaux horaires ne sont pas au programme. + "DTZ011", +] + +[lint.per-file-ignores] +# Dans le squelette, ces imports sont fournis d'avance aux étudiants : +# ils deviennent utiles au fur et à mesure des exercices. +"starter/app.py" = ["F401", "RET504", "RET501"] diff --git a/Semaine_16_Exam_01/Jour_01/atelier-web/starter/README.md b/Semaine_16_Exam_01/Jour_01/atelier-web/starter/README.md new file mode 100644 index 0000000..50cb126 --- /dev/null +++ b/Semaine_16_Exam_01/Jour_01/atelier-web/starter/README.md @@ -0,0 +1,26 @@ +# Bloc&Co — squelette de l'atelier + +## Démarrer + + py -m venv .venv + .venv\Scripts\activate + pip install -r ..\requirements.txt + python app.py + +Sur macOS ou Linux, remplacez les deux premières lignes par : +`python3 -m venv .venv` puis `source .venv/bin/activate` + +Puis ouvrir http://127.0.0.1:5000 + +## Ce qui est fourni + +- `schema.sql` — la table `adherents`, à ne pas modifier +- `templates/` — tout le HTML, déjà écrit +- `static/style.css` — la feuille de style +- `app.py` — la plomberie de la base de données, et des fonctions à compléter +- `verif_poste.py` — vérifie que votre poste est prêt (à lancer en premier) + +## Ce que vous écrivez + +Tout ce qui est marqué `# À FAIRE`. Suivez les exercices dans l'ordre : +l'application ne fonctionnera entièrement qu'à la fin de l'exercice 10. diff --git a/Semaine_16_Exam_01/Jour_01/atelier-web/starter/app.py b/Semaine_16_Exam_01/Jour_01/atelier-web/starter/app.py new file mode 100644 index 0000000..ac013d1 --- /dev/null +++ b/Semaine_16_Exam_01/Jour_01/atelier-web/starter/app.py @@ -0,0 +1,221 @@ +"""Bloc&Co — gestion des adhérents d'une salle d'escalade. + +Squelette de l'atelier. Complétez tout ce qui est marqué « À FAIRE ». +Lancement : python app.py puis http://127.0.0.1:5000 +""" + +import sqlite3 +from datetime import date +from functools import wraps +from pathlib import Path + +from flask import Flask, flash, g, redirect, render_template, request, session, url_for +from werkzeug.security import check_password_hash, generate_password_hash + +BASE = Path(__file__).parent +DB = BASE / "blocandco.db" + +app = Flask(__name__) +app.secret_key = "cle-de-developpement-a-changer" + +NIVEAUX = ["debutant", "intermediaire", "confirme"] + + +# ========================================================================== +# FOURNI — plomberie de la base de données. Ne rien modifier ici. +# ========================================================================== + +def get_db(): + """Une connexion par requête, rangée dans g et fermée automatiquement.""" + if "db" not in g: + g.db = sqlite3.connect(DB) + g.db.row_factory = sqlite3.Row + return g.db + + +@app.teardown_appcontext +def close_db(exception): + db = g.pop("db", None) + if db is not None: + db.close() + + +def init_db(): + """Crée la base au premier lancement et sème un compte administrateur.""" + if DB.exists(): + return + con = sqlite3.connect(DB) + con.executescript((BASE / "schema.sql").read_text(encoding="utf-8")) + con.execute( + "INSERT INTO adherents (nom, prenom, email, mot_de_passe, niveau, role," + " date_inscription) VALUES (?, ?, ?, ?, ?, ?, ?)", + ("Martin", "Claire", "admin@blocandco.fr", + generate_password_hash("Admin2026!"), "confirme", "admin", + date.today().isoformat()), + ) + con.commit() + con.close() + print("Base créée. Compte admin : admin@blocandco.fr / Admin2026!") + + +@app.context_processor +def injecter_utilisateur(): + """Rend la variable `utilisateur` disponible dans tous les templates.""" + return {"utilisateur": utilisateur_courant()} + + +# ========================================================================== +# EXERCICE 6 — qui est connecté ? +# ========================================================================== + +def utilisateur_courant(): + """Renvoie la ligne de l'adhérent connecté, ou None si personne. + + À FAIRE : + - si "user_id" n'est pas dans `session`, renvoyer None + - sinon, aller chercher la ligne correspondante dans la base + """ + return None + + +# ========================================================================== +# EXERCICE 7 — protéger une page +# ========================================================================== + +def login_requis(vue): + """Interdit l'accès à la vue si personne n'est connecté. + + À FAIRE : si `utilisateur_courant()` vaut None, afficher un message + et rediriger vers la page de connexion. Sinon, appeler la vue. + """ + @wraps(vue) + def wrapper(*args, **kwargs): + return vue(*args, **kwargs) + return wrapper + + +# ========================================================================== +# EXERCICE 9 — réserver une page aux administrateurs +# ========================================================================== + +def admin_requis(vue): + """Interdit l'accès à la vue si l'utilisateur n'est pas administrateur. + + À FAIRE : refuser les visiteurs non connectés, PUIS refuser ceux dont + le rôle n'est pas "admin". Deux messages différents. + """ + @wraps(vue) + def wrapper(*args, **kwargs): + return vue(*args, **kwargs) + return wrapper + + +# ========================================================================== +# EXERCICE 5 — valider le formulaire d'inscription +# ========================================================================== + +def valider_inscription(nom, prenom, email, mdp, confirmation): + """Renvoie la liste des messages d'erreur. Liste vide = tout est bon. + + À FAIRE, dans cet ordre : + - nom et prénom non vides + - email contenant "@" et un point après le "@" + - mot de passe d'au moins 12 caractères + - au moins un chiffre, au moins une majuscule + - les deux mots de passe identiques + """ + erreurs = [] + return erreurs + + +# ========================================================================== +# ROUTES +# ========================================================================== + +@app.route("/") +def accueil(): + """EXERCICE 10 — afficher le nombre d'adhérents inscrits. + + À FAIRE : remplacer le 0 par le vrai total, lu dans la base + avec un SELECT COUNT(*). + """ + nb = 0 + return render_template("accueil.html", nb_adherents=nb) + + +@app.route("/inscription", methods=["GET", "POST"]) +def inscription(): + """EXERCICE 5 — créer un compte. + + À FAIRE : + - en POST, récupérer les champs du formulaire + - les valider avec valider_inscription() + - refuser une adresse e-mail déjà présente dans la base + - hacher le mot de passe avec generate_password_hash() + - insérer l'adhérent avec le rôle "adherent" + - rediriger vers la page de connexion + """ + if request.method == "POST": + pass + return render_template("inscription.html") + + +@app.route("/connexion", methods=["GET", "POST"]) +def connexion(): + """EXERCICE 6 — se connecter. + + À FAIRE : + - chercher l'adhérent par son e-mail + - vérifier le mot de passe avec check_password_hash() + - en cas d'échec, afficher LE MÊME message que l'adresse existe ou non + - en cas de succès, vider la session puis y stocker l'identifiant + """ + if request.method == "POST": + pass + return render_template("connexion.html") + + +@app.route("/deconnexion") +def deconnexion(): + """EXERCICE 7 — se déconnecter. + + À FAIRE : vider la session et rediriger vers l'accueil. + """ + return redirect(url_for("accueil")) + + +@app.route("/profil", methods=["GET", "POST"]) +@login_requis +def profil(): + """EXERCICE 8 — consulter et modifier son profil. + + À FAIRE : + - en POST, mettre à jour nom, prénom et niveau de l'utilisateur CONNECTÉ + - refuser un niveau qui n'est pas dans NIVEAUX + """ + if request.method == "POST": + pass + return render_template("profil.html", niveaux=NIVEAUX) + + +@app.route("/admin") +@admin_requis +def admin(): + """EXERCICE 9 — lister les adhérents. + + À FAIRE : lire tous les adhérents, les plus récents d'abord. + """ + adherents = [] + return render_template("admin.html", adherents=adherents) + + +@app.route("/admin/promouvoir/", methods=["POST"]) +@admin_requis +def promouvoir(adherent_id): + """EXERCICE 9 (suite) — promouvoir un adhérent administrateur.""" + return redirect(url_for("admin")) + + +if __name__ == "__main__": + init_db() + app.run(debug=True) diff --git a/Semaine_16_Exam_01/Jour_01/atelier-web/starter/schema.sql b/Semaine_16_Exam_01/Jour_01/atelier-web/starter/schema.sql new file mode 100644 index 0000000..718c4d3 --- /dev/null +++ b/Semaine_16_Exam_01/Jour_01/atelier-web/starter/schema.sql @@ -0,0 +1,15 @@ +-- Schéma de la base « Bloc&Co » +-- Exécuté une seule fois, au premier démarrage de l'application. + +DROP TABLE IF EXISTS adherents; + +CREATE TABLE adherents ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + nom TEXT NOT NULL, + prenom TEXT NOT NULL, + email TEXT NOT NULL UNIQUE, + mot_de_passe TEXT NOT NULL, -- empreinte, jamais le mot de passe + niveau TEXT NOT NULL DEFAULT 'debutant', + role TEXT NOT NULL DEFAULT 'adherent', + date_inscription TEXT NOT NULL +); diff --git a/Semaine_16_Exam_01/Jour_01/atelier-web/starter/static/style.css b/Semaine_16_Exam_01/Jour_01/atelier-web/starter/static/style.css new file mode 100644 index 0000000..2c98700 --- /dev/null +++ b/Semaine_16_Exam_01/Jour_01/atelier-web/starter/static/style.css @@ -0,0 +1,74 @@ +:root { + --bleu: #0b6e9e; + --bleu-fonce: #084f71; + --gris: #5c6672; + --bord: #dde3e9; + --fond: #f6f8fa; + --rouge: #a8341f; + --vert: #1f6b45; +} +* { box-sizing: border-box; } +body { + margin: 0; + font-family: "Segoe UI", system-ui, sans-serif; + color: #1a1f26; + background: var(--fond); + line-height: 1.55; +} +header { + display: flex; align-items: center; gap: 1.5rem; + padding: 0.9rem 1.5rem; background: #fff; + border-bottom: 1px solid var(--bord); +} +.logo { font-weight: 700; font-size: 1.15rem; color: var(--bleu); text-decoration: none; } +nav { margin-left: auto; display: flex; gap: 1.1rem; align-items: center; } +nav a { color: var(--gris); text-decoration: none; font-size: 0.92rem; } +nav a:hover { color: var(--bleu); } +main { max-width: 780px; margin: 0 auto; padding: 2rem 1.5rem 4rem; } +h1 { font-size: 1.7rem; margin: 0 0 0.4rem; } +h2 { font-size: 1.15rem; margin: 2rem 0 0.6rem; } +.chapeau { color: var(--gris); margin-top: 0; } + +.carte { + background: #fff; border: 1px solid var(--bord); border-radius: 8px; + padding: 1.25rem; display: flex; flex-direction: column; gap: 0.85rem; + max-width: 420px; +} +label { display: flex; flex-direction: column; gap: 0.25rem; font-size: 0.88rem; color: var(--gris); } +input, select { + padding: 0.5rem 0.65rem; border: 1px solid var(--bord); border-radius: 5px; + font-size: 0.95rem; font-family: inherit; color: #1a1f26; background: #fff; +} +input:focus, select:focus { outline: 2px solid var(--bleu); outline-offset: 1px; border-color: var(--bleu); } +.aide { font-size: 0.8rem; color: var(--gris); margin: 0; } + +button, .bouton { + background: var(--bleu); color: #fff; border: none; border-radius: 5px; + padding: 0.55rem 1rem; font-size: 0.95rem; font-family: inherit; + cursor: pointer; text-decoration: none; display: inline-block; text-align: center; +} +button:hover, .bouton:hover { background: var(--bleu-fonce); } +button.lien { background: none; color: var(--bleu); padding: 0; text-decoration: underline; } +button.lien:hover { background: none; color: var(--bleu-fonce); } + +.messages { list-style: none; padding: 0; margin: 0 0 1.25rem; display: flex; flex-direction: column; gap: 0.4rem; } +.messages li { padding: 0.6rem 0.85rem; border-radius: 5px; font-size: 0.9rem; border-left: 3px solid; } +.messages .erreur { background: #fbecea; border-color: var(--rouge); color: var(--rouge); } +.messages .succes { background: #e9f3ed; border-color: var(--vert); color: var(--vert); } + +.stat { + display: flex; align-items: baseline; gap: 0.6rem; + background: #fff; border: 1px solid var(--bord); border-radius: 8px; + padding: 1.1rem 1.4rem; margin: 1.5rem 0; max-width: 420px; +} +.chiffre { font-size: 2.4rem; font-weight: 700; color: var(--bleu); line-height: 1; } +.libelle { color: var(--gris); } + +.infos { display: grid; grid-template-columns: auto 1fr; gap: 0.4rem 1.5rem; } +.infos dt { color: var(--gris); font-size: 0.88rem; } +.infos dd { margin: 0; } + +table { width: 100%; border-collapse: collapse; background: #fff; border: 1px solid var(--bord); border-radius: 8px; overflow: hidden; font-size: 0.9rem; } +th, td { text-align: left; padding: 0.6rem 0.8rem; border-bottom: 1px solid var(--bord); } +th { background: #eef2f6; font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--gris); } +tr:last-child td { border-bottom: none; } diff --git a/Semaine_16_Exam_01/Jour_01/atelier-web/starter/templates/accueil.html b/Semaine_16_Exam_01/Jour_01/atelier-web/starter/templates/accueil.html new file mode 100644 index 0000000..7968ccb --- /dev/null +++ b/Semaine_16_Exam_01/Jour_01/atelier-web/starter/templates/accueil.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} +{% block contenu %} +

Salle d'escalade Bloc&Co

+

Blocs, voies et cours collectifs, sept jours sur sept.

+ +
+ {{ nb_adherents }} + adhérent{{ 's' if nb_adherents > 1 else '' }} inscrit{{ 's' if nb_adherents > 1 else '' }} +
+ + {% if not utilisateur %} +

Créer mon compte

+ {% endif %} +{% endblock %} diff --git a/Semaine_16_Exam_01/Jour_01/atelier-web/starter/templates/admin.html b/Semaine_16_Exam_01/Jour_01/atelier-web/starter/templates/admin.html new file mode 100644 index 0000000..c7e580b --- /dev/null +++ b/Semaine_16_Exam_01/Jour_01/atelier-web/starter/templates/admin.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} +{% block titre %}Administration — Bloc&Co{% endblock %} +{% block contenu %} +

Administration

+

{{ adherents|length }} adhérent{{ 's' if adherents|length > 1 else '' }} enregistré{{ 's' if adherents|length > 1 else '' }}.

+ + + + + + + {% for a in adherents %} + + + + + + + + + {% endfor %} + +
NomE-mailNiveauRôleInscription
{{ a.prenom }} {{ a.nom }}{{ a.email }}{{ a.niveau }}{{ a.role }}{{ a.date_inscription }} + {% if a.role != 'admin' %} +
+ +
+ {% endif %} +
+{% endblock %} diff --git a/Semaine_16_Exam_01/Jour_01/atelier-web/starter/templates/base.html b/Semaine_16_Exam_01/Jour_01/atelier-web/starter/templates/base.html new file mode 100644 index 0000000..b4de430 --- /dev/null +++ b/Semaine_16_Exam_01/Jour_01/atelier-web/starter/templates/base.html @@ -0,0 +1,40 @@ + + + + + + {% block titre %}Bloc&Co{% endblock %} + + + +
+ + +
+ +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
    + {% for categorie, texte in messages %} +
  • {{ texte }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %} + + {% block contenu %}{% endblock %} +
+ + diff --git a/Semaine_16_Exam_01/Jour_01/atelier-web/starter/templates/connexion.html b/Semaine_16_Exam_01/Jour_01/atelier-web/starter/templates/connexion.html new file mode 100644 index 0000000..24311d0 --- /dev/null +++ b/Semaine_16_Exam_01/Jour_01/atelier-web/starter/templates/connexion.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} +{% block titre %}Connexion — Bloc&Co{% endblock %} +{% block contenu %} +

Connexion

+
+ + + +
+

Pas encore de compte ? Inscrivez-vous.

+{% endblock %} diff --git a/Semaine_16_Exam_01/Jour_01/atelier-web/starter/templates/inscription.html b/Semaine_16_Exam_01/Jour_01/atelier-web/starter/templates/inscription.html new file mode 100644 index 0000000..ebb36d8 --- /dev/null +++ b/Semaine_16_Exam_01/Jour_01/atelier-web/starter/templates/inscription.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} +{% block titre %}Inscription — Bloc&Co{% endblock %} +{% block contenu %} +

Créer un compte

+
+ + + + + +

12 caractères minimum, au moins une majuscule et un chiffre.

+ +
+{% endblock %} diff --git a/Semaine_16_Exam_01/Jour_01/atelier-web/starter/templates/profil.html b/Semaine_16_Exam_01/Jour_01/atelier-web/starter/templates/profil.html new file mode 100644 index 0000000..0b6dab5 --- /dev/null +++ b/Semaine_16_Exam_01/Jour_01/atelier-web/starter/templates/profil.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} +{% block titre %}Mon profil — Bloc&Co{% endblock %} +{% block contenu %} +

Mon profil

+ +
+
Adresse e-mail
{{ utilisateur.email }}
+
Rôle
{{ utilisateur.role }}
+
Inscrit depuis le
{{ utilisateur.date_inscription }}
+
+ +

Modifier mes informations

+
+ + + + +
+{% endblock %} diff --git a/Semaine_16_Exam_01/Jour_01/atelier-web/starter/test_app.py b/Semaine_16_Exam_01/Jour_01/atelier-web/starter/test_app.py new file mode 100644 index 0000000..87d3f71 --- /dev/null +++ b/Semaine_16_Exam_01/Jour_01/atelier-web/starter/test_app.py @@ -0,0 +1,193 @@ +"""Tests de l'application Bloc&Co. + +Lancement : pytest -v + +Ces tests valident le comportement attendu à la fin de l'atelier. +Donnez-les aux étudiants : un exercice est réussi quand ses tests passent. +""" + +import sqlite3 + +import app as application +import pytest + + +@pytest.fixture() +def client(tmp_path, monkeypatch): + """Une application neuve, avec une base jetable, pour chaque test.""" + base = tmp_path / "test.db" + monkeypatch.setattr(application, "DB", base) + application.init_db() + application.app.config["TESTING"] = True + with application.app.test_client() as c: + yield c + + +def inscrire(client, email="lea@test.fr", mdp="Grimpe2026ABC"): + return client.post("/inscription", data={ + "nom": "Dupont", "prenom": "Lea", "email": email, + "mot_de_passe": mdp, "confirmation": mdp, + }, follow_redirects=True) + + +def connecter(client, email="lea@test.fr", mdp="Grimpe2026ABC"): + return client.post("/connexion", data={ + "email": email, "mot_de_passe": mdp, + }, follow_redirects=True) + + +# --- Exercice 10 : la statistique ---------------------------------------- + +def test_accueil_affiche_le_nombre_d_adherents(client): + reponse = client.get("/") + assert reponse.status_code == 200 + assert b">1<" in reponse.data # le compte admin seul + + inscrire(client) + assert b">2<" in client.get("/").data + + +# --- Exercice 5 : l'inscription ------------------------------------------ + +def test_inscription_mot_de_passe_trop_court_refuse(client): + reponse = inscrire(client, mdp="Court1A") + assert b"12 caract" in reponse.data + + +def test_inscription_mot_de_passe_sans_chiffre_refuse(client): + reponse = inscrire(client, mdp="MotDePasseSansChiffre") + assert b"chiffre" in reponse.data + + +def test_inscription_email_en_double_refuse(client): + inscrire(client) + reponse = inscrire(client) + assert "déjà utilis".encode() in reponse.data + + +def test_inscription_email_insensible_a_la_casse(client): + inscrire(client, email="Lea@Test.fr") + reponse = inscrire(client, email="lea@test.fr") + assert "déjà utilis".encode() in reponse.data + + +def test_inscription_mot_de_passe_jamais_en_clair(client): + inscrire(client) + stocke = sqlite3.connect(application.DB).execute( + "SELECT mot_de_passe FROM adherents WHERE email = 'lea@test.fr'" + ).fetchone()[0] + assert stocke != "Grimpe2026ABC" + assert len(stocke) > 40 + + +# --- Exercice 6 : la connexion ------------------------------------------- + +def test_mauvais_mot_de_passe_refuse(client): + inscrire(client) + reponse = connecter(client, mdp="MauvaisMotDePasse") + assert b"Identifiants incorrects" in reponse.data + + +def test_compte_inexistant_donne_le_meme_message(client): + """Le message ne doit pas révéler si l'adresse existe dans la base.""" + inscrire(client) + inconnu = connecter(client, email="personne@test.fr", mdp="Grimpe2026ABC") + mauvais = connecter(client, mdp="MauvaisMotDePasse") + assert b"Identifiants incorrects" in inconnu.data + assert b"Identifiants incorrects" in mauvais.data + + +def test_connexion_reussie(client): + inscrire(client) + reponse = connecter(client) + assert b"Mon profil" in reponse.data + + +# --- Exercice 7 : les pages protégées ------------------------------------ + +def test_profil_protege_sans_connexion(client): + reponse = client.get("/profil", follow_redirects=True) + assert b"Connectez-vous" in reponse.data + + +def test_profil_protege_apres_deconnexion(client): + inscrire(client) + connecter(client) + client.get("/deconnexion") + reponse = client.get("/profil", follow_redirects=True) + assert b"Connectez-vous" in reponse.data + + +# --- Exercice 8 : le profil ---------------------------------------------- + +def test_profil_modifiable(client): + inscrire(client) + connecter(client) + reponse = client.post("/profil", data={ + "nom": "Dupont", "prenom": "Léa", "niveau": "confirme", + }, follow_redirects=True) + assert "mis à jour".encode() in reponse.data + + +def test_niveau_invalide_rejete(client): + """Le formulaire HTML est modifiable : le serveur doit revalider.""" + inscrire(client) + connecter(client) + reponse = client.post("/profil", data={ + "nom": "Dupont", "prenom": "Lea", "niveau": "super_admin", + }, follow_redirects=True) + assert b"Niveau invalide" in reponse.data + + +# --- Exercice 9 : les rôles ---------------------------------------------- + +def test_admin_interdit_a_un_adherent(client): + inscrire(client) + connecter(client) + reponse = client.get("/admin", follow_redirects=True) + assert "réservé aux administrateurs".encode() in reponse.data + + +def test_admin_accessible_a_l_administrateur(client): + inscrire(client) + connecter(client, email="admin@blocandco.fr", mdp="Admin2026!") + reponse = client.get("/admin") + assert reponse.status_code == 200 + assert b"lea@test.fr" in reponse.data + + +def test_promotion_interdite_a_un_adherent(client): + inscrire(client) + connecter(client) + client.post("/admin/promouvoir/1", follow_redirects=True) + role = sqlite3.connect(application.DB).execute( + "SELECT role FROM adherents WHERE email = 'lea@test.fr'" + ).fetchone()[0] + assert role == "adherent" + + +# --- Exercice 11 : injection SQL ----------------------------------------- + +def test_injection_sql_sur_la_connexion(client): + """Avec des requêtes paramétrées, cette charge ne doit rien ouvrir.""" + inscrire(client) + reponse = client.post("/connexion", data={ + "email": "lea@test.fr' OR '1'='1", + "mot_de_passe": "peu importe", + }, follow_redirects=True) + assert b"Identifiants incorrects" in reponse.data + assert b"Mon profil" not in reponse.data + + +# --- Exercice 12 : XSS --------------------------------------------------- + +def test_xss_dans_le_prenom_est_echappe(client): + inscrire(client) + connecter(client) + client.post("/profil", data={ + "nom": "Dupont", "prenom": "", + "niveau": "debutant", + }, follow_redirects=True) + reponse = client.get("/profil") + assert b"" not in reponse.data + assert b"<script>" in reponse.data diff --git a/Semaine_16_Exam_01/Jour_01/atelier-web/starter/verif_poste.py b/Semaine_16_Exam_01/Jour_01/atelier-web/starter/verif_poste.py new file mode 100644 index 0000000..2b55cb0 --- /dev/null +++ b/Semaine_16_Exam_01/Jour_01/atelier-web/starter/verif_poste.py @@ -0,0 +1,79 @@ +"""Vérification du poste avant l'atelier. + +Lancez simplement : python verif_poste.py +Puis copiez-collez TOUT le résultat au formateur. + +Ce script ne modifie rien et n'installe rien. +""" + +import platform +import sys + +MINIMUM = (3, 9) + + +def version_python(): + v = sys.version_info + actuelle = f"{v.major}.{v.minor}.{v.micro}" + if (v.major, v.minor) >= MINIMUM: + return True, f"Python {actuelle}" + return False, (f"Python {actuelle} — TROP ANCIEN, il faut au moins " + f"{MINIMUM[0]}.{MINIMUM[1]}") + + +def module_present(nom): + try: + __import__(nom) + except ImportError: + return False, f"{nom} : ABSENT" + try: + from importlib.metadata import version + return True, f"{nom} : {version(nom)}" + except Exception: + return True, f"{nom} : présent" + + +def dans_un_venv(): + actif = sys.prefix != getattr(sys, "base_prefix", sys.prefix) + if actif: + return True, "environnement virtuel : actif" + return True, "environnement virtuel : aucun (ce n'est pas bloquant)" + + +def main(): + print("=" * 56) + print(" VERIFICATION DU POSTE - ATELIER PYTHON WEB") + print("=" * 56) + print(f" Systeme : {platform.system()} {platform.release()}") + print(f" Executable : {sys.executable}") + print("-" * 56) + + resultats = [version_python(), dans_un_venv()] + for nom in ("flask", "pytest"): + resultats.append(module_present(nom)) + + for ok, message in resultats: + print((" [ OK ] " if ok else " [ KO ] ") + message) + + print("-" * 56) + + bloquant = not resultats[0][0] + modules = zip(("flask", "pytest"), resultats[2:], strict=False) + manquants = [nom for nom, (ok, _) in modules if not ok] + + if bloquant: + print(" ACTION : installez une version recente de Python depuis") + print(" python.org, en cochant 'Add python.exe to PATH'.") + print(" Prevenez le formateur des ce soir.") + elif manquants: + print(" ACTION : lancez cette commande, puis relancez ce script :") + print() + print(" python -m pip install " + " ".join(manquants)) + else: + print(" Tout est pret. Rien a faire.") + + print("=" * 56) + + +if __name__ == "__main__": + main()