From d18f98e0f8da5362f3409f9a00f9cc6b2f2d2361 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Tue, 19 Jun 2018 22:23:22 +0200 Subject: [PATCH] See #248: can now sign up using invitation code --- api/funkwhale_api/users/admin.py | 16 ++++++++--- api/funkwhale_api/users/serializers.py | 23 +++++++++++++++ api/funkwhale_api/users/views.py | 5 +++- api/tests/users/test_views.py | 33 +++++++++++++++++++++ front/src/components/auth/Signup.vue | 40 +++++++++++++++++--------- front/src/router/index.js | 5 +++- 6 files changed, 103 insertions(+), 19 deletions(-) diff --git a/api/funkwhale_api/users/admin.py b/api/funkwhale_api/users/admin.py index 5c694ab0e..2affd0836 100644 --- a/api/funkwhale_api/users/admin.py +++ b/api/funkwhale_api/users/admin.py @@ -7,12 +7,12 @@ from django.contrib.auth.admin import UserAdmin as AuthUserAdmin from django.contrib.auth.forms import UserChangeForm, UserCreationForm from django.utils.translation import ugettext_lazy as _ -from .models import User +from . import models class MyUserChangeForm(UserChangeForm): class Meta(UserChangeForm.Meta): - model = User + model = models.User class MyUserCreationForm(UserCreationForm): @@ -22,7 +22,7 @@ class MyUserCreationForm(UserCreationForm): ) class Meta(UserCreationForm.Meta): - model = User + model = models.User def clean_username(self): username = self.cleaned_data["username"] @@ -33,7 +33,7 @@ class MyUserCreationForm(UserCreationForm): raise forms.ValidationError(self.error_messages["duplicate_username"]) -@admin.register(User) +@admin.register(models.User) class UserAdmin(AuthUserAdmin): form = MyUserChangeForm add_form = MyUserCreationForm @@ -74,3 +74,11 @@ class UserAdmin(AuthUserAdmin): (_("Important dates"), {"fields": ("last_login", "date_joined")}), (_("Useless fields"), {"fields": ("user_permissions", "groups")}), ) + + +@admin.register(models.Invitation) +class InvitationAdmin(admin.ModelAdmin): + list_select_related = True + list_display = ["owner", "code", "creation_date", "expiration_date"] + search_fields = ["owner__username", "code"] + readonly_fields = ["expiration_date", "code"] diff --git a/api/funkwhale_api/users/serializers.py b/api/funkwhale_api/users/serializers.py index b3bd431c7..f857e8da6 100644 --- a/api/funkwhale_api/users/serializers.py +++ b/api/funkwhale_api/users/serializers.py @@ -1,5 +1,6 @@ from django.conf import settings from rest_auth.serializers import PasswordResetSerializer as PRS +from rest_auth.registration.serializers import RegisterSerializer as RS from rest_framework import serializers from funkwhale_api.activity import serializers as activity_serializers @@ -7,6 +8,28 @@ from funkwhale_api.activity import serializers as activity_serializers from . import models +class RegisterSerializer(RS): + invitation = serializers.CharField( + required=False, allow_null=True, allow_blank=True + ) + + def validate_invitation(self, value): + if not value: + return + + try: + return models.Invitation.objects.open().get(code=value.lower()) + except models.Invitation.DoesNotExist: + raise serializers.ValidationError("Invalid invitation code") + + def save(self, request): + user = super().save(request) + if self.validated_data.get("invitation"): + user.invitation = self.validated_data.get("invitation") + user.save(update_fields=["invitation"]) + return user + + class UserActivitySerializer(activity_serializers.ModelSerializer): type = serializers.SerializerMethodField() name = serializers.CharField(source="username") diff --git a/api/funkwhale_api/users/views.py b/api/funkwhale_api/users/views.py index 69e69d26e..20d63d788 100644 --- a/api/funkwhale_api/users/views.py +++ b/api/funkwhale_api/users/views.py @@ -10,8 +10,11 @@ from . import models, serializers class RegisterView(BaseRegisterView): + serializer_class = serializers.RegisterSerializer + def create(self, request, *args, **kwargs): - if not self.is_open_for_signup(request): + invitation_code = request.data.get("invitation") + if not invitation_code and not self.is_open_for_signup(request): r = {"detail": "Registration has been disabled"} return Response(r, status=403) return super().create(request, *args, **kwargs) diff --git a/api/tests/users/test_views.py b/api/tests/users/test_views.py index 00272c2ae..0ad67fb86 100644 --- a/api/tests/users/test_views.py +++ b/api/tests/users/test_views.py @@ -50,6 +50,39 @@ def test_can_disable_registration_view(preferences, api_client, db): assert response.status_code == 403 +def test_can_signup_with_invitation(preferences, factories, api_client): + url = reverse("rest_register") + invitation = factories["users.Invitation"](code="hello") + data = { + "username": "test1", + "email": "test1@test.com", + "password1": "testtest", + "password2": "testtest", + "invitation": "hello", + } + preferences["users__registration_enabled"] = False + response = api_client.post(url, data) + assert response.status_code == 201 + u = User.objects.get(email="test1@test.com") + assert u.username == "test1" + assert u.invitation == invitation + + +def test_can_signup_with_invitation_invalid(preferences, factories, api_client): + url = reverse("rest_register") + invitation = factories["users.Invitation"](code="hello") + data = { + "username": "test1", + "email": "test1@test.com", + "password1": "testtest", + "password2": "testtest", + "invitation": "nope", + } + response = api_client.post(url, data) + assert response.status_code == 400 + assert "invitation" in response.data + + def test_can_fetch_data_from_api(api_client, factories): url = reverse("api:v1:users:users-me") response = api_client.get(url) diff --git a/front/src/components/auth/Signup.vue b/front/src/components/auth/Signup.vue index 89f4cb1f1..e4e5cebbc 100644 --- a/front/src/components/auth/Signup.vue +++ b/front/src/components/auth/Signup.vue @@ -2,19 +2,22 @@
-

+

{{ $t("Create a funkwhale account") }}

+

+ {{ $t('Registration are closed on this instance, you will need an invitation code to signup.') }} +

+
-
+
{{ $t("We cannot create your account") }}
  • {{ error }}
- +
- +
- +
- +
+ + + +
+
-
@@ -51,13 +64,13 @@ import logger from '@/logging' import PasswordInput from '@/components/forms/PasswordInput' export default { - name: 'login', + props: { + invitation: {type: String, required: false, default: null}, + next: {type: String, default: '/'} + }, components: { PasswordInput }, - props: { - next: {type: String, default: '/'} - }, data () { return { username: '', @@ -85,7 +98,8 @@ export default { username: this.username, password1: this.password, password2: this.password, - email: this.email + email: this.email, + invitation: this.invitation } return axios.post('auth/registration/', payload).then(response => { logger.default.info('Successfully created account') diff --git a/front/src/router/index.js b/front/src/router/index.js index 0d2ad34f9..5528addd4 100644 --- a/front/src/router/index.js +++ b/front/src/router/index.js @@ -96,7 +96,10 @@ export default new Router({ { path: '/signup', name: 'signup', - component: Signup + component: Signup, + props: (route) => ({ + invitation: route.query.invitation + }) }, { path: '/logout',