Browse Source

Users can add passwords to their accounts. Fixes #6

pull/27/head
Pēteris Caune 9 years ago
parent
commit
1dacc8b797
32 changed files with 320 additions and 92 deletions
  1. +22
    -1
      hc/accounts/backends.py
  2. +6
    -1
      hc/accounts/forms.py
  3. +9
    -0
      hc/accounts/models.py
  4. +6
    -5
      hc/accounts/tests/test_check_token.py
  5. +6
    -0
      hc/accounts/urls.py
  6. +66
    -13
      hc/accounts/views.py
  7. +8
    -8
      hc/front/tests/test_add_channel.py
  8. +2
    -2
      hc/front/tests/test_add_check.py
  9. +5
    -5
      hc/front/tests/test_channel_checks.py
  10. +6
    -6
      hc/front/tests/test_log.py
  11. +2
    -2
      hc/front/tests/test_my_checks.py
  12. +6
    -6
      hc/front/tests/test_remove_channel.py
  13. +6
    -6
      hc/front/tests/test_remove_check.py
  14. +8
    -8
      hc/front/tests/test_update_channel.py
  15. +7
    -7
      hc/front/tests/test_update_name.py
  16. +6
    -6
      hc/front/tests/test_update_timeout.py
  17. +1
    -1
      hc/front/tests/test_verify_email.py
  18. +5
    -0
      hc/lib/emails.py
  19. +2
    -2
      hc/payments/tests/test_billing.py
  20. +2
    -2
      hc/payments/tests/test_cancel_plan.py
  21. +2
    -2
      hc/payments/tests/test_create_plan.py
  22. +2
    -2
      hc/payments/tests/test_get_client_token.py
  23. +3
    -3
      hc/payments/tests/test_invoice.py
  24. +2
    -2
      hc/payments/tests/test_pricing.py
  25. +1
    -1
      hc/settings.py
  26. +7
    -0
      static/js/login.js
  27. +37
    -1
      templates/accounts/login.html
  28. +17
    -0
      templates/accounts/profile.html
  29. +39
    -0
      templates/accounts/set_password.html
  30. +18
    -0
      templates/accounts/set_password_link_sent.html
  31. +10
    -0
      templates/emails/set-password-body-html.html
  32. +1
    -0
      templates/emails/set-password-subject.html

+ 22
- 1
hc/accounts/backends.py View File

@ -3,8 +3,17 @@ from django.contrib.auth.models import User
from hc.accounts.models import Profile 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. # Authenticate against the token in user's profile.
class ProfileBackend(object):
class ProfileBackend(BasicBackend):
def authenticate(self, username=None, token=None): def authenticate(self, username=None, token=None):
try: try:
@ -22,3 +31,15 @@ class ProfileBackend(object):
return User.objects.get(pk=user_id) return User.objects.get(pk=user_id)
except User.DoesNotExist: except User.DoesNotExist:
return None 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

+ 6
- 1
hc/accounts/forms.py View File

@ -8,9 +8,14 @@ class LowercaseEmailField(forms.EmailField):
return value.lower() return value.lower()
class EmailForm(forms.Form):
class EmailPasswordForm(forms.Form):
email = LowercaseEmailField() email = LowercaseEmailField()
password = forms.CharField(required=False)
class ReportSettingsForm(forms.Form): class ReportSettingsForm(forms.Form):
reports_allowed = forms.BooleanField(required=False) reports_allowed = forms.BooleanField(required=False)
class SetPasswordForm(forms.Form):
password = forms.CharField()

+ 9
- 0
hc/accounts/models.py View File

@ -40,6 +40,15 @@ class Profile(models.Model):
ctx = {"login_link": settings.SITE_ROOT + path} ctx = {"login_link": settings.SITE_ROOT + path}
emails.login(self.user.email, ctx) 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): def send_report(self):
# reset next report date first: # reset next report date first:
now = timezone.now() now = timezone.now()


+ 6
- 5
hc/accounts/tests/test_check_token.py View File

@ -10,7 +10,8 @@ class CheckTokenTestCase(TestCase):
def setUp(self): def setUp(self):
super(CheckTokenTestCase, self).setUp() super(CheckTokenTestCase, self).setUp()
self.alice = User(username="alice")
self.alice = User(username="alice", email="[email protected]")
self.alice.set_password("password")
self.alice.save() self.alice.save()
self.profile = Profile(user=self.alice) self.profile = Profile(user=self.alice)
@ -21,13 +22,13 @@ class CheckTokenTestCase(TestCase):
r = self.client.get("/accounts/check_token/alice/secret-token/") r = self.client.get("/accounts/check_token/alice/secret-token/")
self.assertRedirects(r, "/checks/") 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): def test_it_redirects_already_logged_in(self):
# Login # Login
self.client.get("/accounts/check_token/alice/secret-token/")
self.client.login(username="[email protected]", password="password")
# Login again, when already authenticated # Login again, when already authenticated
r = self.client.get("/accounts/check_token/alice/secret-token/") r = self.client.get("/accounts/check_token/alice/secret-token/")


