feat(ballot): change ballot format

This commit is contained in:
Alexandre Teles 2023-01-05 19:50:38 -03:00
parent c8e203f740
commit 9df0161f68
7 changed files with 151 additions and 55 deletions

View File

@ -8,7 +8,7 @@ config: dict = load_config()
class Ballot: class Ballot:
"""Implements a ballot for ReVanced Polling API.""" """Implements a ballot for ReVanced Polling API."""
redis = RedisConnector.connect(config['tokens']['database']) redis = RedisConnector.connect(config['ballots']['database'])
BallotLogger = Logger.BallotLogger() BallotLogger = Logger.BallotLogger()
@ -34,3 +34,24 @@ class Ballot:
raise e raise e
return stored 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

View File

@ -2,6 +2,7 @@ from redis import asyncio as aioredis
import app.utils.Logger as Logger import app.utils.Logger as Logger
from app.dependencies import load_config from app.dependencies import load_config
from app.utils.RedisConnector import RedisConnector from app.utils.RedisConnector import RedisConnector
import app.controllers.Ballot as Ballot
config: dict = load_config() config: dict = load_config()
@ -10,6 +11,7 @@ class Clients:
"""Implements a client for ReVanced Polling API.""" """Implements a client for ReVanced Polling API."""
redis = RedisConnector.connect(config['tokens']['database']) redis = RedisConnector.connect(config['tokens']['database'])
ballot = Ballot.Ballot()
UserLogger = Logger.UserLogger() UserLogger = Logger.UserLogger()
@ -41,3 +43,46 @@ class Clients:
return banned 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

View File

@ -1,4 +1,3 @@
from collections import deque
from pydantic import BaseModel from pydantic import BaseModel
class BallotFields(BaseModel): class BallotFields(BaseModel):

View File

@ -1,4 +1,5 @@
from pydantic import BaseModel from pydantic import BaseModel
from app.models.BallotFields import BallotFields
class BallotModel(BaseModel): class BallotModel(BaseModel):
"""Implements the fields for the ballots. """Implements the fields for the ballots.
@ -7,4 +8,4 @@ class BallotModel(BaseModel):
BaseModel (pydantic.BaseModel): BaseModel from pydantic BaseModel (pydantic.BaseModel): BaseModel from pydantic
""" """
vote: str votes: list[BallotFields]

View File

@ -10,35 +10,25 @@ class InternalServerError(BaseModel):
error: str = "Internal Server Error" error: str = "Internal Server Error"
message: str = "An internal server error occurred. Please try again later." message: str = "An internal server error occurred. Please try again later."
class AnnouncementNotFound(BaseModel): class Conflict(BaseModel):
"""Implements the response fields for when an item is not found. """Implements the response fields for when a conflict occurs.
Args: Args:
BaseModel (pydantic.BaseModel): BaseModel from pydantic BaseModel (pydantic.BaseModel): BaseModel from pydantic
""" """
error: str = "Not Found" error: str = "Conflict"
message: str = "No announcement was found." message: str = "User already voted on this ballot."
class ClientNotFound(BaseModel): class PreconditionFailed(BaseModel):
"""Implements the response fields for when a client is not found. """Implements the response fields for when a precondition fails.
Args: Args:
BaseModel (pydantic.BaseModel): BaseModel from pydantic BaseModel (pydantic.BaseModel): BaseModel from pydantic
""" """
error: str = "Not Found" error: str = "Precondition Failed"
message: str = "No client matches the given ID" message: str = "User is not eligible to vote on this ballot."
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"
class Unauthorized(BaseModel): class Unauthorized(BaseModel):
"""Implements the response fields for when the client is unauthorized. """Implements the response fields for when the client is unauthorized.
@ -49,23 +39,4 @@ class Unauthorized(BaseModel):
error: str = "Unauthorized" error: str = "Unauthorized"
message: str = "The client is unauthorized to access this resource" 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."

View File

@ -4,6 +4,7 @@ from fastapi_paseto_auth import AuthPASETO
from fastapi import APIRouter, Request, Response, Depends, status, HTTPException, Header from fastapi import APIRouter, Request, Response, Depends, status, HTTPException, Header
from app.dependencies import load_config from app.dependencies import load_config
from app.controllers.Clients import Clients from app.controllers.Clients import Clients
from app.controllers.Ballot import Ballot
import app.models.ClientModels as ClientModels import app.models.ClientModels as ClientModels
import app.models.GeneralErrors as GeneralErrors import app.models.GeneralErrors as GeneralErrors
import app.models.ResponseModels as ResponseModels import app.models.ResponseModels as ResponseModels
@ -13,6 +14,7 @@ router = APIRouter(
tags=['Authentication'] tags=['Authentication']
) )
clients = Clients() clients = Clients()
ballot = Ballot()
config: dict = load_config() config: dict = load_config()
@router.post('/', response_model=ResponseModels.ClientAuthTokenResponse, status_code=status.HTTP_200_OK) @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: else:
user_claims: dict[str, str] = {} if not ballot.exists(client.discord_id_hash):
user_claims['discord_id_hash'] = client.discord_id_hash user_claims: dict[str, str] = {}
access_token = Authorize.create_access_token(subject=client.id, user_claims['discord_id_hash'] = client.discord_id_hash
user_claims=user_claims, access_token = Authorize.create_access_token(subject=client.id,
fresh=True) user_claims=user_claims,
fresh=True)
return {"access_token": access_token} return {"access_token": access_token}
else:
raise HTTPException(status_code=412, detail={
"error": GeneralErrors.PreconditionFailed().error,
"message": GeneralErrors.PreconditionFailed().message
}
)
else: else:
raise HTTPException(status_code=401, detail={ raise HTTPException(status_code=401, detail={
"error": GeneralErrors.Unauthorized().error, "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) @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: async def revoke_token(request: Request, response: Response, Authorize: AuthPASETO = Depends(), Authorization: str = Header(None)) -> dict:
"""Revoke a token. """Revoke a token.
@ -69,3 +109,4 @@ async def revoke_token(request: Request, response: Response, Authorize: AuthPASE
"message": GeneralErrors.InternalServerError().message "message": GeneralErrors.InternalServerError().message
} }
) )

View File

@ -5,11 +5,14 @@ from app.models.BallotModel import BallotModel
import app.models.GeneralErrors as GeneralErrors import app.models.GeneralErrors as GeneralErrors
import app.models.ResponseModels as ResponseModels import app.models.ResponseModels as ResponseModels
import app.controllers.Ballot as Ballot import app.controllers.Ballot as Ballot
import app.controllers.Clients as Clients
router = APIRouter() router = APIRouter()
ballot_controller = Ballot.Ballot() ballot_controller = Ballot.Ballot()
client = Clients.Clients()
config: dict = load_config() config: dict = load_config()
@router.post('/ballot', response_model=ResponseModels.BallotCastedResponse, @router.post('/ballot', response_model=ResponseModels.BallotCastedResponse,
@ -24,15 +27,30 @@ async def cast_ballot(request: Request, response: Response,
""" """
Authorize.paseto_required() 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: stored: bool = await ballot_controller.store(
return {"created": stored} 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: else:
raise HTTPException(status_code=500, detail={ raise HTTPException(status_code=401, detail={
"error": GeneralErrors.InternalServerError().error, "error": GeneralErrors.Unauthorized().error,
"message": GeneralErrors.InternalServerError().message "message": GeneralErrors.Unauthorized().message
} }
) )