Use standard error rendering for the login form

This provides a consistent UX.
This commit is contained in:
Jonas Schäfer
2021-03-20 13:05:14 +01:00
parent b822000f2e
commit ad229d6700
7 changed files with 96 additions and 97 deletions

View File

@@ -19,12 +19,11 @@ from quart import (
abort, abort,
flash, flash,
) )
import flask_wtf
from flask_babel import lazy_gettext as _l, _ from flask_babel import lazy_gettext as _l, _
from . import prosodyclient from . import prosodyclient
from .infra import client, circle_name from .infra import client, circle_name, BaseForm
bp = Blueprint("admin", __name__, url_prefix="/admin") bp = Blueprint("admin", __name__, url_prefix="/admin")
@@ -35,7 +34,7 @@ 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 class PasswordResetLinkPost(BaseForm):
action_create = wtforms.StringField() action_create = wtforms.StringField()
action_revoke = wtforms.StringField() action_revoke = wtforms.StringField()
@@ -55,7 +54,7 @@ async def users() -> str:
) )
class DeleteUserForm(flask_wtf.FlaskForm): # type:ignore class DeleteUserForm(BaseForm):
action_delete = wtforms.SubmitField( action_delete = wtforms.SubmitField(
_l("Delete user permanently") _l("Delete user permanently")
) )
@@ -132,11 +131,11 @@ async def create_password_reset_link() -> typing.Union[str, quart.Response]:
) )
class InvitesListForm(flask_wtf.FlaskForm): # type:ignore class InvitesListForm(BaseForm):
action_revoke = wtforms.StringField() action_revoke = wtforms.StringField()
class InvitePost(flask_wtf.FlaskForm): # type:ignore class InvitePost(BaseForm):
circles = wtforms.SelectMultipleField( circles = wtforms.SelectMultipleField(
_l("Invite to circle"), _l("Invite to circle"),
# NOTE: This is for when/if we ever support multi-group invites. # NOTE: This is for when/if we ever support multi-group invites.
@@ -230,7 +229,7 @@ async def invitations() -> typing.Union[str, quart.Response]:
) )
class InviteForm(flask_wtf.FlaskForm): # type:ignore class InviteForm(BaseForm):
action_revoke = wtforms.SubmitField( action_revoke = wtforms.SubmitField(
_l("Revoke") _l("Revoke")
) )
@@ -302,7 +301,7 @@ async def edit_invite(id_: str) -> typing.Union[str, quart.Response]:
) )
class CirclePost(flask_wtf.FlaskForm): # type:ignore class CirclePost(BaseForm):
name = wtforms.StringField( name = wtforms.StringField(
_l("Name"), _l("Name"),
validators=[wtforms.validators.InputRequired()], validators=[wtforms.validators.InputRequired()],
@@ -350,7 +349,7 @@ async def create_circle() -> typing.Union[str, quart.Response]:
) )
class EditCircleForm(flask_wtf.FlaskForm): # type:ignore class EditCircleForm(BaseForm):
name = wtforms.StringField( name = wtforms.StringField(
_l("Name"), _l("Name"),
validators=[wtforms.validators.InputRequired()], validators=[wtforms.validators.InputRequired()],

View File

@@ -10,6 +10,7 @@ from quart import (
) )
import flask_babel import flask_babel
import flask_wtf
from flask_babel import _ from flask_babel import _
from . import prosodyclient from . import prosodyclient
@@ -55,3 +56,14 @@ def generate_error_id() -> str:
return base64.b32encode(secrets.token_bytes(8)).decode( return base64.b32encode(secrets.token_bytes(8)).decode(
"ascii" "ascii"
).rstrip("=") ).rstrip("=")
class BaseForm(flask_wtf.FlaskForm): # type:ignore
def __init__(self, *args: typing.Any, **kwargs: typing.Any):
meta = kwargs["meta"] = dict(kwargs.get("meta", {}))
if "locales" not in meta:
locale = flask_babel.get_locale()
if locale:
meta["locales"] = [str(locale)]
super().__init__(*args, **kwargs)

View File

@@ -16,10 +16,9 @@ from quart import (
import wtforms import wtforms
import flask_wtf
from flask_babel import lazy_gettext as _l from flask_babel import lazy_gettext as _l
from .infra import client, selected_locale from .infra import client, selected_locale, BaseForm
bp = Blueprint("invite", __name__) bp = Blueprint("invite", __name__)
@@ -102,7 +101,7 @@ async def view(id_: str) -> typing.Union[quart.Response,
) )
class RegisterForm(flask_wtf.FlaskForm): # type:ignore class RegisterForm(BaseForm):
localpart = wtforms.StringField( localpart = wtforms.StringField(
_l("Username"), _l("Username"),
) )
@@ -173,7 +172,7 @@ async def register(id_: str) -> typing.Union[str, quart.Response]:
) )
class ResetForm(flask_wtf.FlaskForm): # type:ignore class ResetForm(BaseForm):
password = wtforms.PasswordField( password = wtforms.PasswordField(
_l("Password"), _l("Password"),
) )