+ 6
- 0
hc/accounts/urls.py View File

@ -7,6 +7,9 @@ urlpatterns = [
url(r'^login_link_sent/$', url(r'^login_link_sent/$',
views.login_link_sent, name="hc-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-]+)/$', url(r'^check_token/([\w-]+)/([\w-]+)/$',
views.check_token, name="hc-check-token"), views.check_token, name="hc-check-token"),
@ -15,4 +18,7 @@ urlpatterns = [
url(r'^unsubscribe_reports/([\w-]+)/$', url(r'^unsubscribe_reports/([\w-]+)/$',
views.unsubscribe_reports, name="hc-unsubscribe-reports"), views.unsubscribe_reports, name="hc-unsubscribe-reports"),
url(r'^set_password/([\w-]+)/$',
views.set_password, name="hc-set-password"),
] ]

+ 66
- 13
hc/accounts/views.py View File

@ -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 logout as auth_logout
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from django.contrib.auth.decorators import login_required 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.contrib.auth.models import User
from django.core import signing from django.core import signing
from django.http import HttpResponseBadRequest from django.http import HttpResponseBadRequest
from django.shortcuts import redirect, render 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.accounts.models import Profile
from hc.api.models import Channel, Check from hc.api.models import Channel, Check
@ -45,25 +47,38 @@ def _associate_demo_check(request, user):
def login(request): def login(request):
bad_credentials = False
if request.method == 'POST': if request.method == 'POST':
form = EmailForm(request.POST)
form = EmailPasswordForm(request.POST)
if form.is_valid(): if form.is_valid():
email = form.cleaned_data["email"] 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: else:
form = EmailForm()
form = EmailPasswordForm()
bad_link = request.session.pop("bad_link", None) 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) return render(request, "accounts/login.html", ctx)
@ -76,6 +91,10 @@ def login_link_sent(request):
return render(request, "accounts/login_link_sent.html") 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): def check_token(request, username, token):
if request.user.is_authenticated() and request.user.username == username: if request.user.is_authenticated() and request.user.username == username:
# User is already logged in # User is already logged in
@ -102,6 +121,10 @@ def profile(request):
profile = Profile.objects.for_user(request.user) profile = Profile.objects.for_user(request.user)
if request.method == "POST": 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) form = ReportSettingsForm(request.POST)
if form.is_valid(): if form.is_valid():
profile.reports_allowed = form.cleaned_data["reports_allowed"] profile.reports_allowed = form.cleaned_data["reports_allowed"]
@ -115,6 +138,36 @@ def profile(request):
return render(request, "accounts/profile.html", ctx) 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): def unsubscribe_reports(request, username):
try: try:
signing.Signer().unsign(request.GET.get("token")) signing.Signer().unsign(request.GET.get("token"))


+ 8
- 8
hc/front/tests/test_add_channel.py View File

@ -7,7 +7,7 @@ from hc.api.models import Channel
class AddChannelTestCase(TestCase): class AddChannelTestCase(TestCase):
def setUp(self): def setUp(self):
self.alice = User(username="alice")
self.alice = User(username="alice", email="[email protected]")
self.alice.set_password("password") self.alice.set_password("password")
self.alice.save() self.alice.save()
@ -18,7 +18,7 @@ class AddChannelTestCase(TestCase):
url = "/integrations/add/" url = "/integrations/add/"
form = {"kind": "email", "value": "[email protected]"} form = {"kind": "email", "value": "[email protected]"}
self.client.login(username="alice", password="password")
self.client.login(username="alice@example.org", password="password")
r = self.client.post(url, form) r = self.client.post(url, form)
self.assertRedirects(r, "/integrations/") self.assertRedirects(r, "/integrations/")
@ -30,7 +30,7 @@ class AddChannelTestCase(TestCase):
url = "/integrations/add/" url = "/integrations/add/"
form = {"kind": "email", "value": " [email protected] "} form = {"kind": "email", "value": " [email protected] "}
self.client.login(username="alice", password="password")
self.client.login(username="alice@example.org", password="password")
self.client.post(url, form) self.client.post(url, form)
q = Channel.objects.filter(value="[email protected]") q = Channel.objects.filter(value="[email protected]")
@ -40,20 +40,20 @@ class AddChannelTestCase(TestCase):
url = "/integrations/add/" url = "/integrations/add/"
form = {"kind": "dog", "value": "Lassie"} 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) r = self.client.post(url, form)
assert r.status_code == 400, r.status_code assert r.status_code == 400, r.status_code
def test_instructions_work(self): 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"): for frag in ("email", "webhook", "pd", "pushover", "slack", "hipchat"):
url = "/integrations/add_%s/" % frag url = "/integrations/add_%s/" % frag
r = self.client.get(url) r = self.client.get(url)
self.assertContains(r, "Integration Settings", status_code=200) self.assertContains(r, "Integration Settings", status_code=200)
def test_it_adds_pushover_channel(self): 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 = self.client.session
session["po_nonce"] = "n" session["po_nonce"] = "n"
@ -68,7 +68,7 @@ class AddChannelTestCase(TestCase):
assert channels[0].value == "a|0" assert channels[0].value == "a|0"
def test_it_validates_pushover_priority(self): 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 = self.client.session
session["po_nonce"] = "n" session["po_nonce"] = "n"
@ -79,7 +79,7 @@ class AddChannelTestCase(TestCase):
assert r.status_code == 400 assert r.status_code == 400
def test_it_validates_pushover_nonce(self): 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 = self.client.session
session["po_nonce"] = "n" session["po_nonce"] = "n"


