276 lines
8.9 KiB
Python
276 lines
8.9 KiB
Python
import datetime
|
|
|
|
from django.contrib.auth import get_user_model
|
|
from django.core import signing
|
|
from django.db import models
|
|
from django.db.models import Index, Q
|
|
from django.db.models.constraints import UniqueConstraint
|
|
from django.db.models.functions import Upper
|
|
from django.utils import timezone
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
from .. import app_settings as allauth_app_settings
|
|
from . import app_settings, signals
|
|
from .adapter import get_adapter
|
|
from .managers import EmailAddressManager, EmailConfirmationManager
|
|
|
|
|
|
class EmailAddress(models.Model):
|
|
user = models.ForeignKey(
|
|
allauth_app_settings.USER_MODEL,
|
|
verbose_name=_("user"),
|
|
on_delete=models.CASCADE,
|
|
)
|
|
email = models.EmailField(
|
|
max_length=app_settings.EMAIL_MAX_LENGTH,
|
|
verbose_name=_("email address"),
|
|
)
|
|
verified = models.BooleanField(verbose_name=_("verified"), default=False)
|
|
primary = models.BooleanField(verbose_name=_("primary"), default=False)
|
|
|
|
objects = EmailAddressManager()
|
|
|
|
class Meta:
|
|
verbose_name = _("email address")
|
|
verbose_name_plural = _("email addresses")
|
|
unique_together = [("user", "email")]
|
|
if app_settings.UNIQUE_EMAIL:
|
|
constraints = [
|
|
UniqueConstraint(
|
|
fields=["email"],
|
|
name="unique_verified_email",
|
|
condition=Q(verified=True),
|
|
)
|
|
]
|
|
indexes = [Index(Upper("email"), name="account_emailaddress_upper")]
|
|
|
|
def __str__(self):
|
|
return self.email
|
|
|
|
def can_set_verified(self):
|
|
if self.verified:
|
|
return True
|
|
conflict = False
|
|
if app_settings.UNIQUE_EMAIL:
|
|
conflict = (
|
|
EmailAddress.objects.exclude(pk=self.pk)
|
|
.filter(verified=True, email__iexact=self.email)
|
|
.exists()
|
|
)
|
|
return not conflict
|
|
|
|
def set_verified(self, commit=True):
|
|
if self.verified:
|
|
return True
|
|
if self.can_set_verified():
|
|
self.verified = True
|
|
if commit:
|
|
self.save(update_fields=["verified"])
|
|
return self.verified
|
|
|
|
def set_as_primary(self, conditional=False):
|
|
"""Marks the email address as primary. In case of `conditional`, it is
|
|
only marked as primary if there is no other primary email address set.
|
|
"""
|
|
from allauth.account.utils import user_email
|
|
|
|
old_primary = EmailAddress.objects.get_primary(self.user)
|
|
if old_primary:
|
|
if conditional:
|
|
return False
|
|
old_primary.primary = False
|
|
old_primary.save()
|
|
self.primary = True
|
|
self.save()
|
|
user_email(self.user, self.email, commit=True)
|
|
return True
|
|
|
|
def send_confirmation(self, request=None, signup=False):
|
|
if app_settings.EMAIL_CONFIRMATION_HMAC:
|
|
confirmation = EmailConfirmationHMAC(self)
|
|
else:
|
|
confirmation = EmailConfirmation.create(self)
|
|
confirmation.send(request, signup=signup)
|
|
return confirmation
|
|
|
|
def remove(self):
|
|
from allauth.account.utils import user_email
|
|
|
|
self.delete()
|
|
if user_email(self.user) == self.email:
|
|
alt = (
|
|
EmailAddress.objects.filter(user=self.user)
|
|
.order_by("-verified")
|
|
.first()
|
|
)
|
|
alt_email = ""
|
|
if alt:
|
|
alt_email = alt.email
|
|
user_email(self.user, alt_email, commit=True)
|
|
|
|
|
|
class EmailConfirmationMixin:
|
|
def confirm(self, request):
|
|
email_address = self.email_address
|
|
if not email_address.verified:
|
|
confirmed = get_adapter().confirm_email(request, email_address)
|
|
if confirmed:
|
|
signals.email_confirmed.send(
|
|
sender=self.__class__,
|
|
request=request,
|
|
email_address=email_address,
|
|
)
|
|
return email_address
|
|
|
|
def send(self, request=None, signup=False):
|
|
get_adapter().send_confirmation_mail(request, self, signup)
|
|
signals.email_confirmation_sent.send(
|
|
sender=self.__class__,
|
|
request=request,
|
|
confirmation=self,
|
|
signup=signup,
|
|
)
|
|
|
|
|
|
class EmailConfirmation(EmailConfirmationMixin, models.Model):
|
|
email_address = models.ForeignKey(
|
|
EmailAddress,
|
|
verbose_name=_("email address"),
|
|
on_delete=models.CASCADE,
|
|
)
|
|
created = models.DateTimeField(verbose_name=_("created"), default=timezone.now)
|
|
sent = models.DateTimeField(verbose_name=_("sent"), null=True)
|
|
key = models.CharField(verbose_name=_("key"), max_length=64, unique=True)
|
|
|
|
objects = EmailConfirmationManager()
|
|
|
|
class Meta:
|
|
verbose_name = _("email confirmation")
|
|
verbose_name_plural = _("email confirmations")
|
|
|
|
def __str__(self):
|
|
return "confirmation for %s" % self.email_address
|
|
|
|
@classmethod
|
|
def create(cls, email_address):
|
|
key = get_adapter().generate_emailconfirmation_key(email_address.email)
|
|
return cls._default_manager.create(email_address=email_address, key=key)
|
|
|
|
def key_expired(self):
|
|
expiration_date = self.sent + datetime.timedelta(
|
|
days=app_settings.EMAIL_CONFIRMATION_EXPIRE_DAYS
|
|
)
|
|
return expiration_date <= timezone.now()
|
|
|
|
key_expired.boolean = True
|
|
|
|
def confirm(self, request):
|
|
if not self.key_expired():
|
|
return super().confirm(request)
|
|
|
|
def send(self, request=None, signup=False):
|
|
super().send(request=request, signup=signup)
|
|
self.sent = timezone.now()
|
|
self.save()
|
|
|
|
|
|
class EmailConfirmationHMAC(EmailConfirmationMixin, object):
|
|
def __init__(self, email_address):
|
|
self.email_address = email_address
|
|
|
|
@property
|
|
def key(self):
|
|
return signing.dumps(obj=self.email_address.pk, salt=app_settings.SALT)
|
|
|
|
@classmethod
|
|
def from_key(cls, key):
|
|
try:
|
|
max_age = 60 * 60 * 24 * app_settings.EMAIL_CONFIRMATION_EXPIRE_DAYS
|
|
pk = signing.loads(key, max_age=max_age, salt=app_settings.SALT)
|
|
ret = EmailConfirmationHMAC(EmailAddress.objects.get(pk=pk, verified=False))
|
|
except (
|
|
signing.SignatureExpired,
|
|
signing.BadSignature,
|
|
EmailAddress.DoesNotExist,
|
|
):
|
|
ret = None
|
|
return ret
|
|
|
|
|
|
class Login:
|
|
"""
|
|
Represents a user that is in the process of logging in.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
user,
|
|
email_verification,
|
|
redirect_url=None,
|
|
signal_kwargs=None,
|
|
signup=False,
|
|
email=None,
|
|
state=None,
|
|
):
|
|
self.user = user
|
|
self.email_verification = email_verification
|
|
self.redirect_url = redirect_url
|
|
self.signal_kwargs = signal_kwargs
|
|
self.signup = signup
|
|
self.email = email
|
|
self.state = {} if state is None else state
|
|
|
|
def serialize(self):
|
|
from allauth.account.utils import user_pk_to_url_str
|
|
|
|
# :-( Knowledge of the `socialaccount` is entering the `account` app.
|
|
signal_kwargs = self.signal_kwargs
|
|
if signal_kwargs is not None:
|
|
sociallogin = signal_kwargs.get("sociallogin")
|
|
if sociallogin is not None:
|
|
signal_kwargs = signal_kwargs.copy()
|
|
signal_kwargs["sociallogin"] = sociallogin.serialize()
|
|
|
|
data = {
|
|
"user_pk": user_pk_to_url_str(self.user),
|
|
"email_verification": self.email_verification,
|
|
"signup": self.signup,
|
|
"redirect_url": self.redirect_url,
|
|
"email": self.email,
|
|
"signal_kwargs": signal_kwargs,
|
|
"state": self.state,
|
|
}
|
|
return data
|
|
|
|
@classmethod
|
|
def deserialize(cls, data):
|
|
from allauth.account.utils import url_str_to_user_pk
|
|
from allauth.socialaccount.models import SocialLogin
|
|
|
|
user = (
|
|
get_user_model()
|
|
.objects.filter(pk=url_str_to_user_pk(data["user_pk"]))
|
|
.first()
|
|
)
|
|
if user is None:
|
|
raise ValueError()
|
|
try:
|
|
# :-( Knowledge of the `socialaccount` is entering the `account` app.
|
|
signal_kwargs = data["signal_kwargs"]
|
|
if signal_kwargs is not None:
|
|
sociallogin = signal_kwargs.get("sociallogin")
|
|
if sociallogin is not None:
|
|
signal_kwargs = signal_kwargs.copy()
|
|
signal_kwargs["sociallogin"] = SocialLogin.deserialize(sociallogin)
|
|
|
|
return Login(
|
|
user=user,
|
|
email_verification=data["email_verification"],
|
|
redirect_url=data["redirect_url"],
|
|
signup=data["signup"],
|
|
signal_kwargs=signal_kwargs,
|
|
state=data["state"],
|
|
)
|
|
except KeyError:
|
|
raise ValueError()
|