From 1dacc8b797b18fcc3900a10b751408455a9980f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Caune?= Date: Tue, 5 Jan 2016 00:25:08 +0200 Subject: [PATCH] Users can add passwords to their accounts. Fixes #6 --- hc/accounts/backends.py | 23 +++++- hc/accounts/forms.py | 7 +- hc/accounts/models.py | 9 +++ hc/accounts/tests/test_check_token.py | 11 +-- hc/accounts/urls.py | 6 ++ hc/accounts/views.py | 79 ++++++++++++++++--- hc/front/tests/test_add_channel.py | 16 ++-- hc/front/tests/test_add_check.py | 4 +- hc/front/tests/test_channel_checks.py | 10 +-- hc/front/tests/test_log.py | 12 +-- hc/front/tests/test_my_checks.py | 4 +- hc/front/tests/test_remove_channel.py | 12 +-- hc/front/tests/test_remove_check.py | 12 +-- hc/front/tests/test_update_channel.py | 16 ++-- hc/front/tests/test_update_name.py | 14 ++-- hc/front/tests/test_update_timeout.py | 12 +-- hc/front/tests/test_verify_email.py | 2 +- hc/lib/emails.py | 5 ++ hc/payments/tests/test_billing.py | 4 +- hc/payments/tests/test_cancel_plan.py | 4 +- hc/payments/tests/test_create_plan.py | 4 +- hc/payments/tests/test_get_client_token.py | 4 +- hc/payments/tests/test_invoice.py | 6 +- hc/payments/tests/test_pricing.py | 4 +- hc/settings.py | 2 +- static/js/login.js | 7 ++ templates/accounts/login.html | 38 ++++++++- templates/accounts/profile.html | 17 ++++ templates/accounts/set_password.html | 39 +++++++++ .../accounts/set_password_link_sent.html | 18 +++++ templates/emails/set-password-body-html.html | 10 +++ templates/emails/set-password-subject.html | 1 + 32 files changed, 320 insertions(+), 92 deletions(-) create mode 100644 static/js/login.js create mode 100644 templates/accounts/set_password.html create mode 100644 templates/accounts/set_password_link_sent.html create mode 100644 templates/emails/set-password-body-html.html create mode 100644 templates/emails/set-password-subject.html diff --git a/hc/accounts/backends.py b/hc/accounts/backends.py index 1d07c458..f18ca975 100644 --- a/hc/accounts/backends.py +++ b/hc/accounts/backends.py @@ -3,8 +3,17 @@ from django.contrib.auth.models import User from hc.accounts.models import Profile +class BasicBackend: + + def get_user(self, user_id): + try: + return User.objects.get(pk=user_id) + except User.DoesNotExist: + return None + + # Authenticate against the token in user's profile. -class ProfileBackend(object): +class ProfileBackend(BasicBackend): def authenticate(self, username=None, token=None): try: @@ -22,3 +31,15 @@ class ProfileBackend(object): return User.objects.get(pk=user_id) except User.DoesNotExist: return None + + +class EmailBackend(BasicBackend): + + def authenticate(self, username=None, password=None): + try: + user = User.objects.get(email=username) + except User.DoesNotExist: + return None + + if user.check_password(password): + return user diff --git a/hc/accounts/forms.py b/hc/accounts/forms.py index 7936ade3..d4f045dc 100644 --- a/hc/accounts/forms.py +++ b/hc/accounts/forms.py @@ -8,9 +8,14 @@ class LowercaseEmailField(forms.EmailField): return value.lower() -class EmailForm(forms.Form): +class EmailPasswordForm(forms.Form): email = LowercaseEmailField() + password = forms.CharField(required=False) class ReportSettingsForm(forms.Form): reports_allowed = forms.BooleanField(required=False) + + +class SetPasswordForm(forms.Form): + password = forms.CharField() diff --git a/hc/accounts/models.py b/hc/accounts/models.py index cf8274d1..59be222f 100644 --- a/hc/accounts/models.py +++ b/hc/accounts/models.py @@ -40,6 +40,15 @@ class Profile(models.Model): ctx = {"login_link": settings.SITE_ROOT + path} emails.login(self.user.email, ctx) + def send_set_password_link(self): + token = str(uuid.uuid4()) + self.token = make_password(token) + self.save() + + path = reverse("hc-set-password", args=[token]) + ctx = {"set_password_link": settings.SITE_ROOT + path} + emails.set_password(self.user.email, ctx) + def send_report(self): # reset next report date first: now = timezone.now() diff --git a/hc/accounts/tests/test_check_token.py b/hc/accounts/tests/test_check_token.py index 7a7abe95..e902754f 100644 --- a/hc/accounts/tests/test_check_token.py +++ b/hc/accounts/tests/test_check_token.py @@ -10,7 +10,8 @@ class CheckTokenTestCase(TestCase): def setUp(self): super(CheckTokenTestCase, self).setUp() - self.alice = User(username="alice") + self.alice = User(username="alice", email="alice@example.org") + self.alice.set_password("password") self.alice.save() self.profile = Profile(user=self.alice) @@ -21,13 +22,13 @@ class CheckTokenTestCase(TestCase): r = self.client.get("/accounts/check_token/alice/secret-token/") self.assertRedirects(r, "/checks/") - # After login, password should be unusable - self.alice.refresh_from_db() - assert not self.alice.has_usable_password() + # After login, token should be blank + self.profile.refresh_from_db() + self.assertEqual(self.profile.token, "") def test_it_redirects_already_logged_in(self): # Login - self.client.get("/accounts/check_token/alice/secret-token/") + self.client.login(username="alice@example.org", password="password") # Login again, when already authenticated r = self.client.get("/accounts/check_token/alice/secret-token/") diff --git a/hc/accounts/urls.py b/hc/accounts/urls.py index 7ddc9631..333a0c8f 100644 --- a/hc/accounts/urls.py +++ b/hc/accounts/urls.py @@ -7,6 +7,9 @@ urlpatterns = [ url(r'^login_link_sent/$', views.login_link_sent, name="hc-login-link-sent"), + url(r'^set_password_link_sent/$', + views.set_password_link_sent, name="hc-set-password-link-sent"), + url(r'^check_token/([\w-]+)/([\w-]+)/$', views.check_token, name="hc-check-token"), @@ -15,4 +18,7 @@ urlpatterns = [ url(r'^unsubscribe_reports/([\w-]+)/$', views.unsubscribe_reports, name="hc-unsubscribe-reports"), + url(r'^set_password/([\w-]+)/$', + views.set_password, name="hc-set-password"), + ] diff --git a/hc/accounts/views.py b/hc/accounts/views.py index 3eabd3bc..07e2affb 100644 --- a/hc/accounts/views.py +++ b/hc/accounts/views.py @@ -5,11 +5,13 @@ from django.contrib.auth import login as auth_login from django.contrib.auth import logout as auth_logout from django.contrib.auth import authenticate from django.contrib.auth.decorators import login_required +from django.contrib.auth.hashers import check_password from django.contrib.auth.models import User from django.core import signing from django.http import HttpResponseBadRequest from django.shortcuts import redirect, render -from hc.accounts.forms import EmailForm, ReportSettingsForm +from hc.accounts.forms import (EmailPasswordForm, ReportSettingsForm, + SetPasswordForm) from hc.accounts.models import Profile from hc.api.models import Channel, Check @@ -45,25 +47,38 @@ def _associate_demo_check(request, user): def login(request): + bad_credentials = False if request.method == 'POST': - form = EmailForm(request.POST) + form = EmailPasswordForm(request.POST) if form.is_valid(): email = form.cleaned_data["email"] - try: - user = User.objects.get(email=email) - except User.DoesNotExist: - user = _make_user(email) - _associate_demo_check(request, user) - - profile = Profile.objects.for_user(user) - profile.send_instant_login_link() - return redirect("hc-login-link-sent") + password = form.cleaned_data["password"] + if len(password): + user = authenticate(username=email, password=password) + if user is not None and user.is_active: + auth_login(request, user) + return redirect("hc-checks") + bad_credentials = True + else: + try: + user = User.objects.get(email=email) + except User.DoesNotExist: + user = _make_user(email) + _associate_demo_check(request, user) + + profile = Profile.objects.for_user(user) + profile.send_instant_login_link() + return redirect("hc-login-link-sent") else: - form = EmailForm() + form = EmailPasswordForm() bad_link = request.session.pop("bad_link", None) - ctx = {"form": form, "bad_link": bad_link} + ctx = { + "form": form, + "bad_credentials": bad_credentials, + "bad_link": bad_link + } return render(request, "accounts/login.html", ctx) @@ -76,6 +91,10 @@ def login_link_sent(request): return render(request, "accounts/login_link_sent.html") +def set_password_link_sent(request): + return render(request, "accounts/set_password_link_sent.html") + + def check_token(request, username, token): if request.user.is_authenticated() and request.user.username == username: # User is already logged in @@ -102,6 +121,10 @@ def profile(request): profile = Profile.objects.for_user(request.user) if request.method == "POST": + if "set_password" in request.POST: + profile.send_set_password_link() + return redirect("hc-set-password-link-sent") + form = ReportSettingsForm(request.POST) if form.is_valid(): profile.reports_allowed = form.cleaned_data["reports_allowed"] @@ -115,6 +138,36 @@ def profile(request): return render(request, "accounts/profile.html", ctx) +@login_required +def set_password(request, token): + profile = Profile.objects.for_user(request.user) + if not check_password(token, profile.token): + return HttpResponseBadRequest() + + if request.method == "POST": + form = SetPasswordForm(request.POST) + if form.is_valid(): + password = form.cleaned_data["password"] + request.user.set_password(password) + request.user.save() + + profile.token = "" + profile.save() + + # Setting a password logs the user out, so here we + # log them back in. + u = authenticate(username=request.user.email, password=password) + auth_login(request, u) + + messages.info(request, "Your password has been set!") + return redirect("hc-profile") + + ctx = { + } + + return render(request, "accounts/set_password.html", ctx) + + def unsubscribe_reports(request, username): try: signing.Signer().unsign(request.GET.get("token")) diff --git a/hc/front/tests/test_add_channel.py b/hc/front/tests/test_add_channel.py index 04e07cf3..50b2a201 100644 --- a/hc/front/tests/test_add_channel.py +++ b/hc/front/tests/test_add_channel.py @@ -7,7 +7,7 @@ from hc.api.models import Channel class AddChannelTestCase(TestCase): def setUp(self): - self.alice = User(username="alice") + self.alice = User(username="alice", email="alice@example.org") self.alice.set_password("password") self.alice.save() @@ -18,7 +18,7 @@ class AddChannelTestCase(TestCase): url = "/integrations/add/" form = {"kind": "email", "value": "alice@example.org"} - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") r = self.client.post(url, form) self.assertRedirects(r, "/integrations/") @@ -30,7 +30,7 @@ class AddChannelTestCase(TestCase): url = "/integrations/add/" form = {"kind": "email", "value": " alice@example.org "} - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") self.client.post(url, form) q = Channel.objects.filter(value="alice@example.org") @@ -40,20 +40,20 @@ class AddChannelTestCase(TestCase): url = "/integrations/add/" form = {"kind": "dog", "value": "Lassie"} - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") r = self.client.post(url, form) assert r.status_code == 400, r.status_code def test_instructions_work(self): - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") for frag in ("email", "webhook", "pd", "pushover", "slack", "hipchat"): url = "/integrations/add_%s/" % frag r = self.client.get(url) self.assertContains(r, "Integration Settings", status_code=200) def test_it_adds_pushover_channel(self): - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") session = self.client.session session["po_nonce"] = "n" @@ -68,7 +68,7 @@ class AddChannelTestCase(TestCase): assert channels[0].value == "a|0" def test_it_validates_pushover_priority(self): - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") session = self.client.session session["po_nonce"] = "n" @@ -79,7 +79,7 @@ class AddChannelTestCase(TestCase): assert r.status_code == 400 def test_it_validates_pushover_nonce(self): - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") session = self.client.session session["po_nonce"] = "n" diff --git a/hc/front/tests/test_add_check.py b/hc/front/tests/test_add_check.py index 9f403f39..d4f6d06f 100644 --- a/hc/front/tests/test_add_check.py +++ b/hc/front/tests/test_add_check.py @@ -6,13 +6,13 @@ from hc.api.models import Check class AddCheckTestCase(TestCase): def setUp(self): - self.alice = User(username="alice") + self.alice = User(username="alice", email="alice@example.org") self.alice.set_password("password") self.alice.save() def test_it_works(self): url = "/checks/add/" - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") r = self.client.post(url) self.assertRedirects(r, "/checks/") assert Check.objects.count() == 1 diff --git a/hc/front/tests/test_channel_checks.py b/hc/front/tests/test_channel_checks.py index 6e09d4d5..7ab2ac8f 100644 --- a/hc/front/tests/test_channel_checks.py +++ b/hc/front/tests/test_channel_checks.py @@ -6,7 +6,7 @@ from hc.api.models import Channel class ChannelChecksTestCase(TestCase): def setUp(self): - self.alice = User(username="alice") + self.alice = User(username="alice", email="alice@example.org") self.alice.set_password("password") self.alice.save() @@ -17,19 +17,19 @@ class ChannelChecksTestCase(TestCase): def test_it_works(self): url = "/integrations/%s/checks/" % self.channel.code - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") r = self.client.get(url) self.assertContains(r, "alice@example.org", status_code=200) def test_it_checks_owner(self): - mallory = User(username="mallory") + mallory = User(username="mallory", email="mallory@example.org") mallory.set_password("password") mallory.save() # channel does not belong to mallory so this should come back # with 403 Forbidden: url = "/integrations/%s/checks/" % self.channel.code - self.client.login(username="mallory", password="password") + self.client.login(username="mallory@example.org", password="password") r = self.client.get(url) assert r.status_code == 403 @@ -37,6 +37,6 @@ class ChannelChecksTestCase(TestCase): # Valid UUID but there is no channel for it: url = "/integrations/6837d6ec-fc08-4da5-a67f-08a9ed1ccf62/checks/" - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") r = self.client.get(url) assert r.status_code == 404 diff --git a/hc/front/tests/test_log.py b/hc/front/tests/test_log.py index 7a4a46fa..0fb59933 100644 --- a/hc/front/tests/test_log.py +++ b/hc/front/tests/test_log.py @@ -6,7 +6,7 @@ from hc.api.models import Check, Ping class LogTestCase(TestCase): def setUp(self): - self.alice = User(username="alice") + self.alice = User(username="alice", email="alice@example.org") self.alice.set_password("password") self.alice.save() @@ -19,14 +19,14 @@ class LogTestCase(TestCase): def test_it_works(self): url = "/checks/%s/log/" % self.check.code - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") r = self.client.get(url) self.assertContains(r, "Dates and times are", status_code=200) def test_it_handles_bad_uuid(self): url = "/checks/not-uuid/log/" - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") r = self.client.get(url) assert r.status_code == 400 @@ -34,16 +34,16 @@ class LogTestCase(TestCase): # Valid UUID but there is no check for it: url = "/checks/6837d6ec-fc08-4da5-a67f-08a9ed1ccf62/log/" - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") r = self.client.get(url) assert r.status_code == 404 def test_it_checks_ownership(self): - charlie = User(username="charlie") + charlie = User(username="charlie", email="charlie@example.org") charlie.set_password("password") charlie.save() url = "/checks/%s/log/" % self.check.code - self.client.login(username="charlie", password="password") + self.client.login(username="charlie@example.org", password="password") r = self.client.get(url) assert r.status_code == 403 diff --git a/hc/front/tests/test_my_checks.py b/hc/front/tests/test_my_checks.py index cfddb0d2..37a28f00 100644 --- a/hc/front/tests/test_my_checks.py +++ b/hc/front/tests/test_my_checks.py @@ -6,7 +6,7 @@ from hc.api.models import Check class MyChecksTestCase(TestCase): def setUp(self): - self.alice = User(username="alice") + self.alice = User(username="alice", email="alice@example.org") self.alice.set_password("password") self.alice.save() @@ -14,6 +14,6 @@ class MyChecksTestCase(TestCase): self.check.save() def test_it_works(self): - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") r = self.client.get("/checks/") self.assertContains(r, "Alice Was Here", status_code=200) diff --git a/hc/front/tests/test_remove_channel.py b/hc/front/tests/test_remove_channel.py index fe6412ff..b1c25e41 100644 --- a/hc/front/tests/test_remove_channel.py +++ b/hc/front/tests/test_remove_channel.py @@ -6,7 +6,7 @@ from hc.api.models import Channel class RemoveChannelTestCase(TestCase): def setUp(self): - self.alice = User(username="alice") + self.alice = User(username="alice", email="alice@example.org") self.alice.set_password("password") self.alice.save() @@ -17,7 +17,7 @@ class RemoveChannelTestCase(TestCase): def test_it_works(self): url = "/integrations/%s/remove/" % self.channel.code - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") r = self.client.post(url) self.assertRedirects(r, "/integrations/") @@ -26,18 +26,18 @@ class RemoveChannelTestCase(TestCase): def test_it_handles_bad_uuid(self): url = "/integrations/not-uuid/remove/" - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") r = self.client.post(url) assert r.status_code == 400 def test_it_checks_owner(self): url = "/integrations/%s/remove/" % self.channel.code - mallory = User(username="mallory") + mallory = User(username="mallory", email="mallory@example.org") mallory.set_password("password") mallory.save() - self.client.login(username="mallory", password="password") + self.client.login(username="mallory@example.org", password="password") r = self.client.post(url) assert r.status_code == 403 @@ -45,6 +45,6 @@ class RemoveChannelTestCase(TestCase): # Valid UUID but there is no channel for it: url = "/integrations/6837d6ec-fc08-4da5-a67f-08a9ed1ccf62/remove/" - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") r = self.client.post(url) assert r.status_code == 404 diff --git a/hc/front/tests/test_remove_check.py b/hc/front/tests/test_remove_check.py index 94a99f08..9f5cba3f 100644 --- a/hc/front/tests/test_remove_check.py +++ b/hc/front/tests/test_remove_check.py @@ -6,7 +6,7 @@ from hc.api.models import Check class RemoveCheckTestCase(TestCase): def setUp(self): - self.alice = User(username="alice") + self.alice = User(username="alice", email="alice@example.org") self.alice.set_password("password") self.alice.save() @@ -16,7 +16,7 @@ class RemoveCheckTestCase(TestCase): def test_it_works(self): url = "/checks/%s/remove/" % self.check.code - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") r = self.client.post(url) self.assertRedirects(r, "/checks/") @@ -25,18 +25,18 @@ class RemoveCheckTestCase(TestCase): def test_it_handles_bad_uuid(self): url = "/checks/not-uuid/remove/" - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") r = self.client.post(url) assert r.status_code == 400 def test_it_checks_owner(self): url = "/checks/%s/remove/" % self.check.code - mallory = User(username="mallory") + mallory = User(username="mallory", email="mallory@example.org") mallory.set_password("password") mallory.save() - self.client.login(username="mallory", password="password") + self.client.login(username="mallory@example.org", password="password") r = self.client.post(url) assert r.status_code == 403 @@ -44,6 +44,6 @@ class RemoveCheckTestCase(TestCase): # Valid UUID but there is no check for it: url = "/checks/6837d6ec-fc08-4da5-a67f-08a9ed1ccf62/remove/" - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") r = self.client.post(url) assert r.status_code == 404 diff --git a/hc/front/tests/test_update_channel.py b/hc/front/tests/test_update_channel.py index f9aea7fb..b3ecc205 100644 --- a/hc/front/tests/test_update_channel.py +++ b/hc/front/tests/test_update_channel.py @@ -6,7 +6,7 @@ from hc.api.models import Channel, Check class UpdateChannelTestCase(TestCase): def setUp(self): - self.alice = User(username="alice") + self.alice = User(username="alice", email="alice@example.org") self.alice.set_password("password") self.alice.save() @@ -23,7 +23,7 @@ class UpdateChannelTestCase(TestCase): "check-%s" % self.check.code: True } - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") r = self.client.post("/integrations/", data=payload) self.assertRedirects(r, "/integrations/") @@ -33,20 +33,20 @@ class UpdateChannelTestCase(TestCase): assert checks[0].code == self.check.code def test_it_checks_channel_user(self): - mallory = User(username="mallory") + mallory = User(username="mallory", email="mallory@example.org") mallory.set_password("password") mallory.save() payload = {"channel": self.channel.code} - self.client.login(username="mallory", password="password") + self.client.login(username="mallory@example.org", password="password") r = self.client.post("/integrations/", data=payload) # self.channel does not belong to mallory, this should fail-- assert r.status_code == 403 def test_it_checks_check_user(self): - mallory = User(username="mallory") + mallory = User(username="mallory", email="mallory@example.org") mallory.set_password("password") mallory.save() @@ -58,7 +58,7 @@ class UpdateChannelTestCase(TestCase): "channel": mc.code, "check-%s" % self.check.code: True } - self.client.login(username="mallory", password="password") + self.client.login(username="mallory@example.org", password="password") r = self.client.post("/integrations/", data=payload) # mc belongs to mallorym but self.check does not-- @@ -68,7 +68,7 @@ class UpdateChannelTestCase(TestCase): # Correct UUID but there is no channel for it: payload = {"channel": "6837d6ec-fc08-4da5-a67f-08a9ed1ccf62"} - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") r = self.client.post("/integrations/", data=payload) assert r.status_code == 400 @@ -79,6 +79,6 @@ class UpdateChannelTestCase(TestCase): "check-6837d6ec-fc08-4da5-a67f-08a9ed1ccf62": True } - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") r = self.client.post("/integrations/", data=payload) assert r.status_code == 400 diff --git a/hc/front/tests/test_update_name.py b/hc/front/tests/test_update_name.py index 51222b03..520cc8a0 100644 --- a/hc/front/tests/test_update_name.py +++ b/hc/front/tests/test_update_name.py @@ -6,7 +6,7 @@ from hc.api.models import Check class UpdateNameTestCase(TestCase): def setUp(self): - self.alice = User(username="alice") + self.alice = User(username="alice", email="alice@example.org") self.alice.set_password("password") self.alice.save() @@ -17,7 +17,7 @@ class UpdateNameTestCase(TestCase): url = "/checks/%s/name/" % self.check.code payload = {"name": "Alice Was Here"} - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") r = self.client.post(url, data=payload) self.assertRedirects(r, "/checks/") @@ -26,14 +26,14 @@ class UpdateNameTestCase(TestCase): def test_it_checks_ownership(self): - charlie = User(username="charlie") + charlie = User(username="charlie", email="charlie@example.org") charlie.set_password("password") charlie.save() url = "/checks/%s/name/" % self.check.code payload = {"name": "Charlie Sent This"} - self.client.login(username="charlie", password="password") + self.client.login(username="charlie@example.org", password="password") r = self.client.post(url, data=payload) assert r.status_code == 403 @@ -41,7 +41,7 @@ class UpdateNameTestCase(TestCase): url = "/checks/not-uuid/name/" payload = {"name": "Alice Was Here"} - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") r = self.client.post(url, data=payload) assert r.status_code == 400 @@ -50,7 +50,7 @@ class UpdateNameTestCase(TestCase): url = "/checks/6837d6ec-fc08-4da5-a67f-08a9ed1ccf62/name/" payload = {"name": "Alice Was Here"} - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") r = self.client.post(url, data=payload) assert r.status_code == 404 @@ -58,7 +58,7 @@ class UpdateNameTestCase(TestCase): url = "/checks/%s/name/" % self.check.code payload = {"tags": " foo bar\r\t \n baz \n"} - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") self.client.post(url, data=payload) check = Check.objects.get(id=self.check.id) diff --git a/hc/front/tests/test_update_timeout.py b/hc/front/tests/test_update_timeout.py index e3d1560d..1ee55ddc 100644 --- a/hc/front/tests/test_update_timeout.py +++ b/hc/front/tests/test_update_timeout.py @@ -6,7 +6,7 @@ from hc.api.models import Check class UpdateTimeoutTestCase(TestCase): def setUp(self): - self.alice = User(username="alice") + self.alice = User(username="alice", email="alice@example.org") self.alice.set_password("password") self.alice.save() @@ -17,7 +17,7 @@ class UpdateTimeoutTestCase(TestCase): url = "/checks/%s/timeout/" % self.check.code payload = {"timeout": 3600, "grace": 60} - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") r = self.client.post(url, data=payload) self.assertRedirects(r, "/checks/") @@ -29,7 +29,7 @@ class UpdateTimeoutTestCase(TestCase): url = "/checks/not-uuid/timeout/" payload = {"timeout": 3600, "grace": 60} - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") r = self.client.post(url, data=payload) assert r.status_code == 400 @@ -38,18 +38,18 @@ class UpdateTimeoutTestCase(TestCase): url = "/checks/6837d6ec-fc08-4da5-a67f-08a9ed1ccf62/timeout/" payload = {"timeout": 3600, "grace": 60} - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") r = self.client.post(url, data=payload) assert r.status_code == 404 def test_it_checks_ownership(self): - charlie = User(username="charlie") + charlie = User(username="charlie", email="charlie@example.org") charlie.set_password("password") charlie.save() url = "/checks/%s/timeout/" % self.check.code payload = {"timeout": 3600, "grace": 60} - self.client.login(username="charlie", password="password") + self.client.login(username="charlie@example.org", password="password") r = self.client.post(url, data=payload) assert r.status_code == 403 diff --git a/hc/front/tests/test_verify_email.py b/hc/front/tests/test_verify_email.py index 61961938..b38797ab 100644 --- a/hc/front/tests/test_verify_email.py +++ b/hc/front/tests/test_verify_email.py @@ -6,7 +6,7 @@ from hc.api.models import Channel class VerifyEmailTestCase(TestCase): def setUp(self): - self.alice = User(username="alice") + self.alice = User(username="alice", email="alice@example.org") self.alice.set_password("password") self.alice.save() diff --git a/hc/lib/emails.py b/hc/lib/emails.py index 8ba8a6e9..73b3e78c 100644 --- a/hc/lib/emails.py +++ b/hc/lib/emails.py @@ -6,6 +6,11 @@ def login(to, ctx): o.send(to, ctx) +def set_password(to, ctx): + o = InlineCSSTemplateMail("set-password") + o.send(to, ctx) + + def alert(to, ctx): o = InlineCSSTemplateMail("alert") o.send(to, ctx) diff --git a/hc/payments/tests/test_billing.py b/hc/payments/tests/test_billing.py index 77e1374a..f2dcf43a 100644 --- a/hc/payments/tests/test_billing.py +++ b/hc/payments/tests/test_billing.py @@ -7,7 +7,7 @@ from mock import Mock, patch class BillingTestCase(TestCase): def setUp(self): - self.alice = User(username="alice") + self.alice = User(username="alice", email="alice@example.org") self.alice.set_password("password") self.alice.save() @@ -23,7 +23,7 @@ class BillingTestCase(TestCase): m2 = Mock(id="def456", amount=456) mock_braintree.Transaction.search.return_value = [m1, m2] - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") r = self.client.get("/billing/") self.assertContains(r, "123") self.assertContains(r, "def456") diff --git a/hc/payments/tests/test_cancel_plan.py b/hc/payments/tests/test_cancel_plan.py index 9335a9df..40ba3fd0 100644 --- a/hc/payments/tests/test_cancel_plan.py +++ b/hc/payments/tests/test_cancel_plan.py @@ -7,7 +7,7 @@ from mock import patch class CancelPlanTestCase(TestCase): def setUp(self): - self.alice = User(username="alice") + self.alice = User(username="alice", email="alice@example.org") self.alice.set_password("password") self.alice.save() @@ -19,7 +19,7 @@ class CancelPlanTestCase(TestCase): @patch("hc.payments.views.braintree") def test_it_works(self, mock_braintree): - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") r = self.client.post("/pricing/cancel_plan/") self.assertRedirects(r, "/pricing/") diff --git a/hc/payments/tests/test_create_plan.py b/hc/payments/tests/test_create_plan.py index e1ea632d..a476b167 100644 --- a/hc/payments/tests/test_create_plan.py +++ b/hc/payments/tests/test_create_plan.py @@ -8,7 +8,7 @@ from mock import patch class CreatePlanTestCase(TestCase): def setUp(self): - self.alice = User(username="alice") + self.alice = User(username="alice", email="alice@example.org") self.alice.set_password("password") self.alice.save() @@ -26,7 +26,7 @@ class CreatePlanTestCase(TestCase): def run_create_plan(self, plan_id="P5"): form = {"plan_id": plan_id, "payment_method_nonce": "test-nonce"} - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") return self.client.post("/pricing/create_plan/", form, follow=True) @patch("hc.payments.views.braintree") diff --git a/hc/payments/tests/test_get_client_token.py b/hc/payments/tests/test_get_client_token.py index d1f1f9e1..01ba3b62 100644 --- a/hc/payments/tests/test_get_client_token.py +++ b/hc/payments/tests/test_get_client_token.py @@ -7,14 +7,14 @@ from mock import patch class GetClientTokenTestCase(TestCase): def setUp(self): - self.alice = User(username="alice") + self.alice = User(username="alice", email="alice@example.org") self.alice.set_password("password") self.alice.save() @patch("hc.payments.views.braintree") def test_it_works(self, mock_braintree): mock_braintree.ClientToken.generate.return_value = "test-token" - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") r = self.client.get("/pricing/get_client_token/") self.assertContains(r, "test-token", status_code=200) diff --git a/hc/payments/tests/test_invoice.py b/hc/payments/tests/test_invoice.py index e73120df..8ae735cd 100644 --- a/hc/payments/tests/test_invoice.py +++ b/hc/payments/tests/test_invoice.py @@ -7,7 +7,7 @@ from mock import Mock, patch class InvoiceTestCase(TestCase): def setUp(self): - self.alice = User(username="alice") + self.alice = User(username="alice", email="alice@example.org") self.alice.set_password("password") self.alice.save() @@ -25,7 +25,7 @@ class InvoiceTestCase(TestCase): tx.created_at = None mock_braintree.Transaction.find.return_value = tx - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") r = self.client.get("/invoice/abc123/") self.assertContains(r, "ABC123") # tx.id in uppercase @@ -38,6 +38,6 @@ class InvoiceTestCase(TestCase): tx.created_at = None mock_braintree.Transaction.find.return_value = tx - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") r = self.client.get("/invoice/abc123/") self.assertEqual(r.status_code, 403) diff --git a/hc/payments/tests/test_pricing.py b/hc/payments/tests/test_pricing.py index ddb74247..1ae04855 100644 --- a/hc/payments/tests/test_pricing.py +++ b/hc/payments/tests/test_pricing.py @@ -6,7 +6,7 @@ from hc.payments.models import Subscription class PricingTestCase(TestCase): def setUp(self): - self.alice = User(username="alice") + self.alice = User(username="alice", email="alice@example.org") self.alice.set_password("password") self.alice.save() @@ -18,7 +18,7 @@ class PricingTestCase(TestCase): assert Subscription.objects.count() == 0 def test_authenticated(self): - self.client.login(username="alice", password="password") + self.client.login(username="alice@example.org", password="password") r = self.client.get("/pricing/") self.assertContains(r, "Unlimited Checks", status_code=200) diff --git a/hc/settings.py b/hc/settings.py index 7779fd42..fe435b60 100644 --- a/hc/settings.py +++ b/hc/settings.py @@ -52,7 +52,7 @@ MIDDLEWARE_CLASSES = ( ) AUTHENTICATION_BACKENDS = ( - 'django.contrib.auth.backends.ModelBackend', + 'hc.accounts.backends.EmailBackend', 'hc.accounts.backends.ProfileBackend' ) diff --git a/static/js/login.js b/static/js/login.js new file mode 100644 index 00000000..178ac0fa --- /dev/null +++ b/static/js/login.js @@ -0,0 +1,7 @@ +$(function () { + $("#password-toggle").click(function() { + $("#password-toggle").hide(); + $("#password-block").removeClass("hide"); + }); + +}); \ No newline at end of file diff --git a/templates/accounts/login.html b/templates/accounts/login.html index 16fd829d..aa2ce05e 100644 --- a/templates/accounts/login.html +++ b/templates/accounts/login.html @@ -1,4 +1,5 @@ {% extends "base.html" %} +{% load compress staticfiles %} {% block content %}
@@ -20,22 +21,50 @@
{% endif %} + {% if bad_credentials %} +

