From a2dcae0bbfac6048ce7c4bba778d562708b1b809 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sun, 6 May 2018 10:50:40 +0200 Subject: [PATCH 1/8] Fixed some blank spaces in sidebar --- front/src/components/Sidebar.vue | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue index 8e0df8016..97c743bbe 100644 --- a/front/src/components/Sidebar.vue +++ b/front/src/components/Sidebar.vue @@ -35,24 +35,24 @@
{{ $t('My account') }}
{{ $t('Music') }}
@@ -62,7 +62,7 @@ class="item" v-if="$store.state.auth.availablePermissions['import.launch']" :to="{name: 'library.requests', query: {status: 'pending' }}"> - {{ $t('Import requests') }} + {{ $t('Import requests') }}
@@ -72,7 +72,7 @@ class="item" v-if="$store.state.auth.availablePermissions['federation.manage']" :to="{path: '/manage/federation/libraries'}"> - {{ $t('Federation') }} + {{ $t('Federation') }}
From 4325b1be4f8b826804d90358d6994033a6ceacff Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sun, 6 May 2018 11:18:28 +0200 Subject: [PATCH 2/8] Removed radios and listening recording for anonymous users as it was buggy --- api/config/settings/common.py | 1 - api/funkwhale_api/history/models.py | 7 ------ api/funkwhale_api/history/serializers.py | 8 ++----- api/funkwhale_api/history/views.py | 24 +++++++++----------- api/funkwhale_api/radios/models.py | 2 -- api/funkwhale_api/radios/serializers.py | 8 +++---- api/funkwhale_api/radios/views.py | 22 ++++++------------- api/funkwhale_api/users/middleware.py | 11 ---------- api/tests/history/test_history.py | 15 ------------- api/tests/radios/test_radios.py | 28 ++++++------------------ front/src/store/player.js | 5 ++++- 11 files changed, 33 insertions(+), 98 deletions(-) delete mode 100644 api/funkwhale_api/users/middleware.py diff --git a/api/config/settings/common.py b/api/config/settings/common.py index d29480648..50bc52fe0 100644 --- a/api/config/settings/common.py +++ b/api/config/settings/common.py @@ -144,7 +144,6 @@ INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS MIDDLEWARE = ( # Make sure djangosecure.middleware.SecurityMiddleware is listed first 'django.contrib.sessions.middleware.SessionMiddleware', - 'funkwhale_api.users.middleware.AnonymousSessionMiddleware', 'corsheaders.middleware.CorsMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', diff --git a/api/funkwhale_api/history/models.py b/api/funkwhale_api/history/models.py index 762d5bf7b..480461d35 100644 --- a/api/funkwhale_api/history/models.py +++ b/api/funkwhale_api/history/models.py @@ -21,13 +21,6 @@ class Listening(models.Model): class Meta: ordering = ('-creation_date',) - def save(self, **kwargs): - if not self.user and not self.session_key: - raise ValidationError('Cannot have both session_key and user empty for listening') - - super().save(**kwargs) - - def get_activity_url(self): return '{}/listenings/tracks/{}'.format( self.user.get_activity_url(), self.pk) diff --git a/api/funkwhale_api/history/serializers.py b/api/funkwhale_api/history/serializers.py index 8fe6fa6e0..f7333f243 100644 --- a/api/funkwhale_api/history/serializers.py +++ b/api/funkwhale_api/history/serializers.py @@ -36,13 +36,9 @@ class ListeningSerializer(serializers.ModelSerializer): class Meta: model = models.Listening - fields = ('id', 'user', 'session_key', 'track', 'creation_date') - + fields = ('id', 'user', 'track', 'creation_date') def create(self, validated_data): - if self.context.get('user'): - validated_data['user'] = self.context.get('user') - else: - validated_data['session_key'] = self.context['session_key'] + validated_data['user'] = self.context['user'] return super().create(validated_data) diff --git a/api/funkwhale_api/history/views.py b/api/funkwhale_api/history/views.py index d5cbe316b..bea96a418 100644 --- a/api/funkwhale_api/history/views.py +++ b/api/funkwhale_api/history/views.py @@ -1,4 +1,5 @@ from rest_framework import generics, mixins, viewsets +from rest_framework import permissions from rest_framework import status from rest_framework.response import Response from rest_framework.decorators import detail_route @@ -10,31 +11,26 @@ from funkwhale_api.music.serializers import TrackSerializerNested from . import models from . import serializers -class ListeningViewSet(mixins.CreateModelMixin, - mixins.RetrieveModelMixin, - viewsets.GenericViewSet): + +class ListeningViewSet( + mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + viewsets.GenericViewSet): serializer_class = serializers.ListeningSerializer queryset = models.Listening.objects.all() - permission_classes = [ConditionalAuthentication] + permission_classes = [permissions.IsAuthenticated] def perform_create(self, serializer): r = super().perform_create(serializer) - if self.request.user.is_authenticated: - record.send(serializer.instance) + record.send(serializer.instance) return r def get_queryset(self): queryset = super().get_queryset() - if self.request.user.is_authenticated: - return queryset.filter(user=self.request.user) - else: - return queryset.filter(session_key=self.request.session.session_key) + return queryset.filter(user=self.request.user) def get_serializer_context(self): context = super().get_serializer_context() - if self.request.user.is_authenticated: - context['user'] = self.request.user - else: - context['session_key'] = self.request.session.session_key + context['user'] = self.request.user return context diff --git a/api/funkwhale_api/radios/models.py b/api/funkwhale_api/radios/models.py index 0273b5387..8758abc61 100644 --- a/api/funkwhale_api/radios/models.py +++ b/api/funkwhale_api/radios/models.py @@ -55,8 +55,6 @@ class RadioSession(models.Model): related_object = GenericForeignKey('related_object_content_type', 'related_object_id') def save(self, **kwargs): - if not self.user and not self.session_key: - raise ValidationError('Cannot have both session_key and user empty for radio session') self.radio.clean(self) super().save(**kwargs) diff --git a/api/funkwhale_api/radios/serializers.py b/api/funkwhale_api/radios/serializers.py index 2e7e6a409..195b382c9 100644 --- a/api/funkwhale_api/radios/serializers.py +++ b/api/funkwhale_api/radios/serializers.py @@ -38,6 +38,7 @@ class RadioSerializer(serializers.ModelSerializer): return super().save(**kwargs) + class RadioSessionTrackSerializerCreate(serializers.ModelSerializer): class Meta: model = models.RadioSessionTrack @@ -62,17 +63,14 @@ class RadioSessionSerializer(serializers.ModelSerializer): 'user', 'creation_date', 'custom_radio', - 'session_key') + ) def validate(self, data): registry[data['radio_type']]().validate_session(data, **self.context) return data def create(self, validated_data): - if self.context.get('user'): - validated_data['user'] = self.context.get('user') - else: - validated_data['session_key'] = self.context['session_key'] + validated_data['user'] = self.context['user'] if validated_data.get('related_object_id'): radio = registry[validated_data['radio_type']]() validated_data['related_object'] = radio.get_related_object(validated_data['related_object_id']) diff --git a/api/funkwhale_api/radios/views.py b/api/funkwhale_api/radios/views.py index ffd1d1659..37c07c5e4 100644 --- a/api/funkwhale_api/radios/views.py +++ b/api/funkwhale_api/radios/views.py @@ -2,6 +2,7 @@ from django.db.models import Q from django.http import Http404 from rest_framework import generics, mixins, viewsets +from rest_framework import permissions from rest_framework import status from rest_framework.response import Response from rest_framework.decorators import detail_route, list_route @@ -24,7 +25,7 @@ class RadioViewSet( viewsets.GenericViewSet): serializer_class = serializers.RadioSerializer - permission_classes = [ConditionalAuthentication] + permission_classes = [permissions.IsAuthenticated] filter_class = filtersets.RadioFilter def get_queryset(self): @@ -84,21 +85,15 @@ class RadioSessionViewSet(mixins.CreateModelMixin, serializer_class = serializers.RadioSessionSerializer queryset = models.RadioSession.objects.all() - permission_classes = [ConditionalAuthentication] + permission_classes = [permissions.IsAuthenticated] def get_queryset(self): queryset = super().get_queryset() - if self.request.user.is_authenticated: - return queryset.filter(user=self.request.user) - else: - return queryset.filter(session_key=self.request.session.session_key) + return queryset.filter(user=self.request.user) def get_serializer_context(self): context = super().get_serializer_context() - if self.request.user.is_authenticated: - context['user'] = self.request.user - else: - context['session_key'] = self.request.session.session_key + context['user'] = self.request.user return context @@ -106,17 +101,14 @@ class RadioSessionTrackViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet): serializer_class = serializers.RadioSessionTrackSerializer queryset = models.RadioSessionTrack.objects.all() - permission_classes = [ConditionalAuthentication] + permission_classes = [permissions.IsAuthenticated] def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) session = serializer.validated_data['session'] try: - if request.user.is_authenticated: - assert request.user == session.user - else: - assert request.session.session_key == session.session_key + assert request.user == session.user except AssertionError: return Response(status=status.HTTP_403_FORBIDDEN) track = session.radio.pick() diff --git a/api/funkwhale_api/users/middleware.py b/api/funkwhale_api/users/middleware.py deleted file mode 100644 index e3eba95f3..000000000 --- a/api/funkwhale_api/users/middleware.py +++ /dev/null @@ -1,11 +0,0 @@ - - -class AnonymousSessionMiddleware: - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - if not request.session.session_key: - request.session.save() - response = self.get_response(request) - return response diff --git a/api/tests/history/test_history.py b/api/tests/history/test_history.py index 563cf2f08..202725596 100644 --- a/api/tests/history/test_history.py +++ b/api/tests/history/test_history.py @@ -14,21 +14,6 @@ def test_can_create_listening(factories): l = models.Listening.objects.create(user=user, track=track) -def test_anonymous_user_can_create_listening_via_api( - client, factories, preferences): - preferences['common__api_authentication_required'] = False - track = factories['music.Track']() - url = reverse('api:v1:history:listenings-list') - response = client.post(url, { - 'track': track.pk, - }) - - listening = models.Listening.objects.latest('id') - - assert listening.track == track - assert listening.session_key == client.session.session_key - - def test_logged_in_user_can_create_listening_via_api( logged_in_client, factories, activity_muted): track = factories['music.Track']() diff --git a/api/tests/radios/test_radios.py b/api/tests/radios/test_radios.py index e78bd0e2f..b166b648c 100644 --- a/api/tests/radios/test_radios.py +++ b/api/tests/radios/test_radios.py @@ -151,20 +151,6 @@ def test_can_start_radio_for_logged_in_user(logged_in_client): assert session.user == logged_in_client.user -def test_can_start_radio_for_anonymous_user(api_client, db, preferences): - preferences['common__api_authentication_required'] = False - url = reverse('api:v1:radios:sessions-list') - response = api_client.post(url, {'radio_type': 'random'}) - - assert response.status_code == 201 - - session = models.RadioSession.objects.latest('id') - - assert session.radio_type == 'random' - assert session.user is None - assert session.session_key == api_client.session.session_key - - def test_can_get_track_for_session_from_api(factories, logged_in_client): files = factories['music.TrackFile'].create_batch(1) tracks = [f.track for f in files] @@ -227,25 +213,25 @@ def test_can_start_tag_radio(factories): radio = radios.TagRadio() session = radio.start_session(user, related_object=tag) - assert session.radio_type =='tag' + assert session.radio_type == 'tag' for i in range(5): assert radio.pick() in good_tracks -def test_can_start_artist_radio_from_api(api_client, preferences, factories): - preferences['common__api_authentication_required'] = False +def test_can_start_artist_radio_from_api( + logged_in_api_client, preferences, factories): artist = factories['music.Artist']() url = reverse('api:v1:radios:sessions-list') - response = api_client.post( + response = logged_in_api_client.post( url, {'radio_type': 'artist', 'related_object_id': artist.id}) assert response.status_code == 201 session = models.RadioSession.objects.latest('id') - assert session.radio_type, 'artist' - assert session.related_object, artist + assert session.radio_type == 'artist' + assert session.related_object == artist def test_can_start_less_listened_radio(factories): @@ -257,6 +243,6 @@ def test_can_start_less_listened_radio(factories): good_tracks = [f.track for f in good_files] radio = radios.LessListenedRadio() session = radio.start_session(user) - assert session.related_object == user + for i in range(5): assert radio.pick() in good_tracks diff --git a/front/src/store/player.js b/front/src/store/player.js index ed437c3f0..2149b51ff 100644 --- a/front/src/store/player.js +++ b/front/src/store/player.js @@ -85,7 +85,10 @@ export default { togglePlay ({commit, state}) { commit('playing', !state.playing) }, - trackListened ({commit}, track) { + trackListened ({commit, rootState}, track) { + if (!rootState.auth.authenticated) { + return + } return axios.post('history/listenings/', {'track': track.id}).then((response) => {}, (response) => { logger.default.error('Could not record track in history') }) From cd22601f6741df7ff1d3926ca2fb14f79e48c1ad Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sun, 6 May 2018 11:19:20 +0200 Subject: [PATCH 3/8] Extracted password input in a dedicated component --- front/src/components/auth/Settings.vue | 20 +++++-------- front/src/components/auth/Signup.vue | 28 +++++------------- front/src/components/forms/PasswordInput.vue | 31 ++++++++++++++++++++ 3 files changed, 46 insertions(+), 33 deletions(-) create mode 100644 front/src/components/forms/PasswordInput.vue diff --git a/front/src/components/auth/Settings.vue b/front/src/components/auth/Settings.vue index c847bde88..8eeae85a9 100644 --- a/front/src/components/auth/Settings.vue +++ b/front/src/components/auth/Settings.vue @@ -35,21 +35,13 @@
- + +
- + +
@@ -62,8 +54,12 @@ import $ from 'jquery' import axios from 'axios' import logger from '@/logging' +import PasswordInput from '@/components/forms/PasswordInput' export default { + components: { + PasswordInput + }, data () { let d = { // We need to initialize the component with any diff --git a/front/src/components/auth/Signup.vue b/front/src/components/auth/Signup.vue index 57966264f..89f4cb1f1 100644 --- a/front/src/components/auth/Signup.vue +++ b/front/src/components/auth/Signup.vue @@ -34,16 +34,7 @@
-
- - - - -
+
@@ -57,8 +48,13 @@ import axios from 'axios' import logger from '@/logging' +import PasswordInput from '@/components/forms/PasswordInput' + export default { name: 'login', + components: { + PasswordInput + }, props: { next: {type: String, default: '/'} }, @@ -69,8 +65,7 @@ export default { password: '', isLoadingInstanceSetting: true, errors: [], - isLoading: false, - showPassword: false + isLoading: false } }, created () { @@ -104,16 +99,7 @@ export default { self.isLoading = false }) } - }, - computed: { - passwordInputType () { - if (this.showPassword) { - return 'text' - } - return 'password' - } } - } diff --git a/front/src/components/forms/PasswordInput.vue b/front/src/components/forms/PasswordInput.vue new file mode 100644 index 000000000..624a92d87 --- /dev/null +++ b/front/src/components/forms/PasswordInput.vue @@ -0,0 +1,31 @@ + + From 929b50183a486ae84827b9058b5c27e0c08278e3 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sun, 6 May 2018 11:20:18 +0200 Subject: [PATCH 4/8] Now reset cookie on profile fetch, to avoid CSRF bugs --- api/config/settings/common.py | 2 +- front/src/store/auth.js | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/api/config/settings/common.py b/api/config/settings/common.py index 50bc52fe0..2e9421e79 100644 --- a/api/config/settings/common.py +++ b/api/config/settings/common.py @@ -292,7 +292,7 @@ AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend', 'allauth.account.auth_backends.AuthenticationBackend', ) - +SESSION_COOKIE_HTTPONLY = False # Some really nice defaults ACCOUNT_AUTHENTICATION_METHOD = 'username_email' ACCOUNT_EMAIL_REQUIRED = True diff --git a/front/src/store/auth.js b/front/src/store/auth.js index b1753404f..68a15090b 100644 --- a/front/src/store/auth.js +++ b/front/src/store/auth.js @@ -97,6 +97,11 @@ export default { } }, fetchProfile ({commit, dispatch, state}) { + if (document) { + // this is to ensure we do not have any leaking cookie set by django + document.cookie = 'sessionid=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;' + } + return axios.get('users/users/me/').then((response) => { logger.default.info('Successfully fetched user profile') let data = response.data From 22f0b1a2d835f476672bb927a94e8a7128fbdae5 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sun, 6 May 2018 11:30:41 +0200 Subject: [PATCH 5/8] See #187: API logic for password reset --- api/config/settings/common.py | 5 +++++ api/funkwhale_api/users/rest_auth_urls.py | 16 ++++++++++------ api/funkwhale_api/users/serializers.py | 13 ++++++++++++- api/tests/users/test_views.py | 14 ++++++++++++++ 4 files changed, 41 insertions(+), 7 deletions(-) diff --git a/api/config/settings/common.py b/api/config/settings/common.py index 2e9421e79..9c5487d64 100644 --- a/api/config/settings/common.py +++ b/api/config/settings/common.py @@ -391,6 +391,11 @@ REST_FRAMEWORK = { 'django_filters.rest_framework.DjangoFilterBackend', ) } +REST_AUTH_SERIALIZERS = { + 'PASSWORD_RESET_SERIALIZER': 'funkwhale_api.users.serializers.PasswordResetSerializer' # noqa +} +REST_SESSION_LOGIN = False +REST_USE_JWT = True ATOMIC_REQUESTS = False USE_X_FORWARDED_HOST = True diff --git a/api/funkwhale_api/users/rest_auth_urls.py b/api/funkwhale_api/users/rest_auth_urls.py index 31f5384aa..fa6c425cc 100644 --- a/api/funkwhale_api/users/rest_auth_urls.py +++ b/api/funkwhale_api/users/rest_auth_urls.py @@ -1,16 +1,20 @@ from django.views.generic import TemplateView from django.conf.urls import url -from rest_auth.registration.views import VerifyEmailView -from rest_auth.views import PasswordChangeView +from rest_auth.registration import views as registration_views +from rest_auth import views as rest_auth_views -from .views import RegisterView +from . import views urlpatterns = [ - url(r'^$', RegisterView.as_view(), name='rest_register'), - url(r'^verify-email/$', VerifyEmailView.as_view(), name='rest_verify_email'), - url(r'^change-password/$', PasswordChangeView.as_view(), name='change_password'), + url(r'^$', views.RegisterView.as_view(), name='rest_register'), + url(r'^verify-email/$', + registration_views.VerifyEmailView.as_view(), + name='rest_verify_email'), + url(r'^change-password/$', + rest_auth_views.PasswordChangeView.as_view(), + name='change_password'), # This url is used by django-allauth and empty TemplateView is # defined just to allow reverse() call inside app, for example when email diff --git a/api/funkwhale_api/users/serializers.py b/api/funkwhale_api/users/serializers.py index b21aa6935..eadce6154 100644 --- a/api/funkwhale_api/users/serializers.py +++ b/api/funkwhale_api/users/serializers.py @@ -1,5 +1,7 @@ -from rest_framework import serializers +from django.conf import settings +from rest_framework import serializers +from rest_auth.serializers import PasswordResetSerializer as PRS from funkwhale_api.activity import serializers as activity_serializers from . import models @@ -63,3 +65,12 @@ class UserReadSerializer(serializers.ModelSerializer): 'status': o.has_perm(internal_codename) } return perms + + +class PasswordResetSerializer(PRS): + def get_email_options(self): + return { + 'extra_email_context': { + 'funkwhale_url': settings.FUNKWHALE_URL + } + } diff --git a/api/tests/users/test_views.py b/api/tests/users/test_views.py index 4be586965..985a78c8a 100644 --- a/api/tests/users/test_views.py +++ b/api/tests/users/test_views.py @@ -136,6 +136,20 @@ def test_changing_password_updates_secret_key(logged_in_client): assert user.password != password +def test_can_request_password_reset( + factories, api_client, mailoutbox): + user = factories['users.User']() + payload = { + 'email': user.email, + } + emails = len(mailoutbox) + url = reverse('rest_password_reset') + + response = api_client.post(url, payload) + assert response.status_code == 200 + assert len(mailoutbox) > emails + + def test_user_can_patch_his_own_settings(logged_in_api_client): user = logged_in_api_client.user payload = { From 3b9024129d89e29d89c2015702f50f8e1140441f Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sun, 6 May 2018 11:30:52 +0200 Subject: [PATCH 6/8] See #187: Front logic for password reset --- .../registration/password_reset_email.html | 12 +++ front/src/components/auth/Login.vue | 33 ++++--- front/src/router/index.js | 19 +++++ front/src/views/auth/PasswordReset.vue | 75 ++++++++++++++++ front/src/views/auth/PasswordResetConfirm.vue | 85 +++++++++++++++++++ 5 files changed, 211 insertions(+), 13 deletions(-) create mode 100644 api/funkwhale_api/templates/registration/password_reset_email.html create mode 100644 front/src/views/auth/PasswordReset.vue create mode 100644 front/src/views/auth/PasswordResetConfirm.vue diff --git a/api/funkwhale_api/templates/registration/password_reset_email.html b/api/funkwhale_api/templates/registration/password_reset_email.html new file mode 100644 index 000000000..7a587d720 --- /dev/null +++ b/api/funkwhale_api/templates/registration/password_reset_email.html @@ -0,0 +1,12 @@ +{% load i18n %}{% autoescape off %} +{% blocktrans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %} + +{% trans "Please go to the following page and choose a new password:" %} +{{ funkwhale_url }}/auth/password/reset/confirm?uid={{ uid }}&token={{ token }} +{% trans "Your username, in case you've forgotten:" %} {{ user.get_username }} + +{% trans "Thanks for using our site!" %} + +{% blocktrans %}The {{ site_name }} team{% endblocktrans %} + +{% endautoescape %} diff --git a/front/src/components/auth/Login.vue b/front/src/components/auth/Login.vue index b06ce89f0..f3add57b1 100644 --- a/front/src/components/auth/Login.vue +++ b/front/src/components/auth/Login.vue @@ -12,9 +12,15 @@
- +
- - + + +
- - - - + @@ -42,12 +46,15 @@ + + + diff --git a/front/src/views/auth/PasswordResetConfirm.vue b/front/src/views/auth/PasswordResetConfirm.vue new file mode 100644 index 000000000..d29192498 --- /dev/null +++ b/front/src/views/auth/PasswordResetConfirm.vue @@ -0,0 +1,85 @@ + + + + + + From 44ebb9287475279833a33a27b7eea8aae2d389c1 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sun, 6 May 2018 12:50:53 +0200 Subject: [PATCH 7/8] See #187: Front logic for password reset and email confirmation --- .../email/email_confirmation_message.txt | 8 +++ api/funkwhale_api/users/adapters.py | 7 +- front/src/router/index.js | 9 +++ front/src/views/auth/EmailConfirm.vue | 71 +++++++++++++++++++ front/src/views/auth/PasswordReset.vue | 2 +- front/src/views/auth/PasswordResetConfirm.vue | 2 +- 6 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 api/funkwhale_api/templates/account/email/email_confirmation_message.txt create mode 100644 front/src/views/auth/EmailConfirm.vue diff --git a/api/funkwhale_api/templates/account/email/email_confirmation_message.txt b/api/funkwhale_api/templates/account/email/email_confirmation_message.txt new file mode 100644 index 000000000..8aec540fe --- /dev/null +++ b/api/funkwhale_api/templates/account/email/email_confirmation_message.txt @@ -0,0 +1,8 @@ +{% load account %}{% user_display user as user_display %}{% load i18n %}{% autoescape off %}{% blocktrans with site_name=current_site.name site_domain=current_site.domain %}Hello from {{ site_name }}! + +You're receiving this e-mail because user {{ user_display }} at {{ site_domain }} has given yours as an e-mail address to connect their account. + +To confirm this is correct, go to {{ funkwhale_url }}/auth/email/confirm?key={{ key }} +{% endblocktrans %}{% endautoescape %} +{% blocktrans with site_name=current_site.name site_domain=current_site.domain %}Thank you from {{ site_name }}! +{{ site_domain }}{% endblocktrans %} diff --git a/api/funkwhale_api/users/adapters.py b/api/funkwhale_api/users/adapters.py index 96d1b8b1d..7bd341d14 100644 --- a/api/funkwhale_api/users/adapters.py +++ b/api/funkwhale_api/users/adapters.py @@ -1,5 +1,6 @@ -from allauth.account.adapter import DefaultAccountAdapter +from django.conf import settings +from allauth.account.adapter import DefaultAccountAdapter from dynamic_preferences.registries import global_preferences_registry @@ -8,3 +9,7 @@ class FunkwhaleAccountAdapter(DefaultAccountAdapter): def is_open_for_signup(self, request): manager = global_preferences_registry.manager() return manager['users__registration_enabled'] + + def send_mail(self, template_prefix, email, context): + context['funkwhale_url'] = settings.FUNKWHALE_URL + return super().send_mail(template_prefix, email, context) diff --git a/front/src/router/index.js b/front/src/router/index.js index 3bad260bc..b1e208023 100644 --- a/front/src/router/index.js +++ b/front/src/router/index.js @@ -11,6 +11,7 @@ import Settings from '@/components/auth/Settings' import Logout from '@/components/auth/Logout' import PasswordReset from '@/views/auth/PasswordReset' import PasswordResetConfirm from '@/views/auth/PasswordResetConfirm' +import EmailConfirm from '@/views/auth/EmailConfirm' import Library from '@/components/library/Library' import LibraryHome from '@/components/library/Home' import LibraryArtist from '@/components/library/Artist' @@ -69,6 +70,14 @@ export default new Router({ defaultEmail: route.query.email }) }, + { + path: '/auth/email/confirm', + name: 'auth.email-confirm', + component: EmailConfirm, + props: (route) => ({ + defaultKey: route.query.key + }) + }, { path: '/auth/password/reset/confirm', name: 'auth.password-reset-confirm', diff --git a/front/src/views/auth/EmailConfirm.vue b/front/src/views/auth/EmailConfirm.vue new file mode 100644 index 000000000..7ffa3c8d1 --- /dev/null +++ b/front/src/views/auth/EmailConfirm.vue @@ -0,0 +1,71 @@ + + + + + + diff --git a/front/src/views/auth/PasswordReset.vue b/front/src/views/auth/PasswordReset.vue index 6e80661b6..f6b445e00 100644 --- a/front/src/views/auth/PasswordReset.vue +++ b/front/src/views/auth/PasswordReset.vue @@ -5,7 +5,7 @@

{{ $t('Reset your password') }}

-
{{ $('Error while asking for a password reset') }}
+
{{ $t('Error while asking for a password reset') }}
  • {{ error }}
diff --git a/front/src/views/auth/PasswordResetConfirm.vue b/front/src/views/auth/PasswordResetConfirm.vue index d29192498..102ed6126 100644 --- a/front/src/views/auth/PasswordResetConfirm.vue +++ b/front/src/views/auth/PasswordResetConfirm.vue @@ -5,7 +5,7 @@

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

-
{{ $('Error while changing your password') }}
+
{{ $t('Error while changing your password') }}
  • {{ error }}
From 4a7105ae7ef27c144d3813d968893760b5e60b9f Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sun, 6 May 2018 13:48:23 +0200 Subject: [PATCH 8/8] Fix #187: documentation and changelog for email configuration --- api/config/settings/common.py | 6 +++++- api/config/settings/local.py | 3 --- api/setup.cfg | 2 +- changes/changelog.d/187.feature | 24 ++++++++++++++++++++++++ deploy/env.prod.sample | 11 +++++++++++ docs/configuration.rst | 18 ++++++++++++++++++ 6 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 changes/changelog.d/187.feature diff --git a/api/config/settings/common.py b/api/config/settings/common.py index 9c5487d64..1372f59e3 100644 --- a/api/config/settings/common.py +++ b/api/config/settings/common.py @@ -172,7 +172,10 @@ FIXTURE_DIRS = ( # EMAIL CONFIGURATION # ------------------------------------------------------------------------------ -EMAIL_BACKEND = env('DJANGO_EMAIL_BACKEND', default='django.core.mail.backends.smtp.EmailBackend') +EMAIL_CONFIG = env.email_url( + 'EMAIL_CONFIG', default='consolemail://') + +vars().update(EMAIL_CONFIG) # DATABASE CONFIGURATION # ------------------------------------------------------------------------------ @@ -367,6 +370,7 @@ CORS_ORIGIN_ALLOW_ALL = True # 'funkwhale.localhost', # ) CORS_ALLOW_CREDENTIALS = True + REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': ( 'rest_framework.permissions.IsAuthenticated', diff --git a/api/config/settings/local.py b/api/config/settings/local.py index dcbea66d2..592600629 100644 --- a/api/config/settings/local.py +++ b/api/config/settings/local.py @@ -25,9 +25,6 @@ SECRET_KEY = env("DJANGO_SECRET_KEY", default='mc$&b=5j#6^bv7tld1gyjp2&+^-qrdy=0 # ------------------------------------------------------------------------------ EMAIL_HOST = 'localhost' EMAIL_PORT = 1025 -EMAIL_BACKEND = env('DJANGO_EMAIL_BACKEND', - default='django.core.mail.backends.console.EmailBackend') - # django-debug-toolbar # ------------------------------------------------------------------------------ diff --git a/api/setup.cfg b/api/setup.cfg index a2b8b92c6..b1267c904 100644 --- a/api/setup.cfg +++ b/api/setup.cfg @@ -11,7 +11,7 @@ python_files = tests.py test_*.py *_tests.py testpaths = tests env = SECRET_KEY=test - DJANGO_EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend + EMAIL_CONFIG=consolemail:// CELERY_BROKER_URL=memory:// CELERY_TASK_ALWAYS_EAGER=True CACHEOPS_ENABLED=False diff --git a/changes/changelog.d/187.feature b/changes/changelog.d/187.feature new file mode 100644 index 000000000..501331a19 --- /dev/null +++ b/changes/changelog.d/187.feature @@ -0,0 +1,24 @@ +Users can now request password reset by email, assuming +a SMTP server was correctly configured (#187) + +Update +^^^^^^ + +Starting from this release, Funkwhale will send two types +of emails: + +- Email confirmation emails, to ensure a user's email is valid +- Password reset emails, enabling user to reset their password without an admin's intervention + +Email sending is disabled by default, as it requires additional configuration. +In this mode, emails are simply outputed on stdout. + +If you want to actually send those emails to your users, you should edit your +.env file and tweak the EMAIL_CONFIG variable. See :ref:`setting-EMAIL_CONFIG` +for more details. + +.. note:: + + As a result of these changes, the DJANGO_EMAIL_BACKEND variable, + which was not documented, has no effect anymore. You can safely remove it from + your .env file if it is set. diff --git a/deploy/env.prod.sample b/deploy/env.prod.sample index f1718ff7e..dfd17ff4d 100644 --- a/deploy/env.prod.sample +++ b/deploy/env.prod.sample @@ -6,6 +6,7 @@ # - DJANGO_SECRET_KEY # - DJANGO_ALLOWED_HOSTS # - FUNKWHALE_URL +# - EMAIL_CONFIG (if you plan to send emails) # On non-docker setup **only**, you'll also have to tweak/uncomment those variables: # - DATABASE_URL # - CACHE_URL @@ -41,6 +42,16 @@ FUNKWHALE_API_PORT=5000 # your instance FUNKWHALE_URL=https://yourdomain.funwhale +# Configure email sending using this variale +# By default, funkwhale will output emails sent to stdout +# here are a few examples for this setting +# EMAIL_CONFIG=consolemail:// # output emails to console (the default) +# EMAIL_CONFIG=dummymail:// # disable email sending completely +# On a production instance, you'll usually want to use an external SMTP server: +# EMAIL_CONFIG=smtp://user@:password@youremail.host:25' +# EMAIL_CONFIG=smtp+ssl://user@:password@youremail.host:465' +# EMAIL_CONFIG=smtp+tls://user@:password@youremail.host:587' + # Depending on the reverse proxy used in front of your funkwhale instance, # the API will use different kind of headers to serve audio files # Allowed values: nginx, apache2 diff --git a/docs/configuration.rst b/docs/configuration.rst index 1c89feeb8..f498b9c87 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -39,6 +39,24 @@ settings in this interface. Configuration reference ----------------------- +.. _setting-EMAIL_CONFIG: + +``EMAIL_CONFIG`` +^^^^^^^^^^^^^^^^ + +Determine how emails are sent. + +Default: ``consolemail://`` + +Possible values: + +- ``consolemail://``: Output sent emails to stdout +- ``dummymail://``: Completely discard sent emails +- ``smtp://user:password@youremail.host:25``: Send emails via SMTP via youremail.host on port 25, without encryption, authenticating as user "user" with password "password" +- ``smtp+ssl://user:password@youremail.host:465``: Send emails via SMTP via youremail.host on port 465, using SSL encryption, authenticating as user "user" with password "password" +- ``smtp+tls://user:password@youremail.host:587``: Send emails via SMTP via youremail.host on port 587, using TLS encryption, authenticating as user "user" with password "password" + + .. _setting-MUSIC_DIRECTORY_PATH: ``MUSIC_DIRECTORY_PATH``