+ 2
- 2
hc/front/tests/test_add_check.py View File

@ -6,13 +6,13 @@ from hc.api.models import Check
class AddCheckTestCase(TestCase): class AddCheckTestCase(TestCase):
def setUp(self): def setUp(self):
self.alice = User(username="alice")
self.alice = User(username="alice", email="[email protected]")
self.alice.set_password("password") self.alice.set_password("password")
self.alice.save() self.alice.save()
def test_it_works(self): def test_it_works(self):
url = "/checks/add/" url = "/checks/add/"
self.client.login(username="alice", password="password")
self.client.login(username="alice@example.org", password="password")
r = self.client.post(url) r = self.client.post(url)
self.assertRedirects(r, "/checks/") self.assertRedirects(r, "/checks/")
assert Check.objects.count() == 1 assert Check.objects.count() == 1

+ 5
- 5
hc/front/tests/test_channel_checks.py View File

@ -6,7 +6,7 @@ from hc.api.models import Channel
class ChannelChecksTestCase(TestCase): class ChannelChecksTestCase(TestCase):
def setUp(self): def setUp(self):
self.alice = User(username="alice")
self.alice = User(username="alice", email="[email protected]")
self.alice.set_password("password") self.alice.set_password("password")
self.alice.save() self.alice.save()
@ -17,19 +17,19 @@ class ChannelChecksTestCase(TestCase):
def test_it_works(self): def test_it_works(self):
url = "/integrations/%s/checks/" % self.channel.code 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) r = self.client.get(url)
self.assertContains(r, "[email protected]", status_code=200) self.assertContains(r, "[email protected]", status_code=200)
def test_it_checks_owner(self): def test_it_checks_owner(self):
mallory = User(username="mallory")
mallory = User(username="mallory", email="[email protected]")
mallory.set_password("password") mallory.set_password("password")
mallory.save() mallory.save()
# channel does not belong to mallory so this should come back # channel does not belong to mallory so this should come back
# with 403 Forbidden: # with 403 Forbidden:
url = "/integrations/%s/checks/" % self.channel.code 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) r = self.client.get(url)
assert r.status_code == 403 assert r.status_code == 403
@ -37,6 +37,6 @@ class ChannelChecksTestCase(TestCase):
# Valid UUID but there is no channel for it: # Valid UUID but there is no channel for it:
url = "/integrations/6837d6ec-fc08-4da5-a67f-08a9ed1ccf62/checks/" 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) r = self.client.get(url)
assert r.status_code == 404 assert r.status_code == 404

+ 6
- 6
hc/front/tests/test_log.py View File

@ -6,7 +6,7 @@ from hc.api.models import Check, Ping
class LogTestCase(TestCase): class LogTestCase(TestCase):
def setUp(self): def setUp(self):
self.alice = User(username="alice")
self.alice = User(username="alice", email="[email protected]")
self.alice.set_password("password") self.alice.set_password("password")
self.alice.save() self.alice.save()
@ -19,14 +19,14 @@ class LogTestCase(TestCase):
def test_it_works(self): def test_it_works(self):
url = "/checks/%s/log/" % self.check.code 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) r = self.client.get(url)
self.assertContains(r, "Dates and times are", status_code=200) self.assertContains(r, "Dates and times are", status_code=200)
def test_it_handles_bad_uuid(self): def test_it_handles_bad_uuid(self):
url = "/checks/not-uuid/log/" 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) r = self.client.get(url)
assert r.status_code == 400 assert r.status_code == 400
@ -34,16 +34,16 @@ class LogTestCase(TestCase):
# Valid UUID but there is no check for it: # Valid UUID but there is no check for it:
url = "/checks/6837d6ec-fc08-4da5-a67f-08a9ed1ccf62/log/" 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) r = self.client.get(url)
assert r.status_code == 404 assert r.status_code == 404
def test_it_checks_ownership(self): def test_it_checks_ownership(self):
charlie = User(username="charlie")
charlie = User(username="charlie", email="[email protected]")
charlie.set_password("password") charlie.set_password("password")
charlie.save() charlie.save()
url = "/checks/%s/log/" % self.check.code 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) r = self.client.get(url)
assert r.status_code == 403 assert r.status_code == 403

