From cca899bd8cccc491b853e609cded4bd1e73272fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=A4fer?= Date: Mon, 22 Mar 2021 17:44:29 +0100 Subject: [PATCH] Create "Edit user" form This aggregates the user actions behind a single "edit" button on the list view, making it less crammed. It also offers the functionality of actually editing the user, mind. Also in preparation for #42. Requires https://hg.prosody.im/prosody-modules/rev/5bc706c2db8f. --- snikket_web/admin.py | 111 +++++++++--- snikket_web/prosodyclient.py | 20 +++ snikket_web/templates/admin_delete_user.html | 2 +- snikket_web/templates/admin_edit_user.html | 47 +++++ .../templates/admin_reset_user_password.html | 4 +- snikket_web/templates/admin_users.html | 13 +- snikket_web/translations/messages.pot | 166 ++++++++++++------ 7 files changed, 269 insertions(+), 94 deletions(-) create mode 100644 snikket_web/templates/admin_edit_user.html diff --git a/snikket_web/admin.py b/snikket_web/admin.py index c1ce7d8..04c1cb5 100644 --- a/snikket_web/admin.py +++ b/snikket_web/admin.py @@ -35,7 +35,6 @@ async def index() -> str: class PasswordResetLinkPost(BaseForm): - action_create = wtforms.StringField() action_revoke = wtforms.StringField() @@ -57,6 +56,68 @@ async def users() -> str: ) +class EditUserForm(BaseForm): + localpart = wtforms.StringField( + _l("Login name"), + ) + + display_name = wtforms.StringField( + _l("Display name"), + ) + + action_save = wtforms.SubmitField( + _l("Update user"), + ) + + action_create_reset = wtforms.SubmitField( + _l("Create password reset link"), + ) + + +@bp.route("/user//", methods=["GET", "POST"]) +@client.require_admin_session() +async def edit_user(localpart: str) -> typing.Union[quart.Response, str]: + target_user_info = await client.get_user_by_localpart(localpart) + + form = EditUserForm() + if form.validate_on_submit(): + if form.action_create_reset.data: + target_user_info = await client.get_user_by_localpart(localpart) + reset_link = await client.create_password_reset_invite( + localpart=localpart, + ttl=86400, + ) + await flash( + _("Password reset link created"), + "success", + ) + return redirect(url_for( + ".user_password_reset_link", + id_=reset_link.id_, + )) + + await client.update_user( + localpart, + display_name=form.display_name.data, + ) + + await flash( + _("User information updated."), + "success", + ) + return redirect(url_for(".edit_user", localpart=localpart)) + + elif request.method == "GET": + form.localpart.data = target_user_info.localpart + form.display_name.data = target_user_info.display_name + + return await render_template( + "admin_edit_user.html", + target_user=target_user_info, + form=form, + ) + + class DeleteUserForm(BaseForm): action_delete = wtforms.SubmitField( _l("Delete user permanently") @@ -100,36 +161,38 @@ async def debug_user(localpart: str) -> typing.Union[str, quart.Response]: ) -@bp.route("/users/password-reset/-", methods=["POST"]) +@bp.route("/users/password-reset/", methods=["GET", "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, - ) +async def user_password_reset_link( + id_: str, + ) -> typing.Union[str, quart.Response]: + invite_info = await client.get_invite_by_id( + id_, + ) + if invite_info.jid is None: await flash( - _("Password reset link created"), - "success", - ) - elif form.action_revoke.data: - await client.delete_invite(form.action_revoke.data) - await flash( - _("Password reset link deleted"), - "success", + _("Password reset link not found"), + "alert", ) return redirect(url_for(".users")) + localpart = prosodyclient.split_jid(invite_info.jid)[0] + + form = PasswordResetLinkPost() + if form.validate_on_submit(): + if form.action_revoke.data: + await client.delete_invite(id_) + await flash( + _("Password reset link deleted"), + "success", + ) + return redirect(url_for(".edit_user", localpart=localpart)) + abort(400) + return await render_template( "admin_reset_user_password.html", - target_user=target_user_info, - reset_link=reset_link, + localpart=localpart, + reset_link=invite_info, form=form, ) diff --git a/snikket_web/prosodyclient.py b/snikket_web/prosodyclient.py index d7cc201..a98b767 100644 --- a/snikket_web/prosodyclient.py +++ b/snikket_web/prosodyclient.py @@ -858,6 +858,26 @@ class ProsodyClient: self._raise_error_from_response(resp) return AdminUserInfo.from_api_response(await resp.json()) + @autosession + async def update_user( + self, + localpart: str, + *, + display_name: typing.Optional[str], + session: aiohttp.ClientSession, + ) -> None: + payload = { + "username": localpart, + } + if display_name is not None: + payload["display_name"] = display_name + + async with session.put( + self._admin_v1_endpoint("/users/{}".format(localpart)), + json=payload, + ) as resp: + self._raise_error_from_response(resp) + @autosession async def get_user_debug_info( self, diff --git a/snikket_web/templates/admin_delete_user.html b/snikket_web/templates/admin_delete_user.html index 21bb8c5..6e0c645 100644 --- a/snikket_web/templates/admin_delete_user.html +++ b/snikket_web/templates/admin_delete_user.html @@ -16,7 +16,7 @@

