UE5 : Fondamentaux du développement web frontal, niveau 2

Séance 6 : Gestion des utilisateurs

  1. Création des utilisateurs
  2. Connexion
  3. Déconnexion
  4. Rendre obligatoire la connexion sur une route

Créer et gérer des utilisateurs sur un site est courant et presque indispensable désormais. Ce cours se contentera d'apporter les bases de la gestion utilisateurs avec uniquement un prénom et un mot de passe. Rajouter d'autres attributs pourra facilement être fait ultérieurement une fois la méthodologie acquise.

Avant toutes choses, le modèle de la base de données actuellement utilisée pour le Factbook ne contient pas de tables pour la gestion utilisateurs. Deux solutions s'offrent alors:

Pour ajouter la table, exécuter dans un éditeru SQL la requête suivante:

CREATE TABLE "users" (
	"id"	INTEGER NOT NULL,
	"prenom"	TEXT NOT NULL,
	"password"	TEXT NOT NULL,
	PRIMARY KEY("id" AUTOINCREMENT)
);

Vous pouvez sinon utiliser la base de données factbook_users.sqlite. Dans ce cas, il faut penser à changer dans le fichier .env le nom de la base de données et son chemin.

Création des utilisateurs

Comme pour les modèles de base de données ou les modèles de formulaires, les utilisateurs vont utiliser une méthode que l'on connaît déjà.

La première étape est la création de la class Users dans le module models/users.py. Grâce à la requête SQL ci-dessus, on en déduit les attributs de classe suivants:

# models/users.py

from ..app import app, db

class Users(db.Model):
    __tablename__ = "users"

    id = db.Column(db.Integer, unique=True, nullable=False, primary_key=True, autoincrement=True)
    prenom = db.Column(db.Text, nullable=False)
    password = db.Column(db.String(100), nullable=False)

Adossons à cette classe une méthode statique (avec le décorateur @staticmethod), qui nous permettra de créer facilement un utilisateur ainsi : User.ajout("jean", "mot de passe"). L'avantage de la méthode statique est qu'elle n'a pas besoin d'un objet User() existant pour fonctionner. On iteragit donc avec la classe sans que l'objet lui-même existe. On l'appele statique car elle n'est pas liée à une instance, contrairement aux méthodes classiques.

# models/users.py
from werkzeug.security import generate_password_hash

class Users(db.Model):
	...

	@staticmethod
    def ajout(prenom, password):
        erreurs = []
        if not prenom:
            erreurs.append("Le prénom est vide")
        if not password or len(password) < 6:
            erreurs.append("Le mot de passe est vide ou trop court")

        unique = Users.query.filter(
            db.or_(Users.prenom == prenom)
        ).count()
        if unique > 0:
            erreurs.append("Le prénom existe déjà")

        if len(erreurs) > 0:
            return False, erreurs
        
        utilisateur = Users(
            prenom=prenom,
            password=generate_password_hash(password)
        )

        try:
            db.session.add(utilisateur)
            db.session.commit()
            return True, utilisateur
        except Exception as erreur:
            return False, [str(erreur)]

Commentaires:

A propos du sel Pour fonctionner, generate_password_hash a besoin d'une variable SECRET_KEY que l'on définit dans .env et dans config.py. Cette clé secrète doit être unique, non devinable et le plus complexe possible. ATtention, ne pas changer sa valeur durant la vie de l'application, sinon tout ce qui aura été hashé par l'application deviendra introuvable.

# .env

SECRET_KEY=duselnondevinableetunique
# config.py

...
class Config():
    ...
    SECRET_KEY = os.environ.get("SECRET_KEY")

Maintenant que le modèle de users est fait et que sa méthode ajout a été développé, il faut désormais créer ce qui permettra d'exécuter cette méthode ajout, c'est à dire une route, qui renverra un formulaire permettant de saisir un prénom et un mot de passe.

Il faut débuter par la création du FlaskForm:

# models/formulaires.py

class AjoutUtilisateur(FlaskForm):
    prenom = StringField("prenom", validators=[])
    password = PasswordField("password", validators=[])

On découvre ici le PasswordField de Flask-WTF: il permettra de cacher le mot de passe saisi par des étoiles.

Ensuite, il faut créer la route ajout_utilisateur:

#routes/users.py

from flask import url_for, render_template, redirect, request, flash
from ..models.users import Users
from ..models.formulaires import AjoutUtilisateur
from ..utils.transformations import  clean_arg
from ..app import app, db

@app.route("/utilisateurs/ajout", methods=["GET", "POST"])
def ajout_utilisateur():
    form = AjoutUtilisateur()

    if form.validate_on_submit():
        statut, donnees = Users.ajout(
            prenom=clean_arg(request.form.get("prenom", None)),
            password=clean_arg(request.form.get("password", None))
        )
        if statut is True:
            flash("Ajout effectué", "success")
            return redirect(url_for("accueil"))
        else:
            flash(",".join(donnees), "error")
            return render_template("pages/ajout_utilisateur.html", form=form)
    else:
        return render_template("pages/ajout_utilisateur.html", form=form)

Commentaire:

Il reste à créer les templates HTML:

<!-- templates/partials/formulaires/ajout_utilisateur.html -->

<form action="{{url_for('ajout_utilisateur')}}" method="post" name="ajout_utilisateur">
    {{ form.hidden_tag() }}

    <div class="form-group">
        <label for="prenom">Prénom de l'utilisateur</label>
        {{ form.prenom(class_="form-control", placeholder_="Prénom") }}
        <small id="prenom" class="form-text text-muted">Entrer le prénom de l'utilisateur</small>
    </div>

    <div class="form-group">
        <label for="password">Mot de passe</label>
        {{ form.password(class_="form-control", placeholder_="Mot de passe") }}
        <small id="password" class="form-text text-muted">Entrer un mot de passe</small>
    </div>

    <p><input type="submit" value="Ajouter"></p>
