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") return await render_template("invite_invalid.html")
raise 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 = ( play_store_url = (
"https://play.google.com/store/apps/details?" + "https://play.google.com/store/apps/details?" +
urllib.parse.urlencode( urllib.parse.urlencode(
@@ -105,7 +113,14 @@ class RegisterForm(flask_wtf.FlaskForm): # type:ignore
@bp.route("/<id_>/register", methods=["GET", "POST"]) @bp.route("/<id_>/register", methods=["GET", "POST"])
async def register(id_: str) -> typing.Union[str, quart.Response]: async def register(id_: str) -> typing.Union[str, quart.Response]:
try:
invite = await client.get_public_invite_by_id(id_) 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() form = RegisterForm()
if form.validate_on_submit(): 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"]) @bp.route("/success", methods=["GET", "POST"])
async def success() -> str: async def success() -> str:
return await render_template( 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("/-") @bp.route("/-")
async def index() -> quart.Response: async def index() -> quart.Response:
return redirect(url_for("index")) return redirect(url_for("index"))

View File

@@ -121,6 +121,8 @@ class AdminGroupInfo:
class PublicInviteInfo: class PublicInviteInfo:
inviter: typing.Optional[str] inviter: typing.Optional[str]
xmpp_uri: str xmpp_uri: str
reset_localpart: typing.Optional[str]
domain: str
@classmethod @classmethod
def from_api_response( def from_api_response(
@@ -130,6 +132,8 @@ class PublicInviteInfo:
return cls( return cls(
inviter=data.get("inviter") or None, inviter=data.get("inviter") or None,
xmpp_uri=data["uri"], 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> <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> <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> <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) -%} {%- call clipboard_button(jid, show_label=True) -%}
{% trans %}Copy address{% endtrans %} {% trans %}Copy address{% endtrans %}
{%- endcall -%} {%- endcall -%}

View File

@@ -20,7 +20,7 @@
{%- if apple_store_url -%} {%- if apple_store_url -%}
<p>{% trans %}Install the Snikket App on your Android or iOS device.{% endtrans %}</p> <p>{% trans %}Install the Snikket App on your Android or iOS device.{% endtrans %}</p>
{%- else -%} {%- 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 -%} {%- endif -%}
<div class="install-buttons"> <div class="install-buttons">
<ul> <ul>

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PROJECT VERSION\n" "Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -101,38 +101,43 @@ msgstr ""
msgid "Main" msgid "Main"
msgstr "" msgstr ""
#: snikket_web/invite.py:85 #: snikket_web/invite.py:93
msgid "Username" msgid "Username"
msgstr "" 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" msgid "Password"
msgstr "" msgstr ""
#: snikket_web/invite.py:93 #: snikket_web/invite.py:101 snikket_web/invite.py:168
msgid "Confirm password" msgid "Confirm password"
msgstr "" msgstr ""
#: snikket_web/invite.py:97 #: snikket_web/invite.py:105 snikket_web/invite.py:172
msgid "The passwords must match" msgid "The passwords must match"
msgstr "" msgstr ""
#: snikket_web/invite.py:102 #: snikket_web/invite.py:110
msgid "Create account" msgid "Create account"
msgstr "" msgstr ""
#: snikket_web/invite.py:122 #: snikket_web/invite.py:137
msgid "That username is already taken" msgid "That username is already taken"
msgstr "" msgstr ""
#: snikket_web/invite.py:126 #: snikket_web/invite.py:141 snikket_web/invite.py:205
msgid "Registration was declined for unknown reasons" msgid "Registration was declined for unknown reasons"
msgstr "" msgstr ""
#: snikket_web/invite.py:130 #: snikket_web/invite.py:145
msgid "The username is not valid" msgid "The username is not valid"
msgstr "" 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 #: snikket_web/main.py:36
msgid "Address" msgid "Address"
msgstr "" msgstr ""
@@ -717,6 +722,7 @@ msgid "App already installed?"
msgstr "" msgstr ""
#: snikket_web/templates/invite_register.html:16 #: snikket_web/templates/invite_register.html:16
#: snikket_web/templates/invite_reset_view.html:20
#: snikket_web/templates/invite_view.html:39 #: snikket_web/templates/invite_view.html:39
msgid "Open the app" msgid "Open the app"
msgstr "" msgstr ""
@@ -746,6 +752,91 @@ msgstr ""
msgid "Enter a secure password that you do not use anywhere else." msgid "Enter a secure password that you do not use anywhere else."
msgstr "" 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 #: snikket_web/templates/invite_success.html:5
#, python-format #, python-format
msgid "Successfully registered on %(site_name)s | Snikket" 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." msgid "Congratulations! You successfully registered on %(site_name)s as %(jid)s."
msgstr "" msgstr ""
#: snikket_web/templates/invite_success.html:13
msgid "Your address"
msgstr ""
#: snikket_web/templates/invite_success.html:17 #: snikket_web/templates/invite_success.html:17
msgid "" msgid ""
"You can now set up your legacy XMPP client with the above address and the" "You can now set up your legacy XMPP client with the above address and the"
" password you chose during registration." " password you chose during registration."
msgstr "" msgstr ""
#: snikket_web/templates/invite_success.html:18
msgid "You can now safely close this page."
msgstr ""
#: snikket_web/templates/invite_view.html:6 #: snikket_web/templates/invite_view.html:6
#, python-format #, python-format
msgid "Invite to %(site_name)s | Snikket" msgid "Invite to %(site_name)s | Snikket"
@@ -802,8 +893,8 @@ msgstr ""
#, python-format #, python-format
msgid "" msgid ""
"Install the Snikket App on your Android device (<a " "Install the Snikket App on your Android device (<a "
"href=\"%(ios_info_url)s\" target=\"_blank\" rel=\"noopener " "href=\"%(ios_info_url)s\" rel=\"noopener noreferrer\" "
"noreferrer\">iOS coming soon!</a>)." "target=\"_blank\">iOS coming soon!</a>)."
msgstr "" msgstr ""
#: snikket_web/templates/invite_view.html:27 #: snikket_web/templates/invite_view.html:27
@@ -824,10 +915,6 @@ msgid ""
"create an account. If not, simply click the button below." "create an account. If not, simply click the button below."
msgstr "" msgstr ""
#: snikket_web/templates/invite_view.html:43
msgid "Alternatives"
msgstr ""
#: snikket_web/templates/invite_view.html:44 #: snikket_web/templates/invite_view.html:44
#, python-format #, python-format
msgid "" msgid ""
@@ -872,13 +959,6 @@ msgid ""
"'Scan' button at the top." "'Scan' button at the top."
msgstr "" 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 #: snikket_web/templates/library.j2:18
msgid "Copy link" msgid "Copy link"
msgstr "" msgstr ""
@@ -932,11 +1012,6 @@ msgstr ""
msgid "Edit profile" msgid "Edit profile"
msgstr "" 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 #: snikket_web/templates/user_home.html:38
msgid "Your Snikket" msgid "Your Snikket"
msgstr "" msgstr ""