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, redirect,
url_for, url_for,
request, request,
abort,
) )
import flask_wtf import flask_wtf
@@ -34,6 +35,11 @@ async def index() -> str:
return await render_template("admin_home.html") 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") @bp.route("/users")
@client.require_admin_session() @client.require_admin_session()
async def users() -> str: async def users() -> str:
@@ -41,9 +47,11 @@ async def users() -> str:
await client.list_users(), await client.list_users(),
key=lambda x: x.localpart key=lambda x: x.localpart
) )
reset_form = PasswordResetLinkPost()
return await render_template( return await render_template(
"admin_users.html", "admin_users.html",
users=users, 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 class InvitesListForm(flask_wtf.FlaskForm): # type:ignore
action_revoke = wtforms.StringField() action_revoke = wtforms.StringField()
@@ -115,7 +149,7 @@ class InvitePost(flask_wtf.FlaskForm): # type:ignore
) )
reusable = wtforms.BooleanField( reusable = wtforms.BooleanField(
_l("Allow multiple uses"), _l("Invite a group of people"),
) )
action_create_invite = wtforms.SubmitField( action_create_invite = wtforms.SubmitField(
@@ -143,7 +177,11 @@ class InvitePost(flask_wtf.FlaskForm): # type:ignore
@client.require_admin_session() @client.require_admin_session()
async def invitations() -> typing.Union[str, quart.Response]: async def invitations() -> typing.Union[str, quart.Response]:
invites = sorted( 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, key=lambda x: x.created_at,
reverse=True, reverse=True,
) )
@@ -190,9 +228,14 @@ async def create_invite() -> typing.Union[str, quart.Response]:
(c.id_, c.name) for c in circles (c.id_, c.name) for c in circles
] ]
if form.validate_on_submit(): if form.validate_on_submit():
invite = await client.create_invite( 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, group_ids=form.circles.data,
reusable=form.reusable.data,
ttl=form.lifetime.data, ttl=form.lifetime.data,
) )
return redirect(url_for(".edit_invite", id_=invite.id_)) return redirect(url_for(".edit_invite", id_=invite.id_))

View File

