From 4e9ef6fb00211378ca7373b0e41ee29479c9aa44 Mon Sep 17 00:00:00 2001
From: Kevin Morris
Date: Thu, 28 Jan 2021 20:34:27 -0800
Subject: [PATCH] add account edit (settings) routes
* Added account_url filter to jinja2 environment. This produces a path
to the user's account url (/account/{username}).
* Updated archdev-navbar to link to new edit route.
+ Added migrate_cookies(request, response) to aurweb.util, a function
that simply migrates the request cookies to response and returns it.
+ Added account_edit tests to test_accounts_routes.py.
Signed-off-by: Kevin Morris
---
aurweb/routers/accounts.py | 145 ++++++++++++
aurweb/templates.py | 5 +-
aurweb/util.py | 6 +
templates/account/edit.html | 46 ++++
templates/partials/account_form.html | 9 +
templates/partials/archdev-navbar.html | 20 +-
test/test_accounts_routes.py | 296 +++++++++++++++++++++++++
7 files changed, 522 insertions(+), 5 deletions(-)
create mode 100644 templates/account/edit.html
diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py
index a43ba9f7..689f7f58 100644
--- a/aurweb/routers/accounts.py
+++ b/aurweb/routers/accounts.py
@@ -1,5 +1,6 @@
import copy
+from datetime import datetime
from http import HTTPStatus
from fastapi import APIRouter, Form, Request
@@ -284,6 +285,7 @@ def make_account_form_context(context: dict,
context["cn"] = args.get("CN", user.CommentNotify)
context["un"] = args.get("UN", user.UpdateNotify)
context["on"] = args.get("ON", user.OwnershipNotify)
+ context["inactive"] = args.get("J", user.InactivityTS != 0)
else:
context["username"] = args.get("U", str())
context["account_type"] = args.get("T", user_account_type_id)
@@ -301,6 +303,7 @@ def make_account_form_context(context: dict,
context["cn"] = args.get("CN", True)
context["un"] = args.get("UN", False)
context["on"] = args.get("ON", True)
+ context["inactive"] = args.get("J", False)
context["password"] = args.get("P", str())
context["confirm"] = args.get("C", str())
@@ -409,3 +412,145 @@ async def account_register_post(request: Request,
context["complete"] = True
context["user"] = user
return render_template(request, "register.html", context)
+
+
+def cannot_edit(request, user):
+ """ Return a 401 HTMLResponse if the request user doesn't
+ have authorization, otherwise None. """
+ has_dev_cred = request.user.has_credential("CRED_ACCOUNT_EDIT_DEV",
+ approved=[user])
+ if not has_dev_cred:
+ return HTMLResponse(status_code=int(HTTPStatus.UNAUTHORIZED))
+ return None
+
+
+@router.get("/account/{username}/edit", response_class=HTMLResponse)
+@auth_required(True)
+async def account_edit(request: Request,
+ username: str):
+ user = db.query(User, User.Username == username).first()
+ response = cannot_edit(request, user)
+ if response:
+ return response
+
+ context = await make_variable_context(request, "Accounts")
+ context["user"] = user
+
+ context = make_account_form_context(context, request, user, dict())
+ return render_template(request, "account/edit.html", context)
+
+
+@router.post("/account/{username}/edit", response_class=HTMLResponse)
+@auth_required(True)
+async def account_edit_post(request: Request,
+ username: str,
+ U: str = Form(default=str()), # Username
+ J: bool = Form(default=False),
+ E: str = Form(default=str()), # Email
+ H: str = Form(default=False), # Hide Email
+ BE: str = Form(default=None), # Backup Email
+ R: str = Form(default=None), # Real Name
+ HP: str = Form(default=None), # Homepage
+ I: str = Form(default=None), # IRC Nick
+ K: str = Form(default=None), # PGP Key
+ L: str = Form(aurweb.config.get(
+ "options", "default_lang")),
+ TZ: str = Form(aurweb.config.get(
+ "options", "default_timezone")),
+ P: str = Form(default=str()), # New Password
+ C: str = Form(default=None), # Password Confirm
+ PK: str = Form(default=None), # PubKey
+ CN: bool = Form(default=False), # Comment Notify
+ UN: bool = Form(default=False), # Update Notify
+ ON: bool = Form(default=False), # Owner Notify
+ passwd: str = Form(default=str())):
+ from aurweb.db import session
+
+ user = session.query(User).filter(User.Username == username).first()
+ response = cannot_edit(request, user)
+ if response:
+ return response
+
+ context = await make_variable_context(request, "Accounts")
+ context["user"] = user
+
+ if not passwd:
+ context["errors"] = ["Invalid password."]
+ return render_template(request, "account/edit.html", context,
+ status_code=int(HTTPStatus.BAD_REQUEST))
+
+ args = dict(await request.form())
+ context = make_account_form_context(context, request, user, args)
+ ok, errors = process_account_form(request, user, args)
+
+ if not ok:
+ context["errors"] = errors
+ return render_template(request, "account/edit.html", context,
+ status_code=int(HTTPStatus.BAD_REQUEST))
+
+ # Set all updated fields as needed.
+ user.Username = U or user.Username
+ user.Email = E or user.Email
+ user.HideEmail = bool(H)
+ user.BackupEmail = BE or user.BackupEmail
+ user.RealName = R or user.RealName
+ user.Homepage = HP or user.Homepage
+ user.IRCNick = I or user.IRCNick
+ user.PGPKey = K or user.PGPKey
+ user.InactivityTS = datetime.utcnow().timestamp() if J else 0
+
+ # If we update the language, update the cookie as well.
+ if L and L != user.LangPreference:
+ request.cookies["AURLANG"] = L
+ user.LangPreference = L
+ context["language"] = L
+
+ # If we update the timezone, also update the cookie.
+ if TZ and TZ != user.Timezone:
+ user.Timezone = TZ
+ request.cookies["AURTZ"] = TZ
+ context["timezone"] = TZ
+
+ user.CommentNotify = bool(CN)
+ user.UpdateNotify = bool(UN)
+ user.OwnershipNotify = bool(ON)
+
+ # If a PK is given, compare it against the target user's PK.
+ if PK:
+ # Get the second token in the public key, which is the actual key.
+ pubkey = PK.strip().rstrip()
+ fingerprint = get_fingerprint(pubkey)
+ if not user.ssh_pub_key:
+ # No public key exists, create one.
+ user.ssh_pub_key = SSHPubKey(UserID=user.ID,
+ PubKey=PK,
+ Fingerprint=fingerprint)
+ elif user.ssh_pub_key.Fingerprint != fingerprint:
+ # A public key already exists, update it.
+ user.ssh_pub_key.PubKey = PK
+ user.ssh_pub_key.Fingerprint = fingerprint
+ elif user.ssh_pub_key:
+ # Else, if the user has a public key already, delete it.
+ session.delete(user.ssh_pub_key)
+
+ # Commit changes, if any.
+ session.commit()
+
+ if P and not user.valid_password(P):
+ # Remove the fields we consumed for passwords.
+ context["P"] = context["C"] = str()
+
+ # If a password was given and it doesn't match the user's, update it.
+ user.update_password(P)
+ if user == request.user:
+ # If the target user is the request user, login with
+ # the updated password and update AURSID.
+ request.cookies["AURSID"] = user.login(request, P)
+
+ if not errors:
+ context["complete"] = True
+
+ # Update cookies with requests, in case they were changed.
+ response = render_template(request, "account/edit.html", context)
+ return util.migrate_cookies(request, response)
+>>>>>> > dddd1137... add account edit(settings) routes
diff --git a/aurweb/templates.py b/aurweb/templates.py
index d548e92b..c0472b2e 100644
--- a/aurweb/templates.py
+++ b/aurweb/templates.py
@@ -12,7 +12,7 @@ from fastapi.responses import HTMLResponse
import aurweb.config
-from aurweb import captcha, l10n, time
+from aurweb import captcha, l10n, time, util
# Prepare jinja2 objects.
loader = jinja2.FileSystemLoader(os.path.join(
@@ -27,6 +27,9 @@ env.filters["tr"] = l10n.tr
env.filters["captcha_salt"] = captcha.captcha_salt_filter
env.filters["captcha_cmdline"] = captcha.captcha_cmdline_filter
+# Add account utility filters.
+env.filters["account_url"] = util.account_url
+
def make_context(request: Request, title: str, next: str = None):
""" Create a context for a jinja2 TemplateResponse. """
diff --git a/aurweb/util.py b/aurweb/util.py
index 5e1717bd..8b6ddbe7 100644
--- a/aurweb/util.py
+++ b/aurweb/util.py
@@ -82,6 +82,12 @@ def valid_ssh_pubkey(pk):
return base64.b64encode(base64.b64decode(tokens[1])).decode() == tokens[1]
+def migrate_cookies(request, response):
+ for k, v in request.cookies.items():
+ response.set_cookie(k, v)
+ return response
+
+
@jinja2.contextfilter
def account_url(context, user):
request = context.get("request")
diff --git a/templates/account/edit.html b/templates/account/edit.html
new file mode 100644
index 00000000..f8895d92
--- /dev/null
+++ b/templates/account/edit.html
@@ -0,0 +1,46 @@
+{% extends "partials/layout.html" %}
+
+{% block pageContent %}
+
+
{% trans %}Accounts{% endtrans %}
+
+ {% if complete %}
+
+ {{
+ "The account, %s%s%s, has been successfully modified."
+ | tr
+ | format("
", user.Username, "")
+ | safe
+ }}
+
+ {% else %}
+ {% if errors %}
+ {% include "partials/error.html" %}
+ {% else %}
+
+ {{ "Click %shere%s if you want to permanently delete this account."
+ | tr
+ | format('' | format(user | account_url),
+ "")
+ | safe
+ }}
+ {{ "Click %shere%s for user details."
+ | tr
+ | format('' | format(user | account_url),
+ "")
+ | safe
+ }}
+ {{ "Click %shere%s to list the comments made by this account."
+ | tr
+ | format('' | format(user | account_url),
+ "")
+ | safe
+ }}
+
+ {% endif %}
+
+ {% set form_type = "UpdateAccount" %}
+ {% include "partials/account_form.html" %}
+ {% endif %}
+
+{% endblock %}
diff --git a/templates/partials/account_form.html b/templates/partials/account_form.html
index 3af13368..5ae18131 100644
--- a/templates/partials/account_form.html
+++ b/templates/partials/account_form.html
@@ -42,6 +42,15 @@
"account is inactive." | tr }}
+
+
+
+
+
{% if request.user.has_credential("CRED_ACCOUNT_CHANGE_TYPE") %}