From 9df0161f68643cbfb95f36d6f821cdce6c4aadf7 Mon Sep 17 00:00:00 2001 From: Alexandre Teles Date: Thu, 5 Jan 2023 19:50:38 -0300 Subject: [PATCH] feat(ballot): change ballot format --- app/controllers/Ballot.py | 23 +++++++++++++++- app/controllers/Clients.py | 45 ++++++++++++++++++++++++++++++ app/models/BallotFields.py | 1 - app/models/BallotModel.py | 3 +- app/models/GeneralErrors.py | 45 ++++++------------------------ app/routers/auth.py | 55 ++++++++++++++++++++++++++++++++----- app/routers/ballot.py | 34 +++++++++++++++++------ 7 files changed, 151 insertions(+), 55 deletions(-) diff --git a/app/controllers/Ballot.py b/app/controllers/Ballot.py index 1b052b0..5e357d9 100644 --- a/app/controllers/Ballot.py +++ b/app/controllers/Ballot.py @@ -8,7 +8,7 @@ config: dict = load_config() class Ballot: """Implements a ballot for ReVanced Polling API.""" - redis = RedisConnector.connect(config['tokens']['database']) + redis = RedisConnector.connect(config['ballots']['database']) BallotLogger = Logger.BallotLogger() @@ -34,3 +34,24 @@ class Ballot: raise e return stored + + async def exists(self, discord_hashed_id: str): + """Check if the ballot exists. + + Args: + discord_hashed_id (str): Discord hashed ID of the voter + + Returns: + bool: True if the ballot exists, False otherwise + """ + + exists: bool = False + + try: + if await self.redis.exists(discord_hashed_id): + exists = True + except aioredis.RedisError as e: + await self.BallotLogger.log("BALLOT_EXISTS", e) + raise e + + return exists diff --git a/app/controllers/Clients.py b/app/controllers/Clients.py index d161a00..c6bf7f4 100644 --- a/app/controllers/Clients.py +++ b/app/controllers/Clients.py @@ -2,6 +2,7 @@ from redis import asyncio as aioredis import app.utils.Logger as Logger from app.dependencies import load_config from app.utils.RedisConnector import RedisConnector +import app.controllers.Ballot as Ballot config: dict = load_config() @@ -10,6 +11,7 @@ class Clients: """Implements a client for ReVanced Polling API.""" redis = RedisConnector.connect(config['tokens']['database']) + ballot = Ballot.Ballot() UserLogger = Logger.UserLogger() @@ -41,3 +43,46 @@ class Clients: return banned + async def is_token_banned(self, token: str) -> bool: + """Check if the token is banned + + Args: + token (str): Token to check + + Returns: + bool: True if the token is banned, False otherwise + """ + + banned: bool = False + + try: + if await self.redis.exists(token): + banned = True + except aioredis.RedisError as e: + await self.UserLogger.log("IS_TOKEN_BANNED", e) + raise e + + return banned + + async def voted(self, token: str, discord_id: str) -> bool: + """Check if the user already voted + + Args: + token (str): Token to check + + Returns: + bool: True if the user voted, False otherwise + """ + + voted: bool = False + + try: + if (await self.is_token_banned(token) or + await self.ballot.exists(discord_id)): + + voted = True + except aioredis.RedisError as e: + await self.UserLogger.log("AUTH_CHECKS", e) + raise e + + return voted diff --git a/app/models/BallotFields.py b/app/models/BallotFields.py index 3c975d0..8007ba0 100644 --- a/app/models/BallotFields.py +++ b/app/models/BallotFields.py @@ -1,4 +1,3 @@ -from collections import deque from pydantic import BaseModel class BallotFields(BaseModel): diff --git a/app/models/BallotModel.py b/app/models/BallotModel.py index 2a3c16d..647c7b1 100644 --- a/app/models/BallotModel.py +++ b/app/models/BallotModel.py @@ -1,4 +1,5 @@ from pydantic import BaseModel +from app.models.BallotFields import BallotFields class BallotModel(BaseModel): """Implements the fields for the ballots. @@ -7,4 +8,4 @@ class BallotModel(BaseModel): BaseModel (pydantic.BaseModel): BaseModel from pydantic """ - vote: str + votes: list[BallotFields] diff --git a/app/models/GeneralErrors.py b/app/models/GeneralErrors.py index 7562a3a..d792bc2 100644 --- a/app/models/GeneralErrors.py +++ b/app/models/GeneralErrors.py @@ -10,35 +10,25 @@ class InternalServerError(BaseModel): error: str = "Internal Server Error" message: str = "An internal server error occurred. Please try again later." -class AnnouncementNotFound(BaseModel): - """Implements the response fields for when an item is not found. +class Conflict(BaseModel): + """Implements the response fields for when a conflict occurs. Args: BaseModel (pydantic.BaseModel): BaseModel from pydantic """ - error: str = "Not Found" - message: str = "No announcement was found." + error: str = "Conflict" + message: str = "User already voted on this ballot." -class ClientNotFound(BaseModel): - """Implements the response fields for when a client is not found. +class PreconditionFailed(BaseModel): + """Implements the response fields for when a precondition fails. Args: BaseModel (pydantic.BaseModel): BaseModel from pydantic """ - error: str = "Not Found" - message: str = "No client matches the given ID" - -class IdNotProvided(BaseModel): - """Implements the response fields for when the id is not provided. - - Args: - BaseModel (pydantic.BaseModel): BaseModel from pydantic - """ - - error: str = "Bad Request" - message: str = "Missing client id" + error: str = "Precondition Failed" + message: str = "User is not eligible to vote on this ballot." class Unauthorized(BaseModel): """Implements the response fields for when the client is unauthorized. @@ -49,23 +39,4 @@ class Unauthorized(BaseModel): error: str = "Unauthorized" message: str = "The client is unauthorized to access this resource" - -class MirrorNotFoundError(BaseModel): - """Implements the response fields for when a mirror is not found. - Args: - BaseModel (pydantic.BaseModel): BaseModel from pydantic - """ - - error: str = "Not Found" - message: str = "No mirror was found for the organization, repository, and version provided." - -class MirrorAlreadyExistsError(BaseModel): - """Implements the response fields for when a mirror already exists. - - Args: - BaseModel (pydantic.BaseModel): BaseModel from pydantic - """ - - error: str = "Conflict" - message: str = "A mirror already exists for the organization, repository, and version provided. Please use the PUT method to update the mirror." diff --git a/app/routers/auth.py b/app/routers/auth.py index e86bca7..ee51230 100644 --- a/app/routers/auth.py +++ b/app/routers/auth.py @@ -4,6 +4,7 @@ from fastapi_paseto_auth import AuthPASETO from fastapi import APIRouter, Request, Response, Depends, status, HTTPException, Header from app.dependencies import load_config from app.controllers.Clients import Clients +from app.controllers.Ballot import Ballot import app.models.ClientModels as ClientModels import app.models.GeneralErrors as GeneralErrors import app.models.ResponseModels as ResponseModels @@ -13,6 +14,7 @@ router = APIRouter( tags=['Authentication'] ) clients = Clients() +ballot = Ballot() config: dict = load_config() @router.post('/', response_model=ResponseModels.ClientAuthTokenResponse, status_code=status.HTTP_200_OK) @@ -37,13 +39,19 @@ async def auth(request: Request, response: Response, client: ClientModels.Client } ) else: - user_claims: dict[str, str] = {} - user_claims['discord_id_hash'] = client.discord_id_hash - access_token = Authorize.create_access_token(subject=client.id, - user_claims=user_claims, - fresh=True) - - return {"access_token": access_token} + if not ballot.exists(client.discord_id_hash): + user_claims: dict[str, str] = {} + user_claims['discord_id_hash'] = client.discord_id_hash + access_token = Authorize.create_access_token(subject=client.id, + user_claims=user_claims, + fresh=True) + return {"access_token": access_token} + else: + raise HTTPException(status_code=412, detail={ + "error": GeneralErrors.PreconditionFailed().error, + "message": GeneralErrors.PreconditionFailed().message + } + ) else: raise HTTPException(status_code=401, detail={ "error": GeneralErrors.Unauthorized().error, @@ -51,6 +59,38 @@ async def auth(request: Request, response: Response, client: ClientModels.Client } ) +@router.put("/exchange", response_model=ResponseModels.ClientAuthTokenResponse, status_code=status.HTTP_200_OK) +async def exchange_token(request: Request, response: Response, Authorize: AuthPASETO = Depends(), Authorization: str = Header(None)) -> dict: + """Exchange a token for a new one. + + Returns: + access_token: auth token + + """ + Authorize.paseto_required() + + user_claims: dict[str, str | bool] = {} + user_claims['discord_id_hash'] = Authorize.get_user_claims()['discord_id_hash'] + user_claims['is_exchange_token'] = True + access_token = Authorize.create_access_token(subject=Authorize.get_subject(), + user_claims=user_claims, + fresh=True) + if not ballot.exists(Authorize.get_subject()): + if await clients.ban_token(Authorize.get_jti()): + return {"access_token": access_token} + else: + raise HTTPException(status_code=500, detail={ + "error": GeneralErrors.InternalServerError().error, + "message": GeneralErrors.InternalServerError().message + } + ) + else: + raise HTTPException(status_code=412, detail={ + "error": GeneralErrors.PreconditionFailed().error, + "message": GeneralErrors.PreconditionFailed().message + } + ) + @router.delete("/revoke", response_model=ResponseModels.RevokedTokenResponse, status_code=status.HTTP_200_OK) async def revoke_token(request: Request, response: Response, Authorize: AuthPASETO = Depends(), Authorization: str = Header(None)) -> dict: """Revoke a token. @@ -69,3 +109,4 @@ async def revoke_token(request: Request, response: Response, Authorize: AuthPASE "message": GeneralErrors.InternalServerError().message } ) + diff --git a/app/routers/ballot.py b/app/routers/ballot.py index c2cfff3..5286e12 100644 --- a/app/routers/ballot.py +++ b/app/routers/ballot.py @@ -5,11 +5,14 @@ from app.models.BallotModel import BallotModel import app.models.GeneralErrors as GeneralErrors import app.models.ResponseModels as ResponseModels import app.controllers.Ballot as Ballot +import app.controllers.Clients as Clients router = APIRouter() ballot_controller = Ballot.Ballot() +client = Clients.Clients() + config: dict = load_config() @router.post('/ballot', response_model=ResponseModels.BallotCastedResponse, @@ -24,15 +27,30 @@ async def cast_ballot(request: Request, response: Response, """ Authorize.paseto_required() - discord_hashed_id: str = Authorize.get_paseto_claims()['discord_hashed_id'] - stored: bool = await ballot_controller.store(discord_hashed_id, ballot.vote) + if (Authorize.get_paseto_claims()['is_exchange_token'] and + not client.voted( + Authorize.get_jti(), + Authorize.get_paseto_claims()['discord_hashed_id'] + )): - if stored: - return {"created": stored} + stored: bool = await ballot_controller.store( + Authorize.get_paseto_claims()['discord_hashed_id'], + ballot.vote + ) + + if stored: + await client.ban_token(Authorize.get_jti()) + return {"created": stored} + else: + raise HTTPException(status_code=500, detail={ + "error": GeneralErrors.InternalServerError().error, + "message": GeneralErrors.InternalServerError().message + } + ) else: - raise HTTPException(status_code=500, detail={ - "error": GeneralErrors.InternalServerError().error, - "message": GeneralErrors.InternalServerError().message - } + raise HTTPException(status_code=401, detail={ + "error": GeneralErrors.Unauthorized().error, + "message": GeneralErrors.Unauthorized().message + } )