Skip to content

Commit

Permalink
Frontend API preflight support (#359)
Browse files Browse the repository at this point in the history
* add webapp submodule
* add http OPTIONS support
* add cors headers

Co-authored-by: Henri Dickson <[email protected]>
  • Loading branch information
doubaniux and alphatownsman authored Nov 2, 2023
1 parent 0c5f1aa commit 38cf237
Show file tree
Hide file tree
Showing 10 changed files with 95 additions and 33 deletions.
4 changes: 4 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
[submodule "webapp"]
path = webapp
url = https://github.com/neodb-social/webapp.git
branch = main
[submodule "neodb-takahe"]
path = neodb-takahe
url = https://github.com/neodb-social/neodb-takahe.git
Expand Down
15 changes: 15 additions & 0 deletions boofilsic/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@
"polymorphic",
"easy_thumbnails",
"user_messages",
"corsheaders",
"anymail",
# "silk",
]
Expand All @@ -274,6 +275,7 @@
"django.middleware.security.SecurityMiddleware",
# "silk.middleware.SilkyMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"corsheaders.middleware.CorsMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
Expand Down Expand Up @@ -484,4 +486,17 @@

DEVELOPER_CONSOLE_APPLICATION_CLIENT_ID = "NEODB_DEVELOPER_CONSOLE"

# https://github.com/adamchainz/django-cors-headers#configuration
# CORS_ALLOWED_ORIGINS = []
# CORS_ALLOWED_ORIGIN_REGEXES = []
CORS_ALLOW_ALL_ORIGINS = True
CORS_URLS_REGEX = r"^/api/.*$"
CORS_ALLOW_METHODS = (
"DELETE",
"GET",
"OPTIONS",
# "PATCH",
"POST",
# "PUT",
)
DEFAULT_RELAY_SERVER = "https://relay.neodb.net/actor"
60 changes: 40 additions & 20 deletions catalog/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ class SearchResult(Schema):
count: int


