diff --git a/.gitmodules b/.gitmodules index 027a9dfa..71efbd54 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/boofilsic/settings.py b/boofilsic/settings.py index 2b19521d..301a03c9 100644 --- a/boofilsic/settings.py +++ b/boofilsic/settings.py @@ -249,6 +249,7 @@ "polymorphic", "easy_thumbnails", "user_messages", + "corsheaders", "anymail", # "silk", ] @@ -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", @@ -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" diff --git a/catalog/api.py b/catalog/api.py index eb307efb..e28db8cb 100644 --- a/catalog/api.py +++ b/catalog/api.py @@ -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", diff --git a/common/api.py b/common/api.py index 901e7912..1f4415ff 100644 --- a/common/api.py +++ b/common/api.py @@ -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) diff --git a/journal/api.py b/journal/api.py index e23cc870..dbe7440e 100644 --- a/journal/api.py +++ b/journal/api.py @@ -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}, ) diff --git a/mastodon/api.py b/mastodon/api.py index 7fd1149a..6e88b0b1 100644 --- a/mastodon/api.py +++ b/mastodon/api.py @@ -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: diff --git a/requirements.txt b/requirements.txt index 05b0437a..0c6f7243 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/users/account.py b/users/account.py index cf68c7ee..bcafd770 100644 --- a/users/account.py +++ b/users/account.py @@ -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) diff --git a/users/api.py b/users/api.py index 3b354c72..23342388 100644 --- a/users/api.py +++ b/users/api.py @@ -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", diff --git a/webapp b/webapp new file mode 160000 index 00000000..bea95c1f --- /dev/null +++ b/webapp @@ -0,0 +1 @@ +Subproject commit bea95c1f82bbc762e9933e22f520243feba8074a