View File

@@ -22,17 +22,16 @@ import babel
import wtforms import wtforms
import flask_wtf import flask_wtf
from flask_babel import lazy_gettext as _l, _ from flask_babel import lazy_gettext as _l, _
from . import xmpputil, _version from . import xmpputil, _version
from .infra import client from .infra import client, BaseForm
bp = quart.Blueprint("main", __name__) bp = quart.Blueprint("main", __name__)
class LoginForm(flask_wtf.FlaskForm): # type:ignore class LoginForm(BaseForm):
address = wtforms.TextField( address = wtforms.TextField(
_l("Address"), _l("Address"),
validators=[wtforms.validators.InputRequired()], validators=[wtforms.validators.InputRequired()],

View File

@@ -1,5 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% from "library.j2" import box, form_button %} {% from "library.j2" import box, form_button, render_errors %}
{% set body_id = "login" %} {% set body_id = "login" %}
{% block head_lead %} {% block head_lead %}
<title>{{ _("Snikket Login") }}</title> <title>{{ _("Snikket Login") }}</title>
@@ -14,11 +14,7 @@
<p class="form-desc">{{ _("Enter your Snikket address and password to manage your account.") }}</p> <p class="form-desc">{{ _("Enter your Snikket address and password to manage your account.") }}</p>
<form method="POST" action="{{ url_for('.login') }}" name="login" id="login-form" onsubmit="return domainCheck();" data-addressid="{{ form.address.id }}" data-domain="{{ config["SNIKKET_DOMAIN"] }}"> <form method="POST" action="{{ url_for('.login') }}" name="login" id="login-form" onsubmit="return domainCheck();" data-addressid="{{ form.address.id }}" data-domain="{{ config["SNIKKET_DOMAIN"] }}">
{{ form.csrf_token }} {{ form.csrf_token }}
{% if form.errors %} {% call render_errors(form) %}{% endcall %}
{% call box("alert", _("Login failed")) %}
<p>{{ form.errors.values() | flatten | join(", ")}}</p>
{% endcall %}
{% endif %}
<div class="box alert" role="alert" style="display: none;" id="id-warning"> <div class="box alert" role="alert" style="display: none;" id="id-warning">
<header>{% trans %}Incorrect address{% endtrans %}</header> <header>{% trans %}Incorrect address{% endtrans %}</header>
<p>{% trans snikket_domain=config["SNIKKET_DOMAIN"] %}This Snikket service only hosts addresses ending in <em>@{{ snikket_domain }}</em>. Your password was not sent.{% endtrans %}</p> <p>{% trans snikket_domain=config["SNIKKET_DOMAIN"] %}This Snikket service only hosts addresses ending in <em>@{{ snikket_domain }}</em>. Your password was not sent.{% endtrans %}</p>

View File

@@ -17,259 +17,259 @@ 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:60 #: snikket_web/admin.py:59
msgid "Delete user permanently" msgid "Delete user permanently"
msgstr "" msgstr ""
#: snikket_web/admin.py:73 #: snikket_web/admin.py:72
msgid "User deleted" msgid "User deleted"
msgstr "" msgstr ""
#: snikket_web/admin.py:116 #: snikket_web/admin.py:115
msgid "Password reset link created" msgid "Password reset link created"
msgstr "" msgstr ""
#: snikket_web/admin.py:122 #: snikket_web/admin.py:121
msgid "Password reset link deleted" msgid "Password reset link deleted"
msgstr "" msgstr ""
#: snikket_web/admin.py:141 #: snikket_web/admin.py:140
msgid "Invite to circle" msgid "Invite to circle"
msgstr "" msgstr ""
#: snikket_web/admin.py:147 #: snikket_web/admin.py:146
msgid "At least one circle must be selected" msgid "At least one circle must be selected"
msgstr "" msgstr ""
#: snikket_web/admin.py:152 #: snikket_web/admin.py:151
msgid "Valid for" msgid "Valid for"
msgstr "" msgstr ""
#: snikket_web/admin.py:154 #: snikket_web/admin.py:153
msgid "One hour" msgid "One hour"
msgstr "" msgstr ""
#: snikket_web/admin.py:155 #: snikket_web/admin.py:154
msgid "Twelve hours" msgid "Twelve hours"
msgstr "" msgstr ""
#: snikket_web/admin.py:156 #: snikket_web/admin.py:155
msgid "One day" msgid "One day"
msgstr "" msgstr ""
#: snikket_web/admin.py:157 #: snikket_web/admin.py:156
msgid "One week" msgid "One week"
msgstr "" msgstr ""
#: snikket_web/admin.py:158 #: snikket_web/admin.py:157
msgid "Four weeks" msgid "Four weeks"
msgstr "" msgstr ""
#: snikket_web/admin.py:164 snikket_web/templates/admin_edit_invite.html:17 #: snikket_web/admin.py:163 snikket_web/templates/admin_edit_invite.html:17
msgid "Invitation type" msgid "Invitation type"
msgstr "" msgstr ""
#: snikket_web/admin.py:166 snikket_web/templates/library.j2:116 #: snikket_web/admin.py:165 snikket_web/templates/library.j2:116
msgid "Individual" msgid "Individual"
msgstr "" msgstr ""
#: snikket_web/admin.py:167 snikket_web/templates/library.j2:114 #: snikket_web/admin.py:166 snikket_web/templates/library.j2:114
msgid "Group" msgid "Group"
msgstr "" msgstr ""
#: snikket_web/admin.py:173 #: snikket_web/admin.py:172
msgid "New invitation link" msgid "New invitation link"
msgstr "" msgstr ""
#: snikket_web/admin.py:235 #: snikket_web/admin.py:234
msgid "Revoke" msgid "Revoke"
msgstr "" msgstr ""
#: snikket_web/admin.py:259 #: snikket_web/admin.py:258
msgid "Invitation created" msgid "Invitation created"
msgstr "" msgstr ""
#: snikket_web/admin.py:275 #: snikket_web/admin.py:274
msgid "No such invitation exists" msgid "No such invitation exists"
msgstr "" msgstr ""
#: snikket_web/admin.py:290 #: snikket_web/admin.py:289
msgid "Invitation revoked" msgid "Invitation revoked"
msgstr "" msgstr ""
#: snikket_web/admin.py:307 snikket_web/admin.py:355 #: snikket_web/admin.py:306 snikket_web/admin.py:354
msgid "Name" msgid "Name"
msgstr "" msgstr ""
#: snikket_web/admin.py:312 snikket_web/templates/admin_circles.html:47 #: snikket_web/admin.py:311 snikket_web/templates/admin_circles.html:47
msgid "Create circle" msgid "Create circle"
msgstr "" msgstr ""
#: snikket_web/admin.py:342 #: snikket_web/admin.py:341
msgid "Circle created" msgid "Circle created"
msgstr "" msgstr ""
#: snikket_web/admin.py:360 #: snikket_web/admin.py:359
msgid "Select user" msgid "Select user"
msgstr "" msgstr ""
#: snikket_web/admin.py:365 #: snikket_web/admin.py:364
msgid "Update circle" msgid "Update circle"
msgstr "" msgstr ""
#: snikket_web/admin.py:369 #: snikket_web/admin.py:368
msgid "Delete circle permanently" msgid "Delete circle permanently"
msgstr "" msgstr ""
#: snikket_web/admin.py:375 #: snikket_web/admin.py:374
msgid "Add user" msgid "Add user"
msgstr "" msgstr ""
#: snikket_web/admin.py:391 #: snikket_web/admin.py:390
msgid "No such circle exists" msgid "No such circle exists"
msgstr "" msgstr ""
#: snikket_web/admin.py:428 #: snikket_web/admin.py:427
msgid "Circle data updated" msgid "Circle data updated"
msgstr "" msgstr ""
#: snikket_web/admin.py:434 #: snikket_web/admin.py:433
msgid "Circle deleted" msgid "Circle deleted"
msgstr "" msgstr ""
#: snikket_web/admin.py:445 #: snikket_web/admin.py:444
msgid "User added to circle" msgid "User added to circle"
msgstr "" msgstr ""
#: snikket_web/admin.py:454 #: snikket_web/admin.py:453
msgid "User removed from circle" msgid "User removed from circle"
msgstr "" msgstr ""
#: snikket_web/infra.py:40 #: snikket_web/infra.py:41
msgid "Main" msgid "Main"
msgstr "" msgstr ""
#: snikket_web/invite.py:107 #: snikket_web/invite.py:106
msgid "Username" msgid "Username"
msgstr "" msgstr ""
#: snikket_web/invite.py:111 snikket_web/invite.py:178 snikket_web/main.py:42 #: snikket_web/invite.py:110 snikket_web/invite.py:177 snikket_web/main.py:41
msgid "Password" msgid "Password"
msgstr "" msgstr ""
#: snikket_web/invite.py:115 snikket_web/invite.py:182 #: snikket_web/invite.py:114 snikket_web/invite.py:181
msgid "Confirm password" msgid "Confirm password"
msgstr "" msgstr ""
#: snikket_web/invite.py:119 snikket_web/invite.py:186 #: snikket_web/invite.py:118 snikket_web/invite.py:185
msgid "The passwords must match" msgid "The passwords must match"
msgstr "" msgstr ""
#: snikket_web/invite.py:124 #: snikket_web/invite.py:123
msgid "Create account" msgid "Create account"
msgstr "" msgstr ""
#: snikket_web/invite.py:151 #: snikket_web/invite.py:150
msgid "That username is already taken." msgid "That username is already taken."
msgstr "" msgstr ""
#: snikket_web/invite.py:155 snikket_web/invite.py:219 #: snikket_web/invite.py:154 snikket_web/invite.py:218
msgid "Registration was declined for unknown reasons." msgid "Registration was declined for unknown reasons."
msgstr "" msgstr ""
#: snikket_web/invite.py:159 #: snikket_web/invite.py:158
msgid "The username is not valid." msgid "The username is not valid."
msgstr "" msgstr ""
#: snikket_web/invite.py:191 snikket_web/templates/user_home.html:32 #: snikket_web/invite.py:190 snikket_web/templates/user_home.html:32
#: snikket_web/templates/user_passwd.html:29 #: snikket_web/templates/user_passwd.html:29
msgid "Change password" msgid "Change password"
msgstr "" msgstr ""
#: snikket_web/main.py:37 #: snikket_web/main.py:36
msgid "Address" msgid "Address"
msgstr "" msgstr ""
#: snikket_web/main.py:47 #: snikket_web/main.py:46
msgid "Sign in" msgid "Sign in"
msgstr "" msgstr ""
#: snikket_web/main.py:56 #: snikket_web/main.py:55
msgid "Invalid username or password." msgid "Invalid username or password."
msgstr "" msgstr ""
#: snikket_web/main.py:84 #: snikket_web/main.py:83
msgid "Login successful!" msgid "Login successful!"
msgstr "" msgstr ""
#: snikket_web/user.py:29 #: snikket_web/user.py:27
msgid "Current password" msgid "Current password"
msgstr "" msgstr ""
#: snikket_web/user.py:34 #: snikket_web/user.py:32
msgid "New password" msgid "New password"
msgstr "" msgstr ""
#: snikket_web/user.py:39 #: snikket_web/user.py:37
msgid "Confirm new password" msgid "Confirm new password"
msgstr "" msgstr ""
#: snikket_web/user.py:43 #: snikket_web/user.py:41
msgid "The new passwords must match" msgid "The new passwords must match"
msgstr "" msgstr ""
#: snikket_web/user.py:50 #: snikket_web/user.py:48
msgid "Sign out" msgid "Sign out"
msgstr "" msgstr ""
#: snikket_web/user.py:55 #: snikket_web/user.py:53
msgid "Nobody" msgid "Nobody"
msgstr "" msgstr ""
#: snikket_web/user.py:56 #: snikket_web/user.py:54
msgid "Friends only" msgid "Friends only"
msgstr "" msgstr ""
#: snikket_web/user.py:57 #: snikket_web/user.py:55
msgid "Everyone" msgid "Everyone"
msgstr "" msgstr ""
#: snikket_web/templates/admin_delete_user.html:12 #: snikket_web/templates/admin_delete_user.html:12
#: snikket_web/templates/admin_users.html:11 snikket_web/user.py:63 #: snikket_web/templates/admin_users.html:11 snikket_web/user.py:61
msgid "Display name" msgid "Display name"
msgstr "" msgstr ""
#: snikket_web/user.py:67 #: snikket_web/user.py:65
msgid "Avatar" msgid "Avatar"
msgstr "" msgstr ""
#: snikket_web/user.py:71 #: snikket_web/user.py:69
msgid "Profile visibility" msgid "Profile visibility"
msgstr "" msgstr ""
#: snikket_web/user.py:76 #: snikket_web/user.py:74
msgid "Update profile" msgid "Update profile"
msgstr "" msgstr ""
#: snikket_web/user.py:101 #: snikket_web/user.py:99
msgid "Incorrect password." msgid "Incorrect password."
msgstr "" msgstr ""
#: snikket_web/user.py:105 #: snikket_web/user.py:103
msgid "Password changed" msgid "Password changed"
msgstr "" msgstr ""
#: snikket_web/user.py:113 #: snikket_web/user.py:111
msgid "" msgid ""
"The chosen avatar is too big. To be able to upload larger avatars, please" "The chosen avatar is too big. To be able to upload larger avatars, please"
" use the app." " use the app."
msgstr "" msgstr ""
#: snikket_web/user.py:161 #: snikket_web/user.py:159
msgid "Profile updated" msgid "Profile updated"
msgstr "" msgstr ""
#: snikket_web/templates/unauth.html:18 snikket_web/user.py:169 #: snikket_web/templates/unauth.html:18 snikket_web/user.py:167
msgid "Error" msgid "Error"
msgstr "" msgstr ""
@@ -1106,15 +1106,11 @@ msgstr ""
msgid "Enter your Snikket address and password to manage your account." msgid "Enter your Snikket address and password to manage your account."
msgstr "" msgstr ""
#: snikket_web/templates/login.html:18 #: snikket_web/templates/login.html:19
msgid "Login failed"
msgstr ""
#: snikket_web/templates/login.html:23
msgid "Incorrect address" msgid "Incorrect address"
msgstr "" msgstr ""
#: snikket_web/templates/login.html:24 #: snikket_web/templates/login.html:20
#, python-format #, python-format
msgid "" msgid ""
"This Snikket service only hosts addresses ending in " "This Snikket service only hosts addresses ending in "

View File

@@ -15,16 +15,14 @@ import quart.exceptions
import wtforms import wtforms
import flask_wtf
from flask_babel import lazy_gettext as _l, _ from flask_babel import lazy_gettext as _l, _
from .infra import client from .infra import client, BaseForm
bp = Blueprint('user', __name__) bp = Blueprint('user', __name__)
class ChangePasswordForm(flask_wtf.FlaskForm): # type:ignore class ChangePasswordForm(BaseForm):
current_password = wtforms.PasswordField( current_password = wtforms.PasswordField(
_l("Current password"), _l("Current password"),
validators=[wtforms.validators.InputRequired()] validators=[wtforms.validators.InputRequired()]
@@ -45,7 +43,7 @@ class ChangePasswordForm(flask_wtf.FlaskForm): # type:ignore
) )
class LogoutForm(flask_wtf.FlaskForm): # type:ignore class LogoutForm(BaseForm):
action_signout = wtforms.SubmitField( action_signout = wtforms.SubmitField(
_l("Sign out"), _l("Sign out"),
) )
@@ -58,7 +56,7 @@ _ACCESS_MODEL_CHOICES = [
] ]
class ProfileForm(flask_wtf.FlaskForm): # type:ignore class ProfileForm(BaseForm):
nickname = wtforms.TextField( nickname = wtforms.TextField(
_l("Display name"), _l("Display name"),
) )