diff --git a/api/config/api_urls.py b/api/config/api_urls.py index a40ff3047a33b3b66dc8ef1bf2342c7c082cfc0e..2631309eb324ea75536b8d18433775e36986cd4c 100644 --- a/api/config/api_urls.py +++ b/api/config/api_urls.py @@ -6,11 +6,12 @@ from rest_framework_jwt import views as jwt_views from funkwhale_api.activity import views as activity_views from funkwhale_api.common import views as common_views +from funkwhale_api.common import routers as common_routers from funkwhale_api.music import views from funkwhale_api.playlists import views as playlists_views from funkwhale_api.subsonic.views import SubsonicViewSet -router = routers.SimpleRouter() +router = common_routers.OptionalSlashRouter() router.register(r"settings", GlobalPreferencesViewSet, basename="settings") router.register(r"activity", activity_views.ActivityViewSet, "activity") router.register(r"tags", views.TagViewSet, "tags") @@ -79,8 +80,8 @@ v1_patterns += [ r"^oauth/", include(("funkwhale_api.users.oauth.urls", "oauth"), namespace="oauth"), ), - url(r"^token/$", jwt_views.obtain_jwt_token, name="token"), - url(r"^token/refresh/$", jwt_views.refresh_jwt_token, name="token_refresh"), + url(r"^token/?$", jwt_views.obtain_jwt_token, name="token"), + url(r"^token/refresh/?$", jwt_views.refresh_jwt_token, name="token_refresh"), ] urlpatterns = [ diff --git a/api/funkwhale_api/common/routers.py b/api/funkwhale_api/common/routers.py new file mode 100644 index 0000000000000000000000000000000000000000..11e9dec615394f44d52d2b92d9dd84bb1eb2e647 --- /dev/null +++ b/api/funkwhale_api/common/routers.py @@ -0,0 +1,7 @@ +from rest_framework.routers import SimpleRouter + + +class OptionalSlashRouter(SimpleRouter): + def __init__(self): + super().__init__() + self.trailing_slash = "/?" diff --git a/api/funkwhale_api/favorites/urls.py b/api/funkwhale_api/favorites/urls.py index 28d0c867667d24c7daa8cd090b670e2060e5d147..51f3078038e882aeb90a41bf1f4a1c870e7d9d56 100644 --- a/api/funkwhale_api/favorites/urls.py +++ b/api/funkwhale_api/favorites/urls.py @@ -1,8 +1,8 @@ -from rest_framework import routers +from funkwhale_api.common import routers from . import views -router = routers.SimpleRouter() +router = routers.OptionalSlashRouter() router.register(r"tracks", views.TrackFavoriteViewSet, "tracks") urlpatterns = router.urls diff --git a/api/funkwhale_api/federation/api_urls.py b/api/funkwhale_api/federation/api_urls.py index bd2258de961f8d0c80f8b2a3b73d5dd6a9a8ca07..896fa2430448bcb275e05688b9863dff0ed3528e 100644 --- a/api/funkwhale_api/federation/api_urls.py +++ b/api/funkwhale_api/federation/api_urls.py @@ -1,8 +1,8 @@ -from rest_framework import routers +from funkwhale_api.common import routers from . import api_views -router = routers.SimpleRouter() +router = routers.OptionalSlashRouter() router.register(r"fetches", api_views.FetchViewSet, "fetches") router.register(r"follows/library", api_views.LibraryFollowViewSet, "library-follows") router.register(r"inbox", api_views.InboxItemViewSet, "inbox") diff --git a/api/funkwhale_api/history/urls.py b/api/funkwhale_api/history/urls.py index 707e95cd7d3056ced030a6f52b584456134c19e3..c22e58062d38a3eb6c497467dc811e38dcbc212a 100644 --- a/api/funkwhale_api/history/urls.py +++ b/api/funkwhale_api/history/urls.py @@ -1,8 +1,8 @@ -from rest_framework import routers +from funkwhale_api.common import routers from . import views -router = routers.SimpleRouter() +router = routers.OptionalSlashRouter() router.register(r"listenings", views.ListeningViewSet, "listenings") urlpatterns = router.urls diff --git a/api/funkwhale_api/instance/urls.py b/api/funkwhale_api/instance/urls.py index 05682b1e762ace43a2f9f14168f9ca42530ef439..eff731b26453f86586c513903fdcafde2cda3df7 100644 --- a/api/funkwhale_api/instance/urls.py +++ b/api/funkwhale_api/instance/urls.py @@ -1,12 +1,12 @@ from django.conf.urls import url -from rest_framework import routers +from funkwhale_api.common import routers from . import views -admin_router = routers.SimpleRouter() +admin_router = routers.OptionalSlashRouter() admin_router.register(r"admin/settings", views.AdminSettings, "admin-settings") urlpatterns = [ - url(r"^nodeinfo/2.0/$", views.NodeInfo.as_view(), name="nodeinfo-2.0"), - url(r"^settings/$", views.InstanceSettings.as_view(), name="settings"), + url(r"^nodeinfo/2.0/?$", views.NodeInfo.as_view(), name="nodeinfo-2.0"), + url(r"^settings/?$", views.InstanceSettings.as_view(), name="settings"), ] + admin_router.urls diff --git a/api/funkwhale_api/manage/urls.py b/api/funkwhale_api/manage/urls.py index 2d5da59e3ed6b8656c3438fb9121819e18079d47..2af18f5b776dae045e6f097ad8a49bbce99179be 100644 --- a/api/funkwhale_api/manage/urls.py +++ b/api/funkwhale_api/manage/urls.py @@ -1,28 +1,28 @@ from django.conf.urls import include, url -from rest_framework import routers +from funkwhale_api.common import routers from . import views -federation_router = routers.SimpleRouter() +federation_router = routers.OptionalSlashRouter() federation_router.register(r"domains", views.ManageDomainViewSet, "domains") -library_router = routers.SimpleRouter() +library_router = routers.OptionalSlashRouter() library_router.register(r"albums", views.ManageAlbumViewSet, "albums") library_router.register(r"artists", views.ManageArtistViewSet, "artists") library_router.register(r"libraries", views.ManageLibraryViewSet, "libraries") library_router.register(r"tracks", views.ManageTrackViewSet, "tracks") library_router.register(r"uploads", views.ManageUploadViewSet, "uploads") -moderation_router = routers.SimpleRouter() +moderation_router = routers.OptionalSlashRouter() moderation_router.register( r"instance-policies", views.ManageInstancePolicyViewSet, "instance-policies" ) -users_router = routers.SimpleRouter() +users_router = routers.OptionalSlashRouter() users_router.register(r"users", views.ManageUserViewSet, "users") users_router.register(r"invitations", views.ManageInvitationViewSet, "invitations") -other_router = routers.SimpleRouter() +other_router = routers.OptionalSlashRouter() other_router.register(r"accounts", views.ManageActorViewSet, "accounts") urlpatterns = [ diff --git a/api/funkwhale_api/moderation/urls.py b/api/funkwhale_api/moderation/urls.py index 05d2e7a9223774db0329f66ae295255cb88d9b7a..cd3e7bc2d9b2475ec65df6f8e1fd2fbfe77cb9da 100644 --- a/api/funkwhale_api/moderation/urls.py +++ b/api/funkwhale_api/moderation/urls.py @@ -1,8 +1,8 @@ -from rest_framework import routers +from funkwhale_api.common import routers from . import views -router = routers.SimpleRouter() +router = routers.OptionalSlashRouter() router.register(r"content-filters", views.UserFilterViewSet, "content-filters") urlpatterns = router.urls diff --git a/api/funkwhale_api/musicbrainz/urls.py b/api/funkwhale_api/musicbrainz/urls.py index d14447f14a62a73a494e9c6752d67274824f18ed..f4ced5b005dffdf03184a4269be0469d0d0cdc48 100644 --- a/api/funkwhale_api/musicbrainz/urls.py +++ b/api/funkwhale_api/musicbrainz/urls.py @@ -1,9 +1,9 @@ from django.conf.urls import url -from rest_framework import routers +from funkwhale_api.common import routers from . import views -router = routers.SimpleRouter() +router = routers.OptionalSlashRouter() router.register(r"search", views.SearchViewSet, "search") urlpatterns = [ url( diff --git a/api/funkwhale_api/radios/urls.py b/api/funkwhale_api/radios/urls.py index 8b9fd52c8a440d7ec41a8afa1e703f8b2a823143..4890b953f1bdce37c26a67e83bf535703edce1ce 100644 --- a/api/funkwhale_api/radios/urls.py +++ b/api/funkwhale_api/radios/urls.py @@ -1,8 +1,8 @@ -from rest_framework import routers +from funkwhale_api.common import routers from . import views -router = routers.SimpleRouter() +router = routers.OptionalSlashRouter() router.register(r"sessions", views.RadioSessionViewSet, "sessions") router.register(r"radios", views.RadioViewSet, "radios") router.register(r"tracks", views.RadioSessionTrackViewSet, "tracks") diff --git a/api/funkwhale_api/users/api_urls.py b/api/funkwhale_api/users/api_urls.py index 267ee2d69ad6dc1dd9d1b874072e415a9bdda41d..89930f57be2bad970f17e0a46ef3c3bcbe3e1468 100644 --- a/api/funkwhale_api/users/api_urls.py +++ b/api/funkwhale_api/users/api_urls.py @@ -1,8 +1,8 @@ -from rest_framework import routers +from funkwhale_api.common import routers from . import views -router = routers.SimpleRouter() +router = routers.OptionalSlashRouter() router.register(r"users", views.UserViewSet, "users") urlpatterns = router.urls diff --git a/api/funkwhale_api/users/oauth/urls.py b/api/funkwhale_api/users/oauth/urls.py index 832f9ca1ba6cfd64a673213961ed35942aef3c98..4230668e4a1463237ce0f72f90ac23d6d18f3e18 100644 --- a/api/funkwhale_api/users/oauth/urls.py +++ b/api/funkwhale_api/users/oauth/urls.py @@ -1,11 +1,11 @@ from django.conf.urls import url from django.views.decorators.csrf import csrf_exempt -from rest_framework import routers +from funkwhale_api.common import routers from . import views -router = routers.SimpleRouter() +router = routers.OptionalSlashRouter() router.register(r"apps", views.ApplicationViewSet, "apps") router.register(r"grants", views.GrantViewSet, "grants") diff --git a/api/funkwhale_api/users/rest_auth_urls.py b/api/funkwhale_api/users/rest_auth_urls.py index 732a3bbbcead15c5603f16872a97c6703dcbecca..0421473ab14d6e92f628776e7fbd38f438ed30cf 100644 --- a/api/funkwhale_api/users/rest_auth_urls.py +++ b/api/funkwhale_api/users/rest_auth_urls.py @@ -8,12 +8,12 @@ from . import views urlpatterns = [ url(r"^$", views.RegisterView.as_view(), name="rest_register"), url( - r"^verify-email/$", + r"^verify-email/?$", registration_views.VerifyEmailView.as_view(), name="rest_verify_email", ), url( - r"^change-password/$", + r"^change-password/?$", rest_auth_views.PasswordChangeView.as_view(), name="change_password", ), @@ -28,7 +28,7 @@ urlpatterns = [ # view from: # djang-allauth https://github.com/pennersr/django-allauth/blob/master/allauth/account/views.py#L190 url( - r"^account-confirm-email/(?P<key>\w+)/$", + r"^account-confirm-email/(?P<key>\w+)/?$", TemplateView.as_view(), name="account_confirm_email", ), diff --git a/api/tests/common/test_routers.py b/api/tests/common/test_routers.py new file mode 100644 index 0000000000000000000000000000000000000000..3bd5c4e47f5f823ef75e5d07fd9ce9514d67ab4f --- /dev/null +++ b/api/tests/common/test_routers.py @@ -0,0 +1,34 @@ +import pytest +from django import urls + + +@pytest.mark.parametrize( + "url", + [ + "/api/v1/artists", + "/api/v1/albums", + "/api/v1/tracks", + "/api/v1/libraries", + "/api/v1/uploads", + "/api/v1/playlists", + "/api/v1/favorites/tracks", + "/api/v1/auth/registration/verify-email", + "/api/v1/auth/registration/change-password", + "/api/v1/auth/registration/account-confirm-email/key", + "/api/v1/history/listenings", + "/api/v1/radios/sessions", + "/api/v1/users/users/me", + "/api/v1/federation/follows/library", + "/api/v1/manage/accounts", + "/api/v1/oauth/apps", + "/api/v1/moderation/content-filters", + "/api/v1/token", + "/api/v1/token/refresh", + "/api/v1/instance/settings", + "/api/v1/instance/nodeinfo/2.0", + ], +) +@pytest.mark.parametrize("suffix", ["", "/"]) +def test_optional_trailing_slash(url, suffix): + match = urls.resolve(url + suffix) + assert match is not None diff --git a/changes/changelog.d/877.enhancement b/changes/changelog.d/877.enhancement new file mode 100644 index 0000000000000000000000000000000000000000..8f65920c88d5321705b41144dabc7ceb87df4258 --- /dev/null +++ b/changes/changelog.d/877.enhancement @@ -0,0 +1 @@ +Ensure API urls answer with and without a trailing slash (#877)