Implement password reset flow

This commit is contained in:
Jonas Schäfer
2021-01-30 10:47:07 +01:00
parent 985675e012
commit 5f1d3ba307
8 changed files with 281 additions and 34 deletions

View File

@@ -57,6 +57,14 @@ async def view(id_: str) -> str:
return await render_template("invite_invalid.html")
raise
if invite.reset_localpart is not None:
return await render_template(
"invite_reset_view.html",
invite=invite,
invite_id=id_,
account_jid="{}@{}".format(invite.reset_localpart, invite.domain)
)
play_store_url = (
"https://play.google.com/store/apps/details?" +
urllib.parse.urlencode(
@@ -105,7 +113,14 @@ class RegisterForm(flask_wtf.FlaskForm): # type:ignore
@bp.route("/<id_>/register", methods=["GET", "POST"])
async def register(id_: str) -> typing.Union[str, quart.Response]:
invite = await client.get_public_invite_by_id(id_)
try:
invite = await client.get_public_invite_by_id(id_)
except aiohttp.ClientResponseError as exc:
if exc.status == 404:
return redirect(url_for(".view", id_=id_))
if invite.reset_localpart is not None:
return redirect(url_for(".reset", id_=id_))
form = RegisterForm()
if form.validate_on_submit():
@@ -144,6 +159,66 @@ async def register(id_: str) -> typing.Union[str, quart.Response]:
)
class ResetForm(flask_wtf.FlaskForm): # type:ignore
password = wtforms.PasswordField(
_l("Password"),
)
password_confirm = wtforms.PasswordField(
_l("Confirm password"),
validators=[wtforms.validators.InputRequired(),
wtforms.validators.EqualTo(
"password",
_l("The passwords must match")
)]
)
action_reset = wtforms.SubmitField(
_l("Change password")
)
@bp.route("/<id_>/reset", methods=["GET", "POST"])
async def reset(id_: str) -> typing.Union[str, quart.Response]:
try:
invite = await client.get_public_invite_by_id(id_)
except aiohttp.ClientResponseError as exc:
if exc.status == 404:
return redirect(url_for(".view", id_=id_))
if invite.reset_localpart is None:
return redirect(url_for(".register", id_=id_))
form = ResetForm()
if form.validate_on_submit():
# log the user in? show a guide? no idea.
try:
jid = await client.register_with_token(
username=invite.reset_localpart,
password=form.password.data,
token=id_,
)
except aiohttp.ClientResponseError as exc:
if exc.status == 403:
form.localpart.errors.append(
_l("Registration was declined for unknown reasons")
)
elif exc.status == 404:
return redirect(url_for(".view", id_=id_))
else:
raise
else:
http_session[INVITE_SESSION_JID] = jid
return redirect(url_for(".reset_success"))
return await render_template(
"invite_reset.html",
invite=invite,
form=form,
)
@bp.route("/success", methods=["GET", "POST"])
async def success() -> str:
return await render_template(
@@ -152,6 +227,14 @@ async def success() -> str:
)
@bp.route("/success/reset", methods=["GET", "POST"])
async def reset_success() -> str:
return await render_template(
"invite_reset_success.html",
jid=http_session.get(INVITE_SESSION_JID, ""),
)
@bp.route("/-")
async def index() -> quart.Response:
return redirect(url_for("index"))

View File

@@ -121,6 +121,8 @@ class AdminGroupInfo:
class PublicInviteInfo:
inviter: typing.Optional[str]
xmpp_uri: str
reset_localpart: typing.Optional[str]
domain: str
@classmethod
def from_api_response(
@@ -130,6 +132,8 @@ class PublicInviteInfo:
return cls(
inviter=data.get("inviter") or None,
xmpp_uri=data["uri"],
reset_localpart=data.get("reset", None),
domain=data["domain"],
)

View File

@@ -0,0 +1,35 @@
{% extends "unauth.html" %}
{% from "library.j2" import standard_button, render_errors %}
{% block style %}
{{ super() }}
<link rel="stylesheet" type="text/css" href="{{ url_for("static", filename="css/invite.css") }}">
{% endblock %}
{% block head_lead %}
{{ super() }}
<title>{% trans %}Reset your password | Snikket{% endtrans %}</title>
<script async type="text/javascript" src="{{ url_for("static", filename="js/qrcode.min.js") }}"></script>
{% endblock %}
{% block content %}
<form method="POST"><div class="form layout-expanded">
{{- form.csrf_token -}}
<h1 class="form-title">{% trans %}Reset your password online{% endtrans %}</h1>
<p class="form-desc weak">{% trans %}To reset your password online, fill out the fields below and confirm using the button.{% endtrans %}</p>
{%- call render_errors(form.errors) %}{% endcall -%}
<div class="f-ebox">
{{ form.password.label }}
{{ form.password }}
</div>
<div class="f-ebox">
{{ form.password_confirm.label }}
{{ form.password_confirm }}
</div>
<div class="f-bbox">
{%- call form_button("passwd", form.action_reset, class="primary") -%}{%- endcall -%}
</div>
</div></form>
<script type="text/javascript">
var onload = function() {
apply_qr_code(document.getElementById("qr-uri"));
};
</script>
{% endblock %}

View File

@@ -0,0 +1,14 @@
{% extends "unauth.html" %}
{% from "library.j2" import standard_button %}
{% block head_lead %}
{{ super() }}
<title>{% trans %}Password reset successful | Snikket{% endtrans %}</title>
{% endblock %}
{% block content %}
<h1>{% trans %}Password reset successful{% endtrans %}</h1>
<div class="box success">
<header>{% trans %}Your password has been changed{% endtrans %}</header>
<p>{% trans %}You can now log in using your new password.{% endtrans %}</p>
<p>{% trans %}You can now safely close this page.{% endtrans %}</p>
</div>
{% endblock %}

View File

@@ -0,0 +1,36 @@
{% extends "unauth.html" %}
{% from "library.j2" import standard_button %}
{% block style %}
{{ super() }}
<link rel="stylesheet" type="text/css" href="{{ url_for("static", filename="css/invite.css") }}">
{% endblock %}
{% block head_lead %}
{{ super() }}
<title>{% trans %}Reset your password | Snikket{% endtrans %}</title>
<script async type="text/javascript" src="{{ url_for("static", filename="js/qrcode.min.js") }}"></script>
{% endblock %}
{% block content %}
<h1>{% trans %}Reset your password{% endtrans %}</h1>
<p>{% trans account_jid=account_jid %}This page allows you to reset the password of your account, <strong>{{ account_jid }}</strong>, once.{% endtrans %}</p>
<div class="elevated el-2">
<h2>{% trans %}Using the app{% endtrans %}</h2>
<p>{% trans %}To reset your password using the Snikket App, tap the button below.{% endtrans %}</p>
<div>
{%- call standard_button("exit_to_app", invite.xmpp_uri, class="secondary") -%}
{% trans %}Open the app{% endtrans %}
{%- endcall -%}
</div>
<img class="float-right" id="tutorial-scan" aria-hidden="true" alt="" src="{{ url_for("static", filename="img/tutorial-scan.png") }}">
<p>{% trans %}Alternatively, you can scan the below code with the Snikket App using the Scan button at the top.{% endtrans %}</p>
<p>{% trans %}Your camera will turn on. Point it at the square code below until it is within the highlighted square on your screen, and wait until the app recognises it.{% endtrans %}</p>
<p>{% trans %}You will then be prompted to enter a new password for your account.{% endtrans %}</p>
<div id="qr-uri" data-qrdata="{{ invite.xmpp_uri }}" class="qr"></div>
<h2>{% trans %}Alternatives{% endtrans %}</h2>
<p>{% trans reset_url=url_for(".reset", id_=invite_id) %}You can also <a href="{{ reset_url }}">reset your password online</a> if the above button or scanning the QR code does not work for you.{% endtrans %}</p>
</div>
<script type="text/javascript">
var onload = function() {
apply_qr_code(document.getElementById("qr-uri"));
};
</script>
{% endblock %}

View File

@@ -10,7 +10,7 @@
<h1>{% trans site_name=config["SITE_NAME"] %}Successfully registered on {{ site_name }}{% endtrans %}</h1>
<div class="powered-by">{% trans logo_url=url_for("static", filename="img/snikket-logo-text.svg") %}Powered by <img src="{{ logo_url }}" alt="Snikket">{% endtrans %}</div>
<p>{% trans site_name=config["SITE_NAME"], jid=jid %}Congratulations! You successfully registered on {{ site_name }} as {{ jid }}.{% endtrans %}</p>
<input type="text" readonly="readonly" value="{{ jid }}">
<label for="address" class="a11y-only">{% trans %}Your address{% endtrans %}</label><input type="text" readonly="readonly" value="{{ jid }}" id="address">
{%- call clipboard_button(jid, show_label=True) -%}
{% trans %}Copy address{% endtrans %}
{%- endcall -%}

View File

@@ -20,7 +20,7 @@
{%- if apple_store_url -%}
<p>{% trans %}Install the Snikket App on your Android or iOS device.{% endtrans %}</p>
{%- else -%}
<p>{% trans ios_info_url="https://snikket.org/faq/#is-there-an-ios-app" %}Install the Snikket App on your Android device (<a href="{{ ios_info_url }}" target="_blank" rel="noopener noreferrer">iOS coming soon!</a>).{% endtrans %}</p>
<p>{% trans ios_info_url="https://snikket.org/faq/#is-there-an-ios-app" %}Install the Snikket App on your Android device (<a href="{{ ios_info_url }}" rel="noopener noreferrer" target="_blank">iOS coming soon!</a>).{% endtrans %}</p>
{%- endif -%}
<div class="install-buttons">
<ul>

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2021-01-29 15:58+0100\n"
"POT-Creation-Date: 2021-01-30 10:47+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"
@@ -101,38 +101,43 @@ msgstr ""
msgid "Main"
msgstr ""
#: snikket_web/invite.py:85
#: snikket_web/invite.py:93
msgid "Username"
msgstr ""
#: snikket_web/invite.py:89 snikket_web/main.py:41
#: snikket_web/invite.py:97 snikket_web/invite.py:164 snikket_web/main.py:41
msgid "Password"
msgstr ""
#: snikket_web/invite.py:93
#: snikket_web/invite.py:101 snikket_web/invite.py:168
msgid "Confirm password"
msgstr ""
#: snikket_web/invite.py:97
#: snikket_web/invite.py:105 snikket_web/invite.py:172
msgid "The passwords must match"
msgstr ""
#: snikket_web/invite.py:102
#: snikket_web/invite.py:110
msgid "Create account"
msgstr ""
#: snikket_web/invite.py:122
#: snikket_web/invite.py:137
msgid "That username is already taken"
msgstr ""
#: snikket_web/invite.py:126
#: snikket_web/invite.py:141 snikket_web/invite.py:205
msgid "Registration was declined for unknown reasons"
msgstr ""
#: snikket_web/invite.py:130
#: snikket_web/invite.py:145
msgid "The username is not valid"
msgstr ""
#: snikket_web/invite.py:177 snikket_web/templates/user_home.html:32
#: snikket_web/templates/user_passwd.html:32
msgid "Change password"
msgstr ""
#: snikket_web/main.py:36
msgid "Address"
msgstr ""
@@ -717,6 +722,7 @@ msgid "App already installed?"
msgstr ""
#: snikket_web/templates/invite_register.html:16
#: snikket_web/templates/invite_reset_view.html:20
#: snikket_web/templates/invite_view.html:39
msgid "Open the app"
msgstr ""
@@ -746,6 +752,91 @@ msgstr ""
msgid "Enter a secure password that you do not use anywhere else."
msgstr ""
#: snikket_web/templates/invite_reset.html:9
#: snikket_web/templates/invite_reset_view.html:9
msgid "Reset your password | Snikket"
msgstr ""
#: snikket_web/templates/invite_reset.html:15
msgid "Reset your password online"
msgstr ""
#: snikket_web/templates/invite_reset.html:16
msgid ""
"To reset your password online, fill out the fields below and confirm "
"using the button."
msgstr ""
#: snikket_web/templates/invite_reset_success.html:5
msgid "Password reset successful | Snikket"
msgstr ""
#: snikket_web/templates/invite_reset_success.html:8
msgid "Password reset successful"
msgstr ""
#: snikket_web/templates/invite_reset_success.html:10
msgid "Your password has been changed"
msgstr ""
#: snikket_web/templates/invite_reset_success.html:11
msgid "You can now log in using your new password."
msgstr ""
#: snikket_web/templates/invite_reset_success.html:12
#: snikket_web/templates/invite_success.html:18
msgid "You can now safely close this page."
msgstr ""
#: snikket_web/templates/invite_reset_view.html:13
msgid "Reset your password"
msgstr ""
#: snikket_web/templates/invite_reset_view.html:14
#, python-format
msgid ""
"This page allows you to reset the password of your account, "
"<strong>%(account_jid)s</strong>, once."
msgstr ""
#: snikket_web/templates/invite_reset_view.html:16
msgid "Using the app"
msgstr ""
#: snikket_web/templates/invite_reset_view.html:17
msgid "To reset your password using the Snikket App, tap the button below."
msgstr ""
#: snikket_web/templates/invite_reset_view.html:24
msgid ""
"Alternatively, you can scan the below code with the Snikket App using the"
" Scan button at the top."
msgstr ""
#: snikket_web/templates/invite_reset_view.html:25
#: snikket_web/templates/invite_view.html:75
msgid ""
"Your camera will turn on. Point it at the square code below until it is "
"within the highlighted square on your screen, and wait until the app "
"recognises it."
msgstr ""
#: snikket_web/templates/invite_reset_view.html:26
msgid "You will then be prompted to enter a new password for your account."
msgstr ""
#: snikket_web/templates/invite_reset_view.html:28
#: snikket_web/templates/invite_view.html:43
msgid "Alternatives"
msgstr ""
#: snikket_web/templates/invite_reset_view.html:29
#, python-format
msgid ""
"You can also <a href=\"%(reset_url)s\">reset your password online</a> if "
"the above button or scanning the QR code does not work for you."
msgstr ""
#: snikket_web/templates/invite_success.html:5
#, python-format
msgid "Successfully registered on %(site_name)s | Snikket"
@@ -761,16 +852,16 @@ msgstr ""
msgid "Congratulations! You successfully registered on %(site_name)s as %(jid)s."
msgstr ""
#: snikket_web/templates/invite_success.html:13
msgid "Your address"
msgstr ""
#: snikket_web/templates/invite_success.html:17
msgid ""
"You can now set up your legacy XMPP client with the above address and the"
" password you chose during registration."
msgstr ""
#: snikket_web/templates/invite_success.html:18
msgid "You can now safely close this page."
msgstr ""
#: snikket_web/templates/invite_view.html:6
#, python-format
msgid "Invite to %(site_name)s | Snikket"
@@ -802,8 +893,8 @@ msgstr ""
#, python-format
msgid ""
"Install the Snikket App on your Android device (<a "
"href=\"%(ios_info_url)s\" target=\"_blank\" rel=\"noopener "
"noreferrer\">iOS coming soon!</a>)."
"href=\"%(ios_info_url)s\" rel=\"noopener noreferrer\" "
"target=\"_blank\">iOS coming soon!</a>)."
msgstr ""
#: snikket_web/templates/invite_view.html:27
@@ -824,10 +915,6 @@ msgid ""
"create an account. If not, simply click the button below."
msgstr ""
#: snikket_web/templates/invite_view.html:43
msgid "Alternatives"
msgstr ""
#: snikket_web/templates/invite_view.html:44
#, python-format
msgid ""
@@ -872,13 +959,6 @@ msgid ""
"'Scan' button at the top."
msgstr ""
#: snikket_web/templates/invite_view.html:75
msgid ""
"Your camera will turn on. Point it at the square code below until it is "
"within the highlighted square on your screen, and wait until the app "
"recognises it."
msgstr ""
#: snikket_web/templates/library.j2:18
msgid "Copy link"
msgstr ""
@@ -932,11 +1012,6 @@ msgstr ""
msgid "Edit profile"
msgstr ""
#: snikket_web/templates/user_home.html:32
#: snikket_web/templates/user_passwd.html:32
msgid "Change password"
msgstr ""
#: snikket_web/templates/user_home.html:38
msgid "Your Snikket"
msgstr ""