diff --git a/hc/accounts/migrations/0004_profile_api_key.py b/hc/accounts/migrations/0004_profile_api_key.py new file mode 100644 index 00000000..9ca4ed66 --- /dev/null +++ b/hc/accounts/migrations/0004_profile_api_key.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2016-02-16 12:14 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0003_profile_token'), + ] + + operations = [ + migrations.AddField( + model_name='profile', + name='api_key', + field=models.CharField(blank=True, max_length=128), + ), + ] diff --git a/hc/accounts/models.py b/hc/accounts/models.py index 59be222f..72403f44 100644 --- a/hc/accounts/models.py +++ b/hc/accounts/models.py @@ -1,4 +1,8 @@ +import base64 +import os +import uuid from datetime import timedelta + from django.conf import settings from django.contrib.auth.hashers import make_password from django.contrib.auth.models import User @@ -7,7 +11,6 @@ from django.core.urlresolvers import reverse from django.db import models from django.utils import timezone from hc.lib import emails -import uuid class ProfileManager(models.Manager): @@ -28,6 +31,7 @@ class Profile(models.Model): reports_allowed = models.BooleanField(default=True) ping_log_limit = models.IntegerField(default=100) token = models.CharField(max_length=128, blank=True) + api_key = models.CharField(max_length=128, blank=True) objects = ProfileManager() @@ -49,6 +53,10 @@ class Profile(models.Model): ctx = {"set_password_link": settings.SITE_ROOT + path} emails.set_password(self.user.email, ctx) + def set_api_key(self): + self.api_key = base64.urlsafe_b64encode(os.urandom(24)) + self.save() + def send_report(self): # reset next report date first: now = timezone.now() diff --git a/hc/accounts/tests/test_profile.py b/hc/accounts/tests/test_profile.py new file mode 100644 index 00000000..e3cdd6e6 --- /dev/null +++ b/hc/accounts/tests/test_profile.py @@ -0,0 +1,43 @@ +from django.core import mail + +from hc.test import BaseTestCase +from hc.accounts.models import Profile + + +class LoginTestCase(BaseTestCase): + + def test_it_sends_set_password_link(self): + self.client.login(username="alice@example.org", password="password") + + form = {"set_password": "1"} + r = self.client.post("/accounts/profile/", form) + assert r.status_code == 302 + + # profile.token should be set now + profile = Profile.objects.for_user(self.alice) + self.assertTrue(len(profile.token) > 10) + + # And an email should have been sent + self.assertEqual(len(mail.outbox), 1) + expected_subject = 'Set password on healthchecks.io' + self.assertEqual(mail.outbox[0].subject, expected_subject) + + def test_it_creates_api_key(self): + self.client.login(username="alice@example.org", password="password") + + form = {"create_api_key": "1"} + r = self.client.post("/accounts/profile/", form) + assert r.status_code == 200 + + profile = Profile.objects.for_user(self.alice) + self.assertTrue(len(profile.api_key) > 10) + + def test_it_revokes_api_key(self): + self.client.login(username="alice@example.org", password="password") + + form = {"revoke_api_key": "1"} + r = self.client.post("/accounts/profile/", form) + assert r.status_code == 200 + + profile = Profile.objects.for_user(self.alice) + self.assertEqual(profile.api_key, "") diff --git a/hc/accounts/views.py b/hc/accounts/views.py index 07e2affb..20c0613a 100644 --- a/hc/accounts/views.py +++ b/hc/accounts/views.py @@ -120,19 +120,31 @@ def check_token(request, username, token): def profile(request): profile = Profile.objects.for_user(request.user) + show_api_key = False 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"] + elif "create_api_key" in request.POST: + profile.set_api_key() + show_api_key = True + messages.info(request, "The API key has been created!") + elif "revoke_api_key" in request.POST: + profile.api_key = "" profile.save() - messages.info(request, "Your settings have been updated!") + messages.info(request, "The API key has been revoked!") + elif "show_api_key" in request.POST: + show_api_key = True + elif "update_reports_allowed" in request.POST: + form = ReportSettingsForm(request.POST) + if form.is_valid(): + profile.reports_allowed = form.cleaned_data["reports_allowed"] + profile.save() + messages.info(request, "Your settings have been updated!") ctx = { - "profile": profile + "profile": profile, + "show_api_key": show_api_key } return render(request, "accounts/profile.html", ctx) diff --git a/hc/api/migrations/0025_auto_20160216_1214.py b/hc/api/migrations/0025_auto_20160216_1214.py new file mode 100644 index 00000000..2fc02abe --- /dev/null +++ b/hc/api/migrations/0025_auto_20160216_1214.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2016-02-16 12:14 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0024_auto_20160203_2227'), + ] + + operations = [ + migrations.AlterField( + model_name='channel', + name='kind', + field=models.CharField(choices=[('email', 'Email'), ('webhook', 'Webhook'), ('hipchat', 'HipChat'), ('slack', 'Slack'), ('pd', 'PagerDuty'), ('po', 'Pushover'), ('victorops', 'VictorOps')], max_length=20), + ), + ] diff --git a/static/js/checks.js b/static/js/checks.js index 73e4d4a2..47657bd9 100644 --- a/static/js/checks.js +++ b/static/js/checks.js @@ -165,8 +165,4 @@ $(function () { }); - - - - }); \ No newline at end of file diff --git a/templates/accounts/profile.html b/templates/accounts/profile.html index 3401905c..b22c2c26 100644 --- a/templates/accounts/profile.html +++ b/templates/accounts/profile.html @@ -33,6 +33,7 @@ Each month send me a summary of my checks @@ -47,15 +48,84 @@ {% csrf_token %}

Set Password

Attach a password to your healthchecks.io account - +
+
+
+

API Access

+ {% if profile.api_key %} + {% if show_api_key %} + API key: {{ profile.api_key }} + + + {% else %} + + API access is enabled. +
+ {% csrf_token %} + + +
+ {% endif %} + {% else %} + + API access is disabled. +
+ {% csrf_token %} + +
+ {% endif %} +
+
+
+ + + + + {% endblock %}