diff --git a/app/forms/account.py b/app/forms/account.py index 20473d6..660a7e3 100644 --- a/app/forms/account.py +++ b/app/forms/account.py @@ -137,6 +137,35 @@ class DeleteAccountForm(FlaskForm): ) +class AskResetPasswordForm(FlaskForm): + email = EmailField( + 'Adresse email', + validators=[ + Optional(), + Email(message="Addresse email invalide."), + ], + ) + submit = SubmitField('Valider') + + +class ResetPasswordForm(FlaskForm): + password = PasswordField( + 'Mot de passe', + validators=[ + Optional(), + vd.password, + ], + ) + password2 = PasswordField( + 'Répéter le mot de passe', + validators=[ + Optional(), + EqualTo('password', message="Les mots de passe doivent être identiques."), + ], + ) + submit = SubmitField('Valider') + + class AdminUpdateAccountForm(FlaskForm): username = StringField( 'Pseudonyme', diff --git a/app/routes/account/account.py b/app/routes/account/account.py index 51a4318..015a422 100644 --- a/app/routes/account/account.py +++ b/app/routes/account/account.py @@ -1,10 +1,12 @@ from flask import redirect, url_for, request, flash, abort from flask_login import login_required, current_user, logout_user from app import app, db -from app.forms.account import UpdateAccountForm, RegistrationForm, DeleteAccountForm +from app.forms.account import UpdateAccountForm, RegistrationForm, \ + DeleteAccountForm, AskResetPasswordForm, ResetPasswordForm from app.models.users import Member from app.utils.render import render -from app.utils.send_mail import send_validation_mail +from app.utils.send_mail import send_validation_mail, send_reset_password_mail +from app.utils.priv_required import guest_only import app.utils.ldap as ldap from itsdangerous import URLSafeTimedSerializer from config import V5Config @@ -35,6 +37,45 @@ def edit_account(): return render('account/account.html', form=form) +@app.route('/compte/reinitialiser', methods=['GET', 'POST']) +@guest_only +def ask_reset_password(): + form = AskResetPasswordForm() + if form.submit.data: + m = Member.query.filter_by(email=form.email.data).first() + if m is not None: + send_reset_password_mail(m.name, m.email) + flash('Un email a été envoyé à l\'adresse renseignée', 'ok') + return redirect(url_for('login')) + elif request.method == "POST": + flash('Une erreur est survenue', 'error') + + return render('account/ask_reset_password.html', form=form) + +@app.route('/compte/reinitialiser/', methods=['GET', 'POST']) +@guest_only +def reset_password(token): + try: + ts = URLSafeTimedSerializer(app.config["SECRET_KEY"]) + email = ts.loads(token, salt="email-confirm-key", max_age=86400) + except Exception as e: + print(f"Error: {e}") + abort(404) + + form = ResetPasswordForm() + if form.submit.data: + if form.validate_on_submit(): + m = Member.query.filter_by(email=email).first_or_404() + m.set_password(form.password.data) + db.session.merge(m) + db.session.commit() + flash('Modifications effectuées', 'ok') + return redirect(url_for('login')) + else: + flash('Erreur lors de la modification', 'error') + + return render('account/reset_password.html', form=form) + @app.route('/compte/supprimer', methods=['GET', 'POST']) @login_required @@ -54,9 +95,8 @@ def delete_account(): @app.route('/inscription', methods=['GET', 'POST']) +@guest_only def register(): - if current_user.is_authenticated: - return redirect(url_for('index')) form = RegistrationForm() if form.validate_on_submit(): member = Member(form.username.data, form.email.data, form.password.data) @@ -77,6 +117,7 @@ def register(): @app.route('/inscription/validation', methods=['GET']) +@guest_only def validation(): try: mail = request.args['email'] @@ -89,10 +130,8 @@ def validation(): return render('account/validation.html', mail=mail) @app.route('/inscription/validation/', methods=['GET']) +@guest_only def activate_account(token): - if current_user.is_authenticated: - return redirect(url_for('index')) - try: ts = URLSafeTimedSerializer(app.config["SECRET_KEY"]) email = ts.loads(token, salt="email-confirm-key", max_age=86400) diff --git a/app/templates/account/ask_reset_password.html b/app/templates/account/ask_reset_password.html new file mode 100644 index 0000000..631bd2f --- /dev/null +++ b/app/templates/account/ask_reset_password.html @@ -0,0 +1,22 @@ +{% extends "base/base.html" %} + +{% block content %} +
+

Réinitialiser le mot de passe

+ +

Vous recevrez un mail contenant un lien pour réintialiser votre mot de passe.

+ +
+ {{ form.hidden_tag() }} +
+ {{ form.email.label }} +
{{ form.email.description }}
+ {{ form.email() }} + {% for error in form.email.errors %} + {{ error }} + {% endfor %} +
+
{{ form.submit(class_="bg-ok") }}
+
+
+{% endblock %} diff --git a/app/templates/account/login.html b/app/templates/account/login.html index f73f83a..bd84f1b 100644 --- a/app/templates/account/login.html +++ b/app/templates/account/login.html @@ -23,5 +23,6 @@

{{ form.submit(class_="bg-ok") }}

Pas encore de compte ? Créé-en un !

- +

Mot de passe oublié ?

+ {% endblock %} diff --git a/app/templates/account/reset_password.html b/app/templates/account/reset_password.html new file mode 100644 index 0000000..a9a94d2 --- /dev/null +++ b/app/templates/account/reset_password.html @@ -0,0 +1,28 @@ +{% extends "base/base.html" %} + +{% block content %} +
+

Réinitialiser le mot de passe

+ +
+ {{ form.hidden_tag() }} +
+ {{ form.password.label }} +
{{ form.password.description }}
+ {{ form.password() }} + {% for error in form.password.errors %} + {{ error }} + {% endfor %} +
+
+ {{ form.password2.label }} +
{{ form.password2.description }}
+ {{ form.password2() }} + {% for error in form.password2.errors %} + {{ error }} + {% endfor %} +
+
{{ form.submit(class_="bg-ok") }}
+
+
+{% endblock %} diff --git a/app/templates/base/navbar/account.html b/app/templates/base/navbar/account.html index ef44842..d34cea4 100644 --- a/app/templates/base/navbar/account.html +++ b/app/templates/base/navbar/account.html @@ -55,7 +55,7 @@ {{ login_form.remember_me.label }} {{ login_form.remember_me() }}
- Mot de passe oublié ? Inscription + Mot de passe oublié ? {% endif %} diff --git a/app/templates/email/reset_password.html b/app/templates/email/reset_password.html new file mode 100644 index 0000000..92775e9 --- /dev/null +++ b/app/templates/email/reset_password.html @@ -0,0 +1,16 @@ + + +
Bonjour {{ name }},
+ +
Quelqu'un (probablement vous) a demandé a réinitialiser le mot de passe de ce compte.
+ +
Cliquez ici pour changer le mot de passe.
+
Si ça ne fonctionne pas, utilisez ce lien : {{ reset_url }}
+ +
Si vous n'êtes pas à l'origine de cette opération, vous pouvez ignorer ce message.
+ +
À bientôt sur Planète Casio !
+ +
L'équipe du site.
+ + diff --git a/app/templates/email/reset_password.md b/app/templates/email/reset_password.md new file mode 100644 index 0000000..6683f93 --- /dev/null +++ b/app/templates/email/reset_password.md @@ -0,0 +1,11 @@ +Bonjour {{ name }} + +Quelqu'un (probablement vous) a demandé a réinitialiser le mot de passe de ce compte. + +Cliquez sur ce lien pour changer votre mot de passe : {{ reset_url }} + +Si vous n'êtes pas à l'origine de cette opération, vous pouvez ignorer ce message. + +À bientôt sur Planète Casio ! + +L'équipe du site. diff --git a/app/utils/send_mail.py b/app/utils/send_mail.py index 7a32c3a..2044491 100644 --- a/app/utils/send_mail.py +++ b/app/utils/send_mail.py @@ -8,6 +8,21 @@ def send_mail(dest, subject, html="", body=""): m = Message(recipients=[dest], subject=subject, html=html, body=body) mail.send(m) +def send_reset_password_mail(name, email): + ts = URLSafeTimedSerializer(app.config["SECRET_KEY"]) + token = ts.dumps(email, salt='email-confirm-key') + reset_url = f"https://{V5Config.DOMAIN}" \ + + url_for('reset_password', token=token) + subject = "Confirmez votre email pour compléter l'inscription" + html = render_template('email/reset_password.html', reset_url=reset_url, + name=name) + body = render_template('email/reset_password.md', reset_url=reset_url, + name=name) + if V5Config.SEND_MAILS: + send_mail(member.email, subject, html=html, body=body) + else: + print(f"Reset password url: {reset_url}") + def send_validation_mail(name, email): ts = URLSafeTimedSerializer(app.config["SECRET_KEY"]) token = ts.dumps(email, salt='email-confirm-key')