mirror of
https://gitlab.archlinux.org/archlinux/aurweb.git
synced 2025-02-03 10:43:03 +01:00
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:
parent
a6bba601a9
commit
d674aaf736
10 changed files with 808 additions and 3 deletions
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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. """
|
||||
|
|
97
aurweb/routers/trusted_user.py
Normal file
97
aurweb/routers/trusted_user.py
Normal 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)
|
Loading…
Add table
Add a link
Reference in a new issue