Frontend API preflight support (#359)

* add webapp submodule
* add http OPTIONS support
* add cors headers

Co-authored-by: Henri Dickson <90480431+alphatownsman@users.noreply.github.com>
This commit is contained in:
doubaniux 2023-11-02 21:28:11 +01:00 committed by GitHub
parent 0c5f1aa395
commit 38cf2376b1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 95 additions and 33 deletions

4
.gitmodules vendored
View file

@ -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

View file

@ -249,6 +249,7 @@ INSTALLED_APPS = [
"polymorphic",
"easy_thumbnails",
"user_messages",
"corsheaders",
"anymail",
# "silk",
]
@ -274,6 +275,7 @@ MIDDLEWARE = [
"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",
@ -484,4 +486,17 @@ OAUTH2_PROVIDER_APPLICATION_MODEL = "developer.Application"
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"

View file

@ -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",
@ -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",
@ -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,
@ -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,
@ -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,
@ -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,
@ -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,
@ -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,
@ -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,
@ -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,
@ -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,
@ -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,
@ -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",
@ -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",
@ -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",
@ -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",
@ -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",
@ -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",
@ -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",
@ -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",

View file

@ -13,16 +13,23 @@ from oauthlib.oauth2 import Server
_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)

View file

@ -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
@ -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},
)
@ -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},
)
@ -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},
)
@ -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):
"""
@ -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},
)
@ -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},
)

View file

@ -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:

View file

@ -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

View file

@ -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)

View file

@ -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",

1
webapp Submodule

@ -0,0 +1 @@
Subproject commit bea95c1f82bbc762e9933e22f520243feba8074a