kopia lustrzana https://dev.funkwhale.audio/funkwhale/funkwhale
				
				
				
			Merge branch '187-emails' into 'develop'
Resolve "Add email support" Closes #187 See merge request funkwhale/funkwhale!182merge-requests/237/head
						commit
						2649ad88ff
					
				|  | @ -144,7 +144,6 @@ INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS | ||||||
| MIDDLEWARE = ( | MIDDLEWARE = ( | ||||||
|     # Make sure djangosecure.middleware.SecurityMiddleware is listed first |     # Make sure djangosecure.middleware.SecurityMiddleware is listed first | ||||||
|     'django.contrib.sessions.middleware.SessionMiddleware', |     'django.contrib.sessions.middleware.SessionMiddleware', | ||||||
|     'funkwhale_api.users.middleware.AnonymousSessionMiddleware', |  | ||||||
|     'corsheaders.middleware.CorsMiddleware', |     'corsheaders.middleware.CorsMiddleware', | ||||||
|     'django.middleware.common.CommonMiddleware', |     'django.middleware.common.CommonMiddleware', | ||||||
|     'django.middleware.csrf.CsrfViewMiddleware', |     'django.middleware.csrf.CsrfViewMiddleware', | ||||||
|  | @ -173,7 +172,10 @@ FIXTURE_DIRS = ( | ||||||
| 
 | 
 | ||||||
| # EMAIL CONFIGURATION | # 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 | # DATABASE CONFIGURATION | ||||||
| # ------------------------------------------------------------------------------ | # ------------------------------------------------------------------------------ | ||||||
|  | @ -293,7 +295,7 @@ AUTHENTICATION_BACKENDS = ( | ||||||
|     'django.contrib.auth.backends.ModelBackend', |     'django.contrib.auth.backends.ModelBackend', | ||||||
|     'allauth.account.auth_backends.AuthenticationBackend', |     'allauth.account.auth_backends.AuthenticationBackend', | ||||||
| ) | ) | ||||||
| 
 | SESSION_COOKIE_HTTPONLY = False | ||||||
| # Some really nice defaults | # Some really nice defaults | ||||||
| ACCOUNT_AUTHENTICATION_METHOD = 'username_email' | ACCOUNT_AUTHENTICATION_METHOD = 'username_email' | ||||||
| ACCOUNT_EMAIL_REQUIRED = True | ACCOUNT_EMAIL_REQUIRED = True | ||||||
|  | @ -368,6 +370,7 @@ CORS_ORIGIN_ALLOW_ALL = True | ||||||
| #     'funkwhale.localhost', | #     'funkwhale.localhost', | ||||||
| # ) | # ) | ||||||
| CORS_ALLOW_CREDENTIALS = True | CORS_ALLOW_CREDENTIALS = True | ||||||
|  | 
 | ||||||
| REST_FRAMEWORK = { | REST_FRAMEWORK = { | ||||||
|     'DEFAULT_PERMISSION_CLASSES': ( |     'DEFAULT_PERMISSION_CLASSES': ( | ||||||
|         'rest_framework.permissions.IsAuthenticated', |         'rest_framework.permissions.IsAuthenticated', | ||||||
|  | @ -392,6 +395,11 @@ REST_FRAMEWORK = { | ||||||
|         'django_filters.rest_framework.DjangoFilterBackend', |         '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 | ATOMIC_REQUESTS = False | ||||||
| USE_X_FORWARDED_HOST = True | USE_X_FORWARDED_HOST = True | ||||||
|  |  | ||||||
|  | @ -25,9 +25,6 @@ SECRET_KEY = env("DJANGO_SECRET_KEY", default='mc$&b=5j#6^bv7tld1gyjp2&+^-qrdy=0 | ||||||
| # ------------------------------------------------------------------------------ | # ------------------------------------------------------------------------------ | ||||||
| EMAIL_HOST = 'localhost' | EMAIL_HOST = 'localhost' | ||||||
| EMAIL_PORT = 1025 | EMAIL_PORT = 1025 | ||||||
| EMAIL_BACKEND = env('DJANGO_EMAIL_BACKEND', |  | ||||||
|                     default='django.core.mail.backends.console.EmailBackend') |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| # django-debug-toolbar | # django-debug-toolbar | ||||||
| # ------------------------------------------------------------------------------ | # ------------------------------------------------------------------------------ | ||||||
|  |  | ||||||
|  | @ -21,13 +21,6 @@ class Listening(models.Model): | ||||||
|     class Meta: |     class Meta: | ||||||
|         ordering = ('-creation_date',) |         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): |     def get_activity_url(self): | ||||||
|         return '{}/listenings/tracks/{}'.format( |         return '{}/listenings/tracks/{}'.format( | ||||||
|             self.user.get_activity_url(), self.pk) |             self.user.get_activity_url(), self.pk) | ||||||
|  |  | ||||||
|  | @ -36,13 +36,9 @@ class ListeningSerializer(serializers.ModelSerializer): | ||||||
| 
 | 
 | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = models.Listening |         model = models.Listening | ||||||
|         fields = ('id', 'user', 'session_key', 'track', 'creation_date') |         fields = ('id', 'user', 'track', 'creation_date') | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
|     def create(self, validated_data): |     def create(self, validated_data): | ||||||
|         if self.context.get('user'): |         validated_data['user'] = self.context['user'] | ||||||
|             validated_data['user'] = self.context.get('user') |  | ||||||
|         else: |  | ||||||
|             validated_data['session_key'] = self.context['session_key'] |  | ||||||
| 
 | 
 | ||||||
|         return super().create(validated_data) |         return super().create(validated_data) | ||||||
|  |  | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| from rest_framework import generics, mixins, viewsets | from rest_framework import generics, mixins, viewsets | ||||||
|  | from rest_framework import permissions | ||||||
| from rest_framework import status | from rest_framework import status | ||||||
| from rest_framework.response import Response | from rest_framework.response import Response | ||||||
| from rest_framework.decorators import detail_route | from rest_framework.decorators import detail_route | ||||||
|  | @ -10,31 +11,26 @@ from funkwhale_api.music.serializers import TrackSerializerNested | ||||||
| from . import models | from . import models | ||||||
| from . import serializers | from . import serializers | ||||||
| 
 | 
 | ||||||
| class ListeningViewSet(mixins.CreateModelMixin, | 
 | ||||||
|  | class ListeningViewSet( | ||||||
|  |         mixins.CreateModelMixin, | ||||||
|         mixins.RetrieveModelMixin, |         mixins.RetrieveModelMixin, | ||||||
|         viewsets.GenericViewSet): |         viewsets.GenericViewSet): | ||||||
| 
 | 
 | ||||||
|     serializer_class = serializers.ListeningSerializer |     serializer_class = serializers.ListeningSerializer | ||||||
|     queryset = models.Listening.objects.all() |     queryset = models.Listening.objects.all() | ||||||
|     permission_classes = [ConditionalAuthentication] |     permission_classes = [permissions.IsAuthenticated] | ||||||
| 
 | 
 | ||||||
|     def perform_create(self, serializer): |     def perform_create(self, serializer): | ||||||
|         r = super().perform_create(serializer) |         r = super().perform_create(serializer) | ||||||
|         if self.request.user.is_authenticated: |  | ||||||
|         record.send(serializer.instance) |         record.send(serializer.instance) | ||||||
|         return r |         return r | ||||||
| 
 | 
 | ||||||
|     def get_queryset(self): |     def get_queryset(self): | ||||||
|         queryset = super().get_queryset() |         queryset = super().get_queryset() | ||||||
|         if self.request.user.is_authenticated: |  | ||||||
|         return queryset.filter(user=self.request.user) |         return queryset.filter(user=self.request.user) | ||||||
|         else: |  | ||||||
|             return queryset.filter(session_key=self.request.session.session_key) |  | ||||||
| 
 | 
 | ||||||
|     def get_serializer_context(self): |     def get_serializer_context(self): | ||||||
|         context = super().get_serializer_context() |         context = super().get_serializer_context() | ||||||
|         if self.request.user.is_authenticated: |  | ||||||
|         context['user'] = self.request.user |         context['user'] = self.request.user | ||||||
|         else: |  | ||||||
|             context['session_key'] = self.request.session.session_key |  | ||||||
|         return context |         return context | ||||||
|  |  | ||||||
|  | @ -55,8 +55,6 @@ class RadioSession(models.Model): | ||||||
|     related_object = GenericForeignKey('related_object_content_type', 'related_object_id') |     related_object = GenericForeignKey('related_object_content_type', 'related_object_id') | ||||||
| 
 | 
 | ||||||
|     def save(self, **kwargs): |     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) |         self.radio.clean(self) | ||||||
|         super().save(**kwargs) |         super().save(**kwargs) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -38,6 +38,7 @@ class RadioSerializer(serializers.ModelSerializer): | ||||||
| 
 | 
 | ||||||
|         return super().save(**kwargs) |         return super().save(**kwargs) | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| class RadioSessionTrackSerializerCreate(serializers.ModelSerializer): | class RadioSessionTrackSerializerCreate(serializers.ModelSerializer): | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = models.RadioSessionTrack |         model = models.RadioSessionTrack | ||||||
|  | @ -62,17 +63,14 @@ class RadioSessionSerializer(serializers.ModelSerializer): | ||||||
|             'user', |             'user', | ||||||
|             'creation_date', |             'creation_date', | ||||||
|             'custom_radio', |             'custom_radio', | ||||||
|             'session_key') |         ) | ||||||
| 
 | 
 | ||||||
|     def validate(self, data): |     def validate(self, data): | ||||||
|         registry[data['radio_type']]().validate_session(data, **self.context) |         registry[data['radio_type']]().validate_session(data, **self.context) | ||||||
|         return data |         return data | ||||||
| 
 | 
 | ||||||
|     def create(self, validated_data): |     def create(self, validated_data): | ||||||
|         if self.context.get('user'): |         validated_data['user'] = self.context['user'] | ||||||
|             validated_data['user'] = self.context.get('user') |  | ||||||
|         else: |  | ||||||
|             validated_data['session_key'] = self.context['session_key'] |  | ||||||
|         if validated_data.get('related_object_id'): |         if validated_data.get('related_object_id'): | ||||||
|             radio = registry[validated_data['radio_type']]() |             radio = registry[validated_data['radio_type']]() | ||||||
|             validated_data['related_object'] = radio.get_related_object(validated_data['related_object_id']) |             validated_data['related_object'] = radio.get_related_object(validated_data['related_object_id']) | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ from django.db.models import Q | ||||||
| from django.http import Http404 | from django.http import Http404 | ||||||
| 
 | 
 | ||||||
| from rest_framework import generics, mixins, viewsets | from rest_framework import generics, mixins, viewsets | ||||||
|  | from rest_framework import permissions | ||||||
| from rest_framework import status | from rest_framework import status | ||||||
| from rest_framework.response import Response | from rest_framework.response import Response | ||||||
| from rest_framework.decorators import detail_route, list_route | from rest_framework.decorators import detail_route, list_route | ||||||
|  | @ -24,7 +25,7 @@ class RadioViewSet( | ||||||
|         viewsets.GenericViewSet): |         viewsets.GenericViewSet): | ||||||
| 
 | 
 | ||||||
|     serializer_class = serializers.RadioSerializer |     serializer_class = serializers.RadioSerializer | ||||||
|     permission_classes = [ConditionalAuthentication] |     permission_classes = [permissions.IsAuthenticated] | ||||||
|     filter_class = filtersets.RadioFilter |     filter_class = filtersets.RadioFilter | ||||||
| 
 | 
 | ||||||
|     def get_queryset(self): |     def get_queryset(self): | ||||||
|  | @ -84,21 +85,15 @@ class RadioSessionViewSet(mixins.CreateModelMixin, | ||||||
| 
 | 
 | ||||||
|     serializer_class = serializers.RadioSessionSerializer |     serializer_class = serializers.RadioSessionSerializer | ||||||
|     queryset = models.RadioSession.objects.all() |     queryset = models.RadioSession.objects.all() | ||||||
|     permission_classes = [ConditionalAuthentication] |     permission_classes = [permissions.IsAuthenticated] | ||||||
| 
 | 
 | ||||||
|     def get_queryset(self): |     def get_queryset(self): | ||||||
|         queryset = super().get_queryset() |         queryset = super().get_queryset() | ||||||
|         if self.request.user.is_authenticated: |  | ||||||
|         return queryset.filter(user=self.request.user) |         return queryset.filter(user=self.request.user) | ||||||
|         else: |  | ||||||
|             return queryset.filter(session_key=self.request.session.session_key) |  | ||||||
| 
 | 
 | ||||||
|     def get_serializer_context(self): |     def get_serializer_context(self): | ||||||
|         context = super().get_serializer_context() |         context = super().get_serializer_context() | ||||||
|         if self.request.user.is_authenticated: |  | ||||||
|         context['user'] = self.request.user |         context['user'] = self.request.user | ||||||
|         else: |  | ||||||
|             context['session_key'] = self.request.session.session_key |  | ||||||
|         return context |         return context | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -106,17 +101,14 @@ class RadioSessionTrackViewSet(mixins.CreateModelMixin, | ||||||
|                                viewsets.GenericViewSet): |                                viewsets.GenericViewSet): | ||||||
|     serializer_class = serializers.RadioSessionTrackSerializer |     serializer_class = serializers.RadioSessionTrackSerializer | ||||||
|     queryset = models.RadioSessionTrack.objects.all() |     queryset = models.RadioSessionTrack.objects.all() | ||||||
|     permission_classes = [ConditionalAuthentication] |     permission_classes = [permissions.IsAuthenticated] | ||||||
| 
 | 
 | ||||||
|     def create(self, request, *args, **kwargs): |     def create(self, request, *args, **kwargs): | ||||||
|         serializer = self.get_serializer(data=request.data) |         serializer = self.get_serializer(data=request.data) | ||||||
|         serializer.is_valid(raise_exception=True) |         serializer.is_valid(raise_exception=True) | ||||||
|         session = serializer.validated_data['session'] |         session = serializer.validated_data['session'] | ||||||
|         try: |         try: | ||||||
|             if request.user.is_authenticated: |  | ||||||
|             assert request.user == session.user |             assert request.user == session.user | ||||||
|             else: |  | ||||||
|                 assert request.session.session_key == session.session_key |  | ||||||
|         except AssertionError: |         except AssertionError: | ||||||
|             return Response(status=status.HTTP_403_FORBIDDEN) |             return Response(status=status.HTTP_403_FORBIDDEN) | ||||||
|         track = session.radio.pick() |         track = session.radio.pick() | ||||||
|  |  | ||||||
|  | @ -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 %} | ||||||
|  | @ -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 %} | ||||||
|  | @ -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 | from dynamic_preferences.registries import global_preferences_registry | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -8,3 +9,7 @@ class FunkwhaleAccountAdapter(DefaultAccountAdapter): | ||||||
|     def is_open_for_signup(self, request): |     def is_open_for_signup(self, request): | ||||||
|         manager = global_preferences_registry.manager() |         manager = global_preferences_registry.manager() | ||||||
|         return manager['users__registration_enabled'] |         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) | ||||||
|  |  | ||||||
|  | @ -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 |  | ||||||
|  | @ -1,16 +1,20 @@ | ||||||
| from django.views.generic import TemplateView | from django.views.generic import TemplateView | ||||||
| from django.conf.urls import url | from django.conf.urls import url | ||||||
| 
 | 
 | ||||||