{% trans %}The user and their data will be deleted irrevocably, permanently and immediately upon pushing the below button. There is no way back!{% endtrans %}

{% endcall %}
- {%- call standard_button("back", url_for(".index"), class="tertiary") %}{% trans %}Back{% endtrans %}{% endcall -%} + {%- call standard_button("back", url_for(".edit_user", localpart=target_user.localpart), class="tertiary") %}{% trans %}Back{% endtrans %}{% endcall -%} {%- call form_button("delete", form.action_delete, class="primary danger") %}{% endcall -%}
diff --git a/snikket_web/templates/admin_edit_user.html b/snikket_web/templates/admin_edit_user.html new file mode 100644 index 0000000..df070fb --- /dev/null +++ b/snikket_web/templates/admin_edit_user.html @@ -0,0 +1,47 @@ +{% extends "admin_app.html" %} +{% from "library.j2" import box, form_button, standard_button %} +{% block content %} +

{% trans user_name=target_user.localpart %}Edit user {{ user_name }}{% endtrans %}

+
+ {{ form.csrf_token }} +

{% trans %}Edit user{% endtrans %}

+
+ {{ form.localpart.label }} + {{ form.localpart(readonly="readonly") }} +

{% trans %}The login name cannot be changed.{% endtrans %}

+
+
+ {{ form.display_name.label }} + {{ form.display_name }} +
+
+ {%- call standard_button("back", url_for(".users"), class="tertiary") -%} + {%- trans -%}Return to user list{%- endtrans -%} + {%- endcall -%} + {%- call standard_button("delete", url_for(".delete_user", localpart=target_user.localpart), class="secondary") -%} + {%- trans -%}Delete user{%- endtrans -%} + {%- endcall -%} + {%- call form_button("done", form.action_save, class="primary") %}{% endcall -%} +
+
+

{% trans %}Further actions{% endtrans %}

+
+

{% trans %}Reset password{% endtrans %}

+ {{ form.csrf_token }} +

+ {% trans %}If the user has lost their password, you can use the button below to create a special link which allows to change the password of the account, once.{% endtrans %} +

+
+ {%- call form_button("passwd", form.action_create_reset, class="primary") -%}{%- endcall -%} +
+

{% trans %}Debug information{% endtrans %}

+

+ {% trans %}In some cases, extended information about the user account and the connected devices is necessary to troubleshoot issues. The button below reveals this (sensitive) information.{% endtrans %} +

+
+ {%- call standard_button("bug_report", url_for(".debug_user", localpart=target_user.localpart), class="primary") -%} + {%- trans -%}Show debug information{%- endtrans -%} + {%- endcall -%} +
+
+{% endblock %} diff --git a/snikket_web/templates/admin_reset_user_password.html b/snikket_web/templates/admin_reset_user_password.html index ac3fc16..3262b94 100644 --- a/snikket_web/templates/admin_reset_user_password.html +++ b/snikket_web/templates/admin_reset_user_password.html @@ -9,7 +9,7 @@
{{- form.csrf_token -}}
-

{% trans user_name=target_user.localpart %}Password reset link for {{ user_name }}{% endtrans %}

+

{% trans user_name=localpart %}Password reset link for {{ user_name }}{% endtrans %}

{% trans %}The following link will allow the user to reset their password on their device, once.{% endtrans %}

