add /tu/ (get) index

This commit implements the '/tu' Trusted User index page.

In addition to this functionality, this commit introduces
the following jinja2 filters:

- dt: util.timestamp_to_datetime
- as_timezone: util.as_timezone
- dedupe_qs: util.dedupe_qs
- urlencode: urllib.parse.quote_plus

There's also a new decorator that can be used to enforce
permissions: `account_type_required`. If a user does not
meet account type requirements, they are redirected to '/'.

```
@auth_required(True)
@account_type_required({"Trusted User"})
async def some_route(request: fastapi.Request):
    return Response("You are a Trusted User!")
```

Routes added:

- `GET /tu`: aurweb.routers.trusted_user.trusted_user

Signed-off-by: Kevin Morris <kevr@0cost.org>
This commit is contained in:
Kevin Morris 2021-06-18 04:33:48 -07:00
parent a6bba601a9
commit d674aaf736
10 changed files with 808 additions and 3 deletions

View file

@ -16,7 +16,7 @@ from aurweb.auth import BasicAuthBackend
from aurweb.db import get_engine, query
from aurweb.models.accepted_term import AcceptedTerm
from aurweb.models.term import Term
from aurweb.routers import accounts, auth, errors, html, sso
from aurweb.routers import accounts, auth, errors, html, sso, trusted_user
# Setup the FastAPI app.
app = FastAPI(exception_handlers=errors.exceptions)
@ -47,6 +47,7 @@ async def app_startup():
app.include_router(html.router)
app.include_router(auth.router)
app.include_router(accounts.router)
app.include_router(trusted_user.router)
# Initialize the database engine and ORM.
get_engine()

View file

@ -3,6 +3,8 @@ import functools
from datetime import datetime
from http import HTTPStatus
import fastapi
from fastapi.responses import RedirectResponse
from sqlalchemy import and_
from starlette.authentication import AuthCredentials, AuthenticationBackend
@ -11,6 +13,7 @@ from starlette.requests import HTTPConnection
import aurweb.config
from aurweb import l10n, util
from aurweb.models.account_type import ACCOUNT_TYPE_ID
from aurweb.models.session import Session
from aurweb.models.user import User
from aurweb.templates import make_variable_context, render_template
@ -152,6 +155,42 @@ def auth_required(is_required: bool = True,
return decorator
def account_type_required(one_of: set):
""" A decorator that can be used on FastAPI routes to dictate
that a user belongs to one of the types defined in one_of.
This decorator should be run after an @auth_required(True) is
dictated.
- Example code:
@router.get('/some_route')
@auth_required(True)
@account_type_required({"Trusted User", "Trusted User & Developer"})
async def some_route(request: fastapi.Request):
return Response()
:param one_of: A set consisting of strings to match against AccountType.
:return: Return the FastAPI function this decorator wraps.
"""
# Convert any account type string constants to their integer IDs.
one_of = {
ACCOUNT_TYPE_ID[atype]
for atype in one_of
if isinstance(atype, str)
}
def decorator(func):
@functools.wraps(func)
async def wrapper(request: fastapi.Request, *args, **kwargs):
if request.user.AccountType.ID not in one_of:
return RedirectResponse("/",
status_code=int(HTTPStatus.SEE_OTHER))
return await func(request, *args, **kwargs)
return wrapper
return decorator
CRED_ACCOUNT_CHANGE_TYPE = 1
CRED_ACCOUNT_EDIT = 2
CRED_ACCOUNT_EDIT_DEV = 3

View file

@ -3,6 +3,11 @@ from sqlalchemy import Column, Integer
from aurweb import db
from aurweb.models.declarative import Base
USER = "User"
TRUSTED_USER = "Trusted User"
DEVELOPER = "Developer"
TRUSTED_USER_AND_DEV = "Trusted User & Developer"
class AccountType(Base):
""" An ORM model of a single AccountTypes record. """

View file

@ -0,0 +1,97 @@
from datetime import datetime
from urllib.parse import quote_plus
from fastapi import APIRouter, Request
from sqlalchemy import and_, or_
from aurweb import db
from aurweb.auth import account_type_required, auth_required
from aurweb.models.account_type import DEVELOPER, TRUSTED_USER, TRUSTED_USER_AND_DEV
from aurweb.models.tu_vote import TUVote
from aurweb.models.tu_voteinfo import TUVoteInfo
from aurweb.models.user import User
from aurweb.templates import make_context, render_template
router = APIRouter()
# Some TU route specific constants.
ITEMS_PER_PAGE = 10 # Paged table size.
MAX_AGENDA_LENGTH = 75 # Agenda table column length.
# A set of account types that will approve a user for TU actions.
REQUIRED_TYPES = {
TRUSTED_USER,
DEVELOPER,
TRUSTED_USER_AND_DEV
}
@router.get("/tu")
@auth_required(True, redirect="/")
@account_type_required(REQUIRED_TYPES)
async def trusted_user(request: Request,
coff: int = 0, # current offset
cby: str = "desc", # current by
poff: int = 0, # past offset
pby: str = "desc"): # past by
context = make_context(request, "Trusted User")
current_by, past_by = cby, pby
current_off, past_off = coff, poff
context["pp"] = pp = ITEMS_PER_PAGE
context["prev_len"] = MAX_AGENDA_LENGTH
ts = int(datetime.utcnow().timestamp())
if current_by not in {"asc", "desc"}:
# If a malicious by was given, default to desc.
current_by = "desc"
context["current_by"] = current_by
if past_by not in {"asc", "desc"}:
# If a malicious by was given, default to desc.
past_by = "desc"
context["past_by"] = past_by
current_votes = db.query(TUVoteInfo, TUVoteInfo.End > ts).order_by(
TUVoteInfo.Submitted.desc())
context["current_votes_count"] = current_votes.count()
current_votes = current_votes.limit(pp).offset(current_off)
context["current_votes"] = reversed(current_votes.all()) \
if current_by == "asc" else current_votes.all()
context["current_off"] = current_off
past_votes = db.query(TUVoteInfo, TUVoteInfo.End <= ts).order_by(
TUVoteInfo.Submitted.desc())
context["past_votes_count"] = past_votes.count()
past_votes = past_votes.limit(pp).offset(past_off)
context["past_votes"] = reversed(past_votes.all()) \
if past_by == "asc" else past_votes.all()
context["past_off"] = past_off
# TODO
# We order last votes by TUVote.VoteID and User.Username.
# This is really bad. We should add a Created column to
# TUVote of type Timestamp and order by that instead.
last_votes_by_tu = db.query(TUVote).filter(
and_(TUVote.VoteID == TUVoteInfo.ID,
TUVoteInfo.End <= ts,
TUVote.UserID == User.ID,
or_(User.AccountTypeID == 2,
User.AccountTypeID == 4))
).group_by(User.ID).order_by(
TUVote.VoteID.desc(), User.Username.asc())
context["last_votes_by_tu"] = last_votes_by_tu.all()
context["current_by_next"] = "asc" if current_by == "desc" else "desc"
context["past_by_next"] = "asc" if past_by == "desc" else "desc"
context["q"] = '&'.join([
f"coff={current_off}",
f"cby={quote_plus(current_by)}",
f"poff={past_off}",
f"pby={quote_plus(past_by)}"
])
return render_template(request, "tu/index.html", context)