+ 2
- 2
hc/front/tests/test_my_checks.py View File

@ -6,7 +6,7 @@ from hc.api.models import Check
class MyChecksTestCase(TestCase): class MyChecksTestCase(TestCase):
def setUp(self): def setUp(self):
self.alice = User(username="alice")
self.alice = User(username="alice", email="[email protected]")
self.alice.set_password("password") self.alice.set_password("password")
self.alice.save() self.alice.save()
@ -14,6 +14,6 @@ class MyChecksTestCase(TestCase):
self.check.save() self.check.save()
def test_it_works(self): 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/") r = self.client.get("/checks/")
self.assertContains(r, "Alice Was Here", status_code=200) self.assertContains(r, "Alice Was Here", status_code=200)

+ 6
- 6
hc/front/tests/test_remove_channel.py View File

@ -6,7 +6,7 @@ from hc.api.models import Channel
class RemoveChannelTestCase(TestCase): class RemoveChannelTestCase(TestCase):
def setUp(self): def setUp(self):
self.alice = User(username="alice")
self.alice = User(username="alice", email="[email protected]")
self.alice.set_password("password") self.alice.set_password("password")
self.alice.save() self.alice.save()
@ -17,7 +17,7 @@ class RemoveChannelTestCase(TestCase):
def test_it_works(self): def test_it_works(self):
url = "/integrations/%s/remove/" % self.channel.code 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) r = self.client.post(url)
self.assertRedirects(r, "/integrations/") self.assertRedirects(r, "/integrations/")
@ -26,18 +26,18 @@ class RemoveChannelTestCase(TestCase):
def test_it_handles_bad_uuid(self): def test_it_handles_bad_uuid(self):
url = "/integrations/not-uuid/remove/" 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) r = self.client.post(url)
assert r.status_code == 400 assert r.status_code == 400
def test_it_checks_owner(self): def test_it_checks_owner(self):
url = "/integrations/%s/remove/" % self.channel.code url = "/integrations/%s/remove/" % self.channel.code
mallory = User(username="mallory")
mallory = User(username="mallory", email="[email protected]")
mallory.set_password("password") mallory.set_password("password")
mallory.save() mallory.save()
self.client.login(username="mallory", password="password")
self.client.login(username="mallory@example.org", password="password")
r = self.client.post(url) r = self.client.post(url)
assert r.status_code == 403 assert r.status_code == 403
@ -45,6 +45,6 @@ class RemoveChannelTestCase(TestCase):
# Valid UUID but there is no channel for it: # Valid UUID but there is no channel for it:
url = "/integrations/6837d6ec-fc08-4da5-a67f-08a9ed1ccf62/remove/" 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) r = self.client.post(url)
assert r.status_code == 404 assert r.status_code == 404

+ 6
- 6
hc/front/tests/test_remove_check.py View File

@ -6,7 +6,7 @@ from hc.api.models import Check
class RemoveCheckTestCase(TestCase): class RemoveCheckTestCase(TestCase):
def setUp(self): def setUp(self):
self.alice = User(username="alice")
self.alice = User(username="alice", email="[email protected]")
self.alice.set_password("password") self.alice.set_password("password")
self.alice.save() self.alice.save()
@ -16,7 +16,7 @@ class RemoveCheckTestCase(TestCase):
def test_it_works(self): def test_it_works(self):
url = "/checks/%s/remove/" % self.check.code 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) r = self.client.post(url)
self.assertRedirects(r, "/checks/") self.assertRedirects(r, "/checks/")
@ -25,18 +25,18 @@ class RemoveCheckTestCase(TestCase):
def test_it_handles_bad_uuid(self): def test_it_handles_bad_uuid(self):
url = "/checks/not-uuid/remove/" 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) r = self.client.post(url)
assert r.status_code == 400 assert r.status_code == 400
def test_it_checks_owner(self): def test_it_checks_owner(self):
url = "/checks/%s/remove/" % self.check.code url = "/checks/%s/remove/" % self.check.code
mallory = User(username="mallory")
mallory = User(username="mallory", email="[email protected]")
mallory.set_password("password") mallory.set_password("password")
mallory.save() mallory.save()
self.client.login(username="mallory", password="password")
self.client.login(username="mallory@example.org", password="password")
r = self.client.post(url) r = self.client.post(url)
assert r.status_code == 403 assert r.status_code == 403
@ -44,6 +44,6 @@ class RemoveCheckTestCase(TestCase):
# Valid UUID but there is no check for it: # Valid UUID but there is no check for it:
url = "/checks/6837d6ec-fc08-4da5-a67f-08a9ed1ccf62/remove/" 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) r = self.client.post(url)
assert r.status_code == 404 assert r.status_code == 404