{% trans %}Valid until{% endtrans %}
@@ -21,7 +21,7 @@ {%- 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") -%} + {%- call standard_button("back", url_for(".edit_user", localpart=localpart), class="primary") -%} {% trans %}Back{% endtrans %} {%- endcall -%}
diff --git a/snikket_web/templates/admin_users.html b/snikket_web/templates/admin_users.html index 82dfeec..e25290e 100644 --- a/snikket_web/templates/admin_users.html +++ b/snikket_web/templates/admin_users.html @@ -2,8 +2,6 @@ {% from "library.j2" import action_button, value_or_hint, custom_form_button %} {% block content %}

{% trans %}Manage users{% endtrans %}

- -{{- reset_form.csrf_token -}}
@@ -18,14 +16,8 @@ @@ -33,6 +25,5 @@ {% endfor %}
{{ user.localpart }} {% call value_or_hint(user.display_name) %}{% endcall %} - {%- call action_button("delete", 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("passwd", reset_form.action_create.name, user.localpart, class="secondary", slim=True) -%} - {% trans user_name=user.localpart %}Create password reset link for {{ user_name }}{% endtrans %} + {%- call action_button("edit", url_for(".edit_user", localpart=user.localpart), class="primary") -%} + {% trans user_name=user.localpart %}Edit user {{ user_name }}{% endtrans %} {%- endcall -%}
- {%- include "admin_create_invite_form.html" -%} {% endblock %} diff --git a/snikket_web/translations/messages.pot b/snikket_web/translations/messages.pot index d7979f1..c16a963 100644 --- a/snikket_web/translations/messages.pot +++ b/snikket_web/translations/messages.pot @@ -17,131 +17,157 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.9.0\n" -#: snikket_web/admin.py:62 -msgid "Delete user permanently" +#: snikket_web/admin.py:61 snikket_web/templates/admin_delete_user.html:10 +#: snikket_web/templates/admin_users.html:8 +msgid "Login name" msgstr "" -#: snikket_web/admin.py:75 -msgid "User deleted" +#: snikket_web/admin.py:65 snikket_web/templates/admin_delete_user.html:12 +#: snikket_web/templates/admin_users.html:9 snikket_web/user.py:61 +msgid "Display name" msgstr "" -#: snikket_web/admin.py:118 +#: snikket_web/admin.py:69 +msgid "Update user" +msgstr "" + +#: snikket_web/admin.py:73 +msgid "Create password reset link" +msgstr "" + +#: snikket_web/admin.py:91 msgid "Password reset link created" msgstr "" -#: snikket_web/admin.py:124 +#: snikket_web/admin.py:105 +msgid "User information updated." +msgstr "" + +#: snikket_web/admin.py:123 +msgid "Delete user permanently" +msgstr "" + +#: snikket_web/admin.py:136 +msgid "User deleted" +msgstr "" + +#: snikket_web/admin.py:174 +msgid "Password reset link not found" +msgstr "" + +#: snikket_web/admin.py:186 msgid "Password reset link deleted" msgstr "" -#: snikket_web/admin.py:143 +#: snikket_web/admin.py:206 msgid "Invite to circle" msgstr "" -#: snikket_web/admin.py:149 +#: snikket_web/admin.py:212 msgid "At least one circle must be selected" msgstr "" -#: snikket_web/admin.py:154 +#: snikket_web/admin.py:217 msgid "Valid for" msgstr "" -#: snikket_web/admin.py:156 +#: snikket_web/admin.py:219 msgid "One hour" msgstr "" -#: snikket_web/admin.py:157 +#: snikket_web/admin.py:220 msgid "Twelve hours" msgstr "" -#: snikket_web/admin.py:158 +#: snikket_web/admin.py:221 msgid "One day" msgstr "" -#: snikket_web/admin.py:159 +#: snikket_web/admin.py:222 msgid "One week" msgstr "" -#: snikket_web/admin.py:160 +#: snikket_web/admin.py:223 msgid "Four weeks" msgstr "" -#: snikket_web/admin.py:166 snikket_web/templates/admin_edit_invite.html:17 +#: snikket_web/admin.py:229 snikket_web/templates/admin_edit_invite.html:17 msgid "Invitation type" msgstr "" -#: snikket_web/admin.py:168 snikket_web/templates/library.j2:116 +#: snikket_web/admin.py:231 snikket_web/templates/library.j2:116 msgid "Individual" msgstr "" -#: snikket_web/admin.py:169 snikket_web/templates/library.j2:114 +#: snikket_web/admin.py:232 snikket_web/templates/library.j2:114 msgid "Group" msgstr "" -#: snikket_web/admin.py:175 +#: snikket_web/admin.py:238 msgid "New invitation link" msgstr "" -#: snikket_web/admin.py:237 +#: snikket_web/admin.py:300 msgid "Revoke" msgstr "" -#: snikket_web/admin.py:261 +#: snikket_web/admin.py:324 msgid "Invitation created" msgstr "" -#: snikket_web/admin.py:277 +#: snikket_web/admin.py:340 msgid "No such invitation exists" msgstr "" -#: snikket_web/admin.py:292 +#: snikket_web/admin.py:355 msgid "Invitation revoked" msgstr "" -#: snikket_web/admin.py:309 snikket_web/admin.py:357 +#: snikket_web/admin.py:372 snikket_web/admin.py:420 msgid "Name" msgstr "" -#: snikket_web/admin.py:314 snikket_web/templates/admin_circles.html:47 +#: snikket_web/admin.py:377 snikket_web/templates/admin_circles.html:47 msgid "Create circle" msgstr "" -#: snikket_web/admin.py:344 +#: snikket_web/admin.py:407 msgid "Circle created" msgstr "" -#: snikket_web/admin.py:362 +#: snikket_web/admin.py:425 msgid "Select user" msgstr "" -#: snikket_web/admin.py:367 +#: snikket_web/admin.py:430 msgid "Update circle" msgstr "" -#: snikket_web/admin.py:371 +#: snikket_web/admin.py:434 msgid "Delete circle permanently" msgstr "" -#: snikket_web/admin.py:377 +#: snikket_web/admin.py:440 msgid "Add user" msgstr "" -#: snikket_web/admin.py:393 +#: snikket_web/admin.py:456 msgid "No such circle exists" msgstr "" -#: snikket_web/admin.py:430 +#: snikket_web/admin.py:493 msgid "Circle data updated" msgstr "" -#: snikket_web/admin.py:436 +#: snikket_web/admin.py:499 msgid "Circle deleted" msgstr "" -#: snikket_web/admin.py:447 +#: snikket_web/admin.py:510 msgid "User added to circle" msgstr "" -#: snikket_web/admin.py:456 +#: snikket_web/admin.py:519 msgid "User removed from circle" msgstr "" @@ -234,11 +260,6 @@ msgstr "" msgid "Everyone" msgstr "" -#: snikket_web/templates/admin_delete_user.html:12 -#: snikket_web/templates/admin_users.html:11 snikket_web/user.py:61 -msgid "Display name" -msgstr "" - #: snikket_web/user.py:65 msgid "Avatar" msgstr "" @@ -384,7 +405,7 @@ msgstr "" #: snikket_web/templates/admin_circles.html:15 #: snikket_web/templates/admin_invites.html:24 -#: snikket_web/templates/admin_users.html:12 +#: snikket_web/templates/admin_users.html:10 msgid "Actions" msgstr "" @@ -455,12 +476,12 @@ msgid "Copy complete output" msgstr "" #: snikket_web/templates/admin_delete_user.html:4 -#: snikket_web/templates/admin_users.html:22 #, python-format msgid "Delete user %(user_name)s" msgstr "" #: snikket_web/templates/admin_delete_user.html:6 +#: snikket_web/templates/admin_edit_user.html:22 msgid "Delete user" msgstr "" @@ -468,11 +489,6 @@ msgstr "" 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:10 -msgid "Login name" -msgstr "" - #: snikket_web/templates/admin_delete_user.html:15 msgid "Danger" msgstr "" @@ -610,6 +626,54 @@ msgstr "" msgid "Return to invitation list" msgstr "" +#: snikket_web/templates/admin_edit_user.html:4 +#: snikket_web/templates/admin_users.html:20 +#, python-format +msgid "Edit user %(user_name)s" +msgstr "" + +#: snikket_web/templates/admin_edit_user.html:7 +msgid "Edit user" +msgstr "" + +#: snikket_web/templates/admin_edit_user.html:11 +msgid "The login name cannot be changed." +msgstr "" + +#: snikket_web/templates/admin_edit_user.html:19 +msgid "Return to user list" +msgstr "" + +#: snikket_web/templates/admin_edit_user.html:27 +msgid "Further actions" +msgstr "" + +#: snikket_web/templates/admin_edit_user.html:29 +msgid "Reset password" +msgstr "" + +#: snikket_web/templates/admin_edit_user.html:32 +msgid "" +"If the user has lost their password, you can use the button below to " +"create a special link which allows to change the password of the account," +" once." +msgstr "" + +#: snikket_web/templates/admin_edit_user.html:37 +msgid "Debug information" +msgstr "" + +#: snikket_web/templates/admin_edit_user.html:39 +msgid "" +"In some cases, extended information about the user account and the " +"connected devices is necessary to troubleshoot issues. The button below " +"reveals this (sensitive) information." +msgstr "" + +#: snikket_web/templates/admin_edit_user.html:43 +msgid "Show debug information" +msgstr "" + #: snikket_web/templates/admin_home.html:4 msgid "Welcome to the admin panel!" msgstr "" @@ -704,16 +768,6 @@ msgstr "" msgid "Destroy link" msgstr "" -#: snikket_web/templates/admin_users.html:25 -#, python-format -msgid "Show debug information for %(user_name)s" -msgstr "" - -#: snikket_web/templates/admin_users.html:28 -#, python-format -msgid "Create password reset link for %(user_name)s" -msgstr "" - #: snikket_web/templates/app.html:4 msgid "Snikket Web Portal" msgstr ""