mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 07:11:08 -05:00
first commit
This commit is contained in:
20
venv/lib/python3.11/site-packages/allauth/__init__.py
Normal file
20
venv/lib/python3.11/site-packages/allauth/__init__.py
Normal file
@@ -0,0 +1,20 @@
|
||||
r"""
|
||||
_ ___ __ __ .___________. __ __
|
||||
/\| |/\ / \ | | | | | || | | |
|
||||
\ ` ' / / ^ \ | | | | `---| |----`| |__| |
|
||||
|_ _| / /_\ \ | | | | | | | __ |
|
||||
/ , . \ / _____ \ | `--' | | | | | | |
|
||||
\/|_|\//__/ \__\ \______/ |__| |__| |__|
|
||||
|
||||
"""
|
||||
|
||||
VERSION = (65, 1, 0, "final", 0)
|
||||
|
||||
__title__ = "django-allauth"
|
||||
__version_info__ = VERSION
|
||||
__version__ = ".".join(map(str, VERSION[:3])) + (
|
||||
"-{}{}".format(VERSION[3], VERSION[4] or "") if VERSION[3] != "final" else ""
|
||||
)
|
||||
__author__ = "Raymond Penners"
|
||||
__license__ = "MIT"
|
||||
__copyright__ = "Copyright 2010-2024 Raymond Penners and contributors"
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
826
venv/lib/python3.11/site-packages/allauth/account/adapter.py
Normal file
826
venv/lib/python3.11/site-packages/allauth/account/adapter.py
Normal file
@@ -0,0 +1,826 @@
|
||||
import html
|
||||
import json
|
||||
import string
|
||||
import warnings
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import (
|
||||
authenticate,
|
||||
get_backends,
|
||||
get_user_model,
|
||||
login as django_login,
|
||||
logout as django_logout,
|
||||
)
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.contrib.auth.password_validation import (
|
||||
MinimumLengthValidator,
|
||||
validate_password,
|
||||
)
|
||||
from django.contrib.sites.shortcuts import get_current_site
|
||||
from django.core.exceptions import FieldDoesNotExist
|
||||
from django.core.mail import EmailMessage, EmailMultiAlternatives
|
||||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
from django.shortcuts import resolve_url
|
||||
from django.template import TemplateDoesNotExist
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from allauth import app_settings as allauth_app_settings
|
||||
from allauth.account import app_settings, signals
|
||||
from allauth.core import context, ratelimit
|
||||
from allauth.core.internal.adapter import BaseAdapter
|
||||
from allauth.core.internal.httpkit import headed_redirect_response
|
||||
from allauth.utils import generate_unique_username, import_attribute
|
||||
|
||||
|
||||
class DefaultAccountAdapter(BaseAdapter):
|
||||
"""The adapter class allows you to override various functionality of the
|
||||
``allauth.account`` app. To do so, point ``settings.ACCOUNT_ADAPTER`` to
|
||||
your own class that derives from ``DefaultAccountAdapter`` and override the
|
||||
behavior by altering the implementation of the methods according to your own
|
||||
needs.
|
||||
"""
|
||||
|
||||
error_messages = {
|
||||
"account_inactive": _("This account is currently inactive."),
|
||||
"cannot_remove_primary_email": _(
|
||||
"You cannot remove your primary email address."
|
||||
),
|
||||
"duplicate_email": _(
|
||||
"This email address is already associated with this account."
|
||||
),
|
||||
"email_password_mismatch": _(
|
||||
"The email address and/or password you specified are not correct."
|
||||
),
|
||||
"email_taken": _("A user is already registered with this email address."),
|
||||
"enter_current_password": _("Please type your current password."),
|
||||
"incorrect_code": _("Incorrect code."),
|
||||
"incorrect_password": _("Incorrect password."),
|
||||
"invalid_or_expired_key": _("Invalid or expired key."),
|
||||
"invalid_password_reset": _("The password reset token was invalid."),
|
||||
"max_email_addresses": _("You cannot add more than %d email addresses."),
|
||||
"too_many_login_attempts": _(
|
||||
"Too many failed login attempts. Try again later."
|
||||
),
|
||||
"unknown_email": _("The email address is not assigned to any user account"),
|
||||
"unverified_primary_email": _("Your primary email address must be verified."),
|
||||
"username_blacklisted": _(
|
||||
"Username can not be used. Please use other username."
|
||||
),
|
||||
"username_password_mismatch": _(
|
||||
"The username and/or password you specified are not correct."
|
||||
),
|
||||
"username_taken": AbstractUser._meta.get_field("username").error_messages[
|
||||
"unique"
|
||||
],
|
||||
}
|
||||
|
||||
def stash_verified_email(self, request, email):
|
||||
request.session["account_verified_email"] = email
|
||||
|
||||
def unstash_verified_email(self, request):
|
||||
ret = request.session.get("account_verified_email")
|
||||
request.session["account_verified_email"] = None
|
||||
return ret
|
||||
|
||||
def is_email_verified(self, request, email):
|
||||
"""
|
||||
Checks whether or not the email address is already verified
|
||||
beyond allauth scope, for example, by having accepted an
|
||||
invitation before signing up.
|
||||
"""
|
||||
ret = False
|
||||
verified_email = request.session.get("account_verified_email")
|
||||
if verified_email:
|
||||
ret = verified_email.lower() == email.lower()
|
||||
return ret
|
||||
|
||||
def can_delete_email(self, email_address) -> bool:
|
||||
"""
|
||||
Returns whether or not the given email address can be deleted.
|
||||
"""
|
||||
from allauth.account.models import EmailAddress
|
||||
|
||||
has_other = (
|
||||
EmailAddress.objects.filter(user_id=email_address.user_id)
|
||||
.exclude(pk=email_address.pk)
|
||||
.exists()
|
||||
)
|
||||
login_by_email = (
|
||||
app_settings.AUTHENTICATION_METHOD
|
||||
== app_settings.AuthenticationMethod.EMAIL
|
||||
)
|
||||
if email_address.primary:
|
||||
if has_other:
|
||||
# Don't allow, let the user mark one of the others as primary
|
||||
# first.
|
||||
return False
|
||||
elif login_by_email:
|
||||
# Last email & login is by email, prevent dangling account.
|
||||
return False
|
||||
return True
|
||||
elif has_other:
|
||||
# Account won't be dangling.
|
||||
return True
|
||||
elif login_by_email:
|
||||
# This is the last email.
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def format_email_subject(self, subject) -> str:
|
||||
"""
|
||||
Formats the given email subject.
|
||||
"""
|
||||
prefix = app_settings.EMAIL_SUBJECT_PREFIX
|
||||
if prefix is None:
|
||||
site = get_current_site(context.request)
|
||||
prefix = "[{name}] ".format(name=site.name)
|
||||
return prefix + force_str(subject)
|
||||
|
||||
def get_from_email(self):
|
||||
"""
|
||||
This is a hook that can be overridden to programmatically
|
||||
set the 'from' email address for sending emails
|
||||
"""
|
||||
return settings.DEFAULT_FROM_EMAIL
|
||||
|
||||
def render_mail(self, template_prefix, email, context, headers=None):
|
||||
"""
|
||||
Renders an email to `email`. `template_prefix` identifies the
|
||||
email that is to be sent, e.g. "account/email/email_confirmation"
|
||||
"""
|
||||
to = [email] if isinstance(email, str) else email
|
||||
subject = render_to_string("{0}_subject.txt".format(template_prefix), context)
|
||||
# remove superfluous line breaks
|
||||
subject = " ".join(subject.splitlines()).strip()
|
||||
subject = self.format_email_subject(subject)
|
||||
|
||||
from_email = self.get_from_email()
|
||||
|
||||
bodies = {}
|
||||
html_ext = app_settings.TEMPLATE_EXTENSION
|
||||
for ext in [html_ext, "txt"]:
|
||||
try:
|
||||
template_name = "{0}_message.{1}".format(template_prefix, ext)
|
||||
bodies[ext] = render_to_string(
|
||||
template_name,
|
||||
context,
|
||||
globals()["context"].request,
|
||||
).strip()
|
||||
except TemplateDoesNotExist:
|
||||
if ext == "txt" and not bodies:
|
||||
# We need at least one body
|
||||
raise
|
||||
if "txt" in bodies:
|
||||
msg = EmailMultiAlternatives(
|
||||
subject, bodies["txt"], from_email, to, headers=headers
|
||||
)
|
||||
if html_ext in bodies:
|
||||
msg.attach_alternative(bodies[html_ext], "text/html")
|
||||
else:
|
||||
msg = EmailMessage(
|
||||
subject, bodies[html_ext], from_email, to, headers=headers
|
||||
)
|
||||
msg.content_subtype = "html" # Main content is now text/html
|
||||
return msg
|
||||
|
||||
def send_mail(self, template_prefix, email, context):
|
||||
ctx = {
|
||||
"email": email,
|
||||
"current_site": get_current_site(globals()["context"].request),
|
||||
}
|
||||
ctx.update(context)
|
||||
msg = self.render_mail(template_prefix, email, ctx)
|
||||
msg.send()
|
||||
|
||||
def get_signup_redirect_url(self, request):
|
||||
return resolve_url(app_settings.SIGNUP_REDIRECT_URL)
|
||||
|
||||
def get_login_redirect_url(self, request):
|
||||
"""
|
||||
Returns the default URL to redirect to after logging in. Note
|
||||
that URLs passed explicitly (e.g. by passing along a `next`
|
||||
GET parameter) take precedence over the value returned here.
|
||||
"""
|
||||
assert request.user.is_authenticated
|
||||
url = getattr(settings, "LOGIN_REDIRECT_URLNAME", None)
|
||||
if url:
|
||||
warnings.warn(
|
||||
"LOGIN_REDIRECT_URLNAME is deprecated, simply"
|
||||
" use LOGIN_REDIRECT_URL with a URL name",
|
||||
DeprecationWarning,
|
||||
)
|
||||
else:
|
||||
url = settings.LOGIN_REDIRECT_URL
|
||||
return resolve_url(url)
|
||||
|
||||
def get_logout_redirect_url(self, request):
|
||||
"""
|
||||
Returns the URL to redirect to after the user logs out. Note that
|
||||
this method is also invoked if you attempt to log out while no users
|
||||
is logged in. Therefore, request.user is not guaranteed to be an
|
||||
authenticated user.
|
||||
"""
|
||||
return resolve_url(app_settings.LOGOUT_REDIRECT_URL)
|
||||
|
||||
def get_email_verification_redirect_url(self, email_address):
|
||||
"""
|
||||
The URL to return to after email verification.
|
||||
"""
|
||||
get_url = getattr(self, "get_email_confirmation_redirect_url", None)
|
||||
if get_url:
|
||||
# Deprecated.
|
||||
return get_url(self.request)
|
||||
|
||||
if self.request.user.is_authenticated:
|
||||
if app_settings.EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL:
|
||||
return app_settings.EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL
|
||||
else:
|
||||
return self.get_login_redirect_url(self.request)
|
||||
else:
|
||||
return app_settings.EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL
|
||||
|
||||
def get_password_change_redirect_url(self, request):
|
||||
"""
|
||||
The URL to redirect to after a successful password change/set.
|
||||
|
||||
NOTE: Not called during the password reset flow.
|
||||
"""
|
||||
return reverse("account_change_password")
|
||||
|
||||
def is_open_for_signup(self, request):
|
||||
"""
|
||||
Checks whether or not the site is open for signups.
|
||||
|
||||
Next to simply returning True/False you can also intervene the
|
||||
regular flow by raising an ImmediateHttpResponse
|
||||
"""
|
||||
return True
|
||||
|
||||
def new_user(self, request):
|
||||
"""
|
||||
Instantiates a new User instance.
|
||||
"""
|
||||
user = get_user_model()()
|
||||
return user
|
||||
|
||||
def populate_username(self, request, user):
|
||||
"""
|
||||
Fills in a valid username, if required and missing. If the
|
||||
username is already present it is assumed to be valid
|
||||
(unique).
|
||||
"""
|
||||
from .utils import user_email, user_field, user_username
|
||||
|
||||
first_name = user_field(user, "first_name")
|
||||
last_name = user_field(user, "last_name")
|
||||
email = user_email(user)
|
||||
username = user_username(user)
|
||||
if app_settings.USER_MODEL_USERNAME_FIELD:
|
||||
user_username(
|
||||
user,
|
||||
username
|
||||
or self.generate_unique_username(
|
||||
[first_name, last_name, email, username, "user"]
|
||||
),
|
||||
)
|
||||
|
||||
def generate_unique_username(self, txts, regex=None):
|
||||
return generate_unique_username(txts, regex)
|
||||
|
||||
def save_user(self, request, user, form, commit=True):
|
||||
"""
|
||||
Saves a new `User` instance using information provided in the
|
||||
signup form.
|
||||
"""
|
||||
from .utils import user_email, user_field, user_username
|
||||
|
||||
data = form.cleaned_data
|
||||
first_name = data.get("first_name")
|
||||
last_name = data.get("last_name")
|
||||
email = data.get("email")
|
||||
username = data.get("username")
|
||||
user_email(user, email)
|
||||
user_username(user, username)
|
||||
if first_name:
|
||||
user_field(user, "first_name", first_name)
|
||||
if last_name:
|
||||
user_field(user, "last_name", last_name)
|
||||
if "password1" in data:
|
||||
user.set_password(data["password1"])
|
||||
elif "password" in data:
|
||||
user.set_password(data["password"])
|
||||
else:
|
||||
user.set_unusable_password()
|
||||
self.populate_username(request, user)
|
||||
if commit:
|
||||
# Ability not to commit makes it easier to derive from
|
||||
# this adapter by adding
|
||||
user.save()
|
||||
return user
|
||||
|
||||
def clean_username(self, username, shallow=False):
|
||||
"""
|
||||
Validates the username. You can hook into this if you want to
|
||||
(dynamically) restrict what usernames can be chosen.
|
||||
"""
|
||||
for validator in app_settings.USERNAME_VALIDATORS:
|
||||
validator(username)
|
||||
|
||||
# TODO: Add regexp support to USERNAME_BLACKLIST
|
||||
username_blacklist_lower = [
|
||||
ub.lower() for ub in app_settings.USERNAME_BLACKLIST
|
||||
]
|
||||
if username.lower() in username_blacklist_lower:
|
||||
raise self.validation_error("username_blacklisted")
|
||||
# Skipping database lookups when shallow is True, needed for unique
|
||||
# username generation.
|
||||
if not shallow:
|
||||
from .utils import filter_users_by_username
|
||||
|
||||
if filter_users_by_username(username).exists():
|
||||
raise self.validation_error("username_taken")
|
||||
return username
|
||||
|
||||
def clean_email(self, email):
|
||||
"""
|
||||
Validates an email value. You can hook into this if you want to
|
||||
(dynamically) restrict what email addresses can be chosen.
|
||||
"""
|
||||
return email
|
||||
|
||||
def clean_password(self, password, user=None):
|
||||
"""
|
||||
Validates a password. You can hook into this if you want to
|
||||
restric the allowed password choices.
|
||||
"""
|
||||
min_length = app_settings.PASSWORD_MIN_LENGTH
|
||||
if min_length:
|
||||
MinimumLengthValidator(min_length).validate(password)
|
||||
validate_password(password, user)
|
||||
return password
|
||||
|
||||
def validate_unique_email(self, email):
|
||||
return email
|
||||
|
||||
def add_message(
|
||||
self,
|
||||
request,
|
||||
level,
|
||||
message_template=None,
|
||||
message_context=None,
|
||||
extra_tags="",
|
||||
message=None,
|
||||
):
|
||||
"""
|
||||
Wrapper of `django.contrib.messages.add_message`, that reads
|
||||
the message text from a template.
|
||||
"""
|
||||
if getattr(getattr(request, "allauth", None), "headless", None):
|
||||
return
|
||||
if "django.contrib.messages" in settings.INSTALLED_APPS:
|
||||
if message:
|
||||
messages.add_message(request, level, message, extra_tags=extra_tags)
|
||||
return
|
||||
try:
|
||||
if message_context is None:
|
||||
message_context = {}
|
||||
escaped_message = render_to_string(
|
||||
message_template,
|
||||
message_context,
|
||||
context.request,
|
||||
).strip()
|
||||
if escaped_message:
|
||||
message = html.unescape(escaped_message)
|
||||
messages.add_message(request, level, message, extra_tags=extra_tags)
|
||||
except TemplateDoesNotExist:
|
||||
pass
|
||||
|
||||
def ajax_response(self, request, response, redirect_to=None, form=None, data=None):
|
||||
resp = {}
|
||||
status = response.status_code
|
||||
|
||||
if redirect_to:
|
||||
status = 200
|
||||
resp["location"] = redirect_to
|
||||
if form:
|
||||
if request.method == "POST":
|
||||
if form.is_valid():
|
||||
status = 200
|
||||
else:
|
||||
status = 400
|
||||
else:
|
||||
status = 200
|
||||
resp["form"] = self.ajax_response_form(form)
|
||||
if hasattr(response, "render"):
|
||||
response.render()
|
||||
resp["html"] = response.content.decode("utf8")
|
||||
if data is not None:
|
||||
resp["data"] = data
|
||||
return HttpResponse(
|
||||
json.dumps(resp), status=status, content_type="application/json"
|
||||
)
|
||||
|
||||
def ajax_response_form(self, form):
|
||||
form_spec = {
|
||||
"fields": {},
|
||||
"field_order": [],
|
||||
"errors": form.non_field_errors(),
|
||||
}
|
||||
for field in form:
|
||||
field_spec = {
|
||||
"label": force_str(field.label),
|
||||
"value": field.value(),
|
||||
"help_text": force_str(field.help_text),
|
||||
"errors": [force_str(e) for e in field.errors],
|
||||
"widget": {
|
||||
"attrs": {
|
||||
k: force_str(v) for k, v in field.field.widget.attrs.items()
|
||||
}
|
||||
},
|
||||
}
|
||||
form_spec["fields"][field.html_name] = field_spec
|
||||
form_spec["field_order"].append(field.html_name)
|
||||
return form_spec
|
||||
|
||||
def pre_login(
|
||||
self,
|
||||
request,
|
||||
user,
|
||||
*,
|
||||
email_verification,
|
||||
signal_kwargs,
|
||||
email,
|
||||
signup,
|
||||
redirect_url
|
||||
):
|
||||
if not user.is_active:
|
||||
return self.respond_user_inactive(request, user)
|
||||
|
||||
def post_login(
|
||||
self,
|
||||
request,
|
||||
user,
|
||||
*,
|
||||
email_verification,
|
||||
signal_kwargs,
|
||||
email,
|
||||
signup,
|
||||
redirect_url
|
||||
):
|
||||
from .utils import get_login_redirect_url
|
||||
|
||||
response = HttpResponseRedirect(
|
||||
get_login_redirect_url(request, redirect_url, signup=signup)
|
||||
)
|
||||
|
||||
if signal_kwargs is None:
|
||||
signal_kwargs = {}
|
||||
signals.user_logged_in.send(
|
||||
sender=user.__class__,
|
||||
request=request,
|
||||
response=response,
|
||||
user=user,
|
||||
**signal_kwargs,
|
||||
)
|
||||
self.add_message(
|
||||
request,
|
||||
messages.SUCCESS,
|
||||
"account/messages/logged_in.txt",
|
||||
{"user": user},
|
||||
)
|
||||
return response
|
||||
|
||||
def login(self, request, user):
|
||||
# HACK: This is not nice. The proper Django way is to use an
|
||||
# authentication backend
|
||||
if not hasattr(user, "backend"):
|
||||
from .auth_backends import AuthenticationBackend
|
||||
|
||||
backends = get_backends()
|
||||
backend = None
|
||||
for b in backends:
|
||||
if isinstance(b, AuthenticationBackend):
|
||||
# prefer our own backend
|
||||
backend = b
|
||||
break
|
||||
elif not backend and hasattr(b, "get_user"):
|
||||
# Pick the first valid one
|
||||
backend = b
|
||||
backend_path = ".".join([backend.__module__, backend.__class__.__name__])
|
||||
user.backend = backend_path
|
||||
django_login(request, user)
|
||||
|
||||
def logout(self, request):
|
||||
django_logout(request)
|
||||
|
||||
def confirm_email(self, request, email_address):
|
||||
"""
|
||||
Marks the email address as confirmed on the db
|
||||
"""
|
||||
from allauth.account.internal.flows import email_verification
|
||||
|
||||
return email_verification.verify_email(request, email_address)
|
||||
|
||||
def set_password(self, user, password) -> None:
|
||||
"""
|
||||
Sets the password for the user.
|
||||
"""
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
|
||||
def get_user_search_fields(self):
|
||||
ret = []
|
||||
User = get_user_model()
|
||||
candidates = [
|
||||
app_settings.USER_MODEL_USERNAME_FIELD,
|
||||
"first_name",
|
||||
"last_name",
|
||||
"email",
|
||||
]
|
||||
for candidate in candidates:
|
||||
try:
|
||||
User._meta.get_field(candidate)
|
||||
ret.append(candidate)
|
||||
except FieldDoesNotExist:
|
||||
pass
|
||||
return ret
|
||||
|
||||
def is_safe_url(self, url):
|
||||
from django.utils.http import url_has_allowed_host_and_scheme
|
||||
|
||||
# get_host already validates the given host, so no need to check it again
|
||||
allowed_hosts = {context.request.get_host()} | set(settings.ALLOWED_HOSTS)
|
||||
|
||||
if "*" in allowed_hosts:
|
||||
parsed_host = urlparse(url).netloc
|
||||
allowed_host = {parsed_host} if parsed_host else None
|
||||
return url_has_allowed_host_and_scheme(url, allowed_hosts=allowed_host)
|
||||
|
||||
return url_has_allowed_host_and_scheme(url, allowed_hosts=allowed_hosts)
|
||||
|
||||
def send_password_reset_mail(self, user, email, context):
|
||||
"""
|
||||
Method intended to be overridden in case you need to customize the logic
|
||||
used to determine whether a user is permitted to request a password reset.
|
||||
For example, if you are enforcing something like "social only" authentication
|
||||
in your app, you may want to intervene here by checking `user.has_usable_password`
|
||||
|
||||
"""
|
||||
return self.send_mail("account/email/password_reset_key", email, context)
|
||||
|
||||
def get_reset_password_from_key_url(self, key):
|
||||
"""
|
||||
Method intended to be overridden in case the password reset email
|
||||
needs to be adjusted.
|
||||
"""
|
||||
from allauth.account.internal import flows
|
||||
|
||||
return flows.password_reset.get_reset_password_from_key_url(self.request, key)
|
||||
|
||||
def get_email_confirmation_url(self, request, emailconfirmation):
|
||||
"""Constructs the email confirmation (activation) url.
|
||||
|
||||
Note that if you have architected your system such that email
|
||||
confirmations are sent outside of the request context `request`
|
||||
can be `None` here.
|
||||
"""
|
||||
from allauth.account.internal import flows
|
||||
|
||||
return flows.email_verification.get_email_verification_url(
|
||||
request, emailconfirmation
|
||||
)
|
||||
|
||||
def should_send_confirmation_mail(self, request, email_address, signup) -> bool:
|
||||
return True
|
||||
|
||||
def send_account_already_exists_mail(self, email):
|
||||
from allauth.account.internal import flows
|
||||
|
||||
signup_url = flows.signup.get_signup_url(context.request)
|
||||
password_reset_url = flows.password_reset.get_reset_password_url(
|
||||
context.request
|
||||
)
|
||||
ctx = {
|
||||
"request": context.request,
|
||||
"signup_url": signup_url,
|
||||
"password_reset_url": password_reset_url,
|
||||
}
|
||||
self.send_mail("account/email/account_already_exists", email, ctx)
|
||||
|
||||
def send_confirmation_mail(self, request, emailconfirmation, signup):
|
||||
ctx = {
|
||||
"user": emailconfirmation.email_address.user,
|
||||
}
|
||||
if app_settings.EMAIL_VERIFICATION_BY_CODE_ENABLED:
|
||||
ctx.update({"code": emailconfirmation.key})
|
||||
else:
|
||||
ctx.update(
|
||||
{
|
||||
"key": emailconfirmation.key,
|
||||
"activate_url": self.get_email_confirmation_url(
|
||||
request, emailconfirmation
|
||||
),
|
||||
}
|
||||
)
|
||||
if signup:
|
||||
email_template = "account/email/email_confirmation_signup"
|
||||
else:
|
||||
email_template = "account/email/email_confirmation"
|
||||
self.send_mail(email_template, emailconfirmation.email_address.email, ctx)
|
||||
|
||||
def respond_user_inactive(self, request, user):
|
||||
return headed_redirect_response("account_inactive")
|
||||
|
||||
def respond_email_verification_sent(self, request, user):
|
||||
return headed_redirect_response("account_email_verification_sent")
|
||||
|
||||
def _get_login_attempts_cache_key(self, request, **credentials):
|
||||
site = get_current_site(request)
|
||||
login = credentials.get("email", credentials.get("username", "")).lower()
|
||||
return "{site}:{login}".format(site=site.domain, login=login)
|
||||
|
||||
def _delete_login_attempts_cached_email(self, request, **credentials):
|
||||
cache_key = self._get_login_attempts_cache_key(request, **credentials)
|
||||
ratelimit.clear(request, action="login_failed", key=cache_key)
|
||||
|
||||
def pre_authenticate(self, request, **credentials):
|
||||
cache_key = self._get_login_attempts_cache_key(request, **credentials)
|
||||
if not ratelimit.consume(
|
||||
request,
|
||||
action="login_failed",
|
||||
key=cache_key,
|
||||
):
|
||||
raise self.validation_error("too_many_login_attempts")
|
||||
|
||||
def authenticate(self, request, **credentials):
|
||||
"""Only authenticates, does not actually login. See `login`"""
|
||||
from allauth.account.auth_backends import AuthenticationBackend
|
||||
|
||||
self.pre_authenticate(request, **credentials)
|
||||
AuthenticationBackend.unstash_authenticated_user()
|
||||
user = authenticate(request, **credentials)
|
||||
alt_user = AuthenticationBackend.unstash_authenticated_user()
|
||||
user = user or alt_user
|
||||
if user:
|
||||
self._delete_login_attempts_cached_email(request, **credentials)
|
||||
else:
|
||||
self.authentication_failed(request, **credentials)
|
||||
return user
|
||||
|
||||
def authentication_failed(self, request, **credentials):
|
||||
pass
|
||||
|
||||
def reauthenticate(self, user, password):
|
||||
from allauth.account.models import EmailAddress
|
||||
from allauth.account.utils import user_username
|
||||
|
||||
credentials = {"[PASSWORD-REMOVED]}
|
||||
username = user_username(user)
|
||||
if username:
|
||||
credentials["username"] = username
|
||||
email = EmailAddress.objects.get_primary_email(user)
|
||||
if email:
|
||||
credentials["email"] = email
|
||||
reauth_user = self.authenticate(context.request, **credentials)
|
||||
return reauth_user is not None and reauth_user.pk == user.pk
|
||||
|
||||
def is_ajax(self, request):
|
||||
return any(
|
||||
[
|
||||
request.META.get("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest",
|
||||
request.content_type == "application/json",
|
||||
request.META.get("HTTP_ACCEPT") == "application/json",
|
||||
]
|
||||
)
|
||||
|
||||
def get_client_ip(self, request):
|
||||
x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
|
||||
if x_forwarded_for:
|
||||
ip = x_forwarded_for.split(",")[0]
|
||||
else:
|
||||
ip = request.META.get("REMOTE_ADDR")
|
||||
return ip
|
||||
|
||||
def get_http_user_agent(self, request):
|
||||
return request.META.get("HTTP_USER_AGENT", "Unspecified")
|
||||
|
||||
def generate_emailconfirmation_key(self, email):
|
||||
key = get_random_string(64).lower()
|
||||
return key
|
||||
|
||||
def get_login_stages(self):
|
||||
ret = []
|
||||
ret.append("allauth.account.stages.LoginByCodeStage")
|
||||
ret.append("allauth.account.stages.EmailVerificationStage")
|
||||
if allauth_app_settings.MFA_ENABLED:
|
||||
from allauth.mfa import app_settings as mfa_settings
|
||||
|
||||
ret.append("allauth.mfa.stages.AuthenticateStage")
|
||||
|
||||
if mfa_settings.PASSKEY_SIGNUP_ENABLED:
|
||||
ret.append("allauth.mfa.webauthn.stages.PasskeySignupStage")
|
||||
return ret
|
||||
|
||||
def get_reauthentication_methods(self, user):
|
||||
"""The order of the methods returned matters. The first method is the
|
||||
default when using the `@reauthentication_required` decorator.
|
||||
"""
|
||||
from allauth.account.internal.flows.reauthentication import (
|
||||
get_reauthentication_flows,
|
||||
)
|
||||
|
||||
flow_by_id = {f["id"]: f for f in get_reauthentication_flows(user)}
|
||||
ret = []
|
||||
if "reauthenticate" in flow_by_id:
|
||||
entry = {
|
||||
"id": "reauthenticate",
|
||||
"description": _("Use your password"),
|
||||
"url": reverse("account_reauthenticate"),
|
||||
}
|
||||
ret.append(entry)
|
||||
if "mfa_reauthenticate" in flow_by_id:
|
||||
types = flow_by_id["mfa_reauthenticate"]["types"]
|
||||
if "recovery_codes" in types or "totp" in types:
|
||||
entry = {
|
||||
"id": "mfa_reauthenticate",
|
||||
"description": _("Use authenticator app or code"),
|
||||
"url": reverse("mfa_reauthenticate"),
|
||||
}
|
||||
ret.append(entry)
|
||||
if "webauthn" in types:
|
||||
entry = {
|
||||
"id": "mfa_reauthenticate:webauthn",
|
||||
"description": _("Use a security key"),
|
||||
"url": reverse("mfa_reauthenticate_webauthn"),
|
||||
}
|
||||
ret.append(entry)
|
||||
return ret
|
||||
|
||||
def send_notification_mail(self, template_prefix, user, context=None, email=None):
|
||||
from allauth.account.models import EmailAddress
|
||||
|
||||
if not app_settings.EMAIL_NOTIFICATIONS:
|
||||
return
|
||||
if not email:
|
||||
email = EmailAddress.objects.get_primary_email(user)
|
||||
if not email:
|
||||
return
|
||||
ctx = {
|
||||
"timestamp": timezone.now(),
|
||||
"ip": self.get_client_ip(self.request),
|
||||
"user_agent": self.get_http_user_agent(self.request),
|
||||
}
|
||||
if context:
|
||||
ctx.update(context)
|
||||
self.send_mail(template_prefix, email, ctx)
|
||||
|
||||
def generate_login_code(self) -> str:
|
||||
"""
|
||||
Generates a new login code.
|
||||
"""
|
||||
return self._generate_code()
|
||||
|
||||
def generate_email_verification_code(self) -> str:
|
||||
"""
|
||||
Generates a new email verification code.
|
||||
"""
|
||||
return self._generate_code()
|
||||
|
||||
def _generate_code(self):
|
||||
forbidden_chars = "0OI18B2ZAEU"
|
||||
allowed_chars = string.ascii_uppercase + string.digits
|
||||
for ch in forbidden_chars:
|
||||
allowed_chars = allowed_chars.replace(ch, "")
|
||||
return get_random_string(length=6, allowed_chars=allowed_chars)
|
||||
|
||||
def is_login_by_code_required(self, login) -> bool:
|
||||
"""
|
||||
Returns whether or not login-by-code is required for the given
|
||||
login.
|
||||
"""
|
||||
from allauth.account import authentication
|
||||
|
||||
method = None
|
||||
records = authentication.get_authentication_records(self.request)
|
||||
if records:
|
||||
method = records[-1]["method"]
|
||||
if method == "code":
|
||||
return False
|
||||
value = app_settings.LOGIN_BY_CODE_REQUIRED
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
if not value:
|
||||
return False
|
||||
return method is None or method in value
|
||||
|
||||
|
||||
def get_adapter(request=None):
|
||||
return import_attribute(app_settings.ADAPTER)(request)
|
||||
34
venv/lib/python3.11/site-packages/allauth/account/admin.py
Normal file
34
venv/lib/python3.11/site-packages/allauth/account/admin.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from django.contrib import admin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from . import app_settings
|
||||
from .adapter import get_adapter
|
||||
from .models import EmailAddress, EmailConfirmation
|
||||
|
||||
|
||||
class EmailAddressAdmin(admin.ModelAdmin):
|
||||
list_display = ("email", "user", "primary", "verified")
|
||||
list_filter = ("primary", "verified")
|
||||
search_fields = []
|
||||
raw_id_fields = ("user",)
|
||||
actions = ["make_verified"]
|
||||
|
||||
def get_search_fields(self, request):
|
||||
base_fields = get_adapter().get_user_search_fields()
|
||||
return ["email"] + list(map(lambda a: "user__" + a, base_fields))
|
||||
|
||||
def make_verified(self, request, queryset):
|
||||
queryset.update(verified=True)
|
||||
|
||||
make_verified.short_description = _("Mark selected email addresses as verified") # type: ignore[attr-defined]
|
||||
|
||||
|
||||
class EmailConfirmationAdmin(admin.ModelAdmin):
|
||||
list_display = ("email_address", "created", "sent", "key")
|
||||
list_filter = ("sent",)
|
||||
raw_id_fields = ("email_address",)
|
||||
|
||||
|
||||
if not app_settings.EMAIL_CONFIRMATION_HMAC:
|
||||
admin.site.register(EmailConfirmation, EmailConfirmationAdmin)
|
||||
admin.site.register(EmailAddress, EmailAddressAdmin)
|
||||
@@ -0,0 +1,478 @@
|
||||
import warnings
|
||||
from enum import Enum
|
||||
from typing import Set, Union
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
|
||||
class AppSettings:
|
||||
class AuthenticationMethod(str, Enum):
|
||||
USERNAME = "username"
|
||||
EMAIL = "email"
|
||||
USERNAME_EMAIL = "username_email"
|
||||
|
||||
class EmailVerificationMethod(str, Enum):
|
||||
# After signing up, keep the user account inactive until the email
|
||||
# address is verified
|
||||
MANDATORY = "mandatory"
|
||||
# Allow login with unverified email (email verification is
|
||||
# still sent)
|
||||
OPTIONAL = "optional"
|
||||
# Don't send email verification mails during signup
|
||||
NONE = "none"
|
||||
|
||||
def __init__(self, prefix):
|
||||
from django.conf import settings
|
||||
|
||||
self.prefix = prefix
|
||||
# If login is by email, email must be required
|
||||
assert (
|
||||
not self.AUTHENTICATION_METHOD == self.AuthenticationMethod.EMAIL
|
||||
) or self.EMAIL_REQUIRED
|
||||
# If login includes email, login must be unique
|
||||
assert (
|
||||
self.AUTHENTICATION_METHOD == self.AuthenticationMethod.USERNAME
|
||||
) or self.UNIQUE_EMAIL
|
||||
assert (
|
||||
self.EMAIL_VERIFICATION != self.EmailVerificationMethod.MANDATORY
|
||||
) or self.EMAIL_REQUIRED
|
||||
if not self.USER_MODEL_USERNAME_FIELD:
|
||||
assert not self.USERNAME_REQUIRED
|
||||
assert self.AUTHENTICATION_METHOD not in (
|
||||
self.AuthenticationMethod.USERNAME,
|
||||
self.AuthenticationMethod.USERNAME_EMAIL,
|
||||
)
|
||||
if self.MAX_EMAIL_ADDRESSES is not None:
|
||||
assert self.MAX_EMAIL_ADDRESSES > 0
|
||||
if self.CHANGE_EMAIL:
|
||||
if self.MAX_EMAIL_ADDRESSES is not None and self.MAX_EMAIL_ADDRESSES != 2:
|
||||
raise ImproperlyConfigured(
|
||||
"Invalid combination of ACCOUNT_CHANGE_EMAIL and ACCOUNT_MAX_EMAIL_ADDRESSES"
|
||||
)
|
||||
if hasattr(settings, "ACCOUNT_LOGIN_ATTEMPTS_LIMIT") or hasattr(
|
||||
settings, "ACCOUNT_LOGIN_ATTEMPTS_TIMEOUT"
|
||||
):
|
||||
warnings.warn(
|
||||
"settings.ACCOUNT_LOGIN_ATTEMPTS_LIMIT/TIMEOUT is deprecated, use: settings.ACCOUNT_RATE_LIMITS['login_failed']"
|
||||
)
|
||||
|
||||
if hasattr(settings, "ACCOUNT_EMAIL_CONFIRMATION_COOLDOWN"):
|
||||
warnings.warn(
|
||||
"settings.ACCOUNT_EMAIL_CONFIRMATION_COOLDOWN is deprecated, use: settings.ACCOUNT_RATE_LIMITS['confirm_email']"
|
||||
)
|
||||
|
||||
def _setting(self, name, dflt):
|
||||
from allauth.utils import get_setting
|
||||
|
||||
return get_setting(self.prefix + name, dflt)
|
||||
|
||||
@property
|
||||
def PREVENT_ENUMERATION(self):
|
||||
return self._setting("PREVENT_ENUMERATION", True)
|
||||
|
||||
@property
|
||||
def DEFAULT_HTTP_PROTOCOL(self):
|
||||
return self._setting("DEFAULT_HTTP_PROTOCOL", "http").lower()
|
||||
|
||||
@property
|
||||
def EMAIL_CONFIRMATION_EXPIRE_DAYS(self):
|
||||
"""
|
||||
Determines the expiration date of email confirmation mails (#
|
||||
of days)
|
||||
"""
|
||||
from django.conf import settings
|
||||
|
||||
return self._setting(
|
||||
"EMAIL_CONFIRMATION_EXPIRE_DAYS",
|
||||
getattr(settings, "EMAIL_CONFIRMATION_DAYS", 3),
|
||||
)
|
||||
|
||||
@property
|
||||
def EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL(self):
|
||||
"""
|
||||
The URL to redirect to after a successful email confirmation, in
|
||||
case of an authenticated user
|
||||
"""
|
||||
return self._setting("EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL", None)
|
||||
|
||||
@property
|
||||
def EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL(self):
|
||||
"""
|
||||
The URL to redirect to after a successful email confirmation, in
|
||||
case no user is logged in
|
||||
"""
|
||||
from django.conf import settings
|
||||
|
||||
return self._setting(
|
||||
"EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL", settings.LOGIN_URL
|
||||
)
|
||||
|
||||
@property
|
||||
def EMAIL_REQUIRED(self):
|
||||
"""
|
||||
The user is required to hand over an email address when signing up
|
||||
"""
|
||||
return self._setting("EMAIL_REQUIRED", False)
|
||||
|
||||
@property
|
||||
def EMAIL_VERIFICATION(self):
|
||||
"""
|
||||
See email verification method
|
||||
"""
|
||||
ret = self._setting("EMAIL_VERIFICATION", self.EmailVerificationMethod.OPTIONAL)
|
||||
# Deal with legacy (boolean based) setting
|
||||
if ret is True:
|
||||
ret = self.EmailVerificationMethod.MANDATORY
|
||||
elif ret is False:
|
||||
ret = self.EmailVerificationMethod.OPTIONAL
|
||||
return self.EmailVerificationMethod(ret)
|
||||
|
||||
@property
|
||||
def EMAIL_VERIFICATION_BY_CODE_ENABLED(self):
|
||||
return self._setting("EMAIL_VERIFICATION_BY_CODE_ENABLED", False)
|
||||
|
||||
@property
|
||||
def EMAIL_VERIFICATION_BY_CODE_MAX_ATTEMPTS(self):
|
||||
return self._setting("EMAIL_VERIFICATION_BY_CODE_MAX_ATTEMPTS", 3)
|
||||
|
||||
@property
|
||||
def EMAIL_VERIFICATION_BY_CODE_TIMEOUT(self):
|
||||
return self._setting("EMAIL_VERIFICATION_BY_CODE_TIMEOUT", 15 * 60)
|
||||
|
||||
@property
|
||||
def MAX_EMAIL_ADDRESSES(self):
|
||||
return self._setting("MAX_EMAIL_ADDRESSES", None)
|
||||
|
||||
@property
|
||||
def CHANGE_EMAIL(self):
|
||||
return self._setting("CHANGE_EMAIL", False)
|
||||
|
||||
@property
|
||||
def AUTHENTICATION_METHOD(self):
|
||||
ret = self._setting("AUTHENTICATION_METHOD", self.AuthenticationMethod.USERNAME)
|
||||
return self.AuthenticationMethod(ret)
|
||||
|
||||
@property
|
||||
def EMAIL_MAX_LENGTH(self):
|
||||
"""
|
||||
Adjust max_length of email addresses
|
||||
"""
|
||||
return self._setting("EMAIL_MAX_LENGTH", 254)
|
||||
|
||||
@property
|
||||
def UNIQUE_EMAIL(self):
|
||||
"""
|
||||
Enforce uniqueness of email addresses
|
||||
"""
|
||||
return self._setting("UNIQUE_EMAIL", True)
|
||||
|
||||
@property
|
||||
def SIGNUP_EMAIL_ENTER_TWICE(self):
|
||||
"""
|
||||
Signup email verification
|
||||
"""
|
||||
return self._setting("SIGNUP_EMAIL_ENTER_TWICE", False)
|
||||
|
||||
@property
|
||||
def SIGNUP_PASSWORD_ENTER_TWICE(self):
|
||||
"""
|
||||
Signup password verification
|
||||
"""
|
||||
legacy = self._setting("SIGNUP_PASSWORD_VERIFICATION", True)
|
||||
return self._setting("SIGNUP_PASSWORD_ENTER_TWICE", legacy)
|
||||
|
||||
@property
|
||||
def SIGNUP_REDIRECT_URL(self):
|
||||
from django.conf import settings
|
||||
|
||||
return self._setting("SIGNUP_REDIRECT_URL", settings.LOGIN_REDIRECT_URL)
|
||||
|
||||
@property
|
||||
def PASSWORD_MIN_LENGTH(self):
|
||||
"""
|
||||
Minimum password Length
|
||||
"""
|
||||
from django.conf import settings
|
||||
|
||||
ret = None
|
||||
if not settings.AUTH_PASSWORD_VALIDATORS:
|
||||
ret = self._setting("PASSWORD_MIN_LENGTH", 6)
|
||||
return ret
|
||||
|
||||
@property
|
||||
def RATE_LIMITS(self):
|
||||
rls = self._setting("RATE_LIMITS", {})
|
||||
if rls is False:
|
||||
return {}
|
||||
attempts_amount = self._setting("LOGIN_ATTEMPTS_LIMIT", 5)
|
||||
attempts_timeout = self._setting("LOGIN_ATTEMPTS_TIMEOUT", 60 * 5)
|
||||
login_failed_rl = None
|
||||
if attempts_amount and attempts_timeout:
|
||||
login_failed_rl = f"10/m/ip,{attempts_amount}/{attempts_timeout}s/key"
|
||||
|
||||
if self.EMAIL_VERIFICATION_BY_CODE_ENABLED:
|
||||
confirm_email_rl = "1/10s/key"
|
||||
else:
|
||||
cooldown = self._setting("EMAIL_CONFIRMATION_COOLDOWN", 3 * 60)
|
||||
confirm_email_rl = None
|
||||
if cooldown:
|
||||
confirm_email_rl = f"1/{cooldown}s/key"
|
||||
ret = {
|
||||
# Change password view (for users already logged in)
|
||||
"change_password": "5/m/user",
|
||||
# Email management (e.g. add, remove, change primary)
|
||||
"manage_email": "10/m/user",
|
||||
# Request a password reset, global rate limit per IP
|
||||
"reset_password": "20/m/ip,5/m/key",
|
||||
# Reauthentication for users already logged in
|
||||
"reauthenticate": "10/m/user",
|
||||
# Password reset (the view the password reset email links to).
|
||||
"reset_password_from_key": "20/m/ip",
|
||||
# Signups.
|
||||
"signup": "20/m/ip",
|
||||
# Logins.
|
||||
"login": "30/m/ip",
|
||||
# Request a login code: key is the email.
|
||||
"request_login_code": "20/m/ip,3/m/key",
|
||||
# Logins.
|
||||
"login_failed": login_failed_rl,
|
||||
# Confirm email
|
||||
"confirm_email": confirm_email_rl,
|
||||
}
|
||||
ret.update(rls)
|
||||
return ret
|
||||
|
||||
@property
|
||||
def EMAIL_SUBJECT_PREFIX(self):
|
||||
"""
|
||||
Subject-line prefix to use for email messages sent
|
||||
"""
|
||||
return self._setting("EMAIL_SUBJECT_PREFIX", None)
|
||||
|
||||
@property
|
||||
def SIGNUP_FORM_CLASS(self):
|
||||
"""
|
||||
Signup form
|
||||
"""
|
||||
return self._setting("SIGNUP_FORM_CLASS", None)
|
||||
|
||||
@property
|
||||
def SIGNUP_FORM_HONEYPOT_FIELD(self):
|
||||
"""
|
||||
Honeypot field name. Empty string or ``None`` will disable honeypot behavior.
|
||||
"""
|
||||
return self._setting("SIGNUP_FORM_HONEYPOT_FIELD", None)
|
||||
|
||||
@property
|
||||
def USERNAME_REQUIRED(self):
|
||||
"""
|
||||
The user is required to enter a username when signing up
|
||||
"""
|
||||
return self._setting("USERNAME_REQUIRED", True)
|
||||
|
||||
@property
|
||||
def USERNAME_MIN_LENGTH(self):
|
||||
"""
|
||||
Minimum username Length
|
||||
"""
|
||||
return self._setting("USERNAME_MIN_LENGTH", 1)
|
||||
|
||||
@property
|
||||
def USERNAME_BLACKLIST(self):
|
||||
"""
|
||||
List of usernames that are not allowed
|
||||
"""
|
||||
return self._setting("USERNAME_BLACKLIST", [])
|
||||
|
||||
@property
|
||||
def PASSWORD_INPUT_RENDER_VALUE(self):
|
||||
"""
|
||||
render_value parameter as passed to PasswordInput fields
|
||||
"""
|
||||
return self._setting("PASSWORD_INPUT_RENDER_VALUE", False)
|
||||
|
||||
@property
|
||||
def ADAPTER(self):
|
||||
return self._setting("ADAPTER", "allauth.account.adapter.DefaultAccountAdapter")
|
||||
|
||||
@property
|
||||
def CONFIRM_EMAIL_ON_GET(self):
|
||||
return self._setting("CONFIRM_EMAIL_ON_GET", False)
|
||||
|
||||
@property
|
||||
def AUTHENTICATED_LOGIN_REDIRECTS(self):
|
||||
return self._setting("AUTHENTICATED_LOGIN_REDIRECTS", True)
|
||||
|
||||
@property
|
||||
def LOGIN_ON_EMAIL_CONFIRMATION(self):
|
||||
"""
|
||||
Automatically log the user in once they confirmed their email address
|
||||
"""
|
||||
return self._setting("LOGIN_ON_EMAIL_CONFIRMATION", False)
|
||||
|
||||
@property
|
||||
def LOGIN_ON_PASSWORD_RESET(self):
|
||||
"""
|
||||
Automatically log the user in immediately after resetting
|
||||
their password.
|
||||
"""
|
||||
return self._setting("LOGIN_ON_PASSWORD_RESET", False)
|
||||
|
||||
@property
|
||||
def LOGOUT_REDIRECT_URL(self):
|
||||
from django.conf import settings
|
||||
|
||||
return self._setting("LOGOUT_REDIRECT_URL", settings.LOGOUT_REDIRECT_URL or "/")
|
||||
|
||||
@property
|
||||
def LOGOUT_ON_GET(self):
|
||||
return self._setting("LOGOUT_ON_GET", False)
|
||||
|
||||
@property
|
||||
def LOGOUT_ON_PASSWORD_CHANGE(self):
|
||||
return self._setting("LOGOUT_ON_PASSWORD_CHANGE", False)
|
||||
|
||||
@property
|
||||
def USER_MODEL_USERNAME_FIELD(self):
|
||||
return self._setting("USER_MODEL_USERNAME_FIELD", "username")
|
||||
|
||||
@property
|
||||
def USER_MODEL_EMAIL_FIELD(self):
|
||||
return self._setting("USER_MODEL_EMAIL_FIELD", "email")
|
||||
|
||||
@property
|
||||
def SESSION_COOKIE_AGE(self):
|
||||
"""
|
||||
Deprecated -- use Django's settings.SESSION_COOKIE_AGE instead
|
||||
"""
|
||||
from django.conf import settings
|
||||
|
||||
return self._setting("SESSION_COOKIE_AGE", settings.SESSION_COOKIE_AGE)
|
||||
|
||||
@property
|
||||
def SESSION_REMEMBER(self):
|
||||
"""
|
||||
Controls the life time of the session. Set to `None` to ask the user
|
||||
("Remember me?"), `False` to not remember, and `True` to always
|
||||
remember.
|
||||
"""
|
||||
return self._setting("SESSION_REMEMBER", None)
|
||||
|
||||
@property
|
||||
def TEMPLATE_EXTENSION(self):
|
||||
"""
|
||||
A string defining the template extension to use, defaults to `html`.
|
||||
"""
|
||||
return self._setting("TEMPLATE_EXTENSION", "html")
|
||||
|
||||
@property
|
||||
def FORMS(self):
|
||||
return self._setting("FORMS", {})
|
||||
|
||||
@property
|
||||
def EMAIL_CONFIRMATION_HMAC(self):
|
||||
return self._setting("EMAIL_CONFIRMATION_HMAC", True)
|
||||
|
||||
@property
|
||||
def SALT(self):
|
||||
return self._setting("SALT", "account")
|
||||
|
||||
@property
|
||||
def PRESERVE_USERNAME_CASING(self):
|
||||
return self._setting("PRESERVE_USERNAME_CASING", True)
|
||||
|
||||
@property
|
||||
def USERNAME_VALIDATORS(self):
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
from allauth.utils import import_attribute
|
||||
|
||||
path = self._setting("USERNAME_VALIDATORS", None)
|
||||
if path:
|
||||
ret = import_attribute(path)
|
||||
if not isinstance(ret, list):
|
||||
raise ImproperlyConfigured(
|
||||
"ACCOUNT_USERNAME_VALIDATORS is expected to be a list"
|
||||
)
|
||||
else:
|
||||
if self.USER_MODEL_USERNAME_FIELD is not None:
|
||||
ret = (
|
||||
get_user_model()
|
||||
._meta.get_field(self.USER_MODEL_USERNAME_FIELD)
|
||||
.validators
|
||||
)
|
||||
else:
|
||||
ret = []
|
||||
return ret
|
||||
|
||||
@property
|
||||
def PASSWORD_RESET_TOKEN_GENERATOR(self):
|
||||
from allauth.account.forms import EmailAwarePasswordResetTokenGenerator
|
||||
from allauth.utils import import_attribute
|
||||
|
||||
token_generator_path = self._setting("PASSWORD_RESET_TOKEN_GENERATOR", None)
|
||||
if token_generator_path is not None:
|
||||
token_generator = import_attribute(token_generator_path)
|
||||
else:
|
||||
token_generator = EmailAwarePasswordResetTokenGenerator
|
||||
return token_generator
|
||||
|
||||
@property
|
||||
def EMAIL_UNKNOWN_ACCOUNTS(self):
|
||||
return self._setting("EMAIL_UNKNOWN_ACCOUNTS", True)
|
||||
|
||||
@property
|
||||
def REAUTHENTICATION_TIMEOUT(self):
|
||||
return self._setting("REAUTHENTICATION_TIMEOUT", 300)
|
||||
|
||||
@property
|
||||
def EMAIL_NOTIFICATIONS(self):
|
||||
return self._setting("EMAIL_NOTIFICATIONS", False)
|
||||
|
||||
@property
|
||||
def REAUTHENTICATION_REQUIRED(self):
|
||||
return self._setting("REAUTHENTICATION_REQUIRED", False)
|
||||
|
||||
@property
|
||||
def LOGIN_BY_CODE_ENABLED(self):
|
||||
return self._setting("LOGIN_BY_CODE_ENABLED", False)
|
||||
|
||||
@property
|
||||
def LOGIN_BY_CODE_MAX_ATTEMPTS(self):
|
||||
return self._setting("LOGIN_BY_CODE_MAX_ATTEMPTS", 3)
|
||||
|
||||
@property
|
||||
def LOGIN_BY_CODE_TIMEOUT(self):
|
||||
return self._setting("LOGIN_BY_CODE_TIMEOUT", 3 * 60)
|
||||
|
||||
@property
|
||||
def LOGIN_TIMEOUT(self):
|
||||
"""
|
||||
The maximum allowed time (in seconds) for a login to go through the
|
||||
various login stages. This limits, for example, the time span that the
|
||||
2FA stage remains available.
|
||||
"""
|
||||
return self._setting("LOGIN_TIMEOUT", 15 * 60)
|
||||
|
||||
@property
|
||||
def LOGIN_BY_CODE_REQUIRED(self) -> Union[bool, Set[str]]:
|
||||
"""
|
||||
When enabled (in case of ``True``), every user logging in is
|
||||
required to input a login confirmation code sent by email.
|
||||
Alternatively, you can specify a set of authentication methods
|
||||
(``"password"``, ``"mfa"``, or ``"socialaccount"``) for which login
|
||||
codes are required.
|
||||
"""
|
||||
value = self._setting("LOGIN_BY_CODE_REQUIRED", False)
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
return set(value)
|
||||
|
||||
|
||||
_app_settings = AppSettings("ACCOUNT_")
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
# See https://peps.python.org/pep-0562/
|
||||
return getattr(_app_settings, name)
|
||||
21
venv/lib/python3.11/site-packages/allauth/account/apps.py
Normal file
21
venv/lib/python3.11/site-packages/allauth/account/apps.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from allauth import app_settings
|
||||
|
||||
|
||||
class AccountConfig(AppConfig):
|
||||
name = "allauth.account"
|
||||
verbose_name = _("Accounts")
|
||||
default_auto_field = app_settings.DEFAULT_AUTO_FIELD or "django.db.models.AutoField"
|
||||
|
||||
def ready(self):
|
||||
from allauth.account import checks # noqa
|
||||
|
||||
required_mw = "allauth.account.middleware.AccountMiddleware"
|
||||
if required_mw not in settings.MIDDLEWARE:
|
||||
raise ImproperlyConfigured(
|
||||
f"{required_mw} must be added to settings.MIDDLEWARE"
|
||||
)
|
||||
@@ -0,0 +1,99 @@
|
||||
from threading import local
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
|
||||
from . import app_settings
|
||||
from .app_settings import AuthenticationMethod
|
||||
from .utils import filter_users_by_email, filter_users_by_username
|
||||
|
||||
|
||||
_stash = local()
|
||||
|
||||
|
||||
class AuthenticationBackend(ModelBackend):
|
||||
def authenticate(self, request, **credentials):
|
||||
ret = None
|
||||
if app_settings.AUTHENTICATION_METHOD == AuthenticationMethod.EMAIL:
|
||||
ret = self._authenticate_by_email(**credentials)
|
||||
elif app_settings.AUTHENTICATION_METHOD == AuthenticationMethod.USERNAME_EMAIL:
|
||||
ret = self._authenticate_by_email(**credentials)
|
||||
if not ret:
|
||||
ret = self._authenticate_by_username(**credentials)
|
||||
else:
|
||||
ret = self._authenticate_by_username(**credentials)
|
||||
return ret
|
||||
|
||||
def _authenticate_by_username(self, **credentials):
|
||||
username_field = app_settings.USER_MODEL_USERNAME_FIELD
|
||||
username = credentials.get("username")
|
||||
[PASSWORD-REMOVED].get("password")
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
if not username_field or username is None or password is None:
|
||||
return None
|
||||
try:
|
||||
# Username query is case insensitive
|
||||
user = filter_users_by_username(username).get()
|
||||
except User.DoesNotExist:
|
||||
# Run the default password hasher once to reduce the timing
|
||||
# difference between an existing and a nonexistent user.
|
||||
get_user_model()().set_password(password)
|
||||
return None
|
||||
else:
|
||||
if self._check_password(user, password):
|
||||
return user
|
||||
|
||||
def _authenticate_by_email(self, **credentials):
|
||||
# Even though allauth will pass along `email`, other apps may
|
||||
# not respect this setting. For example, when using
|
||||
# django-tastypie basic authentication, the login is always
|
||||
# passed as `username`. So let's play nice with other apps
|
||||
# and use username as fallback
|
||||
email = credentials.get("email", credentials.get("username"))
|
||||
if email:
|
||||
for user in filter_users_by_email(email, prefer_verified=True):
|
||||
if self._check_password(user, credentials["password"]):
|
||||
return user
|
||||
return None
|
||||
|
||||
def _check_password(self, user, password):
|
||||
ret = user.check_password(password)
|
||||
if ret:
|
||||
ret = self.user_can_authenticate(user)
|
||||
if not ret:
|
||||
self._stash_user(user)
|
||||
return ret
|
||||
|
||||
@classmethod
|
||||
def _stash_user(cls, user):
|
||||
"""Now, be aware, the following is quite ugly, let me explain:
|
||||
|
||||
Even if the user credentials match, the authentication can fail because
|
||||
Django's default ModelBackend calls user_can_authenticate(), which
|
||||
checks `is_active`. Now, earlier versions of allauth did not do this
|
||||
and simply returned the user as authenticated, even in case of
|
||||
`is_active=False`. For allauth scope, this does not pose a problem, as
|
||||
these users are properly redirected to an account inactive page.
|
||||
|
||||
This does pose a problem when the allauth backend is used in a
|
||||
different context where allauth is not responsible for the login. Then,
|
||||
by not checking on `user_can_authenticate()` users will allow to become
|
||||
authenticated whereas according to Django logic this should not be
|
||||
allowed.
|
||||
|
||||
In order to preserve the allauth behavior while respecting Django's
|
||||
logic, we stash a user for which the password check succeeded but
|
||||
`user_can_authenticate()` failed. In the allauth authentication logic,
|
||||
we can then unstash this user and proceed pointing the user to the
|
||||
account inactive page.
|
||||
"""
|
||||
global _stash
|
||||
ret = getattr(_stash, "user", None)
|
||||
_stash.user = user
|
||||
return ret
|
||||
|
||||
@classmethod
|
||||
def unstash_authenticated_user(cls):
|
||||
return cls._stash_user(None)
|
||||
@@ -0,0 +1,7 @@
|
||||
from allauth.account.internal.flows.login import (
|
||||
AUTHENTICATION_METHODS_SESSION_KEY,
|
||||
)
|
||||
|
||||
|
||||
def get_authentication_records(request):
|
||||
return request.session.get(AUTHENTICATION_METHODS_SESSION_KEY, [])
|
||||
52
venv/lib/python3.11/site-packages/allauth/account/checks.py
Normal file
52
venv/lib/python3.11/site-packages/allauth/account/checks.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from django.core.checks import Critical, Warning, register
|
||||
|
||||
|
||||
@register()
|
||||
def adapter_check(app_configs, **kwargs):
|
||||
from allauth.account.adapter import get_adapter
|
||||
|
||||
ret = []
|
||||
adapter = get_adapter()
|
||||
if hasattr(adapter, "get_email_confirmation_redirect_url"):
|
||||
ret.append(
|
||||
Warning(
|
||||
msg="adapter.get_email_confirmation_redirect_url(request) is deprecated, use adapter.get_email_verification_redirect_url(email_address)"
|
||||
)
|
||||
)
|
||||
return ret
|
||||
|
||||
|
||||
@register()
|
||||
def settings_check(app_configs, **kwargs):
|
||||
from allauth import app_settings as allauth_app_settings
|
||||
from allauth.account import app_settings
|
||||
|
||||
ret = []
|
||||
if allauth_app_settings.SOCIALACCOUNT_ONLY:
|
||||
if app_settings.LOGIN_BY_CODE_ENABLED:
|
||||
ret.append(
|
||||
Critical(
|
||||
msg="SOCIALACCOUNT_ONLY does not work with ACCOUNT_LOGIN_BY_CODE_ENABLED"
|
||||
)
|
||||
)
|
||||
if allauth_app_settings.MFA_ENABLED:
|
||||
ret.append(
|
||||
Critical(msg="SOCIALACCOUNT_ONLY does not work with 'allauth.mfa'")
|
||||
)
|
||||
if app_settings.EMAIL_VERIFICATION != app_settings.EmailVerificationMethod.NONE:
|
||||
ret.append(
|
||||
Critical(
|
||||
msg="SOCIALACCOUNT_ONLY requires ACCOUNT_EMAIL_VERIFICATION = 'none'"
|
||||
)
|
||||
)
|
||||
if (
|
||||
app_settings.EMAIL_VERIFICATION_BY_CODE_ENABLED
|
||||
and app_settings.EMAIL_VERIFICATION
|
||||
!= app_settings.EmailVerificationMethod.MANDATORY
|
||||
):
|
||||
ret.append(
|
||||
Critical(
|
||||
msg="ACCOUNT_EMAIL_VERFICATION_BY_CODE requires ACCOUNT_EMAIL_VERIFICATION = 'mandatory'"
|
||||
)
|
||||
)
|
||||
return ret
|
||||
102
venv/lib/python3.11/site-packages/allauth/account/decorators.py
Normal file
102
venv/lib/python3.11/site-packages/allauth/account/decorators.py
Normal file
@@ -0,0 +1,102 @@
|
||||
from functools import wraps
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import render, resolve_url
|
||||
|
||||
from allauth.account.internal.flows import reauthentication
|
||||
from allauth.account.models import EmailAddress
|
||||
from allauth.account.utils import (
|
||||
get_next_redirect_url,
|
||||
send_email_confirmation,
|
||||
)
|
||||
from allauth.core.exceptions import ReauthenticationRequired
|
||||
from allauth.core.internal import httpkit
|
||||
|
||||
|
||||
def verified_email_required(
|
||||
function=None, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME
|
||||
):
|
||||
"""
|
||||
Even when email verification is not mandatory during signup, there
|
||||
may be circumstances during which you really want to prevent
|
||||
unverified users to proceed. This decorator ensures the user is
|
||||
authenticated and has a verified email address. If the former is
|
||||
not the case then the behavior is identical to that of the
|
||||
standard `login_required` decorator. If the latter does not hold,
|
||||
email verification mails are automatically resend and the user is
|
||||
presented with a page informing them they needs to verify their email
|
||||
address.
|
||||
"""
|
||||
|
||||
def decorator(view_func):
|
||||
@login_required(redirect_field_name=redirect_field_name, login_url=login_url)
|
||||
def _wrapped_view(request, *args, **kwargs):
|
||||
if not EmailAddress.objects.filter(
|
||||
user=request.user, verified=True
|
||||
).exists():
|
||||
send_email_confirmation(request, request.user)
|
||||
return render(request, "account/verified_email_required.html")
|
||||
return view_func(request, *args, **kwargs)
|
||||
|
||||
return _wrapped_view
|
||||
|
||||
if function:
|
||||
return decorator(function)
|
||||
return decorator
|
||||
|
||||
|
||||
def reauthentication_required(
|
||||
function=None,
|
||||
redirect_field_name=REDIRECT_FIELD_NAME,
|
||||
allow_get=False,
|
||||
enabled=None,
|
||||
):
|
||||
def decorator(view_func):
|
||||
@wraps(view_func)
|
||||
def _wrapper_view(request, *args, **kwargs):
|
||||
pass_method = allow_get and request.method == "GET"
|
||||
ena = (enabled is None) or (
|
||||
enabled(request) if callable(enabled) else enabled
|
||||
)
|
||||
if ena and not pass_method:
|
||||
if (
|
||||
request.user.is_anonymous
|
||||
or not reauthentication.did_recently_authenticate(request)
|
||||
):
|
||||
raise ReauthenticationRequired()
|
||||
return view_func(request, *args, **kwargs)
|
||||
|
||||
return _wrapper_view
|
||||
|
||||
if function:
|
||||
return decorator(function)
|
||||
return decorator
|
||||
|
||||
|
||||
def secure_admin_login(function=None):
|
||||
def decorator(view_func):
|
||||
@wraps(view_func)
|
||||
def _wrapper_view(request, *args, **kwargs):
|
||||
if request.user.is_authenticated:
|
||||
if not request.user.is_staff or not request.user.is_active:
|
||||
raise PermissionDenied()
|
||||
return view_func(request, *args, **kwargs)
|
||||
else:
|
||||
next_url = get_next_redirect_url(request)
|
||||
if not next_url:
|
||||
next_url = request.get_full_path()
|
||||
login_url = resolve_url(settings.LOGIN_URL)
|
||||
login_url = httpkit.add_query_params(
|
||||
login_url, {REDIRECT_FIELD_NAME: next_url}
|
||||
)
|
||||
return HttpResponseRedirect(login_url)
|
||||
|
||||
return _wrapper_view
|
||||
|
||||
if function:
|
||||
return decorator(function)
|
||||
return decorator
|
||||
747
venv/lib/python3.11/site-packages/allauth/account/forms.py
Normal file
747
venv/lib/python3.11/site-packages/allauth/account/forms.py
Normal file
@@ -0,0 +1,747 @@
|
||||
from importlib import import_module
|
||||
|
||||
from django import forms
|
||||
from django.contrib.auth import get_user_model, password_validation
|
||||
from django.contrib.auth.tokens import PasswordResetTokenGenerator
|
||||
from django.core import exceptions, validators
|
||||
from django.urls import NoReverseMatch, reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext, gettext_lazy as _, pgettext
|
||||
|
||||
from allauth.account.internal import flows
|
||||
from allauth.account.internal.stagekit import LOGIN_SESSION_KEY
|
||||
from allauth.account.stages import EmailVerificationStage
|
||||
from allauth.core import context, ratelimit
|
||||
from allauth.utils import get_username_max_length, set_form_field_order
|
||||
|
||||
from . import app_settings
|
||||
from .adapter import DefaultAccountAdapter, get_adapter
|
||||
from .app_settings import AuthenticationMethod
|
||||
from .models import EmailAddress, Login
|
||||
from .utils import (
|
||||
filter_users_by_email,
|
||||
setup_user_email,
|
||||
sync_user_email_addresses,
|
||||
url_str_to_user_pk,
|
||||
user_email,
|
||||
user_pk_to_url_str,
|
||||
user_username,
|
||||
)
|
||||
|
||||
|
||||
class EmailAwarePasswordResetTokenGenerator(PasswordResetTokenGenerator):
|
||||
def _make_hash_value(self, user, timestamp):
|
||||
ret = super(EmailAwarePasswordResetTokenGenerator, self)._make_hash_value(
|
||||
user, timestamp
|
||||
)
|
||||
sync_user_email_addresses(user)
|
||||
email = user_email(user)
|
||||
emails = set([email] if email else [])
|
||||
emails.update(
|
||||
EmailAddress.objects.filter(user=user).values_list("email", flat=True)
|
||||
)
|
||||
ret += "|".join(sorted(emails))
|
||||
return ret
|
||||
|
||||
|
||||
default_token_generator = app_settings.PASSWORD_RESET_TOKEN_GENERATOR()
|
||||
|
||||
|
||||
class PasswordVerificationMixin:
|
||||
def clean(self):
|
||||
cleaned_data = super(PasswordVerificationMixin, self).clean()
|
||||
password1 = cleaned_data.get("password1")
|
||||
password2 = cleaned_data.get("password2")
|
||||
if (password1 and password2) and password1 != password2:
|
||||
self.add_error("password2", _("You must type the same password each time."))
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class PasswordField(forms.CharField):
|
||||
def __init__(self, *args, **kwargs):
|
||||
render_value = kwargs.pop(
|
||||
"render_value", app_settings.PASSWORD_INPUT_RENDER_VALUE
|
||||
)
|
||||
kwargs["widget"] = forms.PasswordInput(
|
||||
render_value=render_value,
|
||||
attrs={"placeholder": kwargs.get("label")},
|
||||
)
|
||||
autocomplete = kwargs.pop("autocomplete", None)
|
||||
if autocomplete is not None:
|
||||
kwargs["widget"].attrs["autocomplete"] = autocomplete
|
||||
super(PasswordField, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class SetPasswordField(PasswordField):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs["autocomplete"] = "new-password"
|
||||
kwargs.setdefault(
|
||||
"help_text", password_validation.password_validators_help_text_html()
|
||||
)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.user = None
|
||||
|
||||
def clean(self, value):
|
||||
value = super().clean(value)
|
||||
value = get_adapter().clean_password(value, user=self.user)
|
||||
return value
|
||||
|
||||
|
||||
class LoginForm(forms.Form):
|
||||
[PASSWORD-REMOVED](label=_("Password"), autocomplete="current-password")
|
||||
remember = forms.BooleanField(label=_("Remember Me"), required=False)
|
||||
|
||||
user = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.request = kwargs.pop("request", None)
|
||||
super(LoginForm, self).__init__(*args, **kwargs)
|
||||
if app_settings.AUTHENTICATION_METHOD == AuthenticationMethod.EMAIL:
|
||||
login_widget = forms.EmailInput(
|
||||
attrs={
|
||||
"placeholder": _("Email address"),
|
||||
"autocomplete": "email",
|
||||
}
|
||||
)
|
||||
login_field = forms.EmailField(label=_("Email"), widget=login_widget)
|
||||
elif app_settings.AUTHENTICATION_METHOD == AuthenticationMethod.USERNAME:
|
||||
login_widget = forms.TextInput(
|
||||
attrs={"placeholder": _("Username"), "autocomplete": "username"}
|
||||
)
|
||||
login_field = forms.CharField(
|
||||
label=_("Username"),
|
||||
widget=login_widget,
|
||||
max_length=get_username_max_length(),
|
||||
)
|
||||
else:
|
||||
assert (
|
||||
app_settings.AUTHENTICATION_METHOD
|
||||
== AuthenticationMethod.USERNAME_EMAIL
|
||||
)
|
||||
login_widget = forms.TextInput(
|
||||
attrs={"placeholder": _("Username or email"), "autocomplete": "email"}
|
||||
)
|
||||
login_field = forms.CharField(
|
||||
label=pgettext("field label", "Login"), widget=login_widget
|
||||
)
|
||||
self.fields["login"] = login_field
|
||||
set_form_field_order(self, ["login", "password", "remember"])
|
||||
if app_settings.SESSION_REMEMBER is not None:
|
||||
del self.fields["remember"]
|
||||
try:
|
||||
reset_url = reverse("account_reset_password")
|
||||
except NoReverseMatch:
|
||||
pass
|
||||
else:
|
||||
forgot_txt = _("Forgot your password?")
|
||||
self.fields["password"].help_text = mark_safe(
|
||||
f'<a href="{reset_url}">{forgot_txt}</a>'
|
||||
)
|
||||
|
||||
def user_credentials(self):
|
||||
"""
|
||||
Provides the credentials required to authenticate the user for
|
||||
login.
|
||||
"""
|
||||
credentials = {}
|
||||
login = self.cleaned_data["login"]
|
||||
if app_settings.AUTHENTICATION_METHOD == AuthenticationMethod.EMAIL:
|
||||
credentials["email"] = login
|
||||
elif app_settings.AUTHENTICATION_METHOD == AuthenticationMethod.USERNAME:
|
||||
credentials["username"] = login
|
||||
else:
|
||||
if self._is_login_email(login):
|
||||
credentials["email"] = login
|
||||
credentials["username"] = login
|
||||
credentials["password"] = self.cleaned_data["password"]
|
||||
return credentials
|
||||
|
||||
def clean_login(self):
|
||||
login = self.cleaned_data["login"]
|
||||
return login.strip()
|
||||
|
||||
def _is_login_email(self, login):
|
||||
try:
|
||||
validators.validate_email(login)
|
||||
ret = True
|
||||
except exceptions.ValidationError:
|
||||
ret = False
|
||||
return ret
|
||||
|
||||
def clean(self):
|
||||
super(LoginForm, self).clean()
|
||||
if self._errors:
|
||||
return
|
||||
credentials = self.user_credentials()
|
||||
adapter = get_adapter(self.request)
|
||||
user = adapter.authenticate(self.request, **credentials)
|
||||
if user:
|
||||
login = Login(user=user, email=credentials.get("email"))
|
||||
if flows.login.is_login_rate_limited(context.request, login):
|
||||
raise adapter.validation_error("too_many_login_attempts")
|
||||
self._login = login
|
||||
self.user = user
|
||||
else:
|
||||
auth_method = app_settings.AUTHENTICATION_METHOD
|
||||
if auth_method == app_settings.AuthenticationMethod.USERNAME_EMAIL:
|
||||
login = self.cleaned_data["login"]
|
||||
if self._is_login_email(login):
|
||||
auth_method = app_settings.AuthenticationMethod.EMAIL
|
||||
else:
|
||||
auth_method = app_settings.AuthenticationMethod.USERNAME
|
||||
raise adapter.validation_error("%s_password_mismatch" % auth_method.value)
|
||||
return self.cleaned_data
|
||||
|
||||
def login(self, request, redirect_url=None):
|
||||
credentials = self.user_credentials()
|
||||
login = self._login
|
||||
login.redirect_url = redirect_url
|
||||
ret = flows.login.perform_password_login(request, credentials, login)
|
||||
remember = app_settings.SESSION_REMEMBER
|
||||
if remember is None:
|
||||
remember = self.cleaned_data["remember"]
|
||||
if remember:
|
||||
request.session.set_expiry(app_settings.SESSION_COOKIE_AGE)
|
||||
else:
|
||||
request.session.set_expiry(0)
|
||||
return ret
|
||||
|
||||
|
||||
class _DummyCustomSignupForm(forms.Form):
|
||||
def signup(self, request, user):
|
||||
"""
|
||||
Invoked at signup time to complete the signup of the user.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def _base_signup_form_class():
|
||||
"""
|
||||
Currently, we inherit from the custom form, if any. This is all
|
||||
not very elegant, though it serves a purpose:
|
||||
|
||||
- There are two signup forms: one for local accounts, and one for
|
||||
social accounts
|
||||
- Both share a common base (BaseSignupForm)
|
||||
|
||||
- Given the above, how to put in a custom signup form? Which form
|
||||
would your custom form derive from, the local or the social one?
|
||||
"""
|
||||
if not app_settings.SIGNUP_FORM_CLASS:
|
||||
return _DummyCustomSignupForm
|
||||
try:
|
||||
fc_module, fc_classname = app_settings.SIGNUP_FORM_CLASS.rsplit(".", 1)
|
||||
except ValueError:
|
||||
raise exceptions.ImproperlyConfigured(
|
||||
"%s does not point to a form class" % app_settings.SIGNUP_FORM_CLASS
|
||||
)
|
||||
try:
|
||||
mod = import_module(fc_module)
|
||||
except ImportError as e:
|
||||
raise exceptions.ImproperlyConfigured(
|
||||
"Error importing form class %s:" ' "%s"' % (fc_module, e)
|
||||
)
|
||||
try:
|
||||
fc_class = getattr(mod, fc_classname)
|
||||
except AttributeError:
|
||||
raise exceptions.ImproperlyConfigured(
|
||||
'Module "%s" does not define a' ' "%s" class' % (fc_module, fc_classname)
|
||||
)
|
||||
if not hasattr(fc_class, "signup"):
|
||||
raise exceptions.ImproperlyConfigured(
|
||||
"The custom signup form must offer"
|
||||
" a `def signup(self, request, user)` method",
|
||||
)
|
||||
return fc_class
|
||||
|
||||
|
||||
class BaseSignupForm(_base_signup_form_class()): # type: ignore[misc]
|
||||
username = forms.CharField(
|
||||
label=_("Username"),
|
||||
min_length=app_settings.USERNAME_MIN_LENGTH,
|
||||
widget=forms.TextInput(
|
||||
attrs={"placeholder": _("Username"), "autocomplete": "username"}
|
||||
),
|
||||
)
|
||||
email = forms.EmailField(
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
"type": "email",
|
||||
"placeholder": _("Email address"),
|
||||
"autocomplete": "email",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
email_required = kwargs.pop("email_required", app_settings.EMAIL_REQUIRED)
|
||||
self.username_required = kwargs.pop(
|
||||
"username_required", app_settings.USERNAME_REQUIRED
|
||||
)
|
||||
self.account_already_exists = False
|
||||
super(BaseSignupForm, self).__init__(*args, **kwargs)
|
||||
username_field = self.fields["username"]
|
||||
username_field.max_length = get_username_max_length()
|
||||
username_field.validators.append(
|
||||
validators.MaxLengthValidator(username_field.max_length)
|
||||
)
|
||||
username_field.widget.attrs["maxlength"] = str(username_field.max_length)
|
||||
|
||||
default_field_order = [
|
||||
"email",
|
||||
"email2", # ignored when not present
|
||||
"username",
|
||||
"password1",
|
||||
"password2", # ignored when not present
|
||||
]
|
||||
if app_settings.SIGNUP_EMAIL_ENTER_TWICE:
|
||||
self.fields["email2"] = forms.EmailField(
|
||||
label=_("Email (again)"),
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
"type": "email",
|
||||
"placeholder": _("Email address confirmation"),
|
||||
}
|
||||
),
|
||||
)
|
||||
if email_required:
|
||||
self.fields["email"].label = gettext("Email")
|
||||
self.fields["email"].required = True
|
||||
else:
|
||||
self.fields["email"].label = gettext("Email (optional)")
|
||||
self.fields["email"].required = False
|
||||
self.fields["email"].widget.is_required = False
|
||||
if self.username_required:
|
||||
default_field_order = [
|
||||
"username",
|
||||
"email",
|
||||
"email2", # ignored when not present
|
||||
"password1",
|
||||
"password2", # ignored when not present
|
||||
]
|
||||
|
||||
if not self.username_required:
|
||||
del self.fields["username"]
|
||||
|
||||
set_form_field_order(
|
||||
self, getattr(self, "field_order", None) or default_field_order
|
||||
)
|
||||
|
||||
def clean_username(self):
|
||||
value = self.cleaned_data["username"]
|
||||
value = get_adapter().clean_username(value)
|
||||
# Note regarding preventing enumeration: if the username is already
|
||||
# taken, but the email address is not, we would still leak information
|
||||
# if we were to send an email to that email address stating that the
|
||||
# username is already in use.
|
||||
return value
|
||||
|
||||
def clean_email(self):
|
||||
value = self.cleaned_data["email"].lower()
|
||||
value = get_adapter().clean_email(value)
|
||||
if value and app_settings.UNIQUE_EMAIL:
|
||||
value = self.validate_unique_email(value)
|
||||
return value
|
||||
|
||||
def clean_email2(self):
|
||||
value = self.cleaned_data["email2"].lower()
|
||||
return value
|
||||
|
||||
def validate_unique_email(self, value):
|
||||
adapter = get_adapter()
|
||||
assessment = flows.manage_email.assess_unique_email(value)
|
||||
if assessment is True:
|
||||
# All good.
|
||||
pass
|
||||
elif assessment is False:
|
||||
# Fail right away.
|
||||
raise adapter.validation_error("email_taken")
|
||||
else:
|
||||
assert assessment is None
|
||||
self.account_already_exists = True
|
||||
return adapter.validate_unique_email(value)
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super(BaseSignupForm, self).clean()
|
||||
if app_settings.SIGNUP_EMAIL_ENTER_TWICE:
|
||||
email = cleaned_data.get("email")
|
||||
email2 = cleaned_data.get("email2")
|
||||
if (email and email2) and email != email2:
|
||||
self.add_error("email2", _("You must type the same email each time."))
|
||||
return cleaned_data
|
||||
|
||||
def custom_signup(self, request, user):
|
||||
self.signup(request, user)
|
||||
|
||||
def try_save(self, request):
|
||||
"""Try and save the user. This can fail in case of a conflict on the
|
||||
email address, in that case we will send an "account already exists"
|
||||
email and return a standard "email verification sent" response.
|
||||
"""
|
||||
if self.account_already_exists:
|
||||
# Don't create a new account, only send an email informing the user
|
||||
# that (s)he already has one...
|
||||
email = self.cleaned_data["email"]
|
||||
resp = flows.signup.prevent_enumeration(request, email)
|
||||
user = None
|
||||
# Fake a login stage.
|
||||
request.session[LOGIN_SESSION_KEY] = EmailVerificationStage.key
|
||||
else:
|
||||
user = self.save(request)
|
||||
resp = None
|
||||
return user, resp
|
||||
|
||||
def save(self, request):
|
||||
email = self.cleaned_data.get("email")
|
||||
if self.account_already_exists:
|
||||
raise ValueError(email)
|
||||
adapter = get_adapter()
|
||||
user = adapter.new_user(request)
|
||||
adapter.save_user(request, user, self)
|
||||
self.custom_signup(request, user)
|
||||
# TODO: Move into adapter `save_user` ?
|
||||
setup_user_email(request, user, [EmailAddress(email=email)] if email else [])
|
||||
return user
|
||||
|
||||
|
||||
class SignupForm(BaseSignupForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.by_passkey = kwargs.pop("by_passkey", False)
|
||||
super(SignupForm, self).__init__(*args, **kwargs)
|
||||
if not self.by_passkey:
|
||||
self.fields["password1"] = PasswordField(
|
||||
label=_("Password"),
|
||||
autocomplete="new-password",
|
||||
help_text=password_validation.password_validators_help_text_html(),
|
||||
)
|
||||
if app_settings.SIGNUP_PASSWORD_ENTER_TWICE:
|
||||
self.fields["password2"] = PasswordField(
|
||||
label=_("Password (again)"), autocomplete="new-password"
|
||||
)
|
||||
|
||||
if hasattr(self, "field_order"):
|
||||
set_form_field_order(self, self.field_order)
|
||||
|
||||
honeypot_field_name = app_settings.SIGNUP_FORM_HONEYPOT_FIELD
|
||||
if honeypot_field_name:
|
||||
self.fields[honeypot_field_name] = forms.CharField(
|
||||
required=False,
|
||||
label="",
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
"style": "position: absolute; right: -99999px;",
|
||||
"tabindex": "-1",
|
||||
"autocomplete": "nope",
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
def try_save(self, request):
|
||||
"""
|
||||
override of parent class method that adds additional catching
|
||||
of a potential bot filling out the honeypot field and returns a
|
||||
'fake' email verification response if honeypot was filled out
|
||||
"""
|
||||
honeypot_field_name = app_settings.SIGNUP_FORM_HONEYPOT_FIELD
|
||||
if honeypot_field_name:
|
||||
if self.cleaned_data[honeypot_field_name]:
|
||||
user = None
|
||||
adapter = get_adapter()
|
||||
# honeypot fields work best when you do not report to the bot
|
||||
# that anything went wrong. So we return a fake email verification
|
||||
# sent response but without creating a user
|
||||
resp = adapter.respond_email_verification_sent(request, None)
|
||||
return user, resp
|
||||
|
||||
return super().try_save(request)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# `password` cannot be of type `SetPasswordField`, as we don't
|
||||
# have a `User` yet. So, let's populate a dummy user to be used
|
||||
# for password validation.
|
||||
User = get_user_model()
|
||||
dummy_user = User()
|
||||
user_username(dummy_user, self.cleaned_data.get("username"))
|
||||
user_email(dummy_user, self.cleaned_data.get("email"))
|
||||
password = self.cleaned_data.get("password1")
|
||||
if password:
|
||||
try:
|
||||
get_adapter().clean_password(password, user=dummy_user)
|
||||
except forms.ValidationError as e:
|
||||
self.add_error("password1", e)
|
||||
|
||||
if (
|
||||
app_settings.SIGNUP_PASSWORD_ENTER_TWICE
|
||||
and "password1" in self.cleaned_data
|
||||
and "password2" in self.cleaned_data
|
||||
):
|
||||
if self.cleaned_data["password1"] != self.cleaned_data["password2"]:
|
||||
self.add_error(
|
||||
"password2",
|
||||
_("You must type the same password each time."),
|
||||
)
|
||||
return self.cleaned_data
|
||||
|
||||
|
||||
class UserForm(forms.Form):
|
||||
def __init__(self, user=None, *args, **kwargs):
|
||||
self.user = user
|
||||
super(UserForm, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class AddEmailForm(UserForm):
|
||||
email = forms.EmailField(
|
||||
label=_("Email"),
|
||||
required=True,
|
||||
widget=forms.TextInput(
|
||||
attrs={"type": "email", "placeholder": _("Email address")}
|
||||
),
|
||||
)
|
||||
|
||||
def clean_email(self):
|
||||
from allauth.account import signals
|
||||
|
||||
value = self.cleaned_data["email"].lower()
|
||||
adapter = get_adapter()
|
||||
value = adapter.clean_email(value)
|
||||
users = filter_users_by_email(value)
|
||||
on_this_account = [u for u in users if u.pk == self.user.pk]
|
||||
on_diff_account = [u for u in users if u.pk != self.user.pk]
|
||||
|
||||
if on_this_account:
|
||||
raise adapter.validation_error("duplicate_email")
|
||||
if (
|
||||
# Email is taken by a different account...
|
||||
on_diff_account
|
||||
# We care about not having duplicate emails
|
||||
and app_settings.UNIQUE_EMAIL
|
||||
# Enumeration prevention is turned off.
|
||||
and (not app_settings.PREVENT_ENUMERATION)
|
||||
):
|
||||
raise adapter.validation_error("email_taken")
|
||||
if not EmailAddress.objects.can_add_email(self.user):
|
||||
raise adapter.validation_error(
|
||||
"max_email_addresses", app_settings.MAX_EMAIL_ADDRESSES
|
||||
)
|
||||
|
||||
signals._add_email.send(
|
||||
sender=self.user.__class__,
|
||||
email=value,
|
||||
user=self.user,
|
||||
)
|
||||
return value
|
||||
|
||||
def save(self, request):
|
||||
if app_settings.EMAIL_VERIFICATION_BY_CODE_ENABLED:
|
||||
email_address = EmailAddress(
|
||||
user=self.user, email=self.cleaned_data["email"]
|
||||
)
|
||||
email_address.send_confirmation(request)
|
||||
return email_address
|
||||
elif app_settings.CHANGE_EMAIL:
|
||||
return EmailAddress.objects.add_new_email(
|
||||
request, self.user, self.cleaned_data["email"]
|
||||
)
|
||||
return EmailAddress.objects.add_email(
|
||||
request, self.user, self.cleaned_data["email"], confirm=True
|
||||
)
|
||||
|
||||
|
||||
class ChangePasswordForm(PasswordVerificationMixin, UserForm):
|
||||
old[PASSWORD-REMOVED](
|
||||
label=_("Current Password"), autocomplete="current-password"
|
||||
)
|
||||
password1 = SetPasswordField(label=_("New Password"))
|
||||
password2 = PasswordField(label=_("New Password (again)"))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ChangePasswordForm, self).__init__(*args, **kwargs)
|
||||
self.fields["password1"].user = self.user
|
||||
|
||||
def clean_oldpassword(self):
|
||||
if not self.user.check_password(self.cleaned_data.get("oldpassword")):
|
||||
raise get_adapter().validation_error("enter_current_password")
|
||||
return self.cleaned_data["oldpassword"]
|
||||
|
||||
def save(self):
|
||||
flows.password_change.change_password(self.user, self.cleaned_data["password1"])
|
||||
|
||||
|
||||
class SetPasswordForm(PasswordVerificationMixin, UserForm):
|
||||
password1 = SetPasswordField(label=_("Password"))
|
||||
password2 = PasswordField(label=_("Password (again)"))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SetPasswordForm, self).__init__(*args, **kwargs)
|
||||
self.fields["password1"].user = self.user
|
||||
|
||||
def save(self):
|
||||
flows.password_change.change_password(self.user, self.cleaned_data["password1"])
|
||||
|
||||
|
||||
class ResetPasswordForm(forms.Form):
|
||||
email = forms.EmailField(
|
||||
label=_("Email"),
|
||||
required=True,
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
"type": "email",
|
||||
"placeholder": _("Email address"),
|
||||
"autocomplete": "email",
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
def clean_email(self):
|
||||
email = self.cleaned_data["email"].lower()
|
||||
email = get_adapter().clean_email(email)
|
||||
self.users = filter_users_by_email(email, is_active=True, prefer_verified=True)
|
||||
if not self.users and not app_settings.PREVENT_ENUMERATION:
|
||||
raise get_adapter().validation_error("unknown_email")
|
||||
return self.cleaned_data["email"]
|
||||
|
||||
def save(self, request, **kwargs) -> str:
|
||||
email = self.cleaned_data["email"]
|
||||
if not self.users:
|
||||
flows.signup.send_unknown_account_mail(request, email)
|
||||
return email
|
||||
|
||||
adapter: DefaultAccountAdapter = get_adapter()
|
||||
token_generator = kwargs.get("token_generator", default_token_generator)
|
||||
for user in self.users:
|
||||
temp_key = token_generator.make_token(user)
|
||||
|
||||
# send the password reset email
|
||||
uid = user_pk_to_url_str(user)
|
||||
# We intentionally pass an opaque `key` on the interface here, and
|
||||
# not implementation details such as a separate `uidb36` and
|
||||
# `key. Ideally, this should have done on `urls` level as well.
|
||||
key = f"{uid}-{temp_key}"
|
||||
url = adapter.get_reset_password_from_key_url(key)
|
||||
context = {
|
||||
"user": user,
|
||||
"password_reset_url": url,
|
||||
"uid": uid,
|
||||
"key": temp_key,
|
||||
"request": request,
|
||||
}
|
||||
|
||||
if app_settings.AUTHENTICATION_METHOD != AuthenticationMethod.EMAIL:
|
||||
context["username"] = user_username(user)
|
||||
adapter.send_password_reset_mail(user, email, context)
|
||||
return email
|
||||
|
||||
|
||||
class ResetPasswordKeyForm(PasswordVerificationMixin, forms.Form):
|
||||
password1 = SetPasswordField(label=_("New Password"))
|
||||
password2 = PasswordField(label=_("New Password (again)"))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.user = kwargs.pop("user", None)
|
||||
self.temp_key = kwargs.pop("temp_key", None)
|
||||
super(ResetPasswordKeyForm, self).__init__(*args, **kwargs)
|
||||
self.fields["password1"].user = self.user
|
||||
|
||||
def save(self):
|
||||
flows.password_reset.reset_password(self.user, self.cleaned_data["password1"])
|
||||
|
||||
|
||||
class UserTokenForm(forms.Form):
|
||||
uidb36 = forms.CharField()
|
||||
key = forms.CharField()
|
||||
|
||||
reset_user = None
|
||||
token_generator = default_token_generator
|
||||
|
||||
def _get_user(self, uidb36):
|
||||
User = get_user_model()
|
||||
try:
|
||||
pk = url_str_to_user_pk(uidb36)
|
||||
return User.objects.get(pk=pk)
|
||||
except (ValueError, User.DoesNotExist):
|
||||
return None
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super(UserTokenForm, self).clean()
|
||||
|
||||
uidb36 = cleaned_data.get("uidb36", None)
|
||||
key = cleaned_data.get("key", None)
|
||||
adapter = get_adapter()
|
||||
if not key:
|
||||
raise adapter.validation_error("invalid_password_reset")
|
||||
|
||||
self.reset_user = self._get_user(uidb36)
|
||||
if self.reset_user is None or not self.token_generator.check_token(
|
||||
self.reset_user, key
|
||||
):
|
||||
raise adapter.validation_error("invalid_password_reset")
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class ReauthenticateForm(forms.Form):
|
||||
[PASSWORD-REMOVED](label=_("Password"), autocomplete="current-password")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.user = kwargs.pop("user")
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def clean_password(self):
|
||||
password = self.cleaned_data.get("password")
|
||||
if not get_adapter().reauthenticate(self.user, password):
|
||||
raise get_adapter().validation_error("incorrect_password")
|
||||
return password
|
||||
|
||||
|
||||
class RequestLoginCodeForm(forms.Form):
|
||||
email = forms.EmailField(
|
||||
widget=forms.EmailInput(
|
||||
attrs={
|
||||
"placeholder": _("Email address"),
|
||||
"autocomplete": "email",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def clean_email(self):
|
||||
adapter = get_adapter()
|
||||
email = self.cleaned_data["email"]
|
||||
if not app_settings.PREVENT_ENUMERATION:
|
||||
users = filter_users_by_email(email, is_active=True, prefer_verified=True)
|
||||
if not users:
|
||||
raise adapter.validation_error("unknown_email")
|
||||
|
||||
if not ratelimit.consume(
|
||||
context.request, action="request_login_code", key=email.lower()
|
||||
):
|
||||
raise adapter.validation_error("too_many_login_attempts")
|
||||
return email
|
||||
|
||||
|
||||
class BaseConfirmCodeForm(forms.Form):
|
||||
code = forms.CharField(
|
||||
label=_("Code"),
|
||||
widget=forms.TextInput(
|
||||
attrs={"placeholder": _("Code"), "autocomplete": "one-time-code"},
|
||||
),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.code = kwargs.pop("code")
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def clean_code(self):
|
||||
code = self.cleaned_data.get("code")
|
||||
if not flows.login_by_code.compare_code(actual=code, expected=self.code):
|
||||
raise get_adapter().validation_error("incorrect_code")
|
||||
return code
|
||||
|
||||
|
||||
class ConfirmLoginCodeForm(BaseConfirmCodeForm):
|
||||
pass
|
||||
|
||||
|
||||
class ConfirmEmailVerificationCodeForm(BaseConfirmCodeForm):
|
||||
pass
|
||||
@@ -0,0 +1,4 @@
|
||||
from allauth.account.internal import flows
|
||||
|
||||
|
||||
__all__ = ["flows"]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,37 @@
|
||||
from functools import wraps
|
||||
|
||||
from django.contrib.auth import decorators
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import reverse
|
||||
from django.views.decorators.cache import never_cache
|
||||
|
||||
from allauth.account.stages import LoginStageController
|
||||
from allauth.account.utils import get_login_redirect_url
|
||||
|
||||
|
||||
def _dummy_login_not_required(view_func):
|
||||
return view_func
|
||||
|
||||
|
||||
login_not_required = getattr(
|
||||
decorators, "login_not_required", _dummy_login_not_required
|
||||
)
|
||||
|
||||
|
||||
def login_stage_required(stage: str, redirect_urlname: str):
|
||||
def decorator(view_func):
|
||||
@never_cache
|
||||
@login_not_required
|
||||
@wraps(view_func)
|
||||
def _wrapper_view(request, *args, **kwargs):
|
||||
if request.user.is_authenticated:
|
||||
return HttpResponseRedirect(get_login_redirect_url(request))
|
||||
login_stage = LoginStageController.enter(request, stage)
|
||||
if not login_stage:
|
||||
return HttpResponseRedirect(reverse(redirect_urlname))
|
||||
request._login_stage = login_stage
|
||||
return view_func(request, *args, **kwargs)
|
||||
|
||||
return _wrapper_view
|
||||
|
||||
return decorator
|
||||
@@ -0,0 +1,26 @@
|
||||
from allauth.account.internal.flows import (
|
||||
email_verification,
|
||||
email_verification_by_code,
|
||||
login,
|
||||
login_by_code,
|
||||
logout,
|
||||
manage_email,
|
||||
password_change,
|
||||
password_reset,
|
||||
reauthentication,
|
||||
signup,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"password_reset",
|
||||
"password_change",
|
||||
"email_verification",
|
||||
"email_verification_by_code",
|
||||
"login",
|
||||
"login_by_code",
|
||||
"logout",
|
||||
"signup",
|
||||
"manage_email",
|
||||
"reauthentication",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,250 @@
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from django.contrib import messages
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.urls import reverse
|
||||
|
||||
from allauth.account import app_settings, signals
|
||||
from allauth.account.adapter import get_adapter
|
||||
from allauth.account.internal.flows.manage_email import emit_email_changed
|
||||
from allauth.account.models import EmailAddress, Login
|
||||
from allauth.core import ratelimit
|
||||
from allauth.core.exceptions import ImmediateHttpResponse
|
||||
from allauth.core.internal.httpkit import get_frontend_url
|
||||
from allauth.utils import build_absolute_uri
|
||||
|
||||
|
||||
def verify_email_indirectly(request: HttpRequest, user, email: str) -> bool:
|
||||
try:
|
||||
email_address = EmailAddress.objects.get_for_user(user, email)
|
||||
except EmailAddress.DoesNotExist:
|
||||
return False
|
||||
else:
|
||||
if not email_address.verified:
|
||||
return verify_email(request, email_address)
|
||||
return True
|
||||
|
||||
|
||||
def verify_email_and_resume(
|
||||
request: HttpRequest, verification
|
||||
) -> Tuple[Optional[EmailAddress], Optional[HttpResponse]]:
|
||||
email_address = verification.confirm(request)
|
||||
if not email_address:
|
||||
return None, None
|
||||
response = login_on_verification(request, verification)
|
||||
return email_address, response
|
||||
|
||||
|
||||
def verify_email(request: HttpRequest, email_address: EmailAddress) -> bool:
|
||||
"""
|
||||
Marks the email address as confirmed on the db
|
||||
"""
|
||||
added = not email_address.pk
|
||||
from_email_address = (
|
||||
EmailAddress.objects.filter(user_id=email_address.user_id)
|
||||
.exclude(pk=email_address.pk)
|
||||
.first()
|
||||
)
|
||||
if not email_address.set_verified(commit=False):
|
||||
get_adapter(request).add_message(
|
||||
request,
|
||||
messages.ERROR,
|
||||
"account/messages/email_confirmation_failed.txt",
|
||||
{"email": email_address.email},
|
||||
)
|
||||
return False
|
||||
email_address.set_as_primary(conditional=(not app_settings.CHANGE_EMAIL))
|
||||
email_address.save()
|
||||
if added:
|
||||
signals.email_added.send(
|
||||
sender=EmailAddress,
|
||||
request=request,
|
||||
user=request.user,
|
||||
email_address=email_address,
|
||||
)
|
||||
signals.email_confirmed.send(
|
||||
sender=EmailAddress,
|
||||
request=request,
|
||||
email_address=email_address,
|
||||
)
|
||||
if app_settings.CHANGE_EMAIL:
|
||||
for instance in EmailAddress.objects.filter(
|
||||
user_id=email_address.user_id
|
||||
).exclude(pk=email_address.pk):
|
||||
instance.remove()
|
||||
emit_email_changed(request, from_email_address, email_address)
|
||||
get_adapter(request).add_message(
|
||||
request,
|
||||
messages.SUCCESS,
|
||||
"account/messages/email_confirmed.txt",
|
||||
{"email": email_address.email},
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def get_email_verification_url(request: HttpRequest, emailconfirmation) -> str:
|
||||
"""Constructs the email confirmation (activation) url.
|
||||
|
||||
Note that if you have architected your system such that email
|
||||
confirmations are sent outside of the request context `request`
|
||||
can be `None` here.
|
||||
"""
|
||||
url = get_frontend_url(request, "account_confirm_email", key=emailconfirmation.key)
|
||||
if not url:
|
||||
url = reverse("account_confirm_email", args=[emailconfirmation.key])
|
||||
url = build_absolute_uri(request, url)
|
||||
return url
|
||||
|
||||
|
||||
def login_on_verification(request, verification) -> Optional[HttpResponse]:
|
||||
"""Simply logging in the user may become a security issue. If you
|
||||
do not take proper care (e.g. don't purge used email
|
||||
confirmations), a malicious person that got hold of the link
|
||||
will be able to login over and over again and the user is
|
||||
unable to do anything about it. Even restoring their own mailbox
|
||||
security will not help, as the links will still work. For
|
||||
password reset this is different, this mechanism works only as
|
||||
long as the attacker has access to the mailbox. If they no
|
||||
longer has access they cannot issue a password request and
|
||||
intercept it. Furthermore, all places where the links are
|
||||
listed (log files, but even Google Analytics) all of a sudden
|
||||
need to be secured. Purging the email confirmation once
|
||||
confirmed changes the behavior -- users will not be able to
|
||||
repeatedly confirm (in case they forgot that they already
|
||||
clicked the mail).
|
||||
|
||||
All in all, we only login on verification when the user that is in the
|
||||
process of signing up is present in the session to avoid all of the above.
|
||||
This may not 100% work in case the user closes the browser (and the session
|
||||
gets lost), but at least we're secure.
|
||||
"""
|
||||
from allauth.account.stages import (
|
||||
EmailVerificationStage,
|
||||
LoginStageController,
|
||||
)
|
||||
|
||||
if not app_settings.LOGIN_ON_EMAIL_CONFIRMATION:
|
||||
return None
|
||||
if request.user.is_authenticated:
|
||||
return None
|
||||
stage = LoginStageController.enter(request, EmailVerificationStage.key)
|
||||
if not stage or not stage.login.user:
|
||||
return None
|
||||
if stage.login.user.pk != verification.email_address.user_id:
|
||||
return None
|
||||
return stage.exit()
|
||||
|
||||
|
||||
def consume_email_verification_rate_limit(
|
||||
request: HttpRequest, email: str, dry_run: bool = False
|
||||
) -> bool:
|
||||
return ratelimit.consume(
|
||||
request, action="confirm_email", key=email.lower(), dry_run=dry_run
|
||||
)
|
||||
|
||||
|
||||
def handle_verification_email_rate_limit(request, email: str) -> bool:
|
||||
"""
|
||||
For email verification by link, it is not an issue if the user runs into rate
|
||||
limits. The reason is that the link is session independent. Therefore, if the
|
||||
user hits rate limits, we can just silently skip sending additional
|
||||
verification emails, as the previous emails that were already sent still
|
||||
contain valid links. This is different from email verification by code. Here,
|
||||
the session contains a specific code, meaning, silently skipping new
|
||||
verification emails is not an option, and we must hard fail (429) instead. The
|
||||
latter was missing, fixed.
|
||||
"""
|
||||
rl_ok = consume_email_verification_rate_limit(request, email)
|
||||
if not rl_ok and app_settings.EMAIL_VERIFICATION_BY_CODE_ENABLED:
|
||||
raise ImmediateHttpResponse(ratelimit.respond_429(request))
|
||||
return rl_ok
|
||||
|
||||
|
||||
def send_verification_email(request, user, signup=False, email=None) -> bool:
|
||||
"""
|
||||
Email verification mails are sent:
|
||||
a) Explicitly: when a user signs up
|
||||
b) Implicitly: when a user attempts to log in using an unverified
|
||||
email while EMAIL_VERIFICATION is mandatory.
|
||||
|
||||
Especially in case of b), we want to limit the number of mails
|
||||
sent (consider a user retrying a few times), which is why there is
|
||||
a cooldown period before sending a new mail. This cooldown period
|
||||
can be configured in ACCOUNT_EMAIL_CONFIRMATION_COOLDOWN setting.
|
||||
|
||||
TODO: This code is doing way too much. Looking up EmailAddress, creating
|
||||
if not present, etc. To be refactored.
|
||||
"""
|
||||
from allauth.account.utils import user_email
|
||||
|
||||
adapter = get_adapter()
|
||||
sent = False
|
||||
email_address = None
|
||||
if not email:
|
||||
email = user_email(user)
|
||||
if not email:
|
||||
email_address = (
|
||||
EmailAddress.objects.filter(user=user).order_by("verified", "pk").first()
|
||||
)
|
||||
if email_address:
|
||||
email = email_address.email
|
||||
|
||||
if email:
|
||||
if email_address is None:
|
||||
try:
|
||||
email_address = EmailAddress.objects.get_for_user(user, email)
|
||||
except EmailAddress.DoesNotExist:
|
||||
pass
|
||||
if email_address is not None:
|
||||
if not email_address.verified:
|
||||
send_email = handle_verification_email_rate_limit(
|
||||
request, email_address.email
|
||||
)
|
||||
if send_email:
|
||||
send_email = adapter.should_send_confirmation_mail(
|
||||
request, email_address, signup
|
||||
)
|
||||
if send_email:
|
||||
email_address.send_confirmation(request, signup=signup)
|
||||
sent = True
|
||||
else:
|
||||
send_email = False
|
||||
else:
|
||||
send_email = True
|
||||
email_address = EmailAddress.objects.add_email(
|
||||
request, user, email, signup=signup, confirm=True
|
||||
)
|
||||
sent = True
|
||||
assert email_address
|
||||
# At this point, if we were supposed to send an email we have sent it.
|
||||
if send_email:
|
||||
adapter.add_message(
|
||||
request,
|
||||
messages.INFO,
|
||||
"account/messages/email_confirmation_sent.txt",
|
||||
{"email": email, "login": not signup, "signup": signup},
|
||||
)
|
||||
return sent
|
||||
|
||||
|
||||
def is_verification_rate_limited(request: HttpRequest, login: Login) -> bool:
|
||||
"""
|
||||
Returns whether or not the email verification is *hard* rate limited.
|
||||
Hard, meaning, it would be blocking login (verification by code, not link).
|
||||
"""
|
||||
if (
|
||||
(not login.email)
|
||||
or (not app_settings.EMAIL_VERIFICATION_BY_CODE_ENABLED)
|
||||
or login.email_verification != app_settings.EmailVerificationMethod.MANDATORY
|
||||
):
|
||||
return False
|
||||
try:
|
||||
email_address = EmailAddress.objects.get_for_user(login.user, login.email)
|
||||
if not email_address.verified:
|
||||
if not consume_email_verification_rate_limit(
|
||||
request, login.email, dry_run=True
|
||||
):
|
||||
return True
|
||||
except EmailAddress.DoesNotExist:
|
||||
pass
|
||||
return False
|
||||
@@ -0,0 +1,112 @@
|
||||
import time
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.http import HttpRequest
|
||||
|
||||
from allauth.account import app_settings
|
||||
from allauth.account.adapter import get_adapter
|
||||
from allauth.account.internal.flows.login_by_code import compare_code
|
||||
from allauth.account.internal.stagekit import clear_login
|
||||
from allauth.account.models import EmailAddress, EmailConfirmationMixin
|
||||
from allauth.core import context
|
||||
|
||||
|
||||
EMAIL_VERIFICATION_CODE_SESSION_KEY = "account_email_verification_code"
|
||||
|
||||
|
||||
class EmailVerificationModel(EmailConfirmationMixin):
|
||||
def __init__(self, email_address: EmailAddress, key: Optional[str] = None):
|
||||
self.email_address = email_address
|
||||
if not key:
|
||||
key = request_email_verification_code(
|
||||
context.request, user=email_address.user, email=email_address.email
|
||||
)
|
||||
self.key = key
|
||||
|
||||
@classmethod
|
||||
def create(cls, email_address: EmailAddress):
|
||||
return EmailVerificationModel(email_address)
|
||||
|
||||
@classmethod
|
||||
def from_key(cls, key):
|
||||
verification, _ = get_pending_verification(context.request, peek=True)
|
||||
if not verification or not compare_code(actual=key, expected=verification.key):
|
||||
return None
|
||||
return verification
|
||||
|
||||
def key_expired(self):
|
||||
return False
|
||||
|
||||
|
||||
def clear_state(request):
|
||||
request.session.pop(EMAIL_VERIFICATION_CODE_SESSION_KEY, None)
|
||||
clear_login(request)
|
||||
|
||||
|
||||
def request_email_verification_code(
|
||||
request: HttpRequest,
|
||||
user,
|
||||
email: str,
|
||||
) -> str:
|
||||
code = ""
|
||||
pending_verification = {
|
||||
"at": time.time(),
|
||||
"failed_attempts": 0,
|
||||
"email": email,
|
||||
}
|
||||
pretend = user is None
|
||||
if not pretend:
|
||||
adapter = get_adapter()
|
||||
code = adapter.generate_email_verification_code()
|
||||
assert user._meta.pk
|
||||
pending_verification.update(
|
||||
{
|
||||
"user_id": user._meta.pk.value_to_string(user),
|
||||
"email": email,
|
||||
"code": code,
|
||||
}
|
||||
)
|
||||
request.session[EMAIL_VERIFICATION_CODE_SESSION_KEY] = pending_verification
|
||||
return code
|
||||
|
||||
|
||||
def get_pending_verification(
|
||||
request: HttpRequest, peek: bool = False
|
||||
) -> Tuple[Optional[EmailVerificationModel], Optional[Dict[str, Any]]]:
|
||||
if peek:
|
||||
data = request.session.get(EMAIL_VERIFICATION_CODE_SESSION_KEY)
|
||||
else:
|
||||
data = request.session.pop(EMAIL_VERIFICATION_CODE_SESSION_KEY, None)
|
||||
if not data:
|
||||
clear_state(request)
|
||||
return None, None
|
||||
if time.time() - data["at"] >= app_settings.EMAIL_VERIFICATION_BY_CODE_TIMEOUT:
|
||||
clear_state(request)
|
||||
return None, None
|
||||
if user_id_str := data.get("user_id"):
|
||||
user_id = get_user_model()._meta.pk.to_python(user_id_str) # type: ignore[union-attr]
|
||||
user = get_user_model().objects.get(pk=user_id)
|
||||
email = data["email"]
|
||||
try:
|
||||
email_address = EmailAddress.objects.get_for_user(user, email)
|
||||
except EmailAddress.DoesNotExist:
|
||||
email_address = EmailAddress(user=user, email=email)
|
||||
verification = EmailVerificationModel(email_address, key=data["code"])
|
||||
else:
|
||||
verification = None
|
||||
return verification, data
|
||||
|
||||
|
||||
def record_invalid_attempt(
|
||||
request: HttpRequest, pending_verification: Dict[str, Any]
|
||||
) -> bool:
|
||||
n = pending_verification["failed_attempts"]
|
||||
n += 1
|
||||
pending_verification["failed_attempts"] = n
|
||||
if n >= app_settings.EMAIL_VERIFICATION_BY_CODE_MAX_ATTEMPTS:
|
||||
clear_state(request)
|
||||
return False
|
||||
else:
|
||||
request.session[EMAIL_VERIFICATION_CODE_SESSION_KEY] = pending_verification
|
||||
return True
|
||||
@@ -0,0 +1,117 @@
|
||||
import time
|
||||
from typing import Any, Dict
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
|
||||
from allauth import app_settings as allauth_settings
|
||||
from allauth.account.adapter import get_adapter
|
||||
from allauth.account.models import Login
|
||||
from allauth.core.exceptions import ImmediateHttpResponse
|
||||
|
||||
|
||||
AUTHENTICATION_METHODS_SESSION_KEY = "account_authentication_methods"
|
||||
|
||||
|
||||
def record_authentication(request, method, **extra_data):
|
||||
"""Here we keep a log of all authentication methods used within the current
|
||||
session. Important to note is that having entries here does not imply that
|
||||
a user is fully signed in. For example, consider a case where a user
|
||||
authenticates using a password, but fails to complete the 2FA challenge.
|
||||
Or, a user successfully signs in into an inactive account or one that still
|
||||
needs verification. In such cases, ``request.user`` is still anonymous, yet,
|
||||
we do have an entry here.
|
||||
|
||||
Example data::
|
||||
|
||||
{'method': 'password',
|
||||
'at': 1701423602.7184925,
|
||||
'username': 'john.doe'}
|
||||
|
||||
{'method': 'socialaccount',
|
||||
'at': 1701423567.6368647,
|
||||
'provider': 'amazon',
|
||||
'uid': 'amzn1.account.K2LI23KL2LK2'}
|
||||
|
||||
{'method': 'mfa',
|
||||
'at': 1701423602.6392953,
|
||||
'id': 1,
|
||||
'type': 'totp'}
|
||||
|
||||
"""
|
||||
methods = request.session.get(AUTHENTICATION_METHODS_SESSION_KEY, [])
|
||||
data = {
|
||||
"method": method,
|
||||
"at": time.time(),
|
||||
**extra_data,
|
||||
}
|
||||
methods.append(data)
|
||||
request.session[AUTHENTICATION_METHODS_SESSION_KEY] = methods
|
||||
|
||||
|
||||
def _get_login_hook_kwargs(login: Login) -> Dict[str, Any]:
|
||||
"""
|
||||
TODO: Just break backwards compatibility and pass only `login` to
|
||||
`pre/post_login()`.
|
||||
"""
|
||||
return dict(
|
||||
email_verification=login.email_verification,
|
||||
redirect_url=login.redirect_url,
|
||||
signal_kwargs=login.signal_kwargs,
|
||||
signup=login.signup,
|
||||
email=login.email,
|
||||
)
|
||||
|
||||
|
||||
def perform_password_login(
|
||||
request: HttpRequest, credentials: Dict[str, Any], login: Login
|
||||
) -> HttpResponse:
|
||||
extra_data = {
|
||||
field: credentials.get(field)
|
||||
for field in ["email", "username"]
|
||||
if credentials.get(field)
|
||||
}
|
||||
record_authentication(request, method="password", **extra_data)
|
||||
return perform_login(request, login)
|
||||
|
||||
|
||||
def perform_login(request: HttpRequest, login: Login) -> HttpResponse:
|
||||
adapter = get_adapter()
|
||||
hook_kwargs = _get_login_hook_kwargs(login)
|
||||
response = adapter.pre_login(request, login.user, **hook_kwargs)
|
||||
if response:
|
||||
return response
|
||||
return resume_login(request, login)
|
||||
|
||||
|
||||
def resume_login(request: HttpRequest, login: Login) -> HttpResponse:
|
||||
from allauth.account.stages import LoginStageController
|
||||
|
||||
adapter = get_adapter()
|
||||
ctrl = LoginStageController(request, login)
|
||||
try:
|
||||
response = ctrl.handle()
|
||||
if response:
|
||||
return response
|
||||
adapter.login(request, login.user)
|
||||
hook_kwargs = _get_login_hook_kwargs(login)
|
||||
response = adapter.post_login(request, login.user, **hook_kwargs)
|
||||
if response:
|
||||
return response
|
||||
except ImmediateHttpResponse as e:
|
||||
if allauth_settings.HEADLESS_ENABLED:
|
||||
from allauth.headless.internal.restkit.response import APIResponse
|
||||
|
||||
if isinstance(e.response, APIResponse):
|
||||
raise e
|
||||
response = e.response
|
||||
return response
|
||||
|
||||
|
||||
def is_login_rate_limited(request, login: Login) -> bool:
|
||||
from allauth.account.internal.flows.email_verification import (
|
||||
is_verification_rate_limited,
|
||||
)
|
||||
|
||||
if is_verification_rate_limited(request, login):
|
||||
return True
|
||||
return False
|
||||
@@ -0,0 +1,133 @@
|
||||
import time
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AbstractBaseUser
|
||||
from django.http import HttpRequest
|
||||
|
||||
from allauth.account import app_settings
|
||||
from allauth.account.adapter import get_adapter
|
||||
from allauth.account.internal.flows.email_verification import (
|
||||
verify_email_indirectly,
|
||||
)
|
||||
from allauth.account.internal.flows.login import (
|
||||
perform_login,
|
||||
record_authentication,
|
||||
)
|
||||
from allauth.account.internal.flows.signup import send_unknown_account_mail
|
||||
from allauth.account.internal.stagekit import clear_login, stash_login
|
||||
from allauth.account.models import Login
|
||||
|
||||
|
||||
LOGIN_CODE_STATE_KEY = "login_code"
|
||||
|
||||
|
||||
def request_login_code(
|
||||
request: HttpRequest, email: str, login: Optional[Login] = None
|
||||
) -> None:
|
||||
from allauth.account.utils import filter_users_by_email
|
||||
|
||||
initiated_by_user = login is None
|
||||
adapter = get_adapter()
|
||||
users = filter_users_by_email(email, is_active=True, prefer_verified=True)
|
||||
pending_login = {
|
||||
"at": time.time(),
|
||||
"email": email,
|
||||
"failed_attempts": 0,
|
||||
"initiated_by_user": initiated_by_user,
|
||||
}
|
||||
if not users:
|
||||
user = None
|
||||
send_unknown_account_mail(request, email)
|
||||
else:
|
||||
user = users[0]
|
||||
code = adapter.generate_login_code()
|
||||
context = {
|
||||
"request": request,
|
||||
"code": code,
|
||||
}
|
||||
adapter.send_mail("account/email/login_code", email, context)
|
||||
pending_login.update(
|
||||
{"code": code, "user_id": user._meta.pk.value_to_string(user)}
|
||||
)
|
||||
adapter.add_message(
|
||||
request,
|
||||
messages.SUCCESS,
|
||||
"account/messages/login_code_sent.txt",
|
||||
{"email": email},
|
||||
)
|
||||
if initiated_by_user:
|
||||
login = Login(user=user, email=email)
|
||||
login.state["stages"] = {"current": "login_by_code"}
|
||||
assert login
|
||||
login.state[LOGIN_CODE_STATE_KEY] = pending_login
|
||||
if initiated_by_user:
|
||||
stash_login(request, login)
|
||||
|
||||
|
||||
def get_pending_login(
|
||||
request, login: Login, peek: bool = False
|
||||
) -> Tuple[Optional[AbstractBaseUser], Optional[Dict[str, Any]]]:
|
||||
if peek:
|
||||
data = login.state.get(LOGIN_CODE_STATE_KEY)
|
||||
else:
|
||||
data = login.state.pop(LOGIN_CODE_STATE_KEY, None)
|
||||
if not data:
|
||||
return None, None
|
||||
if time.time() - data["at"] >= app_settings.LOGIN_BY_CODE_TIMEOUT:
|
||||
login.state.pop(LOGIN_CODE_STATE_KEY, None)
|
||||
clear_login(request)
|
||||
return None, None
|
||||
user_id_str = data.get("user_id")
|
||||
user = None
|
||||
if user_id_str:
|
||||
user_id = get_user_model()._meta.pk.to_python(user_id_str) # type: ignore[union-attr]
|
||||
user = get_user_model().objects.get(pk=user_id)
|
||||
return user, data
|
||||
|
||||
|
||||
def record_invalid_attempt(request, login: Login) -> bool:
|
||||
from allauth.account.internal.stagekit import stash_login, unstash_login
|
||||
|
||||
pending_login = login.state[LOGIN_CODE_STATE_KEY]
|
||||
n = pending_login["failed_attempts"]
|
||||
n += 1
|
||||
pending_login["failed_attempts"] = n
|
||||
if n >= app_settings.LOGIN_BY_CODE_MAX_ATTEMPTS:
|
||||
unstash_login(request)
|
||||
return False
|
||||
else:
|
||||
login.state[LOGIN_CODE_STATE_KEY] = pending_login
|
||||
stash_login(request, login)
|
||||
return True
|
||||
|
||||
|
||||
def perform_login_by_code(
|
||||
request: HttpRequest,
|
||||
stage,
|
||||
redirect_url: Optional[str],
|
||||
):
|
||||
state = stage.login.state[LOGIN_CODE_STATE_KEY]
|
||||
email = state["email"]
|
||||
user = stage.login.user
|
||||
record_authentication(request, method="code", email=email)
|
||||
verify_email_indirectly(request, user, email)
|
||||
if state["initiated_by_user"]:
|
||||
# Just requesting a login code does is not considered to be a real login,
|
||||
# yet, is needed in order to make the stage machinery work. Now that we've
|
||||
# completed the code, let's start a real login.
|
||||
login = Login(
|
||||
user=user,
|
||||
redirect_url=redirect_url,
|
||||
email=email,
|
||||
)
|
||||
return perform_login(request, login)
|
||||
else:
|
||||
return stage.exit()
|
||||
|
||||
|
||||
def compare_code(*, actual, expected) -> bool:
|
||||
actual = actual.replace(" ", "").lower()
|
||||
expected = expected.replace(" ", "").lower()
|
||||
return expected and actual == expected
|
||||
@@ -0,0 +1,16 @@
|
||||
from django.contrib import messages
|
||||
from django.http import HttpRequest
|
||||
|
||||
from allauth.account.internal.stagekit import clear_login
|
||||
|
||||
|
||||
def logout(request: HttpRequest) -> None:
|
||||
from allauth.account.adapter import get_adapter
|
||||
|
||||
if request.user.is_authenticated:
|
||||
adapter = get_adapter()
|
||||
adapter.add_message(
|
||||
request, messages.SUCCESS, "account/messages/logged_out.txt"
|
||||
)
|
||||
adapter.logout(request)
|
||||
clear_login(request)
|
||||
@@ -0,0 +1,173 @@
|
||||
from typing import Optional
|
||||
|
||||
from django.contrib import messages
|
||||
from django.http import HttpRequest
|
||||
|
||||
from allauth.account import app_settings, signals
|
||||
from allauth.account.adapter import get_adapter
|
||||
from allauth.account.internal.flows.reauthentication import (
|
||||
raise_if_reauthentication_required,
|
||||
)
|
||||
from allauth.account.models import EmailAddress
|
||||
|
||||
|
||||
def can_delete_email(email_address: EmailAddress) -> bool:
|
||||
adapter = get_adapter()
|
||||
return adapter.can_delete_email(email_address)
|
||||
|
||||
|
||||
def delete_email(request: HttpRequest, email_address: EmailAddress) -> bool:
|
||||
if app_settings.REAUTHENTICATION_REQUIRED:
|
||||
raise_if_reauthentication_required(request)
|
||||
|
||||
success = False
|
||||
adapter = get_adapter()
|
||||
if not can_delete_email(email_address):
|
||||
adapter.add_message(
|
||||
request,
|
||||
messages.ERROR,
|
||||
"account/messages/cannot_delete_primary_email.txt",
|
||||
{"email": email_address.email},
|
||||
)
|
||||
else:
|
||||
email_address.remove()
|
||||
signals.email_removed.send(
|
||||
sender=EmailAddress,
|
||||
request=request,
|
||||
user=request.user,
|
||||
email_address=email_address,
|
||||
)
|
||||
adapter.add_message(
|
||||
request,
|
||||
messages.SUCCESS,
|
||||
"account/messages/email_deleted.txt",
|
||||
{"email": email_address.email},
|
||||
)
|
||||
adapter.send_notification_mail(
|
||||
"account/email/email_deleted",
|
||||
request.user,
|
||||
{"deleted_email": email_address.email},
|
||||
)
|
||||
success = True
|
||||
return success
|
||||
|
||||
|
||||
def add_email(request: HttpRequest, form):
|
||||
if app_settings.REAUTHENTICATION_REQUIRED:
|
||||
raise_if_reauthentication_required(request)
|
||||
|
||||
email_address = form.save(request)
|
||||
adapter = get_adapter(request)
|
||||
adapter.add_message(
|
||||
request,
|
||||
messages.INFO,
|
||||
"account/messages/email_confirmation_sent.txt",
|
||||
{"email": form.cleaned_data["email"]},
|
||||
)
|
||||
if email_address.pk:
|
||||
signals.email_added.send(
|
||||
sender=EmailAddress,
|
||||
request=request,
|
||||
user=request.user,
|
||||
email_address=email_address,
|
||||
)
|
||||
|
||||
|
||||
def can_mark_as_primary(email_address: EmailAddress):
|
||||
return (
|
||||
email_address.verified
|
||||
or not EmailAddress.objects.filter(
|
||||
user=email_address.user, verified=True
|
||||
).exists()
|
||||
)
|
||||
|
||||
|
||||
def mark_as_primary(request: HttpRequest, email_address: EmailAddress):
|
||||
if app_settings.REAUTHENTICATION_REQUIRED:
|
||||
raise_if_reauthentication_required(request)
|
||||
|
||||
# Not primary=True -- Slightly different variation, don't
|
||||
# require verified unless moving from a verified
|
||||
# address. Ignore constraint if previous primary email
|
||||
# address is not verified.
|
||||
success = False
|
||||
if not can_mark_as_primary(email_address):
|
||||
get_adapter().add_message(
|
||||
request,
|
||||
messages.ERROR,
|
||||
"account/messages/unverified_primary_email.txt",
|
||||
)
|
||||
else:
|
||||
assert request.user.is_authenticated
|
||||
from_email_address = EmailAddress.objects.filter(
|
||||
user=request.user, primary=True
|
||||
).first()
|
||||
email_address.set_as_primary()
|
||||
adapter = get_adapter()
|
||||
adapter.add_message(
|
||||
request,
|
||||
messages.SUCCESS,
|
||||
"account/messages/primary_email_set.txt",
|
||||
)
|
||||
emit_email_changed(request, from_email_address, email_address)
|
||||
success = True
|
||||
return success
|
||||
|
||||
|
||||
def emit_email_changed(request, from_email_address, to_email_address) -> None:
|
||||
user = to_email_address.user
|
||||
signals.email_changed.send(
|
||||
sender=EmailAddress,
|
||||
request=request,
|
||||
user=user,
|
||||
from_email_address=from_email_address,
|
||||
to_email_address=to_email_address,
|
||||
)
|
||||
if from_email_address:
|
||||
get_adapter().send_notification_mail(
|
||||
"account/email/email_changed",
|
||||
user,
|
||||
context={
|
||||
"from_email": from_email_address.email,
|
||||
"to_email": to_email_address.email,
|
||||
},
|
||||
email=from_email_address.email,
|
||||
)
|
||||
|
||||
|
||||
def assess_unique_email(email) -> Optional[bool]:
|
||||
"""
|
||||
True -- email is unique
|
||||
False -- email is already in use
|
||||
None -- email is in use, but we should hide that using email verification.
|
||||
"""
|
||||
from allauth.account.utils import filter_users_by_email
|
||||
|
||||
if not filter_users_by_email(email):
|
||||
# All good.
|
||||
return True
|
||||
elif not app_settings.PREVENT_ENUMERATION:
|
||||
# Fail right away.
|
||||
return False
|
||||
elif (
|
||||
app_settings.EMAIL_VERIFICATION
|
||||
== app_settings.EmailVerificationMethod.MANDATORY
|
||||
):
|
||||
# In case of mandatory verification and enumeration prevention,
|
||||
# we can avoid creating a new account with the same (unverified)
|
||||
# email address, because we are going to send an email anyway.
|
||||
assert app_settings.PREVENT_ENUMERATION
|
||||
return None
|
||||
elif app_settings.PREVENT_ENUMERATION == "strict":
|
||||
# We're going to be strict on enumeration prevention, and allow for
|
||||
# this email address to pass even though it already exists. In this
|
||||
# scenario, you can signup multiple times using the same email
|
||||
# address resulting in multiple accounts with an unverified email.
|
||||
return True
|
||||
else:
|
||||
assert app_settings.PREVENT_ENUMERATION is True
|
||||
# Conflict. We're supposed to prevent enumeration, but we can't
|
||||
# because that means letting the user in, while emails are required
|
||||
# to be unique. In this case, uniqueness takes precedence over
|
||||
# enumeration prevention.
|
||||
return False
|
||||
@@ -0,0 +1,55 @@
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import update_session_auth_hash
|
||||
from django.contrib.auth.models import AbstractBaseUser
|
||||
from django.http import HttpRequest
|
||||
|
||||
from allauth.account import app_settings, signals
|
||||
from allauth.account.adapter import get_adapter
|
||||
from allauth.account.internal.flows.logout import logout
|
||||
|
||||
|
||||
def change_password(user: AbstractBaseUser, password: str) -> None:
|
||||
get_adapter().set_password(user, password)
|
||||
|
||||
|
||||
def finalize_password_change(request: HttpRequest, user: AbstractBaseUser) -> bool:
|
||||
logged_out = logout_on_password_change(request, user)
|
||||
adapter = get_adapter(request)
|
||||
adapter.add_message(
|
||||
request,
|
||||
messages.SUCCESS,
|
||||
"account/messages/password_changed.txt",
|
||||
)
|
||||
adapter.send_notification_mail("account/email/password_changed", user)
|
||||
signals.password_changed.send(
|
||||
sender=user.__class__,
|
||||
request=request,
|
||||
user=user,
|
||||
)
|
||||
return logged_out
|
||||
|
||||
|
||||
def finalize_password_set(request: HttpRequest, user: AbstractBaseUser) -> bool:
|
||||
logged_out = logout_on_password_change(request, user)
|
||||
adapter = get_adapter(request)
|
||||
adapter.add_message(request, messages.SUCCESS, "account/messages/password_set.txt")
|
||||
adapter.send_notification_mail("account/email/password_set", user)
|
||||
signals.password_set.send(
|
||||
sender=user.__class__,
|
||||
request=request,
|
||||
user=user,
|
||||
)
|
||||
return logged_out
|
||||
|
||||
|
||||
def logout_on_password_change(request: HttpRequest, user: AbstractBaseUser) -> bool:
|
||||
# Since it is the default behavior of Django to invalidate all sessions on
|
||||
# password change, this function actually has to preserve the session when
|
||||
# logout isn't desired.
|
||||
logged_out = True
|
||||
if not app_settings.LOGOUT_ON_PASSWORD_CHANGE:
|
||||
update_session_auth_hash(request, user)
|
||||
logged_out = False
|
||||
else:
|
||||
logout(request)
|
||||
return logged_out
|
||||
@@ -0,0 +1,62 @@
|
||||
from urllib.parse import quote
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.models import AbstractBaseUser
|
||||
from django.http import HttpRequest
|
||||
from django.urls import reverse
|
||||
|
||||
from allauth.account import signals
|
||||
from allauth.account.adapter import get_adapter
|
||||
from allauth.account.models import EmailAddress
|
||||
from allauth.core.internal.httpkit import get_frontend_url
|
||||
from allauth.utils import build_absolute_uri
|
||||
|
||||
|
||||
def reset_password(user: AbstractBaseUser, password: str) -> None:
|
||||
get_adapter().set_password(user, password)
|
||||
|
||||
|
||||
def finalize_password_reset(request: HttpRequest, user: AbstractBaseUser) -> None:
|
||||
adapter = get_adapter()
|
||||
if user:
|
||||
# User successfully reset the password, clear any
|
||||
# possible cache entries for all email addresses.
|
||||
for email in EmailAddress.objects.filter(user_id=user.pk):
|
||||
adapter._delete_login_attempts_cached_email(request, email=email.email)
|
||||
|
||||
adapter.add_message(
|
||||
request,
|
||||
messages.SUCCESS,
|
||||
"account/messages/password_changed.txt",
|
||||
)
|
||||
signals.password_reset.send(
|
||||
sender=user.__class__,
|
||||
request=request,
|
||||
user=user,
|
||||
)
|
||||
adapter.send_notification_mail("account/email/password_reset", user)
|
||||
|
||||
|
||||
def get_reset_password_url(request: HttpRequest) -> str:
|
||||
url = get_frontend_url(request, "account_reset_password")
|
||||
if not url:
|
||||
url = build_absolute_uri(request, reverse("account_reset_password"))
|
||||
return url
|
||||
|
||||
|
||||
def get_reset_password_from_key_url(request: HttpRequest, key: str) -> str:
|
||||
"""
|
||||
Method intented to be overriden in case the password reset email
|
||||
needs to point to your frontend/SPA.
|
||||
"""
|
||||
url = get_frontend_url(request, "account_reset_password_from_key", key=key)
|
||||
if not url:
|
||||
# We intentionally accept an opaque `key` on the interface here, and not
|
||||
# implementation details such as a separate `uidb36` and `key. Ideally,
|
||||
# this should have done on `urls` level as well.
|
||||
path = reverse(
|
||||
"account_reset_password_from_key", kwargs={"uidb36": "UID", "key": "KEY"}
|
||||
)
|
||||
path = path.replace("UID-KEY", quote(key))
|
||||
url = build_absolute_uri(request, path)
|
||||
return url
|
||||
@@ -0,0 +1,108 @@
|
||||
import time
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||
from django.http import HttpRequest, HttpResponseRedirect
|
||||
from django.urls import resolve, reverse
|
||||
from django.utils.http import urlencode
|
||||
|
||||
from allauth import app_settings as allauth_settings
|
||||
from allauth.account import app_settings
|
||||
from allauth.account.authentication import get_authentication_records
|
||||
from allauth.account.internal.flows.login import record_authentication
|
||||
from allauth.core.exceptions import ReauthenticationRequired
|
||||
from allauth.core.internal.httpkit import (
|
||||
deserialize_request,
|
||||
serialize_request,
|
||||
)
|
||||
from allauth.utils import import_callable
|
||||
|
||||
|
||||
STATE_SESSION_KEY = "account_reauthentication_state"
|
||||
|
||||
|
||||
def reauthenticate_by_password(request: HttpRequest) -> None:
|
||||
record_authentication(request, method="password", reauthenticated=True)
|
||||
|
||||
|
||||
def stash_and_reauthenticate(
|
||||
request: HttpRequest, state: dict, callback: str
|
||||
) -> HttpResponseRedirect:
|
||||
request.session[STATE_SESSION_KEY] = {
|
||||
"state": state,
|
||||
"callback": callback,
|
||||
}
|
||||
return HttpResponseRedirect(reverse("account_reauthenticate"))
|
||||
|
||||
|
||||
def suspend_request(request: HttpRequest, redirect_to: str) -> HttpResponseRedirect:
|
||||
path = request.get_full_path()
|
||||
if request.method == "POST":
|
||||
request.session[STATE_SESSION_KEY] = {"request": serialize_request(request)}
|
||||
return HttpResponseRedirect(
|
||||
redirect_to + "?" + urlencode({REDIRECT_FIELD_NAME: path})
|
||||
)
|
||||
|
||||
|
||||
def resume_request(request: HttpRequest) -> Optional[HttpResponseRedirect]:
|
||||
from allauth.account.utils import get_next_redirect_url
|
||||
|
||||
state = request.session.pop(STATE_SESSION_KEY, None)
|
||||
if state and "callback" in state:
|
||||
callback = import_callable(state["callback"])
|
||||
return callback(request, state["state"])
|
||||
|
||||
url = get_next_redirect_url(request, REDIRECT_FIELD_NAME)
|
||||
if not url:
|
||||
return None
|
||||
if state and "request" in state:
|
||||
suspended_request = deserialize_request(state["request"], request)
|
||||
if suspended_request.path == url:
|
||||
resolved = resolve(suspended_request.path)
|
||||
return resolved.func(suspended_request, *resolved.args, **resolved.kwargs)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
def raise_if_reauthentication_required(request: HttpRequest) -> None:
|
||||
if not did_recently_authenticate(request):
|
||||
raise ReauthenticationRequired()
|
||||
|
||||
|
||||
def did_recently_authenticate(request: HttpRequest) -> bool:
|
||||
if request.user.is_anonymous:
|
||||
return False
|
||||
if not get_reauthentication_flows(request.user):
|
||||
# TODO: This user only has social accounts attached. Now, ideally, you
|
||||
# would want to reauthenticate over at the social account provider. For
|
||||
# now, this is not implemented. Although definitely suboptimal, this
|
||||
# method is currently used for reauthentication checks over at MFA, and,
|
||||
# users that delegate the security of their account to an external
|
||||
# provider like Google typically use MFA over there anyway.
|
||||
return True
|
||||
methods = get_authentication_records(request)
|
||||
if not methods:
|
||||
return False
|
||||
authenticated_at = methods[-1]["at"]
|
||||
return time.time() - authenticated_at < app_settings.REAUTHENTICATION_TIMEOUT
|
||||
|
||||
|
||||
def get_reauthentication_flows(user) -> List[Dict]:
|
||||
ret: List[Dict] = []
|
||||
if not user.is_authenticated:
|
||||
return ret
|
||||
if user.has_usable_password():
|
||||
entry = {
|
||||
"id": "reauthenticate",
|
||||
}
|
||||
ret.append(entry)
|
||||
if allauth_settings.MFA_ENABLED:
|
||||
from allauth.mfa.models import Authenticator
|
||||
from allauth.mfa.utils import is_mfa_enabled
|
||||
|
||||
types = []
|
||||
for typ in Authenticator.Type:
|
||||
if is_mfa_enabled(user, types=[typ]):
|
||||
types.append(typ)
|
||||
if types:
|
||||
ret.append({"id": "mfa_reauthenticate", "types": types})
|
||||
return ret
|
||||
@@ -0,0 +1,72 @@
|
||||
from django.contrib import messages
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.urls import reverse
|
||||
|
||||
from allauth.account import app_settings, signals
|
||||
from allauth.account.adapter import get_adapter
|
||||
from allauth.account.internal.flows import email_verification_by_code
|
||||
from allauth.account.internal.flows.login import perform_login
|
||||
from allauth.account.models import Login
|
||||
from allauth.core.internal.httpkit import get_frontend_url
|
||||
from allauth.utils import build_absolute_uri
|
||||
|
||||
|
||||
def prevent_enumeration(request: HttpRequest, email: str) -> HttpResponse:
|
||||
adapter = get_adapter(request)
|
||||
adapter.send_account_already_exists_mail(email)
|
||||
adapter.add_message(
|
||||
request,
|
||||
messages.INFO,
|
||||
"account/messages/email_confirmation_sent.txt",
|
||||
{"email": email, "login": False, "signup": True},
|
||||
)
|
||||
if app_settings.EMAIL_VERIFICATION_BY_CODE_ENABLED:
|
||||
email_verification_by_code.request_email_verification_code(
|
||||
request, user=None, email=email
|
||||
)
|
||||
resp = adapter.respond_email_verification_sent(request, None)
|
||||
return resp
|
||||
|
||||
|
||||
def send_unknown_account_mail(request: HttpRequest, email: str) -> None:
|
||||
if not app_settings.EMAIL_UNKNOWN_ACCOUNTS:
|
||||
return None
|
||||
signup_url = get_signup_url(request)
|
||||
context = {
|
||||
"request": request,
|
||||
"signup_url": signup_url,
|
||||
}
|
||||
get_adapter().send_mail("account/email/unknown_account", email, context)
|
||||
|
||||
|
||||
def get_signup_url(request: HttpRequest) -> str:
|
||||
url = get_frontend_url(request, "account_signup")
|
||||
if not url:
|
||||
url = build_absolute_uri(request, reverse("account_signup"))
|
||||
return url
|
||||
|
||||
|
||||
def complete_signup(
|
||||
request,
|
||||
*,
|
||||
user,
|
||||
email_verification=None,
|
||||
redirect_url=None,
|
||||
signal_kwargs=None,
|
||||
by_passkey=False,
|
||||
):
|
||||
if signal_kwargs is None:
|
||||
signal_kwargs = {}
|
||||
signals.user_signed_up.send(
|
||||
sender=user.__class__, request=request, user=user, **signal_kwargs
|
||||
)
|
||||
login = Login(
|
||||
user=user,
|
||||
email_verification=email_verification,
|
||||
redirect_url=redirect_url,
|
||||
signal_kwargs=signal_kwargs,
|
||||
signup=True,
|
||||
)
|
||||
if by_passkey:
|
||||
login.state["passkey_signup"] = True
|
||||
return perform_login(request, login)
|
||||
@@ -0,0 +1,58 @@
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import reverse
|
||||
|
||||
from allauth.account import app_settings
|
||||
from allauth.account.models import Login
|
||||
from allauth.account.stages import LoginStage, LoginStageController
|
||||
|
||||
|
||||
LOGIN_SESSION_KEY = "account_login"
|
||||
|
||||
|
||||
def get_pending_stage(request) -> Optional[LoginStage]:
|
||||
stage = None
|
||||
if not request.user.is_authenticated:
|
||||
login = unstash_login(request, peek=True)
|
||||
if login:
|
||||
ctrl = LoginStageController(request, login)
|
||||
stage = ctrl.get_pending_stage()
|
||||
return stage
|
||||
|
||||
|
||||
def redirect_to_pending_stage(request, stage: LoginStage):
|
||||
if stage.urlname:
|
||||
return HttpResponseRedirect(reverse(stage.urlname))
|
||||
clear_login(request)
|
||||
return HttpResponseRedirect(reverse("account_login"))
|
||||
|
||||
|
||||
def clear_login(request):
|
||||
request.session.pop(LOGIN_SESSION_KEY, None)
|
||||
|
||||
|
||||
def unstash_login(request, peek=False):
|
||||
login = None
|
||||
if peek:
|
||||
data = request.session.get(LOGIN_SESSION_KEY)
|
||||
else:
|
||||
data = request.session.pop(LOGIN_SESSION_KEY, None)
|
||||
if isinstance(data, dict):
|
||||
try:
|
||||
login = Login.deserialize(data)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
if time.time() - login.initiated_at > app_settings.LOGIN_TIMEOUT:
|
||||
login = None
|
||||
clear_login(request)
|
||||
else:
|
||||
request._account_login_accessed = True
|
||||
return login
|
||||
|
||||
|
||||
def stash_login(request, login):
|
||||
request.session[LOGIN_SESSION_KEY] = login.serialize()
|
||||
request._account_login_accessed = True
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,46 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.models import Count
|
||||
|
||||
from allauth.account.models import EmailAddress
|
||||
from allauth.account.utils import user_email
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
for user in self.get_users_with_multiple_primary_email():
|
||||
self.unprimary_extra_primary_emails(user)
|
||||
|
||||
def get_users_with_multiple_primary_email(self):
|
||||
user_pks = []
|
||||
for email_address_dict in (
|
||||
EmailAddress.objects.filter(primary=True)
|
||||
.values("user")
|
||||
.annotate(Count("user"))
|
||||
.filter(user__count__gt=1)
|
||||
):
|
||||
user_pks.append(email_address_dict["user"])
|
||||
return get_user_model().objects.filter(pk__in=user_pks)
|
||||
|
||||
def unprimary_extra_primary_emails(self, user):
|
||||
primary_email_addresses = EmailAddress.objects.filter(user=user, primary=True)
|
||||
|
||||
for primary_email_address in primary_email_addresses:
|
||||
if primary_email_address.email == user_email(user):
|
||||
break
|
||||
else:
|
||||
# Didn't find the main email addresses and break the for loop
|
||||
print(
|
||||
"WARNING: Multiple primary without a user.email match for"
|
||||
"user pk %s; (tried: %s, using: %s)"
|
||||
) % (
|
||||
user.pk,
|
||||
", ".join(
|
||||
[email_address.email for email_address in primary_email_addresses]
|
||||
),
|
||||
primary_email_address,
|
||||
)
|
||||
|
||||
primary_email_addresses.exclude(pk=primary_email_address.pk).update(
|
||||
primary=False
|
||||
)
|
||||
136
venv/lib/python3.11/site-packages/allauth/account/managers.py
Normal file
136
venv/lib/python3.11/site-packages/allauth/account/managers.py
Normal file
@@ -0,0 +1,136 @@
|
||||
from datetime import timedelta
|
||||
from typing import Optional
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
|
||||
from . import app_settings
|
||||
|
||||
|
||||
class EmailAddressManager(models.Manager):
|
||||
def can_add_email(self, user):
|
||||
ret = True
|
||||
if app_settings.CHANGE_EMAIL:
|
||||
# We always allow adding an email in this case, regardless of
|
||||
# `MAX_EMAIL_ADDRESSES`, as adding actually adds a temporary email
|
||||
# that the user wants to change to.
|
||||
return True
|
||||
elif app_settings.MAX_EMAIL_ADDRESSES:
|
||||
count = self.filter(user=user).count()
|
||||
ret = count < app_settings.MAX_EMAIL_ADDRESSES
|
||||
return ret
|
||||
|
||||
def get_new(self, user):
|
||||
"""
|
||||
Returns the email address the user is in the process of changing to, if any.
|
||||
"""
|
||||
assert app_settings.CHANGE_EMAIL
|
||||
return (
|
||||
self.model.objects.filter(user=user, verified=False).order_by("pk").last()
|
||||
)
|
||||
|
||||
def add_new_email(self, request, user, email):
|
||||
"""
|
||||
Adds an email address the user wishes to change to, replacing his
|
||||
current email address once confirmed.
|
||||
"""
|
||||
assert app_settings.CHANGE_EMAIL
|
||||
instance = self.get_new(user)
|
||||
email = email.lower()
|
||||
if not instance:
|
||||
instance = self.model.objects.create(user=user, email=email)
|
||||
else:
|
||||
# Apparently, the user was already in the process of changing his
|
||||
# email. Reuse that temporary email address.
|
||||
instance.email = email
|
||||
instance.verified = False
|
||||
instance.primary = False
|
||||
instance.save()
|
||||
instance.send_confirmation(request)
|
||||
return instance
|
||||
|
||||
def add_email(self, request, user, email, confirm=False, signup=False):
|
||||
email = email.lower()
|
||||
email_address, created = self.get_or_create(
|
||||
user=user, email=email, defaults={"email": email}
|
||||
)
|
||||
|
||||
if created and confirm:
|
||||
email_address.send_confirmation(request, signup=signup)
|
||||
|
||||
return email_address
|
||||
|
||||
def get_verified(self, user):
|
||||
return self.filter(user=user, verified=True).order_by("-primary", "pk").first()
|
||||
|
||||
def get_primary(self, user):
|
||||
try:
|
||||
return self.get(user=user, primary=True)
|
||||
except self.model.DoesNotExist:
|
||||
return None
|
||||
|
||||
def get_primary_email(self, user) -> Optional[str]:
|
||||
from allauth.account.utils import user_email
|
||||
|
||||
primary = self.get_primary(user)
|
||||
if primary:
|
||||
email = primary.email
|
||||
else:
|
||||
email = user_email(user)
|
||||
return email
|
||||
|
||||
def get_users_for(self, email):
|
||||
# this is a list rather than a generator because we probably want to
|
||||
# do a len() on it right away
|
||||
return [
|
||||
address.user for address in self.filter(verified=True, email=email.lower())
|
||||
]
|
||||
|
||||
def fill_cache_for_user(self, user, addresses):
|
||||
"""
|
||||
In a multi-db setup, inserting records and re-reading them later
|
||||
on may result in not being able to find newly inserted
|
||||
records. Therefore, we maintain a cache for the user so that
|
||||
we can avoid database access when we need to re-read..
|
||||
"""
|
||||
user._emailaddress_cache = addresses
|
||||
|
||||
def get_for_user(self, user, email):
|
||||
cache_key = "_emailaddress_cache"
|
||||
addresses = getattr(user, cache_key, None)
|
||||
email = email.lower()
|
||||
if addresses is None:
|
||||
ret = self.get(user=user, email=email)
|
||||
# To avoid additional lookups when e.g.
|
||||
# EmailAddress.set_as_primary() starts touching self.user
|
||||
ret.user = user
|
||||
return ret
|
||||
else:
|
||||
for address in addresses:
|
||||
if address.email == email:
|
||||
return address
|
||||
raise self.model.DoesNotExist()
|
||||
|
||||
def is_verified(self, email):
|
||||
return self.filter(email=email.lower(), verified=True).exists()
|
||||
|
||||
def lookup(self, emails):
|
||||
return self.filter(email__in=[e.lower() for e in emails])
|
||||
|
||||
|
||||
class EmailConfirmationManager(models.Manager):
|
||||
def all_expired(self):
|
||||
return self.filter(self.expired_q())
|
||||
|
||||
def all_valid(self):
|
||||
return self.exclude(self.expired_q()).filter(email_address__verified=False)
|
||||
|
||||
def expired_q(self):
|
||||
sent_threshold = timezone.now() - timedelta(
|
||||
days=app_settings.EMAIL_CONFIRMATION_EXPIRE_DAYS
|
||||
)
|
||||
return Q(sent__lt=sent_threshold)
|
||||
|
||||
def delete_expired_confirmations(self):
|
||||
self.all_expired().delete()
|
||||
@@ -0,0 +1,93 @@
|
||||
import os
|
||||
from types import SimpleNamespace
|
||||
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import NoReverseMatch, reverse
|
||||
from django.utils.decorators import sync_and_async_middleware
|
||||
|
||||
from asgiref.sync import iscoroutinefunction
|
||||
|
||||
from allauth.account.adapter import get_adapter
|
||||
from allauth.account.internal import flows
|
||||
from allauth.core import context
|
||||
from allauth.core.exceptions import (
|
||||
ImmediateHttpResponse,
|
||||
ReauthenticationRequired,
|
||||
)
|
||||
|
||||
|
||||
@sync_and_async_middleware
|
||||
def AccountMiddleware(get_response):
|
||||
if iscoroutinefunction(get_response):
|
||||
|
||||
async def middleware(request):
|
||||
request.allauth = SimpleNamespace()
|
||||
with context.request_context(request):
|
||||
response = await get_response(request)
|
||||
if _should_redirect_accounts(request, response):
|
||||
response = await _aredirect_accounts(request)
|
||||
return response
|
||||
|
||||
else:
|
||||
|
||||
def middleware(request):
|
||||
request.allauth = SimpleNamespace()
|
||||
with context.request_context(request):
|
||||
response = get_response(request)
|
||||
if _should_redirect_accounts(request, response):
|
||||
response = _redirect_accounts(request)
|
||||
return response
|
||||
|
||||
def process_exception(request, exception):
|
||||
if isinstance(exception, ImmediateHttpResponse):
|
||||
return exception.response
|
||||
elif isinstance(exception, ReauthenticationRequired):
|
||||
redirect_url = reverse("account_login")
|
||||
methods = get_adapter().get_reauthentication_methods(request.user)
|
||||
if methods:
|
||||
redirect_url = methods[0]["url"]
|
||||
return flows.reauthentication.suspend_request(request, redirect_url)
|
||||
|
||||
middleware.process_exception = process_exception
|
||||
return middleware
|
||||
|
||||
|
||||
def _should_redirect_accounts(request, response) -> bool:
|
||||
"""
|
||||
URLs should be hackable. Yet, assuming allauth is included like this...
|
||||
|
||||
path("accounts/", include("allauth.urls")),
|
||||
|
||||
... and a user would attempt to navigate to /accounts/, a 404 would be
|
||||
presented. This code catches that 404, and redirects to either the email
|
||||
management overview or the login page, depending on whether or not the user
|
||||
is authenticated.
|
||||
"""
|
||||
if response.status_code != 404:
|
||||
return False
|
||||
try:
|
||||
login_path = reverse("account_login")
|
||||
email_path = reverse("account_email")
|
||||
except NoReverseMatch:
|
||||
# Project might have deviated URLs, let's keep out of the way.
|
||||
return False
|
||||
prefix = os.path.commonprefix([login_path, email_path])
|
||||
if len(prefix) <= 1 or prefix != request.path:
|
||||
return False
|
||||
# If we have a prefix that is not just '/', and that is what our request is
|
||||
# pointing to, redirect.
|
||||
return True
|
||||
|
||||
|
||||
async def _aredirect_accounts(request) -> HttpResponseRedirect:
|
||||
email_path = reverse("account_email")
|
||||
login_path = reverse("account_login")
|
||||
user = await request.auser()
|
||||
return HttpResponseRedirect(email_path if user.is_authenticated else login_path)
|
||||
|
||||
|
||||
def _redirect_accounts(request) -> HttpResponseRedirect:
|
||||
email_path = reverse("account_email")
|
||||
login_path = reverse("account_login")
|
||||
user = request.user
|
||||
return HttpResponseRedirect(email_path if user.is_authenticated else login_path)
|
||||
@@ -0,0 +1,105 @@
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
UNIQUE_EMAIL = getattr(settings, "ACCOUNT_UNIQUE_EMAIL", True)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="EmailAddress",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
verbose_name="ID",
|
||||
serialize=False,
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"email",
|
||||
models.EmailField(
|
||||
unique=UNIQUE_EMAIL,
|
||||
max_length=75,
|
||||
verbose_name="email address",
|
||||
),
|
||||
),
|
||||
(
|
||||
"verified",
|
||||
models.BooleanField(default=False, verbose_name="verified"),
|
||||
),
|
||||
(
|
||||
"primary",
|
||||
models.BooleanField(default=False, verbose_name="primary"),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
verbose_name="user",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "email address",
|
||||
"verbose_name_plural": "email addresses",
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="EmailConfirmation",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
verbose_name="ID",
|
||||
serialize=False,
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"created",
|
||||
models.DateTimeField(
|
||||
default=django.utils.timezone.now,
|
||||
verbose_name="created",
|
||||
),
|
||||
),
|
||||
("sent", models.DateTimeField(null=True, verbose_name="sent")),
|
||||
(
|
||||
"key",
|
||||
models.CharField(unique=True, max_length=64, verbose_name="key"),
|
||||
),
|
||||
(
|
||||
"email_address",
|
||||
models.ForeignKey(
|
||||
verbose_name="email address",
|
||||
to="account.EmailAddress",
|
||||
on_delete=models.CASCADE,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "email confirmation",
|
||||
"verbose_name_plural": "email confirmations",
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
]
|
||||
|
||||
if not UNIQUE_EMAIL:
|
||||
operations += [
|
||||
migrations.AlterUniqueTogether(
|
||||
name="emailaddress",
|
||||
unique_together=set([("user", "email")]),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,32 @@
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
UNIQUE_EMAIL = getattr(settings, "ACCOUNT_UNIQUE_EMAIL", True)
|
||||
EMAIL_MAX_LENGTH = getattr(settings, "ACCOUNT_EMAIL_MAX_LENGTH", 254)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("account", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="emailaddress",
|
||||
name="email",
|
||||
field=models.EmailField(
|
||||
unique=UNIQUE_EMAIL,
|
||||
max_length=EMAIL_MAX_LENGTH,
|
||||
verbose_name="email address",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
if not UNIQUE_EMAIL:
|
||||
operations += [
|
||||
migrations.AlterUniqueTogether(
|
||||
name="emailaddress",
|
||||
unique_together=set([("user", "email")]),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,30 @@
|
||||
# Generated by Django 4.2.2 on 2023-06-14 12:52
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("account", "0002_email_max_length"),
|
||||
]
|
||||
|
||||
operations = (
|
||||
[
|
||||
migrations.AlterUniqueTogether(
|
||||
name="emailaddress",
|
||||
unique_together={("user", "email")},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="emailaddress",
|
||||
constraint=models.UniqueConstraint(
|
||||
condition=models.Q(("verified", True)),
|
||||
fields=["email"],
|
||||
name="unique_verified_email",
|
||||
),
|
||||
),
|
||||
]
|
||||
if getattr(settings, "ACCOUNT_UNIQUE_EMAIL", True)
|
||||
else []
|
||||
)
|
||||
@@ -0,0 +1,21 @@
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
EMAIL_MAX_LENGTH = getattr(settings, "ACCOUNT_EMAIL_MAX_LENGTH", 254)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("account", "0003_alter_emailaddress_create_unique_verified_email"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="emailaddress",
|
||||
name="email",
|
||||
field=models.EmailField(
|
||||
max_length=EMAIL_MAX_LENGTH, verbose_name="email address"
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 4.2.4 on 2023-08-23 18:17
|
||||
|
||||
import django.db.models.functions.text
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("account", "0004_alter_emailaddress_drop_unique_email"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name="emailaddress",
|
||||
index=models.Index(
|
||||
django.db.models.functions.text.Upper("email"),
|
||||
name="account_emailaddress_upper",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,26 @@
|
||||
from django.conf import settings
|
||||
from django.db import migrations
|
||||
from django.db.models.functions import Lower
|
||||
|
||||
from allauth.account import app_settings
|
||||
|
||||
|
||||
def forwards(apps, schema_editor):
|
||||
EmailAddress = apps.get_model("account.EmailAddress")
|
||||
User = apps.get_model(settings.AUTH_USER_MODEL)
|
||||
EmailAddress.objects.all().exclude(email=Lower("email")).update(
|
||||
email=Lower("email")
|
||||
)
|
||||
email_field = app_settings.USER_MODEL_EMAIL_FIELD
|
||||
if email_field:
|
||||
User.objects.all().exclude(**{email_field: Lower(email_field)}).update(
|
||||
**{email_field: Lower(email_field)}
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("account", "0005_emailaddress_idx_upper_email"),
|
||||
]
|
||||
|
||||
operations = [migrations.RunPython(forwards)]
|
||||
@@ -0,0 +1,25 @@
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
EMAIL_MAX_LENGTH = getattr(settings, "ACCOUNT_EMAIL_MAX_LENGTH", 254)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("account", "0006_emailaddress_lower"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveIndex(
|
||||
model_name="emailaddress",
|
||||
name="account_emailaddress_upper",
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="emailaddress",
|
||||
name="email",
|
||||
field=models.EmailField(
|
||||
db_index=True, max_length=EMAIL_MAX_LENGTH, verbose_name="email address"
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,44 @@
|
||||
from django.conf import settings
|
||||
from django.db import migrations
|
||||
from django.db.models import Count
|
||||
|
||||
|
||||
def forwards(apps, schema_editor):
|
||||
EmailAddress = apps.get_model("account.EmailAddress")
|
||||
User = apps.get_model(settings.AUTH_USER_MODEL)
|
||||
user_email_field = getattr(settings, "ACCOUNT_USER_MODEL_EMAIL_FIELD", "email")
|
||||
|
||||
def get_users_with_multiple_primary_email():
|
||||
user_pks = []
|
||||
for email_address_dict in (
|
||||
EmailAddress.objects.filter(primary=True)
|
||||
.values("user")
|
||||
.annotate(Count("user"))
|
||||
.filter(user__count__gt=1)
|
||||
):
|
||||
user_pks.append(email_address_dict["user"])
|
||||
return User.objects.filter(pk__in=user_pks)
|
||||
|
||||
def unset_extra_primary_emails(user):
|
||||
qs = EmailAddress.objects.filter(user=user, primary=True)
|
||||
primary_email_addresses = list(qs)
|
||||
if not primary_email_addresses:
|
||||
return
|
||||
primary_email_address = primary_email_addresses[0]
|
||||
if user_email_field:
|
||||
for address in primary_email_addresses:
|
||||
if address.email.lower() == getattr(user, user_email_field, "").lower():
|
||||
primary_email_address = address
|
||||
break
|
||||
qs.exclude(pk=primary_email_address.pk).update(primary=False)
|
||||
|
||||
for user in get_users_with_multiple_primary_email().iterator():
|
||||
unset_extra_primary_emails(user)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("account", "0007_emailaddress_idx_email"),
|
||||
]
|
||||
|
||||
operations = [migrations.RunPython(forwards)]
|
||||
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 4.2.11 on 2024-05-09 06:09
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("account", "0008_emailaddress_unique_primary_email_fixup"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddConstraint(
|
||||
model_name="emailaddress",
|
||||
constraint=models.UniqueConstraint(
|
||||
condition=models.Q(("primary", True)),
|
||||
fields=("user", "primary"),
|
||||
name="unique_primary_email",
|
||||
),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
193
venv/lib/python3.11/site-packages/allauth/account/mixins.py
Normal file
193
venv/lib/python3.11/site-packages/allauth/account/mixins.py
Normal file
@@ -0,0 +1,193 @@
|
||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.http import HttpResponsePermanentRedirect, HttpResponseRedirect
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.html import format_html
|
||||
from django.views.decorators.cache import never_cache
|
||||
|
||||
from allauth.account import app_settings
|
||||
from allauth.account.adapter import get_adapter
|
||||
from allauth.account.internal import flows
|
||||
from allauth.account.internal.decorators import login_not_required
|
||||
from allauth.account.internal.stagekit import (
|
||||
get_pending_stage,
|
||||
redirect_to_pending_stage,
|
||||
)
|
||||
from allauth.account.utils import (
|
||||
get_login_redirect_url,
|
||||
get_next_redirect_url,
|
||||
passthrough_next_redirect_url,
|
||||
)
|
||||
from allauth.core.exceptions import ImmediateHttpResponse
|
||||
from allauth.utils import get_request_param
|
||||
|
||||
|
||||
def _ajax_response(request, response, form=None, data=None):
|
||||
adapter = get_adapter()
|
||||
if adapter.is_ajax(request):
|
||||
if isinstance(response, HttpResponseRedirect) or isinstance(
|
||||
response, HttpResponsePermanentRedirect
|
||||
):
|
||||
redirect_to = response["Location"]
|
||||
else:
|
||||
redirect_to = None
|
||||
response = adapter.ajax_response(
|
||||
request, response, form=form, data=data, redirect_to=redirect_to
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
class RedirectAuthenticatedUserMixin:
|
||||
@method_decorator(login_not_required)
|
||||
@method_decorator(never_cache)
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if app_settings.AUTHENTICATED_LOGIN_REDIRECTS:
|
||||
if request.user.is_authenticated:
|
||||
redirect_to = self.get_authenticated_redirect_url()
|
||||
response = HttpResponseRedirect(redirect_to)
|
||||
return _ajax_response(request, response)
|
||||
else:
|
||||
stage = get_pending_stage(request)
|
||||
if stage and stage.is_resumable(request):
|
||||
return redirect_to_pending_stage(request, stage)
|
||||
response = super().dispatch(request, *args, **kwargs)
|
||||
return response
|
||||
|
||||
def get_authenticated_redirect_url(self):
|
||||
redirect_field_name = self.redirect_field_name
|
||||
return get_login_redirect_url(
|
||||
self.request,
|
||||
url=self.get_success_url(),
|
||||
redirect_field_name=redirect_field_name,
|
||||
)
|
||||
|
||||
|
||||
class LogoutFunctionalityMixin:
|
||||
def logout(self):
|
||||
flows.logout.logout(self.request)
|
||||
|
||||
|
||||
class AjaxCapableProcessFormViewMixin:
|
||||
def get(self, request, *args, **kwargs):
|
||||
response = super().get(request, *args, **kwargs)
|
||||
form = self.get_form()
|
||||
return _ajax_response(
|
||||
self.request, response, form=form, data=self._get_ajax_data_if()
|
||||
)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
form_class = self.get_form_class()
|
||||
form = self.get_form(form_class)
|
||||
if form.is_valid():
|
||||
response = self.form_valid(form)
|
||||
else:
|
||||
response = self.form_invalid(form)
|
||||
return _ajax_response(
|
||||
self.request, response, form=form, data=self._get_ajax_data_if()
|
||||
)
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = getattr(self, "_cached_form", None)
|
||||
if form is None:
|
||||
form = super().get_form(form_class)
|
||||
self._cached_form = form
|
||||
return form
|
||||
|
||||
def _get_ajax_data_if(self):
|
||||
return (
|
||||
self.get_ajax_data()
|
||||
if get_adapter(self.request).is_ajax(self.request)
|
||||
else None
|
||||
)
|
||||
|
||||
def get_ajax_data(self):
|
||||
return None
|
||||
|
||||
|
||||
class CloseableSignupMixin:
|
||||
template_name_signup_closed = (
|
||||
"account/signup_closed." + app_settings.TEMPLATE_EXTENSION
|
||||
)
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
try:
|
||||
if not self.is_open():
|
||||
return self.closed()
|
||||
except ImmediateHttpResponse as e:
|
||||
return e.response
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def is_open(self):
|
||||
return get_adapter(self.request).is_open_for_signup(self.request)
|
||||
|
||||
def closed(self):
|
||||
response_kwargs = {
|
||||
"request": self.request,
|
||||
"template": self.template_name_signup_closed,
|
||||
}
|
||||
return self.response_class(**response_kwargs)
|
||||
|
||||
|
||||
class NextRedirectMixin:
|
||||
redirect_field_name = REDIRECT_FIELD_NAME
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ret = super().get_context_data(**kwargs)
|
||||
redirect_field_value = get_request_param(self.request, self.redirect_field_name)
|
||||
ret.update(
|
||||
{
|
||||
"redirect_field_name": self.redirect_field_name,
|
||||
"redirect_field_value": redirect_field_value,
|
||||
"redirect_field": (
|
||||
format_html(
|
||||
'<input type="hidden" name="{}" value="{}">',
|
||||
self.redirect_field_name,
|
||||
redirect_field_value,
|
||||
)
|
||||
if redirect_field_value
|
||||
else ""
|
||||
),
|
||||
}
|
||||
)
|
||||
return ret
|
||||
|
||||
def get_success_url(self):
|
||||
"""
|
||||
We're in a mixin, so we cannot rely on the fact that our super() has a get_success_url.
|
||||
Also, we want to check for -- in this order:
|
||||
1) The `?next=/foo`
|
||||
2) The `get_succes_url()` if available.
|
||||
3) The `.success_url` if available.
|
||||
4) A fallback default success URL: `get_default_success_url()`.
|
||||
"""
|
||||
url = self.get_next_url()
|
||||
if url:
|
||||
return url
|
||||
|
||||
if not url:
|
||||
if hasattr(super(), "get_success_url"):
|
||||
try:
|
||||
url = super().get_success_url()
|
||||
except ImproperlyConfigured:
|
||||
# Django's default get_success_url() checks self.succes_url,
|
||||
# and throws this if that is not set. Yet, in our case, we
|
||||
# want to fallback to the default.
|
||||
pass
|
||||
elif hasattr(self, "success_url"):
|
||||
url = self.success_url
|
||||
if url:
|
||||
url = str(url) # reverse_lazy
|
||||
if not url:
|
||||
url = self.get_default_success_url()
|
||||
return url
|
||||
|
||||
def get_default_success_url(self):
|
||||
return None
|
||||
|
||||
def get_next_url(self):
|
||||
return get_next_redirect_url(self.request, self.redirect_field_name)
|
||||
|
||||
def passthrough_next_url(self, url):
|
||||
return passthrough_next_redirect_url(
|
||||
self.request, url, self.redirect_field_name
|
||||
)
|
||||
335
venv/lib/python3.11/site-packages/allauth/account/models.py
Normal file
335
venv/lib/python3.11/site-packages/allauth/account/models.py
Normal file
@@ -0,0 +1,335 @@
|
||||
import datetime
|
||||
import time
|
||||
from typing import Dict, Optional
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AbstractBaseUser
|
||||
from django.core import signing
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.db.models.constraints import UniqueConstraint
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from allauth.account import app_settings, signals
|
||||
from allauth.account.adapter import get_adapter
|
||||
from allauth.account.managers import (
|
||||
EmailAddressManager,
|
||||
EmailConfirmationManager,
|
||||
)
|
||||
|
||||
|
||||
class EmailAddress(models.Model):
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
verbose_name=_("user"),
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
email = models.EmailField(
|
||||
db_index=True,
|
||||
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")]
|
||||
constraints = [
|
||||
UniqueConstraint(
|
||||
fields=["user", "primary"],
|
||||
name="unique_primary_email",
|
||||
condition=Q(primary=True),
|
||||
)
|
||||
]
|
||||
if app_settings.UNIQUE_EMAIL:
|
||||
constraints.append(
|
||||
UniqueConstraint(
|
||||
fields=["email"],
|
||||
name="unique_verified_email",
|
||||
condition=Q(verified=True),
|
||||
)
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.email
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
self.email = self.email.lower()
|
||||
|
||||
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=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):
|
||||
model = get_emailconfirmation_model()
|
||||
confirmation = model.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:
|
||||
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)
|
||||
|
||||
@classmethod
|
||||
def from_key(cls, key):
|
||||
qs = EmailConfirmation.objects.all_valid()
|
||||
qs = qs.select_related("email_address__user")
|
||||
emailconfirmation = qs.filter(key=key.lower()).first()
|
||||
return emailconfirmation
|
||||
|
||||
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 # type: ignore[attr-defined]
|
||||
|
||||
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):
|
||||
def __init__(self, email_address):
|
||||
self.email_address = email_address
|
||||
|
||||
@classmethod
|
||||
def create(cls, email_address):
|
||||
return EmailConfirmationHMAC(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
|
||||
|
||||
def key_expired(self):
|
||||
return False
|
||||
|
||||
|
||||
class Login:
|
||||
"""
|
||||
Represents a user that is in the process of logging in.
|
||||
|
||||
Keyword arguments:
|
||||
|
||||
signup -- Indicates whether or not sending the
|
||||
email is essential (during signup), or if it can be skipped (e.g. in
|
||||
case email verification is optional and we are only logging in).
|
||||
"""
|
||||
|
||||
# Optional, because we might be prentending logins to prevent user
|
||||
# enumeration.
|
||||
user: Optional[AbstractBaseUser]
|
||||
email_verification: app_settings.EmailVerificationMethod
|
||||
signal_kwargs: Optional[Dict]
|
||||
signup: bool
|
||||
email: Optional[str]
|
||||
state: Dict
|
||||
initiated_at: float
|
||||
redirect_url: Optional[str]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
user,
|
||||
email_verification: Optional[app_settings.EmailVerificationMethod] = None,
|
||||
redirect_url: Optional[str] = None,
|
||||
signal_kwargs: Optional[Dict] = None,
|
||||
signup: bool = False,
|
||||
email: Optional[str] = None,
|
||||
state: Optional[Dict] = None,
|
||||
initiated_at: Optional[float] = None,
|
||||
):
|
||||
self.user = user
|
||||
if not email_verification:
|
||||
email_verification = app_settings.EMAIL_VERIFICATION
|
||||
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
|
||||
self.initiated_at = initiated_at if initiated_at else time.time()
|
||||
|
||||
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) if self.user else None,
|
||||
"email_verification": self.email_verification,
|
||||
"signup": self.signup,
|
||||
"redirect_url": self.redirect_url,
|
||||
"email": self.email,
|
||||
"signal_kwargs": signal_kwargs,
|
||||
"state": self.state,
|
||||
"initiated_at": self.initiated_at,
|
||||
}
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def deserialize(cls, data):
|
||||
from allauth.account.utils import url_str_to_user_pk
|
||||
|
||||
user = None
|
||||
user_pk = data["user_pk"]
|
||||
if user_pk is not None:
|
||||
user = (
|
||||
get_user_model().objects.filter(pk=url_str_to_user_pk(user_pk)).first()
|
||||
)
|
||||
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:
|
||||
from allauth.socialaccount.models import SocialLogin
|
||||
|
||||
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"],
|
||||
initiated_at=data["initiated_at"],
|
||||
)
|
||||
except KeyError:
|
||||
raise ValueError()
|
||||
|
||||
|
||||
def get_emailconfirmation_model():
|
||||
if app_settings.EMAIL_VERIFICATION_BY_CODE_ENABLED:
|
||||
from allauth.account.internal.flows.email_verification_by_code import (
|
||||
EmailVerificationModel,
|
||||
)
|
||||
|
||||
return EmailVerificationModel
|
||||
elif app_settings.EMAIL_CONFIRMATION_HMAC:
|
||||
model = EmailConfirmationHMAC
|
||||
else:
|
||||
model = EmailConfirmation
|
||||
return model
|
||||
@@ -0,0 +1,14 @@
|
||||
import warnings
|
||||
|
||||
from allauth.account.internal.flows.reauthentication import (
|
||||
did_recently_authenticate,
|
||||
raise_if_reauthentication_required,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"raise_if_reauthentication_required",
|
||||
"did_recently_authenticate",
|
||||
]
|
||||
|
||||
warnings.warn("allauth.account.reauthentication is deprecated")
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user