+ 8
- 8
hc/front/tests/test_update_channel.py View File

@ -6,7 +6,7 @@ from hc.api.models import Channel, Check
class UpdateChannelTestCase(TestCase): class UpdateChannelTestCase(TestCase):
def setUp(self): def setUp(self):
self.alice = User(username="alice")
self.alice = User(username="alice", email="[email protected]")
self.alice.set_password("password") self.alice.set_password("password")
self.alice.save() self.alice.save()
@ -23,7 +23,7 @@ class UpdateChannelTestCase(TestCase):
"check-%s" % self.check.code: True "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) r = self.client.post("/integrations/", data=payload)
self.assertRedirects(r, "/integrations/") self.assertRedirects(r, "/integrations/")
@ -33,20 +33,20 @@ class UpdateChannelTestCase(TestCase):
assert checks[0].code == self.check.code assert checks[0].code == self.check.code
def test_it_checks_channel_user(self): def test_it_checks_channel_user(self):
mallory = User(username="mallory")
mallory = User(username="mallory", email="[email protected]")
mallory.set_password("password") mallory.set_password("password")
mallory.save() mallory.save()
payload = {"channel": self.channel.code} 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) r = self.client.post("/integrations/", data=payload)
# self.channel does not belong to mallory, this should fail-- # self.channel does not belong to mallory, this should fail--
assert r.status_code == 403 assert r.status_code == 403
def test_it_checks_check_user(self): def test_it_checks_check_user(self):
mallory = User(username="mallory")
mallory = User(username="mallory", email="[email protected]")
mallory.set_password("password") mallory.set_password("password")
mallory.save() mallory.save()
@ -58,7 +58,7 @@ class UpdateChannelTestCase(TestCase):
"channel": mc.code, "channel": mc.code,
"check-%s" % self.check.code: True "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) r = self.client.post("/integrations/", data=payload)
# mc belongs to mallorym but self.check does not-- # 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: # Correct UUID but there is no channel for it:
payload = {"channel": "6837d6ec-fc08-4da5-a67f-08a9ed1ccf62"} 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) r = self.client.post("/integrations/", data=payload)
assert r.status_code == 400 assert r.status_code == 400
@ -79,6 +79,6 @@ class UpdateChannelTestCase(TestCase):
"check-6837d6ec-fc08-4da5-a67f-08a9ed1ccf62": True "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) r = self.client.post("/integrations/", data=payload)
assert r.status_code == 400 assert r.status_code == 400

+ 7
- 7
hc/front/tests/test_update_name.py View File

@ -6,7 +6,7 @@ from hc.api.models import Check
class UpdateNameTestCase(TestCase): class UpdateNameTestCase(TestCase):
def setUp(self): def setUp(self):
self.alice = User(username="alice")
self.alice = User(username="alice", email="[email protected]")
self.alice.set_password("password") self.alice.set_password("password")
self.alice.save() self.alice.save()
@ -17,7 +17,7 @@ class UpdateNameTestCase(TestCase):
url = "/checks/%s/name/" % self.check.code url = "/checks/%s/name/" % self.check.code
payload = {"name": "Alice Was Here"} 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) r = self.client.post(url, data=payload)
self.assertRedirects(r, "/checks/") self.assertRedirects(r, "/checks/")
@ -26,14 +26,14 @@ class UpdateNameTestCase(TestCase):
def test_it_checks_ownership(self): def test_it_checks_ownership(self):
charlie = User(username="charlie")
charlie = User(username="charlie", email="[email protected]")
charlie.set_password("password") charlie.set_password("password")
charlie.save() charlie.save()
url = "/checks/%s/name/" % self.check.code url = "/checks/%s/name/" % self.check.code
payload = {"name": "Charlie Sent This"} 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) r = self.client.post(url, data=payload)
assert r.status_code == 403 assert r.status_code == 403
@ -41,7 +41,7 @@ class UpdateNameTestCase(TestCase):
url = "/checks/not-uuid/name/" url = "/checks/not-uuid/name/"
payload = {"name": "Alice Was Here"} 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) r = self.client.post(url, data=payload)
assert r.status_code == 400 assert r.status_code == 400
@ -50,7 +50,7 @@ class UpdateNameTestCase(TestCase):
url = "/checks/6837d6ec-fc08-4da5-a67f-08a9ed1ccf62/name/" url = "/checks/6837d6ec-fc08-4da5-a67f-08a9ed1ccf62/name/"
payload = {"name": "Alice Was Here"} 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) r = self.client.post(url, data=payload)
assert r.status_code == 404 assert r.status_code == 404
@ -58,7 +58,7 @@ class UpdateNameTestCase(TestCase):
url = "/checks/%s/name/" % self.check.code url = "/checks/%s/name/" % self.check.code
payload = {"tags": " foo bar\r\t \n baz \n"} 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) self.client.post(url, data=payload)
check = Check.objects.get(id=self.check.id) check = Check.objects.get(id=self.check.id)