@api.get(
@api.api_operation(
["GET", "OPTIONS"],
"/catalog/search",
response={200: SearchResult, 400: Result},
summary="Search items in catalog",
Expand Down Expand Up @@ -54,7 +55,8 @@ def search_item(
return 200, {"data": items, "pages": num_pages, "count": count}


@api.get(
@api.api_operation(
["GET", "OPTIONS"],
"/catalog/fetch",
response={200: ItemSchema, 202: Result, 404: Result},
summary="Fetch item from URL of a supported site",
Expand Down Expand Up @@ -94,7 +96,8 @@ def _get_item(cls, uuid, response):
return item


@api.get(
@api.api_operation(
["GET", "OPTIONS"],
"/book/{uuid}",
response={200: EditionSchema, 302: RedirectedResult, 404: Result},
auth=None,
Expand All @@ -103,7 +106,8 @@ def get_book(request, uuid: str, response: HttpResponse):
return _get_item(Edition, uuid, response)


@api.get(
@api.api_operation(
["GET", "OPTIONS"],
"/movie/{uuid}",
response={200: MovieSchema, 302: RedirectedResult, 404: Result},
auth=None,
Expand All @@ -112,7 +116,8 @@ def get_movie(request, uuid: str, response: HttpResponse):
return _get_item(Movie, uuid, response)


@api.get(
@api.api_operation(
["GET", "OPTIONS"],
"/tv/{uuid}",
response={200: TVShowSchema, 302: RedirectedResult, 404: Result},
auth=None,
Expand All @@ -121,7 +126,8 @@ def get_tv_show(request, uuid: str, response: HttpResponse):
return _get_item(TVShow, uuid, response)


@api.get(
@api.api_operation(
["GET", "OPTIONS"],
"/tv/season/{uuid}",
response={200: TVSeasonSchema, 302: RedirectedResult, 404: Result},
auth=None,
Expand All @@ -130,7 +136,8 @@ def get_tv_season(request, uuid: str, response: HttpResponse):
return _get_item(TVSeason, uuid, response)


@api.get(
@api.api_operation(
["GET", "OPTIONS"],
"/tv/episode/{uuid}",
response={200: TVEpisodeSchema, 302: RedirectedResult, 404: Result},
auth=None,
Expand All @@ -139,7 +146,8 @@ def get_tv_episode(request, uuid: str, response: HttpResponse):
return _get_item(TVEpisode, uuid, response)


@api.get(
@api.api_operation(
["GET", "OPTIONS"],
"/podcast/{uuid}",
response={200: PodcastSchema, 302: RedirectedResult, 404: Result},
auth=None,
Expand All @@ -148,7 +156,8 @@ def get_podcast(request, uuid: str, response: HttpResponse):
return _get_item(Podcast, uuid, response)


@api.get(
@api.api_operation(
["GET", "OPTIONS"],
"/album/{uuid}",
response={200: AlbumSchema, 302: RedirectedResult, 404: Result},
auth=None,
Expand All @@ -157,7 +166,8 @@ def get_album(request, uuid: str, response: HttpResponse):
return _get_item(Album, uuid, response)


@api.get(
@api.api_operation(
["GET", "OPTIONS"],
"/game/{uuid}",
response={200: GameSchema, 302: RedirectedResult, 404: Result},
auth=None,
Expand All @@ -166,7 +176,8 @@ def get_game(request, uuid: str, response: HttpResponse):
return _get_item(Game, uuid, response)


@api.get(
@api.api_operation(
["GET", "OPTIONS"],
"/performance/{uuid}",
response={200: PerformanceSchema, 302: RedirectedResult, 404: Result},
auth=None,
Expand All @@ -175,7 +186,8 @@ def get_performance(request, uuid: str, response: HttpResponse):
return _get_item(Performance, uuid, response)


@api.get(
@api.api_operation(
["GET", "OPTIONS"],
"/performance/production/{uuid}",
response={200: PerformanceProductionSchema, 302: RedirectedResult, 404: Result},
auth=None,
Expand All @@ -192,7 +204,8 @@ class SearchResultLegacy(Schema):
pages: int


@api.post(
@api.api_operation(
["POST", "OPTIONS"],
"/catalog/search",
response={200: SearchResult, 400: Result},
summary="This method is deprecated, will be removed by Aug 1 2023; use GET instead",
Expand All @@ -209,7 +222,8 @@ def search_item_legacy(
return 200, {"items": result.items}


@api.post(
@api.api_operation(
["POST", "OPTIONS"],
"/catalog/fetch",
response={200: ItemSchema, 202: Result},
summary="This method is deprecated, will be removed by Aug 1 2023; use GET instead",
Expand All @@ -227,7 +241,8 @@ def fetch_item_legacy(request, url: str):
return 202, {"message": "Fetch in progress"}


@api.get(
@api.api_operation(
["GET", "OPTIONS"],
"/movie/{uuid}/",
response={200: MovieSchema, 302: RedirectedResult, 404: Result},
summary="This method is deprecated, will be removed by Aug 1 2023",
Expand All @@ -238,7 +253,8 @@ def get_movie_legacy(request, uuid: str, response: HttpResponse):
return _get_item(Movie, uuid, response)


@api.get(
@api.api_operation(
["GET", "OPTIONS"],
"/tv/{uuid}/",
response={200: TVShowSchema, 302: RedirectedResult, 404: Result},
summary="This method is deprecated, will be removed by Aug 1 2023",
Expand All @@ -249,7 +265,8 @@ def get_tv_show_legacy(request, uuid: str, response: HttpResponse):
return _get_item(TVShow, uuid, response)


@api.get(
@api.api_operation(
["GET", "OPTIONS"],
"/tvseason/{uuid}/",
response={200: TVSeasonSchema, 302: RedirectedResult, 404: Result},
summary="This method is deprecated, will be removed by Aug 1 2023",
Expand All @@ -260,7 +277,8 @@ def get_tv_season_legacy(request, uuid: str, response: HttpResponse):
return _get_item(TVSeason, uuid, response)


@api.get(
@api.api_operation(
["GET", "OPTIONS"],
"/podcast/{uuid}/",
response={200: PodcastSchema, 302: RedirectedResult, 404: Result},
summary="This method is deprecated, will be removed by Aug 1 2023",
Expand All @@ -271,7 +289,8 @@ def get_podcast_legacy(request, uuid: str, response: HttpResponse):
return _get_item(Podcast, uuid, response)


@api.get(
@api.api_operation(
["GET", "OPTIONS"],
"/album/{uuid}/",
response={200: AlbumSchema, 302: RedirectedResult, 404: Result},
summary="This method is deprecated, will be removed by Aug 1 2023",
Expand All @@ -282,7 +301,8 @@ def get_album_legacy(request, uuid: str, response: HttpResponse):
return _get_item(Album, uuid, response)


@api.get(
@api.api_operation(
["GET", "OPTIONS"],
"/game/{uuid}/",
response={200: GameSchema, 302: RedirectedResult, 404: Result},
summary="This method is deprecated, will be removed by Aug 1 2023",
Expand Down
13 changes: 10 additions & 3 deletions common/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,23 @@
_logger = logging.getLogger(__name__)


PERMITTED_WRITE_METHODS = ["PUT", "POST", "DELETE", "PATCH"]
PERMITTED_READ_METHODS = ["GET", "HEAD", "OPTIONS"]


class OAuthAccessTokenAuth(HttpBearer):
def authenticate(self, request, token):
def authenticate(self, request, token) -> bool:
if not token or not request.user.is_authenticated:
_logger.debug("API auth: no access token or user not authenticated")
return False
request_scopes = []
if request.method in ["GET", "HEAD", "OPTIONS"]:
request_method = request.method
if request_method in PERMITTED_READ_METHODS:
request_scopes = ["read"]
else:
elif request_method in PERMITTED_WRITE_METHODS:
request_scopes = ["write"]
else:
return False
validator = OAuth2Validator()
core = OAuthLibCore(Server(validator))
valid, oauthlib_req = core.verify_request(request, scopes=request_scopes)
Expand Down
27 changes: 20 additions & 7 deletions journal/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ class MarkInSchema(Schema):
post_to_fediverse: bool = False


@api.get("/me/shelf/{type}", response={200: List[MarkSchema], 401: Result, 403: Result})
@api.api_operation(
["GET", "OPTIONS"],
"/me/shelf/{type}",
response={200: List[MarkSchema], 401: Result, 403: Result},
)
@paginate(PageNumberPagination)
def list_marks_on_shelf(
request, type: ShelfType, category: AvailableItemCategory | None = None
Expand All @@ -52,7 +56,8 @@ def list_marks_on_shelf(
return queryset


@api.get(
@api.api_operation(
["GET", "OPTIONS"],
"/me/shelf/item/{item_uuid}",
response={200: MarkSchema, 401: Result, 403: Result, 404: Result},
)
Expand All @@ -69,7 +74,8 @@ def get_mark_by_item(request, item_uuid: str):
return shelfmember


@api.post(
@api.api_operation(
["POST", "OPTIONS"],
"/me/shelf/item/{item_uuid}",
response={200: Result, 401: Result, 403: Result, 404: Result},
)
Expand Down Expand Up @@ -101,7 +107,8 @@ def mark_item(request, item_uuid: str, mark: MarkInSchema):
return 200, {"message": "OK"}


@api.delete(
@api.api_operation(
["DELETE", "OPTIONS"],
"/me/shelf/item/{item_uuid}",
response={200: Result, 401: Result, 403: Result, 404: Result},
)
Expand Down Expand Up @@ -137,7 +144,11 @@ class ReviewInSchema(Schema):
post_to_fediverse: bool = False


@api.get("/me/review/", response={200: List[ReviewSchema], 401: Result, 403: Result})
@api.api_operation(
["GET", "OPTIONS"],
"/me/review/",
response={200: List[ReviewSchema], 401: Result, 403: Result},
)
@paginate(PageNumberPagination)
def list_reviews(request, category: AvailableItemCategory | None = None):
"""
Expand All @@ -151,7 +162,8 @@ def list_reviews(request, category: AvailableItemCategory | None = None):
return queryset.prefetch_related("item")


@api.get(
@api.api_operation(
["GET", "OPTIONS"],
"/me/review/item/{item_uuid}",
response={200: ReviewSchema, 401: Result, 403: Result, 404: Result},
)
Expand Down Expand Up @@ -197,7 +209,8 @@ def review_item(request, item_uuid: str, review: ReviewInSchema):
return 200, {"message": "OK"}


@api.delete(
@api.api_operation(
["DELETE", "OPTIONS"],
"/me/review/item/{item_uuid}",
response={200: Result, 401: Result, 403: Result, 404: Result},
)
Expand Down
2 changes: 1 addition & 1 deletion mastodon/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ def detect_server_info(login_domain):
return domain, api_domain, server_version


def get_mastodon_application(login_domain):
def get_or_create_fediverse_application(login_domain):
domain = login_domain
app = MastodonApplication.objects.filter(domain_name__iexact=domain).first()
if not app:
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ django-anymail
django-auditlog>=3.0.0-beta.2
django-bleach
django-compressor
django-cors-headers
django-environ
django-hijack
django-jsonform
Expand Down
2 changes: 1 addition & 1 deletion users/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ def connect(request):
login_domain.strip().lower().split("//")[-1].split("/")[0].split("@")[-1]
)
try:
app = get_mastodon_application(login_domain)
app = get_or_create_fediverse_application(login_domain)
if app.api_domain and app.api_domain != app.domain_name:
login_domain = app.api_domain
login_url = get_mastodon_login_url(app, login_domain, request)
Expand Down
3 changes: 2 additions & 1 deletion users/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ class UserSchema(Schema):
avatar: str


@api.get(
@api.api_operation(
["GET", "OPTIONS"],
"/me",
response={200: UserSchema, 401: Result},
summary="Get current user's basic info",
Expand Down
1 change: 1 addition & 0 deletions webapp
Submodule webapp added at bea95c

0 comments on commit 38cf237

Please sign in to comment.