| from rest_auth.registration.views import VerifyEmailView | from rest_auth.registration import views as registration_views | ||||||
| from rest_auth.views import PasswordChangeView | from rest_auth import views as rest_auth_views | ||||||
| 
 | 
 | ||||||
| from .views import RegisterView | from . import views | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| urlpatterns = [ | urlpatterns = [ | ||||||
|     url(r'^$', RegisterView.as_view(), name='rest_register'), |     url(r'^$', views.RegisterView.as_view(), name='rest_register'), | ||||||
|     url(r'^verify-email/$', VerifyEmailView.as_view(), name='rest_verify_email'), |     url(r'^verify-email/$', | ||||||
|     url(r'^change-password/$', PasswordChangeView.as_view(), name='change_password'), |         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 |     # This url is used by django-allauth and empty TemplateView is | ||||||
|     # defined just to allow reverse() call inside app, for example when email |     # defined just to allow reverse() call inside app, for example when email | ||||||
|  |  | ||||||
|  | @ -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 funkwhale_api.activity import serializers as activity_serializers | ||||||
| 
 | 
 | ||||||
| from . import models | from . import models | ||||||
|  | @ -63,3 +65,12 @@ class UserReadSerializer(serializers.ModelSerializer): | ||||||
|                 'status': o.has_perm(internal_codename) |                 'status': o.has_perm(internal_codename) | ||||||
|             } |             } | ||||||
|         return perms |         return perms | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class PasswordResetSerializer(PRS): | ||||||
|  |     def get_email_options(self): | ||||||
|  |         return { | ||||||
|  |             'extra_email_context': { | ||||||
|  |                 'funkwhale_url': settings.FUNKWHALE_URL | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  | @ -11,7 +11,7 @@ python_files = tests.py test_*.py *_tests.py | ||||||
| testpaths = tests | testpaths = tests | ||||||
| env = | env = | ||||||
|     SECRET_KEY=test |     SECRET_KEY=test | ||||||
|     DJANGO_EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend |     EMAIL_CONFIG=consolemail:// | ||||||
|     CELERY_BROKER_URL=memory:// |     CELERY_BROKER_URL=memory:// | ||||||
|     CELERY_TASK_ALWAYS_EAGER=True |     CELERY_TASK_ALWAYS_EAGER=True | ||||||
|     CACHEOPS_ENABLED=False |     CACHEOPS_ENABLED=False | ||||||
|  |  | ||||||
|  | @ -14,21 +14,6 @@ def test_can_create_listening(factories): | ||||||
|     l = models.Listening.objects.create(user=user, track=track) |     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( | def test_logged_in_user_can_create_listening_via_api( | ||||||
|         logged_in_client, factories, activity_muted): |         logged_in_client, factories, activity_muted): | ||||||
|     track = factories['music.Track']() |     track = factories['music.Track']() | ||||||
|  |  | ||||||
|  | @ -151,20 +151,6 @@ def test_can_start_radio_for_logged_in_user(logged_in_client): | ||||||
|     assert session.user == logged_in_client.user |     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): | def test_can_get_track_for_session_from_api(factories, logged_in_client): | ||||||
|     files = factories['music.TrackFile'].create_batch(1) |     files = factories['music.TrackFile'].create_batch(1) | ||||||
|     tracks = [f.track for f in files] |     tracks = [f.track for f in files] | ||||||
|  | @ -232,20 +218,20 @@ def test_can_start_tag_radio(factories): | ||||||
|         assert radio.pick() in good_tracks |         assert radio.pick() in good_tracks | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_can_start_artist_radio_from_api(api_client, preferences, factories): | def test_can_start_artist_radio_from_api( | ||||||
|     preferences['common__api_authentication_required'] = False |         logged_in_api_client, preferences, factories): | ||||||
|     artist = factories['music.Artist']() |     artist = factories['music.Artist']() | ||||||
|     url = reverse('api:v1:radios:sessions-list') |     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}) |         url, {'radio_type': 'artist', 'related_object_id': artist.id}) | ||||||
| 
 | 
 | ||||||
