HTML password recovery email. Fixes #14

This commit is contained in:
Éloi Rivard 2020-10-29 12:00:19 +01:00
parent 78db589d61
commit 552728a04e
10 changed files with 228 additions and 79 deletions

View file

@ -3,9 +3,10 @@ import os
import toml
import canaille.admin
import canaille.admin.tokens
import canaille.admin.authorizations
import canaille.admin.clients
import canaille.admin.mail
import canaille.admin.tokens
import canaille.consents
import canaille.commands
import canaille.oauth
@ -141,6 +142,7 @@ def setup_app(app):
canaille.admin.authorizations.bp, url_prefix="/admin/authorization"
)
app.register_blueprint(canaille.admin.clients.bp, url_prefix="/admin/client")
app.register_blueprint(canaille.admin.mail.bp, url_prefix="/admin/mail")
babel = Babel(app)

View file

@ -104,20 +104,32 @@ def forgotten():
recipient = user.mail
base_url = current_app.config.get("URL") or request.url_root
url = base_url + url_for(
reset_url = base_url + url_for(
"canaille.account.reset",
uid=user.uid[0],
hash=profile_hash(user.uid[0], user.userPassword[0]),
)[1:]
subject = _("Password reset on {website_name}").format(
website_name=current_app.config.get("NAME", url)
website_name=current_app.config.get("NAME", reset_url)
)
text_body = render_template(
"mail/reset.txt",
site_name=current_app.config.get("NAME", reset_url),
site_url=current_app.config.get("URL", base_url),
reset_url=reset_url,
)
html_body = render_template(
"mail/reset.html",
site_name=current_app.config.get("NAME", reset_url),
site_url=current_app.config.get("URL", base_url),
reset_url=reset_url,
logo=current_app.config.get("LOGO"),
)
text_body = _(
"To reset your password on {website_name}, visit the following link :\n{url}"
).format(website_name=current_app.config.get("NAME", url), url=url)
msg = email.message.EmailMessage()
msg.set_content(text_body)
msg.add_alternative(html_body, subtype="html")
msg["Subject"] = subject
msg["From"] = current_app.config["SMTP"]["FROM_ADDR"]
msg["To"] = recipient

43
canaille/admin/mail.py Normal file
View file

@ -0,0 +1,43 @@
from flask import Blueprint, render_template, current_app, request, url_for
from canaille.flaskutils import admin_needed
from canaille.account import profile_hash
bp = Blueprint(__name__, "clients")
@bp.route("/reset.html")
@admin_needed()
def reset_html(user):
base_url = current_app.config.get("URL") or request.url_root
reset_url = base_url + url_for(
"canaille.account.reset",
uid=user.uid[0],
hash=profile_hash(user.uid[0], user.userPassword[0]),
)[1:]
return render_template(
"mail/reset.html",
site_name=current_app.config.get("NAME", reset_url),
site_url=current_app.config.get("URL", base_url),
reset_url=reset_url,
logo=current_app.config.get("LOGO"),
)
@bp.route("/reset.txt")
@admin_needed()
def reset_txt(user):
base_url = current_app.config.get("URL") or request.url_root
reset_url = base_url + url_for(
"canaille.account.reset",
uid=user.uid[0],
hash=profile_hash(user.uid[0], user.userPassword[0]),
)[1:]
return render_template(
"mail/reset.txt",
site_name=current_app.config.get("NAME", reset_url),
site_url=current_app.config.get("URL", base_url),
reset_url=reset_url,
)

View file

@ -0,0 +1,44 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "https://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="https://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width"/>
<style type="text/css" style="font-weight: 300">
@import url({{ url_for('static', filename='fonts/lato.css', _external=True) }});
</style>
<!-- <link href="/static/fomanticui/semantic.min.css" rel="stylesheet">
<style>
body {
background: #F8F8F8;
padding: 1em;
margin: auto;
max-width: 800px;
font-family: 'Lato', sans-serif;
font-weight: 400;
}
</style>-->
</head>
<body style="box-sizing: inherit; height: 100%; overflow-x: hidden; min-width: 320px; font-size: 14px; line-height: 1.4285em; color: rgba(0,0,0,.87); background: #F8F8F8; padding: 1em; margin: auto; max-width: 800px; font-family: 'Lato', sans-serif; font-weight: 400;">
<div class="ui segment" style="line-height: 1.4285em; color: rgba(0,0,0,.87); font-family: 'Lato', sans-serif; font-weight: 400; box-sizing: inherit; position: relative; background: #fff; box-shadow: 0 1px 2px 0 rgba(34,36,38,.15); margin: 1rem 0; padding: 1em 1em; border-radius: .28571429rem; border: 1px solid rgba(34,36,38,.15); font-size: 1rem; margin-top: 0; margin-bottom: 0;">
<h3 class="ui top attached header" style="box-sizing: inherit; font-family: Lato,'Helvetica Neue',Arial,Helvetica,sans-serif; font-weight: 700; line-height: 1.28571429em; text-transform: none; color: rgba(0,0,0,.87); font-size: 1.28571429rem; background: #fff; padding: .78571429rem 1rem; margin: 0 -1px 0 -1px; box-shadow: none; border: 1px solid #d4d4d5; border-radius: .28571429rem .28571429rem 0 0;">
{% if logo %}
<img class="ui image" src="{{ logo }}" alt="{{ site_name }}" style="font-family: Lato,'Helvetica Neue',Arial,Helvetica,sans-serif; font-weight: 700; line-height: 1.28571429em; text-transform: none; color: rgba(0,0,0,.87); font-size: 1.28571429rem; box-sizing: inherit; border-style: none; position: relative; max-width: 50px; max-height:50px; background-color: transparent; display: inline-block; margin-top: .14285714em; width: 2.5em; height: auto; vertical-align: middle;">
{% endif %}
<div class="content" style="font-family: Lato,'Helvetica Neue',Arial,Helvetica,sans-serif; font-weight: 700; line-height: 1.28571429em; text-transform: none; color: rgba(0,0,0,.87); font-size: 1.28571429rem; box-sizing: inherit; display: inline-block; padding-left: .75rem; vertical-align: middle;">
{% trans %}Password reinitialisation{% endtrans %}
</div>
</h3>
<div class="ui attached message" style="font-family: 'Lato', sans-serif; font-weight: 400; box-sizing: inherit; position: relative; min-height: 1em; background: #f8f8f9; padding: 1em 1.5em; line-height: 1.4285em; color: rgba(0,0,0,.87); transition: opacity .1s ease,color .1s ease,background .1s ease,box-shadow .1s ease,-webkit-box-shadow .1s ease; font-size: 1em; margin-bottom: -1px; box-shadow: 0 0 0 1px rgba(34,36,38,.15) inset; margin-left: -1px; margin-right: -1px; margin-top: -1px; border-radius: 0;">
{% trans %}
Someone, probably you, asked for a password reinitialization link at {{ site_name }}. If you did not asked for this email, please ignore it. I you need to reset your password, please click on the blue button below and follow the instructions.
{% endtrans %}
</div>
<div class="ui attached stackable buttons" style="line-height: 1.4285em; color: rgba(0,0,0,.87); font-family: 'Lato', sans-serif; font-weight: 400; box-sizing: inherit; -webkit-box-orient: horizontal; -webkit-box-direction: normal; flex-direction: row; font-size: 0; vertical-align: baseline; margin: 0 .25em 0 0; position: relative; display: flex; border-radius: 0; width: auto!important; z-index: auto; margin-left: -1px; margin-right: -1px; box-shadow: none;">
<a class="ui button" href="{{ site_url }}" style="-webkit-box-direction: normal; box-sizing: inherit; cursor: pointer; display: inline-block; min-height: 1em; outline: 0; border: none; vertical-align: baseline; background: #e0e1e2 none; color: rgba(0,0,0,.6); font-family: Lato,'Helvetica Neue',Arial,Helvetica,sans-serif; padding: .78571429em 1.5em .78571429em; text-transform: none; text-shadow: none; font-weight: 700; line-height: 1em; font-style: normal; text-align: center; text-decoration: none; user-select: none; transition: opacity .1s ease,background-color .1s ease,color .1s ease,box-shadow .1s ease,background .1s ease,-webkit-box-shadow .1s ease; will-change: auto; -webkit-tap-highlight-color: transparent; font-size: 1rem; -webkit-box-flex: 1; flex: 1 0 auto; margin: 0; border-left: none; margin-left: 0; border-radius: 0; box-shadow: 0 0 0 1px transparent inset,0 0 0 0 rgba(34,36,38,.15) inset;">{{ site_name }}</a>
<a class="ui primary button" href="{{ reset_url }}" style="-webkit-box-direction: normal; box-sizing: inherit; cursor: pointer; display: inline-block; min-height: 1em; outline: 0; border: none; vertical-align: baseline; background: #e0e1e2 none; font-family: Lato,'Helvetica Neue',Arial,Helvetica,sans-serif; padding: .78571429em 1.5em .78571429em; text-transform: none; font-weight: 700; line-height: 1em; font-style: normal; text-align: center; text-decoration: none; user-select: none; transition: opacity .1s ease,background-color .1s ease,color .1s ease,box-shadow .1s ease,background .1s ease,-webkit-box-shadow .1s ease; will-change: auto; -webkit-tap-highlight-color: transparent; font-size: 1rem; background-color: #2185d0; color: #fff; text-shadow: none; background-image: none; -webkit-box-flex: 1; flex: 1 0 auto; margin: 0; border-radius: 0; box-shadow: 0 0 0 1px transparent inset,0 0 0 0 rgba(34,36,38,.15) inset;">{% trans %}Reset password{% endtrans %}</a>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,6 @@
# {% trans %}Password reinitialisation{% endtrans %}
{% trans %}Someone, probably you, asked for a password reinitialization link at {{ site_name }}. If you did not asked for this email, please ignore it. I you need to reset your password, please click on the link below and follow the instructions.{% endtrans %}
{% trans %}Reset password{% endtrans %}: {{ reset_url }}
{{ site_name }}: {{ site_url }}

View file

@ -1,3 +1,4 @@
[python: **.py]
[jinja2: **/templates/**.html]
[jinja2: **/templates/**.txt]
extensions=jinja2.ext.autoescape,jinja2.ext.with_

View file

@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: contact@yaal.fr\n"
"POT-Creation-Date: 2020-10-29 08:31+0100\n"
"PO-Revision-Date: 2020-10-29 08:32+0100\n"
"POT-Creation-Date: 2020-10-29 11:50+0100\n"
"PO-Revision-Date: 2020-10-29 11:50+0100\n"
"Last-Translator: Éloi Rivard <eloi@yaal.fr>\n"
"Language: fr_FR\n"
"Language-Team: French - France <equipe@yaal.fr>\n"
@ -36,7 +36,7 @@ msgstr "Le profil a été mis à jour avec succès."
msgid "Could not send the password reset link."
msgstr "Impossible d'envoyer le lien de réinitialisation."
#: canaille/account.py:101 canaille/account.py:149
#: canaille/account.py:101 canaille/account.py:161
msgid "A password reset link has been sent at your email address."
msgstr "Un lien de réinitialisation vous a été envoyé à votre adresse."
@ -44,31 +44,23 @@ msgstr "Un lien de réinitialisation vous a été envoyé à votre adresse."
msgid "Password reset on {website_name}"
msgstr "Réinitialisation du mot de passe sur {website_name}"
#: canaille/account.py:115
msgid ""
"To reset your password on {website_name}, visit the following link :\n"
"{url}"
msgstr ""
"Pour réinitialiser votre mot de passe sur {website_name}, veuillez vous "
"rendre sur le lien suivant : {url}"
#: canaille/account.py:144
#: canaille/account.py:156
msgid "Could not reset your password"
msgstr "Impossible de réinitialiser votre mot de passe"
#: canaille/account.py:162
#: canaille/account.py:174
msgid "The password reset link that brought you here was invalid."
msgstr "Le lien de réinitialisation qui vous a amené ici est invalide."
#: canaille/account.py:171
#: canaille/account.py:183
msgid "Your password has been updated successfuly"
msgstr "Votre mot de passe a correctement été mis à jour."
#: canaille/consents.py:28 canaille/tokens.py:29
#: canaille/consents.py:28
msgid "Could not delete this access"
msgstr "Impossible de supprimer cet accès."
#: canaille/consents.py:32 canaille/tokens.py:34
#: canaille/consents.py:32
msgid "The access has been revoked"
msgstr "L'accès a été révoqué."
@ -238,37 +230,31 @@ msgstr "Mon profil"
msgid "My consents"
msgstr "Mes autorisations"
#: canaille/templates/base.html:47 canaille/templates/token_list.html:18
msgid "My tokens"
msgstr "Mes jetons"
#: canaille/templates/base.html:56
#: canaille/templates/base.html:51
msgid "Clients"
msgstr "Clients"
#: canaille/templates/base.html:60
#: canaille/templates/base.html:55
msgid "Tokens"
msgstr "Jetons"
#: canaille/templates/base.html:64
#: canaille/templates/base.html:59
msgid "Codes"
msgstr "Codes"
#: canaille/templates/base.html:68
#: canaille/templates/base.html:63
msgid "Consents"
msgstr "Autorisations"
#: canaille/templates/base.html:75
#: canaille/templates/base.html:70
msgid "Log out"
msgstr "Déconnexion"
#: canaille/templates/consent_list.html:21
#: canaille/templates/token_list.html:21
msgid "Consult and revoke the authorization you gave to websites."
msgstr "Consultez et révoquez les autorisation que vous avez données."
#: canaille/templates/consent_list.html:47
#: canaille/templates/token_list.html:47
msgid "From:"
msgstr "À partir de :"
@ -277,7 +263,6 @@ msgid "Revoked:"
msgstr "Révoqué le :"
#: canaille/templates/consent_list.html:52
#: canaille/templates/token_list.html:51
msgid "Has access to:"
msgstr "A accès à :"
@ -286,12 +271,10 @@ msgid "Remove access"
msgstr "Supprimer l'accès"
#: canaille/templates/consent_list.html:72
#: canaille/templates/token_list.html:71
msgid "Nothing here"
msgstr "Rien ici"
#: canaille/templates/consent_list.html:73
#: canaille/templates/token_list.html:72
msgid "You did not authorize applications yet."
msgstr ""
"Vous n'avez pas encore autorisé d'application à accéder à votre profil."
@ -368,14 +351,6 @@ msgstr "Éditer"
msgid "Password reset"
msgstr "Réinitialisation du mot de passe"
#: canaille/templates/token_list.html:48
msgid "Until:"
msgstr "Jusqu'à :"
#: canaille/templates/token_list.html:61
msgid "Remove token"
msgstr "Supprimer le jeton"
#: canaille/templates/admin/authorization_list.html:18
#: canaille/templates/admin/token_list.html:18
msgid "Token"
@ -438,6 +413,46 @@ msgstr "URL"
msgid "View a token"
msgstr "Voir un jeton"
#: canaille/templates/mail/reset.html:28 canaille/templates/mail/reset.txt:1
msgid "Password reinitialisation"
msgstr "Réinitialisation du mot de passe"
#: canaille/templates/mail/reset.html:33
#, python-format
msgid ""
"\n"
" Someone, probably you, asked for a password reinitialization "
"link at %(site_name)s. If you did not asked for this email, please ignore "
"it. I you need to reset your password, please click on the blue button below "
"and follow the instructions.\n"
" "
msgstr ""
"\n"
" Quelqu'un, probablement vous, a demandé un lien de "
"réinitialisation de votre mot de passe pour le site %(site_name)s. Si vous "
"n'êtes pas à l'origine de ce message, veuillez l'ignorer. Si vous voulez "
"réinitialiser votre mot de passe, veuillez cliquer sur le bouton bleu ci-"
"dessous, et suivre les instructions qui vous seront soumises.\n"
" "
#: canaille/templates/mail/reset.html:40 canaille/templates/mail/reset.txt:5
msgid "Reset password"
msgstr "Réinitialiser votre mot de passe"
#: canaille/templates/mail/reset.txt:3
#, python-format
msgid ""
"Someone, probably you, asked for a password reinitialization link at "
"%(site_name)s. If you did not asked for this email, please ignore it. I you "
"need to reset your password, please click on the link below and follow the "
"instructions."
msgstr ""
"Quelqu'un, probablement vous, a demandé un lien de réinitialisation de votre "
"mot de passe pour le site %(site_name)s. Si vous n'êtes pas à l'origine de "
"ce message, veuillez l'ignorer. Si vous voulez réinitialiser votre mot de "
"passe, veuillez cliquer sur le lien ci-dessous, et suivre les instructions "
"qui vous seront soumises."
#~ msgid "Logged in as"
#~ msgstr "Connecté en tant que"
@ -449,3 +464,19 @@ msgstr "Voir un jeton"
#~ msgid "OpenID Connect LDAP Bridge"
#~ msgstr "OpendID Connect LDAP Bridge"
#~ msgid ""
#~ "To reset your password on {website_name}, visit the following link :\n"
#~ "{url}"
#~ msgstr ""
#~ "Pour réinitialiser votre mot de passe sur {website_name}, veuillez vous "
#~ "rendre sur le lien suivant : {url}"
#~ msgid "My tokens"
#~ msgstr "Mes jetons"
#~ msgid "Until:"
#~ msgstr "Jusqu'à :"
#~ msgid "Remove token"
#~ msgstr "Supprimer le jeton"

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2020-10-29 08:31+0100\n"
"POT-Creation-Date: 2020-10-29 11:50+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -33,7 +33,7 @@ msgstr ""
msgid "Could not send the password reset link."
msgstr ""
#: canaille/account.py:101 canaille/account.py:149
#: canaille/account.py:101 canaille/account.py:161
msgid "A password reset link has been sent at your email address."
msgstr ""
@ -41,29 +41,23 @@ msgstr ""
msgid "Password reset on {website_name}"
msgstr ""
#: canaille/account.py:115
msgid ""
"To reset your password on {website_name}, visit the following link :\n"
"{url}"
msgstr ""
#: canaille/account.py:144
#: canaille/account.py:156
msgid "Could not reset your password"
msgstr ""
#: canaille/account.py:162
#: canaille/account.py:174
msgid "The password reset link that brought you here was invalid."
msgstr ""
#: canaille/account.py:171
#: canaille/account.py:183
msgid "Your password has been updated successfuly"
msgstr ""
#: canaille/consents.py:28 canaille/tokens.py:29
#: canaille/consents.py:28
msgid "Could not delete this access"
msgstr ""
#: canaille/consents.py:32 canaille/tokens.py:34
#: canaille/consents.py:32
msgid "The access has been revoked"
msgstr ""
@ -233,37 +227,31 @@ msgstr ""
msgid "My consents"
msgstr ""
#: canaille/templates/base.html:47 canaille/templates/token_list.html:18
msgid "My tokens"
msgstr ""
#: canaille/templates/base.html:56
#: canaille/templates/base.html:51
msgid "Clients"
msgstr ""
#: canaille/templates/base.html:60
#: canaille/templates/base.html:55
msgid "Tokens"
msgstr ""
#: canaille/templates/base.html:64
#: canaille/templates/base.html:59
msgid "Codes"
msgstr ""
#: canaille/templates/base.html:68
#: canaille/templates/base.html:63
msgid "Consents"
msgstr ""
#: canaille/templates/base.html:75
#: canaille/templates/base.html:70
msgid "Log out"
msgstr ""
#: canaille/templates/consent_list.html:21
#: canaille/templates/token_list.html:21
msgid "Consult and revoke the authorization you gave to websites."
msgstr ""
#: canaille/templates/consent_list.html:47
#: canaille/templates/token_list.html:47
msgid "From:"
msgstr ""
@ -272,7 +260,6 @@ msgid "Revoked:"
msgstr ""
#: canaille/templates/consent_list.html:52
#: canaille/templates/token_list.html:51
msgid "Has access to:"
msgstr ""
@ -281,12 +268,10 @@ msgid "Remove access"
msgstr ""
#: canaille/templates/consent_list.html:72
#: canaille/templates/token_list.html:71
msgid "Nothing here"
msgstr ""
#: canaille/templates/consent_list.html:73
#: canaille/templates/token_list.html:72
msgid "You did not authorize applications yet."
msgstr ""
@ -356,14 +341,6 @@ msgstr ""
msgid "Password reset"
msgstr ""
#: canaille/templates/token_list.html:48
msgid "Until:"
msgstr ""
#: canaille/templates/token_list.html:61
msgid "Remove token"
msgstr ""
#: canaille/templates/admin/authorization_list.html:18
#: canaille/templates/admin/token_list.html:18
msgid "Token"
@ -426,3 +403,31 @@ msgstr ""
msgid "View a token"
msgstr ""
#: canaille/templates/mail/reset.html:28 canaille/templates/mail/reset.txt:1
msgid "Password reinitialisation"
msgstr ""
#: canaille/templates/mail/reset.html:33
#, python-format
msgid ""
"\n"
" Someone, probably you, asked for a password "
"reinitialization link at %(site_name)s. If you did not asked for this "
"email, please ignore it. I you need to reset your password, please click "
"on the blue button below and follow the instructions.\n"
" "
msgstr ""
#: canaille/templates/mail/reset.html:40 canaille/templates/mail/reset.txt:5
msgid "Reset password"
msgstr ""
#: canaille/templates/mail/reset.txt:3
#, python-format
msgid ""
"Someone, probably you, asked for a password reinitialization link at "
"%(site_name)s. If you did not asked for this email, please ignore it. I "
"you need to reset your password, please click on the link below and "
"follow the instructions."
msgstr ""

5
tests/test_mail_admin.py Normal file
View file

@ -0,0 +1,5 @@
def test_reset_html(testclient, logged_admin):
testclient.get("/admin/mail/reset.html")
def test_reset_txt(testclient, logged_admin):
testclient.get("/admin/mail/reset.txt")