Resolves reuse of invitation code

This commit is contained in:
Marcos Peña 2022-11-21 18:50:08 +00:00 committed by JuniorJPDJ
parent 5248a252ec
commit aa17f9679b
8 changed files with 86 additions and 2 deletions

View File

@ -98,12 +98,28 @@ class ManageUserSerializer(serializers.ModelSerializer):
class ManageInvitationSerializer(serializers.ModelSerializer): class ManageInvitationSerializer(serializers.ModelSerializer):
users = ManageUserSimpleSerializer(many=True, required=False) users = ManageUserSimpleSerializer(many=True, required=False)
owner = ManageUserSimpleSerializer(required=False) owner = ManageUserSimpleSerializer(required=False)
invited_user = ManageUserSimpleSerializer(required=False)
code = serializers.CharField(required=False, allow_null=True) code = serializers.CharField(required=False, allow_null=True)
class Meta: class Meta:
model = users_models.Invitation model = users_models.Invitation
fields = ("id", "owner", "code", "expiration_date", "creation_date", "users") fields = (
read_only_fields = ["id", "expiration_date", "owner", "creation_date", "users"] "id",
"owner",
"invited_user",
"code",
"expiration_date",
"creation_date",
"users",
)
read_only_fields = [
"id",
"expiration_date",
"owner",
"invited_user",
"creation_date",
"users",
]
def validate_code(self, value): def validate_code(self, value):
if not value: if not value:

View File

@ -57,6 +57,9 @@ class InvitationFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
class Params: class Params:
expired = factory.Trait(expiration_date=factory.LazyFunction(timezone.now)) expired = factory.Trait(expiration_date=factory.LazyFunction(timezone.now))
with_invited_user = factory.Trait(
invited_user=factory.SubFactory("funkwhale_api.users.factories.UserFactory")
)
class PasswordSetter(factory.PostGenerationMethodCall): class PasswordSetter(factory.PostGenerationMethodCall):

View File

@ -0,0 +1,37 @@
# Generated by Django 3.2.16 on 2022-11-19 18:19
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
def update_invitation_table(apps, schema_editor):
User = apps.get_model("users", "User")
Invitation = apps.get_model("users", "Invitation")
for user in User.objects.all():
if user.invitation:
Invitation.objects.filter(id=user.invitation.id).update(invited_user_id=user.id)
def rewind(*args, **kwargs):
pass
class Migration(migrations.Migration):
dependencies = [
('users', '0021_auto_20210703_1810'),
]
operations = [
migrations.AddField(
model_name='invitation',
name='invited_user',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='user_invitations', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='invitation',
name='owner',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invitations', to=settings.AUTH_USER_MODEL),
),
migrations.RunPython(update_invitation_table, rewind),
]

View File

@ -338,6 +338,9 @@ class Invitation(models.Model):
owner = models.ForeignKey( owner = models.ForeignKey(
User, related_name="invitations", on_delete=models.CASCADE User, related_name="invitations", on_delete=models.CASCADE
) )
invited_user = models.ForeignKey(
User, related_name="user_invitations", null=True, on_delete=models.CASCADE
)
code = models.CharField(max_length=50, unique=True) code = models.CharField(max_length=50, unique=True)
objects = InvitationQuerySet.as_manager() objects = InvitationQuerySet.as_manager()
@ -352,6 +355,10 @@ class Invitation(models.Model):
return super().save(**kwargs) return super().save(**kwargs)
def set_invited_user(self, user):
self.invited_user = user
super().save()
class Application(oauth2_models.AbstractApplication): class Application(oauth2_models.AbstractApplication):
scope = models.TextField(blank=True) scope = models.TextField(blank=True)

View File

@ -40,6 +40,9 @@ class RegisterView(registration_views.RegisterView):
if not user.is_active: if not user.is_active:
# manual approval, we need to send the confirmation e-mail by hand # manual approval, we need to send the confirmation e-mail by hand
authentication.send_email_confirmation(self.request, user) authentication.send_email_confirmation(self.request, user)
if user.invitation:
user.invitation.set_invited_user(user)
return user return user

View File

@ -105,6 +105,13 @@ def test_invitation_generates_random_code_on_save(factories):
assert len(invitation.code) >= 6 assert len(invitation.code) >= 6
def test_invitation_get_deleted_when_user_is_deleted(factories):
invitation = factories["users.Invitation"](with_invited_user=True)
invitation.invited_user.delete()
assert models.Invitation.objects.count() == 0
def test_invitation_expires_after_delay(factories, settings): def test_invitation_expires_after_delay(factories, settings):
delay = settings.USERS_INVITATION_EXPIRATION_DAYS delay = settings.USERS_INVITATION_EXPIRATION_DAYS
invitation = factories["users.Invitation"]() invitation = factories["users.Invitation"]()

View File

@ -0,0 +1 @@
Fixed invitation reuse after the invited user has been deleted (#1952)

View File

@ -75,6 +75,11 @@
Owner Owner
</translate> </translate>
</th> </th>
<th>
<translate translate-context="Content/Admin/Table.Label/Noun">
User
</translate>
</th>
<th> <th>
<translate translate-context="*/*/*"> <translate translate-context="*/*/*">
Status Status
@ -105,6 +110,11 @@
{{ scope.obj.owner.username }} {{ scope.obj.owner.username }}
</router-link> </router-link>
</td> </td>
<td>
<span v-if="scope.obj.invited_user">
{{ scope.obj.invited_user.username }}
</span>
</td>
<td> <td>
<span <span
v-if="scope.obj.users.length > 0" v-if="scope.obj.users.length > 0"