User session tracking #93587

Merged
Oleg-Komarov merged 18 commits from user-session into main 2024-08-02 16:04:09 +02:00
5 changed files with 69 additions and 3 deletions
Showing only changes of commit 6bc5d1eccd - Show all commits

View File

@ -257,3 +257,42 @@ def check_verification_payload(
log.debug("verification OK")
return VerificationResult.OK, payload
def send_new_user_session(session):
if not hasattr(session, 'user'):
log.error('programming error: called for a session without a user')
return False
user = session.user
# sending only a text/plain email to reduce the room for look-alike phishing emails
email_body_txt, subject = construct_new_user_session(session)
email = user.email
try:
send_mail(
subject,
message=email_body_txt,
from_email=None, # just use the configured default From-address.
recipient_list=[email],
fail_silently=False,
)
except (smtplib.SMTPException, OSError):
Oleg-Komarov marked this conversation as resolved Outdated

might be better to make this a background task instead of wrapping into except.

might be better to make this a background task instead of wrapping into except.
log.exception("failed to send a new user session email for account %s", user.pk)
return False
log.info("sent a new user session email for account %s", user.pk)
return True
def construct_new_user_session(session):
context = {
"session": session,
"user": session.user,
"subject": "Blender ID new sign-in",
}
email_body_txt = loader.render_to_string(
"bid_main/emails/new_user_session.txt", context
)
return email_body_txt, context["subject"]

View File

@ -19,6 +19,7 @@ from django.utils import timezone
from django.utils.deconstruct import deconstructible
from django.utils.translation import gettext_lazy as _
import oauth2_provider.models as oa2_models
import user_agents
from . import fields
from . import hashers
@ -682,3 +683,10 @@ class UserSession(models.Model):
def __str__(self):
return f'UserSession pk={self.pk} for {self.user}'
@property
def device(self):
if self.user_agent:
return user_agents.parse(self.user_agent)
else:
return None
Oleg-Komarov marked this conversation as resolved Outdated

superfluous else

superfluous else

View File

@ -6,7 +6,7 @@ from django.db.models import F
from django.db.models.signals import m2m_changed, post_delete
from django.dispatch import receiver
from . import models
from . import email, models
import bid_main.utils as utils
import bid_main.file_utils
@ -20,7 +20,7 @@ def log_exception(sender, **kwargs):
@receiver(user_logged_in)
def process_new_login(sender, request, user, **kwargs):
"""Updates user fields upon login.
"""Updates user fields and creates a UserSession upon login. Sends and email if IP is new.
Only saves specific fields, so that the webhook trigger knows what changed.
"""
@ -33,8 +33,11 @@ def process_new_login(sender, request, user, **kwargs):
if request_ip and user.current_login_ip != request_ip:
user.last_login_ip = F("current_login_ip")
user.current_login_ip = request_ip
fields.update({"last_login_ip", "current_login_ip"})
try:
email.send_new_user_session(request.session.create_model_instance({}))
except Exception:
log.exception('failed to send a new user session email')
Oleg-Komarov marked this conversation as resolved
Review

should be a call of a task

should be a call of a task
user.save(update_fields=fields)
models.UserSession.update_or_create_from_request(request, user)

View File

@ -0,0 +1,15 @@
{% autoescape off %}
Dear {{ user.full_name|default:user.email }}!
A new sign-in for your Blender ID account {{ user.email }}
IP address: {{ session.ip }}
Device: {{ session.device }}
If this was you, you can ignore this message.
If this wasn't you, please change or reset your password.
--
Kind regards,
The Blender Web Team
{% endautoescape %}

View File

@ -51,6 +51,7 @@ sorl-thumbnail==12.7.0 ; python_version >= "3.8" and python_version < "4"
sqlparse==0.5.0 ; python_version >= "3.8" and python_version < "4"
tornado==6.0.3 ; python_version >= "3.8" and python_version < "4"
urllib3==1.25.11 ; python_version >= "3.8" and python_version < "4"
user-agents==2.2.0
uwsgi==2.0.23
wrapt==1.15.0 ; python_version >= "3.8" and python_version < "4"
zipp==0.6.0 ; python_version >= "3.8" and python_version < "4"