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.
This commit is contained in:
Jonas Schäfer
2021-03-22 17:44:29 +01:00
parent 359e6b4ce2
commit cca899bd8c
7 changed files with 269 additions and 94 deletions

View File

@@ -35,7 +35,6 @@ async def index() -> str:
class PasswordResetLinkPost(BaseForm): class PasswordResetLinkPost(BaseForm):
action_create = wtforms.StringField()
action_revoke = 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/<localpart>/", 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): class DeleteUserForm(BaseForm):
action_delete = wtforms.SubmitField( action_delete = wtforms.SubmitField(
_l("Delete user permanently") _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/<id_>", methods=["GET", "POST"])
@client.require_admin_session() @client.require_admin_session()
async def create_password_reset_link() -> typing.Union[str, quart.Response]: async def user_password_reset_link(
form = PasswordResetLinkPost() id_: str,
if not form.validate_on_submit(): ) -> typing.Union[str, quart.Response]:
abort(400) invite_info = await client.get_invite_by_id(
id_,
if form.action_create.data: )
localpart = form.action_create.data if invite_info.jid is None:
target_user_info = await client.get_user_by_localpart(localpart)
reset_link = await client.create_password_reset_invite(
localpart=localpart,
ttl=86400,
)
await flash( await flash(
_("Password reset link created"), _("Password reset link not found"),
"success", "alert",
)
elif form.action_revoke.data:
await client.delete_invite(form.action_revoke.data)
await flash(
_("Password reset link deleted"),
"success",
) )
return redirect(url_for(".users")) 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( return await render_template(
"admin_reset_user_password.html", "admin_reset_user_password.html",
target_user=target_user_info, localpart=localpart,
reset_link=reset_link, reset_link=invite_info,
form=form, form=form,
) )

View File

@@ -858,6 +858,26 @@ class ProsodyClient:
self._raise_error_from_response(resp) self._raise_error_from_response(resp)
return AdminUserInfo.from_api_response(await resp.json()) 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 @autosession
async def get_user_debug_info( async def get_user_debug_info(
self, self,

View File

@@ -16,7 +16,7 @@
<p>{% trans %}The user and their data will be deleted irrevocably, permanently and immediately upon pushing the below button. <strong>There is no way back!</strong>{% endtrans %}</p> <p>{% trans %}The user and their data will be deleted irrevocably, permanently and immediately upon pushing the below button. <strong>There is no way back!</strong>{% endtrans %}</p>
{% endcall %} {% endcall %}
<div class="f-bbox"> <div class="f-bbox">
{%- 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 -%} {%- call form_button("delete", form.action_delete, class="primary danger") %}{% endcall -%}
</div> </div>
</form></div> </form></div>

View File

@@ -0,0 +1,47 @@
{% extends "admin_app.html" %}
{% from "library.j2" import box, form_button, standard_button %}
{% block content %}
<h1>{% trans user_name=target_user.localpart %}Edit user {{ user_name }}{% endtrans %}</h1>
<div class="form layout-expanded"><form method="POST">
{{ form.csrf_token }}
<h2 class="form-title">{% trans %}Edit user{% endtrans %}</h2>
<div class="f-ebox">
{{ form.localpart.label }}
{{ form.localpart(readonly="readonly") }}
<p class="form-desc weak">{% trans %}The login name cannot be changed.{% endtrans %}</p>
</div>
<div class="f-ebox">
{{ form.display_name.label }}
{{ form.display_name }}
</div>
<div class="f-bbox">
{%- 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 -%}
</div>
</form></div>
<h2>{% trans %}Further actions{% endtrans %}</h2>
<div class="form layout-expanded"><form method="POST">
<h2 class="form-title">{% trans %}Reset password{% endtrans %}</h2>
{{ form.csrf_token }}
<p class="form-desc">
{% 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 %}
</p>
<div class="f-bbox">
{%- call form_button("passwd", form.action_create_reset, class="primary") -%}{%- endcall -%}
</div>
<h2 class="form-title">{% trans %}Debug information{% endtrans %}</h2>
<p class="form-desc">
{% 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 %}
</p>
<div class="f-bbox">
{%- call standard_button("bug_report", url_for(".debug_user", localpart=target_user.localpart), class="primary") -%}
{%- trans -%}Show debug information{%- endtrans -%}
{%- endcall -%}
</div>
</form></div>
{% endblock %}

View File

@@ -9,7 +9,7 @@
<form method="POST"> <form method="POST">
{{- form.csrf_token -}} {{- form.csrf_token -}}
<div class="form layout-expanded"> <div class="form layout-expanded">
<h2 class="form-title">{% trans user_name=target_user.localpart %}Password reset link for {{ user_name }}{% endtrans %}</h2> <h2 class="form-title">{% trans user_name=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> <p class="form-desc">{% trans %}The following link will allow the user to reset their password on their device, once.{% endtrans %}</p>
<dd> <dd>
<dt>{% trans %}Valid until{% endtrans %}</dt> <dt>{% trans %}Valid until{% endtrans %}</dt>
@@ -21,7 +21,7 @@
{%- call custom_form_button("remove_link", form.action_revoke.name, reset_link.id_, class="secondary danger") -%} {%- call custom_form_button("remove_link", form.action_revoke.name, reset_link.id_, class="secondary danger") -%}
{% trans %}Destroy link{% endtrans %} {% trans %}Destroy link{% endtrans %}
{%- endcall -%} {%- 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 %} {% trans %}Back{% endtrans %}
{%- endcall -%} {%- endcall -%}
</div> </div>

View File

@@ -2,8 +2,6 @@
{% from "library.j2" import action_button, value_or_hint, custom_form_button %} {% from "library.j2" import action_button, value_or_hint, custom_form_button %}
{% block content %} {% block content %}
<h1>{% trans %}Manage users{% endtrans %}</h1> <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> <div class="elevated el-2"><table>
<thead> <thead>
<tr> <tr>
@@ -18,14 +16,8 @@
<td>{{ user.localpart }}</td> <td>{{ user.localpart }}</td>
<td>{% call value_or_hint(user.display_name) %}{% endcall %}</td> <td>{% call value_or_hint(user.display_name) %}{% endcall %}</td>
<td class="nowrap"> <td class="nowrap">
{%- call action_button("delete", url_for(".delete_user", localpart=user.localpart), class="secondary") -%} {%- call action_button("edit", url_for(".edit_user", localpart=user.localpart), class="primary") -%}
{% trans user_name=user.localpart %}Delete user {{ user_name }}{% endtrans %} {% trans user_name=user.localpart %}Edit 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 %}
{%- endcall -%} {%- endcall -%}
</form> </form>
</td> </td>
@@ -33,6 +25,5 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table></div> </table></div>
</form>
{%- include "admin_create_invite_form.html" -%} {%- include "admin_create_invite_form.html" -%}
{% endblock %} {% endblock %}

View File

@@ -17,131 +17,157 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.9.0\n" "Generated-By: Babel 2.9.0\n"
#: snikket_web/admin.py:62 #: snikket_web/admin.py:61 snikket_web/templates/admin_delete_user.html:10
msgid "Delete user permanently" #: snikket_web/templates/admin_users.html:8
msgid "Login name"
msgstr "" msgstr ""
#: snikket_web/admin.py:75 #: snikket_web/admin.py:65 snikket_web/templates/admin_delete_user.html:12
msgid "User deleted" #: snikket_web/templates/admin_users.html:9 snikket_web/user.py:61
msgid "Display name"
msgstr "" 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" msgid "Password reset link created"
msgstr "" 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" msgid "Password reset link deleted"
msgstr "" msgstr ""
#: snikket_web/admin.py:143 #: snikket_web/admin.py:206
msgid "Invite to circle" msgid "Invite to circle"
msgstr "" msgstr ""
#: snikket_web/admin.py:149 #: snikket_web/admin.py:212
msgid "At least one circle must be selected" msgid "At least one circle must be selected"
msgstr "" msgstr ""
#: snikket_web/admin.py:154 #: snikket_web/admin.py:217
msgid "Valid for" msgid "Valid for"
msgstr "" msgstr ""
#: snikket_web/admin.py:156 #: snikket_web/admin.py:219
msgid "One hour" msgid "One hour"
msgstr "" msgstr ""
#: snikket_web/admin.py:157 #: snikket_web/admin.py:220
msgid "Twelve hours" msgid "Twelve hours"
msgstr "" msgstr ""
#: snikket_web/admin.py:158 #: snikket_web/admin.py:221
msgid "One day" msgid "One day"
msgstr "" msgstr ""
#: snikket_web/admin.py:159 #: snikket_web/admin.py:222
msgid "One week" msgid "One week"
msgstr "" msgstr ""
#: snikket_web/admin.py:160 #: snikket_web/admin.py:223
msgid "Four weeks" msgid "Four weeks"
msgstr "" 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" msgid "Invitation type"
msgstr "" 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" msgid "Individual"
msgstr "" 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" msgid "Group"
msgstr "" msgstr ""
#: snikket_web/admin.py:175 #: snikket_web/admin.py:238
msgid "New invitation link" msgid "New invitation link"
msgstr "" msgstr ""
#: snikket_web/admin.py:237 #: snikket_web/admin.py:300
msgid "Revoke" msgid "Revoke"
msgstr "" msgstr ""
#: snikket_web/admin.py:261 #: snikket_web/admin.py:324
msgid "Invitation created" msgid "Invitation created"
msgstr "" msgstr ""
#: snikket_web/admin.py:277 #: snikket_web/admin.py:340
msgid "No such invitation exists" msgid "No such invitation exists"
msgstr "" msgstr ""
#: snikket_web/admin.py:292 #: snikket_web/admin.py:355
msgid "Invitation revoked" msgid "Invitation revoked"
msgstr "" msgstr ""
#: snikket_web/admin.py:309 snikket_web/admin.py:357 #: snikket_web/admin.py:372 snikket_web/admin.py:420
msgid "Name" msgid "Name"
msgstr "" 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" msgid "Create circle"
msgstr "" msgstr ""
#: snikket_web/admin.py:344 #: snikket_web/admin.py:407
msgid "Circle created" msgid "Circle created"
msgstr "" msgstr ""
#: snikket_web/admin.py:362 #: snikket_web/admin.py:425
msgid "Select user" msgid "Select user"
msgstr "" msgstr ""
#: snikket_web/admin.py:367 #: snikket_web/admin.py:430
msgid "Update circle" msgid "Update circle"
msgstr "" msgstr ""
#: snikket_web/admin.py:371 #: snikket_web/admin.py:434
msgid "Delete circle permanently" msgid "Delete circle permanently"
msgstr "" msgstr ""
#: snikket_web/admin.py:377 #: snikket_web/admin.py:440
msgid "Add user" msgid "Add user"
msgstr "" msgstr ""
#: snikket_web/admin.py:393 #: snikket_web/admin.py:456
msgid "No such circle exists" msgid "No such circle exists"
msgstr "" msgstr ""
#: snikket_web/admin.py:430 #: snikket_web/admin.py:493
msgid "Circle data updated" msgid "Circle data updated"
msgstr "" msgstr ""
#: snikket_web/admin.py:436 #: snikket_web/admin.py:499
msgid "Circle deleted" msgid "Circle deleted"
msgstr "" msgstr ""
#: snikket_web/admin.py:447 #: snikket_web/admin.py:510
msgid "User added to circle" msgid "User added to circle"
msgstr "" msgstr ""
#: snikket_web/admin.py:456 #: snikket_web/admin.py:519
msgid "User removed from circle" msgid "User removed from circle"
msgstr "" msgstr ""
@@ -234,11 +260,6 @@ msgstr ""
msgid "Everyone" msgid "Everyone"
msgstr "" 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 #: snikket_web/user.py:65
msgid "Avatar" msgid "Avatar"
msgstr "" msgstr ""
@@ -384,7 +405,7 @@ msgstr ""
#: snikket_web/templates/admin_circles.html:15 #: snikket_web/templates/admin_circles.html:15
#: snikket_web/templates/admin_invites.html:24 #: snikket_web/templates/admin_invites.html:24
#: snikket_web/templates/admin_users.html:12 #: snikket_web/templates/admin_users.html:10
msgid "Actions" msgid "Actions"
msgstr "" msgstr ""
@@ -455,12 +476,12 @@ msgid "Copy complete output"
msgstr "" msgstr ""
#: snikket_web/templates/admin_delete_user.html:4 #: snikket_web/templates/admin_delete_user.html:4
#: snikket_web/templates/admin_users.html:22
#, python-format #, python-format
msgid "Delete user %(user_name)s" msgid "Delete user %(user_name)s"
msgstr "" msgstr ""
#: snikket_web/templates/admin_delete_user.html:6 #: snikket_web/templates/admin_delete_user.html:6
#: snikket_web/templates/admin_edit_user.html:22
msgid "Delete user" msgid "Delete user"
msgstr "" msgstr ""
@@ -468,11 +489,6 @@ msgstr ""
msgid "Are you sure you want to delete the following user?" msgid "Are you sure you want to delete the following user?"
msgstr "" 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 #: snikket_web/templates/admin_delete_user.html:15
msgid "Danger" msgid "Danger"
msgstr "" msgstr ""
@@ -610,6 +626,54 @@ msgstr ""
msgid "Return to invitation list" msgid "Return to invitation list"
msgstr "" 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 #: snikket_web/templates/admin_home.html:4
msgid "Welcome to the admin panel!" msgid "Welcome to the admin panel!"
msgstr "" msgstr ""
@@ -704,16 +768,6 @@ msgstr ""
msgid "Destroy link" msgid "Destroy link"
msgstr "" 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 #: snikket_web/templates/app.html:4
msgid "Snikket Web Portal" msgid "Snikket Web Portal"
msgstr "" msgstr ""