Files
snikket-web-portal/snikket_web/user.py
2022-06-06 19:46:32 +02:00

237 lines
7.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import asyncio
import typing
import urllib
import quart.flask_patch
from quart import (
Blueprint,
Response,
render_template,
request,
redirect,
url_for,
flash,
current_app,
)
import werkzeug.exceptions
import wtforms
from flask_babel import lazy_gettext as _l, _
from .infra import client, BaseForm
bp = Blueprint('user', __name__)
class ChangePasswordForm(BaseForm):
current_password = wtforms.PasswordField(
_l("Current password"),
validators=[wtforms.validators.InputRequired()]
)
new_password = wtforms.PasswordField(
_l("New password"),
validators=[wtforms.validators.InputRequired()]
)
new_password_confirm = wtforms.PasswordField(
_l("Confirm new password"),
validators=[wtforms.validators.InputRequired(),
wtforms.validators.EqualTo(
"new_password",
_l("The new passwords must match.")
)]
)
class LogoutForm(BaseForm):
action_signout = wtforms.SubmitField(
_l("Sign out"),
)
_ACCESS_MODEL_CHOICES = [
("whitelist", _l("Nobody")),
("presence", _l("Friends only")),
("open", _l("Everyone")),
]
class ProfileForm(BaseForm):
nickname = wtforms.StringField(
_l("Display name"),
)
avatar = wtforms.FileField(
_l("Avatar")
)
profile_access_model = wtforms.RadioField(
_l("Profile visibility"),
choices=_ACCESS_MODEL_CHOICES,
)
action_save = wtforms.SubmitField(
_l("Update profile"),
)
class ImportAccountDataForm(BaseForm):
account_data_file = wtforms.FileField(
_l("Account data")
)
action_upload = wtforms.SubmitField(
_l("Upload"),
)
@bp.route("/")
@client.require_session()
async def index() -> str:
user_info = await client.get_user_info()
return await render_template("user_home.html", user_info=user_info)
@bp.route('/passwd', methods=["GET", "POST"])
@client.require_session()
async def change_pw() -> typing.Union[str, werkzeug.Response]:
form = ChangePasswordForm()
if form.validate_on_submit():
try:
await client.change_password(
form.current_password.data,
form.new_password.data,
)
except (werkzeug.exceptions.Unauthorized,
werkzeug.exceptions.Forbidden):
# server refused current password, set an appropriate error
form.current_password.errors.append(
_("Incorrect password."),
)
else:
await flash(
_("Password changed"),
"success",
)
return redirect(url_for("user.change_pw"))
return await render_template("user_passwd.html", form=form)
EAVATARTOOBIG = _l(
"The chosen avatar is too big. To be able to upload larger "
"avatars, please use the app."
)
@bp.route("/profile", methods=["GET", "POST"])
@client.require_session()
async def profile() -> typing.Union[str, werkzeug.Response]:
max_avatar_size = current_app.config["MAX_AVATAR_SIZE"]
form = ProfileForm()
if request.method != "POST":
user_info = await client.get_user_info()
# TODO: find a better way to determine the access model, e.g. by
# taking the first access model which is defined in [nickname, avatar,
# vcard] or by taking the most open one.-
profile_access_model = await client.guess_profile_access_model()
form.nickname.data = user_info.get("nickname", "")
form.profile_access_model.data = profile_access_model
if form.validate_on_submit():
user_info = await client.get_user_info()
ok = True
file_info = (await request.files).get(form.avatar.name)
if file_info is not None:
mimetype = file_info.mimetype
data = file_info.stream.read()
if len(data) > max_avatar_size:
form.avatar.errors.append(EAVATARTOOBIG)
ok = False
elif len(data) > 0:
await client.set_user_avatar(data, mimetype)
if ok:
if user_info.get("nickname") != form.nickname.data:
await client.set_user_nickname(form.nickname.data)
access_model = form.profile_access_model.data
await asyncio.gather(
client.set_avatar_access_model(access_model),
client.set_vcard_access_model(access_model),
client.set_nickname_access_model(access_model),
)
await flash(
_("Profile updated"),
"success",
)
return redirect(url_for(".profile"))
return await render_template("user_profile.html",
form=form,
max_avatar_size=max_avatar_size,
avatar_too_big_warning_header=_l("Error"),
avatar_too_big_warning=EAVATARTOOBIG)
class DataExportForm(BaseForm):
action_export = wtforms.SubmitField(
_l("Export")
)
@bp.route("/manage_data", methods=["GET", "POST"])
@client.require_session()
async def manage_data() -> typing.Union[str, quart.Response]:
form = DataExportForm()
if form.validate_on_submit():
user_info = await client.get_user_info()
# The UTF-8 version of the filename needs to be percent-encoded
encoded_address = urllib.parse.quote(
user_info["address"].encode(encoding='utf-8', errors='strict')
)
account_data = await client.export_account_data()
if account_data is None:
await flash(
_("You currently have no account data to export."),
"alert"
)
else:
return Response(account_data,
mimetype="application/xml",
headers={
# We provide the UTF-8 filename, but the ASCII
# one will be used as a fallback for legacy
# browsers (RFC 5987)
"Content-Disposition": (
'attachment; filename="account-data.xml"; '
'filename*="UTF-8\'\'account-data-{}.xml"'
).format(encoded_address)
})
return await render_template("user_manage_data.html",
form=form,
)
@bp.route("/logout", methods=["GET", "POST"])
@client.require_session()
async def logout() -> typing.Union[werkzeug.Response, str]:
form = LogoutForm()
if form.validate_on_submit():
await client.logout()
# No flashing here because we dont collect flashes in the login page
# and itd be weird.
# await flash(
# _("Logged out"),
# "success",
# )
return redirect(url_for("main.index"))
return await render_template("user_logout.html", form=form)