</form>
<!-- templates/pages/ajout_utilisateur.html -->

        <div class="col-sm-8">

            <h3 data-toggle="collapse" data-target="#collapseExample" aria-expanded="false"
                aria-controls="collapseExample">
                Ajouter un utilisateur <i class="fa-solid fa-caret-down"></i>
        </h3>
            <div class="collapse show" id="collapseExample">
                <div class="card card-body">
                    {% include "partials/formulaires/ajout_utilisateur.html" %}
                </div>
            </div>
        </div>

Code concerné: Seance6/ajout_utilisateurs

Connexion

Pour gérer les utilisateurs, il existe la librairie Flask-Login, qui va permettre de savoir, très simplement, pour chaque route, si un utilisateur est connecté ou non, quel est son identifiant, etc. Il permet également de créer des sessions utilisateurs et de poser les cookies nécessaires chez le client de manière à ce que la connexion reste établie. Pour l'installer, pip install flask-login.

Comme pour Flask avec app et Flask-SQLAlchemy avec db, il va falloir instancier Flask-Login dans app.py.

# app/app.py 

from flask_login import LoginManager

...
login = LoginManager(app)

login va alors proposer 4 méthodes, dont certaines nous seront utiles:

Pour éviter à avoir à développer ces fonctionnalités de cnotre côté, Flask-Login propose le UserMixin, dont on fera hériter la classe Users:

# models/users.py

from flask_login import UserMixin

class Users(UserMixin, db.Model):
	...

Les seules méthodes de classe que nous avons à développer pour permettre à Flask-Login de fonctionner sont les suivantes:

# models/users.py
from ..app import app, db, login

class Users(UserMixin, db.Model):
	...
	def get_id(self):
        return self.id

	@login.user_loader
    def get_user_by_id(id):
        return Users.query.get(int(id))

Commentaires:

On peut désormais utiliser la gestion des utilisateurs. A nouveau, pour permettre à l'utilisateur de se connecter, il faut faire les étapes habituelles de :

La méthode statique Users.identification(prenom, mot_de_passe) permettra d'identifier l'utilisateur grâce au prénom et au mot de passe qu'il fournira dans le formulaire de connexion.

# models/users.py

from werkzeug.security import check_password_hash

class Users(UserMixin, db.Model):
    ...

    @staticmethod
    def identification(prenom, password):
        utilisateur = Users.query.filter(Users.prenom == prenom).first()
        if utilisateur and check_password_hash(utilisateur.password, password):
            return utilisateur
        return None

Commentaires:

Création du FlaskForm:

# models/formulaires.py

class Connexion(FlaskForm):
    prenom = StringField("prenom", validators=[])
    password = PasswordField("password", validators=[])

Création de la route de connexion:

# routes/users.py
from flask_login import login_user, current_user

@app.route("/utilisateurs/connexion", methods=["GET","POST"])
def connexion():
    form = Connexion()

    if current_user.is_authenticated is True:
        flash("Vous êtes déjà connecté", "info")
        return redirect(url_for("accueil"))

    if form.validate_on_submit():
        utilisateur = Users.identification(
            prenom=clean_arg(request.form.get("prenom", None)),
            password=clean_arg(request.form.get("password", None))
        )
        if utilisateur:
            flash("Connexion effectuée", "success")
            login_user(utilisateur)
            return redirect(url_for("accueil"))
        else:
            flash("Les identifiants n'ont pas été reconnus", "error")
            return render_template("pages/connexion.html", form=form)

    else:
        return render_template("pages/connexion.html", form=form)

login.login_view = 'connexion'

Commentaires:

Il reste à faire les templates partials/formulaires/connexion.html et pages/connxeion.html.

Code concerné: Seance6/connexion

Déconnexion

La déconnexion est très simple avec Flask-Login, elle tient en une route et en une bonne configuration de la barre de navigation en HTML.

# routes/users.py
from flask_login import  current_user, logout_user

@app.route("/utilisateurs/deconnexion", methods=["POST", "GET"])
def deconnexion():
    if current_user.is_authenticated is True:
        logout_user()
    flash("Vous êtes déconnecté", "info")
    return redirect(url_for("accueil"))

Commentaires:

Pour une meilleure expérience utilisateur, il convient de cacher le lien de déconnexion quand il n'est pas connecté, et d'afficher le lien de connexion quand il n'est pas connecté.

<!-- partials/conteneur.html -->

{% if current_user.is_authenticated %}
<a class="dropdown-item" href="{{ url_for('deconnexion') }}">Se déconnecter</a>
{% else %}
<a class="dropdown-item" href="{{ url_for('connexion') }}">Se connecter</a>
{% endif %}

Rendre obligatoire la connexion sur une route

Imaginons que l'on souhaite rendre la recherche avancée dans le Factbook payante. Il faudrait alors restreindre l'accès à cette page uniquement aux utilisateurs ayant payé. Pour cela, il ne suffit que d'une ligne de code dans les routes, en utilisant le décorateur @login_required.

# routes/generales.py

from flask_login import login_required

@app.route("/recherche", methods=['GET', 'POST'])
@app.route("/recherche/<int:page>", methods=['GET', 'POST'])
@login_required
def recherche(page=1):
	...

Si un utilisateur non connecté arrive veut accéder à la route /recherche, il sera alors redirigé vers la page de connexion (indiquée à Flask avec login.login_view = 'connexion' dans routes/users.py) : http://localhost:5000/utilisateurs/connexion?next=%2Frecherche.

Code concerné: Seance6/obligatoire