|     assert response.status_code == 201 |     assert response.status_code == 201 | ||||||
| 
 | 
 | ||||||
|     session = models.RadioSession.objects.latest('id') |     session = models.RadioSession.objects.latest('id') | ||||||
| 
 | 
 | ||||||
|     assert session.radio_type, 'artist' |     assert session.radio_type == 'artist' | ||||||
|     assert session.related_object, artist |     assert session.related_object == artist | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_can_start_less_listened_radio(factories): | 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] |     good_tracks = [f.track for f in good_files] | ||||||
|     radio = radios.LessListenedRadio() |     radio = radios.LessListenedRadio() | ||||||
|     session = radio.start_session(user) |     session = radio.start_session(user) | ||||||
|     assert session.related_object == user | 
 | ||||||
|     for i in range(5): |     for i in range(5): | ||||||
|         assert radio.pick() in good_tracks |         assert radio.pick() in good_tracks | ||||||
|  |  | ||||||
|  | @ -136,6 +136,20 @@ def test_changing_password_updates_secret_key(logged_in_client): | ||||||
|     assert user.password != password |     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): | def test_user_can_patch_his_own_settings(logged_in_api_client): | ||||||
|     user = logged_in_api_client.user |     user = logged_in_api_client.user | ||||||
|     payload = { |     payload = { | ||||||
|  |  | ||||||
|  | @ -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. | ||||||
|  | @ -6,6 +6,7 @@ | ||||||
| # - DJANGO_SECRET_KEY | # - DJANGO_SECRET_KEY | ||||||
| # - DJANGO_ALLOWED_HOSTS | # - DJANGO_ALLOWED_HOSTS | ||||||
| # - FUNKWHALE_URL | # - FUNKWHALE_URL | ||||||
|  | # - EMAIL_CONFIG (if you plan to send emails) | ||||||
| # On non-docker setup **only**, you'll also have to tweak/uncomment those variables: | # On non-docker setup **only**, you'll also have to tweak/uncomment those variables: | ||||||
| # - DATABASE_URL | # - DATABASE_URL | ||||||
| # - CACHE_URL | # - CACHE_URL | ||||||
|  | @ -41,6 +42,16 @@ FUNKWHALE_API_PORT=5000 | ||||||
| # your instance | # your instance | ||||||
| FUNKWHALE_URL=https://yourdomain.funwhale | 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, | # Depending on the reverse proxy used in front of your funkwhale instance, | ||||||
| # the API will use different kind of headers to serve audio files | # the API will use different kind of headers to serve audio files | ||||||
| # Allowed values: nginx, apache2 | # Allowed values: nginx, apache2 | ||||||
|  |  | ||||||
|  | @ -39,6 +39,24 @@ settings in this interface. | ||||||
| Configuration reference | 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: | .. _setting-MUSIC_DIRECTORY_PATH: | ||||||
| 
 | 
 | ||||||
| ``MUSIC_DIRECTORY_PATH`` | ``MUSIC_DIRECTORY_PATH`` | ||||||
|  |  | ||||||
|  | @ -12,9 +12,15 @@ | ||||||
|             </ul> |             </ul> | ||||||
|           </div> |           </div> | ||||||
|           <div class="field"> |           <div class="field"> | ||||||
|             <i18next tag="label" path="Username or email"/> |             <label> | ||||||
|  |               {{ $t('Username or email') }} | | ||||||
|  |               <router-link :to="{path: '/signup'}"> | ||||||
|  |                 {{ $t('Create an account') }} | ||||||
|  |               </router-link> | ||||||
|  |             </label> | ||||||
|             <input |             <input | ||||||
|             ref="username" |             ref="username" | ||||||
|  |             tabindex="1" | ||||||
|             required |             required | ||||||
|             type="text" |             type="text" | ||||||
|             autofocus |             autofocus | ||||||
|  | @ -23,18 +29,16 @@ | ||||||
|             > |             > | ||||||
|           </div> |           </div> | ||||||
|           <div class="field"> |           <div class="field"> | ||||||
|             <i18next tag="label" path="Password"/>             |             <label> | ||||||
|             <input |               {{ $t('Password') }} | | ||||||
|             required |               <router-link :to="{name: 'auth.password-reset', query: {email: credentials.username}}"> | ||||||
|             type="password" |                 {{ $t('Reset your password') }} | ||||||
|             placeholder="Enter your password" |  | ||||||
|             v-model="credentials.password" |  | ||||||
|             > |  | ||||||
|           </div> |  | ||||||
|           <button :class="['ui', {'loading': isLoading}, 'button']" type="submit"><i18next path="Login"/></button> |  | ||||||
|           <router-link class="ui right floated basic button" :to="{path: '/signup'}"> |  | ||||||
|             <i18next path="Create an account"/> |  | ||||||
|               </router-link> |               </router-link> | ||||||
|  |             </label> | ||||||
|  |             <password-input :index="2" required v-model="credentials.password" /> | ||||||
|  | 
 | ||||||
|  |           </div> | ||||||
|  |           <button tabindex="3" :class="['ui', {'loading': isLoading}, 'right', 'floated', 'green', 'button']" type="submit"><i18next path="Login"/></button> | ||||||
|         </form> |         </form> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|  | @ -42,12 +46,15 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script> | <script> | ||||||
|  | import PasswordInput from '@/components/forms/PasswordInput' | ||||||
| 
 | 
 | ||||||
| export default { | export default { | ||||||
|   name: 'login', |  | ||||||
|   props: { |   props: { | ||||||
|     next: {type: String, default: '/'} |     next: {type: String, default: '/'} | ||||||
|   }, |   }, | ||||||
|  |   components: { | ||||||
|  |     PasswordInput | ||||||
|  |   }, | ||||||
|   data () { |   data () { | ||||||
|     return { |     return { | ||||||
|       // We need to initialize the component with any |       // We need to initialize the component with any | ||||||
|  |  | ||||||
|  | @ -35,21 +35,13 @@ | ||||||
|           </div> |           </div> | ||||||
|           <div class="field"> |           <div class="field"> | ||||||
|             <label><i18next path="Old password"/></label> |             <label><i18next path="Old password"/></label> | ||||||
|             <input |             <password-input required v-model="old_password" /> | ||||||
|             required | 
 | ||||||
|             type="password" |  | ||||||
|             autofocus |  | ||||||
|             placeholder="Enter your old password" |  | ||||||
|             v-model="old_password"> |  | ||||||
|           </div> |           </div> | ||||||
|           <div class="field"> |           <div class="field"> | ||||||
|             <label><i18next path="New password"/></label> |             <label><i18next path="New password"/></label> | ||||||
|             <input |             <password-input required v-model="new_password" /> | ||||||
|             required | 
 | ||||||
|             type="password" |  | ||||||
|             autofocus |  | ||||||
|             placeholder="Enter your new password" |  | ||||||
|             v-model="new_password"> |  | ||||||
|           </div> |           </div> | ||||||
|           <button :class="['ui', {'loading': isLoading}, 'button']" type="submit"><i18next path="Change password"/></button> |           <button :class="['ui', {'loading': isLoading}, 'button']" type="submit"><i18next path="Change password"/></button> | ||||||
|         </form> |         </form> | ||||||
|  | @ -62,8 +54,12 @@ | ||||||
| import $ from 'jquery' | import $ from 'jquery' | ||||||
| import axios from 'axios' | import axios from 'axios' | ||||||
| import logger from '@/logging' | import logger from '@/logging' | ||||||
|  | import PasswordInput from '@/components/forms/PasswordInput' | ||||||
| 
 | 
 | ||||||
| export default { | export default { | ||||||
|  |   components: { | ||||||
|  |     PasswordInput | ||||||
|  |   }, | ||||||
|   data () { |   data () { | ||||||
|     let d = { |     let d = { | ||||||
|       // We need to initialize the component with any |       // We need to initialize the component with any | ||||||
|  |  | ||||||
|  | @ -34,16 +34,7 @@ | ||||||
|           </div> |           </div> | ||||||
|           <div class="field"> |           <div class="field"> | ||||||
|             <i18next tag="label" path="Password"/> |             <i18next tag="label" path="Password"/> | ||||||
|             <div class="ui action input"> |             <password-input v-model="password" /> | ||||||
|               <input |  | ||||||
|               required |  | ||||||
|               :type="passwordInputType" |  | ||||||
|               placeholder="Enter your password" |  | ||||||
|               v-model="password"> |  | ||||||
|               <span @click="showPassword = !showPassword" title="Show/hide password" class="ui icon button"> |  | ||||||
|                 <i class="eye icon"></i> |  | ||||||
|               </span> |  | ||||||
|             </div> |  | ||||||
|           </div> |           </div> | ||||||
|           <button :class="['ui', 'green', {'loading': isLoading}, 'button']" type="submit"><i18next path="Create my account"/></button> |           <button :class="['ui', 'green', {'loading': isLoading}, 'button']" type="submit"><i18next path="Create my account"/></button> | ||||||
|         </form> |         </form> | ||||||
|  | @ -57,8 +48,13 @@ | ||||||
| import axios from 'axios' | import axios from 'axios' | ||||||
| import logger from '@/logging' | import logger from '@/logging' | ||||||
| 
 | 
 | ||||||
|  | import PasswordInput from '@/components/forms/PasswordInput' | ||||||
|  | 
 | ||||||
| export default { | export default { | ||||||
|   name: 'login', |   name: 'login', | ||||||
|  |   components: { | ||||||
|  |     PasswordInput | ||||||
|  |   }, | ||||||
|   props: { |   props: { | ||||||
|     next: {type: String, default: '/'} |     next: {type: String, default: '/'} | ||||||
|   }, |   }, | ||||||
|  | @ -69,8 +65,7 @@ export default { | ||||||
|       password: '', |       password: '', | ||||||
|       isLoadingInstanceSetting: true, |       isLoadingInstanceSetting: true, | ||||||
|       errors: [], |       errors: [], | ||||||
|       isLoading: false, |       isLoading: false | ||||||
|       showPassword: false |  | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   created () { |   created () { | ||||||
|  | @ -104,16 +99,7 @@ export default { | ||||||
|         self.isLoading = false |         self.isLoading = false | ||||||
|       }) |       }) | ||||||
|     } |     } | ||||||
|   }, |  | ||||||
|   computed: { |  | ||||||
|     passwordInputType () { |  | ||||||
|       if (this.showPassword) { |  | ||||||
|         return 'text' |  | ||||||
|   } |   } | ||||||
|       return 'password' |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
| } | } | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -0,0 +1,31 @@ | ||||||
|  | <template> | ||||||
|  |   <div class="ui action input"> | ||||||
|  |     <input | ||||||
|  |     required | ||||||
|  |     :tabindex="index" | ||||||
|  |     :type="passwordInputType" | ||||||
|  |     @input="$emit('input', $event.target.value)" | ||||||
|  |     :value="value"> | ||||||
|  |     <span @click="showPassword = !showPassword" :title="$t('Show/hide password')" class="ui icon button"> | ||||||
|  |       <i class="eye icon"></i> | ||||||
|  |     </span> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | <script> | ||||||
|  | export default { | ||||||
|  |   props: ['value', 'index'], | ||||||
|  |   data () { | ||||||
|  |     return { | ||||||
|  |       showPassword: false | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     passwordInputType () { | ||||||
|  |       if (this.showPassword) { | ||||||
|  |         return 'text' | ||||||
|  |       } | ||||||
|  |       return 'password' | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | @ -9,6 +9,9 @@ import Signup from '@/components/auth/Signup' | ||||||
| import Profile from '@/components/auth/Profile' | import Profile from '@/components/auth/Profile' | ||||||
| import Settings from '@/components/auth/Settings' | import Settings from '@/components/auth/Settings' | ||||||
| import Logout from '@/components/auth/Logout' | 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 Library from '@/components/library/Library' | ||||||
| import LibraryHome from '@/components/library/Home' | import LibraryHome from '@/components/library/Home' | ||||||
| import LibraryArtist from '@/components/library/Artist' | import LibraryArtist from '@/components/library/Artist' | ||||||
|  | @ -59,6 +62,31 @@ export default new Router({ | ||||||
|       component: Login, |       component: Login, | ||||||
|       props: (route) => ({ next: route.query.next || '/library' }) |       props: (route) => ({ next: route.query.next || '/library' }) | ||||||
|     }, |     }, | ||||||
|  |     { | ||||||
|  |       path: '/auth/password/reset', | ||||||
|  |       name: 'auth.password-reset', | ||||||
|  |       component: PasswordReset, | ||||||
|  |       props: (route) => ({ | ||||||
|  |         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', | ||||||
|  |       component: PasswordResetConfirm, | ||||||
|  |       props: (route) => ({ | ||||||
|  |         defaultUid: route.query.uid, | ||||||
|  |         defaultToken: route.query.token | ||||||
|  |       }) | ||||||
|  |     }, | ||||||
|     { |     { | ||||||
|       path: '/signup', |       path: '/signup', | ||||||
|       name: 'signup', |       name: 'signup', | ||||||
|  |  | ||||||
|  | @ -97,6 +97,11 @@ export default { | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     fetchProfile ({commit, dispatch, state}) { |     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) => { |       return axios.get('users/users/me/').then((response) => { | ||||||
|         logger.default.info('Successfully fetched user profile') |         logger.default.info('Successfully fetched user profile') | ||||||
|         let data = response.data |         let data = response.data | ||||||
|  |  | ||||||
|  | @ -85,7 +85,10 @@ export default { | ||||||
|     togglePlay ({commit, state}) { |     togglePlay ({commit, state}) { | ||||||
|       commit('playing', !state.playing) |       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) => { |       return axios.post('history/listenings/', {'track': track.id}).then((response) => {}, (response) => { | ||||||
|         logger.default.error('Could not record track in history') |         logger.default.error('Could not record track in history') | ||||||
|       }) |       }) | ||||||
|  |  | ||||||
|  | @ -0,0 +1,71 @@ | ||||||
|  | <template> | ||||||
|  |   <div class="main pusher" v-title="$t('Confirm your email')"> | ||||||
|  |     <div class="ui vertical stripe segment"> | ||||||
|  |       <div class="ui small text container"> | ||||||
|  |         <h2>{{ $t('Confirm your email') }}</h2> | ||||||
|  |         <form v-if="!success" class="ui form" @submit.prevent="submit()"> | ||||||
|  |           <div v-if="errors.length > 0" class="ui negative message"> | ||||||
|  |             <div class="header">{{ $t('Error while confirming your email') }}</div> | ||||||
|  |             <ul class="list"> | ||||||
|  |               <li v-for="error in errors">{{ error }}</li> | ||||||
|  |             </ul> | ||||||
|  |           </div> | ||||||
|  |           <div class="field"> | ||||||
|  |             <label>{{ $t('Confirmation code') }}</label> | ||||||
|  |             <input type="text" required v-model="key" /> | ||||||
|  |           </div> | ||||||
|  |           <router-link :to="{path: '/login'}"> | ||||||
|  |             {{ $t('Back to login') }} | ||||||
|  |           </router-link> | ||||||
|  |           <button :class="['ui', {'loading': isLoading}, 'right', 'floated', 'green', 'button']" type="submit"> | ||||||
|  |             {{ $t('Confirm your email') }}</button> | ||||||
|  |         </form> | ||||||
|  |         <div v-else class="ui positive message"> | ||||||
|  |           <div class="header">{{ $t('Email confirmed') }}</div> | ||||||
|  |           <p>{{ $t('Your email address was confirmed, you can now use the service without limitations.') }}</p> | ||||||
|  |           <router-link :to="{name: 'login'}"> | ||||||
|  |             {{ $t('Proceed to login') }} | ||||||
|  |           </router-link> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | import axios from 'axios' | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   props: ['defaultKey'], | ||||||
|  |   data () { | ||||||
|  |     return { | ||||||
|  |       isLoading: false, | ||||||
|  |       errors: [], | ||||||
|  |       key: this.defaultKey, | ||||||
|  |       success: false | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     submit () { | ||||||
|  |       let self = this | ||||||
|  |       self.isLoading = true | ||||||
|  |       self.errors = [] | ||||||
|  |       let payload = { | ||||||
|  |         key: this.key | ||||||
|  |       } | ||||||
|  |       return axios.post('auth/registration/verify-email/', payload).then(response => { | ||||||
|  |         self.isLoading = false | ||||||
|  |         self.success = true | ||||||
|  |       }, error => { | ||||||
|  |         self.errors = error.backendErrors | ||||||
|  |         self.isLoading = false | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <!-- Add "scoped" attribute to limit CSS to this component only --> | ||||||
|  | <style scoped> | ||||||
|  | </style> | ||||||
|  | @ -0,0 +1,75 @@ | ||||||
|  | <template> | ||||||
|  |   <div class="main pusher" v-title="$t('Reset your password')"> | ||||||
|  |     <div class="ui vertical stripe segment"> | ||||||
|  |       <div class="ui small text container"> | ||||||
|  |         <h2>{{ $t('Reset your password') }}</h2> | ||||||
|  |         <form class="ui form" @submit.prevent="submit()"> | ||||||
|  |           <div v-if="errors.length > 0" class="ui negative message"> | ||||||
|  |             <div class="header">{{ $t('Error while asking for a password reset') }}</div> | ||||||
|  |             <ul class="list"> | ||||||
|  |               <li v-for="error in errors">{{ error }}</li> | ||||||
|  |             </ul> | ||||||
|  |           </div> | ||||||
|  |           <p>{{ $t('Use this form to request a password reset. We will send an email to the given address with instructions to reset your password.') }}</p> | ||||||
|  |           <div class="field"> | ||||||
|  |             <label>{{ $t('Account\'s email') }}</label> | ||||||
|  |             <input | ||||||
|  |               required | ||||||
|  |               ref="email" | ||||||
|  |               type="email" | ||||||
|  |               autofocus | ||||||
|  |               :placeholder="$t('Input the email address binded to your account')" | ||||||
|  |               v-model="email"> | ||||||
|  |           </div> | ||||||
|  |           <router-link :to="{path: '/login'}"> | ||||||
|  |             {{ $t('Back to login') }} | ||||||
|  |           </router-link> | ||||||
|  |           <button :class="['ui', {'loading': isLoading}, 'right', 'floated', 'green', 'button']" type="submit"> | ||||||
|  |             {{ $t('Ask for a password reset') }}</button> | ||||||
|  |         </form> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | import axios from 'axios' | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   props: ['defaultEmail'], | ||||||
|  |   data () { | ||||||
|  |     return { | ||||||
|  |       email: this.defaultEmail, | ||||||
|  |       isLoading: false, | ||||||
|  |       errors: [] | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   mounted () { | ||||||
|  |     this.$refs.email.focus() | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     submit () { | ||||||
|  |       let self = this | ||||||
|  |       self.isLoading = true | ||||||
|  |       self.errors = [] | ||||||
|  |       let payload = { | ||||||
|  |         email: this.email | ||||||
|  |       } | ||||||
|  |       return axios.post('auth/password/reset/', payload).then(response => { | ||||||
|  |         self.isLoading = false | ||||||
|  |         self.$router.push({ | ||||||
|  |           name: 'auth.password-reset-confirm' | ||||||
|  |         }) | ||||||
|  |       }, error => { | ||||||
|  |         self.errors = error.backendErrors | ||||||
|  |         self.isLoading = false | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <!-- Add "scoped" attribute to limit CSS to this component only --> | ||||||
|  | <style scoped> | ||||||
|  | </style> | ||||||
|  | @ -0,0 +1,85 @@ | ||||||
|  | <template> | ||||||
|  |   <div class="main pusher" v-title="$t('Change your password')"> | ||||||
|  |     <div class="ui vertical stripe segment"> | ||||||
|  |       <div class="ui small text container"> | ||||||
|  |         <h2>{{ $t('Change your password') }}</h2> | ||||||
|  |         <form v-if="!success" class="ui form" @submit.prevent="submit()"> | ||||||
|  |           <div v-if="errors.length > 0" class="ui negative message"> | ||||||
|  |             <div class="header">{{ $t('Error while changing your password') }}</div> | ||||||
|  |             <ul class="list"> | ||||||
|  |               <li v-for="error in errors">{{ error }}</li> | ||||||
|  |             </ul> | ||||||
|  |           </div> | ||||||
|  |           <template v-if="token && uid"> | ||||||
|  |             <div class="field"> | ||||||
|  |               <label>{{ $t('New password') }}</label> | ||||||
|  |               <password-input v-model="newPassword" /> | ||||||
|  |             </div> | ||||||
|  |             <router-link :to="{path: '/login'}"> | ||||||
|  |               {{ $t('Back to login') }} | ||||||
|  |             </router-link> | ||||||
|  |             <button :class="['ui', {'loading': isLoading}, 'right', 'floated', 'green', 'button']" type="submit"> | ||||||
|  |               {{ $t('Update your password') }}</button> | ||||||
|  |           </template> | ||||||
|  |           <template v-else> | ||||||
|  |             <p>{{ $t('If the email address provided in the previous step is valid and binded to a user account, you should receive an email with reset instructions in the next couple of minutes.') }}</p> | ||||||
|  |           </template> | ||||||
|  |         </form> | ||||||
|  |         <div v-else class="ui positive message"> | ||||||
|  |           <div class="header">{{ $t('Password updated successfully') }}</div> | ||||||
|  |           <p>{{ $t('Your password has been updated successfully.') }}</p> | ||||||
|  |           <router-link :to="{name: 'login'}"> | ||||||
|  |             {{ $t('Proceed to login') }} | ||||||
|  |           </router-link> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | import axios from 'axios' | ||||||
|  | import PasswordInput from '@/components/forms/PasswordInput' | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   props: ['defaultToken', 'defaultUid'], | ||||||
|  |   components: { | ||||||
|  |     PasswordInput | ||||||
|  |   }, | ||||||
|  |   data () { | ||||||
|  |     return { | ||||||
|  |       newPassword: '', | ||||||
|  |       isLoading: false, | ||||||
|  |       errors: [], | ||||||
|  |       token: this.defaultToken, | ||||||
|  |       uid: this.defaultUid, | ||||||
|  |       success: false | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     submit () { | ||||||
|  |       let self = this | ||||||
|  |       self.isLoading = true | ||||||
|  |       self.errors = [] | ||||||
|  |       let payload = { | ||||||
|  |         uid: this.uid, | ||||||
|  |         token: this.token, | ||||||
|  |         new_password1: this.newPassword, | ||||||
|  |         new_password2: this.newPassword | ||||||
|  |       } | ||||||
|  |       return axios.post('auth/password/reset/confirm/', payload).then(response => { | ||||||
|  |         self.isLoading = false | ||||||
|  |         self.success = true | ||||||
|  |       }, error => { | ||||||
|  |         self.errors = error.backendErrors | ||||||
|  |         self.isLoading = false | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <!-- Add "scoped" attribute to limit CSS to this component only --> | ||||||
|  | <style scoped> | ||||||
|  | </style> | ||||||
		Ładowanie…
	
		Reference in New Issue
	
	 Eliot Berriot
						Eliot Berriot