+ 6
- 6
hc/front/tests/test_update_timeout.py View File

@ -6,7 +6,7 @@ from hc.api.models import Check
class UpdateTimeoutTestCase(TestCase): class UpdateTimeoutTestCase(TestCase):
def setUp(self): def setUp(self):
self.alice = User(username="alice")
self.alice = User(username="alice", email="[email protected]")
self.alice.set_password("password") self.alice.set_password("password")
self.alice.save() self.alice.save()
@ -17,7 +17,7 @@ class UpdateTimeoutTestCase(TestCase):
url = "/checks/%s/timeout/" % self.check.code url = "/checks/%s/timeout/" % self.check.code
payload = {"timeout": 3600, "grace": 60} 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) r = self.client.post(url, data=payload)
self.assertRedirects(r, "/checks/") self.assertRedirects(r, "/checks/")
@ -29,7 +29,7 @@ class UpdateTimeoutTestCase(TestCase):
url = "/checks/not-uuid/timeout/" url = "/checks/not-uuid/timeout/"
payload = {"timeout": 3600, "grace": 60} 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) r = self.client.post(url, data=payload)
assert r.status_code == 400 assert r.status_code == 400
@ -38,18 +38,18 @@ class UpdateTimeoutTestCase(TestCase):
url = "/checks/6837d6ec-fc08-4da5-a67f-08a9ed1ccf62/timeout/" url = "/checks/6837d6ec-fc08-4da5-a67f-08a9ed1ccf62/timeout/"
payload = {"timeout": 3600, "grace": 60} 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) r = self.client.post(url, data=payload)
assert r.status_code == 404 assert r.status_code == 404
def test_it_checks_ownership(self): def test_it_checks_ownership(self):
charlie = User(username="charlie")
charlie = User(username="charlie", email="[email protected]")
charlie.set_password("password") charlie.set_password("password")
charlie.save() charlie.save()
url = "/checks/%s/timeout/" % self.check.code url = "/checks/%s/timeout/" % self.check.code
payload = {"timeout": 3600, "grace": 60} 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) r = self.client.post(url, data=payload)
assert r.status_code == 403 assert r.status_code == 403

+ 1
- 1
hc/front/tests/test_verify_email.py View File

@ -6,7 +6,7 @@ from hc.api.models import Channel
class VerifyEmailTestCase(TestCase): class VerifyEmailTestCase(TestCase):
def setUp(self): def setUp(self):
self.alice = User(username="alice")
self.alice = User(username="alice", email="[email protected]")
self.alice.set_password("password") self.alice.set_password("password")
self.alice.save() self.alice.save()


+ 5
- 0
hc/lib/emails.py View File

@ -6,6 +6,11 @@ def login(to, ctx):
o.send(to, ctx) o.send(to, ctx)
def set_password(to, ctx):
o = InlineCSSTemplateMail("set-password")
o.send(to, ctx)
def alert(to, ctx): def alert(to, ctx):
o = InlineCSSTemplateMail("alert") o = InlineCSSTemplateMail("alert")
o.send(to, ctx) o.send(to, ctx)


+ 2
- 2
hc/payments/tests/test_billing.py View File

@ -7,7 +7,7 @@ from mock import Mock, patch
class BillingTestCase(TestCase): class BillingTestCase(TestCase):
def setUp(self): def setUp(self):
self.alice = User(username="alice")
self.alice = User(username="alice", email="[email protected]")
self.alice.set_password("password") self.alice.set_password("password")
self.alice.save() self.alice.save()
@ -23,7 +23,7 @@ class BillingTestCase(TestCase):
m2 = Mock(id="def456", amount=456) m2 = Mock(id="def456", amount=456)
mock_braintree.Transaction.search.return_value = [m1, m2] 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/") r = self.client.get("/billing/")
self.assertContains(r, "123") self.assertContains(r, "123")
self.assertContains(r, "def456") self.assertContains(r, "def456")