@@ -74,6 +74,7 @@ class AdminInviteInfo:
expires: datetime expires: datetime
reusable: bool reusable: bool
group_ids: typing.Collection[str] group_ids: typing.Collection[str]
is_reset: bool
@classmethod @classmethod
def from_api_response( def from_api_response(
@@ -91,6 +92,7 @@ class AdminInviteInfo:
landing_page=data.get("landing_page"), landing_page=data.get("landing_page"),
group_ids=data.get("groups", []), group_ids=data.get("groups", []),
reusable=data["reusable"], reusable=data["reusable"],
is_reset=data.get("reset", False),
) )
@@ -857,22 +859,63 @@ class ProsodyClient:
self._raise_error_from_response(resp) self._raise_error_from_response(resp)
@autosession @autosession
async def create_invite( async def create_account_invite(
self, 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, session: aiohttp.ClientSession,
) -> AdminInviteInfo: ) -> AdminInviteInfo:
payload = { payload: typing.Dict[str, typing.Any] = {}
"reusable": reusable, payload["groups"] = list(group_ids)
"groups": list(group_ids), if restrict_username is not None:
"ttl": ttl, payload["username"] = restrict_username
} if ttl is not None:
payload["ttl"] = ttl
async with session.post( 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: json=payload) as resp:
self._raise_error_from_response(resp) self._raise_error_from_response(resp)
return AdminInviteInfo.from_api_response(await resp.json()) 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" %} {% 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 %} {% 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>
@@ -19,16 +21,21 @@
<td>{% call value_or_hint(user.display_name) %}{% endcall %}</td> <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.email) %}{% endcall %}</td>
<td class="collapsible">{% call value_or_hint(user.phone) %}{% 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") -%} {%- call action_button("remove", url_for(".delete_user", localpart=user.localpart), class="secondary") -%}
{% trans user_name=user.localpart %}Delete user {{ user_name }}{% endtrans %} {% trans user_name=user.localpart %}Delete user {{ user_name }}{% endtrans %}
{%- endcall -%} {%- endcall -%}
{%- call action_button("bug_report", url_for(".debug_user", localpart=user.localpart), class="secondary") -%} {%- 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 %} {% trans user_name=user.localpart %}Show debug information for {{ user_name }}{% endtrans %}
{%- endcall -%} {%- 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> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table></div> </table></div>
</form>
{% endblock %} {% endblock %}

View File

@@ -18,75 +18,75 @@ 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:52 #: snikket_web/admin.py:60
msgid "Delete user permanently" msgid "Delete user permanently"
msgstr "Benutzer endgültig löschen" msgstr "Benutzer endgültig löschen"
#: snikket_web/admin.py:95 #: snikket_web/admin.py:129
msgid "Invite to circle" msgid "Invite to circle"
msgstr "In Gemeinschaft einladen" msgstr "In Gemeinschaft einladen"
#: snikket_web/admin.py:101 #: snikket_web/admin.py:135
msgid "At least one circle must be selected" msgid "At least one circle must be selected"
msgstr "Mindestens eine Gemeinschaft muss ausgewählt sein" msgstr "Mindestens eine Gemeinschaft muss ausgewählt sein"
#: snikket_web/admin.py:106 #: snikket_web/admin.py:140
msgid "Valid for" msgid "Valid for"
msgstr "Gültig für" msgstr "Gültig für"
#: snikket_web/admin.py:108 #: snikket_web/admin.py:142
msgid "One hour" msgid "One hour"
msgstr "Eine Stunde" msgstr "Eine Stunde"
#: snikket_web/admin.py:109 #: snikket_web/admin.py:143
msgid "Twelve hours" msgid "Twelve hours"
msgstr "Zwölf Stunden" msgstr "Zwölf Stunden"
#: snikket_web/admin.py:110 #: snikket_web/admin.py:144
msgid "One day" msgid "One day"
msgstr "Ein Tag" msgstr "Ein Tag"
#: snikket_web/admin.py:111 #: snikket_web/admin.py:145
msgid "One week" msgid "One week"
msgstr "Eine Woche" msgstr "Eine Woche"
#: snikket_web/admin.py:112 #: snikket_web/admin.py:146
msgid "Four weeks" msgid "Four weeks"
msgstr "Vier Wochen" msgstr "Vier Wochen"
#: snikket_web/admin.py:118 #: snikket_web/admin.py:152
msgid "Allow multiple uses" msgid "Allow multiple uses"
msgstr "Mehrfach verwendbar" msgstr "Mehrfach verwendbar"
#: snikket_web/admin.py:122 #: snikket_web/admin.py:156
msgid "New invitation link" msgid "New invitation link"
msgstr "Neuer Einladungslink" msgstr "Neuer Einladungslink"
#: snikket_web/admin.py:180 #: snikket_web/admin.py:214
msgid "Revoke" msgid "Revoke"
msgstr "Löschen" 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" msgid "Name"
msgstr "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" msgid "Create circle"
msgstr "Gemeinschaft gründen" msgstr "Gemeinschaft gründen"
#: snikket_web/admin.py:279 #: snikket_web/admin.py:313
msgid "Select user" msgid "Select user"
msgstr "Benutzer auswählen" 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" msgid "Apply"
msgstr "Übernehmen" msgstr "Übernehmen"
#: snikket_web/admin.py:288 #: snikket_web/admin.py:322
msgid "Delete circle permanently" msgid "Delete circle permanently"
msgstr "Gemeinschaft endgültig löschen" msgstr "Gemeinschaft endgültig löschen"
#: snikket_web/admin.py:294 #: snikket_web/admin.py:328
msgid "Add user" msgid "Add user"
msgstr "Benutzer hinzufügen" 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:12
#: snikket_web/templates/admin_delete_user.html:16 #: 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" msgid "Display name"
msgstr "Anzeigename" msgstr "Anzeigename"
@@ -244,7 +244,7 @@ msgstr "Mitglieder"
#: snikket_web/templates/admin_circles.html:13 #: snikket_web/templates/admin_circles.html:13
#: 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:14
msgid "Actions" msgid "Actions"
msgstr "Aktionen" msgstr "Aktionen"
@@ -313,7 +313,7 @@ msgid "Copy complete output"
msgstr "Komplette Ausgabe kopieren" msgstr "Komplette Ausgabe kopieren"
#: snikket_web/templates/admin_delete_user.html:4 #: snikket_web/templates/admin_delete_user.html:4
#: snikket_web/templates/admin_users.html:24 #: snikket_web/templates/admin_users.html:26
#, python-format #, python-format
msgid "Delete user %(user_name)s" msgid "Delete user %(user_name)s"
msgstr "Benutzer %(user_name)s löschen" 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?" msgstr "Bist du sicher dass du den folgenden Benutzer löschen willst?"
#: snikket_web/templates/admin_delete_user.html:10 #: 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" msgid "Login name"
msgstr "Anmeldename" msgstr "Anmeldename"
#: snikket_web/templates/admin_delete_user.html:14 #: 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" msgid "Email address"
msgstr "E-Mail-Adresse" msgstr "E-Mail-Adresse"
@@ -344,6 +344,7 @@ msgstr "Gefahr"
#: snikket_web/templates/admin_delete_user.html:23 #: snikket_web/templates/admin_delete_user.html:23
#: snikket_web/templates/admin_edit_circle.html:15 #: snikket_web/templates/admin_edit_circle.html:15
#: snikket_web/templates/admin_edit_invite.html:45 #: 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_logout.html:13
#: snikket_web/templates/user_passwd.html:40 #: snikket_web/templates/user_passwd.html:40
#: snikket_web/templates/user_profile.html:25 #: 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_edit_invite.html:13
#: snikket_web/templates/admin_invites.html:21 #: snikket_web/templates/admin_invites.html:21
#: snikket_web/templates/admin_reset_user_password.html:15
msgid "Valid until" msgid "Valid until"
msgstr "Gültig bis" msgstr "Gültig bis"
#: snikket_web/templates/admin_edit_invite.html:15 #: snikket_web/templates/admin_edit_invite.html:15
#: snikket_web/templates/admin_reset_user_password.html:17
msgid "Link" msgid "Link"
msgstr "Link" msgstr "Link"
@@ -462,6 +465,7 @@ msgid "User information"
msgstr "Benutzerinformationen" msgstr "Benutzerinformationen"
#: snikket_web/templates/admin_edit_user.html:25 #: snikket_web/templates/admin_edit_user.html:25
#: snikket_web/templates/admin_reset_user_password.html:8
msgid "Password reset" msgid "Password reset"
msgstr "Passwort zurücksetzen" msgstr "Passwort zurücksetzen"
@@ -549,15 +553,37 @@ msgstr "Einladung löschen"
msgid "Currently, there are no pending invitations." msgid "Currently, there are no pending invitations."
msgstr "Derzeit gibt es keine ausstehenden Einladungen." 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" msgid "Phone number"
msgstr "Telefonnummer" msgstr "Telefonnummer"
#: snikket_web/templates/admin_users.html:27 #: snikket_web/templates/admin_users.html:29
#, python-format #, python-format
msgid "Show debug information for %(user_name)s" msgid "Show debug information for %(user_name)s"
msgstr "Debugging-Informationen für %(user_name)s anzeigen" 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 #: snikket_web/templates/app.html:4
msgid "Snikket Web Portal" msgid "Snikket Web Portal"
msgstr "Snikket Webportal" msgstr "Snikket Webportal"

View File

@@ -18,75 +18,75 @@ 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:52 #: snikket_web/admin.py:60
msgid "Delete user permanently" msgid "Delete user permanently"
msgstr "" msgstr ""
#: snikket_web/admin.py:95 #: snikket_web/admin.py:129
msgid "Invite to circle" msgid "Invite to circle"
msgstr "" msgstr ""
#: snikket_web/admin.py:101 #: snikket_web/admin.py:135
msgid "At least one circle must be selected" msgid "At least one circle must be selected"
msgstr "" msgstr ""
#: snikket_web/admin.py:106 #: snikket_web/admin.py:140
msgid "Valid for" msgid "Valid for"
msgstr "" msgstr ""
#: snikket_web/admin.py:108 #: snikket_web/admin.py:142
msgid "One hour" msgid "One hour"
msgstr "" msgstr ""
#: snikket_web/admin.py:109 #: snikket_web/admin.py:143
msgid "Twelve hours" msgid "Twelve hours"
msgstr "" msgstr ""
#: snikket_web/admin.py:110 #: snikket_web/admin.py:144
msgid "One day" msgid "One day"
msgstr "" msgstr ""
#: snikket_web/admin.py:111 #: snikket_web/admin.py:145
msgid "One week" msgid "One week"
msgstr "" msgstr ""
#: snikket_web/admin.py:112 #: snikket_web/admin.py:146
msgid "Four weeks" msgid "Four weeks"
msgstr "" msgstr ""
#: snikket_web/admin.py:118 #: snikket_web/admin.py:152
msgid "Allow multiple uses" msgid "Allow multiple uses"
msgstr "" msgstr ""
#: snikket_web/admin.py:122 #: snikket_web/admin.py:156
msgid "New invitation link" msgid "New invitation link"
msgstr "" msgstr ""
#: snikket_web/admin.py:180 #: snikket_web/admin.py:214
msgid "Revoke" msgid "Revoke"
msgstr "" msgstr ""
#: snikket_web/admin.py:231 snikket_web/admin.py:274 #: snikket_web/admin.py:265 snikket_web/admin.py:308
msgid "Name" msgid "Name"
msgstr "" 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" msgid "Create circle"
msgstr "" msgstr ""
#: snikket_web/admin.py:279 #: snikket_web/admin.py:313
msgid "Select user" msgid "Select user"
msgstr "" msgstr ""
#: snikket_web/admin.py:284 snikket_web/user.py:68 #: snikket_web/admin.py:318 snikket_web/user.py:68
msgid "Apply" msgid "Apply"
msgstr "" msgstr ""
#: snikket_web/admin.py:288 #: snikket_web/admin.py:322
msgid "Delete circle permanently" msgid "Delete circle permanently"
msgstr "" msgstr ""
#: snikket_web/admin.py:294 #: snikket_web/admin.py:328
msgid "Add user" msgid "Add user"
msgstr "" msgstr ""
@@ -146,7 +146,7 @@ msgstr ""
#: snikket_web/templates/admin_delete_user.html:12 #: snikket_web/templates/admin_delete_user.html:12
#: snikket_web/templates/admin_delete_user.html:16 #: 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" msgid "Display name"
msgstr "" msgstr ""
@@ -233,7 +233,7 @@ msgstr ""
#: snikket_web/templates/admin_circles.html:13 #: snikket_web/templates/admin_circles.html:13
#: 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:14
msgid "Actions" msgid "Actions"
msgstr "" msgstr ""
@@ -298,7 +298,7 @@ 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:24 #: snikket_web/templates/admin_users.html:26
#, python-format #, python-format
msgid "Delete user %(user_name)s" msgid "Delete user %(user_name)s"
msgstr "" msgstr ""
@@ -313,12 +313,12 @@ 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_delete_user.html:10
#: snikket_web/templates/admin_users.html:8 #: snikket_web/templates/admin_users.html:10
msgid "Login name" msgid "Login name"
msgstr "" msgstr ""
#: snikket_web/templates/admin_delete_user.html:14 #: snikket_web/templates/admin_delete_user.html:14
#: snikket_web/templates/admin_users.html:10 #: snikket_web/templates/admin_users.html:12
#, fuzzy #, fuzzy
msgid "Email address" msgid "Email address"
msgstr "" msgstr ""
@@ -330,6 +330,7 @@ msgstr ""
#: snikket_web/templates/admin_delete_user.html:23 #: snikket_web/templates/admin_delete_user.html:23
#: snikket_web/templates/admin_edit_circle.html:15 #: snikket_web/templates/admin_edit_circle.html:15
#: snikket_web/templates/admin_edit_invite.html:45 #: 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_logout.html:13
#: snikket_web/templates/user_passwd.html:40 #: snikket_web/templates/user_passwd.html:40
#: snikket_web/templates/user_profile.html:25 #: snikket_web/templates/user_profile.html:25
@@ -388,10 +389,12 @@ msgstr ""
#: snikket_web/templates/admin_edit_invite.html:13 #: snikket_web/templates/admin_edit_invite.html:13
#: snikket_web/templates/admin_invites.html:21 #: snikket_web/templates/admin_invites.html:21
#: snikket_web/templates/admin_reset_user_password.html:15
msgid "Valid until" msgid "Valid until"
msgstr "" msgstr ""
#: snikket_web/templates/admin_edit_invite.html:15 #: snikket_web/templates/admin_edit_invite.html:15
#: snikket_web/templates/admin_reset_user_password.html:17
msgid "Link" msgid "Link"
msgstr "" msgstr ""
@@ -440,6 +443,7 @@ msgid "User information"
msgstr "" msgstr ""
#: snikket_web/templates/admin_edit_user.html:25 #: snikket_web/templates/admin_edit_user.html:25
#: snikket_web/templates/admin_reset_user_password.html:8
#, fuzzy #, fuzzy
msgid "Password reset" msgid "Password reset"
msgstr "" msgstr ""
@@ -523,15 +527,35 @@ msgstr ""
msgid "Currently, there are no pending invitations." msgid "Currently, there are no pending invitations."
msgstr "" 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" msgid "Phone number"
msgstr "" msgstr ""
#: snikket_web/templates/admin_users.html:27 #: snikket_web/templates/admin_users.html:29
#, fuzzy, python-format #, fuzzy, python-format
msgid "Show debug information for %(user_name)s" msgid "Show debug information for %(user_name)s"
msgstr "Welcome home, %(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 #: snikket_web/templates/app.html:4
msgid "Snikket Web Portal" msgid "Snikket Web Portal"
msgstr "" msgstr ""