Search code examples
djangodjango-rest-frameworksoft-deletedjango-rest-framework-simplejwt

Handling JWT Authentication and Soft Deleted Users in Django Rest Framework


I'm trying to make a username available once a user deletes their account. By default the username is unique meaning the username won't be available even after the account is soft deleted.

This is, by default, the setup that comes with django out of the box.

class CustomUser(AbstractUser):
    username = models.CharField(
        _('username'),
        max_length=150,
        unique=True,
        help_text=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'),
        validators=[UnicodeUsernameValidator()],
        error_messages={
            'unique': _("A user with that username already exists."),
        },
    )
    is_active = models.BooleanField(
        _('active'),
        default=True,
        help_text=_(
            'Designates whether this user should be treated as active. '
            'Unselect this instead of deleting accounts.'
        ),
    )

is_active is used here to mark a model as deleted.

So that I can be able to take advantage of UniqueConstraint and add a condition, I have to drop the uniqueness of the username.

class CustomUser(AbstractUser):
    username = models.CharField(
        _('username'),
        max_length=150,
        unique=False,
        help_text=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'),
        validators=[UnicodeUsernameValidator()],
        error_messages={
            'unique': _("A user with that username already exists."),
        },
    )
    is_active = models.BooleanField(
        _('active'),
        default=True,
        help_text=_(
            'Designates whether this user should be treated as active. '
            'Unselect this instead of deleting accounts.'
        ),
    )

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=['username'],
                name='active_username_constraint',
                condition=models.Q(is_active=True)
            )
        ]

This works for registrations. After a user has deleted their account, the username can be re-used during registration. However, when a user logs in, the following error is raised.

MultipleObjectsReturned at /api/vdev/login/
get() returned more than one CustomUser -- it returned 2!

I'm trying to find out a way to check if a user is_active as part of the authentication process. Is there a way this can be done?


Solution

  • I'm guessing the error is raised when the authenticate is being called from the UserModel. Looking at the source code, the method calls get_by_natural_key from the user manager. Examining the source code for this method shows us where we need to make the change.

    Hence, what you'll probably have to do is to create a custom user manager, inheriting from BaseUserManager and overriding get_by_natural_key.

    class MyUserManager(BaseUserManager):
        ...
        def get_by_natural_key(self, username):
            return self.get(**{self.model.USERNAME_FIELD: username, "is_active": True})
    

    Then in your custom user model, set your manager to this custom user manager:

    class CustomUser(AbstractUser):
        ...
        objects = MyUserManager()