+ 2
- 2
hc/payments/tests/test_cancel_plan.py View File

@ -7,7 +7,7 @@ from mock import patch
class CancelPlanTestCase(TestCase): class CancelPlanTestCase(TestCase):
def setUp(self): def setUp(self):
self.alice = User(username="alice")
self.alice = User(username="alice", email="[email protected]")
self.alice.set_password("password") self.alice.set_password("password")
self.alice.save() self.alice.save()
@ -19,7 +19,7 @@ class CancelPlanTestCase(TestCase):
@patch("hc.payments.views.braintree") @patch("hc.payments.views.braintree")
def test_it_works(self, mock_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/") r = self.client.post("/pricing/cancel_plan/")
self.assertRedirects(r, "/pricing/") self.assertRedirects(r, "/pricing/")


+ 2
- 2
hc/payments/tests/test_create_plan.py View File

@ -8,7 +8,7 @@ from mock import patch
class CreatePlanTestCase(TestCase): class CreatePlanTestCase(TestCase):
def setUp(self): def setUp(self):
self.alice = User(username="alice")
self.alice = User(username="alice", email="[email protected]")
self.alice.set_password("password") self.alice.set_password("password")
self.alice.save() self.alice.save()
@ -26,7 +26,7 @@ class CreatePlanTestCase(TestCase):
def run_create_plan(self, plan_id="P5"): def run_create_plan(self, plan_id="P5"):
form = {"plan_id": plan_id, "payment_method_nonce": "test-nonce"} 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) return self.client.post("/pricing/create_plan/", form, follow=True)
@patch("hc.payments.views.braintree") @patch("hc.payments.views.braintree")


+ 2
- 2
hc/payments/tests/test_get_client_token.py View File

@ -7,14 +7,14 @@ from mock import patch
class GetClientTokenTestCase(TestCase): class GetClientTokenTestCase(TestCase):
def setUp(self): def setUp(self):
self.alice = User(username="alice")
self.alice = User(username="alice", email="[email protected]")
self.alice.set_password("password") self.alice.set_password("password")
self.alice.save() self.alice.save()
@patch("hc.payments.views.braintree") @patch("hc.payments.views.braintree")
def test_it_works(self, mock_braintree): def test_it_works(self, mock_braintree):
mock_braintree.ClientToken.generate.return_value = "test-token" 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/") r = self.client.get("/pricing/get_client_token/")
self.assertContains(r, "test-token", status_code=200) self.assertContains(r, "test-token", status_code=200)


+ 3
- 3
hc/payments/tests/test_invoice.py View File

@ -7,7 +7,7 @@ from mock import Mock, patch
class InvoiceTestCase(TestCase): class InvoiceTestCase(TestCase):
def setUp(self): def setUp(self):
self.alice = User(username="alice")
self.alice = User(username="alice", email="[email protected]")
self.alice.set_password("password") self.alice.set_password("password")
self.alice.save() self.alice.save()
@ -25,7 +25,7 @@ class InvoiceTestCase(TestCase):
tx.created_at = None tx.created_at = None
mock_braintree.Transaction.find.return_value = tx 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/") r = self.client.get("/invoice/abc123/")
self.assertContains(r, "ABC123") # tx.id in uppercase self.assertContains(r, "ABC123") # tx.id in uppercase
@ -38,6 +38,6 @@ class InvoiceTestCase(TestCase):
tx.created_at = None tx.created_at = None
mock_braintree.Transaction.find.return_value = tx 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/") r = self.client.get("/invoice/abc123/")
self.assertEqual(r.status_code, 403) self.assertEqual(r.status_code, 403)

+ 2
- 2
hc/payments/tests/test_pricing.py View File

@ -6,7 +6,7 @@ from hc.payments.models import Subscription
class PricingTestCase(TestCase): class PricingTestCase(TestCase):
def setUp(self): def setUp(self):
self.alice = User(username="alice")
self.alice = User(username="alice", email="[email protected]")
self.alice.set_password("password") self.alice.set_password("password")
self.alice.save() self.alice.save()
@ -18,7 +18,7 @@ class PricingTestCase(TestCase):
assert Subscription.objects.count() == 0 assert Subscription.objects.count() == 0
def test_authenticated(self): 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/") r = self.client.get("/pricing/")
self.assertContains(r, "Unlimited Checks", status_code=200) self.assertContains(r, "Unlimited Checks", status_code=200)


