diff --git a/hc/accounts/views.py b/hc/accounts/views.py index 4b70dbfa..a0c03c9e 100644 --- a/hc/accounts/views.py +++ b/hc/accounts/views.py @@ -1,4 +1,5 @@ import uuid +import re from django.contrib import messages from django.contrib.auth import login as auth_login @@ -15,6 +16,7 @@ from hc.accounts.forms import (EmailPasswordForm, InviteTeamMemberForm, SetPasswordForm, TeamNameForm) from hc.accounts.models import Profile, Member from hc.api.models import Channel, Check +from hc.lib.badges import get_badge_url def _make_user(email): @@ -186,7 +188,20 @@ def profile(request): profile.save() messages.info(request, "Team Name updated!") + tags = set() + for check in Check.objects.filter(user=request.team.user): + tags.update(check.tags_list()) + + username = request.team.user.username + badge_urls = [] + for tag in sorted(tags, key=lambda s: s.lower()): + if not re.match("^[\w-]+$", tag): + continue + + badge_urls.append(get_badge_url(username, tag)) + ctx = { + "badge_urls": badge_urls, "profile": profile, "show_api_key": show_api_key } diff --git a/hc/api/urls.py b/hc/api/urls.py index b89830d0..3a616f24 100644 --- a/hc/api/urls.py +++ b/hc/api/urls.py @@ -6,4 +6,5 @@ urlpatterns = [ url(r'^ping/([\w-]+)/$', views.ping, name="hc-ping-slash"), url(r'^ping/([\w-]+)$', views.ping, name="hc-ping"), url(r'^api/v1/checks/$', views.create_check), + url(r'^badge/([\w-]+)/([\w-]{8})/([\w-]+).svg$', views.badge, name="hc-badge"), ] diff --git a/hc/api/views.py b/hc/api/views.py index e48a4d03..f9164deb 100644 --- a/hc/api/views.py +++ b/hc/api/views.py @@ -9,6 +9,7 @@ from django.views.decorators.csrf import csrf_exempt from hc.api import schemas from hc.api.decorators import check_api_key, uuid_or_400, validate_json from hc.api.models import Check, Ping +from hc.lib.badges import check_signature, get_badge_svg @csrf_exempt @@ -84,3 +85,25 @@ def create_check(request): return HttpResponse(status=405) return JsonResponse(response, status=code) + + +@never_cache +def badge(request, username, signature, tag): + if not check_signature(username, tag, signature): + return HttpResponseBadRequest() + + status = "up" + q = Check.objects.filter(user__username=username, tags__contains=tag) + for check in q: + if tag not in check.tags_list(): + continue + + if status == "up" and check.in_grace_period(): + status = "late" + + if check.get_status() == "down": + status = "down" + break + + svg = get_badge_svg(tag, status) + return HttpResponse(svg, content_type="image/svg+xml") diff --git a/hc/lib/badges.py b/hc/lib/badges.py new file mode 100644 index 00000000..c735f952 --- /dev/null +++ b/hc/lib/badges.py @@ -0,0 +1,55 @@ +from django.conf import settings +from django.core.signing import base64_hmac +from django.template.loader import render_to_string +from django.core.urlresolvers import reverse + +WIDTHS = {"a": 7, "b": 7, "c": 6, "d": 7, "e": 6, "f": 4, "g": 7, "h": 7, + "i": 3, "j": 3, "k": 7, "l": 3, "m": 10, "n": 7, "o": 7, "p": 7, + "q": 7, "r": 4, "s": 6, "t": 5, "u": 7, "v": 7, "w": 9, "x": 6, + "y": 7, "z": 7, "0": 7, "1": 6, "2": 7, "3": 7, "4": 7, "5": 7, + "6": 7, "7": 7, "8": 7, "9": 7, "A": 8, "B": 7, "C": 8, "D": 8, + "E": 7, "F": 6, "G": 9, "H": 8, "I": 3, "J": 4, "K": 7, "L": 6, + "M": 10, "N": 8, "O": 9, "P": 6, "Q": 9, "R": 7, "S": 7, "T": 7, + "U": 8, "V": 8, "W": 11, "X": 7, "Y": 7, "Z": 7, "-": 4, "_": 6} + +COLORS = { + "up": "#4c1", + "late": "#fe7d37", + "down": "#e05d44" +} + + +def get_width(s): + total = 0 + for c in s: + total += WIDTHS.get(c, 7) + return total + + +def get_badge_svg(tag, status): + w1 = get_width(tag) + 10 + w2 = get_width(status) + 10 + ctx = { + "width": w1 + w2, + "tag_width": w1, + "status_width": w2, + "tag_center_x": w1 / 2, + "status_center_x": w1 + w2 / 2, + "tag": tag, + "status": status, + "color": COLORS[status] + } + + return render_to_string("badge.svg", ctx) + + +def check_signature(username, tag, sig): + ours = base64_hmac(str(username), tag, settings.SECRET_KEY) + ours = ours[:8].decode("utf-8") + return ours == sig + + +def get_badge_url(username, tag): + sig = base64_hmac(str(username), tag, settings.SECRET_KEY) + url = reverse("hc-badge", args=[username, sig[:8], tag]) + return settings.SITE_ROOT + url diff --git a/static/css/settings.css b/static/css/settings.css index 37c0468f..ec252da2 100644 --- a/static/css/settings.css +++ b/static/css/settings.css @@ -1,4 +1,4 @@ -#settings-title { +.settings-title { padding-bottom: 24px; } @@ -9,4 +9,8 @@ .settings-block h2 { margin: 0; padding-bottom: 24px; +} + +#badges-description { + margin-bottom: 24px; } \ No newline at end of file diff --git a/templates/accounts/profile.html b/templates/accounts/profile.html index 90f9707c..e7181e87 100644 --- a/templates/accounts/profile.html +++ b/templates/accounts/profile.html @@ -7,7 +7,7 @@ {% block content %}
-

Settings

+

Settings

{% if messages %}
@@ -57,45 +57,6 @@
-
-
-
-

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 %} -
-
-
-
@@ -154,7 +115,76 @@
+ +
+
+
+

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 %} +
+
+
+ + +{% if badge_urls %} +
+
+
+
+

Status Badges

+

+ Here are status badges for each of the tags you have used. The + badges have public, but hard-to-guess URLs. If you wish, you can + add them to your READMEs, dashboards or status pages. +

+ + {% for badge_url in badge_urls %} + + + + + {% endfor %} +
+ + + {{ badge_url }} +
+
+
+
+{% endif %} +