Files
GNX-mailEnterprise/venv/lib/python3.12/site-packages/allauth/mfa/views.py
Iliyan Angelov c67067a2a4 Mail
2025-09-14 23:24:25 +03:00

219 lines
7.6 KiB
Python

from django import forms
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse, reverse_lazy
from django.utils.decorators import method_decorator
from django.views.generic import TemplateView
from django.views.generic.edit import FormView
from allauth.account import app_settings as account_settings
from allauth.account.adapter import get_adapter as get_account_adapter
from allauth.account.decorators import reauthentication_required
from allauth.account.stages import LoginStageController
from allauth.mfa import app_settings, totp
from allauth.mfa.adapter import get_adapter
from allauth.mfa.forms import ActivateTOTPForm, AuthenticateForm
from allauth.mfa.models import Authenticator
from allauth.mfa.recovery_codes import RecoveryCodes
from allauth.mfa.stages import AuthenticateStage
from allauth.mfa.utils import is_mfa_enabled
class AuthenticateView(FormView):
form_class = AuthenticateForm
template_name = "mfa/authenticate." + account_settings.TEMPLATE_EXTENSION
def dispatch(self, request, *args, **kwargs):
self.stage = LoginStageController.enter(request, AuthenticateStage.key)
if not self.stage or not is_mfa_enabled(
self.stage.login.user, [Authenticator.Type.TOTP]
):
return HttpResponseRedirect(reverse("account_login"))
return super().dispatch(request, *args, **kwargs)
def get_form_kwargs(self):
ret = super().get_form_kwargs()
ret["user"] = self.stage.login.user
return ret
def form_valid(self, form):
return self.stage.exit()
authenticate = AuthenticateView.as_view()
@method_decorator(login_required, name="dispatch")
class IndexView(TemplateView):
template_name = "mfa/index." + account_settings.TEMPLATE_EXTENSION
def get_context_data(self, **kwargs):
ret = super().get_context_data(**kwargs)
authenticators = {
auth.type: auth.wrap()
for auth in Authenticator.objects.filter(user=self.request.user)
}
ret["authenticators"] = authenticators
return ret
index = IndexView.as_view()
@method_decorator(reauthentication_required, name="dispatch")
class ActivateTOTPView(FormView):
form_class = ActivateTOTPForm
template_name = "mfa/totp/activate_form." + account_settings.TEMPLATE_EXTENSION
success_url = reverse_lazy("mfa_view_recovery_codes")
def dispatch(self, request, *args, **kwargs):
if is_mfa_enabled(request.user, [Authenticator.Type.TOTP]):
return HttpResponseRedirect(reverse("mfa_deactivate_totp"))
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
ret = super().get_context_data(**kwargs)
adapter = get_adapter()
totp_url = totp.build_totp_url(
adapter.get_totp_label(self.request.user),
adapter.get_totp_issuer(),
ret["form"].secret,
)
totp_svg = totp.build_totp_svg(totp_url)
ret.update(
{
"totp_svg": totp_svg,
"totp_url": totp_url,
}
)
return ret
def get_form_kwargs(self):
ret = super().get_form_kwargs()
ret["user"] = self.request.user
return ret
def form_valid(self, form):
totp.TOTP.activate(self.request.user, form.secret)
RecoveryCodes.activate(self.request.user)
adapter = get_account_adapter(self.request)
adapter.add_message(
self.request, messages.SUCCESS, "mfa/messages/totp_activated.txt"
)
return super().form_valid(form)
activate_totp = ActivateTOTPView.as_view()
@method_decorator(login_required, name="dispatch")
class DeactivateTOTPView(FormView):
form_class = forms.Form
template_name = "mfa/totp/deactivate_form." + account_settings.TEMPLATE_EXTENSION
success_url = reverse_lazy("mfa_index")
def dispatch(self, request, *args, **kwargs):
self.authenticator = get_object_or_404(
Authenticator,
user=self.request.user,
type=Authenticator.Type.TOTP,
)
if not is_mfa_enabled(request.user, [Authenticator.Type.TOTP]):
return HttpResponseRedirect(reverse("mfa_activate_totp"))
return self._dispatch(request, *args, **kwargs)
@method_decorator(reauthentication_required)
def _dispatch(self, request, *args, **kwargs):
"""There's no point to reauthenticate when MFA is not enabled, so the
`is_mfa_enabled` chheck needs to go first, which is why we cannot slap a
`reauthentication_required` decorator on the `dispatch` directly.
"""
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
self.authenticator.wrap().deactivate()
adapter = get_account_adapter(self.request)
adapter.add_message(
self.request, messages.SUCCESS, "mfa/messages/totp_deactivated.txt"
)
return super().form_valid(form)
deactivate_totp = DeactivateTOTPView.as_view()
@method_decorator(reauthentication_required, name="dispatch")
class GenerateRecoveryCodesView(FormView):
form_class = forms.Form
template_name = "mfa/recovery_codes/generate." + account_settings.TEMPLATE_EXTENSION
success_url = reverse_lazy("mfa_view_recovery_codes")
def form_valid(self, form):
Authenticator.objects.filter(
user=self.request.user, type=Authenticator.Type.RECOVERY_CODES
).delete()
RecoveryCodes.activate(self.request.user)
adapter = get_account_adapter(self.request)
adapter.add_message(
self.request, messages.SUCCESS, "mfa/messages/recovery_codes_generated.txt"
)
return super().form_valid(form)
generate_recovery_codes = GenerateRecoveryCodesView.as_view()
@method_decorator(reauthentication_required, name="dispatch")
class DownloadRecoveryCodesView(TemplateView):
template_name = "mfa/recovery_codes/download.txt"
content_type = "text/plain"
def dispatch(self, request, *args, **kwargs):
self.authenticator = get_object_or_404(
Authenticator,
user=self.request.user,
type=Authenticator.Type.RECOVERY_CODES,
)
self.unused_codes = self.authenticator.wrap().get_unused_codes()
if not self.unused_codes:
return Http404()
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
ret = super().get_context_data(**kwargs)
ret["unused_codes"] = self.unused_codes
return ret
def render_to_response(self, context, **response_kwargs):
response = super().render_to_response(context, **response_kwargs)
response["Content-Disposition"] = 'attachment; filename="recovery-codes.txt"'
return response
download_recovery_codes = DownloadRecoveryCodesView.as_view()
@method_decorator(reauthentication_required, name="dispatch")
class ViewRecoveryCodesView(TemplateView):
template_name = "mfa/recovery_codes/index." + account_settings.TEMPLATE_EXTENSION
def get_context_data(self, **kwargs):
ret = super().get_context_data(**kwargs)
authenticator = get_object_or_404(
Authenticator,
user=self.request.user,
type=Authenticator.Type.RECOVERY_CODES,
)
ret.update(
{
"unused_codes": authenticator.wrap().get_unused_codes(),
"total_count": app_settings.RECOVERY_CODE_COUNT,
}
)
return ret
view_recovery_codes = ViewRecoveryCodesView.as_view()