Incorrect email or password.

+ {% endif %}
{% csrf_token %}
-
@
+
+ +
+ {% if not bad_credentials %} +
+ +
+ {% endif %} + + +
+
+
+ +
+ +
+
+
+{% endblock %} + +{% block scripts %} +{% compress js %} + + +{% endcompress %} {% endblock %} \ No newline at end of file diff --git a/templates/accounts/profile.html b/templates/accounts/profile.html index 67426cf1..3401905c 100644 --- a/templates/accounts/profile.html +++ b/templates/accounts/profile.html @@ -39,6 +39,23 @@ + +
+
+
+ + {% csrf_token %} +

Set Password

+ Attach a password to your healthchecks.io account + + + +
+
+
+ {% endblock %} diff --git a/templates/accounts/set_password.html b/templates/accounts/set_password.html new file mode 100644 index 00000000..a27e31b2 --- /dev/null +++ b/templates/accounts/set_password.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+

Set a Password

+
+

+ Please pick a password for your healthchecks.io account. +

+
+ +
+ {% csrf_token %} + +
+
+
+ +
+ +
+
+ +
+ +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/accounts/set_password_link_sent.html b/templates/accounts/set_password_link_sent.html new file mode 100644 index 00000000..4ca1acf8 --- /dev/null +++ b/templates/accounts/set_password_link_sent.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+

Email with Instructions Sent!

+
+

+ We've sent you an email with instructions to set + a password for your account. Please check your inbox! +

+ +
+
+
+ +{% endblock %} diff --git a/templates/emails/set-password-body-html.html b/templates/emails/set-password-body-html.html new file mode 100644 index 00000000..e06b1e17 --- /dev/null +++ b/templates/emails/set-password-body-html.html @@ -0,0 +1,10 @@ +

Hello,

+ +

Here's a link to set a password for your account on healthchecks.io:

+

{{ set_password_link }}

+ +

+ --
+ Regards,
+ healthchecks.io +

diff --git a/templates/emails/set-password-subject.html b/templates/emails/set-password-subject.html new file mode 100644 index 00000000..717bec49 --- /dev/null +++ b/templates/emails/set-password-subject.html @@ -0,0 +1 @@ +Set password on healthchecks.io