Initial commit
This commit is contained in:
196
app/__init__.py
Normal file
196
app/__init__.py
Normal file
@@ -0,0 +1,196 @@
|
||||
from datetime import datetime
|
||||
from flask import Flask, Blueprint, flash, render_template, request, url_for
|
||||
import ldap, ldap.filter, os
|
||||
from app.exceptions import LoginNotFoundException, MultipleUsersFoundForLoginException
|
||||
from flask_migrate import Migrate
|
||||
from flask_mail import Mail, Message
|
||||
from app.models import db, MagicToken
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
f_app = Flask(__name__)
|
||||
f_app.secret_key = os.getenv("SECRET_KEY")
|
||||
|
||||
bp = Blueprint('mdp', __name__)
|
||||
|
||||
f_app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv("DATABASE_URI")
|
||||
db.init_app(f_app)
|
||||
migrate = Migrate(f_app, db)
|
||||
|
||||
def to_bool(value, default: bool = False):
|
||||
"""
|
||||
Convert a value to a boolean
|
||||
"""
|
||||
if value is None:
|
||||
return default
|
||||
if isinstance(value, str):
|
||||
if value.lower() in ['true', '1', 't', 'y', 'yes']:
|
||||
return True
|
||||
if value.lower() in ['false', '0', 'f', 'n', 'no']:
|
||||
return False
|
||||
if value:
|
||||
return True
|
||||
return default
|
||||
|
||||
f_app.config['MAIL_SERVER']= os.getenv("MAIL_SERVER")
|
||||
f_app.config['MAIL_PORT'] = int(os.getenv("MAIL_PORT"))
|
||||
f_app.config['MAIL_USERNAME'] = os.getenv("MAIL_USERNAME")
|
||||
f_app.config['MAIL_PASSWORD'] = os.getenv("MAIL_PASSWORD")
|
||||
f_app.config['MAIL_USE_TLS'] = to_bool(os.getenv("MAIL_USE_TLS"))
|
||||
f_app.config['MAIL_USE_SSL'] = to_bool(os.getenv("MAIL_USE_SSL"))
|
||||
|
||||
mail = Mail(f_app)
|
||||
|
||||
@bp.route('/')
|
||||
def home():
|
||||
return render_template('pages/home.html.j2')
|
||||
|
||||
@bp.route('/identifiant')
|
||||
def lost_login():
|
||||
return render_template('pages/lost_login.html.j2')
|
||||
|
||||
@bp.route('/mdp', methods=("GET", "POST"))
|
||||
def password_link_form():
|
||||
if request.method == 'GET':
|
||||
return render_template('pages/password_link_form.html.j2')
|
||||
|
||||
login = request.form['login']
|
||||
error = None
|
||||
|
||||
if not login:
|
||||
error = "Le champ 'identifiant' ne peut pas être vide."
|
||||
else:
|
||||
try:
|
||||
user_dn, user_emails = ldap_get_dn_and_emails(login)
|
||||
if len(user_emails) == 0:
|
||||
error = "Aucune adresse email trouvée pour cet identifiant. Rapprochez-vous du secrétariat de votre formation pour mettre à jour votre dossier ou " + render_template("partials/contact_link.html.j2") + "."
|
||||
except LoginNotFoundException:
|
||||
error = "Aucun utilisateur trouvé avec cet identifiant.<br>Si vous avez perdu votre identifiant, <a class=\"text-blue-600 hover:underline\" href=\"" + url_for("mdp.lost_login") + "\">cliquez ici</a>."
|
||||
except MultipleUsersFoundForLoginException:
|
||||
error = "Plusieurs utilisateurs trouvés avec cet identifiant (cette erreur ne devrait jamais s'afficher, " + render_template("partials/contact_link.html.j2") + ")."
|
||||
except (ldap.SERVER_DOWN, ldap.BUSY, ldap.CONNECT_ERROR, ldap.UNAVAILABLE):
|
||||
error = "La connexion au serveur LDAP a échouée."
|
||||
except ldap.TIMEOUT:
|
||||
error = "Le serveur LDAP a mis trop de temps à répondre."
|
||||
except ldap.LDAPError:
|
||||
error = "Une erreur est survenue lors de la connexion à l'annuaire LDAP."
|
||||
|
||||
if error is not None:
|
||||
flash(error)
|
||||
return render_template('pages/password_link_form.html.j2')
|
||||
|
||||
try:
|
||||
magictoken = MagicToken.generate(user_emails, user_dn)
|
||||
db.session.add(magictoken)
|
||||
db.session.commit()
|
||||
except:
|
||||
flash("Une erreur est survenue lors de la création du lien.")
|
||||
return render_template('pages/password_link_form.html.j2')
|
||||
|
||||
try:
|
||||
send_email(user_emails, magictoken)
|
||||
anonymized_emails = [anonymize_email(email) for email in user_emails]
|
||||
except:
|
||||
flash("Une erreur est survenue lors de l'envoi du mail.")
|
||||
return render_template('pages/password_link_form.html.j2')
|
||||
|
||||
return render_template('pages/password_link_sent.html.j2', emails=anonymized_emails, is_student=True)
|
||||
|
||||
@bp.route('/mdp/<token>', methods=("GET", "POST"))
|
||||
def password_reset_form(token: str = None):
|
||||
magictoken = MagicToken.query.filter_by(token=token).first()
|
||||
if magictoken is None:
|
||||
return render_template('pages/invalid_password_token.html.j2')
|
||||
if magictoken.used:
|
||||
return render_template('pages/invalid_password_token.html.j2', used=True)
|
||||
if magictoken.expiration_date < datetime.now():
|
||||
return render_template('pages/invalid_password_token.html.j2', expired=True)
|
||||
|
||||
if request.method == 'GET':
|
||||
return render_template('pages/password_reset_form.html.j2')
|
||||
|
||||
newpassword = request.form['newpasswd']
|
||||
error = None
|
||||
|
||||
if not newpassword:
|
||||
error = "Le champ 'mot de passe' ne peut pas être vide."
|
||||
|
||||
if error is not None:
|
||||
flash(error)
|
||||
return render_template('pages/password_reset_form.html.j2')
|
||||
|
||||
try:
|
||||
ldap_set_password(magictoken.user_dn, newpassword)
|
||||
except (ldap.SERVER_DOWN, ldap.BUSY, ldap.CONNECT_ERROR, ldap.UNAVAILABLE):
|
||||
error = "La connexion au serveur LDAP a échouée."
|
||||
except ldap.TIMEOUT:
|
||||
error = "Le serveur LDAP a mis trop de temps à répondre."
|
||||
except ldap.LDAPError as err:
|
||||
error = "Erreur LDAP. " + str(err)
|
||||
|
||||
if error is not None:
|
||||
flash(error)
|
||||
return render_template('pages/password_reset_form.html.j2')
|
||||
|
||||
magictoken.used = True
|
||||
db.session.commit()
|
||||
|
||||
return render_template('pages/password_reset_success.html.j2')
|
||||
|
||||
|
||||
def send_email(emails: list[str], magictoken: MagicToken):
|
||||
"""
|
||||
Send the magic token to the list of emails
|
||||
"""
|
||||
msg = Message(subject='Initialisation du mot de passe', sender=os.getenv("MAIL_DEFAULT_SENDER"), recipients=emails)
|
||||
msg.body = render_template('emails/reset-link.txt.j2', magictoken=magictoken)
|
||||
msg.html = render_template('emails/reset-link.html.j2', magictoken=magictoken)
|
||||
mail.send(msg)
|
||||
|
||||
def anonymize_email(email: str):
|
||||
"""
|
||||
Anonymize an email address before displaying it on the website
|
||||
"""
|
||||
email_parts = email.split('@')
|
||||
email_parts[0] = email_parts[0][:3] + '*' * (len(email_parts[0]) - 3)
|
||||
known_domains = ['etu.u-pec.fr', 'u-pec.fr', 'iutsf.org', 'iut-fbleau.fr']
|
||||
if email_parts[1].lower() not in known_domains:
|
||||
email_parts[1] = email_parts[1].rsplit('.', 1)
|
||||
email_parts[1][0] = email_parts[1][0][:3] + '*' * (len(email_parts[1][0]) - 3)
|
||||
email_parts[1] = '.'.join(email_parts[1])
|
||||
|
||||
return '@'.join(email_parts)
|
||||
|
||||
def ldap_get_dn_and_emails(login: str):
|
||||
"""
|
||||
Get the user's distinguished name and email addresses from the LDAP server
|
||||
"""
|
||||
if not to_bool(os.getenv("LDAP_TLS_VERIFY")):
|
||||
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
|
||||
conn = ldap.initialize(os.getenv("LDAP_SERVER"))
|
||||
conn.simple_bind_s(os.getenv("LDAP_USER"), os.getenv("LDAP_PASSWORD"))
|
||||
result = conn.search_s(
|
||||
os.getenv("LDAP_BASE_DN"),
|
||||
ldap.SCOPE_SUBTREE,
|
||||
os.getenv("LDAP_SEARCH_FILTER") % ldap.filter.escape_filter_chars(login),
|
||||
[os.getenv("LDAP_EMAIL_ATTRIBUTE")]
|
||||
)
|
||||
if len(result) == 0:
|
||||
raise LoginNotFoundException()
|
||||
if len(result) > 1:
|
||||
raise MultipleUsersFoundForLoginException()
|
||||
return result[0][0], [entry[1][os.getenv("LDAP_EMAIL_ATTRIBUTE")][0].decode('utf-8') for entry in result]
|
||||
|
||||
def ldap_set_password(user_dn: str, newpassword: str):
|
||||
"""
|
||||
Set the user's password in LDAP
|
||||
"""
|
||||
if not to_bool(os.getenv("LDAP_TLS_VERIFY")):
|
||||
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
|
||||
conn = ldap.initialize(os.getenv("LDAP_SERVER"))
|
||||
conn.simple_bind_s(os.getenv("LDAP_USER"), os.getenv("LDAP_PASSWORD"))
|
||||
# https://serverfault.com/questions/937330/update-ad-password-from-python-ldap-code-insuff-access-rights
|
||||
conn.modify_s(user_dn, [(ldap.MOD_REPLACE, 'unicodePwd', ['"{0}"'.format(newpassword).encode('utf-16-le')])])
|
||||
|
||||
|
||||
f_app.register_blueprint(bp)
|
||||
5
app/exceptions.py
Normal file
5
app/exceptions.py
Normal file
@@ -0,0 +1,5 @@
|
||||
class LoginNotFoundException(Exception):
|
||||
pass
|
||||
|
||||
class MultipleUsersFoundForLoginException(Exception):
|
||||
pass
|
||||
39
app/models.py
Normal file
39
app/models.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from datetime import datetime, timedelta
|
||||
import secrets
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
from sqlalchemy.sql import func
|
||||
from typing import Self
|
||||
from flask import url_for
|
||||
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
db = SQLAlchemy()
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
class MagicToken(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
token = db.Column(db.String(length=255, collation="utf8mb4_bin"), nullable=False)
|
||||
email = db.Column(db.String(length=255), nullable=False)
|
||||
expiration_date = db.Column(db.DateTime, nullable=False, server_default=func.now())
|
||||
used = db.Column(db.Boolean, nullable=False, default=False)
|
||||
user_dn = db.Column(db.String(length=255), nullable=False)
|
||||
|
||||
def __init__(self, token, email, expiration_date, user_dn):
|
||||
self.token = token
|
||||
self.email = email
|
||||
self.expiration_date = expiration_date
|
||||
self.user_dn = user_dn
|
||||
|
||||
def url(self) -> str:
|
||||
return "https://mdp.iut-fbleau.fr" + url_for('mdp.password_reset_form', token=self.token)
|
||||
|
||||
@staticmethod
|
||||
def generate(emails: list[str], user_dn: str) -> Self:
|
||||
token = secrets.token_urlsafe(128)
|
||||
expiration_date = datetime.now() + timedelta(minutes=15)
|
||||
return MagicToken(token, ",".join(emails), expiration_date, user_dn)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<MagicToken {self.id} {self.email} {self.expiration_date} {self.used} {self.user_dn}>"
|
||||
33
app/templates/emails/base.html.j2
Normal file
33
app/templates/emails/base.html.j2
Normal file
@@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
{% if title is defined and title.strip() %}
|
||||
<title>Mot de passe IUTF - {% block title %}{% endblock %}</title>
|
||||
{% else %}
|
||||
<title>Mot de passe IUTF</title>
|
||||
{% endif %}
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="format-detection" content="date=no">
|
||||
<meta name="format-detection" content="address=no">
|
||||
<meta name="format-detection" content="email=no">
|
||||
</head>
|
||||
<body>
|
||||
<table role="presentation">
|
||||
<tr>
|
||||
<img src="https://emails-assets.s3.iut-fbleau.fr/logo/upec-iutsf.png" alt="UPEC | IUT Sénart-Fontainebleau" width="200" height="100">
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<nav>
|
||||
<h1>Mot de passe IUTF</h1>
|
||||
</nav>
|
||||
<section class="content">
|
||||
{% block content %}{% endblock %}
|
||||
</section>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
1
app/templates/emails/base.txt.j2
Normal file
1
app/templates/emails/base.txt.j2
Normal file
@@ -0,0 +1 @@
|
||||
{% block content %}{% endblock %}
|
||||
19
app/templates/emails/reset-link.html.j2
Normal file
19
app/templates/emails/reset-link.html.j2
Normal file
@@ -0,0 +1,19 @@
|
||||
{% extends 'emails/base.html.j2' %}
|
||||
|
||||
{% block title %}Email de réinitialisation du mot de passe{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<p>
|
||||
Bonjour,<br>
|
||||
<br>
|
||||
Nous avons reçu une demande de création d'un nouveau mot de passe associé à cette adresse email.<br>
|
||||
Ce mot de passe vous permet d'accéder aux services numériques du campus de l'IUT de Fontainebleau (Wifi IUTF, ordinateurs du campus et services en ligne).<br>
|
||||
<br>
|
||||
Si vous êtes à l'origine de cette demande, veuillez ouvrir ce lien dans votre navigateur : <a href="{{ magictoken.url() }}">{{ magictoken.url() }}</a><br>
|
||||
La durée de validité de ce lien est de 15 minutes.<br>
|
||||
<br>
|
||||
Si vous n'êtes pas l'auteur de cette demande, vous pouvez ignorer cet e-mail.<br>
|
||||
<br>
|
||||
A très bientôt.
|
||||
</p>
|
||||
{% endblock %}
|
||||
16
app/templates/emails/reset-link.txt.j2
Normal file
16
app/templates/emails/reset-link.txt.j2
Normal file
@@ -0,0 +1,16 @@
|
||||
{% extends 'emails/base.txt.j2' %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
Bonjour,
|
||||
|
||||
Nous avons reçu une demande de création d'un nouveau mot de passe associé à cette adresse email.
|
||||
Ce mot de passe vous permet d'accéder aux services numériques du campus de l'IUT de Fontainebleau (Wifi IUTF, ordinateurs du campus et services en ligne).
|
||||
|
||||
Si vous êtes à l'origine de cette demande, veuillez ouvrir ce lien dans votre navigateur : {{ magictoken.url() }}
|
||||
La durée de validité de ce lien est de 15 minutes.
|
||||
|
||||
Si vous n'êtes pas l'auteur de cette demande, vous pouvez ignorer cet e-mail.
|
||||
|
||||
A très bientôt.
|
||||
{% endblock %}
|
||||
37
app/templates/pages/base.html.j2
Normal file
37
app/templates/pages/base.html.j2
Normal file
@@ -0,0 +1,37 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
{% if title is defined %}
|
||||
<title>{{ title }} - Mot de passe IUTF</title>
|
||||
{% else %}
|
||||
<title>Mot de passe IUTF</title>
|
||||
{% endif %}
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
{% if config['DEBUG'] %}
|
||||
<script src="https://unpkg.com/@tailwindcss/browser@4"></script>
|
||||
{% else %}
|
||||
<link href="{{ url_for('static', filename='style.css') }}" rel="stylesheet">
|
||||
{% endif %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<header class="flex w-full justify-center items-center">
|
||||
<a class="w-1/2" href="{{ url_for('mdp.home') }}">
|
||||
<img src="https://public-assets.s3.iut-fbleau.fr/img/logo/upec-iutsf.png" alt="UPEC - IUT Sénart-Fontainebleau">
|
||||
</a>
|
||||
<div class="w-full text-center font-bold">
|
||||
<h1 class="text-3xl"><a href="{{ url_for('mdp.home') }}">Mot de passe</a></h1>
|
||||
{% if title is defined %}
|
||||
<h2 class="text-xl">{{ title }}</h2>
|
||||
{% endif %}</div>
|
||||
</header>
|
||||
<main class="p-5">
|
||||
{% for message in get_flashed_messages() %}
|
||||
<div class="border rounded p-2 bg-yellow-100 my-5">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
30
app/templates/pages/home.html.j2
Normal file
30
app/templates/pages/home.html.j2
Normal file
@@ -0,0 +1,30 @@
|
||||
{% extends 'pages/base.html.j2' %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
<h2 class="text-xl py-5 font-bold">Gérez votre compte numérique en toute sécurité !</h2>
|
||||
<p>
|
||||
Pour bénéficier de l'ensemble des services numériques offerts par le campus de l'IUT de Fontainebleau, vous devez sécuriser votre compte numérique.
|
||||
En sécurisant votre compte, vous acceptez sans réserve les conditions d'utilisation définies dans la <a class="text-blue-600 hover:underline" href="https://iut-fbleau.fr/charte">charte informatique</a> de l'IUT.
|
||||
</p>
|
||||
|
||||
<div class="border rounded p-2 bg-yellow-100 my-5">
|
||||
<p>
|
||||
Ce compte est <span class="underline">dédié aux services numériques de l'IUT de Fontainebleau</span> (salles TP informatique, Wifi IUTF,
|
||||
<a class="text-blue-600 hover:underline" target="_blank" href="https://iut-fbleau.fr">services en ligne sur les sites iut-fbleau.fr</a>, etc).
|
||||
</p>
|
||||
<p>
|
||||
Pour accéder aux services de l'université (<a class="text-blue-600 hover:underline" target="_blank" href="http://outlook.office.com/?realm=u-pec.fr">Messagerie étudiante</a>,
|
||||
<a class="text-blue-600 hover:underline" target="_blank" href="https://eprel.u-pec.fr/">Eprel</a>,
|
||||
<a class="text-blue-600 hover:underline" target="_blank" href="https://www.u-pec.fr/fr/etudiant-e/services-numeriques/wi-fi-etudiant">Eduroam</a>,
|
||||
<a class="text-blue-600 hover:underline" target="_blank" href="https://www.u-pec.fr/fr/etudiant-e/services-numeriques">services en ligne sur les sites u-pec.fr</a> etc.),
|
||||
vous devez utiliser activer votre compte UPEC sur <a class="text-blue-600 hover:underline" target="_blank" href="https://sesame.u-pec.fr">sesame.u-pec.fr</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center gap-5">
|
||||
<a class="text-white bg-red-600 hover:bg-red-700 rounded-lg font-medium text-sm px-5 py-3" href="{{ url_for('mdp.lost_login') }}">J'ai perdu mon identifiant</a>
|
||||
<a class="text-white bg-red-600 hover:bg-red-700 rounded-lg font-medium text-sm px-5 py-3" href="{{ url_for('mdp.password_link_form') }}">J'ai perdu mon mot de passe</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
12
app/templates/pages/invalid_password_token.html.j2
Normal file
12
app/templates/pages/invalid_password_token.html.j2
Normal file
@@ -0,0 +1,12 @@
|
||||
{% extends 'pages/base.html.j2' %}
|
||||
|
||||
{% set title = "Réinitialiser mon mot de passe" %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
<p class="py-2">
|
||||
Ce lien de réinitialisation de mot de passe {% if expired %}a expiré (le délai de 15 minutes est écoulées){% elif used %}a déjà été utilisé{% else %}est invalide{% endif %}.
|
||||
</p>
|
||||
<p class="py-2">Pour demander un nouveau lien de réinitialisation de mot de passe, <a href="{{ url_for('mdp.password_link_form') }}">cliquez ici</a>.</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
17
app/templates/pages/lost_login.html.j2
Normal file
17
app/templates/pages/lost_login.html.j2
Normal file
@@ -0,0 +1,17 @@
|
||||
{% extends 'pages/base.html.j2' %}
|
||||
|
||||
{% set title = "Identifiant perdu" %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
<p class="py-2">
|
||||
Votre identifiant pour accéder aux services numériques du campus de Fontainebleau est composée de votre nom de famille tronqué à 8 caractères.
|
||||
</p>
|
||||
<p class="py-2">
|
||||
En cas d'homonyme, les lettres de votre prénom ou des chiffres sont utilisés pour différencier les identifiants mais le resultat est toujours tronqué à 8 caractères.
|
||||
</p>
|
||||
<p class="py-2">
|
||||
Si vous avez perdu votre identifiant, {% include 'partials/contact_link.html.j2' %}.
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
18
app/templates/pages/password_link_form.html.j2
Normal file
18
app/templates/pages/password_link_form.html.j2
Normal file
@@ -0,0 +1,18 @@
|
||||
{% extends 'pages/base.html.j2' %}
|
||||
|
||||
{% set title = "Réinitialiser mon mot de passe" %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
<p class="mb-5">Remplissez le formulaire ci-dessous pour retrouver votre identifiant pour les services numérique du campus de Fontainebleau</p>
|
||||
|
||||
<form method="POST">
|
||||
<div class="mb-5">
|
||||
<label class="block mb-2 text-sm font-medium text-gray-900" for="login">Identifiant</label>
|
||||
<input type="text" name="login" id="login" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" required>
|
||||
<a class="text-blue-600 hover:underline text-sm" href="{{ url_for('mdp.lost_login') }}">J'ai perdu mon identifiant</a>
|
||||
</div>
|
||||
<button class="text-white bg-red-600 hover:bg-red-700 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center" type="submit">Continuer</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
20
app/templates/pages/password_link_sent.html.j2
Normal file
20
app/templates/pages/password_link_sent.html.j2
Normal file
@@ -0,0 +1,20 @@
|
||||
{% extends 'pages/base.html.j2' %}
|
||||
|
||||
{% set title = "Réinitialiser mon mot de passe" %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
<p class="py-2">Un lien pour réinitialiser votre mot de passe à été envoyé {% if emails|length > 1 %}aux addresses suivantes{% else %}à l'adresse suivante{% endif %} :</p>
|
||||
<ul class="list-disc list-inside pb-2">
|
||||
{% for email in emails %}
|
||||
<li class="ps-5">{{ email }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<p class="py-2">Si vous ne recevez pas l'email, vérifiez votre dossier de courrier indésirable.</p>
|
||||
<p class="py-2">
|
||||
Si vous ne reconnaissez pas {% if emails|length > 1 %}les addresses listées{% else %}l'adresse affichée{% endif %} ci-dessus,
|
||||
rapprochez vous du secrétariat de votre formation pour mettre à jour votre dossier ou {% include 'partials/contact_link.html.j2' %}.
|
||||
</p>
|
||||
<p class="py-2">Ce lien est valable pour les 15 prochaines minutes uniquement !</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
17
app/templates/pages/password_reset_form.html.j2
Normal file
17
app/templates/pages/password_reset_form.html.j2
Normal file
@@ -0,0 +1,17 @@
|
||||
{% extends 'pages/base.html.j2' %}
|
||||
|
||||
{% set title = "Réinitialiser mon mot de passe" %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
<p class="mb-5">Définissez votre nouveau mot de passe</p>
|
||||
|
||||
<form method="POST">
|
||||
<div class="mb-5">
|
||||
<label class="block mb-2 text-sm font-medium text-gray-900" for="newpasswd">Nouveau mot de passe</label>
|
||||
<input type="password" name="newpasswd" id="newpasswd" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" required>
|
||||
</div>
|
||||
<button class="text-white bg-red-600 hover:bg-red-700 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center" type="submit">Enregistrer</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
11
app/templates/pages/password_reset_success.html.j2
Normal file
11
app/templates/pages/password_reset_success.html.j2
Normal file
@@ -0,0 +1,11 @@
|
||||
{% extends 'pages/base.html.j2' %}
|
||||
|
||||
{% set title = "Réinitialiser mon mot de passe" %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
<p class="py-2">
|
||||
Votre mot de passe a été modifié avec succès. Vous pouvez maintenant vous connecter avec votre nouveau mot de passe.
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
1
app/templates/partials/contact_link.html.j2
Normal file
1
app/templates/partials/contact_link.html.j2
Normal file
@@ -0,0 +1 @@
|
||||
contactez <a class="text-blue-600 hover:underline" href="mailto:brouard@u-pec.fr">Régis Brouard</a>
|
||||
Reference in New Issue
Block a user