From 33eecf55cb536298cfe160fd75aa258879b4930b Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Wed, 28 Jun 2017 19:34:05 +0200 Subject: [PATCH 01/11] Fixed #26: can now reorder tracks in queue using drag and drop --- front/package.json | 3 +- front/src/audio/queue.js | 18 +++++++++++ front/src/components/Sidebar.vue | 51 ++++++++++++++++++-------------- 3 files changed, 49 insertions(+), 23 deletions(-) diff --git a/front/package.json b/front/package.json index 732fdb406..7cec50319 100644 --- a/front/package.json +++ b/front/package.json @@ -19,7 +19,8 @@ "semantic-ui-css": "^2.2.10", "vue": "^2.3.3", "vue-resource": "^1.3.4", - "vue-router": "^2.3.1" + "vue-router": "^2.3.1", + "vuedraggable": "^2.14.1" }, "devDependencies": { "autoprefixer": "^6.7.2", diff --git a/front/src/audio/queue.js b/front/src/audio/queue.js index ba0af486f..c91c1d2ac 100644 --- a/front/src/audio/queue.js +++ b/front/src/audio/queue.js @@ -92,6 +92,24 @@ class Queue { } cache.set('volume', newValue) } + + reorder (oldIndex, newIndex) { + // called when the user uses drag / drop to reorder + // tracks in queue + if (oldIndex === this.currentIndex) { + this.currentIndex = newIndex + return + } + if (oldIndex < this.currentIndex && newIndex >= this.currentIndex) { + // item before was moved after + this.currentIndex -= 1 + } + if (oldIndex > this.currentIndex && newIndex <= this.currentIndex) { + // item after was moved before + this.currentIndex += 1 + } + } + append (track, index) { this.previousQueue = null index = index || this.tracks.length diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue index c98dc2f01..90e6d2d06 100644 --- a/front/src/components/Sidebar.vue +++ b/front/src/components/Sidebar.vue @@ -50,27 +50,27 @@
- - - - - - - - + + + + + + + +
{{ index + 1}} - - - - {{ track.title }}
- {{ track.artist.name }} -
- - - -
{{ index + 1}} + + + + {{ track.title }}
+ {{ track.artist.name }} +
+ + + +
@@ -98,6 +98,7 @@ import SearchBar from '@/components/audio/SearchBar' import auth from '@/auth' import queue from '@/audio/queue' import backend from '@/audio/backend' +import draggable from 'vuedraggable' import radios from '@/radios' import $ from 'jquery' @@ -107,7 +108,8 @@ export default { components: { Player, SearchBar, - Logo + Logo, + draggable }, data () { return { @@ -120,6 +122,11 @@ export default { }, mounted () { $(this.$el).find('.menu .item').tab() + }, + methods: { + reorder (e) { + this.queue.reorder(e.oldIndex, e.newIndex) + } } } From 005e3291a213894280f2b1b59f6f4d5116889423 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Wed, 28 Jun 2017 19:49:51 +0200 Subject: [PATCH 02/11] Fixed #27: now include compiled frontend files in docker container --- .gitlab-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b85d6ac3b..85a1e6eb7 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -70,6 +70,7 @@ docker_develop: stage: deploy before_script: - docker login -u $DOCKER_LOGIN -p $DOCKER_PASSWORD + - cp front/dist api/frontend - cd api script: - docker build -t $IMAGE . @@ -83,6 +84,7 @@ docker_release: stage: deploy before_script: - docker login -u $DOCKER_LOGIN -p $DOCKER_PASSWORD + - cp front/dist api/frontend - cd api script: - docker build -t $IMAGE -t $IMAGE_LATEST . From 0db752e0dfc5a002023cbbd6457dfa398736de15 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Wed, 28 Jun 2017 20:11:06 +0200 Subject: [PATCH 03/11] Missing -r flag on cp statement --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 85a1e6eb7..58602d296 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -70,7 +70,7 @@ docker_develop: stage: deploy before_script: - docker login -u $DOCKER_LOGIN -p $DOCKER_PASSWORD - - cp front/dist api/frontend + - cp -r front/dist api/frontend - cd api script: - docker build -t $IMAGE . @@ -84,7 +84,7 @@ docker_release: stage: deploy before_script: - docker login -u $DOCKER_LOGIN -p $DOCKER_PASSWORD - - cp front/dist api/frontend + - cp -r front/dist api/frontend - cd api script: - docker build -t $IMAGE -t $IMAGE_LATEST . From 38f22dab944c5157d197bfbf6a74461e8eee6bc0 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Wed, 28 Jun 2017 20:21:55 +0200 Subject: [PATCH 04/11] Should now copy the frontend files in a dedicated directory on container startup so they can be reached from outside --- api/compose/django/entrypoint.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/api/compose/django/entrypoint.sh b/api/compose/django/entrypoint.sh index a4060f658..98b3681e1 100755 --- a/api/compose/django/entrypoint.sh +++ b/api/compose/django/entrypoint.sh @@ -9,10 +9,15 @@ export REDIS_URL=redis://redis:6379/0 # the official postgres image uses 'postgres' as default user if not set explictly. if [ -z "$POSTGRES_ENV_POSTGRES_USER" ]; then export POSTGRES_ENV_POSTGRES_USER=postgres -fi +fi export DATABASE_URL=postgres://$POSTGRES_ENV_POSTGRES_USER:$POSTGRES_ENV_POSTGRES_PASSWORD@postgres:5432/$POSTGRES_ENV_POSTGRES_USER export CELERY_BROKER_URL=$REDIS_URL -exec "$@" \ No newline at end of file +# we copy the frontend files, if any so we can serve them from the outside +if [ -d "frontend" ]; then + mkdir -p /frontend + cp -r frontend/* /frontend/ +fi +exec "$@" From cfacd9f770ebdf2d7d885331c0bc1e355dcfd6be Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Wed, 28 Jun 2017 20:30:52 +0200 Subject: [PATCH 05/11] Updated sample nginx, docker-compose.yml and documentation to match easier setup for front-end files --- deploy/docker-compose.yml | 1 + docs/installation/docker.rst | 2 +- docs/installation/index.rst | 5 +++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 1a1b81b39..fadf73d7f 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -46,6 +46,7 @@ services: - ./data/music:/music:ro - ./data/media:/app/funkwhale_api/media - ./data/static:/app/staticfiles + - ./front/dist:/frontend ports: - "${FUNKWHALE_API_IP:-127.0.0.1}:${FUNKWHALE_API_PORT:-5000}:5000" links: diff --git a/docs/installation/docker.rst b/docs/installation/docker.rst index 9f7a288f3..76958fb0b 100644 --- a/docs/installation/docker.rst +++ b/docs/installation/docker.rst @@ -46,7 +46,7 @@ Then launch the whole thing: docker-compose up -d -Now, you just need to setup the :ref:`frontend files `, and configure your :ref:`reverse-proxy `. Don't worry, it's quite easy. +Now, you just need to configure your :ref:`reverse-proxy `. Don't worry, it's quite easy. About music acquisition ----------------------- diff --git a/docs/installation/index.rst b/docs/installation/index.rst index 33ac3bb75..1544dfbf0 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -26,6 +26,11 @@ Available installation methods Frontend setup --------------- +.. note:: + + You do not need to do this if you are deploying using Docker, as frontend files + are already included in the funkwhale docker image. + Files for the web frontend are purely static and can simply be downloaded, unzipped and served from any webserver: .. code-block:: bash From e2507598801f0e8e20d0088c5876a9daa66380d7 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Wed, 28 Jun 2017 20:41:25 +0200 Subject: [PATCH 06/11] Missing volumes on sample docker compose --- deploy/docker-compose.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index fadf73d7f..4ffede783 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -6,13 +6,15 @@ services: restart: unless-stopped env_file: .env image: postgres:9.4 + volumes: + - ./data/postgres:/var/lib/postgresql/data redis: restart: unless-stopped env_file: .env image: redis:3 volumes: - - ./data:/data + - ./data/redis:/data celeryworker: restart: unless-stopped From bf12a6358aa3fa1f4cf30954a4a253eefb736b75 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Wed, 28 Jun 2017 22:12:37 +0200 Subject: [PATCH 07/11] Fixed #28: added project links in footer --- front/src/App.vue | 28 ++++++++++++++++++++++++++-- front/src/components/Home.vue | 2 +- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/front/src/App.vue b/front/src/App.vue index 2704ad151..f81d7d3da 100644 --- a/front/src/App.vue +++ b/front/src/App.vue @@ -2,6 +2,26 @@
+
+
@@ -27,12 +47,16 @@ export default { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } -.main.pusher { +.main.pusher, .footer { margin-left: 350px !important; transform: none !important; +} +.main-pusher { padding: 1.5rem 0; } - +#footer { + padding: 1.5rem; +} .ui.stripe.segment { padding: 4em; } diff --git a/front/src/components/Home.vue b/front/src/components/Home.vue index 891e99ae0..ea0f5edc0 100644 --- a/front/src/components/Home.vue +++ b/front/src/components/Home.vue @@ -60,7 +60,7 @@

Clean library

-

Funkwhale takes care of fealing your music.

+

Funkwhale takes care of handling your music.

From bab3981d253ed58bce8757df23b15a638158d6f9 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Wed, 28 Jun 2017 23:30:26 +0200 Subject: [PATCH 08/11] Fixed #15: Ensure we check for authorization for serving audio files, meaning we don't leak the absolute URL anymore --- api/compose/nginx/Dockerfile | 2 - api/config/api_urls.py | 1 + api/config/settings/common.py | 12 +++++- api/funkwhale_api/music/models.py | 7 ++-- api/funkwhale_api/music/tests/factories.py | 39 +++++++++++++++++++ api/funkwhale_api/music/tests/test_api.py | 25 ++++++++++++ api/funkwhale_api/music/views.py | 25 ++++++++++++ deploy/nginx.conf | 10 +++++ dev.yml | 20 +++++----- .../nginx/nginx.conf => docker/nginx/conf.dev | 22 ++++------- docs/changelog.rst | 14 +++++++ 11 files changed, 147 insertions(+), 30 deletions(-) delete mode 100644 api/compose/nginx/Dockerfile create mode 100644 api/funkwhale_api/music/tests/factories.py rename api/compose/nginx/nginx.conf => docker/nginx/conf.dev (72%) diff --git a/api/compose/nginx/Dockerfile b/api/compose/nginx/Dockerfile deleted file mode 100644 index 196395763..000000000 --- a/api/compose/nginx/Dockerfile +++ /dev/null @@ -1,2 +0,0 @@ -FROM nginx:latest -ADD nginx.conf /etc/nginx/nginx.conf \ No newline at end of file diff --git a/api/config/api_urls.py b/api/config/api_urls.py index b56944d4e..9c612c94d 100644 --- a/api/config/api_urls.py +++ b/api/config/api_urls.py @@ -8,6 +8,7 @@ from rest_framework_jwt import views as jwt_views router = routers.SimpleRouter() router.register(r'tags', views.TagViewSet, 'tags') router.register(r'tracks', views.TrackViewSet, 'tracks') +router.register(r'trackfiles', views.TrackFileViewSet, 'trackfiles') router.register(r'artists', views.ArtistViewSet, 'artists') router.register(r'albums', views.AlbumViewSet, 'albums') router.register(r'import-batches', views.ImportBatchViewSet, 'import-batches') diff --git a/api/config/settings/common.py b/api/config/settings/common.py index 93381c4f5..5ba197145 100644 --- a/api/config/settings/common.py +++ b/api/config/settings/common.py @@ -217,7 +217,6 @@ STATICFILES_FINDERS = ( # See: https://docs.djangoproject.com/en/dev/ref/settings/#media-root MEDIA_ROOT = str(APPS_DIR('media')) -USE_SAMPLE_TRACK = env.bool("USE_SAMPLE_TRACK", False) # See: https://docs.djangoproject.com/en/dev/ref/settings/#media-url @@ -261,7 +260,6 @@ BROKER_URL = env("CELERY_BROKER_URL", default='django://') # Location of root django.contrib.admin URL, use {% url 'admin:index' %} ADMIN_URL = r'^admin/' -SESSION_SAVE_EVERY_REQUEST = True # Your common stuff: Below this line define 3rd party library settings CELERY_DEFAULT_RATE_LIMIT = 1 CELERYD_TASK_TIME_LIMIT = 300 @@ -305,3 +303,13 @@ FUNKWHALE_PROVIDERS = { } } ATOMIC_REQUESTS = False + +# Wether we should check user permission before serving audio files (meaning +# return an obfuscated url) +# This require a special configuration on the reverse proxy side +# See https://wellfire.co/learn/nginx-django-x-accel-redirects/ for example +PROTECT_AUDIO_FILES = env.bool('PROTECT_AUDIO_FILES', default=True) + +# Which path will be used to process the internal redirection +# **DO NOT** put a slash at the end +PROTECT_FILES_PATH = env('PROTECT_FILES_PATH', default='/_protected') diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py index 702308477..6a55dfc00 100644 --- a/api/funkwhale_api/music/models.py +++ b/api/funkwhale_api/music/models.py @@ -8,7 +8,6 @@ import markdown from django.conf import settings from django.db import models -from django.contrib.staticfiles.templatetags.staticfiles import static from django.core.files.base import ContentFile from django.core.files import File from django.core.urlresolvers import reverse @@ -354,10 +353,12 @@ class TrackFile(models.Model): @property def path(self): - if settings.USE_SAMPLE_TRACK: - return static('music/sample1.ogg') + if settings.PROTECT_AUDIO_FILES: + return reverse( + 'api:v1:trackfiles-serve', kwargs={'pk': self.pk}) return self.audio_file.url + class ImportBatch(models.Model): creation_date = models.DateTimeField(default=timezone.now) submitted_by = models.ForeignKey('users.User', related_name='imports') diff --git a/api/funkwhale_api/music/tests/factories.py b/api/funkwhale_api/music/tests/factories.py new file mode 100644 index 000000000..dfa7a75e2 --- /dev/null +++ b/api/funkwhale_api/music/tests/factories.py @@ -0,0 +1,39 @@ +import factory + + +class ArtistFactory(factory.django.DjangoModelFactory): + name = factory.Sequence(lambda n: 'artist-{0}'.format(n)) + mbid = factory.Faker('uuid4') + + class Meta: + model = 'music.Artist' + + +class AlbumFactory(factory.django.DjangoModelFactory): + title = factory.Sequence(lambda n: 'album-{0}'.format(n)) + mbid = factory.Faker('uuid4') + release_date = factory.Faker('date') + cover = factory.django.ImageField() + artist = factory.SubFactory(ArtistFactory) + + class Meta: + model = 'music.Album' + + +class TrackFactory(factory.django.DjangoModelFactory): + title = factory.Sequence(lambda n: 'track-{0}'.format(n)) + mbid = factory.Faker('uuid4') + album = factory.SubFactory(AlbumFactory) + artist = factory.SelfAttribute('album.artist') + position = 1 + + class Meta: + model = 'music.Track' + + +class TrackFileFactory(factory.django.DjangoModelFactory): + track = factory.SubFactory(TrackFactory) + audio_file = factory.django.FileField() + + class Meta: + model = 'music.TrackFile' diff --git a/api/funkwhale_api/music/tests/test_api.py b/api/funkwhale_api/music/tests/test_api.py index d8f56eeb9..21a567084 100644 --- a/api/funkwhale_api/music/tests/test_api.py +++ b/api/funkwhale_api/music/tests/test_api.py @@ -10,6 +10,8 @@ from funkwhale_api.music import serializers from funkwhale_api.users.models import User from . import data as api_data +from . import factories + class TestAPI(TMPDirTestCaseMixin, TestCase): @@ -214,3 +216,26 @@ class TestAPI(TMPDirTestCaseMixin, TestCase): with self.settings(API_AUTHENTICATION_REQUIRED=False): response = getattr(self.client, method)(url) self.assertEqual(response.status_code, 200) + + def test_track_file_url_is_restricted_to_authenticated_users(self): + f = factories.TrackFileFactory() + self.assertNotEqual(f.audio_file, None) + url = f.path + + with self.settings(API_AUTHENTICATION_REQUIRED=True): + response = self.client.get(url) + + self.assertEqual(response.status_code, 401) + + user = User.objects.create_superuser( + username='test', email='test@test.com', password='test') + self.client.login(username=user.username, password='test') + with self.settings(API_AUTHENTICATION_REQUIRED=True): + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + + self.assertEqual( + response['X-Accel-Redirect'], + '/_protected{}'.format(f.audio_file.url) + ) diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py index 772f4173e..506db1239 100644 --- a/api/funkwhale_api/music/views.py +++ b/api/funkwhale_api/music/views.py @@ -3,6 +3,7 @@ import json from django.core.urlresolvers import reverse from django.db import models, transaction from django.db.models.functions import Length +from django.conf import settings from rest_framework import viewsets, views from rest_framework.decorators import detail_route, list_route from rest_framework.response import Response @@ -51,6 +52,7 @@ class ArtistViewSet(SearchMixin, viewsets.ReadOnlyModelViewSet): search_fields = ['name'] ordering_fields = ('creation_date',) + class AlbumViewSet(SearchMixin, viewsets.ReadOnlyModelViewSet): queryset = ( models.Album.objects.all() @@ -63,6 +65,7 @@ class AlbumViewSet(SearchMixin, viewsets.ReadOnlyModelViewSet): search_fields = ['title'] ordering_fields = ('creation_date',) + class ImportBatchViewSet(viewsets.ReadOnlyModelViewSet): queryset = models.ImportBatch.objects.all().order_by('-creation_date') serializer_class = serializers.ImportBatchSerializer @@ -70,6 +73,7 @@ class ImportBatchViewSet(viewsets.ReadOnlyModelViewSet): def get_queryset(self): return super().get_queryset().filter(submitted_by=self.request.user) + class TrackViewSet(TagViewSetMixin, SearchMixin, viewsets.ReadOnlyModelViewSet): """ A simple ViewSet for viewing and editing accounts. @@ -120,6 +124,27 @@ class TrackViewSet(TagViewSetMixin, SearchMixin, viewsets.ReadOnlyModelViewSet): return Response(serializer.data) +class TrackFileViewSet(viewsets.ReadOnlyModelViewSet): + queryset = (models.TrackFile.objects.all().order_by('-id')) + serializer_class = serializers.TrackFileSerializer + permission_classes = [ConditionalAuthentication] + + @detail_route(methods=['get']) + def serve(self, request, *args, **kwargs): + try: + f = models.TrackFile.objects.get(pk=kwargs['pk']) + except models.TrackFile.DoesNotExist: + return Response(status=404) + + response = Response() + response["Content-Disposition"] = "attachment; filename={0}".format( + f.audio_file.name) + response['X-Accel-Redirect'] = "{}{}".format( + settings.PROTECT_FILES_PATH, + f.audio_file.url) + return response + + class TagViewSet(viewsets.ReadOnlyModelViewSet): queryset = Tag.objects.all().order_by('name') serializer_class = serializers.TagSerializer diff --git a/deploy/nginx.conf b/deploy/nginx.conf index 7395e37d9..6a0a9f509 100644 --- a/deploy/nginx.conf +++ b/deploy/nginx.conf @@ -47,7 +47,17 @@ server { location /media/ { alias /srv/funkwhale/data/media/; } + + location /_protected/media { + # this is an internal location that is used to serve + # audio files once correct permission / authentication + # has been checked on API side + internal; + alias /srv/funkwhale/data/media; + } + location /staticfiles/ { + # django static files alias /srv/funkwhale/data/static/; } } diff --git a/dev.yml b/dev.yml index 21b0912e3..712288492 100644 --- a/dev.yml +++ b/dev.yml @@ -53,12 +53,14 @@ services: - redis - celeryworker - # nginx: - # env_file: .env.dev - # build: ./api/compose/nginx - # links: - # - api - # volumes: - # - ./api/funkwhale_api/media:/staticfiles/media - # ports: - # - "0.0.0.0:6001:80" + nginx: + env_file: .env.dev + image: nginx + links: + - api + - front + volumes: + - ./docker/nginx/conf.dev:/etc/nginx/nginx.conf + - ./api/funkwhale_api/media:/protected/media + ports: + - "0.0.0.0:6001:80" diff --git a/api/compose/nginx/nginx.conf b/docker/nginx/conf.dev similarity index 72% rename from api/compose/nginx/nginx.conf rename to docker/nginx/conf.dev index 331d8d45f..6ca395fb1 100644 --- a/api/compose/nginx/nginx.conf +++ b/docker/nginx/conf.dev @@ -27,27 +27,21 @@ http { #gzip on; - upstream app { - server django:12081; - } - server { listen 80; charset utf-8; - root /staticfiles; + location /_protected/media { + internal; + alias /protected/media; + } location / { - # checks for static file, if not found proxy to app - try_files $uri @proxy_to_app; - } - - location @proxy_to_app { + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-Proto $scheme; proxy_redirect off; - - proxy_pass http://app; + proxy_pass http://api:12081/; } - } } diff --git a/docs/changelog.rst b/docs/changelog.rst index c4092dc8b..ced5c0a7e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,20 @@ Changelog ========= +next +------- + +* [breaking] we now check for user permission before serving audio files, which requires +a specific configuration block in your reverse proxy configuration: + +.. code-block:: + + location /_protected/media { + internal; + alias /srv/funkwhale/data/media; + } + + 0.1 ------- From 3ccb70d0a827060e79ef3a84a5d9cbe884b1b412 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Thu, 29 Jun 2017 02:27:35 +0200 Subject: [PATCH 09/11] Fixed #15 again, now check authorization also using query param --- .env.dev | 4 +-- api/config/settings/common.py | 1 + api/funkwhale_api/common/authentication.py | 20 ++++++++++++ .../common/tests/test_jwt_querystring.py | 32 +++++++++++++++++++ api/funkwhale_api/music/views.py | 8 +++-- front/src/audio/queue.js | 14 +++++++- front/src/auth/index.js | 8 +++-- front/src/components/browse/Track.vue | 8 ++++- front/src/utils/url.js | 11 +++++++ 9 files changed, 98 insertions(+), 8 deletions(-) create mode 100644 api/funkwhale_api/common/authentication.py create mode 100644 api/funkwhale_api/common/tests/test_jwt_querystring.py create mode 100644 front/src/utils/url.js diff --git a/.env.dev b/.env.dev index de58e2758..a7413b0ff 100644 --- a/.env.dev +++ b/.env.dev @@ -1,3 +1,3 @@ -BACKEND_URL=http://localhost:12081 +BACKEND_URL=http://localhost:6001 YOUTUBE_API_KEY= -API_AUTHENTICATION_REQUIRED=False +API_AUTHENTICATION_REQUIRED=True diff --git a/api/config/settings/common.py b/api/config/settings/common.py index 5ba197145..b6e195ca2 100644 --- a/api/config/settings/common.py +++ b/api/config/settings/common.py @@ -288,6 +288,7 @@ REST_FRAMEWORK = { 'PAGE_SIZE': 25, 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'funkwhale_api.common.authentication.JSONWebTokenAuthenticationQS', 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.BasicAuthentication', diff --git a/api/funkwhale_api/common/authentication.py b/api/funkwhale_api/common/authentication.py new file mode 100644 index 000000000..b75f3b516 --- /dev/null +++ b/api/funkwhale_api/common/authentication.py @@ -0,0 +1,20 @@ +from rest_framework import exceptions +from rest_framework_jwt import authentication +from rest_framework_jwt.settings import api_settings + + +class JSONWebTokenAuthenticationQS( + authentication.BaseJSONWebTokenAuthentication): + + www_authenticate_realm = 'api' + + def get_jwt_value(self, request): + token = request.query_params.get('jwt') + if 'jwt' in request.query_params and not token: + msg = _('Invalid Authorization header. No credentials provided.') + raise exceptions.AuthenticationFailed(msg) + return token + + def authenticate_header(self, request): + return '{0} realm="{1}"'.format( + api_settings.JWT_AUTH_HEADER_PREFIX, self.www_authenticate_realm) diff --git a/api/funkwhale_api/common/tests/test_jwt_querystring.py b/api/funkwhale_api/common/tests/test_jwt_querystring.py new file mode 100644 index 000000000..90e63775d --- /dev/null +++ b/api/funkwhale_api/common/tests/test_jwt_querystring.py @@ -0,0 +1,32 @@ +from test_plus.test import TestCase +from rest_framework_jwt.settings import api_settings + +from funkwhale_api.users.models import User + + +jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER +jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER + + +class TestJWTQueryString(TestCase): + www_authenticate_realm = 'api' + + def test_can_authenticate_using_token_param_in_url(self): + user = User.objects.create_superuser( + username='test', email='test@test.com', password='test') + + url = self.reverse('api:v1:tracks-list') + with self.settings(API_AUTHENTICATION_REQUIRED=True): + response = self.client.get(url) + + self.assertEqual(response.status_code, 401) + + payload = jwt_payload_handler(user) + token = jwt_encode_handler(payload) + print(payload, token) + with self.settings(API_AUTHENTICATION_REQUIRED=True): + response = self.client.get(url, data={ + 'jwt': token + }) + + self.assertEqual(response.status_code, 200) diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py index 506db1239..4a4032c57 100644 --- a/api/funkwhale_api/music/views.py +++ b/api/funkwhale_api/music/views.py @@ -1,5 +1,7 @@ import os import json +import unicodedata +import urllib from django.core.urlresolvers import reverse from django.db import models, transaction from django.db.models.functions import Length @@ -137,8 +139,10 @@ class TrackFileViewSet(viewsets.ReadOnlyModelViewSet): return Response(status=404) response = Response() - response["Content-Disposition"] = "attachment; filename={0}".format( - f.audio_file.name) + filename = "filename*=UTF-8''{}{}".format( + urllib.parse.quote(f.track.full_name), + os.path.splitext(f.audio_file.name)[-1]) + response["Content-Disposition"] = "attachment; {}".format(filename) response['X-Accel-Redirect'] = "{}{}".format( settings.PROTECT_FILES_PATH, f.audio_file.url) diff --git a/front/src/audio/queue.js b/front/src/audio/queue.js index c91c1d2ac..efa3dcdf7 100644 --- a/front/src/audio/queue.js +++ b/front/src/audio/queue.js @@ -5,6 +5,8 @@ import Audio from '@/audio' import backend from '@/audio/backend' import radios from '@/radios' import Vue from 'vue' +import url from '@/utils/url' +import auth from '@/auth' class Queue { constructor (options = {}) { @@ -181,7 +183,17 @@ class Queue { if (!file) { return this.next() } - this.audio = new Audio(backend.absoluteUrl(file.path), { + let path = backend.absoluteUrl(file.path) + + if (auth.user.authenticated) { + // we need to send the token directly in url + // so authentication can be checked by the backend + // because for audio files we cannot use the regular Authentication + // header + path = url.updateQueryString(path, 'jwt', auth.getAuthToken()) + } + + this.audio = new Audio(path, { preload: true, autoplay: true, rate: 1, diff --git a/front/src/auth/index.js b/front/src/auth/index.js index b5a3fb5ad..219a1531f 100644 --- a/front/src/auth/index.js +++ b/front/src/auth/index.js @@ -50,7 +50,7 @@ export default { checkAuth () { logger.default.info('Checking authentication...') - var jwt = cache.get('token') + var jwt = this.getAuthToken() var username = cache.get('username') if (jwt) { this.user.authenticated = true @@ -63,9 +63,13 @@ export default { } }, + getAuthToken () { + return cache.get('token') + }, + // The object to be passed as a header for authenticated requests getAuthHeader () { - return 'JWT ' + cache.get('token') + return 'JWT ' + this.getAuthToken() }, fetchProfile () { diff --git a/front/src/components/browse/Track.vue b/front/src/components/browse/Track.vue index 336af285b..1e1568793 100644 --- a/front/src/components/browse/Track.vue +++ b/front/src/components/browse/Track.vue @@ -61,6 +61,8 @@ diff --git a/front/src/components/library/import/BatchList.vue b/front/src/components/library/import/BatchList.vue new file mode 100644 index 000000000..41b94bd4e --- /dev/null +++ b/front/src/components/library/import/BatchList.vue @@ -0,0 +1,80 @@ + + + + + + diff --git a/front/src/components/library/import/ImportMixin.vue b/front/src/components/library/import/ImportMixin.vue new file mode 100644 index 000000000..f3fc6fca6 --- /dev/null +++ b/front/src/components/library/import/ImportMixin.vue @@ -0,0 +1,81 @@ + + + + + + diff --git a/front/src/components/library/import/Main.vue b/front/src/components/library/import/Main.vue new file mode 100644 index 000000000..10f6f352a --- /dev/null +++ b/front/src/components/library/import/Main.vue @@ -0,0 +1,231 @@ + + + + + diff --git a/front/src/components/library/import/ReleaseImport.vue b/front/src/components/library/import/ReleaseImport.vue new file mode 100644 index 000000000..9f8c1d347 --- /dev/null +++ b/front/src/components/library/import/ReleaseImport.vue @@ -0,0 +1,113 @@ + + + + + + diff --git a/front/src/components/library/import/TrackImport.vue b/front/src/components/library/import/TrackImport.vue new file mode 100644 index 000000000..3081091c5 --- /dev/null +++ b/front/src/components/library/import/TrackImport.vue @@ -0,0 +1,188 @@ + + + + + + + diff --git a/front/src/components/metadata/ArtistCard.vue b/front/src/components/metadata/ArtistCard.vue new file mode 100644 index 000000000..3a50a3155 --- /dev/null +++ b/front/src/components/metadata/ArtistCard.vue @@ -0,0 +1,64 @@ + + + + + + diff --git a/front/src/components/metadata/CardMixin.vue b/front/src/components/metadata/CardMixin.vue new file mode 100644 index 000000000..78aae5e7e --- /dev/null +++ b/front/src/components/metadata/CardMixin.vue @@ -0,0 +1,50 @@ + + + + + + diff --git a/front/src/components/metadata/ReleaseCard.vue b/front/src/components/metadata/ReleaseCard.vue new file mode 100644 index 000000000..201c3ab0c --- /dev/null +++ b/front/src/components/metadata/ReleaseCard.vue @@ -0,0 +1,66 @@ + + + + + + diff --git a/front/src/components/metadata/Search.vue b/front/src/components/metadata/Search.vue new file mode 100644 index 000000000..8a400cf7b --- /dev/null +++ b/front/src/components/metadata/Search.vue @@ -0,0 +1,153 @@ + + + + + + diff --git a/front/src/router/index.js b/front/src/router/index.js index bb92b5ae1..b3d90731f 100644 --- a/front/src/router/index.js +++ b/front/src/router/index.js @@ -4,11 +4,15 @@ import Home from '@/components/Home' import Login from '@/components/auth/Login' import Profile from '@/components/auth/Profile' import Logout from '@/components/auth/Logout' -import Browse from '@/components/browse/Browse' -import BrowseHome from '@/components/browse/Home' -import BrowseArtist from '@/components/browse/Artist' -import BrowseAlbum from '@/components/browse/Album' -import BrowseTrack from '@/components/browse/Track' +import Library from '@/components/library/Library' +import LibraryHome from '@/components/library/Home' +import LibraryArtist from '@/components/library/Artist' +import LibraryAlbum from '@/components/library/Album' +import LibraryTrack from '@/components/library/Track' +import LibraryImport from '@/components/library/import/Main' +import BatchList from '@/components/library/import/BatchList' +import BatchDetail from '@/components/library/import/BatchDetail' + import Favorites from '@/components/favorites/List' Vue.use(Router) @@ -43,13 +47,27 @@ export default new Router({ component: Favorites }, { - path: '/browse', - component: Browse, + path: '/library', + component: Library, children: [ - { path: '', component: BrowseHome }, - { path: 'artist/:id', name: 'browse.artist', component: BrowseArtist, props: true }, - { path: 'album/:id', name: 'browse.album', component: BrowseAlbum, props: true }, - { path: 'track/:id', name: 'browse.track', component: BrowseTrack, props: true } + { path: '', component: LibraryHome }, + { path: 'artist/:id', name: 'library.artist', component: LibraryArtist, props: true }, + { path: 'album/:id', name: 'library.album', component: LibraryAlbum, props: true }, + { path: 'track/:id', name: 'library.track', component: LibraryTrack, props: true }, + { + path: 'import/launch', + name: 'library.import.launch', + component: LibraryImport, + props: (route) => ({ mbType: route.query.type, mbId: route.query.id }) + }, + { + path: 'import/batches', + name: 'library.import.batches', + component: BatchList, + children: [ + ] + }, + { path: 'import/batches/:id', name: 'library.import.batches.detail', component: BatchDetail, props: true } ] } diff --git a/front/src/utils/time.js b/front/src/utils/time.js new file mode 100644 index 000000000..022a365bf --- /dev/null +++ b/front/src/utils/time.js @@ -0,0 +1,16 @@ +function pad (val) { + val = Math.floor(val) + if (val < 10) { + return '0' + val + } + return val + '' +} + +export default { + parse: function (sec) { + let min = 0 + min = Math.floor(sec / 60) + sec = sec - min * 60 + return pad(min) + ':' + pad(sec) + } +} From 0b01bf3038c37a60019edb4e13d12ad25bd08419 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sun, 9 Jul 2017 11:37:47 +0200 Subject: [PATCH 11/11] Changelog && version bump --- api/funkwhale_api/__init__.py | 2 +- docs/changelog.rst | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/api/funkwhale_api/__init__.py b/api/funkwhale_api/__init__.py index 6b304975e..70d6b5ac1 100644 --- a/api/funkwhale_api/__init__.py +++ b/api/funkwhale_api/__init__.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -__version__ = '0.1.0' +__version__ = '0.2.0' __version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace('-', '.', 1).split('.')]) diff --git a/docs/changelog.rst b/docs/changelog.rst index ced5c0a7e..6e609aac9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,9 +1,13 @@ Changelog ========= -next +0.2 ------- +2017-07-09 + +* [feature] can now import artist and releases from youtube and musicbrainz. + This requires a YouTube API key for the search * [breaking] we now check for user permission before serving audio files, which requires a specific configuration block in your reverse proxy configuration: