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:
parent
0c5f1aa395
commit
38cf2376b1
10 changed files with 95 additions and 33 deletions
4
.gitmodules
vendored
4
.gitmodules
vendored
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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},
|
||||
)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
1
webapp
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit bea95c1f82bbc762e9933e22f520243feba8074a
|
Loading…
Add table
Reference in a new issue