mirror of
https://github.com/healthchecks/healthchecks.git
synced 2025-04-04 21:05:26 +00:00
Add a "verify number" step in the Signal onboarding flow
This commit is contained in:
parent
39baf36340
commit
8d06a3e896
8 changed files with 192 additions and 12 deletions
hc
static/js
templates/integrations
|
@ -1184,6 +1184,13 @@ class TokenBucket(models.Model):
|
|||
# 6 messages for a single recipient per minute:
|
||||
return TokenBucket.authorize(value, 6, 60)
|
||||
|
||||
@staticmethod
|
||||
def authorize_signal_verification(user):
|
||||
value = "signal-verify-%d" % user.id
|
||||
|
||||
# 50 signal recipient verifications per day
|
||||
return TokenBucket.authorize(value, 50, 3600 * 24)
|
||||
|
||||
@staticmethod
|
||||
def authorize_pushover(user_key):
|
||||
salted_encoded = (user_key + settings.SECRET_KEY).encode()
|
||||
|
|
|
@ -912,8 +912,9 @@ class Signal(Transport):
|
|||
raise TransportError("Recipient not found")
|
||||
|
||||
if result.get("type") == "RATE_LIMIT_FAILURE" and "token" in result:
|
||||
raw = reply_bytes.decode()
|
||||
self.channel.send_signal_captcha_alert(result["token"], raw)
|
||||
if self.channel:
|
||||
raw = reply_bytes.decode()
|
||||
self.channel.send_signal_captcha_alert(result["token"], raw)
|
||||
raise TransportError("CAPTCHA proof required")
|
||||
|
||||
code = reply["error"].get("code")
|
||||
|
|
77
hc/front/tests/test_verify_signal_number.py
Normal file
77
hc/front/tests/test_verify_signal_number.py
Normal file
|
@ -0,0 +1,77 @@
|
|||
# coding: utf-8
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from hc.api.models import TokenBucket
|
||||
from hc.api.transports import TransportError
|
||||
from hc.test import BaseTestCase
|
||||
|
||||
|
||||
@override_settings(SIGNAL_CLI_SOCKET="/tmp/socket")
|
||||
class VerifySignalNumberTestCase(BaseTestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.url = "/signal_verify/"
|
||||
|
||||
@patch("hc.front.views.Signal")
|
||||
def test_it_works(self, mock_signal):
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.post(self.url, {"phone": "+1234567890"})
|
||||
self.assertContains(r, "All good, the message was sent")
|
||||
|
||||
@patch("hc.front.views.Signal")
|
||||
def test_it_handles_rate_limit(self, mock_signal):
|
||||
mock_signal.return_value.send.side_effect = TransportError(
|
||||
"CAPTCHA proof required"
|
||||
)
|
||||
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.post(self.url, {"phone": "+1234567890"})
|
||||
self.assertContains(r, "We hit a Signal rate-limit")
|
||||
|
||||
@patch("hc.front.views.Signal")
|
||||
def test_it_handles_recipient_not_found(self, mock_signal):
|
||||
mock_signal.return_value.send.side_effect = TransportError(
|
||||
"Recipient not found"
|
||||
)
|
||||
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.post(self.url, {"phone": "+1234567890"})
|
||||
self.assertContains(r, "Recipient not found")
|
||||
|
||||
@patch("hc.front.views.Signal")
|
||||
def test_it_handles_unhandled_error(self, mock_signal):
|
||||
mock_signal.return_value.send.side_effect = TransportError(
|
||||
"signal-cli call failed (123)"
|
||||
)
|
||||
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.post(self.url, {"phone": "+1234567890"})
|
||||
self.assertContains(r, "signal-cli call failed")
|
||||
|
||||
@patch("hc.front.views.Signal")
|
||||
def test_it_handles_invalid_phone_number(self, mock_signal):
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.post(self.url, {"phone": "+123"})
|
||||
self.assertContains(r, "Invalid phone number")
|
||||
|
||||
def test_it_requires_post(self):
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.get(self.url)
|
||||
self.assertEqual(r.status_code, 405)
|
||||
|
||||
def test_it_requires_authenticated_user(self):
|
||||
r = self.client.post(self.url, {"phone": "+1234567890"})
|
||||
self.assertRedirects(r, "/accounts/login/?next=" + self.url)
|
||||
|
||||
def test_it_obeys_verification_rate_limit(self):
|
||||
TokenBucket.objects.create(value=f"signal-verify-{self.alice.id}", tokens=0)
|
||||
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.post(self.url, {"phone": "+1234567890"})
|
||||
self.assertContains(r, "Verification rate limit exceeded")
|
|
@ -114,4 +114,5 @@ urlpatterns = [
|
|||
path("docs/search/", views.docs_search, name="hc-docs-search"),
|
||||
path("docs/<slug:doc>/", views.serve_doc, name="hc-serve-doc"),
|
||||
path("signal_captcha/", views.signal_captcha, name="hc-signal-captcha"),
|
||||
path("signal_verify/", views.verify_signal_number, name="hc-signal-verify"),
|
||||
]
|
||||
|
|
|
@ -46,6 +46,7 @@ from hc.api.models import (
|
|||
Check,
|
||||
Notification,
|
||||
Ping,
|
||||
TokenBucket,
|
||||
)
|
||||
from hc.api.transports import Signal, Telegram, TransportError
|
||||
from hc.front import forms
|
||||
|
@ -2406,4 +2407,28 @@ def signal_captcha(request: HttpRequest) -> HttpResponse:
|
|||
return render(request, "front/signal_captcha.html", ctx)
|
||||
|
||||
|
||||
@require_setting("SIGNAL_CLI_SOCKET")
|
||||
@login_required
|
||||
@require_POST
|
||||
def verify_signal_number(request: HttpRequest):
|
||||
def render_result(result):
|
||||
return render(request, "integrations/signal_result.html", {"result": result})
|
||||
|
||||
if not TokenBucket.authorize_signal_verification(request.user):
|
||||
return render_result("Verification rate limit exceeded")
|
||||
|
||||
form = forms.PhoneNumberForm(request.POST)
|
||||
if not form.is_valid():
|
||||
return render_result("Invalid phone number")
|
||||
|
||||
try:
|
||||
phone = form.cleaned_data["phone"]
|
||||
Signal(None).send(phone, f"Test message from {settings.SITE_NAME}")
|
||||
except TransportError as e:
|
||||
return render_result(e.message)
|
||||
|
||||
# Success!
|
||||
return render_result(None)
|
||||
|
||||
|
||||
# Forks: add custom views after this line
|
||||
|
|
24
static/js/signal_form.js
Normal file
24
static/js/signal_form.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
$(function() {
|
||||
$("#id_number").on("change keyup", function() {
|
||||
$("#submit-btn").attr("disabled", true);
|
||||
});
|
||||
|
||||
$("#verify-btn").click(function() {
|
||||
$("#verify-btn").attr("disabled", true);
|
||||
|
||||
var url = this.dataset.verifyUrl;
|
||||
var token = $('input[name=csrfmiddlewaretoken]').val();
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: "post",
|
||||
headers: {"X-CSRFToken": token},
|
||||
data: {"phone": $("#id_number").val()},
|
||||
success: function(data) {
|
||||
$("#verify-result").html(data);
|
||||
$("#submit-btn").attr("disabled", data.indexOf("alert-success") == -1);
|
||||
$("#verify-btn").attr("disabled", false);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
})
|
|
@ -1,5 +1,5 @@
|
|||
{% extends "base.html" %}
|
||||
{% load humanize static hc_extras %}
|
||||
{% load compress humanize static hc_extras %}
|
||||
|
||||
{% block title %}
|
||||
{% if is_new %}
|
||||
|
@ -47,14 +47,24 @@ Signal Settings - {% site_name %}
|
|||
|
||||
<div class="form-group {{ form.phone.css_classes }}">
|
||||
<label for="id_number" class="col-sm-2 control-label">Phone Number</label>
|
||||
<div class="col-sm-3">
|
||||
<input
|
||||
id="id_number"
|
||||
type="tel"
|
||||
class="form-control"
|
||||
name="phone"
|
||||
placeholder="+1234567890"
|
||||
value="{{ form.phone.value|default:"" }}">
|
||||
<div class="col-sm-6">
|
||||
<div class="input-group">
|
||||
<input
|
||||
id="id_number"
|
||||
type="tel"
|
||||
class="form-control"
|
||||
name="phone"
|
||||
placeholder="+1234567890"
|
||||
value="{{ form.phone.value|default:"" }}">
|
||||
|
||||
<span class="input-group-btn">
|
||||
<button
|
||||
id="verify-btn"
|
||||
type="button"
|
||||
class="btn btn-default"
|
||||
data-verify-url="{% url 'hc-signal-verify' %}">Send Test Message!</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% if form.phone.errors %}
|
||||
<div class="help-block">
|
||||
|
@ -66,6 +76,8 @@ Signal Settings - {% site_name %}
|
|||
country code.
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
<div id="verify-result"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -100,10 +112,21 @@ Signal Settings - {% site_name %}
|
|||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<button type="submit" class="btn btn-primary">Save Integration</button>
|
||||
<button
|
||||
id="submit-btn"
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
disabled>Save Integration</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ block.super }}
|
||||
{% compress js %}
|
||||
<script src="{% static 'js/signal_form.js' %}"></script>
|
||||
{% endcompress %}
|
||||
{% endblock %}
|
||||
|
|
22
templates/integrations/signal_result.html
Normal file
22
templates/integrations/signal_result.html
Normal file
|
@ -0,0 +1,22 @@
|
|||
{% if result is None %}
|
||||
<div class="alert alert-success">
|
||||
<strong>Success!</strong>
|
||||
All good, the message was sent.
|
||||
</div>
|
||||
{% elif result == "CAPTCHA proof required" %}
|
||||
<div class="alert alert-danger">
|
||||
<p>We hit a Signal rate-limit while sending the message.</p>
|
||||
|
||||
<p>Let's try the other way around: from the Signal application, please send a
|
||||
short message to our Signal account.
|
||||
Then click the "Send Test Message!" button again.
|
||||
</p>
|
||||
</div>
|
||||
{% elif result == "Recipient not found" %}
|
||||
<div class="alert alert-danger">
|
||||
<strong>Recipient not found.</strong>
|
||||
Please make sure you have entered the phone number correctly.
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-danger">{{ result }}</div>
|
||||
{% endif %}
|
Loading…
Add table
Reference in a new issue