Password reset link support

This also includes a restructure of the admin API usage because it
was restructured upstream :).
This commit is contained in:
Jonas Schäfer
2021-01-22 16:30:38 +01:00
parent e20f893a89
commit fe43479b19
6 changed files with 241 additions and 69 deletions

View File

@@ -17,6 +17,7 @@ from quart import (
redirect,
url_for,
request,
abort,
)
import flask_wtf
@@ -34,6 +35,11 @@ async def index() -> str:
return await render_template("admin_home.html")
class PasswordResetLinkPost(flask_wtf.FlaskForm): # type: ignore
action_create = wtforms.StringField()
action_revoke = wtforms.StringField()
@bp.route("/users")
@client.require_admin_session()
async def users() -> str:
@@ -41,9 +47,11 @@ async def users() -> str:
await client.list_users(),
key=lambda x: x.localpart
)
reset_form = PasswordResetLinkPost()
return await render_template(
"admin_users.html",
users=users,
reset_form=reset_form,
)
@@ -86,6 +94,32 @@ async def debug_user(localpart: str) -> typing.Union[str, quart.Response]:
)
@bp.route("/users/password-reset/-", methods=["POST"])
@client.require_admin_session()
async def create_password_reset_link() -> typing.Union[str, quart.Response]:
form = PasswordResetLinkPost()
if not form.validate_on_submit():
abort(400)
if form.action_create.data:
localpart = form.action_create.data
target_user_info = await client.get_user_by_localpart(localpart)
reset_link = await client.create_password_reset_invite(
localpart=localpart,
ttl=86400,
)
elif form.action_revoke.data:
await client.delete_invite(form.action_revoke.data)
return redirect(url_for(".users"))
return await render_template(
"admin_reset_user_password.html",
target_user=target_user_info,
reset_link=reset_link,
form=form,
)
class InvitesListForm(flask_wtf.FlaskForm): # type:ignore
action_revoke = wtforms.StringField()
@@ -115,7 +149,7 @@ class InvitePost(flask_wtf.FlaskForm): # type:ignore
)
reusable = wtforms.BooleanField(
_l("Allow multiple uses"),
_l("Invite a group of people"),
)
action_create_invite = wtforms.SubmitField(
@@ -143,7 +177,11 @@ class InvitePost(flask_wtf.FlaskForm): # type:ignore
@client.require_admin_session()
async def invitations() -> typing.Union[str, quart.Response]:
invites = sorted(
await client.list_invites(),
(
invite
for invite in await client.list_invites()
if not invite.is_reset
),
key=lambda x: x.created_at,
reverse=True,
)
@@ -190,11 +228,16 @@ async def create_invite() -> typing.Union[str, quart.Response]:
(c.id_, c.name) for c in circles
]
if form.validate_on_submit():
invite = await client.create_invite(
group_ids=form.circles.data,
reusable=form.reusable.data,
ttl=form.lifetime.data,
)
if form.reusable.data:
invite = await client.create_group_invite(
group_ids=form.circles.data,
ttl=form.lifetime.data,
)
else:
invite = await client.create_account_invite(
group_ids=form.circles.data,
ttl=form.lifetime.data,
)
return redirect(url_for(".edit_invite", id_=invite.id_))
return await render_template("admin_create_invite.html",
invite_form=form)

View File

