"""Django Account Authentication views."""
import traceback
from typing import Union
from django import forms
from django.contrib.auth import authenticate, login
from django.http import HttpResponse, HttpResponseRedirect
from smarter.apps.account.models import User, get_resolved_user
from smarter.common.helpers.email_helpers import email_helper
from smarter.lib import logging
from smarter.lib.django.http.shortcuts import (
SmarterHttpResponseBadRequest,
SmarterHttpResponseForbidden,
SmarterHttpResponseNotFound,
)
from smarter.lib.django.shortcuts import reverse
from smarter.lib.django.token_generators import (
ExpiringTokenGenerator,
SmarterTokenConversionError,
SmarterTokenExpiredError,
SmarterTokenIntegrityError,
SmarterTokenParseError,
)
from smarter.lib.django.views import (
SmarterAuthenticatedNeverCachedWebView,
SmarterNeverCachedWebView,
redirect_and_expire_cache,
)
from smarter.lib.django.waffle import SmarterWaffleSwitches
logger = logging.getSmarterLogger(
__name__, any_switches=[SmarterWaffleSwitches.ACCOUNT_LOGGING, SmarterWaffleSwitches.VIEW_LOGGING]
)
# ------------------------------------------------------------------------------
# Public Access Views
# ------------------------------------------------------------------------------
[docs]
class AccountInactiveView(SmarterNeverCachedWebView):
"""View for inactive account page."""
template_path = "account/account-inactive.html"
[docs]
def get(self, request, *args, **kwargs) -> HttpResponse:
logger.debug(
"%s.AccountInactiveView.get() called with request type: %s %s, args: %s, kwargs: %s",
self.formatted_class_name,
type(request),
request,
args,
kwargs,
)
return self.clean_http_response(request, template_path=self.template_path)
[docs]
class AccountRegisterView(SmarterNeverCachedWebView):
"""View for signing up."""
template_path = "account/authentication/sign-up.html"
[docs]
def get(self, request, *args, **kwargs) -> Union[HttpResponseRedirect, HttpResponse]:
logger.debug(
"%s.AccountRegisterView.get() called with request type: %s %s, args: %s, kwargs: %s",
self.formatted_class_name,
type(request),
request,
args,
kwargs,
)
user = get_resolved_user(request.user)
if user and hasattr(user, "is_authenticated") and user.is_authenticated:
return redirect_and_expire_cache(path="/")
form = AccountRegisterView.SignUpForm()
context = {"form": form}
return self.clean_http_response(request, template_path=self.template_path, context=context)
[docs]
def post(self, request, *args, **kwargs) -> Union[HttpResponseRedirect, HttpResponse]:
logger.debug(
"%s.AccountRegisterView.post() called with request type: %s %s, args: %s, kwargs: %s",
self.formatted_class_name,
type(request),
request,
args,
kwargs,
)
form = AccountRegisterView.SignUpForm(request.POST)
if form.is_valid():
username = form.cleaned_data["email"]
email = form.cleaned_data["email"]
password = form.cleaned_data["password"]
User.objects.create_user(username, password=password, email=email)
authenticated_user = authenticate(request, username=username, password=password)
if authenticated_user is not None:
login(request, authenticated_user)
return redirect_and_expire_cache(path="/welcome/")
else:
# pylint: disable=broad-exception-raised
raise Exception(
f"{self.formatted_class_name}.post() Authentication failed immediately after registration. This is a bug."
)
return self.get(request=request)
[docs]
class AccountActivationEmailView(SmarterAuthenticatedNeverCachedWebView):
"""View for activating an account via an email with a single-use activation link."""
template_path = "account/activation.html"
email_template_path = "account/authentication/email/account-activation.html"
expiring_token = ExpiringTokenGenerator()
@property
def formatted_class_name(self) -> str:
"""Returns a formatted string of the class name for logging purposes."""
class_name = f"{__name__}.{AccountActivationEmailView.__name__}[{id(self)}]"
return self.formatted_text(class_name)
[docs]
def get(self, request, *args, **kwargs) -> HttpResponse:
logger.debug(
"%s.AccountActivationEmailView.get() called with request type: %s %s, args: %s, kwargs: %s",
self.formatted_class_name,
type(request),
request,
args,
kwargs,
)
# generate and send the activation email
user = get_resolved_user(request.user)
if not isinstance(user, User) or not hasattr(user, "is_authenticated") or not user.is_authenticated:
logger.warning(
"%s.AccountActivationEmailView.get() user is not authenticated or not found: %s",
self.formatted_class_name,
user,
)
return SmarterHttpResponseNotFound(
request=request, error_message="User not found. Please log in to activate your account."
)
# pylint: disable=C0415
from smarter.apps.account.urls import AccountReverseNames
url = self.expiring_token.encode_link(
request, user, ":".join([AccountReverseNames.namespace, AccountReverseNames.ACCOUNT_ACTIVATE])
)
context = {
"account_activation": {
"url": url,
}
}
body = self.render_clean_html(request, template_path=self.email_template_path, context=context)
subject = "Activate your account."
to = user.email
logger.debug(
"%s.AccountActivationEmailView.get() sending account activation email to %s with url: %s",
self.formatted_class_name,
to,
url,
)
email_helper.send_email(subject=subject, body=body, to=to, html=True)
# render a page to let the user know the email was sent. Add a link to resend the email.
email_resend_url = reverse(AccountReverseNames.namespace, AccountReverseNames.ACCOUNT_ACTIVATION)
context = {"account_activation": {"resend": email_resend_url}}
return self.clean_http_response(request, template_path=self.template_path, context=context)
[docs]
class AccountActivateView(SmarterNeverCachedWebView):
"""View for welcoming a newly activated user to the platform."""
template_path = "account/welcome.html"
expiring_token = ExpiringTokenGenerator()
@property
def formatted_class_name(self) -> str:
"""Returns a formatted string of the class name for logging purposes."""
class_name = f"{__name__}.{AccountActivateView.__name__}[{id(self)}]"
return logging.formatted_text(class_name)
[docs]
def get(self, request, *args, **kwargs):
logger.debug(
"%s.AccountActivateView.get() called with request type: %s %s, args: %s, kwargs: %s",
self.formatted_class_name,
type(request),
request,
args,
kwargs,
)
uidb64 = kwargs.get("uidb64", None)
token = kwargs.get("token", None)
try:
user = self.expiring_token.decode_link(uidb64, token)
user.is_active = True
user.save()
except User.DoesNotExist:
logger.debug(
"%s.AccountActivateView.get() invalid password reset link. User does not exist.",
self.formatted_class_name,
)
return SmarterHttpResponseNotFound(
request=request, error_message="Invalid password reset link. User does not exist."
)
except (
TypeError,
ValueError,
OverflowError,
SmarterTokenParseError,
SmarterTokenConversionError,
SmarterTokenIntegrityError,
) as e:
logger.error(
"%s.AccountActivateView.get() bad token error: %s\n%s",
self.formatted_class_name,
str(e),
traceback.format_exc(),
)
return SmarterHttpResponseBadRequest(request=request, error_message=str(e))
except SmarterTokenExpiredError as e:
logger.debug(
"%s.AccountActivateView.get() expired token error: %s",
self.formatted_class_name,
str(e),
)
return SmarterHttpResponseForbidden(request=request, error_message=str(e))
return self.clean_http_response(request, template_path=self.template_path)
# ------------------------------------------------------------------------------
# Private Access Views
# ------------------------------------------------------------------------------
[docs]
class AccountDeactivateView(SmarterAuthenticatedNeverCachedWebView):
"""View for the account deactivation page."""
template_path = "account/account-deactivated.html"
@property
def formatted_class_name(self) -> str:
"""Returns a formatted string of the class name for logging purposes."""
class_name = f"{__name__}.{AccountDeactivateView.__name__}[{id(self)}]"
return self.formatted_text(class_name)