aboutsummaryrefslogtreecommitdiffstats
path: root/backend/routes
diff options
context:
space:
mode:
Diffstat (limited to 'backend/routes')
-rw-r--r--backend/routes/auth/authorize.py12
-rw-r--r--backend/routes/discord.py83
-rw-r--r--backend/routes/forms/discover.py2
-rw-r--r--backend/routes/forms/form.py61
-rw-r--r--backend/routes/forms/index.py6
-rw-r--r--backend/routes/forms/response.py11
-rw-r--r--backend/routes/forms/responses.py15
-rw-r--r--backend/routes/forms/submit.py2
8 files changed, 142 insertions, 50 deletions
diff --git a/backend/routes/auth/authorize.py b/backend/routes/auth/authorize.py
index d4587f0..42fb3ec 100644
--- a/backend/routes/auth/authorize.py
+++ b/backend/routes/auth/authorize.py
@@ -17,7 +17,7 @@ from starlette.requests import Request
from backend import constants
from backend.authentication.user import User
from backend.constants import SECRET_KEY
-from backend.discord import fetch_bearer_token, fetch_user_details
+from backend.discord import fetch_bearer_token, fetch_user_details, get_member
from backend.route import Route
from backend.validation import ErrorMessage, api
@@ -34,8 +34,8 @@ class AuthorizeResponse(BaseModel):
async def process_token(
- bearer_token: dict,
- request: Request
+ bearer_token: dict,
+ request: Request
) -> Union[AuthorizeResponse, AUTH_FAILURE]:
"""Post a bearer token to Discord, and return a JWT and username."""
interaction_start = datetime.datetime.now()
@@ -46,6 +46,9 @@ async def process_token(
AUTH_FAILURE.delete_cookie("token")
return AUTH_FAILURE
+ user_id = user_details["id"]
+ member = await get_member(request.state.db, user_id, force_refresh=True)
+
max_age = datetime.timedelta(seconds=int(bearer_token["expires_in"]))
token_expiry = interaction_start + max_age
@@ -53,11 +56,12 @@ async def process_token(
"token": bearer_token["access_token"],
"refresh": bearer_token["refresh_token"],
"user_details": user_details,
+ "in_guild": bool(member),
"expiry": token_expiry.isoformat()
}
token = jwt.encode(data, SECRET_KEY, algorithm="HS256")
- user = User(token, user_details)
+ user = User(token, user_details, member)
response = responses.JSONResponse({
"username": user.display_name,
diff --git a/backend/routes/discord.py b/backend/routes/discord.py
new file mode 100644
index 0000000..bca1edb
--- /dev/null
+++ b/backend/routes/discord.py
@@ -0,0 +1,83 @@
+"""Routes which directly interact with discord related data."""
+
+import pydantic
+from spectree import Response
+from starlette.authentication import requires
+from starlette.responses import JSONResponse
+from starlette.routing import Request
+
+from backend import discord, models, route
+from backend.validation import ErrorMessage, api
+
+NOT_FOUND_EXCEPTION = JSONResponse(
+ {"error": "Could not find the requested resource in the guild or cache."}, status_code=404
+)
+
+
+class RolesRoute(route.Route):
+ """Refreshes the roles database."""
+
+ name = "roles"
+ path = "/roles"
+
+ class RolesResponse(pydantic.BaseModel):
+ """A list of all roles on the configured server."""
+
+ roles: list[models.DiscordRole]
+
+ @requires(["authenticated", "admin"])
+ @api.validate(
+ resp=Response(HTTP_200=RolesResponse),
+ tags=["roles"]
+ )
+ async def patch(self, request: Request) -> JSONResponse:
+ """Refresh the roles database."""
+ roles = await discord.get_roles(request.state.db, force_refresh=True)
+
+ return JSONResponse(
+ {"roles": [role.dict() for role in roles]},
+ )
+
+
+class MemberRoute(route.Route):
+ """Retrieve information about a server member."""
+
+ name = "member"
+ path = "/member"
+
+ class MemberRequest(pydantic.BaseModel):
+ """An ID of the member to update."""
+
+ user_id: str
+
+ @requires(["authenticated", "admin"])
+ @api.validate(
+ resp=Response(HTTP_200=models.DiscordMember, HTTP_400=ErrorMessage),
+ json=MemberRequest,
+ tags=["auth"]
+ )
+ async def delete(self, request: Request) -> JSONResponse:
+ """Force a resync of the cache for the given user."""
+ body = await request.json()
+ member = await discord.get_member(request.state.db, body["user_id"], force_refresh=True)
+
+ if member:
+ return JSONResponse(member.dict())
+ else:
+ return NOT_FOUND_EXCEPTION
+
+ @requires(["authenticated", "admin"])
+ @api.validate(
+ resp=Response(HTTP_200=models.DiscordMember, HTTP_400=ErrorMessage),
+ json=MemberRequest,
+ tags=["auth"]
+ )
+ async def get(self, request: Request) -> JSONResponse:
+ """Get a user's roles on the configured server."""
+ body = await request.json()
+ member = await discord.get_member(request.state.db, body["user_id"])
+
+ if member:
+ return JSONResponse(member.dict())
+ else:
+ return NOT_FOUND_EXCEPTION
diff --git a/backend/routes/forms/discover.py b/backend/routes/forms/discover.py
index d7351d5..b993075 100644
--- a/backend/routes/forms/discover.py
+++ b/backend/routes/forms/discover.py
@@ -29,7 +29,7 @@ EMPTY_FORM = Form(
features=__FEATURES,
questions=[__QUESTION],
name="Auth form",
- description="An empty form to help you get a token."
+ description="An empty form to help you get a token.",
)
diff --git a/backend/routes/forms/form.py b/backend/routes/forms/form.py
index 0f96b85..567c197 100644
--- a/backend/routes/forms/form.py
+++ b/backend/routes/forms/form.py
@@ -10,13 +10,15 @@ from starlette.authentication import requires
from starlette.requests import Request
from starlette.responses import JSONResponse
-from backend import constants
+from backend import constants, discord
from backend.models import Form
from backend.route import Route
from backend.routes.forms.discover import EMPTY_FORM
from backend.routes.forms.unittesting import filter_unittests
from backend.validation import ErrorMessage, OkayResponse, api
+PUBLIC_FORM_FEATURES = (constants.FormFeatures.OPEN, constants.FormFeatures.DISCOVERABLE)
+
class SingleForm(Route):
"""
@@ -31,9 +33,19 @@ class SingleForm(Route):
@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.admin if request.user.is_authenticated else False
form_id = request.path_params["form_id"].lower()
+ try:
+ await discord.verify_edit_access(form_id, request)
+ admin = True
+ except discord.FormNotFoundError:
+ if not constants.PRODUCTION and form_id == EMPTY_FORM.id:
+ # Empty form to help with authentication in development.
+ return JSONResponse(EMPTY_FORM.dict(admin=False))
+ raise
+ except discord.UnauthorizedError:
+ admin = False
+
filters = {
"_id": form_id
}
@@ -41,25 +53,18 @@ class SingleForm(Route):
if not admin:
filters["features"] = {"$in": ["OPEN", "DISCOVERABLE"]}
- if raw_form := await request.state.db.forms.find_one(filters):
- form = Form(**raw_form)
- if not admin:
- form = filter_unittests(form)
-
- return JSONResponse(form.dict(admin=admin))
-
- elif not constants.PRODUCTION and form_id == EMPTY_FORM.id:
- # Empty form to help with authentication in development.
- return JSONResponse(EMPTY_FORM.dict(admin=admin))
+ form = Form(**await request.state.db.forms.find_one(filters))
+ if not admin:
+ form = filter_unittests(form)
- return JSONResponse({"error": "not_found"}, status_code=404)
+ return JSONResponse(form.dict(admin=admin))
- @requires(["authenticated", "admin"])
+ @requires(["authenticated"])
@api.validate(
resp=Response(
HTTP_200=OkayResponse,
HTTP_400=ErrorMessage,
- HTTP_404=ErrorMessage
+ HTTP_404=ErrorMessage,
),
tags=["forms"]
)
@@ -70,10 +75,12 @@ class SingleForm(Route):
except json.decoder.JSONDecodeError:
return JSONResponse("Expected a JSON body.", 400)
- form_id = {"_id": request.path_params["form_id"].lower()}
- if raw_form := await request.state.db.forms.find_one(form_id):
+ form_id = request.path_params["form_id"].lower()
+ await discord.verify_edit_access(form_id, request)
+
+ if raw_form := await request.state.db.forms.find_one({"_id": form_id}):
if "_id" in data or "id" in data:
- if (data.get("id") or data.get("_id")) != form_id["_id"]:
+ if (data.get("id") or data.get("_id")) != form_id:
return JSONResponse({"error": "locked_field"}, status_code=400)
# Build Data Merger
@@ -90,7 +97,7 @@ class SingleForm(Route):
except ValidationError as e:
return JSONResponse(e.errors(), status_code=422)
- await request.state.db.forms.replace_one(form_id, form.dict())
+ await request.state.db.forms.replace_one({"_id": form_id}, form.dict())
return JSONResponse(form.dict())
else:
@@ -98,21 +105,15 @@ class SingleForm(Route):
@requires(["authenticated", "admin"])
@api.validate(
- resp=Response(HTTP_200=OkayResponse, HTTP_404=ErrorMessage),
+ resp=Response(HTTP_200=OkayResponse, HTTP_401=ErrorMessage, 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(
- {"_id": request.path_params["form_id"].lower()}
- ):
- return JSONResponse({"error": "not_found"}, status_code=404)
+ form_id = request.path_params["form_id"].lower()
+ await discord.verify_edit_access(form_id, request)
- await request.state.db.forms.delete_one(
- {"_id": request.path_params["form_id"].lower()}
- )
- await request.state.db.responses.delete_many(
- {"form_id": request.path_params["form_id"].lower()}
- )
+ await request.state.db.forms.delete_one({"_id": form_id})
+ await request.state.db.responses.delete_many({"form_id": form_id})
return JSONResponse({"status": "ok"})
diff --git a/backend/routes/forms/index.py b/backend/routes/forms/index.py
index 22171fa..38be693 100644
--- a/backend/routes/forms/index.py
+++ b/backend/routes/forms/index.py
@@ -15,13 +15,13 @@ from backend.validation import ErrorMessage, OkayResponse, api
class FormsList(Route):
"""
- List all available forms for administrator viewing.
+ List all available forms for authorized viewers.
"""
name = "forms_list_create"
path = "/"
- @requires(["authenticated", "admin"])
+ @requires(["authenticated", "Admins"])
@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."""
@@ -38,7 +38,7 @@ class FormsList(Route):
forms
)
- @requires(["authenticated", "admin"])
+ @requires(["authenticated", "Helpers"])
@api.validate(
json=Form,
resp=Response(HTTP_200=OkayResponse, HTTP_400=ErrorMessage),
diff --git a/backend/routes/forms/response.py b/backend/routes/forms/response.py
index d8d8d17..565701f 100644
--- a/backend/routes/forms/response.py
+++ b/backend/routes/forms/response.py
@@ -1,11 +1,13 @@
"""
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 import discord
from backend.models import FormResponse
from backend.route import Route
from backend.validation import ErrorMessage, OkayResponse, api
@@ -17,23 +19,26 @@ class Response(Route):
name = "response"
path = "/{form_id:str}/responses/{response_id:str}"
- @requires(["authenticated", "admin"])
+ @requires(["authenticated"])
@api.validate(
resp=RouteResponse(HTTP_200=FormResponse, HTTP_404=ErrorMessage),
tags=["forms", "responses"]
)
async def get(self, request: Request) -> JSONResponse:
"""Return a single form response by ID."""
+ form_id = request.path_params["form_id"]
+ await discord.verify_response_access(form_id, request)
+
if raw_response := await request.state.db.responses.find_one(
{
"_id": request.path_params["response_id"],
- "form_id": request.path_params["form_id"]
+ "form_id": form_id
}
):
response = FormResponse(**raw_response)
return JSONResponse(response.dict())
else:
- return JSONResponse({"error": "not_found"}, status_code=404)
+ return JSONResponse({"error": "response_not_found"}, status_code=404)
@requires(["authenticated", "admin"])
@api.validate(
diff --git a/backend/routes/forms/responses.py b/backend/routes/forms/responses.py
index f3c4cd7..818ebce 100644
--- a/backend/routes/forms/responses.py
+++ b/backend/routes/forms/responses.py
@@ -7,9 +7,10 @@ from starlette.authentication import requires
from starlette.requests import Request
from starlette.responses import JSONResponse
+from backend import discord
from backend.models import FormResponse, ResponseList
from backend.route import Route
-from backend.validation import api, ErrorMessage, OkayResponse
+from backend.validation import ErrorMessage, OkayResponse, api
class ResponseIdList(BaseModel):
@@ -24,20 +25,18 @@ class Responses(Route):
name = "form_responses"
path = "/{form_id:str}/responses"
- @requires(["authenticated", "admin"])
+ @requires(["authenticated"])
@api.validate(
- resp=Response(HTTP_200=ResponseList, HTTP_404=ErrorMessage),
+ resp=Response(HTTP_200=ResponseList),
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(
- {"_id": request.path_params["form_id"]}
- ):
- return JSONResponse({"error": "not_found"}, 404)
+ form_id = request.path_params["form_id"]
+ await discord.verify_response_access(form_id, request)
cursor = request.state.db.responses.find(
- {"form_id": request.path_params["form_id"]}
+ {"form_id": form_id}
)
responses = [
FormResponse(**response) for response in await cursor.to_list(None)
diff --git a/backend/routes/forms/submit.py b/backend/routes/forms/submit.py
index 95e30b0..baf403d 100644
--- a/backend/routes/forms/submit.py
+++ b/backend/routes/forms/submit.py
@@ -83,7 +83,7 @@ class SubmitForm(Route):
try:
if hasattr(request.user, User.refresh_data.__name__):
old = request.user.token
- await request.user.refresh_data()
+ await request.user.refresh_data(request.state.db)
if old != request.user.token:
try: