aboutsummaryrefslogtreecommitdiffstats
path: root/backend/routes
diff options
context:
space:
mode:
authorGravatar Hassan Abouelela <[email protected]>2020-12-17 12:53:14 +0300
committerGravatar Hassan Abouelela <[email protected]>2020-12-17 12:55:59 +0300
commit2552ca6aa43d148b12d19dd6c511fa14864cede4 (patch)
treeb0e485760dec7c82f3165bfce0651f1f70534b07 /backend/routes
parentAdds Webhook Sending Functionality (diff)
parentMerge pull request #37 from python-discord/docs/api-spec (diff)
Merge branch 'main' into discord-webhook
Signed-off-by: Hassan Abouelela <[email protected]> # Conflicts: # backend/routes/forms/submit.py
Diffstat (limited to 'backend/routes')
-rw-r--r--backend/routes/auth/authorize.py28
-rw-r--r--backend/routes/forms/discover.py6
-rw-r--r--backend/routes/forms/form.py7
-rw-r--r--backend/routes/forms/index.py19
-rw-r--r--backend/routes/forms/response.py14
-rw-r--r--backend/routes/forms/responses.py8
-rw-r--r--backend/routes/forms/submit.py25
-rw-r--r--backend/routes/index.py18
8 files changed, 113 insertions, 12 deletions
diff --git a/backend/routes/auth/authorize.py b/backend/routes/auth/authorize.py
index 41c0a0b..2509109 100644
--- a/backend/routes/auth/authorize.py
+++ b/backend/routes/auth/authorize.py
@@ -2,13 +2,26 @@
Use a token received from the Discord OAuth2 system to fetch user information.
"""
+import httpx
import jwt
+from pydantic.fields import Field
+from pydantic.main import BaseModel
+from spectree.response import Response
from starlette.requests import Request
from starlette.responses import JSONResponse
from backend.constants import SECRET_KEY
from backend.route import Route
from backend.discord import fetch_bearer_token, fetch_user_details
+from backend.validation import ErrorMessage, api
+
+
+class AuthorizeRequest(BaseModel):
+ token: str = Field(description="The access token received from Discord.")
+
+
+class AuthorizeResponse(BaseModel):
+ token: str = Field(description="A JWT token containing the user information")
class AuthorizeRoute(Route):
@@ -19,11 +32,22 @@ class AuthorizeRoute(Route):
name = "authorize"
path = "/authorize"
+ @api.validate(
+ json=AuthorizeRequest,
+ resp=Response(HTTP_200=AuthorizeResponse, HTTP_400=ErrorMessage),
+ tags=["auth"]
+ )
async def post(self, request: Request) -> JSONResponse:
+ """Generate an authorization token."""
data = await request.json()
- bearer_token = await fetch_bearer_token(data["token"])
- user_details = await fetch_user_details(bearer_token["access_token"])
+ try:
+ bearer_token = await fetch_bearer_token(data["token"])
+ user_details = await fetch_user_details(bearer_token["access_token"])
+ except httpx.HTTPStatusError:
+ return JSONResponse({
+ "error": "auth_failure"
+ }, status_code=400)
user_details["admin"] = await request.state.db.admins.find_one(
{"_id": user_details["id"]}
diff --git a/backend/routes/forms/discover.py b/backend/routes/forms/discover.py
index bba6fd4..9400f05 100644
--- a/backend/routes/forms/discover.py
+++ b/backend/routes/forms/discover.py
@@ -1,11 +1,13 @@
"""
Return a list of all publicly discoverable forms to unauthenticated users.
"""
+from spectree.response import Response
from starlette.requests import Request
from starlette.responses import JSONResponse
-from backend.models import Form
+from backend.models import Form, FormList
from backend.route import Route
+from backend.validation import api
class DiscoverableFormsList(Route):
@@ -16,7 +18,9 @@ class DiscoverableFormsList(Route):
name = "discoverable_forms_list"
path = "/discoverable"
+ @api.validate(resp=Response(HTTP_200=FormList), tags=["forms"])
async def get(self, request: Request) -> JSONResponse:
+ """List all discoverable forms that should be shown on the homepage."""
forms = []
cursor = request.state.db.forms.find({"features": "DISCOVERABLE"})
diff --git a/backend/routes/forms/form.py b/backend/routes/forms/form.py
index 8fdd8a2..c953135 100644
--- a/backend/routes/forms/form.py
+++ b/backend/routes/forms/form.py
@@ -1,12 +1,14 @@
"""
Returns or deletes a single form given an ID.
"""
+from spectree.response import Response
from starlette.authentication import requires
from starlette.requests import Request
from starlette.responses import JSONResponse
from backend.route import Route
from backend.models import Form
+from backend.validation import OkayResponse, api, ErrorMessage
class SingleForm(Route):
@@ -19,6 +21,7 @@ class SingleForm(Route):
name = "form"
path = "/{form_id:str}"
+ @api.validate(resp=Response(HTTP_200=Form, HTTP_404=ErrorMessage), tags=["forms"])
async def get(self, request: Request) -> JSONResponse:
"""Returns single form information by ID."""
admin = request.user.payload["admin"] if request.user.is_authenticated else False # noqa
@@ -37,6 +40,10 @@ class SingleForm(Route):
return JSONResponse({"error": "not_found"}, status_code=404)
@requires(["authenticated", "admin"])
+ @api.validate(
+ resp=Response(HTTP_200=OkayResponse, HTTP_404=ErrorMessage),
+ tags=["forms"]
+ )
async def delete(self, request: Request) -> JSONResponse:
"""Deletes form by ID."""
if not await request.state.db.forms.find_one(
diff --git a/backend/routes/forms/index.py b/backend/routes/forms/index.py
index bb2b299..d1373e4 100644
--- a/backend/routes/forms/index.py
+++ b/backend/routes/forms/index.py
@@ -1,13 +1,14 @@
"""
Return a list of all forms to authenticated users.
"""
-from pydantic import ValidationError
+from spectree.response import Response
from starlette.authentication import requires
from starlette.requests import Request
from starlette.responses import JSONResponse
from backend.route import Route
-from backend.models import Form
+from backend.models import Form, FormList
+from backend.validation import ErrorMessage, OkayResponse, api
class FormsList(Route):
@@ -19,7 +20,9 @@ class FormsList(Route):
path = "/"
@requires(["authenticated", "admin"])
+ @api.validate(resp=Response(HTTP_200=FormList), tags=["forms"])
async def get(self, request: Request) -> JSONResponse:
+ """Return a list of all forms to authenticated users."""
forms = []
cursor = request.state.db.forms.find()
@@ -34,12 +37,16 @@ class FormsList(Route):
)
@requires(["authenticated", "admin"])
+ @api.validate(
+ json=Form,
+ resp=Response(HTTP_200=OkayResponse, HTTP_400=ErrorMessage),
+ tags=["forms"]
+ )
async def post(self, request: Request) -> JSONResponse:
+ """Create a new form."""
form_data = await request.json()
- try:
- form = Form(**form_data)
- except ValidationError as e:
- return JSONResponse(e.errors())
+
+ form = Form(**form_data)
if await request.state.db.forms.find_one({"_id": form.id}):
return JSONResponse({
diff --git a/backend/routes/forms/response.py b/backend/routes/forms/response.py
index acaa647..d8d8d17 100644
--- a/backend/routes/forms/response.py
+++ b/backend/routes/forms/response.py
@@ -1,12 +1,14 @@
"""
Returns or deletes form response by ID.
"""
+from spectree import Response as RouteResponse
from starlette.authentication import requires
from starlette.requests import Request
from starlette.responses import JSONResponse
from backend.models import FormResponse
from backend.route import Route
+from backend.validation import ErrorMessage, OkayResponse, api
class Response(Route):
@@ -16,8 +18,12 @@ class Response(Route):
path = "/{form_id:str}/responses/{response_id:str}"
@requires(["authenticated", "admin"])
+ @api.validate(
+ resp=RouteResponse(HTTP_200=FormResponse, HTTP_404=ErrorMessage),
+ tags=["forms", "responses"]
+ )
async def get(self, request: Request) -> JSONResponse:
- """Returns single form response by ID."""
+ """Return a single form response by ID."""
if raw_response := await request.state.db.responses.find_one(
{
"_id": request.path_params["response_id"],
@@ -30,8 +36,12 @@ class Response(Route):
return JSONResponse({"error": "not_found"}, status_code=404)
@requires(["authenticated", "admin"])
+ @api.validate(
+ resp=RouteResponse(HTTP_200=OkayResponse, HTTP_404=ErrorMessage),
+ tags=["forms", "responses"]
+ )
async def delete(self, request: Request) -> JSONResponse:
- """Deletes form response by ID."""
+ """Delete a form response by ID."""
if not await request.state.db.responses.find_one(
{
"_id": request.path_params["response_id"],
diff --git a/backend/routes/forms/responses.py b/backend/routes/forms/responses.py
index ee8ab84..54da246 100644
--- a/backend/routes/forms/responses.py
+++ b/backend/routes/forms/responses.py
@@ -1,12 +1,14 @@
"""
Returns all form responses by form ID.
"""
+from spectree import Response
from starlette.authentication import requires
from starlette.requests import Request
from starlette.responses import JSONResponse
-from backend.models import FormResponse
+from backend.models import FormResponse, ResponseList
from backend.route import Route
+from backend.validation import api, ErrorMessage
class Responses(Route):
@@ -18,6 +20,10 @@ class Responses(Route):
path = "/{form_id:str}/responses"
@requires(["authenticated", "admin"])
+ @api.validate(
+ resp=Response(HTTP_200=ResponseList, HTTP_404=ErrorMessage),
+ tags=["forms", "responses"]
+ )
async def get(self, request: Request) -> JSONResponse:
"""Returns all form responses by form ID."""
if not await request.state.db.forms.find_one(
diff --git a/backend/routes/forms/submit.py b/backend/routes/forms/submit.py
index 7cd7576..c476468 100644
--- a/backend/routes/forms/submit.py
+++ b/backend/routes/forms/submit.py
@@ -5,10 +5,13 @@ Submit a form.
import binascii
import hashlib
import uuid
+from typing import Any, Optional
import httpx
import pydnsbl
from pydantic import ValidationError
+from pydantic.main import BaseModel
+from spectree import Response
from starlette.background import BackgroundTask
from starlette.requests import Request
from starlette.responses import JSONResponse
@@ -16,6 +19,7 @@ from starlette.responses import JSONResponse
from backend.constants import FRONTEND_URL, FormFeatures, HCAPTCHA_API_SECRET
from backend.models import Form, FormResponse
from backend.route import Route
+from backend.validation import AuthorizationHeaders, ErrorMessage, api
HCAPTCHA_VERIFY_URL = "https://hcaptcha.com/siteverify"
HCAPTCHA_HEADERS = {
@@ -23,6 +27,16 @@ HCAPTCHA_HEADERS = {
}
+class SubmissionResponse(BaseModel):
+ form: Form
+ response: FormResponse
+
+
+class PartialSubmission(BaseModel):
+ response: dict[str, Any]
+ captcha: Optional[str]
+
+
class SubmitForm(Route):
"""
Submit a form with the provided form ID.
@@ -31,7 +45,18 @@ class SubmitForm(Route):
name = "submit_form"
path = "/submit/{form_id:str}"
+ @api.validate(
+ json=PartialSubmission,
+ resp=Response(
+ HTTP_200=SubmissionResponse,
+ HTTP_404=ErrorMessage,
+ HTTP_400=ErrorMessage
+ ),
+ headers=AuthorizationHeaders,
+ tags=["forms", "responses"]
+ )
async def post(self, request: Request) -> JSONResponse:
+ """Submit a response to the form."""
data = await request.json()
data["timestamp"] = None
diff --git a/backend/routes/index.py b/backend/routes/index.py
index b37f381..dd40d01 100644
--- a/backend/routes/index.py
+++ b/backend/routes/index.py
@@ -1,10 +1,24 @@
"""
Index route for the forms API.
"""
+from pydantic import BaseModel
+from pydantic.fields import Field
+from spectree import Response
from starlette.requests import Request
from starlette.responses import JSONResponse
from backend.route import Route
+from backend.validation import api
+
+
+class IndexResponse(BaseModel):
+ message: str = Field(description="A hello message")
+ client: str = Field(
+ description=(
+ "The connecting client, in production this will"
+ " be an IP of our internal load balancer"
+ )
+ )
class IndexRoute(Route):
@@ -17,7 +31,11 @@ class IndexRoute(Route):
name = "index"
path = "/"
+ @api.validate(resp=Response(HTTP_200=IndexResponse))
def get(self, request: Request) -> JSONResponse:
+ """
+ Return a hello from Python Discord forms!
+ """
response_data = {
"message": "Hello, world!",
"client": request.client.host,