From 24cb1d95191d3bc83278ff30da37c2b4b2a35534 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Wed, 9 May 2018 22:18:33 +0200 Subject: [PATCH] See #75: user can now manage the Subsonic API token from their settings page --- api/funkwhale_api/users/views.py | 26 +++- api/tests/subsonic/test_views.py | 7 + api/tests/users/test_views.py | 71 +++++++++ front/src/components/auth/Settings.vue | 27 +++- .../src/components/auth/SubsonicTokenForm.vue | 137 ++++++++++++++++++ 5 files changed, 264 insertions(+), 4 deletions(-) create mode 100644 front/src/components/auth/SubsonicTokenForm.vue diff --git a/api/funkwhale_api/users/views.py b/api/funkwhale_api/users/views.py index 7c58363a3..0cc317889 100644 --- a/api/funkwhale_api/users/views.py +++ b/api/funkwhale_api/users/views.py @@ -1,11 +1,13 @@ from rest_framework.response import Response from rest_framework import mixins from rest_framework import viewsets -from rest_framework.decorators import list_route +from rest_framework.decorators import detail_route, list_route from rest_auth.registration.views import RegisterView as BaseRegisterView from allauth.account.adapter import get_adapter +from funkwhale_api.common import preferences + from . import models from . import serializers @@ -37,6 +39,28 @@ class UserViewSet( serializer = serializers.UserReadSerializer(request.user) return Response(serializer.data) + @detail_route( + methods=['get', 'post', 'delete'], url_path='subsonic-token') + def subsonic_token(self, request, *args, **kwargs): + if not self.request.user.username == kwargs.get('username'): + return Response(status=403) + if not preferences.get('subsonic__enabled'): + return Response(status=405) + if request.method.lower() == 'get': + return Response({ + 'subsonic_api_token': self.request.user.subsonic_api_token + }) + if request.method.lower() == 'delete': + self.request.user.subsonic_api_token = None + self.request.user.save(update_fields=['subsonic_api_token']) + return Response(status=204) + self.request.user.update_subsonic_api_token() + self.request.user.save(update_fields=['subsonic_api_token']) + data = { + 'subsonic_api_token': self.request.user.subsonic_api_token + } + return Response(data) + def update(self, request, *args, **kwargs): if not self.request.user.username == kwargs.get('username'): return Response(status=403) diff --git a/api/tests/subsonic/test_views.py b/api/tests/subsonic/test_views.py index b69be0d44..bd445e070 100644 --- a/api/tests/subsonic/test_views.py +++ b/api/tests/subsonic/test_views.py @@ -45,6 +45,13 @@ def test_exception_wrong_credentials(f, db, api_client): assert response.data == expected +def test_disabled_subsonic(preferences, api_client): + preferences['subsonic__enabled'] = False + url = reverse('api:subsonic-ping') + response = api_client.get(url) + assert response.status_code == 405 + + @pytest.mark.parametrize('f', ['xml', 'json']) def test_get_license(f, db, logged_in_api_client, mocker): url = reverse('api:subsonic-get-license') diff --git a/api/tests/users/test_views.py b/api/tests/users/test_views.py index 985a78c8a..fffc762fd 100644 --- a/api/tests/users/test_views.py +++ b/api/tests/users/test_views.py @@ -167,6 +167,77 @@ def test_user_can_patch_his_own_settings(logged_in_api_client): assert user.privacy_level == 'me' +def test_user_can_request_new_subsonic_token(logged_in_api_client): + user = logged_in_api_client.user + user.subsonic_api_token = 'test' + user.save() + + url = reverse( + 'api:v1:users:users-subsonic-token', + kwargs={'username': user.username}) + + response = logged_in_api_client.post(url) + + assert response.status_code == 200 + user.refresh_from_db() + assert user.subsonic_api_token != 'test' + assert user.subsonic_api_token is not None + assert response.data == { + 'subsonic_api_token': user.subsonic_api_token + } + + +def test_user_can_get_new_subsonic_token(logged_in_api_client): + user = logged_in_api_client.user + user.subsonic_api_token = 'test' + user.save() + + url = reverse( + 'api:v1:users:users-subsonic-token', + kwargs={'username': user.username}) + + response = logged_in_api_client.get(url) + + assert response.status_code == 200 + assert response.data == { + 'subsonic_api_token': 'test' + } +def test_user_can_request_new_subsonic_token(logged_in_api_client): + user = logged_in_api_client.user + user.subsonic_api_token = 'test' + user.save() + + url = reverse( + 'api:v1:users:users-subsonic-token', + kwargs={'username': user.username}) + + response = logged_in_api_client.post(url) + + assert response.status_code == 200 + user.refresh_from_db() + assert user.subsonic_api_token != 'test' + assert user.subsonic_api_token is not None + assert response.data == { + 'subsonic_api_token': user.subsonic_api_token + } + + +def test_user_can_delete_subsonic_token(logged_in_api_client): + user = logged_in_api_client.user + user.subsonic_api_token = 'test' + user.save() + + url = reverse( + 'api:v1:users:users-subsonic-token', + kwargs={'username': user.username}) + + response = logged_in_api_client.delete(url) + + assert response.status_code == 204 + user.refresh_from_db() + assert user.subsonic_api_token is None + + @pytest.mark.parametrize('method', ['put', 'patch']) def test_user_cannot_patch_another_user( method, logged_in_api_client, factories): diff --git a/front/src/components/auth/Settings.vue b/front/src/components/auth/Settings.vue index 8eeae85a9..5468358ae 100644 --- a/front/src/components/auth/Settings.vue +++ b/front/src/components/auth/Settings.vue @@ -26,6 +26,10 @@

+
+ {{ $t('Changing your password will also change your Subsonic API password if you have requested one.') }} + {{ $t('You will have to update your password on your clients that use this password.') }} +
@@ -41,10 +45,25 @@
-
- + + {{ $t('Change password') }} +

{{ $t('Change your password?') }}

+
+

{{ $t("Changing your password will have the following consequences") }}

+
    +
  • {{ $t('You will be logged out from this session and have to log out with the new one') }}
  • +
  • {{ $t('Your Subsonic password will be changed to a new, random one, logging you out from devices that used the old Subsonic password') }}
  • +
+
+

{{ $t('Disable access') }}

+
+
@@ -55,10 +74,12 @@ import $ from 'jquery' import axios from 'axios' import logger from '@/logging' import PasswordInput from '@/components/forms/PasswordInput' +import SubsonicTokenForm from '@/components/auth/SubsonicTokenForm' export default { components: { - PasswordInput + PasswordInput, + SubsonicTokenForm }, data () { let d = { diff --git a/front/src/components/auth/SubsonicTokenForm.vue b/front/src/components/auth/SubsonicTokenForm.vue new file mode 100644 index 000000000..dd0bd5cae --- /dev/null +++ b/front/src/components/auth/SubsonicTokenForm.vue @@ -0,0 +1,137 @@ + + + + + +