From d18f98e0f8da5362f3409f9a00f9cc6b2f2d2361 Mon Sep 17 00:00:00 2001 From: Eliot Berriot <contact@eliotberriot.com> 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 5c694ab0..2affd083 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 b3bd431c..f857e8da 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 69e69d26..20d63d78 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 00272c2a..0ad67fb8 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 89f4cb1f..e4e5cebb 100644 --- a/front/src/components/auth/Signup.vue +++ b/front/src/components/auth/Signup.vue @@ -2,19 +2,22 @@ <div class="main pusher" v-title="'Sign Up'"> <div class="ui vertical stripe segment"> <div class="ui small text container"> - <h2><i18next path="Create a funkwhale account"/></h2> + <h2>{{ $t("Create a funkwhale account") }}</h2> <form - v-if="$store.state.instance.settings.users.registration_enabled.value" :class="['ui', {'loading': isLoadingInstanceSetting}, 'form']" @submit.prevent="submit()"> + <p class="ui message" v-if="!$store.state.instance.settings.users.registration_enabled.value"> + {{ $t('Registration are closed on this instance, you will need an invitation code to signup.') }} + </p> + <div v-if="errors.length > 0" class="ui negative message"> - <div class="header"><i18next path="We cannot create your account"/></div> + <div class="header">{{ $t("We cannot create your account") }}</div> <ul class="list"> <li v-for="error in errors">{{ error }}</li> </ul> </div> <div class="field"> - <i18next tag="label" path="Username"/> + <label>{{ $t("Username") }}</label> <input ref="username" required @@ -24,7 +27,7 @@ v-model="username"> </div> <div class="field"> - <i18next tag="label" path="Email"/> + <label>{{ $t("Email") }}</label> <input ref="email" required @@ -33,12 +36,22 @@ v-model="email"> </div> <div class="field"> - <i18next tag="label" path="Password"/> + <label>{{ $t("Password") }}</label> <password-input v-model="password" /> </div> - <button :class="['ui', 'green', {'loading': isLoading}, 'button']" type="submit"><i18next path="Create my account"/></button> + <div class="field"> + <label v-if="!$store.state.instance.settings.users.registration_enabled.value">{{ $t("Invitation code") }}</label> + <label v-else>{{ $t("Invitation code (optional)") }}</label> + <input + :required="!$store.state.instance.settings.users.registration_enabled.value" + type="text" + :placeholder="$t('Enter your invitation code (case insensitive)')" + v-model="invitation"> + </div> + <button :class="['ui', 'green', {'loading': isLoading}, 'button']" type="submit"> + {{ $t("Create my account") }} + </button> </form> - <i18next v-else tag="p" path="Registration is currently disabled on this instance, please try again later."/> </div> </div> </div> @@ -51,13 +64,13 @@ import logger from '@/logging' import PasswordInput from '@/components/forms/PasswordInput' export default { - name: 'login', - components: { - PasswordInput - }, props: { + invitation: {type: String, required: false, default: null}, next: {type: String, default: '/'} }, + components: { + PasswordInput + }, 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 0d2ad34f..5528addd 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', -- GitLab