Merge branch '187-emails' into 'develop'
Resolve "Add email support" Closes #187 See merge request funkwhale/funkwhale!182
This commit is contained in:
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,
|
|
||||||
mixins.RetrieveModelMixin,
|
class ListeningViewSet(
|
||||||
viewsets.GenericViewSet):
|
mixins.CreateModelMixin,
|
||||||
|
mixins.RetrieveModelMixin,
|
||||||
|
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]
|
||||||
|
@ -227,25 +213,25 @@ def test_can_start_tag_radio(factories):
|
||||||
|
|
||||||
radio = radios.TagRadio()
|
radio = radios.TagRadio()
|
||||||
session = radio.start_session(user, related_object=tag)
|
session = radio.start_session(user, related_object=tag)
|
||||||
assert session.radio_type =='tag'
|
assert session.radio_type == 'tag'
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
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``
|
||||||
|
|
|
@ -35,24 +35,24 @@
|
||||||
<div class="header">{{ $t('My account') }}</div>
|
<div class="header">{{ $t('My account') }}</div>
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
<router-link class="item" v-if="$store.state.auth.authenticated" :to="{name: 'profile', params: {username: $store.state.auth.username}}"><i class="user icon"></i>{{ $t('Logged in as {%name%}', { name: $store.state.auth.username }) }}</router-link>
|
<router-link class="item" v-if="$store.state.auth.authenticated" :to="{name: 'profile', params: {username: $store.state.auth.username}}"><i class="user icon"></i>{{ $t('Logged in as {%name%}', { name: $store.state.auth.username }) }}</router-link>
|
||||||
<router-link class="item" v-if="$store.state.auth.authenticated" :to="{name: 'logout'}"><i class="sign out icon"></i> {{ $t('Logout') }}</router-link>
|
<router-link class="item" v-if="$store.state.auth.authenticated" :to="{name: 'logout'}"><i class="sign out icon"></i>{{ $t('Logout') }}</router-link>
|
||||||
<router-link class="item" v-else :to="{name: 'login'}"><i class="sign in icon"></i> {{ $t('Login') }}</router-link>
|
<router-link class="item" v-else :to="{name: 'login'}"><i class="sign in icon"></i>{{ $t('Login') }}</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<div class="header">{{ $t('Music') }}</div>
|
<div class="header">{{ $t('Music') }}</div>
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
<router-link class="item" :to="{path: '/library'}"><i class="sound icon"> </i>{{ $t('Browse library') }}</router-link>
|
<router-link class="item" :to="{path: '/library'}"><i class="sound icon"> </i>{{ $t('Browse library') }}</router-link>
|
||||||
<router-link class="item" v-if="$store.state.auth.authenticated" :to="{path: '/favorites'}"><i class="heart icon"></i> {{ $t('Favorites') }}</router-link>
|
<router-link class="item" v-if="$store.state.auth.authenticated" :to="{path: '/favorites'}"><i class="heart icon"></i>{{ $t('Favorites') }}</router-link>
|
||||||
<a
|
<a
|
||||||
@click="$store.commit('playlists/chooseTrack', null)"
|
@click="$store.commit('playlists/chooseTrack', null)"
|
||||||
v-if="$store.state.auth.authenticated"
|
v-if="$store.state.auth.authenticated"
|
||||||
class="item">
|
class="item">
|
||||||
<i class="list icon"></i> {{ $t('Playlists') }}
|
<i class="list icon"></i>{{ $t('Playlists') }}
|
||||||
</a>
|
</a>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="$store.state.auth.authenticated"
|
v-if="$store.state.auth.authenticated"
|
||||||
class="item" :to="{path: '/activity'}"><i class="bell icon"></i> {{ $t('Activity') }}</router-link>
|
class="item" :to="{path: '/activity'}"><i class="bell icon"></i>{{ $t('Activity') }}</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="item" v-if="showAdmin">
|
<div class="item" v-if="showAdmin">
|
||||||
|
@ -62,7 +62,7 @@
|
||||||
class="item"
|
class="item"
|
||||||
v-if="$store.state.auth.availablePermissions['import.launch']"
|
v-if="$store.state.auth.availablePermissions['import.launch']"
|
||||||
:to="{name: 'library.requests', query: {status: 'pending' }}">
|
:to="{name: 'library.requests', query: {status: 'pending' }}">
|
||||||
<i class="download icon"></i> {{ $t('Import requests') }}
|
<i class="download icon"></i>{{ $t('Import requests') }}
|
||||||
<div
|
<div
|
||||||
:class="['ui', {'teal': notifications.importRequests > 0}, 'label']"
|
:class="['ui', {'teal': notifications.importRequests > 0}, 'label']"
|
||||||
:title="$t('Pending import requests')">
|
:title="$t('Pending import requests')">
|
||||||
|
@ -72,7 +72,7 @@
|
||||||
class="item"
|
class="item"
|
||||||
v-if="$store.state.auth.availablePermissions['federation.manage']"
|
v-if="$store.state.auth.availablePermissions['federation.manage']"
|
||||||
:to="{path: '/manage/federation/libraries'}">
|
:to="{path: '/manage/federation/libraries'}">
|
||||||
<i class="sitemap icon"></i> {{ $t('Federation') }}
|
<i class="sitemap icon"></i>{{ $t('Federation') }}
|
||||||
<div
|
<div
|
||||||
:class="['ui', {'teal': notifications.federation > 0}, 'label']"
|
:class="['ui', {'teal': notifications.federation > 0}, 'label']"
|
||||||
:title="$t('Pending follow requests')">
|
:title="$t('Pending follow requests')">
|
||||||
|
|
|
@ -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"
|
</router-link>
|
||||||
v-model="credentials.password"
|
</label>
|
||||||
>
|
<password-input :index="2" required v-model="credentials.password" />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<button :class="['ui', {'loading': isLoading}, 'button']" type="submit"><i18next path="Login"/></button>
|
<button tabindex="3" :class="['ui', {'loading': isLoading}, 'right', 'floated', 'green', '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>
|
|
||||||
</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>
|
Loading…
Reference in New Issue