Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docker/prod/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ SECRET_KEY="changeme"
PRIMARY_HOST="https://test.hypha.app"
EMAIL_HOST="hypha.app"

EMAIL_SUBJECT_PREFIX="[Hypha]"
EMAIL_SUBJECT_PREFIX="[Hypha] "
ORG_EMAIL="hello@hypha.app"
SERVER_EMAIL="test@hypha.app"

Expand Down
8 changes: 8 additions & 0 deletions hypha/apply/users/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django.apps import AppConfig


class UsersConfig(AppConfig):
name = "hypha.apply.users"

def ready(self):
from . import signals # NOQA
42 changes: 42 additions & 0 deletions hypha/apply/users/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from django.conf import settings
from django.contrib.auth.signals import user_logged_in
from django.dispatch import receiver
from django.utils import formats, timezone
from django.utils.translation import gettext_lazy as _
from wagtail.models import Site

from hypha.core.mail import MarkdownMail

HIJACK_VIEW_NAMES = {
"hijack-become",
"users:hijack",
"hijack:acquire",
"hijack:release",
}


@receiver(user_logged_in)
def send_login_notification(sender, request, user, **kwargs):
if not settings.SEND_MESSAGES or not user.email:
return

if request and getattr(request, "resolver_match", None):
if request.resolver_match.view_name in HIJACK_VIEW_NAMES:
return

subject = _("Successful login to %(org)s") % {"org": settings.ORG_LONG_NAME}
if settings.EMAIL_SUBJECT_PREFIX:
subject = str(settings.EMAIL_SUBJECT_PREFIX) + str(subject)

email = MarkdownMail("users/emails/login_notification.md")
email.send(
to=user.email,
subject=subject,
from_email=settings.DEFAULT_FROM_EMAIL,
context={
"user": user,
"login_time": formats.date_format(timezone.now(), "SHORT_DATETIME_FORMAT"),
"site": Site.find_for_request(request) if request else None,
"ORG_EMAIL": settings.ORG_EMAIL,
},
)
19 changes: 19 additions & 0 deletions hypha/apply/users/templates/users/emails/login_notification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{% load i18n wagtailadmin_tags %}{% base_url_setting as base_url %}
{% blocktrans %}Dear {{ user }},{% endblocktrans %}

{% blocktrans %}This is to notify you that your account was successfully logged in to {{ ORG_LONG_NAME }}.{% endblocktrans %}

{% blocktrans with login_time=login_time %}Login time: {{ login_time }}{% endblocktrans %}

{% blocktrans %}If you did not log in, please contact us immediately and consider changing your password.{% endblocktrans %}

{% if ORG_EMAIL %}
{% blocktrans %}If you have any questions, please contact us at {{ ORG_EMAIL }}.{% endblocktrans %}
{% endif %}

{% blocktrans %}Kind Regards,
The {{ ORG_SHORT_NAME }} Team{% endblocktrans %}

--
{{ ORG_LONG_NAME }}
{% if site %}{{ site.root_url }}{% else %}{{ base_url }}{% endif %}
80 changes: 80 additions & 0 deletions hypha/apply/users/tests/test_signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from unittest.mock import MagicMock

from django.contrib.auth.signals import user_logged_in
from django.core import mail
from django.test import RequestFactory, TestCase, override_settings

from .factories import UserFactory


@override_settings(SEND_MESSAGES=True)
class TestSendLoginNotification(TestCase):
def setUp(self):
self.factory = RequestFactory()
self.user = UserFactory()

def _fire_signal(self, user=None, request=None):
if user is None:
user = self.user
if request is None:
request = self.factory.get("/")
user_logged_in.send(sender=user.__class__, request=request, user=user)

def test_sends_email_on_login(self):
self._fire_signal()
self.assertEqual(len(mail.outbox), 1)

def test_email_sent_to_user(self):
self._fire_signal()
self.assertIn(self.user.email, mail.outbox[0].to)

def test_email_subject_contains_org_name(self):
from django.conf import settings

self._fire_signal()
self.assertIn(settings.ORG_LONG_NAME, mail.outbox[0].subject)

def test_no_email_when_send_messages_disabled(self):
with self.settings(SEND_MESSAGES=False):
self._fire_signal()
self.assertEqual(len(mail.outbox), 0)

def test_no_email_when_user_has_no_email(self):
self.user.email = ""
self.user.save()
self._fire_signal()
self.assertEqual(len(mail.outbox), 0)

def test_no_email_when_request_is_none(self):
# Signal can be fired without a request (e.g. management commands)
self._fire_signal(request=None)
self.assertEqual(len(mail.outbox), 1)

def test_email_body_contains_login_time(self):
self._fire_signal()
self.assertTrue(any("Login time" in part for part in [mail.outbox[0].body]))

def _fire_signal_with_view_name(self, view_name):
request = self.factory.get("/")
request.resolver_match = MagicMock(view_name=view_name)
self._fire_signal(request=request)

def test_no_email_on_hijack_acquire(self):
self._fire_signal_with_view_name("hijack:acquire")
self.assertEqual(len(mail.outbox), 0)

def test_no_email_on_hijack_release(self):
self._fire_signal_with_view_name("hijack:release")
self.assertEqual(len(mail.outbox), 0)

def test_no_email_on_hijack_become(self):
self._fire_signal_with_view_name("hijack-become")
self.assertEqual(len(mail.outbox), 0)

def test_no_email_on_users_hijack_view(self):
self._fire_signal_with_view_name("users:hijack")
self.assertEqual(len(mail.outbox), 0)

def test_email_sent_for_non_hijack_view(self):
self._fire_signal_with_view_name("account_login")
self.assertEqual(len(mail.outbox), 1)
2 changes: 1 addition & 1 deletion hypha/settings/django.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"hypha.apply.dashboard",
"hypha.apply.flags",
"hypha.home",
"hypha.apply.users",
"hypha.apply.users.apps.UsersConfig",
"hypha.apply.review",
"hypha.apply.determinations",
"hypha.apply.stream_forms",
Expand Down
Loading