@@ -74,6 +74,7 @@ class AdminInviteInfo:
expires: datetime
reusable: bool
group_ids: typing.Collection[str]
is_reset: bool
@classmethod
def from_api_response(
@@ -91,6 +92,7 @@ class AdminInviteInfo:
landing_page=data.get("landing_page"),
group_ids=data.get("groups", []),
reusable=data["reusable"],
is_reset=data.get("reset", False),
)
@@ -857,22 +859,63 @@ class ProsodyClient:
self._raise_error_from_response(resp)
@autosession
async def create_invite(
async def create_account_invite(
self,
group_ids: typing.Collection[str],
reusable: bool,
ttl: int,
*,
group_ids: typing.Collection[str] = [],
restrict_username: typing.Optional[str] = None,
ttl: typing.Optional[int] = None,
session: aiohttp.ClientSession,
) -> AdminInviteInfo:
payload = {
"reusable": reusable,
"groups": list(group_ids),
"ttl": ttl,
}
payload: typing.Dict[str, typing.Any] = {}
payload["groups"] = list(group_ids)
if restrict_username is not None:
payload["username"] = restrict_username
if ttl is not None:
payload["ttl"] = ttl
async with session.post(
self._admin_v1_endpoint("/invites"),
self._admin_v1_endpoint("/invites/account"),
json=payload) as resp:
self._raise_error_from_response(resp)
return AdminInviteInfo.from_api_response(await resp.json())
@autosession
async def create_group_invite(
self,
*,
group_ids: typing.Collection[str] = [],
ttl: typing.Optional[int] = None,
session: aiohttp.ClientSession,
) -> AdminInviteInfo:
payload: typing.Dict[str, typing.Any] = {
"groups": list(group_ids),
}
if ttl is not None:
payload["ttl"] = ttl
async with session.post(
self._admin_v1_endpoint("/invites/group"),
json=payload) as resp:
self._raise_error_from_response(resp)
return AdminInviteInfo.from_api_response(await resp.json())
@autosession
async def create_password_reset_invite(
self,
*,
localpart: str,
ttl: typing.Optional[int] = None,
session: aiohttp.ClientSession,
) -> AdminInviteInfo:
payload: typing.Dict[str, typing.Any] = {
"username": localpart,
}
if ttl is not None:
payload["ttl"] = ttl
async with session.post(
self._admin_v1_endpoint("/invites/reset"),
json=payload) as resp:
self._raise_error_from_response(resp)
return AdminInviteInfo.from_api_response(await resp.json())

View File

@@ -0,0 +1,29 @@
{% extends "app.html" %}
{% from "library.j2" import showuri, standard_button, custom_form_button %}
{% block head_lead %}
{{ super() }}
{% include "copy-snippet.html" %}
{% endblock %}
{% block content %}
<h1>{% trans %}Password reset{% endtrans %}</h1>
<form method="POST">
{{- form.csrf_token -}}
<div class="form layout-expanded">
<h2 class="form-title">{% trans user_name=target_user.localpart %}Password reset link for {{ user_name }}{% endtrans %}</h2>
<p class="form-desc">{% trans %}The following link will allow the user to reset their password on their device, once.{% endtrans %}</p>
<dd>
<dt>{% trans %}Valid until{% endtrans %}</dt>
<dd>{{ reset_link.expires | format_date }}</dd>
<dt>{% trans %}Link{% endtrans %}</dt>
<dd>{% call showuri(reset_link.landing_page) %}{% endcall %}</dd>
</dd>
<div class="f-bbox">
{%- call custom_form_button("remove_link", form.action_revoke.name, reset_link.id_, class="secondary danger") -%}
{% trans %}Destroy link{% endtrans %}
{%- endcall -%}
{%- call standard_button("back", url_for(".users"), class="primary") -%}
{% trans %}Back{% endtrans %}
{%- endcall -%}
</div>
</div></form>
{% endblock %}

View File

@@ -1,7 +1,9 @@
{% extends "admin_app.html" %}
{% from "library.j2" import action_button, value_or_hint %}
{% from "library.j2" import action_button, value_or_hint, custom_form_button %}
{% block content %}
<h1>{% trans %}Manage users{% endtrans %}</h1>
<form method="POST" action="{{ url_for(".create_password_reset_link") }}">
{{- reset_form.csrf_token -}}
<div class="elevated el-2"><table>
<thead>
<tr>
@@ -19,16 +21,21 @@
<td>{% call value_or_hint(user.display_name) %}{% endcall %}</td>
<td class="collapsible">{% call value_or_hint(user.email) %}{% endcall %}</td>
<td class="collapsible">{% call value_or_hint(user.phone) %}{% endcall %}</td>
<td>
<td class="nowrap">
{%- call action_button("remove", url_for(".delete_user", localpart=user.localpart), class="secondary") -%}
{% trans user_name=user.localpart %}Delete user {{ user_name }}{% endtrans %}
{%- endcall -%}
{%- call action_button("bug_report", url_for(".debug_user", localpart=user.localpart), class="secondary") -%}
{% trans user_name=user.localpart %}Show debug information for {{ user_name }}{% endtrans %}
{%- endcall -%}
{%- call custom_form_button("create_link", reset_form.action_create.name, user.localpart, class="secondary", slim=True) -%}
{% trans user_name=user.localpart %}Create password reset link for {{ user_name }}{% endtrans %}
{%- endcall -%}
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table></div>
</form>
{% endblock %}

View File

@@ -18,75 +18,75 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.9.0\n"
#: snikket_web/admin.py:52
#: snikket_web/admin.py:60
msgid "Delete user permanently"
msgstr "Benutzer endgültig löschen"
#: snikket_web/admin.py:95
#: snikket_web/admin.py:129
msgid "Invite to circle"
msgstr "In Gemeinschaft einladen"
#: snikket_web/admin.py:101
#: snikket_web/admin.py:135
msgid "At least one circle must be selected"
msgstr "Mindestens eine Gemeinschaft muss ausgewählt sein"
#: snikket_web/admin.py:106
#: snikket_web/admin.py:140
msgid "Valid for"
msgstr "Gültig für"
#: snikket_web/admin.py:108
#: snikket_web/admin.py:142
msgid "One hour"
msgstr "Eine Stunde"
#: snikket_web/admin.py:109
#: snikket_web/admin.py:143
msgid "Twelve hours"
msgstr "Zwölf Stunden"
#: snikket_web/admin.py:110
#: snikket_web/admin.py:144
msgid "One day"
msgstr "Ein Tag"
#: snikket_web/admin.py:111
#: snikket_web/admin.py:145
msgid "One week"
msgstr "Eine Woche"
#: snikket_web/admin.py:112
#: snikket_web/admin.py:146
msgid "Four weeks"
msgstr "Vier Wochen"
#: snikket_web/admin.py:118
#: snikket_web/admin.py:152
msgid "Allow multiple uses"
msgstr "Mehrfach verwendbar"
#: snikket_web/admin.py:122
#: snikket_web/admin.py:156
msgid "New invitation link"
msgstr "Neuer Einladungslink"
#: snikket_web/admin.py:180
#: snikket_web/admin.py:214
msgid "Revoke"
msgstr "Löschen"
#: snikket_web/admin.py:231 snikket_web/admin.py:274
#: snikket_web/admin.py:265 snikket_web/admin.py:308
msgid "Name"
msgstr "Name"
#: snikket_web/admin.py:235 snikket_web/templates/admin_circles.html:42
#: snikket_web/admin.py:269 snikket_web/templates/admin_circles.html:42
msgid "Create circle"
msgstr "Gemeinschaft gründen"
#: snikket_web/admin.py:279
#: snikket_web/admin.py:313
msgid "Select user"
msgstr "Benutzer auswählen"
#: snikket_web/admin.py:284 snikket_web/user.py:68
#: snikket_web/admin.py:318 snikket_web/user.py:68
msgid "Apply"
msgstr "Übernehmen"
#: snikket_web/admin.py:288
#: snikket_web/admin.py:322
msgid "Delete circle permanently"
msgstr "Gemeinschaft endgültig löschen"
#: snikket_web/admin.py:294
#: snikket_web/admin.py:328
msgid "Add user"
msgstr "Benutzer hinzufügen"
@@ -144,7 +144,7 @@ msgstr "Jeder"
#: snikket_web/templates/admin_delete_user.html:12
#: snikket_web/templates/admin_delete_user.html:16
#: snikket_web/templates/admin_users.html:9 snikket_web/user.py:55
#: snikket_web/templates/admin_users.html:11 snikket_web/user.py:55
msgid "Display name"
msgstr "Anzeigename"
@@ -244,7 +244,7 @@ msgstr "Mitglieder"
#: snikket_web/templates/admin_circles.html:13
#: snikket_web/templates/admin_invites.html:24
#: snikket_web/templates/admin_users.html:12
#: snikket_web/templates/admin_users.html:14
msgid "Actions"
msgstr "Aktionen"
@@ -313,7 +313,7 @@ msgid "Copy complete output"
msgstr "Komplette Ausgabe kopieren"
#: snikket_web/templates/admin_delete_user.html:4
#: snikket_web/templates/admin_users.html:24
#: snikket_web/templates/admin_users.html:26
#, python-format
msgid "Delete user %(user_name)s"
msgstr "Benutzer %(user_name)s löschen"
@@ -328,12 +328,12 @@ msgid "Are you sure you want to delete the following user?"
msgstr "Bist du sicher dass du den folgenden Benutzer löschen willst?"
#: snikket_web/templates/admin_delete_user.html:10
#: snikket_web/templates/admin_users.html:8
#: snikket_web/templates/admin_users.html:10
msgid "Login name"
msgstr "Anmeldename"
#: snikket_web/templates/admin_delete_user.html:14
#: snikket_web/templates/admin_users.html:10
#: snikket_web/templates/admin_users.html:12
msgid "Email address"
msgstr "E-Mail-Adresse"
@@ -344,6 +344,7 @@ msgstr "Gefahr"
#: snikket_web/templates/admin_delete_user.html:23
#: snikket_web/templates/admin_edit_circle.html:15
#: snikket_web/templates/admin_edit_invite.html:45
#: snikket_web/templates/admin_reset_user_password.html:25
#: snikket_web/templates/user_logout.html:13
#: snikket_web/templates/user_passwd.html:40
#: snikket_web/templates/user_profile.html:25
@@ -404,10 +405,12 @@ msgstr "Einladung anzeigen"
#: snikket_web/templates/admin_edit_invite.html:13
#: snikket_web/templates/admin_invites.html:21
#: snikket_web/templates/admin_reset_user_password.html:15
msgid "Valid until"
msgstr "Gültig bis"
#: snikket_web/templates/admin_edit_invite.html:15
#: snikket_web/templates/admin_reset_user_password.html:17
msgid "Link"
msgstr "Link"
@@ -462,6 +465,7 @@ msgid "User information"
msgstr "Benutzerinformationen"
#: snikket_web/templates/admin_edit_user.html:25
#: snikket_web/templates/admin_reset_user_password.html:8
msgid "Password reset"
msgstr "Passwort zurücksetzen"
@@ -549,15 +553,37 @@ msgstr "Einladung löschen"
msgid "Currently, there are no pending invitations."
msgstr "Derzeit gibt es keine ausstehenden Einladungen."
#: snikket_web/templates/admin_users.html:11
#: snikket_web/templates/admin_reset_user_password.html:12
#, python-format
msgid "Password reset link for %(user_name)s"
msgstr "Link zum Zurücksetzen des Passwortes für %(user_name)s"
#: snikket_web/templates/admin_reset_user_password.html:13
msgid ""
"The following link will allow the user to reset their password on their "
"device, once."
msgstr ""
"Der folgende Link erlaubt es, das Passwort für das Nutzerkonto einmalig "
"zurückzusetzen."
#: snikket_web/templates/admin_reset_user_password.html:22
msgid "Destroy link"
msgstr "Link zerstören"
#: snikket_web/templates/admin_users.html:13
msgid "Phone number"
msgstr "Telefonnummer"
#: snikket_web/templates/admin_users.html:27
#: snikket_web/templates/admin_users.html:29
#, python-format
msgid "Show debug information for %(user_name)s"
msgstr "Debugging-Informationen für %(user_name)s anzeigen"
#: snikket_web/templates/admin_users.html:32
#, python-format
msgid "Create password reset link for %(user_name)s"
msgstr "Benutzer %(user_name)s löschen"
#: snikket_web/templates/app.html:4
msgid "Snikket Web Portal"
msgstr "Snikket Webportal"

View File

@@ -18,75 +18,75 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.9.0\n"
#: snikket_web/admin.py:52
#: snikket_web/admin.py:60
msgid "Delete user permanently"
msgstr ""
#: snikket_web/admin.py:95
#: snikket_web/admin.py:129
msgid "Invite to circle"
msgstr ""
#: snikket_web/admin.py:101
#: snikket_web/admin.py:135
msgid "At least one circle must be selected"
msgstr ""
#: snikket_web/admin.py:106
#: snikket_web/admin.py:140
msgid "Valid for"
msgstr ""
#: snikket_web/admin.py:108
#: snikket_web/admin.py:142
msgid "One hour"
msgstr ""
#: snikket_web/admin.py:109
#: snikket_web/admin.py:143
msgid "Twelve hours"
msgstr ""
#: snikket_web/admin.py:110
#: snikket_web/admin.py:144
msgid "One day"
msgstr ""
#: snikket_web/admin.py:111
#: snikket_web/admin.py:145
msgid "One week"
msgstr ""
#: snikket_web/admin.py:112
#: snikket_web/admin.py:146
msgid "Four weeks"
msgstr ""
#: snikket_web/admin.py:118
#: snikket_web/admin.py:152
msgid "Allow multiple uses"
msgstr ""
#: snikket_web/admin.py:122
#: snikket_web/admin.py:156
msgid "New invitation link"
msgstr ""
#: snikket_web/admin.py:180
#: snikket_web/admin.py:214
msgid "Revoke"
msgstr ""
#: snikket_web/admin.py:231 snikket_web/admin.py:274
#: snikket_web/admin.py:265 snikket_web/admin.py:308
msgid "Name"
msgstr ""
#: snikket_web/admin.py:235 snikket_web/templates/admin_circles.html:42
#: snikket_web/admin.py:269 snikket_web/templates/admin_circles.html:42
msgid "Create circle"
msgstr ""
#: snikket_web/admin.py:279
#: snikket_web/admin.py:313
msgid "Select user"
msgstr ""
#: snikket_web/admin.py:284 snikket_web/user.py:68
#: snikket_web/admin.py:318 snikket_web/user.py:68
msgid "Apply"
msgstr ""
#: snikket_web/admin.py:288
#: snikket_web/admin.py:322
msgid "Delete circle permanently"
msgstr ""
#: snikket_web/admin.py:294
#: snikket_web/admin.py:328
msgid "Add user"
msgstr ""
@@ -146,7 +146,7 @@ msgstr ""
#: snikket_web/templates/admin_delete_user.html:12
#: snikket_web/templates/admin_delete_user.html:16
#: snikket_web/templates/admin_users.html:9 snikket_web/user.py:55
#: snikket_web/templates/admin_users.html:11 snikket_web/user.py:55
msgid "Display name"
msgstr ""
@@ -233,7 +233,7 @@ msgstr ""
#: snikket_web/templates/admin_circles.html:13
#: snikket_web/templates/admin_invites.html:24
#: snikket_web/templates/admin_users.html:12
#: snikket_web/templates/admin_users.html:14
msgid "Actions"
msgstr ""
@@ -298,7 +298,7 @@ msgid "Copy complete output"
msgstr ""
#: snikket_web/templates/admin_delete_user.html:4
#: snikket_web/templates/admin_users.html:24
#: snikket_web/templates/admin_users.html:26
#, python-format
msgid "Delete user %(user_name)s"
msgstr ""
@@ -313,12 +313,12 @@ msgid "Are you sure you want to delete the following user?"
msgstr ""
#: snikket_web/templates/admin_delete_user.html:10
#: snikket_web/templates/admin_users.html:8
#: snikket_web/templates/admin_users.html:10
msgid "Login name"
msgstr ""
#: snikket_web/templates/admin_delete_user.html:14
#: snikket_web/templates/admin_users.html:10
#: snikket_web/templates/admin_users.html:12
#, fuzzy
msgid "Email address"
msgstr ""
@@ -330,6 +330,7 @@ msgstr ""
#: snikket_web/templates/admin_delete_user.html:23
#: snikket_web/templates/admin_edit_circle.html:15
#: snikket_web/templates/admin_edit_invite.html:45
#: snikket_web/templates/admin_reset_user_password.html:25
#: snikket_web/templates/user_logout.html:13
#: snikket_web/templates/user_passwd.html:40
#: snikket_web/templates/user_profile.html:25
@@ -388,10 +389,12 @@ msgstr ""
#: snikket_web/templates/admin_edit_invite.html:13
#: snikket_web/templates/admin_invites.html:21
#: snikket_web/templates/admin_reset_user_password.html:15
msgid "Valid until"
msgstr ""
#: snikket_web/templates/admin_edit_invite.html:15
#: snikket_web/templates/admin_reset_user_password.html:17
msgid "Link"
msgstr ""
@@ -440,6 +443,7 @@ msgid "User information"
msgstr ""
#: snikket_web/templates/admin_edit_user.html:25
#: snikket_web/templates/admin_reset_user_password.html:8
#, fuzzy
msgid "Password reset"
msgstr ""
@@ -523,15 +527,35 @@ msgstr ""
msgid "Currently, there are no pending invitations."
msgstr ""
#: snikket_web/templates/admin_users.html:11
#: snikket_web/templates/admin_reset_user_password.html:12
#, fuzzy, python-format
msgid "Password reset link for %(user_name)s"
msgstr "Welcome home, %(user_name)s."
#: snikket_web/templates/admin_reset_user_password.html:13
msgid ""
"The following link will allow the user to reset their password on their "
"device, once."
msgstr ""
#: snikket_web/templates/admin_reset_user_password.html:22
msgid "Destroy link"
msgstr ""
#: snikket_web/templates/admin_users.html:13
msgid "Phone number"
msgstr ""
#: snikket_web/templates/admin_users.html:27
#: snikket_web/templates/admin_users.html:29
#, fuzzy, python-format
msgid "Show debug information for %(user_name)s"
msgstr "Welcome home, %(user_name)s."
#: snikket_web/templates/admin_users.html:32
#, python-format
msgid "Create password reset link for %(user_name)s"
msgstr ""
#: snikket_web/templates/app.html:4
msgid "Snikket Web Portal"
msgstr ""