+ 1
- 1
hc/settings.py View File

@ -52,7 +52,7 @@ MIDDLEWARE_CLASSES = (
) )
AUTHENTICATION_BACKENDS = ( AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'hc.accounts.backends.EmailBackend',
'hc.accounts.backends.ProfileBackend' 'hc.accounts.backends.ProfileBackend'
) )


+ 7
- 0
static/js/login.js View File

@ -0,0 +1,7 @@
$(function () {
$("#password-toggle").click(function() {
$("#password-toggle").hide();
$("#password-block").removeClass("hide");
});
});

+ 37
- 1
templates/accounts/login.html View File

@ -1,4 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load compress staticfiles %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
@ -20,22 +21,50 @@
</div> </div>
{% endif %} {% endif %}
{% if bad_credentials %}
<p class="alert alert-danger">Incorrect email or password.</p>
{% endif %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
<div class="form-group"> <div class="form-group">
<div class="input-group input-group-lg"> <div class="input-group input-group-lg">
<div class="input-group-addon">@</div>
<div class="input-group-addon">
<span class="glyphicon glyphicon-user"></span>
</div>
<input <input
type="text" type="text"
class="form-control" class="form-control"
id="id_email" id="id_email"
name="email" name="email"
value="{{ form.email.value|default:"" }}"
placeholder="Email"> placeholder="Email">
</div> </div>
</div> </div>
{% if not bad_credentials %}
<div class="checkbox" id="password-toggle">
<label>
<input type="checkbox"> I want to use a password
</label>
</div>
{% endif %}
<div id="password-block" class="form-group {% if not bad_credentials %} hide {% endif %}">
<div class="input-group input-group-lg">
<div class="input-group-addon">
<span class="glyphicon glyphicon-lock"></span>
</div>
<input
type="password"
class="form-control"
name="password"
placeholder="password">
</div>
</div>
<div class="clearfix"> <div class="clearfix">
<button type="submit" class="btn btn-lg btn-primary pull-right"> <button type="submit" class="btn btn-lg btn-primary pull-right">
Log In Log In
@ -45,4 +74,11 @@
</div> </div>
</div> </div>
</div> </div>
{% endblock %}
{% block scripts %}
{% compress js %}
<script src="{% static 'js/jquery-2.1.4.min.js' %}"></script>
<script src="{% static 'js/login.js' %}"></script>
{% endcompress %}
{% endblock %} {% endblock %}

+ 17
- 0
templates/accounts/profile.html View File

@ -39,6 +39,23 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col-sm-6">
<div class="panel panel-default">
<div class="panel-body settings-block">
<form method="post">
{% csrf_token %}
<h2>Set Password</h2>
Attach a password to your healthchecks.io account
<input type="hidden" name="set_password" value="1" />
<button
type="submit"
class="btn btn-default pull-right">Set Password</button>
</form>
</div>
</div>
</div>
</div> </div>
{% endblock %} {% endblock %}

+ 39
- 0
templates/accounts/set_password.html View File

@ -0,0 +1,39 @@
{% extends "base.html" %}
{% block content %}
<div class="row">
<div class="col-sm-6 col-sm-offset-3">
<div class="hc-dialog">
<h1>Set a Password</h1>
<div class="dialog-body">
<p>
Please pick a password for your healthchecks.io account.
</p>
</div>
<form method="post">
{% csrf_token %}
<div class="form-group">
<div class="input-group input-group-lg">
<div class="input-group-addon">
<span class="glyphicon glyphicon-user"></span>
</div>
<input
type="password"
class="form-control"
name="password"
placeholder="pick a password">
</div>
</div>
<div class="clearfix">
<button type="submit" class="btn btn-lg btn-primary pull-right">
Set Password
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

+ 18
- 0
templates/accounts/set_password_link_sent.html View File

@ -0,0 +1,18 @@
{% extends "base.html" %}
{% block content %}
<div class="row">
<div class="col-sm-6 col-sm-offset-3">
<div class="hc-dialog">
<h1>Email with Instructions Sent!</h1>
<br />
<p>
We've sent you an email with instructions to set
a password for your account. Please check your inbox!
</p>
</div>
</div>
</div>
{% endblock %}

+ 10
- 0
templates/emails/set-password-body-html.html View File

@ -0,0 +1,10 @@
<p>Hello,</p>
<p>Here's a link to set a password for your account on healthchecks.io:</p>
<p><a href="{{ set_password_link }}">{{ set_password_link }}</a></p>
<p>
--<br />
Regards,<br />
healthchecks.io
</p>

+ 1
- 0
templates/emails/set-password-subject.html View File

@ -0,0 +1 @@
Set password on healthchecks.io

Loading…
Cancel
Save