mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-11-26 11:31:08 +00:00
Allow users to temporarily deactivate their accounts (#2324)
This commit is contained in:
parent
3ebd957d3d
commit
eae1866992
16 changed files with 333 additions and 39 deletions
|
@ -7,6 +7,7 @@ from django.utils.translation import gettext_lazy as _
|
|||
import pyotp
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.settings import DOMAIN
|
||||
from .custom_form import CustomForm
|
||||
|
||||
|
||||
|
@ -20,6 +21,21 @@ class LoginForm(CustomForm):
|
|||
"password": forms.PasswordInput(),
|
||||
}
|
||||
|
||||
def infer_username(self):
|
||||
"""Users may enter their localname, username, or email"""
|
||||
localname = self.data.get("localname")
|
||||
if "@" in localname: # looks like an email address to me
|
||||
try:
|
||||
return models.User.objects.get(email=localname).username
|
||||
except models.User.DoesNotExist: # maybe it's a full username?
|
||||
return localname
|
||||
return f"{localname}@{DOMAIN}"
|
||||
|
||||
def add_invalid_password_error(self):
|
||||
"""We don't want to be too specific about this"""
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
self.non_field_errors = _("Username or password are incorrect")
|
||||
|
||||
|
||||
class RegisterForm(CustomForm):
|
||||
class Meta:
|
||||
|
|
52
bookwyrm/migrations/0160_auto_20221101_2251.py
Normal file
52
bookwyrm/migrations/0160_auto_20221101_2251.py
Normal file
|
@ -0,0 +1,52 @@
|
|||
# Generated by Django 3.2.15 on 2022-11-01 22:51
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0159_auto_20220924_0634"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="allow_reactivation",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="connector",
|
||||
name="deactivation_reason",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("pending", "Pending"),
|
||||
("self_deletion", "Self deletion"),
|
||||
("self_deactivation", "Self deactivation"),
|
||||
("moderator_suspension", "Moderator suspension"),
|
||||
("moderator_deletion", "Moderator deletion"),
|
||||
("domain_block", "Domain block"),
|
||||
],
|
||||
max_length=255,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="deactivation_reason",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("pending", "Pending"),
|
||||
("self_deletion", "Self deletion"),
|
||||
("self_deactivation", "Self deactivation"),
|
||||
("moderator_suspension", "Moderator suspension"),
|
||||
("moderator_deletion", "Moderator deletion"),
|
||||
("domain_block", "Domain block"),
|
||||
],
|
||||
max_length=255,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,13 @@
|
|||
# Generated by Django 3.2.15 on 2022-11-10 20:34
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookwyrm", "0160_auto_20221101_2251"),
|
||||
("bookwyrm", "0162_importjob_task_id"),
|
||||
]
|
||||
|
||||
operations = []
|
|
@ -17,6 +17,7 @@ from .fields import RemoteIdField
|
|||
DeactivationReason = [
|
||||
("pending", _("Pending")),
|
||||
("self_deletion", _("Self deletion")),
|
||||
("self_deactivation", _("Self deactivation")),
|
||||
("moderator_suspension", _("Moderator suspension")),
|
||||
("moderator_deletion", _("Moderator deletion")),
|
||||
("domain_block", _("Domain block")),
|
||||
|
|
|
@ -47,6 +47,7 @@ def site_link():
|
|||
return f"{protocol}://{DOMAIN}"
|
||||
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
class User(OrderedCollectionPageMixin, AbstractUser):
|
||||
"""a user who wants to read books"""
|
||||
|
||||
|
@ -169,6 +170,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
|||
max_length=255, choices=DeactivationReason, null=True, blank=True
|
||||
)
|
||||
deactivation_date = models.DateTimeField(null=True, blank=True)
|
||||
allow_reactivation = models.BooleanField(default=False)
|
||||
confirmation_code = models.CharField(max_length=32, default=new_access_code)
|
||||
|
||||
name_field = "username"
|
||||
|
@ -367,12 +369,28 @@ class User(OrderedCollectionPageMixin, AbstractUser):
|
|||
self.create_shelves()
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
"""deactivate rather than delete a user"""
|
||||
"""We don't actually delete the database entry"""
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
self.is_active = False
|
||||
# skip the logic in this class's save()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def deactivate(self):
|
||||
"""Disable the user but allow them to reactivate"""
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
self.is_active = False
|
||||
self.deactivation_reason = "self_deactivation"
|
||||
self.allow_reactivation = True
|
||||
super().save(broadcast=False)
|
||||
|
||||
def reactivate(self):
|
||||
"""Now you want to come back, huh?"""
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
self.is_active = True
|
||||
self.deactivation_reason = None
|
||||
self.allow_reactivation = False
|
||||
super().save(broadcast=False)
|
||||
|
||||
@property
|
||||
def local_path(self):
|
||||
"""this model doesn't inherit bookwyrm model, so here we are"""
|
||||
|
|
|
@ -21,7 +21,7 @@ RELEASE_API = env(
|
|||
PAGE_LENGTH = env("PAGE_LENGTH", 15)
|
||||
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
|
||||
|
||||
JS_CACHE = "e678183b"
|
||||
JS_CACHE = "e678183c"
|
||||
|
||||
# email
|
||||
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
{% if show_confirmed_email %}
|
||||
<p class="notification is-success">{% trans "Success! Email address confirmed." %}</p>
|
||||
{% endif %}
|
||||
<form name="login-confirm" method="post" action="/login">
|
||||
<form name="login-confirm" method="post" action="{% url 'login' %}">
|
||||
{% csrf_token %}
|
||||
{% if show_confirmed_email %}<input type="hidden" name="first_login" value="true">{% endif %}
|
||||
<div class="field">
|
||||
|
|
60
bookwyrm/templates/landing/reactivate.html
Normal file
60
bookwyrm/templates/landing/reactivate.html
Normal file
|
@ -0,0 +1,60 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Reactivate Account" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="title">{% trans "Reactivate Account" %}</h1>
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-half">
|
||||
{% if login_form.non_field_errors %}
|
||||
<p class="notification is-danger">{{ login_form.non_field_errors }}</p>
|
||||
{% endif %}
|
||||
|
||||
<form name="login-confirm" method="post" action="{% url 'prefs-reactivate' %}">
|
||||
{% csrf_token %}
|
||||
<div class="field">
|
||||
<label class="label" for="id_localname_confirm">{% trans "Username:" %}</label>
|
||||
<div class="control">
|
||||
<input type="text" name="localname" maxlength="255" class="input" required="" id="id_localname_confirm" value="{{ login_form.localname.value|default:'' }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="id_password_confirm">{% trans "Password:" %}</label>
|
||||
<div class="control">
|
||||
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password_confirm" aria-describedby="desc_password">
|
||||
</div>
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=login_form.password.errors id="desc_password" %}
|
||||
</div>
|
||||
<div class="field is-grouped">
|
||||
<div class="control">
|
||||
<button class="button is-primary" type="submit">{% trans "Reactivate account" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% if site.allow_registration %}
|
||||
<div class="column is-half">
|
||||
<div class="box has-background-primary-light">
|
||||
<h2 class="title">{% trans "Create an Account" %}</h2>
|
||||
<form name="register" method="post" action="/register">
|
||||
{% include 'snippets/register_form.html' %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="column">
|
||||
<div class="box">
|
||||
{% include 'snippets/about.html' %}
|
||||
|
||||
<p class="block">
|
||||
<a href="{% url 'about' %}">{% trans "More about this site" %}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -8,22 +8,37 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block panel %}
|
||||
<div class="block">
|
||||
<h2 class="title is-4">{% trans "Deactivate account" %}</h2>
|
||||
<div class="box">
|
||||
<p class="notification is-link is-light">
|
||||
{% trans "Your account will be hidden. You can log back in at any time to re-activate your account." %}
|
||||
</p>
|
||||
|
||||
<form name="deactivate-user" action="{% url 'prefs-deactivate' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="button is-link">{% trans "Deactivate Account" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<h2 class="title is-4">{% trans "Permanently delete account" %}</h2>
|
||||
<p class="notification is-danger is-light">
|
||||
{% trans "Deleting your account cannot be undone. The username will not be available to register in the future." %}
|
||||
</p>
|
||||
<div class="box">
|
||||
<p class="notification is-danger is-light">
|
||||
{% trans "Deleting your account cannot be undone. The username will not be available to register in the future." %}
|
||||
</p>
|
||||
|
||||
<form name="delete-user" action="{% url 'prefs-delete' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="field">
|
||||
<label class="label" for="id_password">{% trans "Confirm password:" %}</label>
|
||||
<input class="input {% if form.password.errors %}is-danger{% endif %}" type="password" name="password" id="id_password" required aria-describedby="desc_password">
|
||||
<form name="delete-user" action="{% url 'prefs-delete' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="field">
|
||||
<label class="label" for="id_password">{% trans "Confirm password:" %}</label>
|
||||
<input class="input {% if form.password.errors %}is-danger{% endif %}" type="password" name="password" id="id_password" required aria-describedby="desc_password">
|
||||
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.password.errors id="desc_password" %}
|
||||
</div>
|
||||
<button type="submit" class="button is-danger">{% trans "Delete Account" %}</button>
|
||||
</form>
|
||||
{% include 'snippets/form_errors.html' with errors_list=form.password.errors id="desc_password" %}
|
||||
</div>
|
||||
<button type="submit" class="button is-danger">{% trans "Delete Account" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ from bookwyrm.tests.validate_html import validate_html
|
|||
class LoginViews(TestCase):
|
||||
"""login and password management"""
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def setUp(self):
|
||||
"""we need basic test data and mocks"""
|
||||
self.factory = RequestFactory()
|
||||
|
@ -81,7 +82,7 @@ class LoginViews(TestCase):
|
|||
self.assertEqual(result.status_code, 302)
|
||||
|
||||
def test_login_post_username(self, *_):
|
||||
"""there are so many views, this just makes sure it LOADS"""
|
||||
"""valid login where the user provides their user@domain.com username"""
|
||||
view = views.Login.as_view()
|
||||
form = forms.LoginForm()
|
||||
form.data["localname"] = "mouse@your.domain.here"
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import json
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.template.response import TemplateResponse
|
||||
from django.test import TestCase
|
||||
|
@ -15,6 +16,7 @@ from bookwyrm.tests.validate_html import validate_html
|
|||
class DeleteUserViews(TestCase):
|
||||
"""view user and edit profile"""
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def setUp(self):
|
||||
"""we need basic test data and mocks"""
|
||||
self.factory = RequestFactory()
|
||||
|
@ -22,14 +24,18 @@ class DeleteUserViews(TestCase):
|
|||
"bookwyrm.activitystreams.populate_stream_task.delay"
|
||||
), patch("bookwyrm.lists_stream.populate_lists_task.delay"):
|
||||
self.local_user = models.User.objects.create_user(
|
||||
"mouse@local.com",
|
||||
"mouse@your.domain.here",
|
||||
"mouse@mouse.mouse",
|
||||
"password",
|
||||
local=True,
|
||||
localname="mouse",
|
||||
)
|
||||
self.rat = models.User.objects.create_user(
|
||||
"rat@local.com", "rat@rat.rat", "password", local=True, localname="rat"
|
||||
"rat@your.domain.here",
|
||||
"rat@rat.rat",
|
||||
"password",
|
||||
local=True,
|
||||
localname="rat",
|
||||
)
|
||||
|
||||
self.book = models.Edition.objects.create(
|
||||
|
@ -44,6 +50,8 @@ class DeleteUserViews(TestCase):
|
|||
shelf=self.local_user.shelf_set.first(),
|
||||
)
|
||||
|
||||
self.anonymous_user = AnonymousUser
|
||||
self.anonymous_user.is_authenticated = False
|
||||
models.SiteSettings.objects.create()
|
||||
|
||||
def test_delete_user_page(self, _):
|
||||
|
@ -84,3 +92,52 @@ class DeleteUserViews(TestCase):
|
|||
self.local_user.refresh_from_db()
|
||||
self.assertFalse(self.local_user.is_active)
|
||||
self.assertEqual(self.local_user.deactivation_reason, "self_deletion")
|
||||
|
||||
def test_deactivate_user(self, _):
|
||||
"""Impermanent deletion"""
|
||||
self.assertTrue(self.local_user.is_active)
|
||||
view = views.DeactivateUser.as_view()
|
||||
request = self.factory.post("")
|
||||
request.user = self.local_user
|
||||
middleware = SessionMiddleware()
|
||||
middleware.process_request(request)
|
||||
request.session.save()
|
||||
|
||||
view(request)
|
||||
|
||||
self.local_user.refresh_from_db()
|
||||
self.assertFalse(self.local_user.is_active)
|
||||
self.assertEqual(self.local_user.deactivation_reason, "self_deactivation")
|
||||
|
||||
def test_reactivate_user_get(self, _):
|
||||
"""Reactication page"""
|
||||
view = views.ReactivateUser.as_view()
|
||||
request = self.factory.get("")
|
||||
request.user = self.anonymous_user
|
||||
|
||||
result = view(request)
|
||||
self.assertIsInstance(result, TemplateResponse)
|
||||
validate_html(result.render())
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
def test_reactivate_user_post(self, _):
|
||||
"""Reactivate action"""
|
||||
self.local_user.deactivate()
|
||||
self.local_user.refresh_from_db()
|
||||
|
||||
view = views.ReactivateUser.as_view()
|
||||
form = forms.LoginForm()
|
||||
form.data["localname"] = "mouse"
|
||||
form.data["password"] = "password"
|
||||
request = self.factory.post("", form.data)
|
||||
request.user = self.local_user
|
||||
middleware = SessionMiddleware()
|
||||
middleware.process_request(request)
|
||||
request.session.save()
|
||||
|
||||
with patch("bookwyrm.views.preferences.delete_user.login"):
|
||||
view(request)
|
||||
|
||||
self.local_user.refresh_from_db()
|
||||
self.assertTrue(self.local_user.is_active)
|
||||
self.assertIsNone(self.local_user.deactivation_reason)
|
||||
|
|
|
@ -526,6 +526,16 @@ urlpatterns = [
|
|||
),
|
||||
re_path(r"^preferences/export/?$", views.Export.as_view(), name="prefs-export"),
|
||||
re_path(r"^preferences/delete/?$", views.DeleteUser.as_view(), name="prefs-delete"),
|
||||
re_path(
|
||||
r"^preferences/deactivate/?$",
|
||||
views.DeactivateUser.as_view(),
|
||||
name="prefs-deactivate",
|
||||
),
|
||||
re_path(
|
||||
r"^preferences/reactivate/?$",
|
||||
views.ReactivateUser.as_view(),
|
||||
name="prefs-reactivate",
|
||||
),
|
||||
re_path(r"^preferences/block/?$", views.Block.as_view(), name="prefs-block"),
|
||||
re_path(r"^block/(?P<user_id>\d+)/?$", views.Block.as_view()),
|
||||
re_path(r"^unblock/(?P<user_id>\d+)/?$", views.unblock),
|
||||
|
|
|
@ -31,7 +31,7 @@ from .admin.user_admin import UserAdmin, UserAdminList
|
|||
from .preferences.change_password import ChangePassword
|
||||
from .preferences.edit_user import EditUser
|
||||
from .preferences.export import Export
|
||||
from .preferences.delete_user import DeleteUser
|
||||
from .preferences.delete_user import DeleteUser, DeactivateUser, ReactivateUser
|
||||
from .preferences.block import Block, unblock
|
||||
from .preferences.two_factor_auth import (
|
||||
Edit2FA,
|
||||
|
|
|
@ -6,12 +6,10 @@ from django.contrib.auth.decorators import login_required
|
|||
from django.shortcuts import redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
from django.views.decorators.debug import sensitive_variables, sensitive_post_parameters
|
||||
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm.settings import DOMAIN
|
||||
from bookwyrm.views.helpers import set_language
|
||||
|
||||
|
||||
|
@ -40,19 +38,13 @@ class Login(View):
|
|||
return redirect("/")
|
||||
login_form = forms.LoginForm(request.POST)
|
||||
|
||||
localname = login_form.data.get("localname")
|
||||
|
||||
if "@" in localname: # looks like an email address to me
|
||||
try:
|
||||
username = models.User.objects.get(email=localname).username
|
||||
except models.User.DoesNotExist: # maybe it's a full username?
|
||||
username = localname
|
||||
else:
|
||||
username = f"{localname}@{DOMAIN}"
|
||||
# who do we think is trying to log in
|
||||
username = login_form.infer_username()
|
||||
password = login_form.data.get("password")
|
||||
|
||||
# perform authentication
|
||||
user = authenticate(request, username=username, password=password)
|
||||
|
||||
if user is not None:
|
||||
# if 2fa is set, don't log them in until they enter the right code
|
||||
if user.two_factor_auth:
|
||||
|
@ -76,14 +68,22 @@ class Login(View):
|
|||
|
||||
return set_language(user, redirect("/"))
|
||||
|
||||
# maybe the user is pending email confirmation
|
||||
if models.User.objects.filter(
|
||||
username=username, is_active=False, deactivation_reason="pending"
|
||||
).exists():
|
||||
user_attempt = models.User.objects.filter(
|
||||
username=username, is_active=False
|
||||
).first()
|
||||
if user_attempt and user_attempt.deactivation_reason == "pending":
|
||||
# maybe the user is pending email confirmation
|
||||
return redirect("confirm-email")
|
||||
if (
|
||||
user_attempt
|
||||
and user_attempt.allow_reactivation
|
||||
and user_attempt.check_password(password)
|
||||
):
|
||||
# maybe we want to reactivate an account?
|
||||
return redirect("prefs-reactivate")
|
||||
|
||||
# login errors
|
||||
login_form.non_field_errors = _("Username or password are incorrect")
|
||||
login_form.add_invalid_password_error()
|
||||
register_form = forms.RegisterForm()
|
||||
data = {"login_form": login_form, "register_form": register_form}
|
||||
return TemplateResponse(request, "landing/login.html", data)
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
""" edit your own account """
|
||||
from django.contrib.auth import logout
|
||||
import time
|
||||
|
||||
from django.contrib.auth import login, logout
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.shortcuts import redirect
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
|
@ -23,7 +25,7 @@ class DeleteUser(View):
|
|||
return TemplateResponse(request, "preferences/delete_user.html", data)
|
||||
|
||||
def post(self, request):
|
||||
"""les get fancy with images"""
|
||||
"""There's no going back from this"""
|
||||
form = forms.DeleteUserForm(request.POST, instance=request.user)
|
||||
# idk why but I couldn't get check_password to work on request.user
|
||||
user = models.User.objects.get(id=request.user.id)
|
||||
|
@ -36,3 +38,49 @@ class DeleteUser(View):
|
|||
form.errors["password"] = ["Invalid password"]
|
||||
data = {"form": form, "user": request.user}
|
||||
return TemplateResponse(request, "preferences/delete_user.html", data)
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class DeactivateUser(View):
|
||||
"""deactivate user view"""
|
||||
|
||||
def post(self, request):
|
||||
"""You can reactivate"""
|
||||
request.user.deactivate()
|
||||
logout(request)
|
||||
return redirect("/")
|
||||
|
||||
|
||||
class ReactivateUser(View):
|
||||
"""now reactivate the user"""
|
||||
|
||||
def get(self, request):
|
||||
"""so you want to rejoin?"""
|
||||
if request.user.is_authenticated:
|
||||
return redirect("/")
|
||||
data = {"login_form": forms.LoginForm()}
|
||||
return TemplateResponse(request, "landing/reactivate.html", data)
|
||||
|
||||
def post(self, request):
|
||||
"""reactivate that baby"""
|
||||
login_form = forms.LoginForm(request.POST)
|
||||
|
||||
username = login_form.infer_username()
|
||||
password = login_form.data.get("password")
|
||||
user = get_object_or_404(models.User, username=username)
|
||||
|
||||
# we can't use "authenticate" because that requires an active user
|
||||
if not user.check_password(password):
|
||||
login_form.add_invalid_password_error()
|
||||
data = {"login_form": login_form}
|
||||
return TemplateResponse(request, "landing/reactivate.html", data)
|
||||
|
||||
# Correct password, do you need 2fa too?
|
||||
if user.two_factor_auth:
|
||||
request.session["2fa_user"] = user.username
|
||||
request.session["2fa_auth_time"] = time.time()
|
||||
return redirect("login-with-2fa")
|
||||
|
||||
user.reactivate()
|
||||
login(request, user)
|
||||
return redirect("/")
|
||||
|
|
|
@ -133,6 +133,9 @@ class LoginWith2FA(View):
|
|||
request, "two_factor_auth/two_factor_login.html", data
|
||||
)
|
||||
|
||||
# is this a reactivate? let's go for it
|
||||
if not user.is_active and user.allow_reactivation:
|
||||
user.reactivate()
|
||||
# log the user in - we are bypassing standard login
|
||||
login(request, user)
|
||||
user.update_active_date()
|
||||
|
|
Loading…
Reference in a new issue