Compare commits

..

2192 commits

Author SHA1 Message Date
Pēteris Caune
c498f2e047
Fix typos 2024-12-23 08:53:09 +02:00
Pēteris Caune
279aed2080
Add --start-period and --start-interval in Dockerfile
... to make the container go into a healthy state sooner.

cc: #1108
2024-12-22 18:11:16 +02:00
Pēteris Caune
2dd4994259
Fix fetchstatus.py (again) to handle SITE_ROOT with a path
cc: #1108
2024-12-20 11:38:33 +02:00
Amin Vakil
81869989fb
Set pipefail true for bash in Dockerfile (#1104)
Set pipefail true for bash in Dockerfile
2024-12-19 15:54:42 +02:00
Pēteris Caune
5f94889d01
Fix mypy warning 2024-12-19 12:34:38 +02:00
Pēteris Caune
ff36755345
Update settings.py to populate ALLOWED_HOSTS from SITE_ROOT
The default value for ALLOWED_HOSTS was "*". The default value
now is the domain part of SITE_ROOT.
2024-12-19 12:31:40 +02:00
Pēteris Caune
f7334f7a0e
Add a system check for testing SITE_ROOT against ALLOWED_HOSTS
It checks if the hostname in SITE_ROOT would validate
against ALLOWED_HOSTS.
2024-12-19 11:45:07 +02:00
Pēteris Caune
46bf30ac7f
Move ^/projects/ routes from hc.urls to hc.accounts.urls 2024-12-19 11:15:48 +02:00
Pēteris Caune
0f6099b311
Fix fetchstatus.py to handle SITE_ROOT with a path
Fixes: #1107
2024-12-19 09:35:31 +02:00
Pēteris Caune
a3766734ae
Update settings.py to use f-strings 2024-12-19 09:14:38 +02:00
Pēteris Caune
45b8bd64df
Simplify hosting under subpath
Instead of using SCRIPT_NAME / FORCE_SCRIPT_NAME, PATH_INFO
and their associated issues, update urls.py to add the subpath
to all routes. This allows us to get rid of several hacks:

* the uwsgi.ini magic which parses SITE_ROOT, sets SCRIPT_NAME
  and fixes PATH_INFO
* set_script_prefix() in sendalerts
* chopping the subpath off an URL in hc.accounts.views._allow_redirect

The idea comes from @apollo13
in https://code.djangoproject.com/ticket/35985#comment:5

cc: #1091
2024-12-19 09:04:45 +02:00
Pēteris Caune
c51582eb93
Bump package versions 2024-12-18 13:51:47 +02:00
Pēteris Caune
03710eec88
Update CHANGELOG 2024-12-18 13:43:30 +02:00
Pēteris Caune
281ce65504
isort 2024-12-18 13:42:18 +02:00
Pēteris Caune
0fb011464f
Update SMS notification template to include failure reason
cc: #1069
2024-12-18 12:31:22 +02:00
Pēteris Caune
aba1161597
Update RocketChat notification template to include failure reason
cc: #1069
2024-12-18 11:51:16 +02:00
Pēteris Caune
22695bfdde
Update Spike notification template to include failure reason
cc: #1069
2024-12-18 09:39:56 +02:00
Pēteris Caune
3fa80a8800
Update PagerTree notification template to include failure reason
cc: #1069
2024-12-18 09:13:05 +02:00
Pēteris Caune
fd0d58e96d
Update Zulip notification template to include failure reason
cc: #1069
2024-12-18 09:00:56 +02:00
Pēteris Caune
bc5354b05b
Switch off pip's verbose output 2024-12-18 08:51:29 +02:00
Pēteris Caune
46c51787bb
Update PushBullet notification template to include failure reason
cc: #1069
2024-12-18 08:50:58 +02:00
Pēteris Caune
90ab7e0180
Switch to a newer tonistiigi/binfmt image
cc: #1103
2024-12-17 17:59:45 +02:00
Pēteris Caune
c28ae32261
Update VictorOps notification template to include failure reason
cc: #1069
2024-12-17 16:01:51 +02:00
Pēteris Caune
dcc5d7a7c4
Update Gotify notification template to include failure reason
cc: #1069
2024-12-17 15:22:19 +02:00
Pēteris Caune
fa2c3732cc
Remove "pip install --upgrade pip"
The base image already comes with an up-to-date pip
2024-12-17 14:46:06 +02:00
Pēteris Caune
d8d8d280ca
Update OpsGenie notification template to include failure reason
cc: #1069
2024-12-17 10:09:55 +02:00
Pēteris Caune
f390d6eece
Update MS Teams Workflows notification to include failure reason
cc: #1069
2024-12-17 09:50:29 +02:00
Pēteris Caune
aff41f6688
Update ntfy notification template to include failure reason
cc: #1069
2024-12-17 08:58:31 +02:00
Pēteris Caune
4628deb395
Update PagerDuty notification template to include failure reason
cc: #1069
2024-12-17 08:46:35 +02:00
Pēteris Caune
cd70e88c52
Update Pushover notification template to include failure reason
cc: #1069
2024-12-17 08:23:01 +02:00
Pēteris Caune
4808f35a4c
Update Signal notification template to include failure reason
cc: #1069
2024-12-16 17:20:28 +02:00
Pēteris Caune
3769bf3ede
Fix mypy warning 2024-12-16 15:55:36 +02:00
Pēteris Caune
ca0d639c6b
Update Slack notification template to include failure reason
cc: #1069
2024-12-16 15:43:25 +02:00
Pēteris Caune
f41e1e52ea
Clean up transports.Matrix.get_url() 2024-12-16 15:24:16 +02:00
Pēteris Caune
645efa5ce4
Update Telegram notification template to include failure reason
cc: #1069
2024-12-16 15:20:45 +02:00
Pēteris Caune
28d1ce056e
Increase pip's verbosity to debug slow cryptography builds 2024-12-16 15:13:08 +02:00
Pēteris Caune
ff5b060e86
Move repeating flip reason descriptions to Flip.reason_long() 2024-12-16 14:35:36 +02:00
Pēteris Caune
14bcc84a78
Bump action versions 2024-12-16 08:38:16 +02:00
Pēteris Caune
67dce23f56
Update Dockerfile to install rust using profile "minimal"
This should speed up the build a little bit.
2024-12-15 19:58:37 +02:00
Pēteris Caune
0fc4452366
Remove the /var/lib/docker as-ramdisk workaround
This was needed to work around a bug in QEMU [1],
let's see if we still need this workaround.

[1] https://github.com/rust-lang/cargo/issues/8719
2024-12-15 17:27:08 +02:00
Pēteris Caune
558b832016
Upgrade pycurl to 7.45.4 2024-12-13 17:03:25 +02:00
Pēteris Caune
4a2775c3fe
Update changelog for v3.8.1 release 2024-12-13 13:44:25 +02:00
Pēteris Caune
e09fd28836
Improve Matrix notifications (include tags, period, last ping type etc.)
cc: #1069
2024-12-13 13:34:41 +02:00
Pēteris Caune
850cab6fa6
Update Dockerfile to use Python 3.13.1
cc: #1101
2024-12-13 10:06:51 +02:00
Pēteris Caune
04ebbeb0bd
Update CHANGELOG for v3.8 release 2024-12-09 15:15:29 +02:00
Pēteris Caune
ae074a7b2a
Update SITE_ROOT docs and CHANGELOG
cc: #1091
2024-12-09 15:13:25 +02:00
Pēteris Caune
a2c7259d1b
Fix redirects after login when using a path in SITE_ROOT
Fixes: #382

cc: #1091
2024-12-09 12:35:02 +02:00
Pēteris Caune
0ee3f03baf
Fix redirect to login when SITE_ROOT contains a path
cc: #382, #1091
2024-12-09 12:20:09 +02:00
Pēteris Caune
b685e66b71
Add a workaround for reverse() omitting script prefix when on thread
https://code.djangoproject.com/ticket/35985

cc: #1091
2024-12-09 11:53:53 +02:00
dependabot[bot]
3f2ff2adf3
Bump django from 5.1.3 to 5.1.4 (#1099)
Bumps [django](https://github.com/django/django) from 5.1.3 to 5.1.4.
- [Commits](https://github.com/django/django/compare/5.1.3...5.1.4)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-09 08:33:31 +02:00
Pēteris Caune
2ff68cb597
Update docker/uwsgi.ini to handle subpath in SITE_ROOT
cc: #1091
2024-12-04 11:00:53 +02:00
Pēteris Caune
7836da6448
Update settings.py to allow subpath in SITE_ROOT
If SITE_ROOT contains a subpath
(e.g., "http://example.org/healthchecks"), settings.py now adjusts
FORCE_SCRIPT_NAME and STATIC_URL.

Also, hc.lib.urls.absolute_reverse() now recognizes subpath in
SITE_ROOT, and makes sure it does not show up twice in the
generated URLs.

cc: #1091
2024-12-04 10:37:52 +02:00
Pēteris Caune
22268c1484
Move absolute URL construction to hc.lib.urls.absolute_reverse()
absolute_reverse() works the same as django.urls.reverse()
except it generates absolute URLs (starting with http[s]://)
2024-12-03 17:24:27 +02:00
Pēteris Caune
35eb727ebd
Fix the note about SCRIPT_NAME
SITE_ROOT should *not* contain the subpath, I had it wrong previously.

cc: #1091
2024-12-03 12:48:58 +02:00
Pēteris Caune
5e848f4976
Add index on api_flip (owner_id, created)
This helps queries in hc.front.views._get_events,
especially for checks that are flipping between up and down
states a lot.
2024-12-03 10:37:01 +02:00
Pēteris Caune
205baf56b9
Add a note about serving on subpath
Fixes: #1091
2024-12-03 09:20:10 +02:00
Pēteris Caune
4a618cd7e9
Fix elf -> self 2024-12-02 11:32:53 +02:00
Jacek Kowalski
8485c758eb
Fix Trello integration (token regexp too specific) (#1094)
* Fix Trello configuration failing with HTTP 400

* Add additional tests regarding Trello token
2024-12-02 11:31:02 +02:00
Pēteris Caune
2bf9c357ef
Update WhatsApp logo 2024-11-25 15:03:52 +02:00
Pēteris Caune
29c02ab986
Replace Matrix logo with a larger, sharper version 2024-11-25 14:51:25 +02:00
Pēteris Caune
410a767496
Fix the off state for rocketchat, ntfy, telegram icons 2024-11-25 12:33:33 +02:00
Pēteris Caune
87dc3aff1d
Update Signal and Telegram icons 2024-11-25 12:10:16 +02:00
Pēteris Caune
cb7e53677d
Increase ntfy.sh topic max length to 64 2024-11-25 11:16:15 +02:00
Pēteris Caune
a81da0b1ca
Remove some IE9 stuff from bootstrap.css 2024-11-24 14:59:48 +02:00
Pēteris Caune
c4a018a2eb
Remove unused "speak: never;" from icomoon.css 2024-11-24 14:59:17 +02:00
Pēteris Caune
0f2168a5a5
Remove unused CSS 2024-11-24 14:58:46 +02:00
Pēteris Caune
8ee0664801
Remove unneeded newlines 2024-11-24 14:39:29 +02:00
Pēteris Caune
9c4e5eb99f
Remove unused CSS rule (for styling HipChat icons) 2024-11-24 14:38:50 +02:00
Pēteris Caune
e024196f1f
Add IcoMoon project export under /stuff/ 2024-11-22 17:10:17 +02:00
Pēteris Caune
964a4f264e
Remove unused icons and fix "Glyph bbox was incorrect" FF warnings
The "Glyph bbox was incorrect" is a Firefox warning about
incorrect bounding box values in the font. The icon font, including
the incorrect bounding box values is generatedby icomoon.
I ran the TTF file through fontsquirrel and that seems to have
fixed the issue.

More about this warning: https://bugzilla.mozilla.org/show_bug.cgi?id=1847959
2024-11-22 17:08:36 +02:00
Pēteris Caune
3d6fe5a423
Change the clippy icon's size from 14px to 16px
It is designed to look crisp at 16px
2024-11-22 17:05:39 +02:00
Pēteris Caune
35b6b94a1d
Remove ic-started from CSS (unused) 2024-11-22 17:04:51 +02:00
Pēteris Caune
534d6973c4
Reduce the generated HTML size of the checks table
... by removing class="indicator-cell" and using
:first-child in CSS to target it instead.
2024-11-22 14:21:48 +02:00
Pēteris Caune
2c4719ecc3
Add backrest in the "Third-Party Resources" page
Fixes: #1087
2024-11-22 12:55:32 +02:00
Pēteris Caune
9af904297d
Rearrange channels in the integrations page, "Add More" section 2024-11-19 11:03:49 +02:00
Pēteris Caune
06d7c61297
Fix kerning issue in SVG badge on Windows 10 2024-11-15 16:49:26 +02:00
Pēteris Caune
62b10be5fe
Implement "no matching checks" message when searching/filtering
(instead of showing a table with a header row and no data rows)
2024-11-15 12:11:18 +02:00
Pēteris Caune
2ef550e16e
Update Changelog 2024-11-14 14:57:09 +02:00
Pēteris Caune
4b49004768
Fix nav divider color in selectize dropdowns 2024-11-14 14:55:04 +02:00
Pēteris Caune
c51096e17e
Fix dropdown button background in dark mode 2024-11-14 14:50:35 +02:00
Pēteris Caune
0c8bba95ac
Hide filter buttons on mobile 2024-11-14 14:29:19 +02:00
Pēteris Caune
5ecea08e48
Fix type annotation 2024-11-14 14:28:47 +02:00
Pēteris Caune
5184249abe
Move status matching logic to a separate function 2024-11-14 14:18:39 +02:00
Pēteris Caune
b328c8739f
Reduce the number of Check.get_status() calls 2024-11-14 13:33:21 +02:00
Pēteris Caune
7c8d43414f
Implement filtering by status on the server side 2024-11-14 12:27:31 +02:00
Pēteris Caune
7abcd4adbe
Fix JS to run applyFilters only when statuses have changed 2024-11-14 11:57:30 +02:00
Pēteris Caune
9ce8249123
Move "Add Check" button to the top, implement filtering by status 2024-11-14 11:51:37 +02:00
Pēteris Caune
7719cc4c28
Reformat 2024-11-14 11:45:11 +02:00
Pēteris Caune
27ecb40c88
Mark the project name textbox as required in the "Add Project" dialog 2024-11-13 14:27:15 +02:00
Pēteris Caune
5f3bbfd940
De-emphasize the unsubscribe link in email notifications
I received a report about multiple cases of users clicking
on the unsubscribe link by accident. The unsubscribe action
does not have a confirmation step so the users get instantly
unsubscribed. I do not want to add a confirmation step, so
instead I'm making a few small changes to hopefully reduce
the chances of accidental clicks:

- The footer text now has a "You are receiving this email because..."
  blurb to separate the footer a little more from the primary
  content
- The unsubscribe link is now a shorter single word: "Unsubscribe",
  making it a smaller click target
- the footer now uses a slightly smaller font than the rest of
  the email message

The people looking for the unsubscribe link should still be able
to easily find it, but hopefully it should now draw less
"accidental attention".
2024-11-12 14:51:05 +02:00
Pēteris Caune
5c67c94654
Add a missing article 2024-11-08 11:31:09 +02:00
Pēteris Caune
5912758a8a
Update email alerts to mention failure reason
cc: #1069
2024-11-08 11:20:44 +02:00
Pēteris Caune
9edae634c7
Add Flip.reason field
cc: #1069
2024-11-08 10:24:50 +02:00
Pēteris Caune
79da9e9f4f
Fix auto-fixable ruff warnings
(`ruff check --fix`)
2024-11-07 15:15:58 +02:00
Pēteris Caune
4907073c55
Remove unneeded quotes 2024-11-06 19:32:44 +02:00
Pēteris Caune
5f8b723f46
Update package versions 2024-11-06 19:27:56 +02:00
Pēteris Caune
e048ec4c48
Fix "class Foo(object):" -> "class Foo:"
In Python 3 these are equivalent, and shorter is better.
2024-10-29 17:57:50 +02:00
Pēteris Caune
a6ca589b34
Fix pyright warning 2024-10-29 11:54:48 +02:00
Pēteris Caune
f421317aea
Update mypy and django-stubs 2024-10-28 10:06:48 +02:00
Pēteris Caune
7d6fccb05d
Update package versions 2024-10-25 11:02:43 +03:00
Pēteris Caune
c372e3232f
Update MS Teams legacy webhook retirement date to Jan 2025
Microsoft pushed it forward again:
https://devblogs.microsoft.com/microsoft365dev/retirement-of-office-365-connectors-within-microsoft-teams/
2024-10-25 09:51:58 +03:00
Pēteris Caune
e3cbe79f57
Update CustomHeaderMiddleware to normalize email addresses to lower case
Also add a data migration to normalize any already existing user
accounts with non-lower-case email addresses too.

Fixes: #1074
2024-10-24 13:22:37 +03:00
Pēteris Caune
0a4412a493
Fix js to enable "pause" button when a check is paused but started 2024-10-23 14:32:06 +03:00
Pēteris Caune
374f034bf9
Update check's status text to show how long it has been running
In check's details page, we have a "Current Status" section.
It shows an icon, a status text, and a table with downtime
statistics. When the check has received a start signal, the icon
also has an animated progress indicator under it.

With this change, the status text will also indicate the running
state and the elapsed time. Example:

"Currently running, started 3 minutes ago".
2024-10-23 14:14:44 +03:00
Pēteris Caune
dd0a4c1582
Update Dockerfile to use Python 3.13 2024-10-23 13:22:41 +03:00
Pēteris Caune
9139d6825a
Update Dockerfile to explicitly install libexpat1
libexpat1 used to come preinstalled in python -slim base images,
but starting from 3.12.7 it does not any more.

Fixes: #1076
2024-10-23 13:19:59 +03:00
Pēteris Caune
7225165f9e
Remove --start-period argument in Dockerfile
I don't think it has any effect if it is lower than --interval
2024-10-23 13:15:48 +03:00
Pēteris Caune
5b689d9f87
Increase the interval of Docker HEALTHCHECK instruction to 60s
cc: #1071
2024-10-23 10:43:57 +03:00
Pēteris Caune
0bf73bf743
Rewrite the fetchstatus.py script to reduce Docker container CPU use
fetchstatus.py was a Django management command, and is now
a standalone script. This way we avoid doing Django housekeeping
every time it runs (every 10 seconds). It still needs to access
the ALLOWED_HOSTS setting, to make the HTTP request with
a correct Host header.

Fixes: #1071
2024-10-22 10:53:47 +03:00
Pēteris Caune
9e69b5b5f5
Fix smtp listener to reject email addresses with unexpected domain
cc: #1077
2024-10-21 17:48:57 +03:00
Pēteris Caune
84f22c8978
Fix type warnings 2024-10-21 17:34:02 +03:00
Pēteris Caune
a5d4dc2db5
Fix smtp listener to reject email addresses with non-UUID local parts
cc: #1077)
2024-10-21 15:56:24 +03:00
Pēteris Caune
bbd45f2737
Update changelog for v3.7 release 2024-10-21 12:59:47 +03:00
Pēteris Caune
d723e14284
Switch to an older version of the base image
Related: #1076

This is temporary until we find out if libexpat will be
reinstated in the base image or not.
2024-10-21 12:08:15 +03:00
Pēteris Caune
c91213179f
Fix API to gracefully handle too long slugs 2024-10-16 12:35:30 +03:00
Pēteris Caune
f40ad2be72
Pin mypy version to 1.11.2
Mypy Django plugin is apparently not compatible with mypy 1.12 yet
2024-10-15 15:14:16 +03:00
Pēteris Caune
b42ca36b8c
Fix triple-click selection of ping URLs
When user double-clicks on text, browser selects a single word.
When user triple-clicks, browser selects a line or a paragraph
of text.

We have JS code that enables user to click on ping URL to copy
it to the clipboard. After copying it shows a small non-native
tooltip saying "Copied!" above the ping URL. This was interfering
with triple-click selection, the word "Copied!" would get selected
as well.

The fix is to use document body as the container of the tooltip,
not the ping URL span (which is the default).
2024-10-15 15:00:11 +03:00
Pēteris Caune
8c210e151f
Update the Signal integration to retry on network errors 2024-10-14 11:19:37 +03:00
Pēteris Caune
b886c93cb7
Make slider labels clickable in the "Update Period & Grace" dialog
Fixes: #1039
2024-10-11 13:37:29 +03:00
Pēteris Caune
d574fa65fc
Update _refresh_last_active_date to also refresh user session
Fixes: #1063
2024-10-10 15:41:00 +03:00
Pēteris Caune
e1ccf5f86a
Add Elestio to the 3rd party resources page
Fixes: #1062
2024-10-10 13:19:40 +03:00
Pēteris Caune
7dc13d897f
Add a hint in ntfy form
Fixes: #1059
2024-10-10 10:23:48 +03:00
Pēteris Caune
4f9b0b11b9
Update Signal transport to log unexpected signal-cli replies
When signal-cli returns an error that we are not handling yet,
log the precise JSON message that signal-cli returns. This
is for debug & development: We can look at the logged messages
and see what additional special error handling may be needed.
2024-10-10 10:21:08 +03:00
Pēteris Caune
e49b5f8fbd
Remove LINE Notify onboarding form
LINE Notify is shutting down on Apr 1, 2025:
https://notify-bot.line.me/closing-announce

I'm removing the onboarding form so people don't set up new
integrations that will stop working in 5 months.

The code for sending LINE Notify notifications still exists,
and the existing integrations will continue to work (until LINE
Notify stops working).
2024-10-08 09:13:03 +03:00
Pēteris Caune
fd96cc794b
Remove unused bits 2024-10-04 17:34:30 +03:00
Pēteris Caune
a51420744c
Add RiskCheck: disable in SMS transport
This is to reduce the chance of hitting Twilio error 30453,
"Message couldn't be delivered".

https://www.twilio.com/docs/api/errors/30453
2024-10-02 17:01:23 +03:00
Pēteris Caune
5a43e8f197
Fix formatting 2024-10-02 09:44:03 +03:00
Pēteris Caune
508094ff17
Improve wording 2024-10-02 09:36:32 +03:00
Pēteris Caune
20fb599ad9
Add note about local_settings in SECURE_PROXY_SSL_HEADER docs 2024-10-02 09:36:20 +03:00
Pēteris Caune
de4c4897e3
Remove prunenotifications management command
Notifications are now cleaned up automatically during pinging.
2024-10-02 09:24:01 +03:00
Pēteris Caune
13f92b90ef
Update settings.py to read SECURE_PROXY_SSL_HEADER from env vars
And add it to docs.

And add a system check to make sure it, if set, is a tuple
with 2 elements.

cc: #851
2024-10-01 19:13:26 +03:00
Pēteris Caune
342423ee93
Update the "Accessing Administration Panel" section 2024-10-01 16:30:57 +03:00
Pēteris Caune
94b588f584
Update "Configuration" and "External Authentication" sections 2024-10-01 16:15:56 +03:00
Pēteris Caune
6ed9de4d01
Fix whitespace 2024-10-01 16:15:39 +03:00
Pēteris Caune
69a6588121
Update "Database Cleanup" in docs 2024-10-01 15:57:23 +03:00
Pēteris Caune
e73d7a1ece
Remove pruneflips management command
Flips are now cleaned up automatically during pinging.
2024-10-01 15:33:56 +03:00
Pēteris Caune
d77b96a40f
Update the uuid/slug switching links to not lose currently selected tags 2024-09-30 10:29:30 +03:00
Pēteris Caune
0b7b77c8d7
Add a note about setting REMOTE_USER_HEADER in local_settings.py
cc: #1061
2024-09-24 12:13:33 +03:00
Pēteris Caune
12cccaf7d1
Fix Project.num_checks naming collision
The Project model has (well, had) a num_checks() method.
In the project admin we are also annotating project queryset
with a "num_checks" property. Using the same name for two different
things causes type confusion for mypy and can also lead to
coding accidents.

This commit removes the Project.num_checks() method. This was easier
to do than changing admin, as the method is very simple and was used
in only two places.
2024-09-24 10:18:22 +03:00
Pēteris Caune
71f92d19fa
Improve the "TLS Termination" section in docs
cc: #1065
2024-09-23 10:29:55 +03:00
Bilal Kamran
14a08e2778
Add a minimal uWSGI example in README (#1056)
Co-authored-by: Pēteris Caune <cuu508@monkeyseemonkeydo.lv>
2024-09-12 11:53:16 +03:00
Pēteris Caune
2cb47d3742
Make the sorting of null values in Flip.select_channels() explicit 2024-09-12 10:52:06 +03:00
Pēteris Caune
f241d070e1
Update Flip.select_channels() to sort channels by last_notify_duration
If a check has multiple associated channels, some are slow and
some are quick, handle the quick ones first.
2024-09-12 10:44:56 +03:00
Pēteris Caune
f60af9a156
Update ntfy integration to give up db connection before network IO 2024-09-12 10:30:58 +03:00
Pēteris Caune
28af3720f4
Increase outgoing webhook timeout from 10 to 30 seconds
Also simplify the retry logic: each retry attempt is now
allowed to use the full 30 seconds. This means, a single
webhook delivery can take up to 3*30=90 seconds.
2024-09-11 12:37:40 +03:00
Pēteris Caune
13217af304
Add --pool parameter in manage.py sendalerts
If sendalerts receives this parameter, it reconfigures
settings.DATABASES to enable db connection pooling
(using psycopg_pool with default parameters).

This lets us use many concurrent worker threads but not
run out of database connections. For example, with
`--num-workers 100 --pool`, up to 100 worker threads can run
concurrently, but only 3 threads can get a database connection
from the pool, the rest have to wait. When a worker thread
gives up a connection (by calling `close_old_connections`),
another thread can continue.

A worker thread can give up a db connection before it is fully
finished if it anticipates a long network IO operation ahead.
The Webhook transport does this before making a curl call.

psycopg_pool's default pool size is 4 connections. One
connection is used up by the main thread, so 3 connections
are available for the worker threads.
2024-09-10 14:58:24 +03:00
Pēteris Caune
8eecece0bb
Add db migration for the updated msteams name 2024-09-10 14:45:48 +03:00
Pēteris Caune
fd0c428e29
Update sqlite settings to avoid "Database is locked" errors
Fixes: #1057

"PRAGMA busy_timeout" configures the database to wait when a
database is locked instead of giving up immediately.

"transaction_mode IMMEDIATE" starts transactions in read/write
mode, required to make busy_timeout work.

Reference: https://gcollazo.com/optimal-sqlite-settings-for-django/
2024-09-09 10:11:22 +03:00
Pēteris Caune
6bf588d984
Remove unused import 2024-09-04 10:49:09 +03:00
Pēteris Caune
5a19f9658a
Update changelog for v3.6 release 2024-09-04 10:18:56 +03:00
Pēteris Caune
4097cdee61
Bump Django to 5.1.1 2024-09-04 09:27:51 +03:00
Pēteris Caune
a72f3adc45
Update requirements to require only pure-python psycopg
... and install psycopg-c using instuctions in Dockerfile.

This way, getting a development environment or CI environment ready
is quick and easy, but Docker images still get the C optimizations.
2024-09-03 16:10:53 +03:00
Pēteris Caune
fea767723c
Upgrade to psycopg 3 2024-09-03 11:30:43 +03:00
Pēteris Caune
9d4fc031aa
Fix sendalerts to check the self.shutdown flag more often 2024-09-03 10:30:18 +03:00
Pēteris Caune
3275e0ffaa
Update notify() to return logs instead of printing them 2024-09-03 10:23:15 +03:00
Pēteris Caune
8c56ca6dde
Update sendalerts to mark flip as processed on thread
Previously this was done in process_one_flip (so on the main thread).
The advantage of doing this way is the flip gets marked as processed
only when the thread has started and has acquired a db connection.
There is now a smaller pause between a sendalerts process claiming a
flip, and actually starting work on it.
2024-09-01 15:28:48 +03:00
Pēteris Caune
fd75049e0c
Fix type warnings 2024-08-31 19:23:10 +03:00
Pēteris Caune
a463daa775
Update Webhook transport to close db connection before network IO
Webhook requests can take 20+ seconds. During that time we hold
on to a database connection. With this commit, the Webhook transport
closes its DB connection before making a curl call.

With psycopg2 this does not have much effect. But with
psycopg 3 & connection pooling we will be able to use more
sendalerts workers than we have database connections. While one
worker is busy making a slow curl call, another worker can
grab its freed up connection and do some work.

Django's test runner is not happy with connections closed
mid-test, so I patched out close_old_connections() in affected tests.
2024-08-31 19:18:17 +03:00
Pēteris Caune
9803d77a1d
Set explicit max_workers value for ThreadPoolExecutor
This is a tricky one: the default value for max_workers is
None. But it doesn't mean "unlimited", in Python 3.8+ it
means "min(32, os.cpu_count() + 4)"

For example on 8-core CPU the effective value would be 8 + 4 = 12,
and passing anything above 12 to `--max-workers` would have no effect.
2024-08-31 19:11:39 +03:00
Pēteris Caune
4cd677536d
Remove sent notification counter
The counter was slightly wrong (it counted lost races as sent
notifications). Rather than complicating code to make it correct,
let's rather just remove it :-)
2024-08-31 19:07:25 +03:00
Pēteris Caune
faa1a2c99f
Add logging for exceptions thrown inside notify() 2024-08-31 19:04:41 +03:00
Pēteris Caune
7641f2a9a1
Switch to using close_old_connections() instead of connection.close() 2024-08-31 19:02:11 +03:00
Pēteris Caune
d76dc53e49
Increase Signal send timeout to 60 seconds 2024-08-31 11:07:17 +03:00
Pēteris Caune
b1b0a57033
Tweak sendalerts log format 2024-08-30 17:00:30 +03:00
Pēteris Caune
8a3a9b2a7e
Fix code comments 2024-08-29 16:30:28 +03:00
Pēteris Caune
029881f3b9
Refactor sendalerts
* Remove the --no-loop and --no-threads arguments
* Use a threadpool to do multiple sends concurrently
* Add a new `--num-workers` argument. It limits how many flips we grab
  from the database and process concurrently.
* Do not prioritize flips with historically low send times any more
  (not as important now with concurrent sending, and simpler this way)
* Workers close db connections when they finish
  (to keep the number of idle connections low)

Note: concurrent.futures.ThreadPoolExecutor internally has an unbounded
queue, it will accept any amount of jobs and keep them queued. We don't
want that. We only want to grab a flip, and commit to processing it,
if we know there's a free worker for it. Therefore we're tracking the
number of jobs in flight using a semaphore (`self.seats`).
2024-08-29 16:20:36 +03:00
Pēteris Caune
3968a4f9e0
Update MS Teams Connector EOL date 2024-08-27 16:34:59 +03:00
Pēteris Caune
320a7c7733
Fix the Docker healthcheck script to supply correct Host header
Commit 8fed685f12 added a HEALTHCHECK
instruction in the Dockerfile. The healthcheck script calls http://localhost:8000/api/v3/status/, which fails if localhost is not in ALLOWED_HOSTS.

With this change, the healthcheck script is now a Django management
command. It reads Django's ALLOWED_HOSTS setting, grabs the first
element, and uses it in the "Host:" HTTP header when making a HTTP
request.

cc: #1051
2024-08-21 15:52:19 +03:00
Pēteris Caune
027fcc1097
Simplify and eliminate assert 2024-08-20 14:39:11 +03:00
Pēteris Caune
0a4f038987
Simplify and eliminate assert 2024-08-20 14:13:58 +03:00
Pēteris Caune
b27ffe07a6
Update email_form to use more precise type annotation 2024-08-20 13:58:52 +03:00
Pēteris Caune
6d15c45b21
Update CHANGELOG for v3.5.1 release 2024-08-20 13:46:09 +03:00
Pēteris Caune
6f11b9c0dd
Remove unneeded bits 2024-08-20 13:27:28 +03:00
Pēteris Caune
79b9aae660
Update Dockerfile to install recent rustc (needed to build cryptography)
* Healthchecks depends on python library "fido2"
* fido2 depends on python library "cryptography"
* building cryptography requires recent (1.65+) rustc
* cryptography has prebuilt binary wheels for most architectures
  but not for arm/v7
* Dockerfile uses bookworm as base, which ships rustc 1.63
* So we now install rust using rustup

This is all terrible.
2024-08-20 13:11:29 +03:00
Pēteris Caune
ca75c7e984
Update CHANGELOG for v3.5 release 2024-08-20 11:20:34 +03:00
Pēteris Caune
001ba8b69b
Fix type warnings 2024-08-20 11:06:55 +03:00
Pēteris Caune
5e051bfc30
Fix AJAX views to better handle user logging out
Rather than redirecting to login page, return HTTP 403 Forbidden
2024-08-20 10:57:36 +03:00
Pēteris Caune
15e1a988c8
Upgrade docker-compose.yml to use postgres 16, add upgrade instructions 2024-08-19 11:00:37 +03:00
Pēteris Caune
8fed685f12
Update Dockerfile to report container health in docker ps
This commit adds a HEALTHCHECK instruction in Dockerfile.
The HEALTHCHECK instruction calls /docker/fetchstatus.sh
which in turn makes a HTTP request to
http://localhost:8000/api/v3/status/
This endpoint makes a test database query and returns non-200
response if the query fails. So, in short, if the Healthchecks
container for any reason is unable to query database, `docker ps`
will now show the container as "unhealthy".

cc: #1045
2024-08-19 10:17:05 +03:00
Pēteris Caune
70b55a777b
Add migration which updates Channel.kind values
This is to go with 8054191be3,
and should have been in there :-)

cc: #1050
2024-08-17 12:12:47 +03:00
Pēteris Caune
d3ae4e7fac
Add support for $SLUG placeholder in webhook payloads
Fixes: #1049
2024-08-16 13:24:12 +03:00
Pēteris Caune
cda744d0c1
Implement search by slug in the checks list
cc: #1048
2024-08-15 14:17:28 +03:00
Pēteris Caune
56bac98816
Update the "Set Password" page to reject very weak passwords 2024-08-15 12:04:28 +03:00
Pēteris Caune
5d63057e78
Improve password quality meter for very weak passwords
Previously, if the user enters a weak password like "qwerty",
the score is 0, the password strength bar is empty (all gray).
It is easy to not notice the password strength bar at all.

Now, the lowest score for a non-empty password is 1, meaning
the user will see one red bar. This will hopefully draw more
attention to the password strength bar.

Users are still allowed to choose weak passwords.
2024-08-15 11:10:14 +03:00
Pēteris Caune
81515e3ed2
Fix selectize optgroup separator in dark mode 2024-08-13 14:54:08 +03:00
Pēteris Caune
3fbba0c2f0
Update timezone dropdowns to show frequently used timezones at the top 2024-08-13 13:57:52 +03:00
Pēteris Caune
b859a71920
Rename "sign in" to "log in"
I like "sign in" better, but users from time
to time confuse "sign in" and "sign up" forms. To reduce
confusion potential, I'm renaming "sign in" to "log in".
2024-08-12 15:09:58 +03:00
Pēteris Caune
56862a1c49
Update NotificationsAdmin to use __ lookup in list_display 2024-08-07 17:39:17 +03:00
Pēteris Caune
f7876f67d7
Remove unused code 2024-08-07 17:38:43 +03:00
Pēteris Caune
bd5582872a
Upgrade to Django 5.1 2024-08-07 17:24:27 +03:00
Pēteris Caune
a3bc9f3b37
Upgrade to Django 5.0.8 2024-08-07 17:20:17 +03:00
Joel Pérez
28168a5651
Fix django version in self hosted documentation (#1034)
* Update self_hosted.md

* Update self_hosted.html-fragment
2024-07-30 19:24:31 +03:00
Pēteris Caune
aa2bd8cf66
Fix a testcase not correctly using sample values 2024-07-29 10:36:29 +03:00
Pēteris Caune
26ed70eccd
Bump package versions 2024-07-29 10:31:06 +03:00
Pēteris Caune
ba8a58a8a7
Fix type annotation 2024-07-29 09:57:28 +03:00
Pēteris Caune
42b733540d
Fix type annotation
It used the wrong model name and neither me nor mypy noticed
until upgrade to django-stubs 5.0.4
2024-07-29 09:50:56 +03:00
Pēteris Caune
7346994ae8
Fix field name in TypedDict used for type checking 2024-07-18 18:19:01 +03:00
Pēteris Caune
bdb6f18a3d
Add "uuid" field in API responses when read/write key is used
The API responses already contain ping_url, update_url, resume_url,
pause_url fields where the UUID can be extracted from, so we are
not exposing new information. The extraction can be finicky in,
say, shell-scripting scenarios. So for API user convenience we will
now also provide the check's code (UUID) as a separate field.

Fixes: #1007
2024-07-18 18:15:52 +03:00
Pēteris Caune
8054191be3
Remove HipChat, Pagerteam, Zendesk channel kinds
HipChat and Pagerteam products have long been shut down,
the Zendesk integration was never fully implemented.
2024-07-18 16:21:45 +03:00
Pēteris Caune
61bdd975e8
Add "(stops working Oct 2024)" note to the old MS Teams integration 2024-07-18 10:27:51 +03:00
Pēteris Caune
9ebab5d317
Rename illustrations to bust cached versions 2024-07-18 10:27:49 +03:00
Pēteris Caune
9660bc293c
Update hc.lib.s3 to retry failed requests *one time* 2024-07-17 17:26:49 +03:00
Pēteris Caune
ce5d9bcf56
Re-enable S3 retries 2024-07-17 17:04:33 +03:00
Pēteris Caune
e83f60cc0b
Implement Implement MS Teams Workflows integration
We already have a MS Teams integration but MS Teams is discontinuing
the incoming webhook feature used by this integration:

https://devblogs.microsoft.com/microsoft365dev/retirement-of-office-365-connectors-within-microsoft-teams/

MS Teams now recommends to use Workflows to post messages
via webhook. MS Teams does not provide backwards compatibility or
an upgrade path for existing integrations.

This commit adds a new "msteamsw" integration which uses MS Teams
Workflows to post notifications. It also updates the instructions
and illustrations in the "Add MS Teams Integration" page.

cc: #1024
2024-07-17 13:35:17 +03:00
Pēteris Caune
1877a8324f
Disable S3 API request retries
urrlib3's default number of retries is 3.
If requests to the S3 API are timing out, the retries usually
don't help, but a 10-second timeout turns into 10*3=30 seconds
of python code being blocked.
2024-07-12 03:09:21 +03:00
Pēteris Caune
70c5be5c4b
Fix type warning 2024-07-11 17:45:51 +03:00
Pēteris Caune
1b695c6970
Improve performance of loading ping body previews
Defer loading body_raw, instead load its first 150 bytes
as "body_raw_preview". This reduces both network I/O to database,
and disk I/O on the database host if the database contains large
request bodies.

cc: #1023
2024-07-11 17:38:25 +03:00
Pēteris Caune
3e5080d9eb
Remove Ping.body field 2024-07-11 16:34:18 +03:00
Pēteris Caune
997154e3b0
Remove usages of Ping.body 2024-07-11 16:17:21 +03:00
Pēteris Caune
daaee30c88
Add data migration to move Check.body -> Check.body_raw
We used "body" to store request body as text.
In 2022 we added "body_raw" and started to use it to store request
body as bytes.

In python code we currently need to inspect both fields,
because the data could be in "body" (for old pings) or in
"body_raw" (for newer pings). My plan is to eventually get rid
of the "body" field, and have "body_raw" only. This data migration
is a step towards that: for any Ping objects that have non-empty
"body" field, it moves the data to the "body_raw" field. After
applying this migration, the "body" field should be empty (empty
string or null) for all Ping objects.
2024-07-11 14:38:36 +03:00
Pēteris Caune
bc8fb90fed
Update Check.ping() to use select_for_update()
Without it, on MariaDB, concurrent pings can lead to a deadlock.
This results in OperationalError and HTTP 500 response to the client.

cc: #1023
2024-07-10 19:50:39 +03:00
Pēteris Caune
cc51d2bd79
Bump Django to 5.0.7 2024-07-10 14:13:07 +03:00
Pēteris Caune
3da7cf6027
Bump package versions 2024-07-08 19:59:52 +03:00
Pēteris Caune
b3de36d15c
Reorder system checks in hc.api.apps 2024-07-04 11:32:28 +03:00
Pēteris Caune
23f3256abc
Rename and clean up the apprise system check 2024-07-04 11:28:58 +03:00
Pēteris Caune
cf619bc68b
Fix hc.api.transports to not alter settings.APPRISE_ENABLED setting.
Instead, make it set a local `have_apprise` variable, and use
it in the hc.api.transports.Apprise class.

If hc.api.transports sets APPRISE_ENABLED to False,
then the apprise system check in hc.api.apps will not see the
original value and therefore will not run.
2024-07-04 11:28:16 +03:00
Rajesh Kumar
57459b0375
Show warning if apprise is enabled but apprise package is not installed (#1021)
* fix: show warning if apprise is enabled and not installed in environment

* renamed appraise check register

* revert back changes in transport for apprise
2024-07-04 11:12:05 +03:00
Pēteris Caune
3fbe8d52f0
Document criteria for accepting new integrations in CONTRIBUTING 2024-07-02 10:19:48 +03:00
Pēteris Caune
8d0930c4b9
Fix unclosed sockets in statsd tests 2024-06-27 11:03:29 +03:00
Pēteris Caune
b5eced26cf
Fix migrations for Django 5.1 2024-06-27 10:20:27 +03:00
Pēteris Caune
324fa10ce7
Fix Check.lock_and_delete() to gracefully handle already deleted check 2024-06-20 15:57:53 +03:00
Viktor Szépe
9a44ef1571 Fix typos 2024-06-20 15:41:42 +03:00
Pēteris Caune
17d01ee6e2
Update changelog for release 2024-06-20 15:15:29 +03:00
Pēteris Caune
1a5ca45d09
Fix InvalidResponseError() initialization parameters 2024-06-20 15:15:14 +03:00
Pēteris Caune
9dbd961beb
Add test case for InvalidResponseError handling 2024-06-20 15:11:42 +03:00
Pēteris Caune
475bdff87d
Add handling for minio.InvalidResponseError exceptions 2024-06-18 23:50:11 +03:00
Pēteris Caune
44b163cc52
Fix JS formatting 2024-06-17 14:14:36 +03:00
NKETIAH53
0874ced368 format dates in logs to include year unless they are from current year
fixes: #1008
2024-06-17 14:07:18 +03:00
Pēteris Caune
e007f75a70
Bump package versions 2024-06-17 10:48:53 +03:00
Pēteris Caune
b2c5e91c70
Implement legacy -> canonical timezone conversion
There are three related changes:

* Removed legacy timezones from hc.lib.tz.all_timezones
* Added data migration to update existing Check.tz values
* For backwards compatibility, added code to automatically
  replace a legacy timezone with a canonical timezone when a
  legacy timezone is passed to an API call

I used the timezone mapping on
https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
2024-06-14 12:55:57 +03:00
Pēteris Caune
52f2b534a6
Fix API to accept Europe/Kiev but save it as Europe/Kyiv 2024-06-13 15:23:27 +03:00
Pēteris Caune
c5bd666faf
Add data migration to update timezone "Europe/Kiev" to "Europe/Kyiv" 2024-06-13 15:03:51 +03:00
Pēteris Caune
5584c2cd21
Fix copy/pasting error 2024-06-12 09:46:58 +03:00
Pēteris Caune
3b081c3c83
Rearrange docs navigation 2024-06-11 21:38:15 +03:00
Pēteris Caune
037499f33a
Add Docs / Pinging / Network Routers 2024-06-11 21:00:34 +03:00
Pēteris Caune
24e5e83bbc
Update Ping Details dialog to also show formatted datetimes
Fixes: #975
2024-05-29 15:43:06 +03:00
Pēteris Caune
7ef18cd706
Fix type warning and upgrade django-stubs-ext 2024-05-27 15:59:18 +03:00
Pēteris Caune
d56105e670
Update Slack integration to use channel name as the integration name
Fixes: #1003
2024-05-27 15:54:30 +03:00
Pēteris Caune
410b56baef
Fix type hints for django-stubs 5.0.1 2024-05-27 14:35:32 +03:00
Pēteris Caune
12bd59b2c1
Add system hostname logging in hc.logs.Handler 2024-05-24 13:58:18 +03:00
Pēteris Caune
0f8b6ca4c6
Re-enable strict type checking 2024-05-21 14:22:56 +03:00
dependabot[bot]
4d6884e20f
Bump aiosmtpd from 1.4.5 to 1.4.6 (#1002)
Bumps [aiosmtpd](https://github.com/aio-libs/aiosmtpd) from 1.4.5 to 1.4.6.
- [Release notes](https://github.com/aio-libs/aiosmtpd/releases)
- [Changelog](https://github.com/aio-libs/aiosmtpd/blob/master/release.py)
- [Commits](https://github.com/aio-libs/aiosmtpd/compare/v1.4.5...v1.4.6)

---
updated-dependencies:
- dependency-name: aiosmtpd
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-21 14:18:28 +03:00
Pēteris Caune
26a57343b1
Add a data migration to fill null api_notification.code values
Using model's default didn't quite work, as Django tried to use
the same UUID for all rows.
2024-05-17 10:43:46 +03:00
Pēteris Caune
d486d2db14
Add uniqueness constraint to api_notification.code
This is primarily to make notification lookups by code efficient.
We look up notifications by code in hc.api.views.boundces.

This field has a default value (uuid.uuid4), so any null values
will be filled with random UUIDs during migration.
2024-05-17 10:30:01 +03:00
Pēteris Caune
a8bd589c87
Move the Ansible collection under API Wrappers 2024-05-10 17:14:02 +03:00
Pēteris Caune
5202938b9c
Update log entry template to show body before UA 2024-05-10 16:09:13 +03:00
Pēteris Caune
582e7dcfb1
Add andrewthetechie/py-healthchecks.io in the 3rd party resources page 2024-05-10 11:23:06 +03:00
Pēteris Caune
1bdfbac775
Fix Sign In page to hide "Email Link" option if SMTP is not configured
Fixes: #922
2024-05-10 11:04:21 +03:00
Pēteris Caune
da90d33d38
Fix a bug in the log page that caused log events to sometimes load twice 2024-05-10 10:27:40 +03:00
Pēteris Caune
46c70a69ca
Disable strict type checking until we've sorted types-pycurl failure 2024-05-01 11:59:41 +03:00
Pēteris Caune
448226ab08
Pin types-pycurl version 2024-05-01 11:52:19 +03:00
Pēteris Caune
f4da8fba73
Update mypy action to install pygments types stubs 2024-05-01 11:44:07 +03:00
Pēteris Caune
de4c582d68
Enable strict type checking
(yay!!!)
2024-05-01 11:40:34 +03:00
Pēteris Caune
66d482c4af
Disable a type warning related to statsd having no type hints yet
python-statsd does have a PR for adding type hints, so
hopefully this is temporary.
2024-05-01 11:39:20 +03:00
Pēteris Caune
c940239757
Improve type hints in the pygmentize management command 2024-05-01 11:34:39 +03:00
Pēteris Caune
99d74d2c2c
Add type hint for view_on_site in channel admin 2024-05-01 11:18:31 +03:00
Pēteris Caune
5b905c1cbc
Bump django-stubs-ext to 5.0.0 2024-05-01 11:11:54 +03:00
Pēteris Caune
42b5e3168a
Fix inconsistent capitalization 2024-04-26 10:33:48 +03:00
Pēteris Caune
1ef7ef96b7
Update email notifications to include the timestamps of status flips 2024-04-26 10:24:45 +03:00
Pēteris Caune
4ec7a48082
Update the Discord integration to disable channel on HTTP 404 responses 2024-04-26 09:25:42 +03:00
Pēteris Caune
872e4d743e
Increase the timeout for sending Signal messages to 20 seconds
We're sometimes overshooting the 15 seconds, so let's try increasing
the limit a little.
2024-04-25 14:52:15 +03:00
Pēteris Caune
6fb46aee32
Fix integrations to include oncalendar schedules in notifications 2024-04-24 16:08:55 +03:00
Pēteris Caune
011fa98154
Improve PING_EMAIL_DOMAIN docs some more :-) 2024-04-22 15:34:44 +03:00
Pēteris Caune
8835d49798
Update CSS to highlight h2:target in "Server Configuration" page 2024-04-22 15:17:22 +03:00
Pēteris Caune
64f27edff4
Tweak PING_EMAIL_DOMAIN docs 2024-04-22 15:15:02 +03:00
Pēteris Caune
8391045b06
Add a link to /docker/README.md in PING_EMAIL_DOMAIN docs 2024-04-22 14:58:56 +03:00
Pēteris Caune
77f12085bf
Add a note about SMTPD_PORT in README 2024-04-22 14:55:15 +03:00
Pēteris Caune
8b7160bf09
Improve PING_EMAIL_DOMAIN docs 2024-04-22 14:10:52 +03:00
Pēteris Caune
ca01e4ab14
Upgrade pydantic to 2.7.0 2024-04-22 13:30:20 +03:00
Pēteris Caune
fce1ffbd4a
Update CHANGELOG 2024-04-22 13:03:45 +03:00
Pēteris Caune
4181399659
Fix Spike integration to not disclose check's code in incident data 2024-04-22 13:01:38 +03:00
Pēteris Caune
ddae6a04bf
Fix VictorOps integration to not disclose check's code in incident data 2024-04-22 12:57:10 +03:00
Pēteris Caune
c08ba1d872
Fix PagerTree integration to not disclose check's code in incident data 2024-04-22 12:46:18 +03:00
Pēteris Caune
53f554df1e
Fix type warning 2024-04-22 12:45:51 +03:00
Pēteris Caune
994bc10857
Update PagerDuty integration to use ping.formatted_kind_created 2024-04-22 12:31:03 +03:00
Pēteris Caune
18bd44a68b
Fix PagerDuty integration to not disclose check's code in incident data 2024-04-22 12:12:22 +03:00
Pēteris Caune
4e108073ac
Add handling for Discord "max num of webhooks reached" OAuth response 2024-04-22 10:50:51 +03:00
Aine
340379c12b
Add gitlab.com/etke.cc/go/healthchecks to docs/resources (#992) 2024-04-22 10:12:31 +03:00
Pēteris Caune
602ff2b667
Fix senddeletionscheduled to set the "created" field on flip objects
The "senddeletionscheduled" management command creates dummy
Flip objects, but does not save them to the database.
Some transport classes expect the flip object to have a non-null
"created" field. Normally it gets set when saving the flip object
to the database, here we need to do that manually.
2024-04-22 09:21:46 +03:00
Pēteris Caune
e683496bed
Move reusable ping formatting code to Ping model 2024-04-19 12:38:20 +03:00
Pēteris Caune
5c73556050
Include ping's kind in Opsgenie notification's "Last ping" field 2024-04-19 12:19:26 +03:00
Pēteris Caune
b9e82e44c9
Fix type warning 2024-04-19 12:02:20 +03:00
Pēteris Caune
7f03a9e738
Improve Opsgenie notifications (include description, schedule, link...) 2024-04-19 11:58:35 +03:00
Pēteris Caune
577602ae21
Fix Opsgenie integration to not disclose check's code in incident data 2024-04-19 11:24:12 +03:00
Pēteris Caune
bd64fab619
Fix hc.front.views.docs_search to handle AND/OR/NOT as query strings 2024-04-18 15:36:48 +03:00
Pēteris Caune
82ed392361
Update transport classes to use regular spaces instead of non-breaking 2024-04-17 16:35:43 +03:00
Pēteris Caune
a4d2094cef
Update the remove_project view to delete checks using lock_and_delete()
If we delete project by naively calling project.delete() then checks
can receive pings during the deletion, causing the deletion operation
to fail with an IntegrityError.

So instead do it like so:

* iterate over project's checks, call Check.lock_and_delete() on each
* in the end, call project.delete()
2024-04-16 16:36:49 +03:00
Pēteris Caune
0b28aa1cdf
Add a "set -o pipefail" tip in docs 2024-04-16 12:37:57 +03:00
Pēteris Caune
7c7a103632
Improve type hints 2024-04-15 15:41:10 +03:00
Pēteris Caune
a567eb23e0
Add a missing closing div tag 2024-04-15 15:33:55 +03:00
Pēteris Caune
83f161d657
Update transport classes to use Transport.last_ping() consistently
* Instead of check.n_pings (int) use last_ping().n
* Instead of check.last_ping (datetime) use last_ping().created

There is a time gap from creating a flip object to processing
it (sending out an alert). We want the notification to reflect
the check's state at the moment the flip was created. To do this,
we use the Transport.last_ping() helper method which retrieves
the last ping *that is not newer than the flip*.

This commit updates transport classes and templates to use
Transport.last_ping() consistently everywhere.
2024-04-15 15:09:17 +03:00
Pēteris Caune
d8a46349a8
Update Transport.last_ping() to ignore pings newer than the flip 2024-04-15 12:47:11 +03:00
Pēteris Caune
f77eac08b0
Update CHANGELOG 2024-04-15 10:44:33 +03:00
moraj-turing
3718ff57c7
Add support for system theme (#987)
Add support for system theme

---------

Co-authored-by: Juan Mora <juan@nimble.gt>
Co-authored-by: Pēteris Caune <cuu508@gmail.com>
2024-04-15 10:42:16 +03:00
Pēteris Caune
8ba75475bb
Add missing word in docs 2024-04-14 13:02:24 +03:00
Pēteris Caune
26f7facda2
Clean up 2024-04-12 20:13:35 +03:00
Pēteris Caune
81f202e2ac
Rename notify_flip -> notify 2024-04-12 15:49:47 +03:00
Pēteris Caune
4df58aaaef
Fix type warnings 2024-04-12 15:46:46 +03:00
Pēteris Caune
5bdb01baf9
Fix the Zulip integration to use Flip.new_status 2024-04-12 15:43:06 +03:00
Pēteris Caune
6631a5f76a
Fix the WhatsApp integration to use Flip.new_status 2024-04-12 15:40:19 +03:00
Pēteris Caune
3301cce251
Fix the Splunk On-Call integration to use Flip.new_status 2024-04-12 15:37:43 +03:00
Pēteris Caune
6e27d88ec9
Fix the Trello integration to use Flip.new_status 2024-04-12 15:34:54 +03:00
Pēteris Caune
bcecf058c2
Fix the Telegram integration to use Flip.new_status 2024-04-12 15:33:03 +03:00
Pēteris Caune
17e9f33bb9
Fix the Spike integration to use Flip.new_status 2024-04-12 15:30:26 +03:00
Pēteris Caune
2913b8faf5
Fix the Signal integration to use Flip.new_status 2024-04-12 15:27:13 +03:00
Pēteris Caune
f673f599c5
Fix the RocketChat integration to use Flip.new_status 2024-04-12 15:18:56 +03:00
Pēteris Caune
af36078f10
Fix the Pushover integration to use Flip.new_status 2024-04-12 15:14:19 +03:00
Pēteris Caune
a485bea2b2
Fix the Pushullet integration to use Flip.new_status 2024-04-12 15:08:25 +03:00
Pēteris Caune
f24a1dbc25
Fix the PagerDuty integration to use Flip.new_status 2024-04-12 15:03:31 +03:00
Pēteris Caune
f060874be5
Fix the PagerTree integration to use Flip.new_status 2024-04-12 15:00:53 +03:00
Pēteris Caune
e5018b1195
Fix the Opsgenie integration to use Flip.new_status 2024-04-12 14:57:21 +03:00
Pēteris Caune
4da32b9214
Fix the ntfy integration to use Flip.new_status 2024-04-12 14:53:29 +03:00
Pēteris Caune
462d4776d8
Fix the MS Teams integration to use Flip.new_status 2024-04-12 14:48:19 +03:00
Pēteris Caune
b3dad4ac57
Fix the Matrix integration to use Flip.new_status 2024-04-12 14:45:13 +03:00
Pēteris Caune
1e51d0d177
Fix the LINE Notify integration to use Flip.new_status 2024-04-12 14:42:57 +03:00
Pēteris Caune
1aa068553d
Fix the Gotify integration to use Flip.new_status 2024-04-12 14:39:08 +03:00
Pēteris Caune
5b6dfb9101
Fix Slack, Discord, Mattermost integrations to use Flip.new_status 2024-04-12 14:34:55 +03:00
Pēteris Caune
2fd50312c4
Update the Call integration to take a Flip argument 2024-04-12 14:25:19 +03:00
Pēteris Caune
fe9bb0d1e5
Fix the Apprise integration to use Flip.new_status 2024-04-12 14:17:12 +03:00
Pēteris Caune
a6b8f4c71e
Fix the shell integration to use Flip.new_status 2024-04-12 14:10:34 +03:00
Pēteris Caune
8372abf019
Fix the email integration to use Flip.new_status 2024-04-12 14:05:44 +03:00
Pēteris Caune
28fdfd1362
Change Channel.notify() signature to take Flip object as an argument
... and pass it to Transport.notify_flip().

This allows us to pass flip-specific information (the flip timestamp,
the new status) to transport classes.
2024-04-12 13:54:16 +03:00
Pēteris Caune
6e130f1749
Change Transport.is_noop() to accept status:str instead of check:Check
I'm planning to change Channel.notify() signature to take a Flip
object as an argument instead of a Check object. This change is
in preparation for these changes.
2024-04-12 13:23:29 +03:00
Pēteris Caune
aaa8681fec
Update Check.prune() to also delete flip objects.
Check.prune() now deletes flips older than the oldest
retained ping *and* older than 3*31=93 days.
2024-04-11 12:56:28 +03:00
Pēteris Caune
b3bd21a408
Fix the sorting of events with identical timestamps
If timestamps are equal, put flips chronologically after pings.
We are showing events in reverse chronological order (newer
events at the top), so the flips will now display
*above* the pings that caused them.
2024-04-10 14:50:31 +03:00
Pēteris Caune
9bb5656d40
Implement dynamic favicon in the projects overview page
cc: #971
2024-04-10 14:36:42 +03:00
Pēteris Caune
71e8112c95
Clean up CSS 2024-04-10 09:52:58 +03:00
Pēteris Caune
27c065230a
Switch back to using integer timestamps in the log page
The live-updating code still needs float timestamps, but
we only need them for the most recent event (so we know
the lower threshold for fetching new events). We now send
the float timestamp separately:

* in the `/log/` view, we put it in HTML content, in a <script> tag
* in the `/log_events/` view we put it in response header

The main benefit of this is smaller response sizes for the
`/log/` and `/log_events/` views.
2024-04-09 14:24:43 +03:00
Pēteris Caune
d7948d9939
Show status changes (flips) in check's log page
Fixes: #447
2024-04-09 12:39:42 +03:00
Pēteris Caune
c99c357709
Improve styling for live-loaded log events
Live-loaded notification events used blue border which
looked bad both in light mode and dark mode. Now fixed to use red.
2024-04-08 10:07:54 +03:00
Pēteris Caune
7734839bcc
Fix Dockerfile to avoid having /wheels/ in the final image
This is experimental, let's see if it works...
2024-04-05 13:59:34 +03:00
Pēteris Caune
198c80305e
Remove unused files from docker image, reducing its size slightly 2024-04-05 13:38:42 +03:00
Pēteris Caune
6e2df6a71c
Update CHANGELOG for v3.3 release 2024-04-03 18:15:10 +03:00
Pēteris Caune
e1344e10b3
Upgrade to Django 5.0.4 2024-04-03 18:14:04 +03:00
Pēteris Caune
037d268bb6
Add PING_ENDPOINT in local_settings.py.example
cc: #985
2024-04-02 15:23:48 +03:00
Pēteris Caune
b29ba7c11b
Make prunepingsslow more resilient to concurrent check deletes 2024-04-02 12:11:26 +03:00
Pēteris Caune
400393010f
Clarify two Slack setup flows in README 2024-03-30 09:38:55 +02:00
Pēteris Caune
84ce0199ae
Update package versions 2024-03-30 09:17:29 +02:00
Pēteris Caune
97adf7c2eb
Update moment.js timezone data to 0.5.44-2023d 2024-03-29 17:14:40 +02:00
Pēteris Caune
17b7947bed
Add SITE_NAME setting override in test_details 2024-03-26 17:33:13 +02:00
Pēteris Caune
430d9ad955
Add testcases for favicons 2024-03-26 17:28:58 +02:00
Pēteris Caune
9f5cec1c7a
Simplify JS that sets the favicon
* look up the favicon DOM element only once
* update the favicon only if title changes, not on every refresh
* details.js: use the base URL when constructing favicon URL
2024-03-26 17:28:47 +02:00
Pēteris Caune
ab639e6cfd
Fix templates to output correct favicon tag 2024-03-26 17:27:32 +02:00
Prince Khunt
249b9e163d
Proposing addition of a visual indication to favicon on down (#977)
Implement dynamic favicon in the "Checks" and "Details" pages

Fixes: #971
2024-03-26 17:05:23 +02:00
Pēteris Caune
bc5e9fde80
Fix the filtering code to handle 0 hits 2024-03-21 15:43:17 +02:00
Pēteris Caune
f313e17678
Remove whitespace to avoid +1 empty tbody on every refresh 2024-03-21 15:24:37 +02:00
Pēteris Caune
b5364651ee
Increase the event limit in the details page from 20 to 30 2024-03-21 14:52:46 +02:00
Pēteris Caune
9c5ee74c20
Improve tests 2024-03-21 14:34:50 +02:00
Pēteris Caune
b9e788b9c6
Change log_events response content type from JSON to HTML 2024-03-21 13:21:09 +02:00
Pēteris Caune
3026162a85
Fix date bounds check in log_events
log_events was using Check.created as the lower bound.
If a check has many pings, we need to use the oldest
visible ping's date as the lower bound, otherwise
we may return notifications older than the oldest visible
ping.
2024-03-21 11:37:46 +02:00
Pēteris Caune
855fac0973
Implement filtering by event type in the Log page
Fixes: #873
2024-03-21 11:04:45 +02:00
Pēteris Caune
df6895ed1f
Fix regression in log auto-updating code
Also switch back to using integers for the slider min and max
values, and for the hc.front.forms.LogFilterForm.
2024-03-19 19:11:46 +02:00
Pēteris Caune
47894f6add
Rearrange the log page to make room for more filters
* Switch from nouislider to simpler <input type="range">
* Move it to a sidebar

Also, fix a bug in _get_events where the "start" local variable
got clobbered, and made the date range for the Notification
query wrong.
2024-03-19 18:47:13 +02:00
Pēteris Caune
22325565b5
Remove unused import 2024-03-18 13:02:46 +02:00
Pēteris Caune
3615d26aea
Remove debug statement 2024-03-18 12:56:18 +02:00
Pēteris Caune
274a59956a
Make statsd metrics collection optional
To enable, set STATSD_HOST env var (or set STATSD_HOST in
local_settings.py):

STATSD_HOST=localhost:1234

cc: #974
2024-03-18 12:55:36 +02:00
Pēteris Caune
33e58fa014
Change the signup flow to accept registered users
(and sign them in instead)
2024-03-15 17:30:06 +02:00
Pēteris Caune
e6a974ac1f
Remove CodeQL workflow (will use GitHub's default setup instead) 2024-03-15 13:04:47 +02:00
Pēteris Caune
49b496fc3f
Add a decimal place in the reported uptime percentage
(for example, instead of "uptime: 99.9%" report "uptime: 99.93%)
2024-03-14 10:25:53 +02:00
Pēteris Caune
49abf7b12d
Upgrade python-fido2 to 1.1.3 2024-03-14 09:17:29 +02:00
Pēteris Caune
a0545b875e
Add "Last ping subject" field in email notifications 2024-03-13 15:45:27 +02:00
Pēteris Caune
e57e3185ad
Fix typo 2024-03-12 18:30:40 +02:00
Pēteris Caune
30b30d6af5
Add id attributes for addressing 2024-03-12 18:25:21 +02:00
Pēteris Caune
4485c8bd6e
Improve docs about check and project transfers 2024-03-12 17:54:24 +02:00
Pēteris Caune
035989db3a
Downgrade pycurl to 7.45.2
pycurl 7.45.3 returns error code 77 (CURLE_SSL_CACERT_BADFILE)
for TLS connections on Debian-based systems:

https://github.com/pycurl/pycurl/issues/834
2024-03-12 11:37:56 +02:00
Pēteris Caune
0a7744c389
Bump package versions 2024-03-05 18:54:11 +02:00
Pēteris Caune
638d11c969
Improve type hints 2024-03-05 17:18:55 +02:00
Pēteris Caune
5715ac9f24
Simplify Signal response parsing 2024-03-04 12:31:18 +02:00
Pēteris Caune
5c82b2e9ac
Add handling for Signal messages with null recipient number
If the recipient has set up an username, the responses
from Signal will return "number": null.

Example scenario:

* Alice sets up a Signal integration
* Alice sets up an username for their Signal account
* Alice deletes their Signal account
* Healthchecks sends a message to Alice's Signal account
* Signal responds with UNREGISTERED_FAILURE, but the
  number field in the response is null

The fix is to:

* Allow null in the "number" field as a valid value
* Ignore the value of the "number" field when reading Signal responses
2024-03-04 12:26:56 +02:00
Pēteris Caune
d7b683e9d1
Update more notification templates to handle Check.last_ping == None 2024-03-01 16:25:05 +02:00
Pēteris Caune
4d2a3227f3
Update more notification templates to handle Check.last_ping == None 2024-03-01 16:07:13 +02:00
Pēteris Caune
b0c0c1d8c3
Fix top nav in the badges page 2024-03-01 12:36:29 +02:00
Pēteris Caune
4c97068776
Update notification templates to handle Check.last_ping == None 2024-02-29 14:38:35 +02:00
Pēteris Caune
8955c1d7cd
Remove "Last ping was <time interval> ago." from WhatsApp messages
In some rare cases the last ping can be None (i.e., there was no
last ping). In these cases we would need to omit the
"Last ping was <time interval> ago." part from the message.
To avoid creating a *third* WhatsApp content template, remove
the "Last ping was <time interval> ago." part from the message
altogether.

What are the cases when last ping could be None? Here's one:

* User creates a check
* User sends a "/start" signal
* The grace time passes, the check goes down. There's no
  previous "success" or "fail" signal, so the check's
  "last_ping" field will be None.
2024-02-29 11:42:44 +02:00
Pēteris Caune
af6731a24f
Fix code to use project.check_set instead of Check.objects.filter(...)
No functional difference, just cleaner.
2024-02-28 10:01:52 +02:00
Pēteris Caune
dbc8ebb73a
Tweak the badges page 2024-02-27 13:06:31 +02:00
Pēteris Caune
1322bb1123
Add support for per-check status badges
Fixes: #853
2024-02-27 12:55:51 +02:00
Pēteris Caune
5eb21a6919
Fix HTML snippet in the Status Badges page 2024-02-26 17:47:43 +02:00
Pēteris Caune
4959856e58
Redesign the "Status Badges" page 2024-02-26 12:34:26 +02:00
Pēteris Caune
6686147cb1
Update CHANGELOG 2024-02-23 11:43:02 +02:00
Michael Boateng
9770bc1ea0
Add auto-refresh functionality to log page (#963)
Add auto-refresh functionality to log page

Fixes: #957

---------

Co-authored-by: Pēteris Caune <cuu508@gmail.com>
2024-02-23 11:36:31 +02:00
Pēteris Caune
51977e185b
Bump pydantic version to 2.6.1 2024-02-21 18:49:39 +02:00
Pēteris Caune
86930d1fba
Update MS Teams onboarding instructions 2024-02-20 12:49:40 +02:00
Pēteris Caune
f6378fc26b
Fix Gotify integration to handle Gotify server URLs with paths
Fixes: #964
2024-02-20 10:12:54 +02:00
Pēteris Caune
52a8df4dbf
Fix naturaltime usage in the WhatsApp integration
When preparing WhatsApp payload we call naturaltime() and
serialize its output as JSON. naturaltime can return a lazy object
which causes an exception during JSON serialization. The fix
is to wrap naturaltime in str() to force its output to be a string.
2024-02-17 10:05:59 +02:00
Pēteris Caune
12b5c36897
Add a system check that warns about WhatsApp misconfiguration 2024-02-16 12:58:48 +02:00
Pēteris Caune
4ef470730f
Fix tests 2024-02-16 12:45:10 +02:00
Pēteris Caune
db31cacc86
Fix syntax 2024-02-16 12:37:59 +02:00
Pēteris Caune
ec0791b4ee
Update the WhatsApp integration to use Twilio Content Templates 2024-02-16 12:37:11 +02:00
Pēteris Caune
0d5c139348
Tweak wording 2024-02-15 13:22:35 +02:00
Pēteris Caune
aefc0d36c6
Improve email docs 2024-02-15 13:17:24 +02:00
Pēteris Caune
1ec4fbc0c9
Add a note about flip retention in docs 2024-02-15 11:16:01 +02:00
Pēteris Caune
1250195e3e
Add support for $NAME_JSON and $BODY_JSON placeholders 2024-02-14 11:52:35 +02:00
Pēteris Caune
b1cd529532
Fix type annotation 2024-02-12 11:34:27 +02:00
mmomjian
3728231132
Update filtering_rules_modal.html (#956)
whoops!
2024-02-12 11:17:29 +02:00
Pēteris Caune
c99b644a22
Update CHANGELOG for v3.2 release 2024-02-09 14:09:45 +02:00
Pēteris Caune
33284bd93f
Fix MariaDB version check
Naively comparing with string "10.7" does not quite work for
versions 10.10 and 10.11 :-)
2024-02-09 14:01:48 +02:00
Pēteris Caune
d881afa3f0
Add a system check to warn about a required MariaDB UUID migration
cc: #929
2024-02-09 11:32:30 +02:00
Pēteris Caune
d37f830be9
Tweak the sizing of grace time input groups some more 2024-02-08 16:13:50 +02:00
Pēteris Caune
97442e9d2d
Tweak the sizing of grace time input groups 2024-02-08 16:10:54 +02:00
Pēteris Caune
56f8bd93e3
Update CHANGELOG 2024-02-08 15:44:27 +02:00
Michael Boateng
6bfd9c901c
Make grace time editable when job is created (#953)
Fixes: #945
2024-02-08 15:34:52 +02:00
dependabot[bot]
1b36f3a6f6
Bump django from 5.0.1 to 5.0.2 (#955)
Bumps [django](https://github.com/django/django) from 5.0.1 to 5.0.2.
- [Commits](https://github.com/django/django/compare/5.0.1...5.0.2)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-08 11:22:24 +02:00
Pēteris Caune
4d0cf7fb80
Remove @nolog decorator
The purpose of @nolog was to disable logging in certain
test cases to avoid console spam during tests. But with the
current logging configuration console is clean even without it.
2024-02-06 17:30:24 +02:00
Pēteris Caune
2923a39a46
Add logging for "signal-cli call failed (<error_code>)" errors 2024-02-06 17:20:29 +02:00
Pēteris Caune
6919202a8f
Add missing space 2024-02-06 15:26:43 +02:00
Pēteris Caune
2ea7ff5950
Improve copy in email.md 2024-02-06 10:09:25 +02:00
mmomjian
00c5b767d4
Update filtering rules to specify that they are case sensitive (#944)
* Update filtering_rules_modal.html

We should specify that the keywords are case sensitive - this isn't obvious to the user.

* Update filtering_rules_modal.html

* Update email.html-fragment

* Update email.md
2024-02-06 09:48:47 +02:00
Pēteris Caune
79382ca82e
Fix systemd expression in a testcase to work after 2024-02-01 2024-02-02 13:00:29 +02:00
Pēteris Caune
7ba2374616
Add API docs for /api/v3/status/
Fixes #949
2024-01-29 11:31:05 +02:00
Pēteris Caune
08eb279574
Bump pydantic version from 2.5.2 to 2.5.3 2024-01-23 19:26:07 +02:00
Pēteris Caune
7cb47188b9
Remove redundant cast 2024-01-22 16:09:20 +02:00
Pēteris Caune
16450a66c7
Add tooltips to tag buttons in the checks list screen
Fixes: #911
2024-01-22 15:20:09 +02:00
Pēteris Caune
42f88f4fb0
Add S3_SECURE setting
It controls whether to use secure (TLS) connection to S3 or not.
2024-01-10 12:19:45 +02:00
Pēteris Caune
ae94648efd
Fix minio-related mypy warnings
A few warnings remain, I think these should be fixed in minio-py:

https://github.com/minio/minio-py/pull/1389
2024-01-04 16:01:34 +02:00
Pēteris Caune
40742bba1d
Bump Django from 5.0 to 5.0.1 2024-01-04 15:29:05 +02:00
Pēteris Caune
492d031341
Update Third-Party Resources page
* remove projects that have not been updated in years
* add hc-monitor and danidelvalle/healthchecks-decorator
2024-01-04 15:15:51 +02:00
Pēteris Caune
9562e48329
Improve PING_ENDPOINT docs
Fixes: #933
2023-12-22 14:06:38 +02:00
Pēteris Caune
1284ee27a6
Fix the case where PING_BODY_LIMIT is None
cc: #931
2023-12-22 12:29:32 +02:00
Pēteris Caune
54ffe39143
Fix the handling of ping bodies > 2.5MB
Django has a DATA_UPLOAD_MAX_MEMORY_SIZE which controls the maximum
allowed request body size, and is 2.5MB by default.

We now bump up DATA_UPLOAD_MAX_MEMORY_SIZE to be no lower
than PING_BODY_LIMIT.

Fixes: #931
2023-12-22 12:05:16 +02:00
Pēteris Caune
7c5e3130fb
Hardcode a higher S3 operation timeout, add sorting by Check.code 2023-12-21 16:12:22 +02:00
Pēteris Caune
767c3ae702
Add a management command for pruning all checks 2023-12-21 14:55:05 +02:00
Pēteris Caune
b0f8c730f5
Change query in Check.prune() to work around pg index selection issue
In prune(), we need to look up the earliest ping in the database
for a given check. The old version did:

    ping = self.ping_set.earliest("id")

The new version does:

    ping = self.ping_set.earliest("created")

Both yield the same result, but in the first case Postgres may
decide to use the index for the api_ping.id column and scan
almost the entire table.

In the second case it uses the index for the api_ping.owner_id column,
and scans just the rows associated with the check.
2023-12-21 12:00:05 +02:00
Pēteris Caune
f0f2a9aed8
Add a system check which warns about missing SMTP credentials 2023-12-19 14:47:12 +02:00
Pēteris Caune
2a0b8f4e6f
Simplify datetime object initializations in tests 2023-12-19 14:20:04 +02:00
Pēteris Caune
c8897b7026
Improve the handling of StopIteration exceptions
Instead of returning a datetime in far future,
get_grace_start() now returns None which (meaning "never").
2023-12-19 14:05:10 +02:00
Pēteris Caune
1d6e7297be
Fix get_grace_start to handle StopIteration exceptions
These can happen with "one-shot" OnCalendar schedules,
for example: "2023-12-19 11:30"
2023-12-19 13:29:52 +02:00
Pēteris Caune
ce1d7bd4cb
Add a test for #930 2023-12-18 15:58:04 +02:00
Marlene Koh
2b318cab84
Fix crash when email host not configured (#930)
* Log exception when email host not configured instead of raising assertion error

* Fix tests

* Reset to master

* Check email host before sending email for project invites
2023-12-18 15:53:35 +02:00
Pēteris Caune
39a21861ba
Improve type hints 2023-12-15 14:31:30 +02:00
Pēteris Caune
cb1b216125
Increase uWSGI buffer size to allow requests with large cookies
Fixes: #925
2023-12-15 13:43:27 +02:00
Pēteris Caune
1026c0a1f2
Update Dockerfile to use Python 3.12 2023-12-15 11:15:42 +02:00
Pēteris Caune
3b8d473980
Add --skip-checks to "manage.py smtpd" to avoid multiple check runs 2023-12-15 11:15:31 +02:00
Pēteris Caune
65b9dd371f
Add system check to validate settings.SITE_ROOT
Fixes: #895
2023-12-15 11:04:21 +02:00
Pēteris Caune
6172a0bbaa
Update Spike.sh instructions 2023-12-14 12:20:36 +02:00
Pēteris Caune
be004c3e0d
Update Opsgenie instructions 2023-12-14 11:41:23 +02:00
Pēteris Caune
24a741f630
Update CHANGELOG for v3.1 release 2023-12-13 14:45:02 +02:00
Pēteris Caune
86e3a38239
Fix capitalization 2023-12-13 14:44:32 +02:00
Pēteris Caune
734d146283
Add "Auto provisioning" page under Docs > Guides 2023-12-13 14:26:28 +02:00
Pēteris Caune
9b27600cbe
Fix oncalendar_preview to require a logged in user 2023-12-13 12:55:12 +02:00
Pēteris Caune
bd86109cd1
Update screenshots 2023-12-13 10:55:44 +02:00
Pēteris Caune
5cc5acdec0
Update screenshots 2023-12-13 10:40:50 +02:00
Pēteris Caune
5d07645823
Update the "Configuring Checks" docs page with OnCalendar schedules 2023-12-12 13:39:00 +02:00
Pēteris Caune
bb76bf42ac
Fix cron and oncalendar preview when timezone changes 2023-12-12 13:33:51 +02:00
Pēteris Caune
7bef951484
Fix oncalendar schedule display in admin 2023-12-12 12:51:13 +02:00
Pēteris Caune
d8888c033f
Upgrade to oncalendar 1.0 2023-12-11 14:59:31 +02:00
Pēteris Caune
0abfe58cef
Implement OnCalendar schedules in the "Add Check" dialog 2023-12-08 12:02:01 +02:00
Pēteris Caune
f33c296349
Fix checks list view to display oncalendar schedules 2023-12-07 16:07:45 +02:00
Pēteris Caune
52ec0bf4f1
Add a testcase for oncalendar syntax in hc.api.tests.check_test_model 2023-12-07 15:52:32 +02:00
Pēteris Caune
cadea5ab88
Add maxlength attributes 2023-12-07 15:46:46 +02:00
Pēteris Caune
b82274d827
Clean up JS for the "Update Timeout" modal
+ fix a bug where the preview doesn't update after closing
and re-opening the dialog.
2023-12-07 15:41:04 +02:00
Pēteris Caune
7ad01c4692
Fix labels and add a link to OnCalendar reference 2023-12-07 15:40:14 +02:00
Pēteris Caune
6a2fbba40a
Tweak paddings and margins in the "Update Timeout" modal 2023-12-07 15:39:47 +02:00
Pēteris Caune
964ec7a1d4
Update the API docs regarding OnCalendar schedules 2023-12-07 15:03:50 +02:00
Pēteris Caune
54b8092be9
Fix mypy warnings 2023-12-07 14:44:31 +02:00
Pēteris Caune
fc56cf2635
Add API support for OnCalendar schedules 2023-12-07 14:03:35 +02:00
Pēteris Caune
980520e3b2
Fix the validation of multi-line OnCalendar expressions 2023-12-06 16:50:59 +02:00
Pēteris Caune
fa7e37360f
Tweak the number of iterations in oncalendar preview 2023-12-06 16:34:48 +02:00
Pēteris Caune
5cd874f1de
Fix duplicate HTML element ids in the "Update timeout" modal 2023-12-06 16:34:28 +02:00
Pēteris Caune
461b7ec751
Move "Update timeout" modal's CSS to a separate file 2023-12-06 16:33:50 +02:00
Pēteris Caune
82e5c60ffa
Update timezone selectors to recognize "oncalendar" schedules 2023-12-06 16:32:39 +02:00
Pēteris Caune
e9882879ed
Reduce repetition in test_cron_preview 2023-12-06 16:07:20 +02:00
Pēteris Caune
c1a3ae6f48
Rename CronExpressionValidator -> CronValidator 2023-12-06 16:05:50 +02:00
Pēteris Caune
1ab0e8e270
Enable type hint checking for the oncalendar package 2023-12-06 15:57:01 +02:00
Pēteris Caune
d65f41d192
Add support for systemd's OnCalendar schedules
(work-in-progress)

cc: #919
2023-12-06 15:42:57 +02:00
Pēteris Caune
5daa13a57f
Upgrade to Django 5.0 2023-12-06 11:19:57 +02:00
Pēteris Caune
7c0442cf5f
Update Django to 4.2.8 2023-12-04 11:02:20 +02:00
Pēteris Caune
c241ddb5e6
Update package versions 2023-11-30 10:24:18 +02:00
Pēteris Caune
0f62a8cd14
Update the Signal integration to disable channel on UNREGISTERED_FAILURE 2023-11-28 13:57:27 +02:00
Pēteris Caune
18e1a3d9e4
Update Pushover onboarding code to generate lowercase state values
Since recently, Pushover seems to return them converted
to lowercase, this is a workaround for that.
2023-11-27 14:52:19 +02:00
Pēteris Caune
b9df18728a
Improve error logging in hc.lib.s3._remove_objects 2023-11-24 10:11:31 +02:00
Pēteris Caune
9c8bb54f7f
Update Twilio integrations to not retry error 21211 2023-11-23 16:18:05 +02:00
Pēteris Caune
ccc7e103ba
Fix capitalization 2023-11-23 15:43:31 +02:00
Pēteris Caune
1b1899052c
Make payments_subscription.user_id not nullable 2023-11-22 10:17:01 +02:00
Pēteris Caune
fa25453734
Add migration for the Group integration 2023-11-22 10:16:27 +02:00
Pēteris Caune
8f28d26b0b
Remove Unpack from mypy.ini (now enabled by default) 2023-11-21 17:10:26 +02:00
Pēteris Caune
16a5f2fb98
Update pydantic to 2.5.2 2023-11-21 13:49:49 +02:00
Pēteris Caune
96823a7f90
Add logging for failed webauthn key registrations 2023-11-17 16:06:39 +02:00
Pēteris Caune
decd1d4b87
Improve TOTP auto-submit code
* Listen to "input" event only, don't need "keyup"
* Add form double-submit protection
* Rewrite in plain JS (the page no longer loads jQuery)
2023-11-17 11:00:41 +02:00
Pēteris Caune
daad54aea9
Disable autosubmit in TOTP form as it causes issues in Vivaldi 2023-11-17 10:03:20 +02:00
Pēteris Caune
7c8dbec62e
Fix webauthn registration failure on Firefox < 119 with Ed25519 keys 2023-11-15 15:58:32 +02:00
Pēteris Caune
2a2c7d66ec
Fix mypy warning 2023-11-15 10:22:22 +02:00
Pēteris Caune
cff4138774
Fix webauthn registration failure on Firefox with Bitwarden extension 2023-11-15 10:13:40 +02:00
Pēteris Caune
788d3fa661
Add extra logging for unexpected Twilio API responses 2023-11-14 14:56:21 +02:00
Pēteris Caune
e308aa473f
Update Slack transport to not log HTTP 400 "invalid_token" events
(But still log other HTTP 400 responses)
2023-11-14 10:10:30 +02:00
Pēteris Caune
64b97b2e06
Add error filter in Notification admin 2023-11-14 09:54:33 +02:00
Pēteris Caune
ee62dc174c
Implement audo-submit in TOTP entry screen. Fixes #905 2023-11-13 15:20:23 +02:00
Pēteris Caune
336e71e52f
Update Matrix onboarding instructions 2023-11-13 13:03:36 +02:00
Pēteris Caune
6964d67dd7
Fix Pushover to not inspect HTTP 500 response bodies 2023-11-13 11:40:38 +02:00
Pēteris Caune
1b942a8e77
Update the Pushover integration to not retry on invalid user errors 2023-11-13 11:27:01 +02:00
Pēteris Caune
4a3a6f2b03
Improve Pushover notifications (include tags, period, last ping type) 2023-11-13 10:29:03 +02:00
Pēteris Caune
49d6f3fe5b
Fix "Ping Details" dialog to handle email bodies not yet uploaded to S3 2023-11-10 12:49:10 +02:00
Pēteris Caune
c3b4100f3b
Add a management command to notify admins about new log records 2023-11-09 14:00:22 +02:00
Pēteris Caune
71f1a8d9a8
Update Slack integration to not retry when it hits 400 "invalid_token" 2023-11-09 12:52:25 +02:00
Pēteris Caune
d7a2e24ccf
Replace "Slack integration" with "{site_name} integration for Slack" 2023-11-03 09:07:58 +02:00
Pēteris Caune
97ec5c6ee0
Update the Splunk On-Call integration to not retry HTTP 404 responses 2023-11-02 13:32:09 +02:00
Pēteris Caune
9d9875e3ed
Add "By last error" filter and "Disable" action in Channel admin 2023-11-02 10:08:22 +02:00
Pēteris Caune
8ff30a1536
Add logging for Slack HTTP 400 responses 2023-11-01 12:27:15 +02:00
Pēteris Caune
f73e4520fd
Update tests to check if logger.warning() gets called 2023-11-01 12:14:30 +02:00
Pēteris Caune
c74f2b166a
Update package versions 2023-11-01 09:42:59 +02:00
Pēteris Caune
4fa91e8336
Fix merge oops
Add back database fields that are present in migrations
but not in the model itself
2023-11-01 09:38:50 +02:00
Pēteris Caune
c18d2ea1dd
Update logging configuration to write logs to database 2023-11-01 09:34:45 +02:00
Pēteris Caune
f08ac87888
Update CHANGELOG for v3.0.1 release 2023-10-30 12:52:17 +02:00
Pēteris Caune
f8a9077c76
Fix DST handling in Check.get_grace_start() 2023-10-30 11:53:52 +02:00
Pēteris Caune
a7b8799e36
Fix Signal transport to handle JSON-RPC messages with no ids 2023-10-27 21:30:26 +03:00
Pēteris Caune
cf40553033
Update Signal transport to log unexpected responses 2023-10-27 17:41:09 +03:00
Pēteris Caune
fe67db5726
Fix the "Update Check" API call to handle concurrent deletes 2023-10-27 14:21:10 +03:00
Pēteris Caune
f962e869ba
Fix a race condition in concurrent "Delete Check" API calls 2023-10-27 13:36:47 +03:00
Pēteris Caune
ec9649a6a6
Fix logging calls to avoid string formatting in client code 2023-10-27 10:33:55 +03:00
Pēteris Caune
1eb92c9fe7
Switch to using Pydantic for parsing Gotify configuration 2023-10-26 12:01:42 +03:00
Pēteris Caune
cdac9b3128
Switch to using Pydantic for parsing Trello configuration 2023-10-26 11:57:49 +03:00
Pēteris Caune
dea66b85af
Switch to using Pydantic for parsing ntfy configuration
Also, fix a bug in the "Edit ntfy integration" form,
where the token was not filled in the form on page load.
2023-10-26 10:32:41 +03:00
Pēteris Caune
ab7449a279
Improve type hints in hc.lib.s3 2023-10-26 10:19:28 +03:00
Pēteris Caune
d059431b08
Improve type hints in hc.lib.webauthn 2023-10-26 10:14:30 +03:00
Pēteris Caune
6872ab3eb6
Improve type hints in hc.accounts.backends 2023-10-26 09:31:11 +03:00
Pēteris Caune
8a42d2b66b
Add type hints in hc.accounts.middleware 2023-10-26 09:16:18 +03:00
Pēteris Caune
73ca8d2c9d
Improve type hints 2023-10-26 08:51:48 +03:00
Pēteris Caune
db1088eb9c
Add type hints in hc.front.templatetags.linemode 2023-10-26 08:41:05 +03:00
Pēteris Caune
5afc2ea20c
Improve type hints 2023-10-26 08:35:06 +03:00
Pēteris Caune
343e55bd4f
Improve type hints 2023-10-25 18:12:12 +03:00
Pēteris Caune
37ee900798
Improve Discord tests 2023-10-25 14:51:54 +03:00
Pēteris Caune
232f534bc0
Improve type hints in hc.lib.curl 2023-10-25 14:47:52 +03:00
Pēteris Caune
65eddaacd6
Improve API error handling in hc.front.views.add_linenotify_complete 2023-10-25 14:46:57 +03:00
Pēteris Caune
bff59c92ed
Improve API error handling in hc.front.views.trello_settings 2023-10-25 14:20:51 +03:00
Pēteris Caune
da22899cd6
Improve OAuth error handling in hc.front.views.add_discord_complete 2023-10-25 13:49:14 +03:00
Pēteris Caune
8b006e0d58
Move nolog decorator to hc.test so it can be reused 2023-10-25 13:48:55 +03:00
Pēteris Caune
511863594c
Improve OAuth error handling in hc.front.views.add_slack_complete 2023-10-25 13:40:56 +03:00
Pēteris Caune
4d0ec64da8
Update Signal integration to use Ping.get_kind_display() 2023-10-23 11:47:17 +03:00
Pēteris Caune
6110b10c3c
Update Telegram integration to use Ping.get_kind_display() 2023-10-23 11:43:07 +03:00
Pēteris Caune
c9540f8ddd
Update email integration to use Ping.get_kind_display() 2023-10-23 11:39:37 +03:00
Pēteris Caune
6c7f5881a7
Update ntfy integration to use Ping.get_kind_display() 2023-10-23 11:37:18 +03:00
Pēteris Caune
60630c1dee
Fix special character escaping in ntfy notifications 2023-10-23 11:22:14 +03:00
Pēteris Caune
7e503d9c33
Move the "join Matrix room" code to hc.lib.matrix 2023-10-23 09:32:03 +03:00
Pēteris Caune
1004ca776a
Switch to using Pydantic for validating Matrix join responses 2023-10-23 08:53:15 +03:00
Pēteris Caune
cf552c86cb
Switch to using Pydantic for validating Pushbullet OAuth responses 2023-10-23 08:52:31 +03:00
Pēteris Caune
06328dbecf
Fix mypy warning 2023-10-22 16:57:02 +03:00
Pēteris Caune
5911075282
Fix HTTP verb in hc.lib.curl.post 2023-10-22 12:38:44 +03:00
Pēteris Caune
7e7cbe9f75
Fix the Login form to not perform form validation in GET requests 2023-10-22 10:45:06 +03:00
Pēteris Caune
30a3a784c3
Move AuthenticatedHttpRequest to hc.accounts.http module
If AuthenticatedHttpRequest lives in the hc.lib.typealias
module then hc.lib.typealias imports User and Profile,
and so needs configured Django settings. Most of the stuff
in hc.lib is intended to work standalone, and not rely on Django.
2023-10-21 18:16:48 +03:00
Pēteris Caune
0971051418
Refactor Webhook transport 2023-10-21 18:04:27 +03:00
Pēteris Caune
2d447d5bc8
Enable mock autospeccing in more places 2023-10-21 11:55:30 +03:00
Pēteris Caune
470f70b164
Update curl.get and curl.post to closer match requests fn signatures
In requests, "params" is a positional argument for the get function:

    requests.get(url, params=None, **kwargs)

So I made params positional here too.

In requests, "data" is a positional argument for the post function:

    requests.post(url, data=None, json=None, **kwargs)

So I data positional here too.
2023-10-21 11:45:45 +03:00
Pēteris Caune
7c7774d18c
Replace **kwargs with explicit keyword arguments in hc.lib.curl
This makes the code more verbose, but at the same time
makes type hinting cleaner, and allows mock autospeccing
to catch more problems.
2023-10-21 11:21:42 +03:00
Pēteris Caune
4226ec4059
Remove unnecessary type conversion 2023-10-20 19:14:44 +03:00
Pēteris Caune
52ab8c4703
Improve type hints in hc.lib.curl and hc.api.transports
This results in changes in other places too:

* curl.post() does not accept `data` as positional arg,
  it must now be a keyword argument
* we need asserts and if clauses in a few places to make sure
  we are not passing `None` in the arguments to hc.lib.curl.request
2023-10-20 19:11:08 +03:00
Pēteris Caune
33a0f65144
Improve type hints in hc.api.decorators 2023-10-20 18:41:11 +03:00
Pēteris Caune
5ebc7696d7
Remove braintree dependency, should no longer be needed to run tests 2023-10-19 15:04:54 +03:00
Pēteris Caune
7900eb55b8
Remove a few more files that I missed 2023-10-19 14:58:39 +03:00
Pēteris Caune
a9c7dbc397
Remove billing.html which I meant to remove before but missed 2023-10-19 14:44:17 +03:00
Pēteris Caune
61e03fa88d
Fix base_docs.html -> docs_single.html in CONTRIBUTING.md 2023-10-19 14:41:01 +03:00
Pēteris Caune
a308997b3a
Remove most of the hc.payments stuff
Why remove:

* For self-hosters, payment-related features are unused and dead weight
* For SaaS (any would-be Healthchecks.io competitors), the existing
  payment handling logic is not very useful either, as it would need to
  be heavily modified to match their business model, pricing, chosen
  payment gateway
* For the hosted service (Healthchecks.io), the up-to-date billing code
  lives in a private fork of this repo. Maintenance is easier if this
  repo does not have an older, diverging version of the same
  functionality

A few payment-related bits are staying at least for time being:

* the "USE_PAYMENTS" setting
* the hc.payments.models.Subscription model
* tiny stubs for the "Pricing" and "Billing" pages

They are used in various places in the code and templates,
and I think ripping them out in one go would be too disruptive.
2023-10-19 14:17:44 +03:00
Pēteris Caune
fa2e456353
Fix mypy warning 2023-10-19 10:56:59 +03:00
Pēteris Caune
0aa21d5ac8
Update mypy.ini, remove a few "ignore_missing_imports"
fido2, apprise, braintree, urllib3 now have type hints,
so don't need "ignore_missing_imports" in mypy.ini any more, nice!
2023-10-18 16:56:58 +03:00
Pēteris Caune
fdfe03158d
Fix mypy warnings 2023-10-18 16:33:15 +03:00
Pēteris Caune
1aec03dfc6
Improve type hints in management commands 2023-10-18 16:15:01 +03:00
Pēteris Caune
e8be347d1a
Improve type hints in management commands 2023-10-18 13:47:02 +03:00
Pēteris Caune
ce622da6bd
Improve type hints and remove threading support which was unused
sendalerts had support for sending notifications
synchronously (with the --no-threads flag) and asynchronously using
threads (the default).

It turns out there was a bug in argument handling and sendalerts
was always using the synchronous mode regardless of the
presence/absence of the "--no-threads" flag. Since noone seems to
have noticed, I removed the unused async code.
2023-10-18 13:45:23 +03:00
Pēteris Caune
286b8a5a3b
Improve type hints in hc.front.templatetags.hc_extras 2023-10-18 10:08:29 +03:00
Pēteris Caune
c22c8a0a76
Improve markdown markup 2023-10-18 09:17:31 +03:00
Pēteris Caune
e4f233ba30
Sync templates/docs/self_hosted_docker.md and docker/README.md 2023-10-18 09:00:38 +03:00
Pēteris Caune
c123e4d3a9
Add note about SMTPD_PORT in docs
cc: #791
2023-10-18 08:39:22 +03:00
Pēteris Caune
3d5850d2da
Improve type hints, use pydantic for parsing Zulip configurations 2023-10-17 16:58:57 +03:00
Pēteris Caune
bb7632f467
Fix mypy warning 2023-10-17 16:15:37 +03:00
Pēteris Caune
42a137e128
Switch to pydantic for validating Telegram incoming webhooks 2023-10-17 16:10:35 +03:00
Pēteris Caune
86eb2e43e5
Make the "notification" parameter non-optional in Transport.notify()
Its signature was "notification: Notification | None = None".

But callers always specify it, the default value is never used.

So I changed the signature to "notification: Notification".
2023-10-17 14:56:54 +03:00
Pēteris Caune
02888e6db9
Fix sending test notification to a group integration 2023-10-17 11:37:12 +03:00
Pēteris Caune
89aa0a832b
Update uwsgi.ini to allow UWSGI_PROCESSES env var to override it 2023-10-17 10:24:20 +03:00
Pēteris Caune
1553a79205
Update CHANGELOG for v3.0 release 2023-10-16 09:24:18 +03:00
Pēteris Caune
d8ff82bbf1
Improve the event summary for group notifications 2023-10-12 15:05:40 +03:00
Pēteris Caune
37caa94ff5
Improve channel rendering in the group form
* Use the custom-styled checkbox
* Use PNG instead of icon font for channel kind logos
* Show channel's description the same way as in the channels list
  (using a reusable template, templates/front/channel_description.html)
2023-10-12 14:37:00 +03:00
Pēteris Caune
7b03e1f95b
Fix mypy warning 2023-10-12 11:55:47 +03:00
Pēteris Caune
2276fcff60
Rename my_checks -> checks; my_checks_desktop -> checks_table 2023-10-12 11:48:42 +03:00
Pēteris Caune
12114c647e
Fix channel sorting in the "my checks" page to show groups first 2023-10-12 11:43:20 +03:00
Pēteris Caune
51b9bf7ca1
Add testcases for "Add group" and "Edit group" forms 2023-10-07 10:59:57 +03:00
Pēteris Caune
e537a38dd8
Temporarily remove type hints for the view_on_site method 2023-10-06 20:02:00 +03:00
Pēteris Caune
93fcc79926
Improve tests, fix the handling of "no-op" errors 2023-10-06 19:51:08 +03:00
Florian Apolloner
ca10841503
Fix mypy warnings in hc.font.views (#904) 2023-10-06 17:39:20 +03:00
Pēteris Caune
fd54bf87bf
Update changelog 2023-10-06 17:08:43 +03:00
Pēteris Caune
4fc6ff5c26
Tweak CSS in the group form, make sure channel's name is preloaded 2023-10-06 17:06:41 +03:00
Florian Apolloner
7057f6d3a5
Notification groups. Fixes #894 (#901)
* MVP for notification groups.

* Addressed review comments.

* Push notification group to the front.

* Updated icons.

* Reduce code duplication.

* Show groups at the top.

* Add label to group forms.

* Add checkboxes for integration selection.

* CSS for checkboxes.

* Added tests for group notify.
2023-10-06 17:02:41 +03:00
Pēteris Caune
24bad5af52
Tighten type hints in hc.api.admin 2023-10-06 15:33:10 +03:00
Pēteris Caune
fa9db53631
Update admin to use format_html instead of string interpolation 2023-10-06 13:54:15 +03:00
Pēteris Caune
f68b2c01a6
Simplify the counting of updated objects in admin actions 2023-10-06 11:35:32 +03:00
Pēteris Caune
7b1258ceaa
Improve type hints in accounts admin, fix HTML escaping issue
In Project admin, when listing project members, for single-member
projects the owner's email address was being displayed
unescaped. This allowed unescaped amperstand and quote characters
to appear in HTML output.
2023-10-05 16:04:21 +03:00
Pēteris Caune
97830deb29
Improve type hints in test_notify_signal 2023-10-05 15:55:41 +03:00
Pēteris Caune
527f361a9a
Upgrade to Django 4.2.6 2023-10-05 12:45:25 +03:00
Pēteris Caune
fd87cad455
Remove unused imports 2023-10-05 12:44:40 +03:00
Pēteris Caune
d8149cbe84
Add django-stubs-ext to requirements 2023-10-04 16:08:08 +03:00
Pēteris Caune
8fb7d16864
Improve type hints, simplify pagination in Ping admin
Pagination, filtering, and result counting in Ping admin is tricky
as the pings table can contain millions of rows.

We previously used "select reltuples from pg_class" trick to
estimate the total number of rows. This only works on Postgres,
and does not handle WHERE filters.

This commit removes the search box, and changes the pagination
template to allow pagination between pages 1-9 and nothing more.
2023-10-04 16:00:41 +03:00
Pēteris Caune
e065871056
Update package versions 2023-10-04 13:52:32 +03:00
Pēteris Caune
339586e402
Add Python 3.12 to the testing matrix 2023-10-04 13:48:13 +03:00
Pēteris Caune
633e612b59
Increase bottom margin for modal windows to work around Safari issue
cc: #899
2023-10-04 10:22:53 +03:00
Pēteris Caune
5d99c544ee
Increase the precision in hc.lib.date.format_approx_duration
Format durations in one of the following forms:

* "{x} days {y} h"
* "{y} h {z} min"
* "{z} min {w} sec"
2023-10-02 12:50:59 +03:00
Pēteris Caune
d40ce1eaac
Rename DowntimeSummary -> DowntimeRecorder 2023-09-28 09:02:13 +03:00
Pēteris Caune
95b92a9b3b
Fix type warning 2023-09-27 17:47:25 +03:00
Pēteris Caune
2a0ae809a7
Add DowntimeRecord.no_data field 2023-09-27 17:43:18 +03:00
Pēteris Caune
58d7c8cc55
Simplify DowntimeSummary 2023-09-27 17:16:16 +03:00
Pēteris Caune
f7af738c76
Add monthly uptime percentage display in Check Details page
Fixes: #773
2023-09-27 13:44:35 +03:00
Pēteris Caune
db5d8adeb5
Switch from namedtuple to dataclass for mutability 2023-09-27 11:03:58 +03:00
Pēteris Caune
46dbaff2c3
Update Check.downtimes() to return records in descending datetime order
This way we can avoid one sort operation in Check.downtimes()
and one reverse operation in the "details_downtimes.html" template.
2023-09-27 10:42:05 +03:00
Pēteris Caune
f0085933c3
Update Check.downtimes() to return a list of namedtuples
This way it should be easier to add extra fields like uptime
to the returned data structure.
2023-09-27 10:18:52 +03:00
Pēteris Caune
e48d361331
Fix time interval formatting in check details - downtime summary
In hc.lib.date.format_approx_duration, we were calling
timedeltaobj.total_seconds() and basing all calculations off that.
This method returns float, so the final result was "2.0 hours" or
"3.0 days" and similar. We now convert it to int, to get "2 hours",
"3 days" etc.
2023-09-26 10:30:34 +03:00
Pēteris Caune
507fd840d8
Exclude "body" from the Ping admin form
"body" is an obsolete field and kept around for the sake
of very old pings. For all new pings body data goes either
into the "body_raw" binary field (for small bodies)
or in the object storage (for not so small bodies).
So it is not useful to show the body field in the Ping admin.
2023-09-22 09:36:27 +03:00
Git'Fellow
8dbbd5b9d6
Switch to apt-get (#893)
More suitable and less error prone to use in scripts
2023-09-20 10:02:34 +03:00
Pēteris Caune
aa541e760b
Add Channel.opsgenie property
It replaces:

* Channel.opsgenie_key
* Channel.opsgenie_region

Also, add data migration to normalize Opsgenie channel values
to always be JSON maps.
2023-09-11 16:44:38 +03:00
Pēteris Caune
dcf9f327f6
Add Channel.email property
It replaces:

* Channel.email_value
* Channel.email_notify_up
* Channel.email_notify_down
2023-09-11 11:45:25 +03:00
Pēteris Caune
ccc8faa953
Fix admin test
The test is checking if a Pushbullet channel shows up in the
channels list. It was looking for the string "Pushbullet".
The problem is this string shows up unconditionally on
the right side filters panel. The fix is to look for
"ic-pushbullet" (the pushbullet icon), which should only
appear inside the list view.
2023-09-11 11:41:52 +03:00
Pēteris Caune
cf4498eca2
Add Channel.phone property
It has typed fields: value, notify_up, notify_down.
It replaces:

* Channel.phone_number
* Channel.sms_notify_up
* Channel.sms_notify_down
* Channel.whatsapp_notify_up
* Channel.whatsapp_notify_down
* Channel.signal_notify_up
* Channel.signal_notify_down
2023-09-09 11:09:52 +03:00
Pēteris Caune
1881fa1da0
Fix tests 2023-09-08 16:54:13 +03:00
Pēteris Caune
ee40cd8dab
Fix type warnings 2023-09-08 16:47:27 +03:00
Pēteris Caune
f1d1ccd281
Add Channel.shell and Channel.pd properties 2023-09-08 16:33:12 +03:00
Pēteris Caune
e17f555fb1
Replace Channel.telegram_* properties with a Channel.telegram property
The Channel.telegram property has typed fields:
id, thread_id, type, name.

Usage sites change from `channel.telegram_id` to `channel.telegram.id`
2023-09-08 16:02:26 +03:00
Pēteris Caune
5746bbb015
Improve type hints in hc.api.models 2023-09-08 15:58:39 +03:00
Pēteris Caune
a6ab647a46
Improve type hints in hc.accounts.models 2023-09-08 15:54:47 +03:00
Pēteris Caune
92bf9773dc
Improve type hints in hc.lib 2023-09-08 13:05:19 +03:00
Pēteris Caune
9173475194
Improve type hints in hc.api.views 2023-09-07 14:38:55 +03:00
Pēteris Caune
a2fa62c7be
Improve function and variable names 2023-09-07 14:05:47 +03:00
Pēteris Caune
18aac7f46c
Update Spec model to handle int -> timedelta conversion 2023-09-07 13:19:21 +03:00
Pēteris Caune
53200a0559
Switch to using None as the sentinel object for absent fields
It means we need to have an extra checks for null values
in Spec.check_nulls() but it makes mypy more happy.
2023-09-07 12:56:35 +03:00
Pēteris Caune
370a75373b
Combine the "validate_json" and "authorize" decorators 2023-09-07 11:18:14 +03:00
Pēteris Caune
ee30448c0b
Update hc.api.views to use Pydantic for incoming data validation 2023-09-07 10:31:54 +03:00
Pēteris Caune
05d2d0065e
Remove usage of backports.zoneinfo 2023-09-06 12:54:27 +03:00
Pēteris Caune
77e1ea731c
Improve type hints in hc.accounts.views 2023-09-06 12:51:27 +03:00
Pēteris Caune
c9bf3132fe
Replace usage of typing.List, typing.Optional etc. with modern syntax 2023-09-06 11:27:43 +03:00
Pēteris Caune
d79069f669
Fix type warnings, improve type hints in hc.front.views 2023-09-06 11:18:47 +03:00
Pēteris Caune
e9ac841d01
Move reusable type aliases to hc.lib.typealias 2023-09-06 10:52:47 +03:00
Pēteris Caune
3c19d51836
Drop Python 3.8 and Python 3.9 support 2023-09-06 10:31:13 +03:00
Pēteris Caune
65b3acf964
Improve type hints in hc.accounts.views 2023-09-06 10:02:50 +03:00
Pēteris Caune
0c83ca4fd8
Improve type hints in hc.accounts.forms 2023-09-05 18:31:35 +03:00
Pēteris Caune
9153c1a552
Improve type hints 2023-09-05 13:31:59 +03:00
Pēteris Caune
1ccd96a045
Fix type warnings 2023-09-05 11:53:57 +03:00
Pēteris Caune
688191e6d5
Upgrade Django to 4.2.5 2023-09-05 10:45:26 +03:00
Pēteris Caune
8472bd5d1e
Update Channel.webhook_spec to return an object instead of a dict
This simplifies type annotations, as object's fields
can be type-annotated easily, and JSON->object parsing can be handled
by Pydantic.
2023-09-03 16:20:22 +03:00
Pēteris Caune
957cd59fc7
Fix type warnings 2023-09-03 15:46:31 +03:00
Pēteris Caune
8fa70dc59c
Fix type warnings 2023-09-03 09:29:44 +03:00
Pēteris Caune
ef3837e7e7
Clean up and improve code comments 2023-09-03 09:29:31 +03:00
Pēteris Caune
2901f03146
Fix type warnings 2023-09-03 09:04:38 +03:00
Pēteris Caune
4a819a8018
Convert Signal.send() to a classmethod
This fixes a couple mypy warnings, and also fixes a code smell:
we were doing `Signal(None).send(...)` in a few places,
and can now do `Signal.send(...)` instead.
2023-09-02 18:48:25 +03:00
Pēteris Caune
ec607afbde
Improve test coverage for the Signal transport class 2023-09-02 18:24:14 +03:00
Pēteris Caune
23eadc5e64
Fix type warnings 2023-09-02 17:19:31 +03:00
Pēteris Caune
aecdbfb017
Remove dead code in Channel.webhook_spec
The "method_down" and "method_up" fields are always set
in webhook configurations, no point checking them.
2023-09-02 16:54:16 +03:00
Pēteris Caune
9932855745
Fix type annotations 2023-09-02 16:52:46 +03:00
Pēteris Caune
1c0dc52099
Remove debug 2023-09-02 12:58:25 +03:00
Pēteris Caune
6b11e60d9c
Update Signal class to parse signal-cli JSON-RPC messages using pydantic 2023-09-02 12:47:42 +03:00
Pēteris Caune
fcdf8dd60e
Fix type annotations for Py 3.8 2023-09-01 10:07:20 +03:00
Pēteris Caune
3430a8dc92
Combine Telegram.ErrorModel and Telegram.MigrationModel classes 2023-09-01 09:49:37 +03:00
Pēteris Caune
c82b720d04
Update Opsgenie and Zulip transports to use pydantic 2023-09-01 09:39:40 +03:00
Pēteris Caune
cc2b8fa1c2
Use pydantic to validate JSON from Telegram API (experimental) 2023-09-01 09:16:33 +03:00
Pēteris Caune
477f690057
Improve type hints 2023-08-31 14:17:08 +03:00
Pēteris Caune
4affd70822
Fix JSON type aliases for Py 3.8 again 2023-08-31 12:25:18 +03:00
Pēteris Caune
e54d96690f
Remove usage of typing.TypeAlias (not available in Py 3.9) 2023-08-31 12:21:17 +03:00
Pēteris Caune
3311712610
Improve type hints, add reusable JSON type aliases in hc.lib.typealias 2023-08-31 12:17:15 +03:00
Pēteris Caune
6bd25cc52c
Improve type hints in hc.api.transports 2023-08-30 16:08:43 +03:00
Pēteris Caune
e23db5a8fa
Fix "JSONValue" type alias for Py 3.8-3.9, take two 2023-08-30 10:16:22 +03:00
Pēteris Caune
1e8d02385d
Fix "JSONValue" type alias for Py 3.8-3.9 2023-08-30 10:10:38 +03:00
Pēteris Caune
e022c17bed
Improve type hints 2023-08-30 09:57:34 +03:00
Pēteris Caune
ab3dad4ecc
Fix type annotations in tests to use _MonkeyPatchedWSGIResponse 2023-08-29 22:35:51 +03:00
Pēteris Caune
2b73ddde17
Improve type hints 2023-08-29 19:10:27 +03:00
Pēteris Caune
aaa172cd28
Improve type hints 2023-08-29 17:52:20 +03:00
Pēteris Caune
afd3d62c08
Improve type hints 2023-08-29 16:51:22 +03:00
Pēteris Caune
4147bd951d
Make Channel.disabled non-nullable 2023-08-29 16:29:29 +03:00
Pēteris Caune
47ec070aee
Make Profile.user non-nullable
Each Profile must have a corresponding User object, there should
be no profiles with null "user_id" fields.
2023-08-29 16:19:08 +03:00
Pēteris Caune
9483556a1f
Improve type hints and fix bugs revealed by type checker 2023-08-29 16:07:54 +03:00
Pēteris Caune
75290f8f45
Upgrade psycopg2 to 2.9.7 2023-08-24 15:24:19 +03:00
Pēteris Caune
26725b9f4e
Add an "Account closed." confirmation message after closing an account 2023-08-24 09:07:41 +03:00
Pēteris Caune
42edc0c308
Improve wording in the email report footer 2023-08-23 15:41:14 +03:00
Pēteris Caune
bc192df0b3
Improve ntfy notifications (include tags, period, last ping type etc.) 2023-08-23 15:32:06 +03:00
Pēteris Caune
bbb06ff358
Add support for ntfy access tokens
Fixes: #879
2023-08-23 14:52:50 +03:00
Pēteris Caune
d57ea3309a
Split report and reminder (nag) templates 2023-08-23 11:20:50 +03:00
Pēteris Caune
fbc6ac58ba
Clean up the report/reminder generation code 2023-08-22 15:31:45 +03:00
Pēteris Caune
70a5429ae3
Remove unused bits 2023-08-22 15:21:14 +03:00
Pēteris Caune
646f79c54e
Update email reminders to only show checks in the "down" state
Fixes: #881
2023-08-22 15:04:14 +03:00
Pēteris Caune
ea89237c16
Fix hc.accounts.views.check_token to handle non-UUID usernames
Fixes: #882
2023-08-22 13:45:54 +03:00
Pēteris Caune
455dc66ce2
Update senddeletionscheduled to also notify over configured channels 2023-08-21 15:38:11 +03:00
Pēteris Caune
a7395115db
Fix "createsuperuser" to reject already registered email addresses
Fixes: #880
2023-08-21 13:57:40 +03:00
Pēteris Caune
582372d27f
Rename senddeletionnotices -> sendinactivitynotices 2023-08-21 12:56:27 +03:00
Pēteris Caune
f8d8ca56b2
Fix slug validation regex 2023-08-17 11:51:57 +03:00
Pēteris Caune
abe0b28926
Update senddeletionnotices to handle inactive team members 2023-08-14 21:04:41 +03:00
Pēteris Caune
8f4936e2e8
Improve grammar 2023-08-14 10:57:48 +03:00
Pēteris Caune
af682bae0c
Improve grammar 2023-08-11 11:18:49 +03:00
Pēteris Caune
897cf0088b
Add bold and monospace text formatting in Signal notifications 2023-08-04 12:38:34 +03:00
Tony F
200a2d1dd7
Replace healtchecks with healthchecks (#872) 2023-08-04 09:56:55 +03:00
Pēteris Caune
d164e12de3
Add "Time Zone" field in Trello notifications
Also, fix the escaping of "*" symbols in Trello notifications.
2023-08-03 14:58:57 +03:00
Pēteris Caune
0456b3d5c4
Add "Time Zone" field in Telegram notifications 2023-08-03 14:36:42 +03:00
Pēteris Caune
dd523c53e5
Add "Time Zone" field in Signal notifications 2023-08-03 14:32:54 +03:00
Pēteris Caune
163656e6e4
Add "Time Zone" field in PagerDuty notifications 2023-08-03 14:16:51 +03:00
Pēteris Caune
f6805834d6
Add "Time Zone" field in Rocket.Chat notifications 2023-08-03 11:40:45 +03:00
Pēteris Caune
97dc6e73c5
Add "Time Zone" field in email notifications 2023-08-03 11:29:41 +03:00
Pēteris Caune
ba1d45e600
Add "Time Zone" field in Slack, Mattermost, Discord, MS Teams alerts
Fixes: #863
2023-08-03 11:29:14 +03:00
Pēteris Caune
228330ad06
Add release badge in README 2023-08-03 10:57:40 +03:00
Pēteris Caune
074b92f415
Move coverage to a separate GHA workflow 2023-08-03 10:45:07 +03:00
Pēteris Caune
4fa88e7998
Add dummy local_settings.py to hopefully make mypy happy 2023-08-03 10:33:40 +03:00
Pēteris Caune
216fe3e7af
Add mypy.ini 2023-08-03 10:28:05 +03:00
Pēteris Caune
cd0cd2932f
Add GHA workflow for running Mypy on every push 2023-08-03 10:23:25 +03:00
Pēteris Caune
af5a435804
Upgrade to Django 4.2.4 2023-08-02 14:29:36 +03:00
Pēteris Caune
f8c869596a
Fix "senddeletionnotices" to recognize "Supporter" subscriptions 2023-08-02 12:40:29 +03:00
Pēteris Caune
61a5c7a1a0
Add code comment so we don't trip on this again 2023-08-01 12:44:53 +03:00
Pēteris Caune
2942eefdf6
Tweak ping query to work around postgres index selection issue
Sorting by "n" or by "id" does not change query results.
But sorting by "id" can cause postgres to pick the
api_ping.id index (slow if api_ping table is big) instead of
api_ping.owner_id index (fast because any given check will have
a limited number of pings in the api_ping table).
2023-08-01 12:32:05 +03:00
Pēteris Caune
5190cafbb2
Update package versions 2023-07-31 14:40:21 +03:00
Pēteris Caune
d5573fbc63
Enable sorting for date columns in Profile admin 2023-07-24 11:33:59 +03:00
Pēteris Caune
c6afd94baf
Fix "senddeletionscheduled" to avoid duplicate recipients 2023-07-17 11:35:49 +03:00
Pēteris Caune
fc91838774
Update senddeletionscheduled to put multiple rcpts in To: field 2023-07-17 09:48:27 +03:00
Pēteris Caune
763e7caf2d
Update Telegram integration to treat blocked bot as permanent error 2023-07-14 10:14:42 +03:00
Pēteris Caune
05742f42f9
Update the senddeletionscheduled command to notify team members too 2023-07-14 09:51:36 +03:00
Pēteris Caune
c4851f3b49
Fix lint issues 2023-07-12 13:44:35 +03:00
Pēteris Caune
774471e4e8
Improve type annotations in hc.api.views._update 2023-07-12 12:23:45 +03:00
Pēteris Caune
4a0ddb989e
Reformat bad markdown->HTML conversion 2023-07-12 12:22:51 +03:00
Pēteris Caune
ef88aa3a85
Silence a ruff warning about local_settings import 2023-07-12 11:37:10 +03:00
Pēteris Caune
792cc59bef
Fix lint issues 2023-07-12 10:29:55 +03:00
Pēteris Caune
58cc623377
Format migration files with black 2023-07-12 10:11:07 +03:00
Pēteris Caune
f6a04d2256
Fix lint issues 2023-07-12 10:07:14 +03:00
Pēteris Caune
a3965fcae5
Add last notify date in channel admin 2023-07-12 08:51:54 +03:00
Pēteris Caune
5b322e5c5e
Optimize queries in admin 2023-07-11 16:45:02 +03:00
Pēteris Caune
88016c4317
Add over_limit_date in Profile admin's list view 2023-07-11 12:36:41 +03:00
Pēteris Caune
e26830ea99
Tweak Profile admin's list view 2023-07-11 12:28:58 +03:00
Pēteris Caune
32b607533a
Make QoL improvements in admin
* Add "view on site" links in Check, Channel, and Project admins
* Expose over_limit_date in Profile admin
* Display last notify duration ("Time" column) in Channel admin
* Add last notify duration filter for Channel admin
2023-07-11 12:14:45 +03:00
Pēteris Caune
4adba44381
Add convenience functions in Profile model
* is_past_over_limit_grace(): Returns True if this profile is
over limits for 31 or more days.
* schedule_for_deletion(): Sets the deletion_scheduled_date
field to 31 days in the future.
2023-07-11 10:14:10 +03:00
Pēteris Caune
fb58a50301
Exclude "apiv2" from docs search 2023-07-08 10:54:55 +03:00
Pēteris Caune
7ecbe8fc4e
Make log output more compact 2023-07-08 10:39:47 +03:00
Pēteris Caune
89c26b46a4
Refactor sendalerts and Flip.send_alerts() for cleaner logs 2023-07-08 10:28:40 +03:00
Pēteris Caune
fc41af50f4
Fix sorting of NULLs when fetching a Flip in sendalerts 2023-07-07 18:21:26 +03:00
Pēteris Caune
68bcc5389f
Fix sendalerts to allow "handle_going_down()" to run more often 2023-07-07 17:58:55 +03:00
Pēteris Caune
368e76016d
Add Channel.last_notify_duration, use in sendalerts for prioritization 2023-07-07 16:40:23 +03:00
Pēteris Caune
39682d900f
Update screenshots in docs to show the slug field 2023-07-07 10:22:35 +03:00
Pēteris Caune
9c00938516
Update email bounce handler to log diagnostic codes 2023-07-06 15:21:02 +03:00
Pēteris Caune
c69c1f5ec4
Add management command for sending "scheduled for deletion" warnings 2023-07-04 12:50:50 +03:00
Pēteris Caune
9304536131
Upgrade to Django 4.2.3 2023-07-04 09:10:10 +03:00
Viktor Szépe
1bfd1d22b0
Add link to CI badge in README (#856) 2023-07-02 15:32:20 +03:00
Pēteris Caune
fc1db22ec7
Fix JS error in the login page when REGISTRATION_OPEN=False 2023-07-02 15:29:41 +03:00
Viktor Szépe
dedb17feb9
Fix JS name and remove references to .map files (#854) 2023-07-02 15:25:04 +03:00
Viktor Szépe
573b76a082
Fix typos (#855) 2023-07-02 15:14:13 +03:00
Pēteris Caune
a2fdb5dc52
Update CHANGELOG for v2.10 release 2023-07-02 10:07:02 +03:00
Pēteris Caune
e208b7f543
Upgrade cronsim to 2.5 2023-07-01 10:54:21 +03:00
Pēteris Caune
dba356c5d4
For cron checks, change default display timezone to check's timezone
In the "Details" and "Log" pages Healthchecks displays a list
of events (incoming pings and sent alerts). At the top of the
events list is a two- or three-way selector for selecting
the timezone for formatting event dates and times. The selector
options are "UTC", check's configured timezone, and "Browser's
time zone". The "Browser's time zone" used to be default, initial
selection for all checks.

With this change, for checks that use cron schedule, the default
selected timezone will be the check's configured timezone.
The "Browser's time zone" option is of course still there and the
user can switch to it to see dates and times in their local time.

Rationale: I semi-regularly get support requests about unexpected
or missing alerts, where the problem boil downs to a timezone
mismatch between the client and the Healthchecks server. Sometimes
the confusion seems to be caused by the user seeing ping arrival
times in  their local time zone, comparing them to their cron
expression, and not realizing their server may be using a different
timezone. By switching the default display timezone to the check's
configured timezone, I hope users will be more likely to notice
discrepancies between ping arrival times, the cron schedule,
and their local clock.

For checks using simple schedules (timeout and grace), we still
default to browser's timezone for display.
2023-06-29 12:12:54 +03:00
Pēteris Caune
b2c43a57db
Update Telegram onboarding instructions 2023-06-28 15:47:52 +03:00
Pēteris Caune
011b97f75a
Fix signup tests to pass regardless of REGISTRATION_OPEN value 2023-06-28 15:26:39 +03:00
Pēteris Caune
68c42db58c
Add support for Telegram topics
Fixes: #852
2023-06-28 15:22:59 +03:00
Pēteris Caune
931876a737
Upgrade selectize.js to v0.15.2, fix CSS issues
Fixed CSS issues:

* In the tag input element, when entering a new tag,
  the "Add ..." item was missing padding. Padding added.
* In the timezone selects the caret was hidden, so it was
  hard to discover that timezone options can be filtered by typing.
  Caret in timezone selects is now visible and blinking.
2023-06-28 11:42:00 +03:00
Pēteris Caune
161c274586
Update requirement versions 2023-06-28 10:55:17 +03:00
Pēteris Caune
68cbcf9039
Fix syntax highlighting in docs 2023-06-27 15:25:06 +03:00
Pēteris Caune
a53e79cc49
Add a note about pre-built images in docs/docker 2023-06-27 15:20:53 +03:00
Pēteris Caune
651aec4ac7
Add API support for filtering checks by slug
Fixes: #844
2023-06-27 12:41:31 +03:00
Pēteris Caune
58d317adc7
Make check auto-provisioning opt-in
cc: #849, #626
2023-06-27 09:45:38 +03:00
Pēteris Caune
e342f057df
Hopefully fix DB connection timeouts in manage.py smtpd
The smtpd management command runs a SMTP listener for receiving
ping signals as emails. If emails arrive infrequently, the
database connections can time out. A brute-force workaround for
this was to call `connections.close_all()` before making new
DB queries.

Code moved around while migrating to aiosmtpd, and looks like
the workaround did not work any more – #847. This commit
replaces `connections.close_all()` with `connection.close()` and
moves it to the `_process_message()` function, which *hopefully* fixes 
the problem.
2023-06-26 16:24:56 +03:00
Pēteris Caune
730d448c76
Fix pinging by slug to return 201 when a check is auto-created
cc: #849, #626
2023-06-26 13:06:20 +03:00
Pēteris Caune
b062caa2eb
Add support for the $EXITSTATUS placeholder in webhook payloads
Fixes: #826
2023-06-22 17:53:03 +03:00
Pēteris Caune
db07a4d796
Update docs with notes about auto-provisioning
cc: #626
2023-06-22 14:48:06 +03:00
Pēteris Caune
34530e0e91
Add Profile.over_limit_date DB field 2023-06-16 16:28:03 +03:00
Pēteris Caune
1ffb9ed18f
Highlight the number of checks in the over-limit notice 2023-06-16 15:16:58 +03:00
Pēteris Caune
2677c052c8
Fix plan name lookup 2023-06-16 15:04:34 +03:00
Pēteris Caune
928f82e220
Add "Your account is currently over its check limit" notice 2023-06-16 15:00:33 +03:00
Pēteris Caune
228516b4a2
Add samarpan-rai/healthchecks_wrapper to the 3rd-party resouces page 2023-06-16 13:12:07 +03:00
Pēteris Caune
3202eebbcf
Fix ping_by_slug to assign all channels to a newly created check 2023-06-15 15:53:33 +03:00
Pēteris Caune
fc09d4c084
Implement check auto-provisioning when pinging by slug
cc: #626
2023-06-15 15:36:16 +03:00
Pēteris Caune
45cccdecb0
Reduce code repetition 2023-06-15 14:36:32 +03:00
Pēteris Caune
d4060279f1
Fix arm/v7 build 2023-06-15 13:56:04 +03:00
Pēteris Caune
20e8408f24
Update v2->v3 2023-06-15 10:29:25 +03:00
Pēteris Caune
9172e8de62
Add API v3 docs 2023-06-15 10:22:49 +03:00
Pēteris Caune
8d41d284e5
Add libxml2 dependency now required by uwsgi 2023-06-15 09:14:44 +03:00
Pēteris Caune
6dbd665c67
Update Dockerfile to use Debian Bookworm as the base 2023-06-15 09:09:17 +03:00
Pēteris Caune
ebcb060ece
Update slug validation rules to disallow uppercase 2023-06-15 09:05:26 +03:00
Pēteris Caune
4ccee09f73
Add /api/v3/ (adds ability to set slug when creating or updating checks) 2023-06-14 16:52:45 +03:00
Pēteris Caune
d7d9702ee0
Add support for regex validation in hc.lib.jsonschema
Also, switch to f-strings and add tests for validation
positive cases
2023-06-14 16:30:27 +03:00
Pēteris Caune
002bc9b083
Decouple check's name from slug, allow users to set hand-picked slugs 2023-06-14 15:06:37 +03:00
Pēteris Caune
132873826a
Remove healthchecks/hchk from the resources page as it's unmaintained 2023-06-13 14:25:04 +03:00
Pēteris Caune
c324787809
Update Dockerfile to prepare a writable location for data volume 2023-06-13 13:52:59 +03:00
Pēteris Caune
db9fd529e2
Make hc.lib.emails raise exceptions when EMAIL_ settings are not set 2023-06-13 13:27:58 +03:00
Pēteris Caune
475cd574ee
Improve logging in hc.lib.s3._remove_objects 2023-06-12 11:25:58 +03:00
Pēteris Caune
91a7e3d29e
Fix base_project to not include project modal for unauthenticated users 2023-06-12 09:33:37 +03:00
Pēteris Caune
fcf21c68a7
Reduce code repetition in Channel.transport() 2023-06-08 15:46:06 +03:00
Pēteris Caune
dec5d51e87
Configure logging to log exceptions to console even when DEBUG=False
Fixes: #835
2023-06-08 10:51:49 +03:00
Pēteris Caune
518dee4ae2
Update the login_webauthn view to return HTTP 404 when RP_ID is not set 2023-06-08 10:49:57 +03:00
Pēteris Caune
5cdc104a16
Update serve_doc and its tests to use pathlib 2023-06-07 17:04:03 +03:00
Pēteris Caune
79c85acb91
Re-populate search.db 2023-06-07 16:47:58 +03:00
Pēteris Caune
c184f5ca42
Fix populate_searchdb to use ".html-fragment" file suffix
(and update it to use pathlib)
2023-06-07 16:43:12 +03:00
Pēteris Caune
4a72519c71
Update settings.BASE_DIR usage sites to use pathlib 2023-06-07 16:36:19 +03:00
Pēteris Caune
b9d016b799
Update settings.py to use pathlib 2023-06-07 16:16:18 +03:00
Pēteris Caune
98c0917b69
Fix dl/dt/dd CSS for the "Pinging API" page on mobile screens 2023-06-07 15:45:08 +03:00
Pēteris Caune
ee73091b72
Update CHANGELOG for v2.9.2 release 2023-06-05 23:27:23 +03:00
Pēteris Caune
dac16453a1
Upgrade to Django 4.2.2 2023-06-05 23:26:47 +03:00
Max
19d05498ae
Fix smtpd service without stdin (#840)
Fixes: #839
2023-06-05 23:23:37 +03:00
Pēteris Caune
3a07a94098
Update CHANGELOG for v2.9.1 release 2023-06-05 16:24:08 +03:00
Pēteris Caune
292133526f
Update Dockerfile to install rust via rustup
(The rust version in bullseye repositories is now
too old to build cryptography.)
2023-06-05 15:39:13 +03:00
Pēteris Caune
1f0ca10185
Update CHANGELOG for v2.9 release 2023-06-05 11:33:31 +03:00
Pēteris Caune
4bd305f4fe
Fix docs to display the ping body limit defined in settings 2023-06-04 19:32:11 +03:00
Pēteris Caune
c743b7a76e
Move "fix_asterisks" from template tags to transports.py 2023-06-04 15:01:18 +03:00
Pēteris Caune
628d2ca637
Add escaping for asterisks in MS Teams messages 2023-06-04 14:56:28 +03:00
Pēteris Caune
4afb1cfe23
Move MS Teams message preparation from template to Python
Also, remove Markdown escaping for the description field.
This is for consistency with Slack, Mattermost, Discord, and
Rocket.Chat -- none of them attempt to escape Markdown syntax.
2023-06-04 14:39:22 +03:00
Pēteris Caune
bc0b76cfaf
Remove duplicate code 2023-06-04 11:25:00 +03:00
Pēteris Caune
564d5cda31
Move Discord message preparation from template to Python 2023-06-04 11:16:48 +03:00
Pēteris Caune
5f710b4949
Move Mattermost message preparation from template to Python 2023-06-04 11:03:56 +03:00
Pēteris Caune
3e8a89bb7a
Move Slack message preparation from template to Python 2023-06-04 09:44:15 +03:00
Pēteris Caune
1d003da29d
Fix the display of ignored pings with non-zero exitstatus 2023-06-03 19:37:45 +03:00
Pēteris Caune
dd1569457e
Remove MD escaping because it is inconsistent between web and mobile 2023-06-03 19:02:45 +03:00
Pēteris Caune
91e5a8430b
Add Markdown escaping in Rocket.Chat messages 2023-06-03 18:56:55 +03:00
Pēteris Caune
81a3e3352b
Fix singular/plural in the "Last Ping Body" field 2023-06-03 18:32:02 +03:00
Pēteris Caune
0ecbe1bd1d
Move Rocket.Chat message preparation from template to Python
cc: #463
2023-06-03 18:15:29 +03:00
Pēteris Caune
da4be28003
Fix the Rocket.Chat template to use concrete color codes 2023-06-03 10:11:20 +03:00
Pēteris Caune
0f8c459987
Improve Rocket.Chat notification template
* Move the text field outside of the attachment object
* Rename "username" to "alias"
* Rename "icon_url" to "avatar"
* Remove unused, Slack-specific fields
* Add "Last Ping Body" field with a link to view full body

cc: #463
2023-06-03 09:58:34 +03:00
Pēteris Caune
eb7dfe5c63
Add Rocket.Chat integration
cc: #463
2023-05-30 12:30:29 +03:00
Pēteris Caune
e02a87ef48
Fix Message-ID generation when From address contains angle brackets 2023-05-26 16:51:52 +03:00
Pēteris Caune
dc44f67b83
Update hc.lib.emails to use DEFAULT_FROM_EMAIL for generating Message-ID 2023-05-26 14:32:09 +03:00
Pēteris Caune
554f6cba68
Replace bootstrap-select with the native <select> element 2023-05-26 13:44:24 +03:00
Pēteris Caune
4314e21b61
Switch from bootstrap-select to selectize in pushover and ntfy forms 2023-05-26 13:06:34 +03:00
Pēteris Caune
1e6f4cfd9f
Fix bottom padding in webhook and shell forms 2023-05-26 11:43:40 +03:00
Pēteris Caune
b065bb02c1
Fix outline on focused select.selectpicker 2023-05-26 11:29:02 +03:00
Pēteris Caune
cd0404d257
In the "Add/Edit Webhook" form, replace selectpicker with regular select 2023-05-26 11:25:23 +03:00
Pēteris Caune
7144f780bf
Fix select dropdown in Chrome and dark mode
The form controls with the .form-control CSS class have
background: transparent (as defined in Bootstrap's variables.less).

On Chrome, in dark mode, this causes <select> dropdowns to have
light text on light background. As a workaround, we set the
select.form-control background in base.css to the same color as panel
background.
2023-05-26 10:22:51 +03:00
Pēteris Caune
0fa9222264
Switch to case-insensitive bounce id signatures 2023-05-24 18:13:36 +03:00
Pēteris Caune
efc5bc0168
Improve the email delivery error message 2023-05-24 14:56:48 +03:00
Pēteris Caune
bb361dec7b
Add a view for handling email bounce notifications 2023-05-24 13:00:02 +03:00
Pēteris Caune
8926fd5ac6
Add experimental support for setting custom MAIL FROM address
If settings.EMAIL_MAIL_FROM_TMPL is set, it will be used
as the MAIL FROM (envelope sender) address.

The EMAIL_MAIL_FROM_TMPL value should contain a "%s" placeholder.

This is intended for routing email bounce notifications to a specific
MX server. For example, if the site runs on example.com
but we want to receive bounce notifications at the mail server running
on bounces.example.com, we can set

    EMAIL_MAIL_FROM_TMPL = "%s@bounces.example.com"

Experimental, may change!
2023-05-23 15:38:39 +03:00
Pēteris Caune
e23b154aa6
Edit note about data migration when enabling/disabling S3 storage 2023-05-17 09:38:30 +03:00
Pēteris Caune
1a7d177baf
Add note about data migration when enabling/disabling S3 storage 2023-05-17 09:27:56 +03:00
Pēteris Caune
c17a483be4
Add USE_GZIP_MIDDLEWARE env var which enables GZipMiddleware 2023-05-11 17:31:14 +03:00
Pēteris Caune
d92412a0df
Fix uwsgi.ini to check for .gz files only in the CACHE dir 2023-05-11 17:03:18 +03:00
dependabot[bot]
69c7f1299e
Bump django from 4.2 to 4.2.1 (#829)
Bumps [django](https://github.com/django/django) from 4.2 to 4.2.1.
- [Commits](https://github.com/django/django/compare/4.2...4.2.1)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-11 16:56:56 +03:00
Pēteris Caune
6a5d151026
Update uwsgi.ini to serve compressed static files 2023-05-11 16:50:26 +03:00
Pēteris Caune
202e39d23b
Add COMPRESSOR_STORAGE=GzipCompressorFileStorage
(to generate .gz versions of static files)
2023-05-11 16:03:29 +03:00
Pēteris Caune
4b05fc6a8c
Update the smtpd management command to use the aiosmtpd library
smtpd from the standard library is deprecated and will
be removed in Python 3.12. aiosmtpd is the recommended
replacement.
2023-05-09 17:23:18 +03:00
Pēteris Caune
c992aa0d8a
Clean up hc.front.templatetags.hc_extras.naturalize_int_match 2023-05-09 16:29:15 +03:00
Pēteris Caune
45c9f837ad
Remove debug 2023-05-09 10:36:25 +03:00
Pēteris Caune
56636e5e08
Fix sorting 2023-05-09 10:34:23 +03:00
Pēteris Caune
d0216a861c
Fix the checks list to preserve filters when changing sort order
Fixes: #828
2023-05-09 10:14:30 +03:00
Pēteris Caune
796b9540e7
Remove USE_L10N from settings (deprecated) 2023-05-04 11:13:00 +03:00
Pēteris Caune
0c45424a92
Change timezone.now import in sendalerts and sendreports 2023-05-04 11:05:52 +03:00
Pēteris Caune
0a724a44c7
Change timezone.now import in hc.lib.date and fix tests 2023-05-04 11:02:09 +03:00
Pēteris Caune
1fd343a820
Remove usages of django.utils.timezone.utc which is deprecated 2023-05-04 10:56:19 +03:00
Pēteris Caune
db8749e38f
Fix tests to use Mock.assert_called_once() and Mock.assert_not_called() 2023-05-04 10:27:56 +03:00
Pēteris Caune
1c80b32adf
Use simpler base classes in hc.lib.tests 2023-05-04 10:20:36 +03:00
Pēteris Caune
5113b96879
Fix mock.call_args usage in tests 2023-05-03 15:07:49 +03:00
Pēteris Caune
db1b75e966
Add support for specifying MessagingServiceSid in Twilio API requests 2023-05-03 13:06:08 +03:00
Pēteris Caune
4e3034978f
Fix another instance of pinging/deleting race condition 2023-05-02 14:11:56 +03:00
Pēteris Caune
2eeb0b5471
Improve code comments 2023-05-02 13:25:16 +03:00
Pēteris Caune
6375b0aac4
Fix a race condition when pinging and deleting checks at the same time 2023-05-02 13:22:16 +03:00
Pēteris Caune
5de8d6fce7
Move body truncation code into get_ping_body() 2023-04-28 18:05:02 +03:00
Nikolai T. Jensen
ca18da5d62
Add last ping body in MS Teams notifications (#817)
* Include body in msteams template if exists.

Support it like it does in https://github.com/healthchecks/healthchecks/blob/master/templates/integrations/slack_message.json
The markdown codeblock works well in teams also.

* Move body to a separate section, fix transport class, add tests

---------

Co-authored-by: Pēteris Caune <cuu508@gmail.com>
2023-04-28 17:51:39 +03:00
Pēteris Caune
c3b80b06cf
Add Profile.deletion_scheduled_deleted field and UI banner when it's set 2023-04-28 15:03:04 +03:00
Pēteris Caune
5f2b795a88
Add "Activate" and "Deactivate" actions in User admin 2023-04-28 14:26:19 +03:00
Pēteris Caune
cb6bdf0dd3
Update Signal notification template to include more data 2023-04-28 13:11:33 +03:00
Pēteris Caune
c6de5c406c
Upgrade to cronsim 2.4.1 2023-04-27 18:53:27 +03:00
Pēteris Caune
c0a5bedb0c
Tweak wording and CSS in Cron Expression Cheatsheet 2023-04-27 11:37:57 +03:00
Pēteris Caune
9ddae08437
Upgrade to cronsim 2.4 2023-04-26 18:18:31 +03:00
Pēteris Caune
9ead449d90
Add error logging in hc.lib.s3.get_object 2023-04-14 15:14:23 +03:00
Pēteris Caune
b1e9bdde76
Fix test_ping_details to avoid real S3 calls during tests 2023-04-14 15:12:12 +03:00
Pēteris Caune
dcd174f761
Add statsd metric collection in hc.lib.s3.get_object() 2023-04-14 13:23:16 +03:00
Pēteris Caune
dc58910bb5
Switch from CssAbsoluteFilter to CssRelativeFilter
cc: #822

This should fix icon font loading when serving Healthchecks
from a subdirectory.
2023-04-12 11:37:14 +03:00
Pēteris Caune
05722cbf9e
Update CHANGELOG for v2.8.1 release 2023-04-11 20:51:31 +03:00
Pēteris Caune
b9996e63c8
Fix django-compressor warning with github_actions.html
HTML files in /templates/docs/ are not Django templates,
they contain HTML content to be used verbatim in
hc.front.views.serve_doc view.

Some of these files contain "{{ ... }}" syntax. When
we run "./manage.py compress", django-compressor trips
up on this syntax because it treats them as Django templates.

The fix is to change file extension for these files
from .html to something else (I picked .html-fragment)
so django-compressor would ignore them.
2023-04-11 20:34:31 +03:00
Pēteris Caune
b1ce88d56a
Update CHANGELOG for v2.8 release 2023-04-11 08:26:15 +03:00
Pēteris Caune
9ea92c3f5e
Document sendreports 2023-04-07 13:40:33 +03:00
Pēteris Caune
6fa0d49467
Add note about uwsgi running background tasks automatically 2023-04-07 13:35:27 +03:00
Pēteris Caune
a55549894c
Add info about Dockerfile and Docker images in README 2023-04-07 13:24:35 +03:00
Pēteris Caune
7a06f01fca
Update links to Django docs 2023-04-07 12:50:09 +03:00
Pēteris Caune
80b6aa89ea
Add cron expression tester and samples in the cron cheatsheet page 2023-04-04 13:54:24 +03:00
Pēteris Caune
e722404842
Fix tests 2023-04-04 11:57:47 +03:00
Pēteris Caune
a5324ac13c
Make warnings about no backup second factor more assertive 2023-04-04 11:53:34 +03:00
Pēteris Caune
eab52ed73c
Add email fallback for Signal notifications that hit rate limit 2023-04-04 10:21:35 +03:00
Pēteris Caune
04e6d9aab5
Add DEFAULT_FROM_EMAIL in the "Sending Emails" section of README 2023-04-04 09:28:24 +03:00
Pēteris Caune
c4ebd4e6ba
Render code samples with newer Pygments 2023-04-04 09:23:06 +03:00
Pēteris Caune
beaad419f6
Add the "Email Delivery Delays" section 2023-04-04 09:20:38 +03:00
Pēteris Caune
4ebe928d39
Upgrade to Django 4.2 and psycopg2 2.9.6 2023-04-03 15:12:06 +03:00
Pēteris Caune
82cac0aed8
Tweak uwsgi post buffer size and options
* set post-buffering (buffer for POST data) to 16192
* add auto-procname for friendlier process names
* add strict, uwsgi won't start if uwsgi.ini contains invalid options
2023-04-03 12:10:51 +03:00
Pēteris Caune
f0267ce936
Add Arduino usage example 2023-04-01 12:13:39 +03:00
Pēteris Caune
e21ada67f1
Remove L10N markup from base.html, and associated translations 2023-03-29 19:19:03 +03:00
Pēteris Caune
64f13d1219
Update Trello onboarding form to allow tokens up to 256 chars long
cc: #806
2023-03-29 15:19:45 +03:00
Pēteris Caune
bea84b744a
Update Trello onboarding form to allow longer Trello auth tokens
Trello token length change announcement:
https://community.developer.atlassian.com/t/trello-tokens-are-getting-longer/62964

Fixes: #806
2023-03-29 14:21:06 +03:00
Pēteris Caune
f6aeda978d
Fix hc.lib.s3.get_object to handle more urllib3 exceptions 2023-03-29 14:00:10 +03:00
Pēteris Caune
c6ce8918c5
Remove dollar signs from shell snippets in docker/README
cc: #814
2023-03-29 12:08:29 +03:00
Pēteris Caune
56e003b613
Fix indentation 2023-03-29 11:47:31 +03:00
Pēteris Caune
34631e65cd
Remove dollar signs from shell snippets in README
Fixes: #814
2023-03-29 11:46:00 +03:00
Pēteris Caune
5fcbcb637c
Fix .env.example to have SMTPD_PORT undefined by default 2023-03-29 09:42:20 +03:00
Pēteris Caune
50d15d53e5
Add a "Remove TOTP" action in Profile admin 2023-03-28 17:36:57 +03:00
Pēteris Caune
f8026a73b6
Add a test for fdfab66a81 2023-03-10 16:33:57 +02:00
Pēteris Caune
fdfab66a81
Fix notification query in the Log page
The bug: the Log page would sometimes show a number of "zombie"
notifications at the very end: notifications that should not be
shown to the user, but have not yet been garbage-collected.

The fix: when preparing the created__gte filter value for the
notification query, make sure the filter value is not lower than the
timestamp of the oldest visible ping.
2023-03-10 16:25:06 +02:00
Pēteris Caune
312c53c2b3
Fix check name wrapping in the "Assign Checks to Integration" dialog 2023-03-10 14:55:53 +02:00
Pēteris Caune
a779ffd365
Tweak the positioning of the project switcher 2023-03-10 13:27:11 +02:00
Pēteris Caune
4d552efd67
Improve styling of the project switcher 2023-03-10 11:50:41 +02:00
Pēteris Caune
bad4b9adbf
Add a "Switch Project" menu in top navigation 2023-03-10 10:36:09 +02:00
Pēteris Caune
9e9bdfd353
Update CHANGELOG 2023-03-09 13:34:21 +02:00
Pēteris Caune
9656c51034
Update tests to check the active tab in the Ping Details dialog 2023-03-09 13:33:38 +02:00
seidnerj
87841b6038
Update the ping details dialog to show the "HTML" tab by default (if available) (#801)
* typo correction in README.md

* when opening the "ping details" dialog, by default set the active tab to "HTML" (if html content exists), otherwise set the active tab to "Text".
2023-03-09 13:17:16 +02:00
Pēteris Caune
0e2d2154c8
Make API docs fit better on mobile screens 2023-03-08 09:29:43 +02:00
Pēteris Caune
bb3f139335
Update the Dockerfile to use Python 3.11 2023-03-07 16:33:34 +02:00
Pēteris Caune
f9f32adc11
Fix wording 2023-03-06 19:33:42 +02:00
Pēteris Caune
acc64e4e46
Add GitHub Actions examples 2023-03-06 19:31:12 +02:00
Pēteris Caune
bce9d4ddef
Update changelog for v2.7 release 2023-03-06 13:00:38 +02:00
Pēteris Caune
920caacc0d
Update SITE_LOGO_URL docs
cc: #797
2023-02-25 11:07:09 +02:00
Pēteris Caune
312b63e592
Update package versions 2023-02-25 10:38:52 +02:00
Pēteris Caune
a8170a6e6c
Fix typo 2023-02-25 10:36:48 +02:00
Pēteris Caune
d269b54ca1
Fix tests 2023-02-21 11:11:43 +02:00
Pēteris Caune
16d94f642d
Add tiny drop shadow to buttons
For primary buttons, the drop shadow is green-tinted.
For red buttons, the shadow is red-tinted.
2023-02-21 11:10:51 +02:00
Pēteris Caune
97703f10cf
Tweak :active and :focus button styles
Make the darkening effect less pronounced. Unfortunately this
requires changing bootstrap's .less files.
2023-02-21 11:08:40 +02:00
Pēteris Caune
fe54cbe049
Make radio and checkbox borders brighter in dark mode 2023-02-21 11:04:43 +02:00
Pēteris Caune
e2e289da2a
Add form double submit protection when registering a WebAuthn key 2023-02-20 11:05:55 +02:00
Pēteris Caune
04c9398da3
Fix the "Test" button in the Integrations screen for read-only users
(I broke it by accident in 963f1758de)
2023-02-20 10:21:41 +02:00
Pēteris Caune
d84a97acef
Add @sensitive_post_parameters() to views that handle passwords 2023-02-20 10:09:16 +02:00
Pēteris Caune
c2f828df83
Add custom ExceptionReporterFilter which filters out TWILIO_AUTH 2023-02-20 09:43:03 +02:00
Pēteris Caune
a316c36086
Fix more typos, spelling and grammar mistakes in docs 2023-02-19 14:37:19 +02:00
Krasimir Nedelchev
2d42e5af11
Fix typo in docs (#795) 2023-02-19 13:47:50 +02:00
Pēteris Caune
b62faf5bd0
Clean up promise chaining in signup.js 2023-02-15 09:41:15 +02:00
Pēteris Caune
423dac4b19
Add a protection for non-bool settings.SESSION_COOKIE_SECURE value 2023-02-15 09:20:00 +02:00
Pēteris Caune
3d728325fe
Fix the SameSite and Secure attributes on the "auto-login" cookie
The "auto-login" cookie is a part of a work-around for
some email clients automatically clicking links in emails:

- when sending an one-time sign-in link, server also sends the
  "auto-login" cookie to the client
- when end user clicks on the sign-in link, the server checks
  if client's request contains the "auto-login" cookie
- if the "auto-login" cookie is present, log the user in
- if the "auto-login" cookie is absent, serve a HTTP POST form
  with a submit button. The user must click the button to log in.

This commit fixes attributes on the "auto-login" cookie:

- it sets SameSite=Lax
- it sets Secure=true if SESSION_COOKIE_SECURE=True
2023-02-15 09:17:09 +02:00
Pēteris Caune
c8750ad05b
Fix the signup form to work with httpOnly CSRF cookies 2023-02-14 14:20:27 +02:00
Pēteris Caune
8531ef89b5
Bump Django version to 4.1.7 2023-02-14 14:02:06 +02:00
Pēteris Caune
e46cf3725b
Add CSRF protection in the signup view 2023-02-14 09:15:46 +02:00
Pēteris Caune
f27e7c82a2
Optimize SQL query in hc.front.views.status
Filter checks by project.id instead of project.code,
this avoids a JOIN in the query.
2023-02-10 12:06:56 +02:00
Pēteris Caune
0d0087d898
Update Telegram notification template to include more data 2023-02-08 15:28:39 +02:00
Pēteris Caune
b1d47abd97
Fix tests when TELEGRAM_BOT_NAME has a custom value 2023-02-08 14:22:55 +02:00
Pēteris Caune
311f7064dc
Fix a race condition in Check.ping method
The code in Check.ping() updates a Check object, then
creates a Ping object. There's a possible race condition
where the "sendalerts" command sees# the updated Check object
before the Ping object is created. This is especially likely
when offloading ping bodies to S3, because Ping gets created
*after* the upload completes, which can take some time.

To avoid this, put both operations inside a transaction,
but keep the S3 upload *outside* the transaction--uploads
can hang, and we want to avoid long transactions.
2023-02-08 13:12:05 +02:00
Fabrizio Ferrai
9939e45c5a
Add body to Telegram notifications (#783)
Add body to Telegram notifications

---------

Co-authored-by: Pēteris Caune <cuu508@gmail.com>
2023-02-08 12:52:36 +02:00
Pēteris Caune
08849d6f22
Update Docker image's uwsgi.ini to use SMTPD_PORT env var
Fixes: #791
2023-02-07 13:38:05 +02:00
Pēteris Caune
42a47463f9
Add a note about private ips in the http_proxy section 2023-02-03 10:26:51 +02:00
Pēteris Caune
19383d0414
Improve the error message about rejected private IPs 2023-02-03 10:08:06 +02:00
Pēteris Caune
e79fc0bdc2
Fix Mattermost and Matrix icon display in dark mode 2023-02-01 13:59:47 +02:00
Pēteris Caune
ac354179ac
Upgrade to Django==4.1.6 2023-02-01 13:32:13 +02:00
Pēteris Caune
ba9ebc5a96
Update CHANGELOG 2023-02-01 13:25:15 +02:00
boopzz
55361d5ae2
Amended Mattermost class to include the BODY in the message (#785)
Add last ping body in Mattermost notifications

---------

Co-authored-by: Pēteris Caune <cuu508@gmail.com>
2023-02-01 13:22:54 +02:00
Pēteris Caune
8fa932470f
Improve the example in docs/attaching_logs.md 2023-02-01 12:11:24 +02:00
Pēteris Caune
73c46a7ac1
Add another example in docs/attaching_logs.md 2023-02-01 12:09:25 +02:00
Pēteris Caune
e995d299b8
Improve hc.lib.s3 tests 2023-02-01 10:25:17 +02:00
Pēteris Caune
3992c0927b
Add handling for ProtocolError exceptions in hc.lib.s3.get_object 2023-02-01 09:31:15 +02:00
Pēteris Caune
091310f34b
Add HiDPI Telegram setup illustrations 2023-01-30 15:37:42 +02:00
Pēteris Caune
114faf1d42
Improve type hints 2023-01-30 13:07:03 +02:00
Pēteris Caune
88325b4d90
Fix mypy warnings 2023-01-30 13:02:00 +02:00
Pēteris Caune
f4bd1d69f2
Fix URL validation to allow hostnames with no TLD
Fixes: #782
2023-01-30 11:19:51 +02:00
Pēteris Caune
09593c80d9
Fix a crash in the "createsuperuser" management command
Fixes: #779
2023-01-26 09:20:35 +02:00
Pēteris Caune
f406ce8d4d
Improve title 2023-01-25 14:08:31 +02:00
Pēteris Caune
7099305df5
Document exported metrics in Prometheus docs 2023-01-25 13:53:53 +02:00
Pēteris Caune
6c40ff8684
Update package versions 2023-01-24 09:14:20 +02:00
Pēteris Caune
1660e8076c
Remove unused bit 2023-01-24 09:06:02 +02:00
Pēteris Caune
d67144ed3a
Update CHANGELOG for release 2023-01-23 15:01:53 +02:00
Pēteris Caune
737405679f
Fix EmailLoginForm initialization 2023-01-23 14:53:49 +02:00
Pēteris Caune
2cfb37f097
Add rate limiting by client IP in the signup and login views 2023-01-23 14:35:45 +02:00
Pēteris Caune
359edbd270
Fix login and signup views to make email enumeration harder 2023-01-23 13:05:49 +02:00
Pēteris Caune
e8c226220a
Upgrade to django==4.1.5 2023-01-23 13:05:46 +02:00
Pēteris Caune
58c6bd0a86
Create SECURITY.md
Fixes: #777
2023-01-21 11:20:19 +02:00
Pēteris Caune
a9b084ec9a
Add "Start Keyword" filtering for inbound emails
Fixes: #716
2023-01-16 13:19:35 +02:00
Pēteris Caune
f849c5e1a1
Fix wording in the invite email when inviting read-only users 2023-01-12 10:14:18 +02:00
Pēteris Caune
4716168da2
Fix check transfer between same account's projects when at check limit 2023-01-12 09:46:02 +02:00
Pēteris Caune
a161498e85
Tighten Signal number verification rate limiting 2023-01-11 15:33:04 +02:00
Pēteris Caune
188b261000
Improve the "Send test message!" button 2023-01-11 14:50:01 +02:00
Pēteris Caune
8d06a3e896
Add a "verify number" step in the Signal onboarding flow 2023-01-10 12:54:25 +02:00
Pēteris Caune
39baf36340
Update the bundled dashboard to use api v2 2022-12-22 16:40:16 +02:00
Pēteris Caune
afbce84731
Reduce SQL queries in "status_single", "details", "log" views 2022-12-22 14:35:24 +02:00
Pēteris Caune
2bf0d0dbc5
Fix special character encoding in project invite emails 2022-12-22 12:05:37 +02:00
Pēteris Caune
18c17fb4b5
Fix project sort order to be case-insensitive everywhere in the UI
Fixes: #768
2022-12-22 11:39:20 +02:00
Pēteris Caune
d19156801f
Fix special character encoding in Signal notifications
Fixes: #767
2022-12-21 15:58:52 +02:00
Pēteris Caune
a49bc4ef3a
Fix the Signal integration to handle unexpected RPC messages better
Fixes: #763
cc: #758
2022-12-21 12:18:03 +02:00
Pēteris Caune
70a7024cf2
Remove support for obsolete signal-cli versions
Due to Signal server-side changes, signal-cli versions
before 0.11.2 do not work any more. Hence there is no point
supporting them.
2022-12-21 10:43:25 +02:00
Pēteris Caune
bc19f87be5
Improve signal-cli instructions in README 2022-12-21 10:30:39 +02:00
Pēteris Caune
ad481cf932
Optimize pagertree setup illustrations 2022-12-21 09:53:42 +02:00
Pēteris Caune
5ba96b1767
Combine steps 1 and 2 2022-12-21 09:24:54 +02:00
Austin Miller
95d5f32d7f Upgrade the PagerTree documentation with new screenshots and accurate directions for their new UI. 2022-12-20 13:20:47 -07:00
Pēteris Caune
ae53aaaa3a
Update settings.py to read the ADMINS setting from an env variable 2022-12-20 16:23:33 +02:00
Pēteris Caune
f5f05b0589
Exclude the "Management API v1" page from docs search 2022-12-20 10:38:10 +02:00
Pēteris Caune
506ffa2278
Update CHANGELOG 2022-12-20 10:30:25 +02:00
Pēteris Caune
2ed197e7ef
Improve type hints, mark arguments as keyword-only 2022-12-20 09:50:47 +02:00
Pēteris Caune
4863dda6c3
Add /api/v2/ which reports check's status slightly differently
cc: #633
2022-12-19 22:31:38 +02:00
Pēteris Caune
1d7f4a50ad
Add signal-cli TCP socket test and update docs 2022-12-15 19:29:00 +02:00
Pēteris Caune
73a5cb0d57
Add support for communicating with signal-cli over TCP
cc: #732
2022-12-15 17:46:37 +02:00
Pēteris Caune
347765557e
Remove debug line 2022-12-15 15:06:50 +02:00
Pēteris Caune
36e8843481
Drop clipboard.js dependency, use navigator.clipboard directly 2022-12-15 15:05:40 +02:00
Pēteris Caune
43a900c802
Improve layout in "My Checks" for checks with long ping URLs
Fixes: #745
2022-12-15 11:40:57 +02:00
Pēteris Caune
30e88beda3
Update CHANGELOG for release 2022-12-14 15:53:40 +02:00
Pēteris Caune
2f59995601
Tweak wording in the "Register a backup key!" message 2022-12-14 14:42:15 +02:00
Pēteris Caune
ee5015a141
Update package versions 2022-12-06 12:03:49 +02:00
Pēteris Caune
5e826ec15a
Fix pruneflips 2022-12-02 12:19:52 +02:00
Pēteris Caune
c3369b22d6
Add more tests 2022-12-02 09:54:07 +02:00
Pēteris Caune
ef5df02238
Add max width limit for the timezone select 2022-12-01 16:18:33 +02:00
Pēteris Caune
15cbb39bd3
Change "Settings - Email Reports" page to allow manual tz selection 2022-12-01 16:12:32 +02:00
Pēteris Caune
86262ef620
Make datetime.datetime imports consistent everywhere 2022-12-01 15:36:35 +02:00
Pēteris Caune
260f6e36a7
Fix templates to use user's timezone when displaying dates 2022-12-01 15:21:40 +02:00
Pēteris Caune
818ccad56f
Fix week, month boundary calculation to use user's timezone 2022-12-01 13:46:21 +02:00
Pēteris Caune
8cc6498b1b
Improve REMOTE_USER_HEADER docs
cc: #743
2022-12-01 09:51:15 +02:00
Pēteris Caune
ff7b963d15
Move "send report", "deactivate" admin actions to the Profile admin 2022-12-01 09:35:13 +02:00
Pēteris Caune
a65aa171f4
Replace var=[...];if var: [...] usages with the walrus operator 2022-12-01 09:16:19 +02:00
Pēteris Caune
b5006f2741
Fix downtime calculation for recently created checks 2022-11-30 16:19:02 +02:00
Pēteris Caune
91c7321f38
Update CHANGELOG 2022-11-30 14:05:17 +02:00
Pēteris Caune
12f6f59e0a
Refactor and improve type hints 2022-11-30 14:02:03 +02:00
Pēteris Caune
ac2f2fefc2
Improve sendreports tests 2022-11-30 12:17:16 +02:00
Pēteris Caune
34bd608acd
Update Profile.send_report to prepare weekly totals for weekly reports
Fixes: #736
2022-11-30 11:51:00 +02:00
Pēteris Caune
796c6b9272
Add Check.downtimes_by_boundary, add hc.lib.date.week_boundaries
cc: #736
2022-11-30 10:49:42 +02:00
Pēteris Caune
f4dc008c55
Refactor Check.downtimes to handle any boundaries, not just monthly
cc: #736
2022-11-30 10:29:42 +02:00
Pēteris Caune
dbb360e524
Improve TOC and section titles in Management API docs 2022-11-28 14:58:57 +02:00
Pēteris Caune
144d50417c
Update CHANGELOG 2022-11-28 14:50:51 +02:00
Pēteris Caune
c3e6fca6a7
Fix "get body" views to preserve body bytes, avoid string conversions 2022-11-28 14:48:55 +02:00
Pēteris Caune
ea2f2d9ec0
Add more tests 2022-11-28 14:04:57 +02:00
Pēteris Caune
de7097e1eb
Improve docs 2022-11-28 13:40:40 +02:00
Pēteris Caune
9e9490d815
Move test to separate file, remove trailing slash from URL 2022-11-28 13:28:08 +02:00
Martin Lablans
a55a2692dc
Allow to retrieve a ping's body (#737)
Add /api/v1/checks/<uuid>/pings/<n>/body endpoint for retrieving ping body (#737)
2022-11-28 13:21:26 +02:00
Pēteris Caune
8930bedd4a
Refactor the other "edit channel" views, add type hints 2022-11-25 12:48:04 +02:00
Pēteris Caune
346ebc184c
Refactor hc.front.views.ntfy_form, add type hints 2022-11-25 11:23:58 +02:00
Pēteris Caune
c275bf09f1
Improve type hints 2022-11-25 10:07:57 +02:00
Pēteris Caune
f1fe0b9643
Fix alignment in the "add ntfy" form 2022-11-24 18:32:30 +02:00
Pēteris Caune
646aa1cb48
Add ".txt" suffix to the filename when downloading ping body
Fixes: #738
2022-11-24 18:22:34 +02:00
Pēteris Caune
c0a0c97388
Tweak wording 2022-11-24 14:59:12 +02:00
Pēteris Caune
413b97c48f
Fix ntfy edit form initialization 2022-11-24 14:48:19 +02:00
Pēteris Caune
d2810d62d8
Simplify AddPushoverForm 2022-11-24 13:43:26 +02:00
Pēteris Caune
390bb781ca
Refactor duplicated code 2022-11-24 13:26:09 +02:00
Pēteris Caune
3dcc7d60a2
Add ntfy integration
Fixes: #728
2022-11-24 12:09:53 +02:00
Pēteris Caune
9977789cac
Add a special case for the last ping body containing backticks 2022-11-22 20:22:37 +02:00
Pēteris Caune
e962429e79
Update CHANGELOG 2022-11-22 17:52:22 +02:00
Sebastian Schneider
6481ed0d19
Add last ping body body to Slack notifications (#735)
Co-authored-by: Sebastian Schneider <sebastian.schneider@boxine.de>
Co-authored-by: Pēteris Caune <cuu508@gmail.com>
2022-11-22 17:50:11 +02:00
Pēteris Caune
75188e218e
Fix duration calculation in the "Get Pings" API call 2022-11-11 13:04:08 +02:00
Pēteris Caune
14017392fa
Optimize PNG 2022-11-11 10:45:23 +02:00
Pēteris Caune
5a464f186f
Add "Specifying Run IDs" section in docs 2022-11-10 18:34:35 +02:00
Pēteris Caune
a26ca60046
Fix run ID display in dark mode 2022-11-10 12:28:03 +02:00
Pēteris Caune
85f7a1c348
Update Ping API and Management API docs for run IDs 2022-11-10 11:49:38 +02:00
Pēteris Caune
7458770b41
Improve alerting logic when run IDs are used
* Add Check.last_start_rid field
* Fill Check.last_start_rid on every start event
* Clear Check.last_start on every "fail" event
* Clear Check.last_start on success event if either case is true:
 - the event's rid matches Check.last_start_rid
 - the event does not specify rid

In human terms, the alerting logic will be: we track the
execution time of the most recent "start" event only. It would
take a major redesign to track the execution time of all
concurrent "start" events and send alerts when *any* of them
overshoots the time budget. So, whenever we see a "start" event,
the timer resets.

Example:

* 00:00 client sends start signal with rid=A, timer starts
* 00:10 client sends start signal with rid=B, timer resets
* 00:20 client sends success signal with rid=A, timer
  does not reset because rid A does not match the rid seen in
  the most recent start signal (it was B)
* 00:30 the grace time runs out, the check's status shows
  as started + failed

At this point the check can be reset to a healthy state in 3
different ways:

* send a success signal with rid=B
* send a failure signal with any rid value or without it
* send a success signal without a rid value
2022-11-09 19:01:22 +02:00
Pēteris Caune
e58a9ee71e
Add protection for n queries problem in _get_events
If every fetched ping is a success event, and has an unique
run ID, then we cannot determine the duration just from the
fetched data, and must fall back to Ping.duration(). This
would generate a SQL query per displayed ping.

The solution is to count how many times we would need to use
the fallback, and if it goes above some threshold (currently,
10 times), then disable duration display altogether.
2022-11-08 12:41:46 +02:00
Pēteris Caune
8f249b8c59
Refactor test case some more 2022-11-08 12:12:01 +02:00
Pēteris Caune
7591fe35dc
Refactor test case 2022-11-08 11:57:15 +02:00
seidnerj
b6027fa126
Added support for a "run id" parameter (#722)
Add support for specifying a run ID via a "rid" query parameter

cc: #461
2022-11-08 11:34:26 +02:00
Pēteris Caune
0ec5117a72
Add autofocus attribute to the TOTP input field
Fixes: #726
2022-11-07 10:33:21 +02:00
Pēteris Caune
024622c146
Fix tests 2022-11-02 14:48:22 +02:00
Pēteris Caune
ccfcf26e65
Update Mattermost setup instructions 2022-11-02 14:45:44 +02:00
seidnerj
6c18c85ffe
updated README.md to include MacOS specific instructions for pycurl when setting up a development environment (#721) 2022-11-02 09:23:04 +02:00
Pēteris Caune
f1c6d87168
Tweak the dark mode text-success and text-danger colors 2022-11-01 15:02:47 +02:00
Pēteris Caune
d3406aef25
Fix the most recent ping lookup in the "Ping Details" dialog 2022-11-01 13:42:09 +02:00
Pēteris Caune
0682ddfa93
Improve layout in the Ping Details dialog 2022-11-01 12:39:14 +02:00
Pēteris Caune
e29235c5a5
Improve tests 2022-11-01 12:15:54 +02:00
Pēteris Caune
d100cb7ead
Fix duration calculation bug in _get_events
The bug: _get_events sometimes does not have enough data to
calculate all durations correctly (some needed "start" events
may be outside the fetched data). The fix is to fall back to
Ping.duration() in these cases.
2022-11-01 12:04:14 +02:00
Pēteris Caune
30fcc6a936
Fix duration calculation bug 2022-11-01 10:29:43 +02:00
Pēteris Caune
a87c3bf04b
Rewrite to avoid self.owner access to save a SQL query 2022-11-01 09:56:34 +02:00
Pēteris Caune
26b6e20fda
Fix hc.front.views.ping_details to set duration on all ping instances
The code in hc.front.views.ping_details calculates durations
and sets them on the Ping.duration field, overriding the cached
property. But, it only does this for pings where the duration can
be calculated. If there's a streak of "success" pings with no
"start" pings between, the loop will not set the Ping.duration
field for most of them. So, when rendering the template, the
property code will run and cause an additional SQL query for each
ping. The fix is, in hc.front.views.ping_details, to set the
Ping.duration field for each and every ping.
2022-11-01 09:22:29 +02:00
seidnerj
c82c1a3a4a
Added duration to ping details (#720)
* Added duration to ping details. This is useful on a device with a small screen, since the duration cannot be seen in the main view so now one can see it in the ping's details.
* Changed terms across the board from "delta" to "duration"
* timedelta is now consistently imported as "td" across the entire project (even in Django generated migration files)
2022-11-01 09:18:34 +02:00
Pēteris Caune
c26dbc96f5
Refactor render_docs to support multiple docs directories 2022-10-28 12:05:01 +03:00
Pēteris Caune
85be1ce481
Fix the link to the Signal CAPTCHA form 2022-10-28 08:28:32 +03:00
Pēteris Caune
49fbd98615
Update .po and .mo files, remove obsolete strings 2022-10-27 14:21:49 +03:00
Pēteris Caune
20a5e3ffca
Add tests and usability tweaks for the signal_captcha view 2022-10-27 13:00:12 +03:00
Pēteris Caune
8d75f1adc3
Add a form for submitting Signal CAPTCHA solutions 2022-10-27 11:57:52 +03:00
Pēteris Caune
b4f11aa899
Fix horizontal scrollbar in events view on small screens 2022-10-26 14:43:14 +03:00
Pēteris Caune
cc4a03df6d
Add Python 3.11 to the testing matrix 2022-10-26 12:19:45 +03:00
Pēteris Caune
a189872938
Improve type hints 2022-10-20 13:03:10 +03:00
Pēteris Caune
895e8e856a
Improve type hints 2022-10-20 12:24:50 +03:00
Pēteris Caune
e4a956679e
Move port scrubbing to hc.api.views.ping, add test case
cc: #714
2022-10-20 11:59:19 +03:00
Cam
5aa4c64a6f Add IPv4 check to guard clause 2022-10-20 11:41:03 +03:00
Cam
ca1b9a6b12 Remove port portion of remote_addr 2022-10-20 11:41:03 +03:00
Pēteris Caune
c7b519dc86
Add emborg in "3rd party resources" 2022-10-20 09:23:06 +03:00
Pēteris Caune
ff5d4a926b
Update the CodeQL workflow to install build dependencies 2022-10-19 09:24:12 +03:00
Pēteris Caune
21ded7ce83
Update action versions in the "run django tests" workflow 2022-10-19 09:21:37 +03:00
Pēteris Caune
a944c05f68
Upgrade to fido2 1.1.0 and simplify hc.lib.webauthn 2022-10-19 09:16:01 +03:00
Pēteris Caune
85dcadd053
Update CHANGELOG for v2.4.1 2022-10-18 17:45:52 +03:00
Pēteris Caune
949dfa5446
Try another cryptography/rust/qemu/armv7 workaround 2022-10-18 16:56:16 +03:00
Pēteris Caune
09f7bec8f7
Try a different cryptography/rust/qemu/armv7 workaround 2022-10-18 16:35:55 +03:00
Pēteris Caune
2481ea2794
Update action versions in the publish_docker_image workflow 2022-10-18 16:06:54 +03:00
Pēteris Caune
414d5a9424
Fix MySQL 8 support in the Docker image (#717) 2022-10-18 14:06:05 +03:00
Pēteris Caune
03f46630eb
Format with black 2022-10-17 17:02:11 +03:00
Pēteris Caune
161430fb10
Sort imports and add "from __future__ import annotations" 2022-10-17 16:52:15 +03:00
Pēteris Caune
5969e940a5
Switch to using "from __future__ import annotations" 2022-10-17 16:29:48 +03:00
Pēteris Caune
66b318d505
Add libcurl4-openssl-dev, libssl-dev in README, install instructions 2022-10-17 15:25:57 +03:00
Pēteris Caune
4351287c62
Fix the signup form to prevent multiple concurrent form submits 2022-10-14 12:34:05 +03:00
Pēteris Caune
2de1b457e5
Add descriptions for the screenshots in README 2022-10-13 18:03:30 +03:00
Pēteris Caune
0cbe7798ac
Add max-fd=10000 in uwsgi.ini to work around uWSGI issue on Fedora
Fixes: #713
2022-10-13 16:26:19 +03:00
Pēteris Caune
7ac8458b07
Optimize the "Get a List of Existing Checks" API call 2022-10-12 17:02:47 +03:00
Pēteris Caune
11997e75c8
Fix test_tokenbucket failure 2022-10-09 12:22:46 +03:00
Pēteris Caune
3e3aaec8f5
Update Zulip setup instructions 2022-10-09 12:12:39 +03:00
Pēteris Caune
963f1758de
Improve type hints 2022-10-09 11:51:13 +03:00
Pēteris Caune
4d69ff937e
Add support for custom topics in Zulip notifications
Fixes: #583
2022-10-09 11:23:14 +03:00
Pēteris Caune
4a5123d67b
Remove mypy from CI for now, more work needed 2022-10-07 12:24:43 +03:00
Pēteris Caune
a7038a9ec4
Fix remaining mypy errors and add mypy to CI 2022-10-07 12:21:45 +03:00
Pēteris Caune
291323a531
Implement the "Clear Events" function 2022-10-07 11:19:08 +03:00
Pēteris Caune
0b0a2d993c
Add "btn-remove" CSS class for remove/delete/close buttons 2022-10-07 10:07:22 +03:00
Pēteris Caune
948912bfe3
Upgrade to Django 4.1.2 2022-10-04 16:02:40 +03:00
Pēteris Caune
35ad358614
Update LargeTablePaginator to use cached_property decorator 2022-09-29 09:14:35 +03:00
Pēteris Caune
08d90697c8
Improve type hints 2022-09-29 08:58:11 +03:00
Pēteris Caune
51d7216e30
Upgrade to cronsim 2.3 2022-09-29 08:35:41 +03:00
Pēteris Caune
599b4d2e52
Add more type hints 2022-09-27 18:54:22 +03:00
Pēteris Caune
e5e369257c
Update the "Supported Placeholders" dialog 2022-09-26 10:35:04 +03:00
Berk D. Demir
b4871bd432 Webhooks support for $BODY placeholder
Adds support for sending ping body contents with webhooks.
Only allowed in the 'body' template of the webhook configuration.
2022-09-26 10:06:24 +03:00
Pēteris Caune
ec25b319ab
Upgrade to cronsim 2.2 2022-09-22 16:16:39 +03:00
Pēteris Caune
94b49b5f34
Fix the earliest ping lookup query to order by n instead of id
Ordering by id causes Postgres to pick a bad
index (api_ping_pkey). Ordering by n gives equivalent results,
but Postgres picks a better index (owner_id)

Fixes: #705
2022-09-19 19:34:14 +03:00
Pēteris Caune
7d625a6844
Tweak variable names in slider initialization JS 2022-09-12 11:49:32 +03:00
Pēteris Caune
5b4073374e
Clean up hc.front.views.log 2022-09-09 16:43:21 +03:00
Pēteris Caune
4312154552
Fix contrast for slider labels and pips 2022-09-09 15:05:09 +03:00
Pēteris Caune
37bbe5a9c7
Add date filters in the Log page 2022-09-09 14:16:17 +03:00
Pēteris Caune
1b7bdea9e9
Optimize HTML output of the events log some more 2022-09-05 19:45:11 +03:00
Pēteris Caune
6a69d1afba
Fix CI 2022-09-05 16:24:06 +03:00
Pēteris Caune
29da76b953
Fix clicks on log events 2022-09-05 16:12:01 +03:00
Pēteris Caune
10014af352
Optimize HTML output of the events log 2022-09-05 16:01:14 +03:00
Pēteris Caune
58da098d6b
Add timeout handling in hc.lib.s3._remove_objects() 2022-08-25 10:54:59 +03:00
Pēteris Caune
b54c5381e1
Fix tests to pass when SITE_LOGO_URL is set 2022-08-25 10:45:56 +03:00
Pēteris Caune
6320c3ac6e
Add "Developing a New Integration" section in CONTRIBUTING.md 2022-08-25 10:04:47 +03:00
Pēteris Caune
118aa0ec62
Add TOC in docs/self_hosted_configuration 2022-08-23 18:07:34 +03:00
Pēteris Caune
202ded7639
Improve SITE_LOGO_URL docs 2022-08-23 11:52:10 +03:00
Pēteris Caune
b2a6fc20ab
Improve SITE_LOGO_URL docs
cc: #697
2022-08-23 11:40:46 +03:00
Pēteris Caune
a5d5b0c4db
Tweak wording in the cron monitoring guide 2022-08-23 09:02:01 +03:00
Pēteris Caune
914202e45b
Tweak the search snippet size 2022-08-22 13:02:58 +03:00
Pēteris Caune
7ac1437c8a
Switch from "trigram" to "porter unicode61" tokenizer
The trigram tokenizer seems to give better results, but is not
available on the SQLite version that ships with Ubuntu 20.04.
2022-08-22 12:51:53 +03:00
Pēteris Caune
5d5e469347
Implement documentation search 2022-08-22 11:56:02 +03:00
Pēteris Caune
42b9fbec46
Extend docs for INTEGRATIONS_ALLOW_PRIVATE_IPS, add docs for http_proxy 2022-08-18 15:58:14 +03:00
Pēteris Caune
3b137df825
Fix latin-1 handling in headers 2022-08-18 10:49:28 +03:00
Pēteris Caune
3a8b6ef51e
Add NOSIGNAL=1 option
Required when using pycurl from multiple threads [1]. Not sure if
we are but better safe than sorry.

[1] http://pycurl.io/docs/latest/thread-safety.html
2022-08-18 09:55:40 +03:00
Pēteris Caune
1f10aedf6c
Add more tests for hc.lib.curl 2022-08-17 16:12:21 +03:00
Pēteris Caune
9fc13403b4
Add tests for hc.lib.curl 2022-08-17 15:39:17 +03:00
Pēteris Caune
f69a1975e3
Improve error handling 2022-08-17 14:39:29 +03:00
Pēteris Caune
218798b8d2
Fix mypy warnings 2022-08-17 14:21:00 +03:00
Pēteris Caune
7695010398
Document hc.lib.curl.request 2022-08-17 13:07:20 +03:00
Pēteris Caune
3a50396806
Switch from requests to pycurl in integration onboarding views 2022-08-17 12:02:06 +03:00
Pēteris Caune
86e364eb24
Fix hc.lib.curl to send Content-Length header 2022-08-17 10:32:52 +03:00
Pēteris Caune
61a0cf9c2f
Fix Content-Type header when uploading JSON data 2022-08-17 10:11:18 +03:00
Pēteris Caune
fc127e3c01
Fix non-ASCII character handling in URLs 2022-08-17 09:22:30 +03:00
Pēteris Caune
e156c6bcae
Fix hc.lib.curl.request to handle the "auth" kwarg 2022-08-16 12:46:22 +03:00
Pēteris Caune
62acb04bba
Fix hc.lib.curl.request to handle the "params" kwarg 2022-08-16 12:28:53 +03:00
Pēteris Caune
5baa8a53de
Update Dockerfile with pycurl dependencies 2022-08-16 12:01:15 +03:00
Pēteris Caune
a5045a8c54
Add dependencies for building pycurl, take two 2022-08-16 11:45:21 +03:00
Pēteris Caune
90a6e964fe
Add dependencies for building pycurl 2022-08-16 11:40:55 +03:00
Pēteris Caune
64bb43f74f
Limit allowed protocols, add INTEGRATIONS_ALLOW_PRIVATE_IPS setting 2022-08-16 11:13:14 +03:00
Pēteris Caune
41428d1dee
Switch transports from requests to pycurl
With requests, there is no clean way to set a time limit
on a single HTTP request:  https://stackoverflow.com/a/71453648/5821
2022-08-16 09:17:57 +03:00
Pēteris Caune
3799bd3c20
Fix the handling of TooManyRedirects exceptions 2022-08-15 10:52:50 +03:00
Pēteris Caune
04403bbba0
Update CHANGELOG 2022-08-09 08:33:31 +03:00
Pēteris Caune
dd1c05a706
Update links to Django docs, render docs 2022-08-09 08:31:54 +03:00
Facorazza
6f1900cfa3
Add support for SMTP with implicit TLS 2022-08-09 08:25:07 +03:00
Pēteris Caune
1bab426fc2
Simplify Profile.checks_from_all_projects()
Curiously, Django ORM translates the old and the new
version to the exact same SQL
2022-08-08 17:02:55 +03:00
Pēteris Caune
b85cf5d1fe
Update 3rd party resources 2022-08-08 16:31:39 +03:00
Pēteris Caune
3b1a3a6154
Update 3rd party resources 2022-08-08 16:30:23 +03:00
Pēteris Caune
f371561fe5
Update the logout action to use HTTP POST 2022-08-08 15:16:24 +03:00
Pēteris Caune
16083d1037
Update CHANGELOG for v2.3 release 2022-08-05 12:08:55 +03:00
Pēteris Caune
5a0bf4062f
Add API support for resuming paused checks
Fixes: #687
2022-08-04 14:00:46 +03:00
Pēteris Caune
47e624900f
Fix the pause views to create Flip objects
I found a bug in the downtime statistics calculation. The
scenario:

* at T=0 a check goes down
* at T=5 some time later the user pauses it
* at T=10 the check receives a ping and goes up

If we don't record a status change (a flip) at T=5, then
the calculated total downtime will come out wrong (10)

This change fixes the pause views (hc.api.views.pause,
hc.front.views.pause) to create Flip objects.
2022-08-04 12:20:06 +03:00
Pēteris Caune
8ca0e2d636
Remove deprecated django.utils.timezone.utc usage 2022-08-04 11:24:06 +03:00
Pēteris Caune
b9de55043a
Fix duration calculation again, kind can also be "" (empty string) 2022-08-04 10:29:53 +03:00
Pēteris Caune
b4ad057e67
Add section about the "/log" endpoint in the "Attaching Logs" page 2022-08-04 10:07:23 +03:00
Pēteris Caune
c44148e715
Fix duration calculation to skip "log" and "ign" events 2022-08-04 09:26:40 +03:00
Pēteris Caune
ea810e286b
Upgrade to Django 4.1 and django-compressor 4.1 2022-08-04 09:01:15 +03:00
Pēteris Caune
c840f72e3a
Clean up tests 2022-08-03 17:06:38 +03:00
Pēteris Caune
2c545c4b38
Downgrade to Django 4.0.7
I jumped the gun here, django-compressor doesn't yet have a Django 4.1
compatible release. With Django 4.1, "manage.py compress" currently
throws an error.
2022-08-03 14:50:50 +03:00
Pēteris Caune
4947aab26b
Upgrade to Django 4.1 2022-08-03 14:09:20 +03:00
Pēteris Caune
51fd339602
Add auto-refresh and running indicator in My Projects
Fixes: #681
2022-08-02 16:37:25 +03:00
Pēteris Caune
2f562bb502
Fix the checks list to preserve filters when adding/updating checks
Fixes: #684
2022-08-02 13:27:04 +03:00
Pēteris Caune
6644b43577
Fix the update_timeout view to record more information
Checks can flip from "up" to "down" state as a result of changing
check's schedule.  We don't want to send notifications when changing
schedule interactively in the web UI. So we update the `alert_after`
and `status` fields the same way as `sendalerts` would do. This is not
new, this was already being done.

With this change, we now additionally create Flip objects, recording
the fact that a check went down at such-and-such date. We fill the
"processed" field, to make sure sendalerts skips over these objects.

We need the Flip objects because otherwise the calculation
in Check.downtimes() could come out wrong (when a check later comes up,
we would have no record of when it went down).
2022-08-02 09:43:35 +03:00
Pēteris Caune
f9a716710b
Upgrade to requests 2.28.1, segno 1.5.2 2022-08-01 16:22:41 +03:00
Pēteris Caune
dd6be22ab4
Upgrade to fido2 1.0.0 2022-08-01 16:16:15 +03:00
Pēteris Caune
453b426090
Add testcase and update CHANGELOG 2022-07-29 12:58:58 +03:00
Leandro Britez
37ff7b1b05 Add support for multiple RCPT TO in incoming email 2022-07-29 12:56:30 +03:00
Pēteris Caune
a4b6fc61ad
Replace HipChat, Pagerteam classes with transports.RemovedTransport 2022-07-29 11:35:03 +03:00
Pēteris Caune
c322fb8bbb
Add the "Badges" page in docs 2022-07-28 12:34:31 +03:00
Pēteris Caune
580304110e
Make tests not sensitive to the SITE_NAME setting 2022-07-27 15:26:32 +03:00
Pēteris Caune
65cef0b271
Fix grouping and sorting in the text version of the report/nag emails
Fixes: #679
2022-07-27 15:22:41 +03:00
Pēteris Caune
874548874c
Fix grammar 2022-07-27 11:58:30 +03:00
Pēteris Caune
8cef4dfbbd
Improve title 2022-07-27 11:22:24 +03:00
Pēteris Caune
a9b9adf178
Improve the "Monitoring Cron Jobs" page 2022-07-27 10:30:44 +03:00
Pēteris Caune
0f659241fe
Tweak copy in docs - introduction 2022-07-26 14:11:22 +03:00
Pēteris Caune
b2b361e2b9
Improve Notification admin: link to project from the list view 2022-07-26 11:04:03 +03:00
Pēteris Caune
5a94e6809e
Update links to Django docs (3.1 -> 4.0) 2022-07-26 10:40:50 +03:00
Pēteris Caune
a9ac8715e4
Add "Where to See Captured Logs" in docs 2022-07-22 14:58:25 +03:00
Pēteris Caune
438c94efb7
Fix a race condition in the "Change Email" flow
The race scenario was as follows:

* Alice initiates email address change to bob@example.org
* a verification link is sent to bob@example.org
* separately, somebody creates a new account for bob@example.org
* Alice clicks on the verification link

At this point,
- if the database has an uniqueness constraint on auth_user.email,
  Alice will receive a HTTP 500 error
- if there's no uniqueness constraint, the email change
  will succeed and the system will have two accounts with the
  same email address

The simple fix is to re-check the address availability just
before finalizing the email address change. Currently this is
not done in a transaction block, so the race condition still
exists in theory, but is much less likely to happen in practice.
2022-07-21 15:14:51 +03:00
Pēteris Caune
d2c79b0c2b
Rename "Log" -> "Events" in the UI 2022-07-21 14:41:23 +03:00
Pēteris Caune
01720ca9ae
Update notification templates to handle "log" events 2022-07-21 14:22:41 +03:00
Pēteris Caune
dc107ff7f5
Add ping endpoints for "log" events 2022-07-21 10:30:52 +03:00
Pēteris Caune
efa5acc1b4
Add support for the $JSON placeholder in webhook payloads 2022-07-20 12:06:39 +03:00
Pēteris Caune
cd087d2fbf
Add API support for enabling/disabling filtering in message body
Specifically, add read/write support for the new fields:

* success_kw
* failure_kw
* filter_subject
* filter_body

The API still supports reading/writing the "subject" and
"subject_fail" fields, but these are now marked as deprecated
in API documentation.

Fixes: #653
2022-07-15 13:04:41 +03:00
Pēteris Caune
6fc89562b1
Fix grammar and inaccuracies in docs 2022-07-15 11:19:25 +03:00
Pēteris Caune
5318e584fe
Upgrade to HiDPI screenshots in the documentation 2022-07-15 11:06:23 +03:00
Pēteris Caune
27d9ff9ffb
Update docs
cc: #653
2022-07-13 12:42:06 +03:00
Pēteris Caune
ad87f9d44e
Update template to match "not null" database fields
The template had "value|default:''" fallback for
null values. This is not needed any more.
2022-07-13 12:27:31 +03:00
Pēteris Caune
4766aade95
Fix migrations
When adding "NOT NULL" on multiple columns at once, Django
throws errors:

    django.db.utils.OperationalError:
    cannot ALTER TABLE "api_check" because it has
    pending trigger events

A workaround is to modify columns one by one in
separate migrations.
2022-07-13 12:24:47 +03:00
Pēteris Caune
ae4ee37053
Add "NOT NULL" constraints on the new api_check fields
cc: #653
2022-07-13 12:02:58 +03:00
Pēteris Caune
cc32af6127
Remove api_check.subject and api_check.subject_fail fields
cc: #653
2022-07-13 11:40:35 +03:00
Pēteris Caune
426d6d07b3
Update API to use success_kw and failure_kw fields
cc: #653
2022-07-13 11:05:13 +03:00
Pēteris Caune
c3c5342c3c
Fix the handling of null values in the "Filtering Rules" dialog 2022-07-13 10:18:09 +03:00
Pēteris Caune
003d35d431
Add "Filter by keywords in the message body" feature
cc: #653
2022-07-12 15:46:15 +03:00
Pēteris Caune
3c43e5aa45
Optimize the spinner animation
Use a CSS box-shadow trick to implement it with just a single
DOM element instead of four.
2022-07-07 10:25:13 +03:00
Pēteris Caune
1f27d3c9cd
Upgrade to Django 4.0.6 2022-07-04 13:13:19 +03:00
Pēteris Caune
272c2daa38
Upgrade to cron-descriptor 1.2.30 2022-07-04 10:33:27 +03:00
Pēteris Caune
3effd77e70
Add retry limit in hc.lib.s3.put_object 2022-07-01 23:47:47 +03:00
Pēteris Caune
227a8407bb
Update the Signal integration to handle RATE_LIMIT_FAILURE errors 2022-07-01 15:12:13 +03:00
Pēteris Caune
0553f0a38a
Fix the display of ignored pings with non-zero exit status 2022-06-30 16:49:09 +03:00
Pēteris Caune
ec0be60ca8
Include last ping type in MS Teams notifications 2022-06-30 13:15:40 +03:00
Pēteris Caune
56a0d9f78b
Include last ping type in Slack, Mattermost, Discord notifications 2022-06-30 12:56:09 +03:00
Pēteris Caune
b3f2bc64a5
Improve "Show Usage Examples" dialog, "Email" tab 2022-06-30 10:22:46 +03:00
Pēteris Caune
867fae0db6
Add code comment 2022-06-30 10:22:18 +03:00
Pēteris Caune
25b22dba72
Optimize <select name="tz"> initialization 2022-06-29 11:45:50 +03:00
Pēteris Caune
a354f8eb52
Update selectize 0.12 -> 0.13 2022-06-29 11:14:49 +03:00
Pēteris Caune
0c6223ffa5
Implement the "Add Check" dialog 2022-06-29 10:35:12 +03:00
Pēteris Caune
4d644ef161
Fix checkbox and radio border color in dark mode 2022-06-28 16:20:21 +03:00
Pēteris Caune
229f97ef9a
Optimize HTML in the "My Checks" screen 2022-06-27 10:05:32 +03:00
Pēteris Caune
5bb143caa2
Fix grammar issues, document the TWILIO_* settings 2022-06-26 16:02:29 +03:00
Pēteris Caune
c0809c1b16
Optimize spinner HTML 2022-06-26 11:04:09 +03:00
Pēteris Caune
b22164a9a1
Add Pushbullet integration setup instructions
cc: #24
2022-06-26 11:03:41 +03:00
Pēteris Caune
1b6269b29f
Improve Credential admin 2022-06-22 10:21:14 +03:00
Pēteris Caune
d37f12a528
Update CodeQL workflow 2022-06-22 10:15:11 +03:00
Pēteris Caune
fa3b2dc6e3
Add code comments and type hints, remove now-unused cbor.js 2022-06-19 12:51:01 +03:00
Pēteris Caune
2b623453c1
Update tests 2022-06-19 12:31:27 +03:00
Pēteris Caune
a4c4df976c
Split the helper class in GetHelper and CreateHelper 2022-06-19 11:30:37 +03:00
Pēteris Caune
57021e962c
Refactor webauthn implementation, use webauthn-json 2022-06-19 10:10:57 +03:00
Pēteris Caune
64a6245736
Improve tests 2022-06-17 15:44:47 +03:00
Pēteris Caune
93c13b8221
Include check.desc in email text template, update tests 2022-06-17 14:55:34 +03:00
Pēteris Caune
7912f1e4df
Update Dockerfile to start SMTP listener
Fixes: #668
2022-06-13 17:15:51 +03:00
Pēteris Caune
f4a30fb25a
Update CHANGELOG for release 2022-06-13 14:57:10 +03:00
Pēteris Caune
d0d36d5da5
Fix the version number displayed in the footer 2022-06-13 14:56:12 +03:00
Pēteris Caune
f2ce7aed8e
Improve the text version of the alert email template 2022-06-13 14:30:44 +03:00
Pēteris Caune
cce52bf298
Update CHANGELOG for release 2022-06-13 12:57:15 +03:00
Pēteris Caune
2105347897
Bump Django and whitenoise versions 2022-06-13 12:56:34 +03:00
Pēteris Caune
9e578f6dfc
Add "Disabled" priority for Pushover notifications
Fixes: #663
2022-06-10 18:19:12 +03:00
Pēteris Caune
ca392c07ce
Eliminate jQuery usage in the login page 2022-06-08 09:46:51 +03:00
Pēteris Caune
51f7fe7332
Expose subject and subject_fail in API GET calls, improve docs 2022-06-03 09:59:20 +03:00
Pēteris Caune
6a68fd2c23
Add subject and subject_fail type and length validation 2022-06-03 09:18:22 +03:00
Tyler
d61909ffd2 Expose subject and subject_fail via API 2022-06-03 09:12:46 +03:00
Pēteris Caune
a37a937209
Improve Gotify instructions and event description 2022-06-01 16:37:54 +03:00
Pēteris Caune
b19ddab1bd
Add Gotify integration
Fixes: #270
2022-06-01 16:13:41 +03:00
Pēteris Caune
03dea07ae2
Remove obsolete field: Check.last_ping_was_fail 2022-05-31 15:13:00 +03:00
Pēteris Caune
8216377da6
Improve tests 2022-05-30 17:33:10 +03:00
Pēteris Caune
f7b4a6d71c
Remove support for unsigned login tokens 2022-05-30 16:59:13 +03:00
Pēteris Caune
c1ff8875e3
Implement login link expiration
Login links will now expire in 1 hour.
2022-05-30 15:48:51 +03:00
Pēteris Caune
66b7f4dd32
Fix upload test to specify its own PING_BODY_LIMIT 2022-05-30 14:42:23 +03:00
Pēteris Caune
1d340d24aa
Add notes in docs about configuring uWSGI via UWSGI_ env vars
cc: #656
2022-05-27 15:13:03 +03:00
Pēteris Caune
a5e5b45983
Reduce logging, add Ctrl+C handler in sendalerts and sendreports
cc: #656
2022-05-27 14:49:44 +03:00
Pēteris Caune
901f944055
Test pyflakes warnings 2022-05-26 21:39:53 +03:00
Pēteris Caune
09a99d3e9c
Add tests 2022-05-20 18:14:43 +03:00
Pēteris Caune
6790d867a6
Add address verification step in the "Change Email" flow
A similar issue has come up multiple times: the user
changes account's email address, enters a bad address
by mistake, and gets locked out of their account.

This commit adds an extra step in the "Change Email" flow:

* In "Account Settings", user clicks on [Change Email]
* User gets a prompt for a 6-digit confirmation code, which
  has been sent to their old address. This is to prevent
  account takeover when Eve sits down at a computer where Alice
  is logged in.
* The user enters the confirmation code, and a "Change Email"
  form loads.
* The user enters their new email address.
* (The new step!) Instead of changing the email right away,
  we send a special login link to user's specified new address.
* (The new step, continued) The user clicks on the login link,
  their account's email address gets updated, and they get
  logged in.

The additional step makes sure the user can receive email
at their new address. If they cannot receive email there,
they cannot complete the "Change Email" procedure.
2022-05-20 17:54:45 +03:00
Pēteris Caune
8da87cdea5
Update code to not use related managers of unsaved objects
When testing with django==4.1a1, some tests were failing with
a message:

> ValueError: 'Check' instance needs to have a primary key
> value before this relationship can be used.

This commit fixes these failures, but there might be more
places to fix, that are not covered by tests yet.
2022-05-18 13:07:50 +03:00
Pēteris Caune
59e112852b
Switch from auto_now_add=True to default=now
I've run in the following problem a few times in tests:

* I create a model instance
* set its "created" field to a specific value
* I save the model instance
* I write testcase logic which relies on that specific "created" value

The testcase fails, because, with auto_now_add=True, Django
overwrites the created field. I can work around this by:

* Create and save a model instance
* Save it
* Set the created field to my desired value
* Save it again

But this is annoying to do, and annoying to troubleshoot
– it's easy to forget about the auto_now_add behaviour.

So I'm replacing auto_now_add=True with
default=django.utils.timezone.now.
2022-05-18 11:42:56 +03:00
Pēteris Caune
7247983fdd
Add logic to handle ContentDecodingError exceptions 2022-05-17 16:16:24 +03:00
Pēteris Caune
1a41e4e199
Enable error emails from management commands 2022-05-17 10:29:13 +03:00
Pēteris Caune
fb0e3bc10d
Update hc.front.views.channels to handle empty strings in settings
Fixes: #635
2022-05-16 15:10:26 +03:00
Pēteris Caune
0178cee02c
Update docker docs 2022-05-16 15:01:44 +03:00
Pēteris Caune
7889fa3d64
Remove obsolete key 2022-05-16 14:59:56 +03:00
Pēteris Caune
e3ff8bf3ca
Update CHANGELOG for v2.1 release 2022-05-10 16:32:03 +03:00
Pēteris Caune
e7076155e7
Add "Ping-Body-Limit" response header in ping API responses
The header format is:

    Ping-Body-Limit: n

Where "n" is an integer number, the value of the  PING_BODY_LIMIT
configuration setting.

Clients can use this header to decide how much POST data to send
in HTTP requests. If a client sends more than "n" bytes in the
request body, Healthchecks will store the first "n" bytes, and
ignore the rest.

The default value for PING_BODY_LIMIT is 10000 (10KB).
2022-05-10 15:44:27 +03:00
Pēteris Caune
eac023caa1
Enable searching channels by code in admin UI 2022-05-10 15:36:49 +03:00
Pēteris Caune
7a5b6b6b31
Fix tests 2022-05-07 20:56:39 +03:00
Pēteris Caune
62c631a1e8
Increase the default profile limits
Accounts on self-hosted instances should have "unlimited" limits.
I had originally assumed 500 checks should be *enough for everybody*,
but in practice people do have accounts with 1000+ checks,
and may want to do scale testing with even more checks
(see #649). This commit raises the "unlimited" limits higher
to make it less likely users bump into them.
2022-05-07 20:51:07 +03:00
Pēteris Caune
756257a4a4
Document webhook retry policy in the "Add Webhook" page 2022-05-02 14:54:02 +03:00
Pēteris Caune
f8382fd84e
Increase max displayed duration from 24h to 72h
Fixes: #644
2022-05-02 12:07:14 +03:00
Pēteris Caune
d1d9dd5021
Fix "Test" button for integrations that only send "up" notifications 2022-05-02 11:55:00 +03:00
y8l
3b358cf6bf Closing #645
Fixing the URL path to Cron dialog screenshot in static/img with 2x zoom
2022-05-01 09:21:07 +03:00
Pēteris Caune
bc0eb8dc7d
Upgrade to cronsim 2.1 2022-04-30 09:27:48 +03:00
Pēteris Caune
98f2536825
Oops, remove debug code 2022-04-25 21:14:35 +03:00
Pēteris Caune
b07f670b05
Regenerate snippet HTML using latest Pygments 2022-04-25 21:06:27 +03:00
Pēteris Caune
0bd09a6e65
Update the C# snippet 2022-04-25 20:55:16 +03:00
Pēteris Caune
156fc321bc
Upgrade to django-compressor 4.0
Fixes: #615
2022-04-23 18:12:20 +03:00
Pēteris Caune
85ae2ce724
Remove UTF8 mention in attaching_logs.md 2022-04-20 17:37:22 +03:00
Pēteris Caune
b776762ba9
Fix prunenotifications to handle checks with missing pings
Fixes: #636
2022-04-20 16:25:19 +03:00
Pēteris Caune
51d1b88a75
Rename /docker/.env -> /docker/.env.example
This should help avoid merge conflicts when users fork the
repository and make changes to /docker/.env

cc: #638
2022-04-20 15:55:46 +03:00
Pēteris Caune
4c58c55741
Fix tests to skip time.sleep() 2022-04-19 11:39:28 +03:00
Pēteris Caune
aa2571b7fc
Add small delay in transports.Email.notify to allow ping body to upload 2022-04-19 11:37:48 +03:00
Pēteris Caune
cc3e4d8ab3
Fix wording 2022-04-13 10:52:18 +03:00
Pēteris Caune
32f021b9c5
Update email template to handle not yet uploaded ping bodies 2022-04-13 10:43:12 +03:00
Pēteris Caune
bb38ad3187
Remove the Signal CAPTCHA form (use "submitchallenge" command instead) 2022-04-11 14:24:28 +03:00
Pēteris Caune
a26ab36ddc
Upgrade to Django 4.0.4 2022-04-11 11:55:59 +03:00
Pēteris Caune
6124ad59cd
Improve CAPTCHA alert email 2022-04-11 09:38:30 +03:00
Pēteris Caune
d1033ba6b5
Fix CAPTCHA email alert 2022-04-11 09:34:58 +03:00
Pēteris Caune
90d30c8b62
Add management command to submit Signal CAPTCHAs 2022-04-11 09:29:33 +03:00
Pēteris Caune
842f0c3e16
Improve CSS in Channel and Notification admin 2022-04-09 17:16:10 +03:00
Pēteris Caune
cb36e17440
Improve Notification admin 2022-04-09 16:43:39 +03:00
Pēteris Caune
7bd916558b
Add @login_required and update CHANGELOG 2022-04-09 16:35:57 +03:00
Pēteris Caune
131ffe14fb
Add experimental UI for submitting Signal rate limit challenges 2022-04-09 16:29:26 +03:00
Pēteris Caune
1826f6f654
Improve Channel and Notification admin 2022-04-09 15:03:59 +03:00
Pēteris Caune
14e2ee39f4
Clean up code in HttpTransport._request 2022-04-08 17:11:40 +03:00
Pēteris Caune
c5e677681b
Add test for hc_check_up and update CHANGELOG 2022-04-08 11:47:50 +03:00
Igor Rzegocki
e7333d6352 Add hc_check_started prometheus metrics 2022-04-08 11:25:57 +03:00
Pēteris Caune
a3d1bc7386
Implement the "started" progress spinner in the details pages 2022-04-08 10:56:36 +03:00
Pēteris Caune
29a24c34b0
Fix tests 2022-04-07 09:40:13 +03:00
Pēteris Caune
911d63a2eb
Add logic to alert ADMINS when Signal transport hits a CAPTCHA challenge 2022-04-07 09:31:20 +03:00
Pēteris Caune
087ed0ac12
Improve Matrix notification template 2022-04-04 13:38:40 +03:00
Pēteris Caune
1b2defa6ee
Optimize HTML output in /checks/<uuid>/log/ 2022-04-04 11:11:55 +03:00
Pēteris Caune
580eaceeb5
Optimize HTML output in /checks/<uuid>/log/
* Don't use 'date' and 'time' CSS classes in HTML output
* Replace the span.ua-body CSS class with code tags
2022-04-04 10:24:31 +03:00
Pēteris Caune
1d20be94ff
Optimize HTML output in /checks/<uuid>/log/
* Simplify the "n" cell
* Don't send details URLs, construct them on client side
* Strip microseconds in timestamps
2022-04-04 09:51:07 +03:00
Pēteris Caune
e11e8587a0
Tweak wording in the "check limit reached" message 2022-04-01 18:29:27 +03:00
Pēteris Caune
c4bbb4432a
Fix unwanted localization in badge SVG generation
Fixes: #629
2022-04-01 15:58:17 +03:00
Pēteris Caune
a6e52bc254
Tweak wording in the "check limit reached" message 2022-04-01 14:36:56 +03:00
Pēteris Caune
01be572a54 Use higher resolution screenshots in the README 2022-03-22 11:09:55 +02:00
Pēteris Caune
d54dcb5ea6
Fix the GHA workflow for building arm/v7 docker image 2022-03-18 13:24:18 +02:00
Pēteris Caune
e8160e4cf3
Add minio in the Dockerfile 2022-03-18 09:56:24 +02:00
Pēteris Caune
baa6e8e5d9
Add an experimental workaround for failing armv7 build
Context: https://github.com/JonasAlfredsson/docker-on-tmpfs
2022-03-18 09:39:41 +02:00
Pēteris Caune
e2a8e712c4
Update CHANGELOG for v2.0 release 2022-03-18 09:05:55 +02:00
Pēteris Caune
6958a9e898
Add a "Download Original" link in the "Ping Details" dialog 2022-03-16 17:31:01 +02:00
Pēteris Caune
fd15302d2a
Improve the loading state of the "Ping Details" dialog
If the "Ping details" dialog takes more than ~300ms to load,
show an animated progress indicator.

Also, move the loading code to a ping_details.js file
to reduce code repetition.
2022-03-16 13:51:26 +02:00
Pēteris Caune
2c4b57fa87
Tweak title 2022-03-15 11:10:26 +02:00
Pēteris Caune
527bc32d67
Fix hc.lib.s3.get_object to handle missing S3 credentials 2022-03-14 10:21:11 +02:00
Pēteris Caune
620fda5589
Tweak User and Profile admin
- change the "By check count" filter in Profile admin
- add "Last active" column in user list view
2022-03-13 17:04:00 +02:00
Pēteris Caune
7ae0849652
Fix crash in _remove_objects when n is negative 2022-03-11 14:33:46 +02:00
Pēteris Caune
56a6fe2381
Add "deactivate" action in user admin 2022-03-09 17:12:16 +02:00
Pēteris Caune
920166032f
Fix typo 2022-03-09 16:51:13 +02:00
Pēteris Caune
b6530a0027
Update docs with the new S3_ environment variables 2022-03-09 16:47:43 +02:00
Pēteris Caune
664c6018ab
Add pruneobjects management command and update README 2022-03-09 15:30:52 +02:00
Pēteris Caune
fcaf894d46
Fix Mattermost integration to treat 404 as a transient error
Fixes: #613
2022-03-09 10:10:38 +02:00
Pēteris Caune
16a2cd204e
Fix unwanted localization of period and grace values
cc: #617
2022-03-09 09:13:09 +02:00
Pēteris Caune
383cff6255
Improve Dockerfile
Changes:
- Switch to Python 3.10
- Drop pywheels.org repository. Build cryptography, uwsgi, psycopg2
  etc. from source. This means builds on armhf will be slower, but
  hopefully fewer worries about the piwheels binary package
  not finding a .so file in our base image
- Don't ship the .git folder (facepalm)
2022-03-07 21:33:39 +02:00
Pēteris Caune
fc02b7dd99
Fix uwsgi breakage on armhf
The binary uwsgi package from piwheels.org crashes with:

    uwsgi: error while loading shared libraries: libxml2.so.2:
    cannot open shared object file: No such file or directory

(when run in a docker environment, with python:3.9-slim-buster
base image. It works fine in Raspbian)

The workaround is to build uwsgi from source, hence the "--no-binary"
flag.
2022-03-07 17:59:36 +02:00
Pēteris Caune
833e74474a
Update Dockerfile to include mysqlclient
cc: #612
2022-03-07 15:55:25 +02:00
Pēteris Caune
01780dd0dc
Update requirement versions 2022-03-07 10:53:10 +02:00
Pēteris Caune
beda1f1a05
Remove dead code 2022-03-04 17:37:05 +02:00
Pēteris Caune
c1ac2ffc9c
Fix s3.py to reuse Minio client and retry InternalError errors 2022-03-04 17:33:13 +02:00
Pēteris Caune
1aad3e181b
Add "block" boolean argument to hc.lib.s3.remove_objects 2022-03-04 14:26:11 +02:00
Pēteris Caune
4422f2cf95
Add timeout for S3 operations 2022-03-04 13:09:38 +02:00
Pēteris Caune
5774757843
Add handling for missing S3 objects 2022-03-04 12:48:28 +02:00
Pēteris Caune
82f3b0a046
Fix ping_details to run get_body() only once 2022-03-04 12:37:43 +02:00
Pēteris Caune
7ee64893b9
Optimize CSS 2022-03-04 11:36:32 +02:00
Pēteris Caune
0976dea8a1
Enable GitHub Sponsors
Fixes: #605
2022-03-02 08:37:59 +02:00
Pēteris Caune
6b82f1d17f
Document the trickery in s3.py 2022-02-28 15:36:26 +02:00
Pēteris Caune
67edf5a442
Fix tests 2022-02-28 11:55:47 +02:00
Pēteris Caune
05bb80130b
Add support for storing ping bodies in S3-compatible object storage
This is an initial, minimal implementation. It is currently
missing: error handling, timeouts for S3 operations, documentation.

cc: #609
2022-02-28 11:51:04 +02:00
Pēteris Caune
6755935322
Add task-mon to 3rd party resources 2022-02-28 09:53:09 +02:00
Pēteris Caune
5ecd625c0b
Add Ping.body_raw field for storing body as bytes 2022-02-25 16:50:54 +02:00
Pēteris Caune
3b56fd4175
Fix Signal integration to handle UNREGISTERED_FAILURE errors 2022-02-23 11:32:17 +02:00
Pēteris Caune
704ced868d
Add a pinging example using axios 2022-02-21 18:06:21 +02:00
Pēteris Caune
8b0cbde680
Remove X-Bounce-Url from report email headers (unused) 2022-02-21 11:10:55 +02:00
Pēteris Caune
e9c99ab711
Update 3rd party resources, remove inactive library 2022-02-21 10:55:23 +02:00
Pēteris Caune
613318b26c
Add logging for OSError exceptions in the Signal transport class
signal-cli has some stability issues, this adds extra
logging so its easier to keep an eye on it.

With the default Django logging configuration, when OSError
is raised, Django will send an error report to ADMINS.

If sentry_sdk is installed and configured, the exception
will also show up in Sentry.
2022-02-18 17:13:12 +02:00
Pēteris Caune
9c35b519b3
Update /docker/.env, replace SIGNAL_CLI_ENABLED -> SIGNAL_CLI_SOCKET 2022-02-18 09:51:25 +02:00
Pēteris Caune
8dcc1bfdc1
Fix Telegram bot to handle TransportError exceptions
The Telegram onboarding flow in a nutshell:

1. user invites our bot in a channel and types "/start"
2. Telegram calls our webhook (/integration/telegram/bot/)
3. Our webhook generates and posts an invite link to the chat
4. User clicks the invite link, we show "Add Telegram" form
...

In step 3, when we post the invitation link, if Telegram returns
an error (for example, CHAT_WRITE_FORBIDDEN), our webhook was
throwing HTTP 500. In response, Telegram would retry the webhook
several times, but that's probably futile.

After this commit, the webhook will return HTTP 200, regardless
of whether we could post the invite to the Telegram chat or not.
2022-02-16 16:12:54 +02:00
Pēteris Caune
ec4b5b05ed
Fix language 2022-02-16 16:01:07 +02:00
Pēteris Caune
6ae88a16b8
Fix special character escaping in Zulip notifications 2022-02-09 10:39:29 +02:00
Pēteris Caune
934f2e7959
Fix special character escaping in VictorOps notifications 2022-02-09 10:33:58 +02:00
Pēteris Caune
1e02208422
Fix special character escaping in PagerTree notifications 2022-02-09 10:06:41 +02:00
Pēteris Caune
3f521b16f7
Make email non-editable in "Invite Member" when team limit reached
There is a specific limit of how many other users a given user
can invite in their projects (depends on the plan they are on).
When the limit is reached, the user cannot invite *new* users
in their projects, but they can still invite team members
from one project into another project. In other words, we count
the number of unique invited users, not the number of memberships.

There was an UI bug in the "Invite a Team Member" dialog. The
dialog has an editable "Email" text field. When an user has reached
the team limit, and they open the "Invite" dialog, they could
enter a new user's email address in the Email field and try to invite
them. The server would refuse to exceed the team limit and would
return a plain HTTP 403 page. This is of course confusing to the 
end user.

The fix is to show "Email" as a text field only if the user has
not yet exceeded their team size. If they have, then show "Email"
as non-editable text.
2022-02-04 20:43:17 +02:00
Pēteris Caune
a2e8e31c31
Fix special character escaping in Trello notifications 2022-02-04 20:00:54 +02:00
Pēteris Caune
5ae85f850c
Fix JS error after copying a code snippet 2022-02-04 17:05:45 +02:00
Pēteris Caune
6539173a0f
Fix special character escaping in LINE Notify notifications 2022-02-04 15:16:56 +02:00
Pēteris Caune
d5103a8231
Disable special character escaping in Pushbullet notifications 2022-02-04 15:02:15 +02:00
Pēteris Caune
529a47b09f
Add test cases for special character escaping in notifications 2022-02-04 14:55:02 +02:00
Pēteris Caune
d38ebee06c
Disable HTML escaping in Spike.sh notifications 2022-02-04 14:27:02 +02:00
Pēteris Caune
b56f27e4e2
Improve PagerDuty notifications
- Include additional data in the "details" key
- Don't escape HTML characters in the "description" field

cc: #600
2022-02-04 10:36:33 +02:00
Pēteris Caune
14e77f0acc
Disable HTML escaping in Pushover notification titles
Fixes: #606
2022-02-04 10:04:07 +02:00
Pēteris Caune
22ab91bae5
Upgrade to Django 4.0.2 2022-02-01 13:18:00 +02:00
Pēteris Caune
ca93e95632
Eliminate bootstrap's list-group 2022-02-01 09:06:35 +02:00
Pēteris Caune
c2d6799969
Optimize PNG files with oxipng 2022-01-29 17:17:21 +02:00
Pēteris Caune
622755f7aa
Improve the X-Forwarded-Proto note
cc: #597
2022-01-24 15:58:24 +02:00
Pēteris Caune
e5ac8d7dbc
Update the "Add TOTP" form to display plaintext TOTP secret
Fixes: #602
2022-01-24 15:17:48 +02:00
Pēteris Caune
59147c530a
Update Notification admin to allow searching by code 2022-01-21 16:24:00 +02:00
Pēteris Caune
7b5c5498c2
Add a "notification" keyword in "notify()" methods
Each transport class has a notify() method. This method
used to have a single argument: "check". Some transport
classes require some extra information, and we were passing
it by adding ad-hoc fields to Check objects in
hc.api.models.Channel.notify().

After this change, notify() of each transport class expects
two arguments: "check" and "notification". This is cleaner
and more explicit than stuffing extra information in the
Check object.
2022-01-19 15:51:30 +02:00
Pēteris Caune
15be40ce6b
Add note about X-Forwarded-Proto
cc: #597
2022-01-17 11:34:06 +02:00
Pēteris Caune
e9eaa6d578
Update Signal integration to handle missing "id" in JSON RPC response 2022-01-15 14:43:47 +02:00
Pēteris Caune
665244d298
Simplify mock Socket object 2022-01-14 12:29:35 +02:00
Pēteris Caune
081016fce6
Update the Signal integration to check JSON RPC message IDs
Based on the conversation here:
https://github.com/AsamK/signal-cli/discussions/799
2022-01-14 12:23:17 +02:00
Pēteris Caune
82663a2a52
Update Signal integration to use JSON RPC over UNIX socket 2022-01-13 18:12:33 +02:00
Pēteris Caune
24a36beb77
Update email bounce handler to mark email channels as disabled
User can re-enable a disabled email channel by editing it
(in the Integrations page, click the "Fix..." button).

Fixes: #446
2022-01-13 12:10:08 +02:00
Pēteris Caune
b88e4dd2de
Update Channel admin to allow searching channels by name 2022-01-07 16:58:41 +02:00
Pēteris Caune
39db47387f
Update Telegram to treat "group chat was deleted" as permanent error 2022-01-07 16:16:24 +02:00
Pēteris Caune
731c54529c
Update CHANGELOG for v1.25.0 release 2022-01-07 12:17:22 +02:00
Pēteris Caune
9108e84d1b
Fix unclosed file warning 2022-01-07 11:03:29 +02:00
Pēteris Caune
7beb4330d8
Upgrade to requests 2.27.1 2022-01-05 21:42:54 +02:00
Pēteris Caune
d615cde23e
Update Dockerfile to avoid running "pip wheel" more than once
The problem:
- the first "pip wheel" collects a specific version of requests
- the second "pip wheel" collects the latest version of requests
- later "pip install" tries to install both and fails

The fix is to run "pip wheel" once, and it will then pick a single
version of requests that satisfies all constraints.

Fixes: #594
2022-01-05 21:35:09 +02:00
Pēteris Caune
6805d75a29
Bump the min. Python version to 3.8 (as required by Django 4) 2022-01-05 16:13:49 +02:00
Pēteris Caune
a155f40861
Upgrade to Django 4
Replace usages of pytz with zoneinfo, upgrade to cronsim==2.0,
which is compatible with zoneinfo.
2022-01-05 16:01:48 +02:00
Pēteris Caune
d15ea01077
Bump the minimal Python version from 3.6 (EOL) to 3.7 2022-01-05 14:25:07 +02:00
Pēteris Caune
fc79066c49
Move chat_id updating logic to the Channel model 2022-01-05 10:40:01 +02:00
Pēteris Caune
22f5fb9016
Add more type hints in transports.py 2022-01-05 10:33:45 +02:00
Pēteris Caune
7317adc7f7
Refactor transport classes, add Channel.disabled field
- Refactor transport classes to raise exceptions
  on delivery problems, instead of returning error
  message as string. Exceptions can carry extra meta
  information (see TransportError.permanent field, see
  MigrationRequiredError subclass). I considered attaching
  the extra information to strings by subclassing str, but
  using exceptions felt cleaner and less hacky.

- Add Channel.disabled field, for disabling integrations
  on permanent errors. For example, if Slack returns
  HTTP 404, we will now mark the integration as disabled
  and will not make requests to that Slack endpoint again.
2022-01-05 09:46:39 +02:00
Pēteris Caune
baca9c0112
Upgrade to Django 3.2.11 2022-01-04 15:06:22 +02:00
Pēteris Caune
ee8cd29f63
Remove prunepings mention from selfhosted docs
cc: #591
2022-01-03 10:09:09 +02:00
Pēteris Caune
2cf1ed417e
Update the Slack integration to not retry when Slack returns 404 2021-12-30 18:17:53 +02:00
Pēteris Caune
d8f1659e45
Implement Telegram group to supergroup migration
Fixes: #132
2021-12-30 11:54:02 +02:00
Pēteris Caune
abb7bc7150
Add support for Telegram channels
To make this work, existing installations must re-run
the "settelegramwebhook" management command.

Fixes: #592
2021-12-28 18:50:06 +02:00
Pēteris Caune
543deb5a30
Upgrade to django-compressor 3.0 2021-12-13 09:49:41 +02:00
Pēteris Caune
aaaeee7835
Remove unused import 2021-12-11 10:52:59 +02:00
Pēteris Caune
c04d4e45b0
Switch from pytz.all_timezones to hc.lib.tz.all_timezones
In preparation to Django 4.0 upgrade, I'm removing non-essential
uses of pytz. One of the uses was to test if a timezone name
supplied by the user (e.g. "Europe/Riga") is valid by looking
it up in pytz.all_timezones.

This commit adds hc.lib.tz.all_timezones, which is a plain
simple Python list with timezone names. In the future,
I could in theory switch to zoneinfo.available_timezones(), but
will likely stick with the list.

hc.lib.tz.all_timezones is a
2021-12-11 10:51:13 +02:00
Pēteris Caune
303036b8e2
Switch from pytz.UTC to datetime.timezone.utc 2021-12-11 10:44:56 +02:00
Pēteris Caune
ea7535775c
Switch to "from django.utils.timezone import now" 2021-12-11 10:43:26 +02:00
Pēteris Caune
393391df65
Switch to "from django.utils.timezone import now" 2021-12-11 10:27:30 +02:00
Pēteris Caune
248ce93942
Switch to "from django.utils.timezone import now"
Before:

    from django.utils import timezone
    now = timezone.now()

After:

    from django.utils.timezone import now
    frozen_now = now()

I'm making this change because I'm planning an upgrade to
Django 4.0, and to zoneinfo, and to using datetime.timezone
more. I want to minimize confusion between these two:

    from datetime import timezone
    from django.utils import timezone
2021-12-11 10:17:10 +02:00
Pēteris Caune
96c1759c69
Switch from pytz.UTC to datetime.timezone.utc 2021-12-11 09:59:17 +02:00
Pēteris Caune
54298a8367
Remove "now" parameter in Check.get_status() signature
It was used only for tests in test_check_model,
and we can use mock.patch in the testcases instead.
2021-12-11 09:52:03 +02:00
Pēteris Caune
644ce928a7
Fix docstring 2021-12-10 13:42:40 +02:00
Pēteris Caune
cc3a402042
Remove site_scheme template tag, it was used in a single template 2021-12-10 13:27:20 +02:00
Pēteris Caune
edb8a765f0
Clean up test_notify_* test cases 2021-12-10 12:16:24 +02:00
Pēteris Caune
1da1a02be4
Add "The following checks are also down: ..." in Signal notifications 2021-12-10 11:30:48 +02:00
Pēteris Caune
5943c8017a
Improve formatting in pushover_message.html 2021-12-10 11:15:59 +02:00
Pēteris Caune
336c55e601
Implement "linemode" tag, improve formatting in telegram_message.html 2021-12-09 17:45:52 +02:00
Pēteris Caune
d317b8e38e
Add a size limit for the "The following checks are also down" section 2021-12-08 16:58:01 +02:00
Pēteris Caune
307dfbb99e
Add "The following checks are also down: ..." in Telegram notifications 2021-12-08 16:06:08 +02:00
Pēteris Caune
c7c48477df
Fix report templates to not show the "started" status 2021-12-08 09:41:49 +02:00
Pēteris Caune
10a8c48fc0
Upgrade Django 3.2.9 -> 3.2.10 2021-12-07 10:03:47 +02:00
Pēteris Caune
1da03f1662
Add HTTP POST example in PowerShell usage examples
Fixes #575
2021-12-02 15:42:15 +02:00
Pēteris Caune
1b513c0802
Add "View on site_name" link in Pushover notifications 2021-11-21 13:23:43 +02:00
Pēteris Caune
7fb64c8249
Implement Pushover emergency alert cancellation when check goes up 2021-11-21 13:15:35 +02:00
Pēteris Caune
8d9a6866a4
Update CHANGELOG for the v1.24.1 release 2021-11-10 13:51:29 +02:00
Pēteris Caune
cb78bb02d3
Update Dockerfile to install everything from piwheels on arm/v7 2021-11-10 13:38:05 +02:00
Pēteris Caune
6ce002c064
Add libffi-dev in Dockerfile to fix cffi build 2021-11-10 13:12:00 +02:00
Pēteris Caune
a0d3d40033
Update CHANGELOG for the v1.24.0 release 2021-11-10 12:29:47 +02:00
Pēteris Caune
9e36eb5fcc
Remove the "welcome" landing page
Redirect unauthenticated users to the sign in page
instead. Rationale:

- The content on the welcome page is what often belongs
  to a separate "marketing site". The marketing content
  is of no use on self-hosted instances, which typically
  have new signups disabled and are for internal use only
- (the real reason, let's be honest) a number of
  self-hosted instances are accessible over the public
  internet. Search engines index the nearly identical
  landing pages and see them as duplicated content.
2021-11-10 11:59:55 +02:00
Pēteris Caune
1299738f50
Add SIGTERM handling in sendreports 2021-11-07 11:05:10 +02:00
Pēteris Caune
bc2d127c27
Add SIGTERM handling in sendalerts 2021-11-06 19:54:41 +02:00
Pēteris Caune
4c79021f25
Update screenshots 2021-11-06 15:56:44 +02:00
Pēteris Caune
22503bc554
Update screenshots 2021-11-06 15:42:21 +02:00
Pēteris Caune
5cd3e62d94
Update screenshots 2021-11-06 15:38:24 +02:00
Pēteris Caune
71ba57e46a
Update screenshots 2021-11-06 15:30:45 +02:00
Pēteris Caune
3b4fe525f8
Update screenshots 2021-11-06 15:09:31 +02:00
Pēteris Caune
81b74b6a73
Upgrade fido2 0.9.1 -> 0.9.2 2021-11-06 11:18:57 +02:00
Pēteris Caune
e1b601928f
Fix button outlines in chrome 2021-11-04 16:10:50 +02:00
Pēteris Caune
7e08e3a938
Fix dark mode bug in the timezone dropdown 2021-11-04 16:04:17 +02:00
Pēteris Caune
9deb61b3e1
Hide the spinner buttons in cron dialog, grace time field 2021-11-04 15:58:16 +02:00
Pēteris Caune
c0af2ac7b9
Fix tests 2021-11-04 15:52:04 +02:00
Pēteris Caune
e0d2f36928
Improve period and grace controls, allow up to 365 day periods
Fixes: #281
2021-11-04 15:44:51 +02:00
Pēteris Caune
a127ab0f0c
Add handling for HTTP 502 response from Matrix server 2021-11-03 11:23:01 +02:00
Pēteris Caune
46646c78e9
Upgrade to Django==3.2.9 2021-11-03 10:36:46 +02:00
Pēteris Caune
cfd0bd2a6e
Change "Add Users from Other Teams" -> "Add Users from Other Projects" 2021-11-03 10:34:56 +02:00
Pēteris Caune
b77c54f665
Update Dockerfile to install apprise
Fixes: #581
2021-10-25 21:55:11 +03:00
Pēteris Caune
e1f51093f1
Implement automatic api_ping and api_notification pruning
cc: #556
2021-10-21 14:35:02 +03:00
Pēteris Caune
6bb0c77934
Replace backfillchannels with a data migration 2021-10-21 12:35:29 +03:00
Pēteris Caune
bcc7009437
Update channels.html to use Channel.last_notify
Channel.last_notify is the datetime of the most recent
notification sent via the channel. Channel.last_error is
the error message (blank if the delivery was successful).

In the integrations list, "Last Notification" column,
use these fields instead of looking up the most recent
Notification object. This saves some db queries,
and also fixes a subtle issue: if prunenotifications
cleans up all notifications for a given channel, the
"Last Notification" column would display "Never", which
would not be correct – not any more.
2021-10-21 11:42:13 +03:00
Pēteris Caune
33fb4a36ca
Add Channel.last_notify and a command to backfill it 2021-10-21 10:56:03 +03:00
Pēteris Caune
ba3f222f37
Fix a crash in hc.api.views.pause with an int in request body
The jsonify decorator parses request payload as JSON
and puts it in request.json. The payload would normally
be a complex object, but if a client sends, let's say,
a single integer, then request.json is a python int.

The authorize decorator looks for API key first in request
headers, then in request body. It expects the request
body to be a complex object.

This commit changes adds the following validation rule in
the jsonify decorator: if request body is not empty, it
*must* parse as JSON, and the root element of the parsed
document *must* be a dict.
2021-10-20 23:06:21 +03:00
Pēteris Caune
829a39f4cf
Fix hc.api.views.ping to handle non-utf8 data in request body
Fixes: #574
2021-10-19 19:19:46 +03:00
Pēteris Caune
38c480bab5
Update PowerShell example 2021-10-19 16:33:37 +03:00
Pēteris Caune
dc0ea1e8ce
Remove unused bits from the base email template 2021-10-18 18:58:39 +03:00
Pēteris Caune
d2c701fb77
Add {% spaceless %} tags to reduce uncompressed email size
If the email body is above a certain size, Gmail trims it
and displays "[Message clipped]  View entire message" at
the end. The spaceless tag is a quick fix to reduce
HTML size a bit and allow more table rows to fit before
clipping.
2021-10-18 17:47:01 +03:00
Pēteris Caune
dfc257ef22
Update hc.front.views.cron_preview to catch CronSimError explicitly
With a catch-all "except:" rule, we would swallow any unexpected
exceptions (ValueError, etc.) in cronsim. But we want to know
about them. cron_preview is a place where we can afford to crash, and generate a crash report.
2021-10-15 11:13:04 +03:00
Pēteris Caune
3f9f219a58
Remove hc.lib.cronsim, install it from PyPI 2021-10-15 11:05:39 +03:00
Pēteris Caune
55bf29c6a6
Remove debug statement 2021-10-14 17:50:38 +03:00
Pēteris Caune
f3b857ad82
Simplify the retry logic 2021-10-14 16:40:46 +03:00
Pēteris Caune
6158f9c539
Change outgoing webhook timeout to 10s, and change the retry logic
Previous retry logic was:
- max 3 tries
- every try times out after 5 seconds

The new retry logic is:
- max 3 tries
- every try times out after 10 seconds
- if the first two tries have used > 10 seconds, don't
  do the third try

cc: #569
2021-10-14 16:22:14 +03:00
Pēteris Caune
cee023063b
Fix JS regression in cron preview 2021-10-14 15:22:47 +03:00
Pēteris Caune
c5b170c086
Exclude migrations from coverage reports 2021-10-14 15:11:05 +03:00
Pēteris Caune
141d71d9fe
Switch from croniter to cronsim (vendored in hc.lib.cronsim) 2021-10-14 12:42:31 +03:00
Pēteris Caune
0056cbf058
Fix release dates in CHANGELOG 2021-10-13 12:27:23 +03:00
Pēteris Caune
30a3482d0e
Fix missing uwsgi dependencies in arm/v7 Docker image 2021-10-13 10:43:04 +03:00
Pēteris Caune
3e0ff1cf81
Update CHANGELOG for v1.23.0 release 2021-10-13 09:35:00 +03:00
Pēteris Caune
1b0f5e92f1
Update Dockerfile to use the piwheels.org repo on armhf
cc: #565, #568
2021-10-12 22:40:11 +03:00
Pēteris Caune
4ef23c7309
Remove linux/arm/v7 from publish_docker_image (it does not work)
Context: #568
2021-10-12 21:21:49 +03:00
Pēteris Caune
4306350df8
Add docker/setup-qemu-action@v1.0.1 2021-10-12 09:11:16 +03:00
Pēteris Caune
4d4730fefc
Add docker/setup-buildx-action@v1 2021-10-12 09:07:01 +03:00
Pēteris Caune
63aa63c124
Update publish_docker_image.yml to build ARM images 2021-10-12 09:03:01 +03:00
Pēteris Caune
129e1edf1e
Upgrade pytz 2021.1 -> 2021.3 2021-10-09 09:22:15 +03:00
Pēteris Caune
bee0512d80
Fix dark mode bug in button groups, remove button outlines in Chrome 2021-10-06 16:46:01 +03:00
Pēteris Caune
b8771b9eb8
Add uuid/slug switcher in the Details page 2021-10-06 16:12:28 +03:00
Pēteris Caune
5656515830
Add 'schemaVersion' field in the shields.io endpoint
Fixes: #566
2021-10-02 13:38:45 +03:00
Pēteris Caune
28d15d7bf2
Fix dark mode bug in the "Set Password" screen 2021-10-01 13:36:37 +03:00
Pēteris Caune
5fe4a60b30
Upgrade to jQuery 3.6.0 2021-10-01 13:12:20 +03:00
Pēteris Caune
148894bd9e
Upgrade to Bootstrap 3.4.1 2021-10-01 12:21:54 +03:00
Pēteris Caune
27da637e86
Fix Dockerfile to correctly build cryptography==35.0.0 on 32-bit arm
Fixes: #565

Also, split Dockerfile into two stages, so rust
and other build dependencies don't end up in the final image.

Note cryptography has binary wheels for various architectures,
but unfortunately not for 32-bit arm. And, starting from v35.0.0,
cryptography requires rust to build from source.
2021-10-01 09:53:48 +03:00
Jake Howard
cd4cc1f2d9 Pin debian version to Buster
Bullseye isn't currently supported on the Pi 4
2021-10-01 08:47:05 +03:00
Jake Howard
7a5afc26be Install libpq-dev so psycopg2 can build correctly 2021-10-01 08:47:05 +03:00
Jake Howard
25da46f5b6 Use slim version of docker container
This massively reduces the size of the final container
2021-10-01 08:47:05 +03:00
Pēteris Caune
b5f7ec1324
Add "Ping Key Required" dialog
In the Details page, if the user click "Ping Now",
and the project is using {ping-key}/{slug} URLs,
but the ping key is not set, then show a
"Ping Key Required" message instead of trying to ping
and invalid URL.
2021-09-24 16:25:24 +03:00
Pēteris Caune
1b0d0eac6a
Enable retry for SMTPDataError 2021-09-23 15:20:42 +03:00
Pēteris Caune
4618108046
Make "or" bold
cc: #547
2021-09-20 08:44:33 +03:00
Pēteris Caune
2adf4b6aee
Add "(not unique)" note next to ambiguous ping URLs 2021-09-17 15:09:31 +03:00
Pēteris Caune
7f766e4f48
Improve docs 2021-09-16 09:16:29 +03:00
Pēteris Caune
9299ee4516
Improve response code descriptions in Ping API docs 2021-09-15 13:30:19 +03:00
Pēteris Caune
027920ef2b
Fix ping handler to return "not found" in 404 body 2021-09-15 13:29:54 +03:00
Pēteris Caune
be66ec73e5
Add the slug-based endpoints in Ping API docs 2021-09-15 11:39:05 +03:00
Pēteris Caune
6e3a1c790d
Fix the ping handler to reject status codes > 255 2021-09-15 11:36:15 +03:00
Pēteris Caune
5905560583
Add content about the new slug-based ping URLs in docs/introduction.md 2021-09-14 16:40:29 +03:00
Pēteris Caune
0134077bd0
Simplify JS 2021-09-14 09:56:12 +03:00
Pēteris Caune
d48b95c902
Improve tests to cover @csrf_exempt usage 2021-09-14 09:54:18 +03:00
Pēteris Caune
f8131741ef
Fix minor API inconsistencies
1. Drop API support for GET, DELETE requests with a request body.
Healthchecks had an undocumented quirk where you could authenticate a
GET or DELETE request by putting a '{"api_key":"..."}' in request body.
This commit removes this feature.

Note: POST requests can still authenticate either by sending
a X-Api-Key header, or by putting a "api_key" key in request body.
GET and DELETE requests can now only authenticate with the
request header.

2. Add missing @csrf_exempt annotations in API views
When client sends a HTTP POST request to a GET-only endpoint,
the server is supposed to respond with "405 Method Not Allowed".
Due to CSRF checking, a couple endpoints were responding with
"403 Forbidden" instead. Adding @csrf_exempt annotations fixes
the problem.
2021-09-10 22:49:12 +03:00
Pēteris Caune
51f996ab4b
Fix /api/v1/badges/ to handle requests with missing X-Api-Key header 2021-09-10 17:52:03 +03:00
Pēteris Caune
66af88145a
Add "if read-write" conditionals for modals in project.html 2021-09-09 15:13:04 +03:00
Pēteris Caune
3dfdbc09ca
Add ability to create/revoke individual keys 2021-09-09 14:55:17 +03:00
Pēteris Caune
688aa5b3c3
Implement hc.api.views.ping_by_slug 2021-09-09 11:43:25 +03:00
Pēteris Caune
9517035501
Fix N+1 queries issue in "My Checks" and clean up Check.url() 2021-09-09 10:56:23 +03:00
Pēteris Caune
250a8580ae
Fix tests 2021-09-09 10:08:10 +03:00
Pēteris Caune
5b9008e321
Implement alternative ping URLs, WIP 2021-09-09 09:32:10 +03:00
Pēteris Caune
5fafc871dd
Fix unwanted text wrapping in the URL cell 2021-08-26 17:24:04 +03:00
Pēteris Caune
3f078e6cda
Optimize HTML in the "list of checks" page
In a project with ~300 checks,

* HTML size (uncompressed) before: 772KiB
* HTML size (uncompressed) after: 703KiB
2021-08-26 17:11:56 +03:00
Pēteris Caune
be641aea96
Add tests for LINE and Trello transports 2021-08-26 15:54:05 +03:00
Pēteris Caune
3e9e13104e
Remove unused code 2021-08-26 14:57:34 +03:00
Pēteris Caune
f9c470ef59
Clean up redundant json.loads() in the Channel class 2021-08-26 13:59:18 +03:00
Pēteris Caune
2c662dac20
Fix HTML validation issues 2021-08-26 12:02:09 +03:00
Pēteris Caune
6f5a22fd98
Improve up/down flag validation
In SMS, Signal and WhatsApp forms, reject the form if
user unchecks both "alert when check goes DOWN" and
"alert when check goes UP".
2021-08-26 10:35:05 +03:00
Pēteris Caune
8541ec59ca
Add ability to edit existing WhatsApp integrations 2021-08-26 10:17:01 +03:00
Pēteris Caune
5af09ed4dd
Add ability to edit existing Signal integrations 2021-08-26 10:01:09 +03:00
Pēteris Caune
3807c200ce
Add ability to edit existing SMS integrations 2021-08-26 09:42:35 +03:00
Pēteris Caune
c2e00f2105
Fix assign_all_checks() usage, add tests for it 2021-08-25 23:45:41 +03:00
Pēteris Caune
a27652d762
Combine the add_webhook and edit_webhook views 2021-08-25 20:29:46 +03:00
Pēteris Caune
2a9a544ddf
Add ability to edit existing email integrations 2021-08-25 18:04:54 +03:00
Yann Papouin
b5fd5cfaed Fix db-data volume in docker-compose.yml
Use data folder otherwise a second volume is created that becomes orphaned after a docker-compose down.
2021-08-25 16:28:56 +03:00
Pēteris Caune
252acd0a83
Fix dark mode bug 2021-08-23 10:57:03 +03:00
Pēteris Caune
ebac101209
Remove unused class attributes 2021-08-23 10:53:45 +03:00
Pēteris Caune
c7317a87fc
Improve /api/v1/badges/ docs 2021-08-19 13:00:42 +03:00
Pēteris Caune
28506deb74
Improve /api/v1/badges/ docs
cc: #552
2021-08-19 12:42:24 +03:00
Pēteris Caune
f424d14d83
Remove obsolete CSS vendor prefixes 2021-08-19 11:51:43 +03:00
Pēteris Caune
6b59bbe2bc
Remove unused CSS 2021-08-19 11:30:58 +03:00
Pēteris Caune
6ef1a9a01d
Remove obsolete CSS 2021-08-19 11:21:46 +03:00
Pēteris Caune
98eb7cc14a
Add /api/v1/badges/ endpoint
cc: #552
2021-08-18 17:47:57 +03:00
Pēteris Caune
c2ee8222e4
Update the "experimental" note 2021-08-18 17:00:01 +03:00
Pēteris Caune
eb25cde241
Simplify jumbotron CSS 2021-08-18 16:36:09 +03:00
Pēteris Caune
a2fe5653c0
Improve copy in the "Change Schedule" dialog
cc: #547
2021-08-18 14:02:32 +03:00
Pēteris Caune
1247cc4ea7
Fix a crash during login when user's profile does not exist
Fixes: #77
2021-08-18 10:32:10 +03:00
Pēteris Caune
f7dd16abca
Remove site_root from template context, it's never used 2021-08-13 15:03:09 +03:00
Pēteris Caune
642d436ae9
Add absolute_site_logo_url template tag
This commit adds a {% absolute_site_logo_url %} template tag.
The tag emits an absolute url pointing to either
SITE_LOGO_URL or to the fallback picture.

The tag is used in base email template, in slack message
template, and in "Add MS Teams" page.

This commit also fixes a couple instances where absolute URLs
were constructed like so:

    {% site_root %}/docs/

This would result in incorrect links if Healthchecks is not
running at webserver's root. The correct way is:

    {% site_root %}{% url 'hc-docs' %}

Finally, this commit removes stuff/logo.svg and
stuff/logo-full.svg. Selfhosted sites should not use the
official Healthchecks.io logos, so no point keeping them around
there.
2021-08-13 14:57:15 +03:00
Pēteris Caune
484c0befbc
Fix email template to use SITE_LOGO_URL (with img/logo.png fallback)
Fixes: #550
2021-08-13 14:21:20 +03:00
Pēteris Caune
3901a2825b
Replace str(exc) with exc.message to make CodeQL happy 2021-08-12 17:27:43 +03:00
Pēteris Caune
8109529329
Render docs 2021-08-12 16:53:08 +03:00
Jan Dittrich
289afd5683 Fix grammar issue in docs 2021-08-12 16:40:15 +03:00
Pēteris Caune
4756527185
Improve docs 2021-08-12 13:07:19 +03:00
Pēteris Caune
1248dd22ea
Add a note about keyword filtering in Docs / Email 2021-08-12 11:55:55 +03:00
Pēteris Caune
234b681df8
Improve docs, addd "Concepts" section
cc: #547
2021-08-12 09:30:45 +03:00
Pēteris Caune
c196dc16d7
Fix latin-1 handling in webhook header values 2021-08-10 21:14:05 +03:00
Pēteris Caune
b43612806f
Fix dark mode bug in selectpicker widgets 2021-08-10 16:47:47 +03:00
Pēteris Caune
544ec7ea69
Add handling for non-latin-1 characters in webhook headers 2021-08-10 10:36:58 +03:00
Pēteris Caune
78113e1aea
Improve "Grace Time" description in docs
cc: #547
2021-08-09 17:52:20 +03:00
Pēteris Caune
74f56a5501
Improve the note about start signals and alerting logic
cc: #547
2021-08-06 16:10:06 +03:00
Pēteris Caune
2a9bc42dd4
Update Changelog for v1.22.0 release 2021-08-06 14:27:15 +03:00
Pēteris Caune
af7e8fc949
Fix the login view to handle already authenticated users
If an already authenticated user visits /accounts/login/,
Healthchecks will now redirect them to their dashboard
instead of showing the login form.
2021-08-06 13:54:12 +03:00
Pēteris Caune
7252f2f101
Fix _allow_redirect function to reject absolute URLs
This fixes a security issue:
- attacker can crafts a redirect URL to an external site
- attacker gets victim to click on it
- victim logs in
- after login, Healthchecks redirects victim to the external site

The _allow_redirect function now additionally
requires the redirect URL is relative (has no scheme or domain).
2021-08-06 13:34:40 +03:00
Pēteris Caune
f85aec225d
Fix redirect-after-login when using TOTP
If user has both WebAuthn and TOTP configured,
when logging in, they will be asked to choose between
"Use security keys" and "Use authenticator app".
The "Use authenticator app" is a link to a different
page (/accounts/login/two_factor/totp/). This commit makes
sure the ?next= query parameter is preserved when navigating
to that page.

For reference, the ?next= query parameter is the URL we should
redirect to after a successful login. Use case:
User is logged out. They click on a bookmarked "Check Details"
link. They get redirected to the login form. After
entering username & password and completing 2FA,
they get redirected to the "Check Details" page they
originally wanted to visit.
2021-08-06 12:09:41 +03:00
Pēteris Caune
e6427995b7
Add Whitenoise and improve README
Fixes: #548
2021-08-05 18:06:47 +03:00
Pēteris Caune
ca3afa33f9
Add auth method selection step
This has dual purpose:

* if user has both WebAuthn and TOTP set up, they can choose
  between the two as equal options.
* we initiate WebAuthn flow only after an explicit user action
  (button press). This may help with authentication failures
  on recent MacOS, iOS and iPadOS versions [1]

[1] https://support.yubico.com/hc/en-us/articles/360022004600-No-reaction-when-using-WebAuthn-on-macOS-iOS-and-iPadOS
2021-08-05 16:27:06 +03:00
Pēteris Caune
f3af13654e
Refactor email sending functions to allow customization
For example, if we need to use a custom From: address,
we can now do:

    m = make_message("template-name", recipient, ctx)
    m.from_email = "...."  # customize here
    send(m)
2021-08-05 14:13:25 +03:00
Pēteris Caune
fca600659d
Improve hc.lib.emails.send()
- add optional `from_email` argument
- add test cases that exercise the retry loop
2021-08-03 19:02:25 +03:00
Pēteris Caune
c3d458f6f0
Fix the unsubscribe_reports view to handle already deleted users 2021-08-02 12:51:05 +03:00
Pēteris Caune
934099510d
Upgrade to Django 3.2.6 2021-08-02 10:51:31 +03:00
Pēteris Caune
d60d8a43b6
Add protection against TOTP code reuse 2021-07-30 18:17:21 +03:00
Pēteris Caune
8ed5e93cd2
Add rate limiting for TOTP auth attempts 2021-07-30 17:30:28 +03:00
Pēteris Caune
222722569e
Add support for 2FA using TOTP
Fixes: #354
2021-07-30 16:43:23 +03:00
Pēteris Caune
0d9d094882
Update docs with the Manager role 2021-07-26 15:55:08 +03:00
Pēteris Caune
dfa6f404e6
Improve the "Invite a Team Member" dialog 2021-07-26 15:21:45 +03:00
Pēteris Caune
bbd2786e0f
Optimize queries and fix team member sorting 2021-07-26 14:27:03 +03:00
Pēteris Caune
74427ba3f1
Fix wording in the "Team size limit reached" message 2021-07-26 13:12:06 +03:00
Pēteris Caune
e1c3beb4e9
Add test cases for manager operations 2021-07-26 13:07:05 +03:00
Pēteris Caune
4f83f8c06b
Fix a 403 when transferring a project to a read-only team member 2021-07-26 12:50:43 +03:00
swoga
9640d2242f feat: add manager role 2021-07-26 12:26:06 +03:00
Pēteris Caune
ce9ff3ac42
Add a migration to remove Member.rw 2021-07-22 17:40:08 +03:00
Pēteris Caune
cb799dbd29
Remove the Member.rw field (superseded by Member.role) 2021-07-22 17:28:38 +03:00
Pēteris Caune
936a5213f8
Switch from Member.rw to Member.role as the source of truth 2021-07-22 17:16:52 +03:00
Pēteris Caune
d19cb8c681
Add a data migration to populate Member.role 2021-07-22 16:28:02 +03:00
Pēteris Caune
5230dbb425
Add Member.role field 2021-07-22 16:13:41 +03:00
Pēteris Caune
e46000ecdf
Add admin action to log in as any user 2021-07-20 11:16:12 +03:00
Pēteris Caune
79dc4d2e7a
Fix html structure in the signup dialog 2021-07-16 16:45:26 +03:00
Pēteris Caune
02cdbb9222
Fix page structure, update copy 2021-07-16 16:36:32 +03:00
Pēteris Caune
94c5ea3e13
Fix page structure 2021-07-16 16:34:15 +03:00
Pēteris Caune
2382bf6722
Add SITE_LOGO_URL setting
Fixes: #323
2021-07-16 15:30:34 +03:00
Pēteris Caune
dd88924660
Fix dark mode styling issues in Cron Syntax Cheatsheet 2021-07-16 12:25:16 +03:00
Pēteris Caune
b75b062559
Remove unsigned token support in hc.front.views.unsubscribe_email 2021-07-16 12:01:44 +03:00
Pēteris Caune
e186d039fc
Upgrade to psycopg2==2.9.1 and requests==2.26.0 2021-07-16 11:24:45 +03:00
Pēteris Caune
2271a4dbb0
Remove glyphicons (unused) 2021-07-07 15:23:35 +03:00
Pēteris Caune
99bb71c920
Use multicolor channel icons for better appearance in the dark mode 2021-07-07 15:23:02 +03:00
Pēteris Caune
5c54afadb5
Fix contrast in "Add Integration" pages, step circles 2021-07-05 18:03:37 +03:00
Pēteris Caune
c94e39c9d3
Add CSS to invert Matrix and Mattermost logos in dark mode 2021-07-05 17:55:13 +03:00
Pēteris Caune
92a9910092
Improve logos for the dark mode 2021-07-05 17:31:10 +03:00
Pēteris Caune
0e7252d8fa
Update Discord logo 2021-07-05 17:16:29 +03:00
Pēteris Caune
5a4c06ffae
Update CHANGELOG for v1.21.0 release 2021-07-02 16:52:24 +03:00
Pēteris Caune
92ef81c0a5
Add workflow_dispatch for testing 2021-07-02 16:47:06 +03:00
Pēteris Caune
83eb10b99e
Rename secret names in publish_docker_image.yml 2021-07-02 16:40:20 +03:00
Pēteris Caune
ec56ceae8f
Merge branch 'master' of https://github.com/mirobertod/healthchecks into mirobertod-master 2021-07-02 16:38:30 +03:00
Pēteris Caune
d243f502d3
Fix off-by-one-month error in monthly reports, downtime columns
Fixes: #539
2021-07-02 15:22:51 +03:00
Roberto Dedoro
760c3757f3
update docker image name 2021-07-02 13:23:38 +08:00
Roberto Dedoro
c2d8166e74
Create publish_docker_image.yml 2021-07-02 13:17:36 +08:00
Pēteris Caune
61a8a8de26
Remove Profile.reports_allowed (obsolete)
It is obsoleted by Profile.reports
2021-06-29 14:38:06 +03:00
swoga
b70e2c9a25 feat: treat failure before success 2021-06-29 14:05:56 +03:00
Pēteris Caune
8a154cbaf5
Expose Credentials model in Django admin
This is to help troubleshoot 2FA issues without
running manual SQL queries.
2021-06-29 10:46:08 +03:00
Pēteris Caune
52e1d420b5
Add nicoandrade/healthchecks-front to resources
Fixes: #536
2021-06-28 13:41:57 +03:00
Pēteris Caune
1c02d1ff87
Fix dark mode bugs 2021-06-21 15:57:50 +03:00
Pēteris Caune
c75d17618c
Fix renaming mistake 2021-06-21 15:19:03 +03:00
Pēteris Caune
93a881d0ba
Move color overrides to variables.less where possible 2021-06-21 15:12:06 +03:00
Pēteris Caune
fdfd988c5c
Fix dark mode bugs 2021-06-21 12:14:03 +03:00
Pēteris Caune
6e01af3327
Fix dark mode bugs 2021-06-21 11:42:18 +03:00
Pēteris Caune
2d20f439dd
Remove PagerDuty Connect
PagerDuty Connect is deprecated and will be discontinued.
It is replaced by PagerDuty Simple Install Flow (see
README for setup instructions).
2021-06-21 10:44:21 +03:00
Pēteris Caune
059a855b3f
Fix more contrast issues 2021-06-18 17:07:27 +03:00
Pēteris Caune
b185a28676
Fix contrast issues 2021-06-18 15:28:35 +03:00
Pēteris Caune
6c10980889
Add Account Settings > Appearance page 2021-06-18 13:51:07 +03:00
Pēteris Caune
13334d2ab0
Implement explicit light/dark mode selection (WIP) 2021-06-18 12:27:43 +03:00
Pēteris Caune
4f72c9e204
Fix dark mode CSS for tabs take 2 2021-06-16 16:00:24 +03:00
Pēteris Caune
dd104ff672
Fix dark mode CSS for tabs 2021-06-16 15:57:33 +03:00
Pēteris Caune
c5229d6505
Add CSS for dark mode 2021-06-16 15:23:34 +03:00
Pēteris Caune
fd7ab5e767
Implement PagerDuty Simple Install Flow 2021-06-16 14:18:32 +03:00
Pēteris Caune
2cd2bfed6f
Update Django to 3.2.4 2021-06-03 08:03:10 +03:00
Pēteris Caune
a0cd2c63e9
Update report templates for weekly reports 2021-05-26 09:48:23 +03:00
Pēteris Caune
8ce09ab9e5
Widen report time window to 9AM - 11AM 2021-05-24 15:17:27 +03:00
Pēteris Caune
548b2ac33c
Update the signup form to collect browser's timezone 2021-05-24 14:38:12 +03:00
Pēteris Caune
6094bca241
Improve wording 2021-05-24 14:13:43 +03:00
Pēteris Caune
fa5dd8b45a
Add mitigation for bad tz values 2021-05-24 14:04:05 +03:00
Pēteris Caune
df44ee58c0
Add an option for weekly reports (in addition to monthly) 2021-05-24 13:44:34 +03:00
Pēteris Caune
03a538c5e2
Add Profile.reports field
This is in preparation of adding an option for weekly
reports (#407)
2021-05-24 11:20:28 +03:00
Pēteris Caune
5ca7262164
Update Zulip icon in the icon font 2021-05-24 08:59:28 +03:00
Puneeth Chaganti
82dc6844ae Update Zulip logo 2021-05-24 08:21:45 +03:00
Pēteris Caune
ac83bf8896 Fix attribution in .po header 2021-05-21 14:19:42 +03:00
Pēteris Caune
32ca8b3420 Move under LC_MESSAGES and fix template syntax 2021-05-21 14:19:42 +03:00
Richard Lippmann
3e207e538a German translation of django.po and django.mo 2021-05-21 14:19:42 +03:00
Pēteris Caune
e91441d814
Add fallback for legacy sms values 2021-05-21 13:05:37 +03:00
Pēteris Caune
855d188981
Add support for "... is UP" SMS notifications
Fixes: #512
2021-05-21 12:57:23 +03:00
Pēteris Caune
e090aa5403
Improve the handling of unknown email addresses in the Sign In form 2021-05-12 13:49:56 +03:00
Pēteris Caune
94416c90dc
Update pytz to 2021.1 2021-05-12 13:00:10 +03:00
Pēteris Caune
ae4487b6c3
Update to Django 3.2.2 2021-05-06 11:07:51 +03:00
Pēteris Caune
64f2e86051
Increase "Success / Failure Keywords" field lengths to 200 2021-05-06 11:00:36 +03:00
Pēteris Caune
aa71629ffc
Add a note in README about per-user ping log limit 2021-05-05 20:21:59 +03:00
Pēteris Caune
599f481d58
Set maxlength on input fields in the Filtering Rules dialog 2021-05-05 17:37:47 +03:00
Pēteris Caune
a36c326e32
Update Django version to 3.2.1 2021-05-05 10:30:09 +03:00
Pēteris Caune
e2b96d9bd8
Update CHANGELOG for v1.20.0 release 2021-04-22 13:03:07 +03:00
Pēteris Caune
6ed983cdd5
Improve copy in "Profile" > "Email and Password" section
When an account has a password, replace "Set Password"
button's label with "Change Password"
2021-04-22 10:31:35 +03:00
Pēteris Caune
6d2c67338c
Improve the ALLOWED_HOSTS description
Fixes: #499
2021-04-21 17:59:10 +03:00
Pēteris Caune
6c8b6a2a19
Remove functools.cached_property usage
Cannot use functools.cached_property, as it was added in Py 3.8,
but we support 3.6+
2021-04-14 16:29:28 +03:00
Pēteris Caune
738a648407
Improve project sorting in the "My Projects" page
Primary sort key: projects with overall_status=down go first
Secondary sort key: project's name
2021-04-14 16:18:43 +03:00
Pēteris Caune
4587b45cab
Add more tests for hc.api.views.create_check 2021-04-14 12:21:58 +03:00
Pēteris Caune
2831e5d7c1
Add a test case for filtering flips by timestamp 2021-04-14 12:00:32 +03:00
Pēteris Caune
742af7bfd8
Remove unused return statement 2021-04-14 11:54:43 +03:00
Pēteris Caune
78652b5659
Upgrade Django version to 3.2 2021-04-07 11:39:11 +03:00
Pēteris Caune
67d11e8d40
Fix the month boundary calculation in monthly reports
Fixes: #497
2021-04-02 13:49:55 +03:00
Pēteris Caune
aa7ef5e9bb
Upgrade croniter to 1.0.8 2021-03-15 14:13:27 +02:00
Pēteris Caune
68b1d5bb8b
Fix the "Email Reports" screen to clear Profile.next_nag_date 2021-03-15 13:06:57 +02:00
Pēteris Caune
1d6b75d5dc
Move Profile *model* tests to test_profile_model 2021-03-15 12:56:07 +02:00
Pēteris Caune
05db43f95d
Fix the pause action to clear Profile.next_nag_date if all checks up 2021-03-15 12:52:35 +02:00
Pēteris Caune
7ba5fcbb71
Fix sendalerts to clear Profile.next_nag_date if all checks up
Profile.next_nag_date tracks when the next hourly/daily reminder
should be sent. Normally, sendalerts sets this field when
a check goes down, and sendreports clears it out whenever
it is about to send a reminder but realizes all checks are up.

The problem: sendalerts can set next_nag_date to a non-null
value, but it does not clear it out when all checks are up.
This can result in a hourly/daily reminder being sent out
at the wrong time. Specific example, assuming hourly reminders:

13:00: Check A goes down. next_nag_date gets set to 14:00.
13:05: Check A goes up. next_nag_date remains set to 14:00.
13:55: Check B goes down. next_nag_date remains set to 14:00.
14:00: Healthchecks sends a hourly reminder, just 5 minutes
       after Check B going down. It should have sent the reminder
       at 13:55 + 1 hour = 14:55

The fix: sendalerts can now both set and clear the next_nag_date
field. The main changes are in Project.update_next_nag_dates()
and in Profile.update_next_nag_date(). With the fix:

13:00: Check A goes down. next_nag_date gets set to 14:00.
13:05: Check A goes up. next_nag_date gets set to null.
13:55: Check B goes down. next_nag_date gets set to 14:55.
14:55: Healthchecks sends a hourly reminder.
2021-03-15 12:34:39 +02:00
Pēteris Caune
502ff7567e
Fix md formatting in the REMOTE_USER_HEADER section 2021-03-11 14:48:07 +02:00
Mathias Chevalier
a101b1de91 [DOCKER] Fix failing build on armhf, due to cryptography lib trying to build rust 2021-03-11 14:08:36 +02:00
Pēteris Caune
57336187a7
Fix HTML email preview in the checks list 2021-03-10 12:29:20 +02:00
Pēteris Caune
ad886fe157 Add more content in CONTRIBUTING 2021-03-08 17:16:24 +02:00
Ivan Smirnov
0c58a9b9ee Add CONTRIBUTING.md
- Add requirements for debian hosts
- Add section that explains how to add new docs.
2021-03-08 17:16:24 +02:00
Pēteris Caune
e2576607f5
Remove the width attribute to preserve logo aspect ratio
See also: #483
2021-03-08 15:04:43 +02:00
cocide
e66725e23f Added formatting on ping.body in emails
Not all email clients are formatting the `ping.body` contents uniformly. Even using different applications from the same email provider results in a different display of the `ping.body` contents. There are two basic issues:
* Not all email clients are honoring the fixed-width font that should be used inside `<pre>` tags. Using fixed-width font is listed in the definition on https://www.w3schools.com/tags/tag_pre.asp
* Not all email clients are displaying the text with a 1em line height. This was a recent change to the healthchecks WebUI in 9fd9c8e4ef but is not part of the definition of the `<pre>` tag. I'd like to add this to the emails to make Healthchecks more uniform between the website and the email notification.

Gmail Webmail:
- [x] Is using fixed-width font
- [ ] Line height is set by the webmail client to 18px
Gmail Android App:
- [ ] Text is not fixed-width
- [ ] Line height has extra padding

ProtonMail Webmail:
- [x] Is using fixed-width font
- [x] Line height is correct
ProtonMail Android:
- [ ] Text is not fixed width
- [ ] Line height has extra padding

The testing I performed is not extensive, but it does show how multiple clients are displaying the contents differently. To make the display of the `ping.body` more uniform I'd like to add a bit of formatting information to the `<pre>` tag.
2021-03-08 12:38:48 +02:00
cocide
9fd9c8e4ef fix line height on request body
The line height in the ping request body is being set by the bootstrap.css body tag to 1.42857143. The additional line height makes request bodies with unicode characters have spacing between lines that shouldn't be there.
2021-03-04 17:49:55 +02:00
Pēteris Caune
2bfea987e9
Replace details_url with cloaked_url in email and chat notifications 2021-03-04 16:55:05 +02:00
Pēteris Caune
5321f772fe
Add a link to check's details page in Slack notifications
Fixes: #486
2021-03-04 15:51:35 +02:00
Laura Hausmann
448721e916 Add colored emojis to telegram messages
so you can see whether it's up or down at first glance
2021-03-04 12:21:28 +02:00
Pēteris Caune
1d62176f34
Remove non-standard "zoom: 1" CSS property 2021-02-27 09:04:49 +02:00
Pēteris Caune
d4fc314696
Set iframe's charset to utf8 2021-02-26 16:36:45 +02:00
Pēteris Caune
46bc7d8306
Improve HTML email display in the "Ping Details" dialog 2021-02-26 16:25:39 +02:00
Pēteris Caune
2a63d24812
Add a "Subject" field in the "Ping Details" dialog 2021-02-26 11:19:44 +02:00
Pēteris Caune
1bc89f0d5d
Implement email body decoding in the "Ping Details" dialog 2021-02-23 17:34:33 +02:00
Pēteris Caune
18b39a5a79
Bump django to 3.1.7 2021-02-22 10:13:49 +02:00
Pēteris Caune
44a677f327
Fix hc.api.views.notification_status to always return 200
If the notification does not exist, or is more than a hour
old, return HTTP 200 (instead of 400 or 404) so the other
party doesn't retry over and over again.
2021-02-09 14:25:26 +02:00
Pēteris Caune
1e84cac37d
Relax cron expression validation
Accept all expressions that croniter accepts.
If cron-descriptor throws an exception, don't show the
description to the user.
2021-02-09 11:34:53 +02:00
Pēteris Caune
f06616a934
Add Python 3.9 to the testing matrix 2021-02-05 09:33:20 +02:00
Pēteris Caune
6cd3f0e35a
Upgrade psycopg2 2.8.4 -> 2.8.6 2021-02-05 09:22:30 +02:00
Pēteris Caune
68e19c938e
Upgrade requests version 2.23.0 -> 2.25.1 2021-02-05 08:51:19 +02:00
Pēteris Caune
438ae0264e
Pin fido2 version 2021-02-05 08:47:37 +02:00
Pēteris Caune
474d782869
Rename VictorOps -> Splunk On-Call 2021-02-03 16:23:15 +02:00
Pēteris Caune
c1f433bb71
Rename VictorOps -> Splunk On-Call 2021-02-03 16:09:24 +02:00
Pēteris Caune
5979204691
Fix downtime summary to handle months when the check didn't exist
Fixes: #472
2021-02-03 14:27:06 +02:00
Pēteris Caune
0a0b48a3fe
Update CHANGELOG for v1.19.0 release 2021-02-03 10:57:39 +02:00
Pēteris Caune
b788c7e4f5
Add Healthchecks version in the site footer 2021-02-03 10:56:50 +02:00
Pēteris Caune
67560c96e1
Change icon CSS class prefix to work around Fanboy's filter list
Problem: if you use uBlock Origin, and enable the
"Fanboy's Social" filter list, Healthchecks does not show
Telegram or WhatsApp icons. This is because the filter list
contains "##.icon-telegram" and "##.icon-whatsapp" entries.

This commit changes the CSS class prefix to "ic-". So we're
now using icon classes like "ic-telegram" and "ic-whatsapp".

As a bonus, we save 2 bytes in HTML per displayed icon :-)
2021-02-03 10:44:35 +02:00
Pēteris Caune
dc9fcfa0ab
Fix a grid misalignment in the welcome page 2021-02-03 10:05:06 +02:00
Pēteris Caune
65ace8238a
Add the ZULIP_ENABLED setting 2021-02-03 09:11:32 +02:00
Pēteris Caune
e2c90c05b8
Add the VICTOROPS_ENABLED setting 2021-02-03 09:00:28 +02:00
Pēteris Caune
205f1ccce6
Upgrade croniter to 1.0.6 2021-02-02 08:50:25 +02:00
snyk-bot
a5ea8a03c6 fix: requirements.txt to reduce vulnerabilities
The following vulnerabilities are fixed by pinning transitive dependencies:
- https://snyk.io/vuln/SNYK-PYTHON-DJANGO-1066259
2021-02-02 08:45:14 +02:00
Pēteris Caune
238d0b8ff1
Upgrade croniter to 1.0.5 2021-01-29 16:29:50 +02:00
Pēteris Caune
8811640d45
Add the SPIKE_ENABLED setting 2021-01-29 15:21:38 +02:00
Pēteris Caune
725be65bdd
Add the PROMETHEUS_ENABLED setting 2021-01-29 15:05:42 +02:00
Pēteris Caune
419d96da7a
Add the PAGERTREE_ENABLED setting 2021-01-29 14:21:02 +02:00
Pēteris Caune
28150e85fa
Add the PD_ENABLED setting 2021-01-29 14:06:40 +02:00
Pēteris Caune
8d5890d883
Add the OPSGENIE_ENABLED setting, rename OpsGenie -> Opsgenie 2021-01-29 13:47:13 +02:00
Pēteris Caune
5f31b8b873
Add the MSTEAMS_ENABLED setting 2021-01-29 13:20:44 +02:00
Pēteris Caune
6c3debaf11
Add the MATTERMOST_ENABLED setting 2021-01-29 12:36:47 +02:00
Pēteris Caune
52435a9a0c
Add the SLACK_ENABLED setting 2021-01-29 11:59:33 +02:00
Pēteris Caune
67ff8a9bee
Add the WEBHOOKS_ENABLED setting 2021-01-29 11:16:11 +02:00
Pēteris Caune
45078e6566
Set the SECRET_KEY default value back to "---"
Previously, I had changed the default value to "", to force
users to set the SECRET_KEY value (the app refuses to start
if SECRET_KEY is empty).

The problem with that is, out of the box, with the default
configuration, the tests also don't run and complain about the
empty SECRET_KEY.

So, a compromise: revert back to the default value "---".
At runtime, if SECRET_KEY has the default value, show a  warning
at the top of every page.
2021-01-28 15:38:14 +02:00
Pēteris Caune
dc39831aef
Reorder integrations in settings.py in A-Z order 2021-01-28 15:08:53 +02:00
Pēteris Caune
59ebcb963f
Remove the "Configuration" section, link to docs instead 2021-01-28 14:32:23 +02:00
Pēteris Caune
4e480cac57
Update instructions in docker/README.md 2021-01-28 14:18:06 +02:00
Pēteris Caune
c2bb4b31b5
Add rate limiting for Pushover notifications 2021-01-28 14:07:39 +02:00
Pēteris Caune
ae976a38b6
Fix a crash when adding an integration for an empty Trello account 2021-01-28 12:57:08 +02:00
Pēteris Caune
b9997137a6
Bump croniter version to 1.0.2 2021-01-27 09:46:33 +02:00
Pēteris Caune
98b1e13aa1
Update the Docker Compose sample to use an .env file 2021-01-26 14:00:54 +02:00
Pēteris Caune
168f8b0bc6
Fix alphabetic order 2021-01-26 14:00:23 +02:00
msansen1
5ee0ef6381
Adding french and gitignore file
revert gitignore file

fix remove a space

fix french locale

Add .mo for the French translations
2021-01-26 11:30:26 +02:00
Pēteris Caune
1419da460e
Add id attributes to headings in the "Server Configuration" page 2021-01-23 14:30:38 +02:00
Pēteris Caune
35e6d41793
Add README 2021-01-21 17:43:28 +02:00
Pēteris Caune
a763fa1de3
Fix DEFAULT_FROM_EMAIL 2021-01-21 17:36:11 +02:00
Pēteris Caune
98439623c5
Add experimental Dockerfile and docker-compose.yml 2021-01-21 17:32:58 +02:00
Pēteris Caune
601d8fac94
Remove the warning about a missing local_settings.py 2021-01-21 17:32:25 +02:00
Pēteris Caune
376d80afd4
Add more content from README 2021-01-21 13:57:55 +02:00
Pēteris Caune
7e6afba8bd
Fix CI: set SECRET_KEY 2021-01-21 11:38:52 +02:00
Pēteris Caune
b7c769fc0e
Add a section in Docs about running self-hosted instances
Fixes: #467
2021-01-21 11:35:09 +02:00
Pēteris Caune
fbefcbc0ed
Update apprise tests to skip if apprise is not installed 2021-01-19 13:57:55 +02:00
Pēteris Caune
94abe0fbb5
Update intro and dev setup steps in README 2021-01-19 13:19:46 +02:00
Shea Polansky
5540fc2c6d Update README.txt to include more dev setup steps 2021-01-19 12:36:08 +02:00
Pēteris Caune
d45dc2f6a3
Change Zulip onboarding, ask for the zuliprc file
Fixes: #202
2021-01-19 11:04:38 +02:00
Pēteris Caune
9a0888aacd
Update sendalerts to log per-notification send times
To send notifications, sendalerts calls Flip.send_alerts().
I updated Flip.send_alerts() to be a generator, and to yield
a (channel, error, send_time_in_seconds) triple per sent
notification.
2021-01-15 15:15:00 +02:00
Pēteris Caune
3b6afae140
Specify timeout in the DBus calls 2021-01-14 09:59:06 +02:00
Pēteris Caune
1e46cd6e93
Tweak coveralls configuration
coveralls.io is throwing 422 and breaking CI,
this may or may not help.

Related: https://github.com/TheKevJames/coveralls-python/issues/252
2021-01-13 15:43:18 +02:00
Pēteris Caune
d7c7ae6531
Fix tests 2021-01-13 12:13:14 +02:00
Pēteris Caune
ce7e32ac03
Fix tests 2021-01-13 11:57:19 +02:00
Pēteris Caune
74ed15e0aa
Update the signal integration to use DBus
The initial implementation was just calling signal-cli directly
using `subprocess.run`.

Going with DBus makes it easier to shield signal-cli from the
rest of the system. It also makes sure the signal-cli daemon is
running in the background and receiving messages. This is important
when a recipient does the "Reset secure connection" from the app. We
must receive their new keys, otherwise our future messages will
appear as "bad encrypted message" for them.
2021-01-13 11:52:42 +02:00
Pēteris Caune
a80b831eea
Add rate-limiting for Signal messages 2021-01-11 15:07:34 +02:00
Pēteris Caune
d4aac691ce
Increase the timeout for sending Signal messages 2021-01-11 12:56:53 +02:00
Pēteris Caune
ee37d305ef
Tighten Telegram rate limit to 6 messages / minute
With the previous 10 minutes / minute limit we were still hitting
Telegram API rate limit (the 429, "Too Many Requests" response)
from time to time.

Therefore, tighten the limit a bit on our side.
2021-01-11 10:54:46 +02:00
Pēteris Caune
f607ee67d5
Allow searching in the error field in Notifications admin 2021-01-11 10:08:36 +02:00
Pēteris Caune
0aeef7d06e
Fix unwanted HTML escaping in SMS and WhatsApp notifications 2021-01-10 18:29:38 +02:00
Pēteris Caune
55a22e5043
Split AddSmsForm into PhoneNumberForm and PhoneUpDownForm
The PhoneNumberForm is used in "Add SMS" and "Add Phone Call" pages.
The PhoneUpDownForm is a subclass of PhoneNumberForm and
adds "up" and "down" boolean fields. It is used in "Add Signal"
and "Add WhatsApp" pages.
2021-01-10 15:52:33 +02:00
Pēteris Caune
847a610af9
Sort hc-add-* routes 2021-01-09 16:52:48 +02:00
Pēteris Caune
cd99af14ba
Add Signal integration
Fixes: #428
2021-01-09 11:58:18 +02:00
Pēteris Caune
959df1ffaa
Upgrade Django to 3.1.5 2021-01-04 11:02:19 +02:00
Pēteris Caune
17a404f04b
Fix email template to always show the current year in the footer 2021-01-01 22:43:00 +02:00
Pēteris Caune
599f35e4f0
Improve the crontab snippet in the "Check Details" page
Fixes: #465
2020-12-30 13:49:33 +02:00
Pēteris Caune
bf3df906f7
Tweak email CSS for nicer display in dark mode 2020-12-29 17:50:26 +02:00
Pēteris Caune
54081208c5
Add doctype declaration in the alert email template
Need it to escape quirks mode in email clients.
2020-12-29 16:08:23 +02:00
Pēteris Caune
efc44fd47c
Update report template to use same font size for all check names
Fixes: #347
2020-12-29 15:14:37 +02:00
Pēteris Caune
ca3d1d3a3b
Add the "Last Ping Type" field in the email notification template 2020-12-28 17:34:58 +02:00
Pēteris Caune
26a7918b5b
Bump pytz version 2020.1 -> 2020.5 2020-12-28 14:23:48 +02:00
Pēteris Caune
02b5ec3657
Rename "Signalling Failures" -> "Signaling Failures" 2020-12-28 14:12:07 +02:00
Pēteris Caune
70519fcd89
Fix spelling, grammar, style mistakes 2020-12-28 14:06:54 +02:00
Pēteris Caune
8fa0d04830
Exclude Bootstrap's popovers
Not using them anywhere on the site currently, so commented them out
in bootstrap.less and regenerated bootstrap.css.
2020-12-28 12:34:02 +02:00
Tim Gates
1f641962d2
docs: fix simple typo, libary -> library (#464)
Fix simple typo in docs, libary -> library

There is a small typo in templates/docs/python.md.

Should read `library` rather than `libary`.
2020-12-28 12:30:58 +02:00
Pēteris Caune
ce0f84a112
Fix styling 2020-12-26 18:19:55 +02:00
Pēteris Caune
8fe8e0f605
Update alert email template: more information, less styling
Fixes: #348
2020-12-26 18:11:36 +02:00
Pēteris Caune
c3b6d40012
Fix selectize initialization in the Details page
Fixes: #462
2020-12-26 14:51:30 +02:00
Pēteris Caune
823b3dbc7b
Fix tests 2020-12-16 14:11:36 +02:00
Pēteris Caune
77a5f11cf9
Update OpsGenie instructions
Fixes: #450
2020-12-16 14:09:48 +02:00
Pēteris Caune
0f1abd3498
Add tighter parameter checks in hc.front.views.serve_doc 2020-12-14 19:08:36 +02:00
Pēteris Caune
b8f1bdaf96
Update changelog for release 2020-12-09 16:03:49 +02:00
Pēteris Caune
dfd159ab18
Add a "Lost password?" link with instructions in the Sign In page 2020-12-09 15:38:19 +02:00
Shea Polansky
54a95a0ee2
Add http header auth (#457)
* Add HTTP header authentiation backend/middleware

* Add docs for remote header auth

* Improve docs on external auth

* Add warning for unknown  REMOTE_USER_HEADER_TYPE

* Move active check for header auth to middleware
Add extra header type sanity check to the backend

* Add test cases for remote header login

* Improve header-based authentication

- remove the 'ID' mode
- add CustomHeaderBackend to AUTHENTICATION_BACKENDS conditionally
- rewrite CustomHeaderBackend and CustomHeaderMiddleware to
use less inherited code
- add more test cases

Co-authored-by: Pēteris Caune <cuu508@gmail.com>
2020-12-09 11:25:56 +02:00
Pēteris Caune
5e3e371661
Set up CodeQL analysis 2020-12-09 11:09:35 +02:00
Pēteris Caune
70ef9c1904
Remove unused CSS 2020-12-08 11:25:09 +02:00
Pēteris Caune
ea6d04d061
Bump Django version to 3.1.4 2020-12-07 11:11:51 +02:00
Pēteris Caune
5d650f07fb
Fix db field overflow when copying a check with a long name 2020-12-03 13:01:53 +02:00
Pēteris Caune
9623e3eacb
Update 3rd party resources
Move terraform-provider-healthchecksio to the "API Wrappers"
category, which is more appropriate than "Tools for Self-Hosting".
2020-12-01 15:05:36 +02:00
Pēteris Caune
ec40082550
Update 3rd party resources
Move terraform-provider-healthchecksio to the "API Wrappers"
category, which is more appropriate than "Tools for Self-Hosting".
2020-12-01 15:04:30 +02:00
Pēteris Caune
617bd92434
Add Ping.exitstatus field, store received exit status values in db
Fixes #455
2020-11-29 12:12:44 +02:00
Pēteris Caune
524d1a7375
Implement badge mode (up/down vs up/late/down) selector
Fixes #282
2020-11-27 12:57:25 +02:00
Pēteris Caune
dd45c888a7
Rearrange resources, add msfjarvis/healthchecks-rs 2020-11-22 20:02:35 +02:00
Pēteris Caune
b9abcbcdee
Update build badge, remove Travis configuration 2020-11-21 00:43:14 +02:00
Pēteris Caune
62fcd30ce8
Add configuration for running tests with Github Actions (#453) 2020-11-21 00:31:15 +02:00
Pēteris Caune
eed7ef36d1
Improve text instructions 2020-11-19 17:35:21 +02:00
Pēteris Caune
0b4251bdee
Add logic to handle exceptions thrown by the fido2 library 2020-11-19 16:53:58 +02:00
Pēteris Caune
c8d387aee4
Improve text instructions 2020-11-19 16:35:44 +02:00
Pēteris Caune
3cfc31610a
Add extra security checks in the login_webauthn view 2020-11-19 16:21:31 +02:00
Pēteris Caune
8448f882cf
Add notes about adding a second key, and removing the last key 2020-11-19 15:05:08 +02:00
Pēteris Caune
568a287850
Fix WebAuthn registration to use random bytes for user handle
User handle is used in a username-less authentication, to map a
credential received from browser with an user account in the
database. Since we only use security keys as a second factor,
the user handle is not of much use to us.

The user handle:
 - must not be blank,
 - must not be a constant value,
 - must not contain personally identifiable information.

So we use random bytes, and don't store them on our end.
2020-11-19 13:59:23 +02:00
Pēteris Caune
8dbf9e02af
Fix capitalization, Webauthn -> WebAuthn 2020-11-19 13:01:26 +02:00
Pēteris Caune
7124383a53
Add checks for RP_ID, add a 2FA section in README 2020-11-19 12:54:00 +02:00
Pēteris Caune
9401bc3987
Update the "Close Account" function to use confirmation codes 2020-11-16 16:22:25 +02:00
Pēteris Caune
48750ee668
Update "Change Password" to show messages in panel's footer 2020-11-16 15:45:25 +02:00
Pēteris Caune
fb79948759
Update the "Change Email" function to use confirmation codes 2020-11-16 15:33:29 +02:00
Pēteris Caune
ed6b15bfa9
Update the "Set Password" function to use confirmation codes 2020-11-16 14:53:50 +02:00
Pēteris Caune
1ca4caa3a8
Update the set_password view to use update_session_auth_hash
Changing user's password logs themselves out. To avoid that,
we were logging the user back in right after changing the password.

I recently discovered update_session_auth_hash, which seems to
be the proper way to do this.

Docs: https://docs.djangoproject.com/en/3.1/topics/auth/default/#session-invalidation-on-password-change
2020-11-16 14:29:52 +02:00
Pēteris Caune
adb7702f39
Rename login_tfa to login_webauthn 2020-11-16 14:16:06 +02:00
Pēteris Caune
7639f0dd69
Add test cases for the login_tfa view 2020-11-16 14:01:04 +02:00
Pēteris Caune
d0f327b213
Add Base64Field field (base64-encoded binary data) 2020-11-16 13:10:38 +02:00
Pēteris Caune
839c309cf7
Refactor for testability, add more test cases 2020-11-16 12:52:26 +02:00
Pēteris Caune
155a1f132b
Simplify super() calls in tests 2020-11-16 11:20:01 +02:00
Pēteris Caune
155226d82a
Add tests for sudo mode 2020-11-16 10:58:38 +02:00
Pēteris Caune
ecf964ea3b
Remove a verify_origin workaround 2020-11-15 21:49:25 +02:00
Pēteris Caune
9f58ebfd3e
Hook up a 2FA check after a password or email link authentication 2020-11-15 21:39:49 +02:00
Pēteris Caune
64be87137b
Add a two-factor authentication form (WIP) 2020-11-14 12:54:26 +02:00
Pēteris Caune
2ac0f87560
Implement a "Remove Security Key" feature 2020-11-14 11:45:09 +02:00
Pēteris Caune
42497fe91a
Add rate limiting to the sudo code form 2020-11-13 22:04:19 +02:00
Pēteris Caune
2c3286c280
Improve the "add security key" UX, require sudo mode 2020-11-13 16:23:28 +02:00
Pēteris Caune
e3aedd3b03
Add require_sudo_mode decorator
Planning to use it for sensitive operations (add/remove security keys),
change email, change password, close account.

The decorator sends a six-digit confirmation code to user's email
and renders a form for entering it back. If the user enters the
correct code, the decorators sets a sudo=active marker in
user's session, valid for 30 minutes.
2020-11-13 11:08:06 +02:00
Pēteris Caune
03ea725612
Add Credential.created field 2020-11-12 18:03:12 +02:00
Pēteris Caune
53688f1d87
Add error handling on the client side, use Django form API 2020-11-12 17:08:23 +02:00
Pēteris Caune
1eaa216d3a
Add experimental code for registering Webauthn credentials 2020-11-12 16:15:07 +02:00
Pēteris Caune
cdd2e98bd0
Remove USE_I18N and USE_L10N from settings
They have the default values and so are redundant.
2020-11-06 18:51:30 +02:00
Pēteris Caune
816c158744
Fix code formatting in the Notification model 2020-11-06 18:50:23 +02:00
Pēteris Caune
d5502c50ca
Add retries to the the email sending logic
When sending email using Django's default email
backend (SMTP), and if there is a network issue, the backend
can throw SMTPServerDisconnected.

This commit adds a retry logic which retries sending the
email two times when SMTPServerDisconnected is thrown.
2020-10-30 14:18:38 +02:00
Pēteris Caune
0b685e8b5a
Disable retries when testing webhook integration
Normally, when a webhook call fails (timeout, connection
error, non-2xx response), the HTTP request is retried up to two
times (so up to 3 times total). This is useful when sending
actual notifications, in case the webhook target has a temporary
glitch.

When interactively testing a webhook integration
("Send Test Notification" in the
"Integrations" page), we would prefer to see any errors ASAP
on the screen instead of retrying and so possibly swallowing them.

One specific use case is webhook targets that take long time to
generate a response. "Send Test Notification" is synchronous,
meaning that the user could be stuck for
5 x 3 = 15 seconds waiting for the  test HTTP request to time out
three times.
2020-10-30 12:36:17 +02:00
Pēteris Caune
f7e004b2ea
Improve phone number sanitization: remove spaces and hyphens 2020-10-30 11:32:09 +02:00
Pēteris Caune
81e59ac553
Add support for script's exit status in ping URLs
Fixes: #429
2020-10-28 14:28:32 +02:00
Pēteris Caune
6f56ed7f92
Reduce the number of SQL queries used in the "Get Checks" API call 2020-10-27 16:19:57 +02:00
Pēteris Caune
078577cbb7
Update the read-only dashboard's CSS for better mobile support
Fixes: #442
2020-10-27 15:27:44 +02:00
Pēteris Caune
a37e83aca8
Update AddSmsForm to remove any invisible unicode characers 2020-10-20 15:53:27 +03:00
Pēteris Caune
7534f1856f
Add testcases for setting channels in the "Create Check" API call 2020-10-14 18:12:35 +03:00
Pēteris Caune
7e56156d32
Optimize the "Update Check" API call
In the "Update Check" API call, if no fields have changed,
don't save the changes to the database.
2020-10-14 18:03:13 +03:00
Pēteris Caune
0e77064c44
Update API to allow specifying channels by names
Fixes: #440
2020-10-14 15:37:04 +03:00
Pēteris Caune
20008a1d7e
Fix wording 2020-10-14 13:15:11 +03:00
Pēteris Caune
71d7b46379
Add a tooltip to the 'confirmation link' label
Fixes: #436
2020-10-14 13:13:22 +03:00
Pēteris Caune
a10215ce65
Update CHANGELOG for 1.17.0 release 2020-10-14 12:39:42 +03:00
Pēteris Caune
463ec8c988
Set the "title" and "summary" fields in MS Teams notifications
Fixes: #435
2020-10-06 16:43:56 +03:00
Pēteris Caune
63beeb05a1
Add missing slashes 2020-10-03 21:00:35 +03:00
Pēteris Caune
a13b44284e
Django 3.1.2 2020-10-01 09:16:59 +03:00
Pēteris Caune
1967c712ca
Add Matrix setup instructions in README cc: #427 2020-09-21 15:04:57 +03:00
Pēteris Caune
fd8da1b642
Update screenshots in Matrix setup instructions 2020-09-21 14:47:22 +03:00
Pēteris Caune
05c81e0a41
Escape markdown in MS Teams notifications. cc: #426 2020-09-11 11:49:46 +03:00
Pēteris Caune
b64c8d1cb8
API support for setting the allowed HTTP methods for making ping requests 2020-09-10 10:29:44 +03:00
Pēteris Caune
c13f65e118
Grammar and style fixes. 2020-09-09 17:53:24 +03:00
Pēteris Caune
b4729cdb57
Grammar and style fixes. 2020-09-09 12:21:18 +03:00
Pēteris Caune
e63aa9fe8d
Grammar and style fixes, updated illustration. 2020-09-09 11:33:50 +03:00
Pēteris Caune
66a1a108bf
When decoding inbound emails, decode encoded headers. Fixes #420 2020-09-08 12:06:32 +03:00
Pēteris Caune
bd98174d4c
Fix missing Resume button. Fixes #421 2020-09-04 13:17:54 +03:00
Pēteris Caune
0f0930fbf5
Merge pull request #419 from healthchecks/snyk-fix-3b4d7e5e456fc8fadd61239890135796
[Snyk] Security upgrade django from 3.1 to 3.1.1
2020-09-03 11:00:41 +03:00
snyk-bot
c84626040c
fix: requirements.txt to reduce vulnerabilities
The following vulnerabilities are fixed by pinning transitive dependencies:
- https://snyk.io/vuln/SNYK-PYTHON-DJANGO-609368
- https://snyk.io/vuln/SNYK-PYTHON-DJANGO-609369
2020-09-02 22:16:56 +00:00
Pēteris Caune
0362df55ba
Docs: update the "Filtering Rules" section with the new options. 2020-09-01 15:00:41 +03:00
Pēteris Caune
ad720af242
Rename "hc-p-channels" to "hc-channels" 2020-09-01 12:56:35 +03:00
Pēteris Caune
5ebb5958ea
Remove unused "project" parameter in Pushbullet tests. 2020-09-01 12:18:24 +03:00
Pēteris Caune
9ba9032389
Cleaner OAuth redirect_uri generation 2020-09-01 12:07:13 +03:00
Pēteris Caune
d1b1a6c02e
The LINE Notify integration uses OAuth2 flow. 2020-09-01 11:38:08 +03:00
Pēteris Caune
4f53325730
THe LINE Notify integration uses OAuth2 flow. 2020-09-01 11:37:54 +03:00
Pēteris Caune
25a8ec6bd9
Capitalize title 2020-08-31 13:01:45 +03:00
Pēteris Caune
b4ba582255
Docs: add the "Viewing cron logs using journalctl" section 2020-08-31 12:51:24 +03:00
Pēteris Caune
ae578a29c2
Docs: add "Using Runitor" and "Handling More Than 10KB of Logs" sections 2020-08-31 12:32:16 +03:00
Pēteris Caune
a2c123c74b
Docs: add a section about read-only team members 2020-08-31 11:33:06 +03:00
Pēteris Caune
0a85c5ed12
In Account Settings > My Projects, indicate read-only memberships as read-only 2020-08-31 11:07:39 +03:00
Pēteris Caune
e424176a1f
Remove mentions of "whitelist" 2020-08-26 16:38:29 +03:00
Pēteris Caune
b2a1c0d343
Set USE_L10N to False until we've fixed issues caused by decimal comma formatting in templates. Fixes #416 2020-08-26 16:15:29 +03:00
Pēteris Caune
d73de68f70
Specify the read-write/read-only flag when inviting a team member. 2020-08-26 16:09:17 +03:00
Pēteris Caune
adb004b333
Read-only users cannot change project settings. 2020-08-26 15:04:12 +03:00
Pēteris Caune
39198c827a
Read-only users cannot edit or remove channels. 2020-08-26 14:48:31 +03:00
Pēteris Caune
24c34430ac
Read-only users cannot resume checks. 2020-08-26 14:12:52 +03:00
Pēteris Caune
bdf99e0ea7
The "Add Integration" pages require read-write access. 2020-08-26 14:06:51 +03:00
Pēteris Caune
c9baa2d8eb
Read-only users cannot toggle channels on and off. 2020-08-26 12:50:02 +03:00
Pēteris Caune
024d0adb9c
Read-only users cannot copy, transfer or remove checks. 2020-08-26 12:44:55 +03:00
Pēteris Caune
cbd7ffbffb
Read-only users cannot edit filtering rules. 2020-08-26 12:36:05 +03:00
Pēteris Caune
11d8e6197c
Read-only users cannot add checks.
Read-only users cannot pause checks.
2020-08-26 12:29:03 +03:00
Pēteris Caune
00790dc33c
Member.rw flag. Read-only users cannot edit check's name/desc/tags or schedule 2020-08-26 12:16:43 +03:00
Pēteris Caune
84cc33412a
When copying a check, copy all fields from the "Filtering Rules" dialog 2020-08-26 10:08:37 +03:00
Pēteris Caune
40f95d5a56
When copying a check, also copy the "failure keyword" field Fixes #417 2020-08-26 10:00:49 +03:00
Pēteris Caune
a5e1343a66
Merge pull request #415 from xakraz/master
Updated REAMDE.md: Add Slack integration instructions
2020-08-21 18:02:53 +03:00
Xavier Krantz
dd5ca9d783 Updated REAMDE.md: Add Slack integration instructions 2020-08-21 14:49:59 +02:00
Pēteris Caune
11c02d89c1
Go usage example in docs 2020-08-20 13:56:43 +03:00
Pēteris Caune
33639964b8
Add LINE Notify icon to the icon font. 2020-08-20 11:37:30 +03:00
Pēteris Caune
94b993354f
Sort integrations in A-Z order. Rename "LineNotify" -> "LINE Notify". Update the LINE Notify icon. 2020-08-20 11:16:59 +03:00
carson.wang
f15e16a0bb
Remove HTML markup 2020-08-20 10:42:06 +03:00
carson.wang
74668551a7
Add tests & Doesn't get LineNotify token using setting 2020-08-20 10:42:04 +03:00
carson.wang
65b65188d1
Test LineNotify integration with healthcheck 2020-08-20 10:42:00 +03:00
Pēteris Caune
2346ac3e80
Bugfix: don't allow duplicate team memberships 2020-08-19 12:07:48 +03:00
Pēteris Caune
9a1127005e
Link to the "Security" section in dashboard's README 2020-08-18 14:21:38 +03:00
Pēteris Caune
b7e2404f98
Host a read-only dashboard (from github.com/healthchecks/dashboard/), link to it from "Project Settings" > "Show API keys" 2020-08-18 14:07:55 +03:00
Pēteris Caune
c75a37570e
In channels admin, don't show the notification counts, querying it is too expensive. 2020-08-18 13:30:24 +03:00
Pēteris Caune
c7af52637a
Less verbose output in the senddeletionnotices command 2020-08-18 11:05:04 +03:00
Pēteris Caune
8ea510cda6
Removing unused /api/v1/notifications/{uuid}/bounce endpoint 2020-08-17 13:18:39 +03:00
Pēteris Caune
a29b82a0ed
In api.views.notification_status, always return HTTP 200 so the other party doesn't retry over and over again 2020-08-17 13:10:07 +03:00
Pēteris Caune
697cb19bde
Handle excessively long email addresses in the team member invite form. 2020-08-17 12:05:19 +03:00
Pēteris Caune
ffafc16fe5
Handle excessively long email addresses in the signup form. 2020-08-17 11:31:24 +03:00
Pēteris Caune
8223b0c402
Merge pull request #411 from henrywhitaker3/master
Added PHP wrapper to docs
2020-08-12 12:35:40 +03:00
Henry Whitaker
77f81b82e7
Merge pull request #1 from henrywhitaker3/henrywhitaker3-patch-1
Update resources.html
2020-08-12 09:54:24 +01:00
Henry Whitaker
cef71b1159
Added PHP wrapper to docs 2020-08-12 09:53:11 +01:00
Henry Whitaker
99b0786f19
Update resources.html 2020-08-12 09:40:04 +01:00
Pēteris Caune
b63f3bed8e
Limit project name to 60 characters to prevent abuse 2020-08-10 11:23:59 +03:00
Pēteris Caune
f131123e0e
In the test_it_sends_link testcase, explicitly set the USE_PAYMENTS setting. This way tests work regardless of what's in the environment variable or local_settings.py file. 2020-08-05 17:35:37 +03:00
Pēteris Caune
96d458fcf3
Merge pull request #409 from iphoting/patch-1
Fix logic bug in test_signup (#408)
2020-08-05 17:32:34 +03:00
Ronald Ip
c476f042ba
Fix logic bug in test_signup (#408)
Resolves #408 by fixing the test_signup logic bug introduced in 8c13457.
2020-08-05 22:27:44 +08:00
Pēteris Caune
ae01c7a9d1
Handle Twilio status callbacks for SMS, WhatsApp and phone call notifications. 2020-08-05 17:12:23 +03:00
Pēteris Caune
95d58d26d5
Handle status callbacks from Twilio, show SMS delivery failures in the Integrations page. 2020-08-05 16:10:30 +03:00
Pēteris Caune
750b96c374
Use Django 3.1 2020-08-05 13:11:39 +03:00
Pēteris Caune
9edb8aa08d
Update changelog for v1.16.0 release. 2020-08-04 19:24:41 +03:00
Pēteris Caune
2ed9a8fd30
Rename Channel.sms_number property to Channel.phone_number. It is now used for SMS, WhatsApp and phone call notifications, so "sms_number" is not accurate any more. 2020-08-04 16:26:13 +03:00
Pēteris Caune
732df19374
Fix the "Paid plan required" notice. 2020-08-04 16:01:58 +03:00
Pēteris Caune
d05691f86f
SMS and phone calls now have separate "limit reached" email templates. 2020-08-03 18:00:48 +03:00
Pēteris Caune
8c13457037
Use separate counters for SMS and phone calls. 2020-08-03 17:52:09 +03:00
Pēteris Caune
77ee8452c5
Update docs. 2020-07-29 19:29:34 +03:00
Pēteris Caune
ee9ac0ffef
New integration: phone calls. Fixes #403 2020-07-29 18:30:50 +03:00
Pēteris Caune
43e56ce788
Add support for multiple, comma-separated keywords (cc: #396) 2020-07-23 12:06:17 +03:00
Pēteris Caune
5acea4c89d
Update Node.js pinging examples -- handle the 'error' event. 2020-07-22 18:36:11 +03:00
Pēteris Caune
1ff7b2c581
Merge pull request #406 from UniversitaDellaCalabria/locale-it
IT localization
2020-07-22 15:02:17 +03:00
Giuseppe
ce50755314 IT localization 2020-07-22 13:43:06 +02:00
Pēteris Caune
ea896c907f
Translation tweaks 2020-07-22 11:05:26 +03:00
Pēteris Caune
fd14e0e03b
Experimental L10N support in base and welcome templates. cc: #404 2020-07-21 22:57:40 +03:00
Pēteris Caune
519a666057
{% site_name %} -> {{ site_name }} so we can use blocktrans tags for L10N 2020-07-21 17:59:39 +03:00
Pēteris Caune
0d03e3f00b
Add "Failure Keyword" filtering for inbound emails (cc: #396) 2020-07-21 14:57:48 +03:00
Pēteris Caune
556e8c67c5
Syntax highlighting for PHP examples. 2020-07-17 19:55:11 +03:00
Pēteris Caune
59e566117b
Update pinging examples. 2020-07-17 17:21:10 +03:00
Pēteris Caune
6834adf878
Django 3.0.8 2020-07-17 16:51:39 +03:00
Pēteris Caune
028e131327
Update pinging examples. 2020-07-17 16:51:23 +03:00
Pēteris Caune
589c0c0363
Updated Discord integration to use discord.com instead of discordapp.com 2020-07-17 13:36:41 +03:00
Pēteris Caune
f814035f03
Declutter /admin/accounts/profile/ 2020-07-16 16:31:57 +03:00
Pēteris Caune
255d4e7bb7
Reduce the number of queries in /admin/api/channel/ 2020-07-16 16:15:58 +03:00
Pēteris Caune
ec5ee03a3e
Add "check_id" in Spike payload. 2020-07-15 17:56:18 +03:00
Pēteris Caune
f789cad2af
Handle HTTP 429 responses from Matrix server when joining a Matrix room 2020-07-10 16:44:49 +03:00
Pēteris Caune
f5ceb612e0
Link to the Prometheus docs from the welcome page. 2020-07-10 15:24:19 +03:00
Pēteris Caune
80fdfbfa59
Add Spike icon in the iconfont (cc: #393) 2020-07-10 15:12:33 +03:00
Pēteris Caune
62fe42e953
".field-email span" selector was too broad and affecting profile details page, fixed. 2020-07-09 18:45:51 +03:00
Pēteris Caune
58f16da935
Edits to Spike setup instructions. 2020-07-09 11:22:14 +03:00
Pēteris Caune
1f978ff80e
Fix tests. 2020-07-09 10:48:51 +03:00
Divyansh
6300947c77
integration for Spike 2020-07-09 10:44:40 +03:00
Pēteris Caune
d34854f838
Update bash examples with the "-m" parameter. 2020-07-08 17:19:13 +03:00
Pēteris Caune
911293e1d2
Add a few missing meta description tags. 2020-07-08 16:10:30 +03:00
Pēteris Caune
3f44eac485
Update the section about read-write and read-only API keys. 2020-07-08 13:55:46 +03:00
Pēteris Caune
c160045bda
Update the section about read-write and read-only API keys. 2020-07-08 13:52:31 +03:00
Pēteris Caune
d6c0d9722b
Use PING_URL placeholder in the PHP example. 2020-07-07 21:22:56 +03:00
Pēteris Caune
2510e387e6
Use PING_URL placeholder in the PHP example. 2020-07-07 21:17:31 +03:00
Pēteris Caune
4324843c41
Merge pull request #395 from smknstd/master
[docs] add php curl example with timeout and retry options
2020-07-07 20:57:40 +03:00
Arnaud Becher
2cb0ac907d add php curl example with timeout and retry options 2020-07-07 18:36:41 +02:00
Pēteris Caune
e89229a2ca
In admin, visualize account's number of checks 2020-07-06 18:39:27 +03:00
Pēteris Caune
27a91bfe22
Tweak navigation in docs, added "Docs > Reliability Tips" page (cc: #384) 2020-07-02 18:39:30 +03:00
Pēteris Caune
df65ec9d89
Adding pauldenver/healthchecks-io-client to the 3rd party resources page 2020-07-02 12:15:32 +03:00
Pēteris Caune
f573578108
Some JS linting fixes 2020-07-01 19:23:50 +03:00
Pēteris Caune
3a00c0d2aa
Sending a test notification updates Channel.last_error. Fixes #391 2020-07-01 14:03:11 +03:00
Pēteris Caune
ae4918db86
CSS tweaks: do slightly less white-on-white painting 2020-07-01 12:32:28 +03:00
Pēteris Caune
1e53027b84
CSS tweaks: do slightly less white-on-white painting 2020-06-30 12:39:12 +03:00
Pēteris Caune
5b3928ce79
render_docs 2020-06-26 10:11:08 +03:00
Pēteris Caune
192e72c243
Edit Prometheus guide, add "API Keys" screenshot. 2020-06-26 10:10:59 +03:00
Pēteris Caune
3c461473ec
Merge pull request #387 from issmirnov/prometheus-docs
Create configuring_prometheus.md
2020-06-26 09:49:30 +03:00
Ivan Smirnov
634b525d1a generate html assets 2020-06-25 15:06:09 -07:00
Ivan Smirnov
0b5fa40f68 Create configuring_prometheus.md
Add documentation on how to export metrics to prometheus.
2020-06-25 15:00:20 -07:00
Pēteris Caune
149096811d
In the checks list, indicate a started check with a progress spinner under the status icon (cc: #338) 2020-06-25 16:44:25 +03:00
Pēteris Caune
a18eb134f5
Refactor: change Check.get_status(with_started=...) default value from True to False (with_started=False is or will be useful in more places) 2020-06-25 15:23:59 +03:00
Pēteris Caune
eccc193b87
In the cron expression dialog, show a human-friendly version of the expression 2020-06-19 11:25:46 +03:00
Pēteris Caune
a3b58d25ff
Change "--output" to "-o" in curl examples. 2020-06-15 16:14:03 +03:00
Pēteris Caune
bd3f150284
Merge pull request #380 from Simonmicro/master
Switched from pipeing to --output /dev/null for curl
2020-06-15 15:57:59 +03:00
Pēteris Caune
84889d6160
Render docs. 2020-06-15 13:28:20 +03:00
Pēteris Caune
276c36841a
Merge branch 'jameskirsop-return-single-history' 2020-06-15 13:27:37 +03:00
Pēteris Caune
5ab09f61f7
Update changelog 2020-06-15 13:26:56 +03:00
Pēteris Caune
c3d8ee0965
Update API docs. 2020-06-15 13:25:55 +03:00
Pēteris Caune
832580f343
Simplify hc.api.views.flips, add validation and more tests. 2020-06-15 13:08:17 +03:00
Pēteris Caune
60d1c6e2a3
Format timestamp as ISO 8601 without microseconds, same as elsewhere. 2020-06-15 12:20:07 +03:00
Pēteris Caune
a90f8a3a56
Remove unused code. 2020-06-15 12:17:15 +03:00
Pēteris Caune
f9c10d99c1
Merge pull request #383 from makom/master
fix typo
2020-06-15 12:05:05 +03:00
James Kirsop
368d7a4fec Commit with requested changes and tests 2020-06-15 13:15:57 +10:00
Martin
c11526a05d
Merge pull request #1 from makom/fix-typo
fix typo
2020-06-14 17:22:43 +02:00
Martin
bc0684df63
fix typo 2020-06-14 14:51:11 +02:00
James Kirsop
c5c4e0f782 Returning all historical flips if no parameters are passed 2020-06-12 17:42:45 +10:00
James Kirsop
7d625cb6a6 Merge branch 'return-single-history' of https://github.com/jameskirsop/healthchecks into return-single-history 2020-06-12 13:39:13 +10:00
James Kirsop
90d4246848 Second interation of this 2020-06-12 13:39:03 +10:00
James Kirsop
4b1b232959 Chnange 'status' field in response to 'up' 2020-06-12 09:16:59 +10:00
James Kirsop
bc6ccd55b3 Implementation of history using Flips model statuses for a check 2020-06-12 09:16:59 +10:00
James Kirsop
aaadf6031f Sample work for review 2020-06-12 09:16:59 +10:00
Simon Beginn
4592987810 Switched from piping to --output /dev/null for curl 2020-06-11 20:30:41 +02:00
Pēteris Caune
beff11ceff
Fixing typo 2020-06-11 16:07:04 +03:00
Pēteris Caune
01fafd9908
Merge branch 'jameskirsop-Retrieve-check-by-unique_key' 2020-06-11 15:25:27 +03:00
Pēteris Caune
cdafc06c65
In urls.py, route "api/v1/checks/<sha1:unique_key>" directly to the hc.api.views.get_check_by_unique_key view.
Minor API documentation edits.
2020-06-11 15:24:45 +03:00
James Kirsop
8725c81144 Implementing new changes discussed to resolve #370 2020-06-11 17:00:27 +10:00
Pēteris Caune
fd4d59c4e1
API, optimization: avoid retrieving project twice from the database 2020-06-09 18:51:42 +03:00
Pēteris Caune
0e5d578360
Update _get_events to work same way as hc.api.views.pings (iterate over pings in ascending order) 2020-06-09 18:41:09 +03:00
Pēteris Caune
a07325e40f
Add "Get a list of checks's logged pings" API call (#371) 2020-06-09 18:09:57 +03:00
Pēteris Caune
461ef5e088
Paused ping handling can be controlled via API. Fixes #376 2020-06-09 15:16:39 +03:00
Pēteris Caune
8e51d26595
Removing Pager Team integration, project appears to be discontinued 2020-06-09 13:26:15 +03:00
Pēteris Caune
ffc45f0c74
Update CHANGELOG for release. 2020-06-04 15:07:34 +03:00
Pēteris Caune
4f1f06e29f
Merge pull request #374 from healthchecks/snyk-fix-e4c69a4ee669f785e6b47fee436364ef
[Snyk] Security upgrade django from 3.0.4 to 3.0.7
2020-06-04 15:05:30 +03:00
snyk-bot
b2175c9260 fix: requirements.txt to reduce vulnerabilities
The following vulnerabilities are fixed by pinning transitive dependencies:
- https://snyk.io/vuln/SNYK-PYTHON-DJANGO-571013
- https://snyk.io/vuln/SNYK-PYTHON-DJANGO-571014
2020-06-03 22:17:38 +00:00
Pēteris Caune
3eebd8968d
Added "When paused, ignore pings" option in the Filtering Rules dialog (#369) 2020-06-02 10:54:16 +03:00
Pēteris Caune
5c8b5b7b63
adaptiveSetInterval fires the first request immediately if runNow is true, in 3 seconds otherwise. 2020-06-01 11:51:40 +03:00
Pēteris Caune
cfb294862f
DRY, have a single "No billing address" modal dialog. 2020-05-29 15:33:33 +03:00
Pēteris Caune
95279f6f3f
Billing page allows setting up a subscription before a payment method is added. 2020-05-29 15:08:00 +03:00
Pēteris Caune
9617be6e1b
Fix alignment of plan columns. 2020-05-06 12:00:56 +03:00
Pēteris Caune
c70a2588c6
Update package versions 2020-05-01 11:05:44 +03:00
Pēteris Caune
b433e91b48
Merge pull request #366 from bdd/master
Add runitor to resources.{md,html}
2020-05-01 10:47:51 +03:00
Berk D. Demir
eb279c4c21 Add runitor to resources.{md,html}
From its README:

Why Do I Need This Instead of Calling curl from a Shell Script?

In addition to clean separation of concerns from the thing that needs to
run and the act of calling an external monitor, runitor packs a few neat
extra features that are bit more involved than single line additions to
a script.

It can capture the stdout and stderr of the command to send it along
with execution reports, a.k.a. pings. When you respond to an alert you
can quickly start investigating the issue with the relevant context
already available.

It can be used as a long running process acting as a task scheduler,
executing the command at specified intervals. This feature comes in
handy when you don't readily have access to a job scheduler like crond
or systemd.timer. Works well in one process per container environments.
2020-04-30 19:43:27 -07:00
Pēteris Caune
3730c67c80
Return max notification_id in metrics. 2020-04-26 20:34:52 +03:00
Pēteris Caune
98310eeeaa
Include timestamp in the metrics response. 2020-04-26 19:34:36 +03:00
Pēteris Caune
edbfd4b437
Added /api/v1/metrics/ endpoint, useful for monitoring the service itself 2020-04-26 17:45:50 +03:00
Pēteris Caune
7994259003
When an invited user logs in, redirect them to the new project 2020-04-24 14:46:43 +03:00
Pēteris Caune
fbd8419700
CSS tweaks in the welcome page, fix footer margin. 2020-04-24 14:02:55 +03:00
Pēteris Caune
9bfdbc4214
Fix login link. 2020-04-21 15:46:56 +03:00
Pēteris Caune
385021b44c
Don't let users clone checks if the account is at check limit 2020-04-20 19:34:35 +03:00
Pēteris Caune
e04a92ccf1
Profiles admin: filtering by number of checks, show check count by project. 2020-04-20 19:11:15 +03:00
Pēteris Caune
3cca17560a
Fix tests. 2020-04-20 17:11:00 +03:00
Pēteris Caune
00ea45655d
In checks list, the pause button asks for confirmation. Fixes #356 2020-04-20 17:09:48 +03:00
Pēteris Caune
825110a354
Channel icons in Admin > Channels 2020-04-20 13:56:24 +03:00
Pēteris Caune
abdff95ce8
Admin tweaks. 2020-04-20 13:33:21 +03:00
Pēteris Caune
c057dbfb2c
Cleanup. 2020-04-20 11:54:27 +03:00
Pēteris Caune
6ede17d93f
Cleanup and comments. 2020-04-20 11:23:07 +03:00
Pēteris Caune
d6bb2b5435
Merge pull request #360 from bdd/master
Remove redundant '-X POST' to curl
2020-04-19 11:47:11 +03:00
Berk D. Demir
34807dc5aa Remove redundant '-X POST' to curl
Passing `--data-raw` to curl implies the request is method will be POST.
Unless we intend to do something entirely different, -X method override
shouldn't be used.

Curl's author Daniel Stenberg (@bagder) wrote about this back in 2015
https://daniel.haxx.se/blog/2015/09/11/unnecessary-use-of-curl-x/
2020-04-18 15:05:17 -07:00
Pēteris Caune
dda08a6143
capitalize plan's name 2020-04-14 10:30:59 +03:00
Pēteris Caune
4331497ccd
Merge pull request #359 from SuperSandro2000/typos
Fix typos with codespell
2020-04-14 10:28:01 +03:00
Sandro Jäckel
38382d662d
Fix typos with codespell 2020-04-14 03:53:16 +02:00
Pēteris Caune
ca715dd8d4
Check membership when initiating project's transfer. Use transaction.atomic() when completing the transfer. 2020-04-13 15:19:37 +03:00
Pēteris Caune
57da17b8e2
Send an "Ownership Transfer Request" email notification. 2020-04-13 15:04:59 +03:00
Pēteris Caune
da954000fd
Remove unused CSS 2020-04-13 13:40:56 +03:00
Pēteris Caune
3bf1ad9746
Fix invite suggestions. 2020-04-13 12:26:05 +03:00
Pēteris Caune
532b752e3c
cleanup: don't import each form individually 2020-04-13 12:16:39 +03:00
Pēteris Caune
f7acaa57af
Adding tests. 2020-04-12 18:21:08 +03:00
Pēteris Caune
f42b2b144a
New feature: Project Settings > Transfer Ownership (WIP, missing tests) 2020-04-12 14:46:12 +03:00
Pēteris Caune
cb19bac70f
Merge pull request #358 from lobovkin/lobovkin-patch-1
Using existing function getAmount
2020-04-09 10:39:48 +03:00
Anton Lobovkin
4e0460c69b
Using existing function getAmount 2020-04-08 22:58:31 +02:00
Pēteris Caune
a982ad7123
Tooltips and updated FAQ in the pricing page. 2020-04-07 14:35:21 +03:00
Pēteris Caune
f1880657fd
Added "Supporter" billing plan. 2020-04-07 12:32:20 +03:00
Pēteris Caune
733c589e47
Section labels in the welcome tour. 2020-04-07 10:12:46 +03:00
Pēteris Caune
8c7d3570a5
Remove unused imports, cleanup. 2020-04-07 10:08:20 +03:00
Pēteris Caune
c596f485a5
DRY: adding "now_isoformat" template tag 2020-04-06 15:02:49 +03:00
Pēteris Caune
92542fa818
"Edit Webhook Parameters" button in the "Edit Name" modal. 2020-04-06 14:52:47 +03:00
Pēteris Caune
609f78c5ed
"Edit" function for webhook integrations (#176) 2020-04-06 14:48:47 +03:00
Pēteris Caune
f12a649c72
Fix tests. 2020-04-06 13:36:46 +03:00
Pēteris Caune
a1791ea404
Make sure long project names don't break layout. 2020-04-06 12:29:26 +03:00
Pēteris Caune
56bb49f1f3
Use Slack V2 OAuth flow 2020-04-02 10:57:10 +03:00
James Kirsop
74f4744c62 Implementation of history using Flips model statuses for a check 2020-03-27 14:19:57 +11:00
James Kirsop
010bbc9507 Sample work for review 2020-03-27 09:30:26 +11:00
Pēteris Caune
9d2cf4f008
Don't escape HTML in the subject line of notification emails 2020-03-25 17:18:14 +02:00
Pēteris Caune
4a43ed59fc
Rate limiting for Telegram notifications (10 notifications per chat per minute) 2020-03-24 23:33:02 +02:00
Pēteris Caune
76ae42bc8f
"Get a single check" API call now supports read-only API keys. Fixes #346 2020-03-24 16:10:42 +02:00
Pēteris Caune
5a297ba6a2
v1.14.0 2020-03-23 12:34:42 +02:00
Pēteris Caune
f1750a5f6e
Add Zulip in the Welcome page. 2020-03-23 12:25:30 +02:00
Pēteris Caune
119965b432
Change the order of fields in slack notifications: start with description, project name and tags. Follow with period, last ping, total pings. 2020-03-23 12:23:25 +02:00
Pēteris Caune
1baa8ad46d
Merge pull request #342 from sairam/patch-1
Introduce Project Name in Slack notification
2020-03-23 12:13:58 +02:00
Pēteris Caune
abebdca527
Docs: PING_URL substitution got lost during refactoring, adding it back 2020-03-23 12:01:40 +02:00
Pēteris Caune
da4cf5241e
Minor cleanup, update CHANGELOG 2020-03-23 11:54:41 +02:00
Pēteris Caune
ffc7ccddf2
Merge branch 'jameskirsop-api-single-check' 2020-03-23 11:42:31 +02:00
James Kirsop
613ef2d0cf Merge branch 'api-single-check' of https://github.com/jameskirsop/healthchecks into api-single-check 2020-03-23 11:39:23 +11:00
James Kirsop
456a80f1fa Adding tests and docs 2020-03-23 11:37:32 +11:00
James Kirsop
6373db8aa1 Changes to prototype this for testing with real data 2020-03-23 10:58:02 +11:00
Sai Ram Kunala
9c9be4f181
Update 'Project Name' to 'Project' 2020-03-20 10:50:20 +05:30
Pēteris Caune
25d7d5409f
Telegram integration returns more detailed error messages 2020-03-19 22:16:22 +02:00
Pēteris Caune
5f2c20e46b
Zulip integration returns more detailed error messages 2020-03-19 22:05:13 +02:00
Pēteris Caune
8c7f3977e2
OpsGenie integration returns more detailed error messages 2020-03-19 21:58:17 +02:00
Sai Ram Kunala
c9979cc125
Introduce Project Name in Slack notification 2020-03-14 17:48:51 +05:30
Pēteris Caune
50118d90c5
Remove an extra quote. 2020-03-11 16:47:26 +02:00
Pēteris Caune
b689c8aa4e
Experimental Zulip integration. Fixes #202 2020-03-11 16:38:40 +02:00
Pēteris Caune
f352efdd5f
Experimental Zulip integration. Fixes #202 2020-03-11 16:38:29 +02:00
Pēteris Caune
1cb2ec16fb
Fix wording 2020-03-10 15:53:06 +02:00
Pēteris Caune
5d513658e3
Adding Docs > Cloning Checks 2020-03-10 15:43:34 +02:00
Pēteris Caune
bf1294a100
Docs / Shell scripts: add "Auto-provisioning New Checks" section 2020-03-09 18:05:21 +02:00
Pēteris Caune
ab692236eb
Fix selectize initialization when the project has 0 existing tags. 2020-03-09 14:39:03 +02:00
Pēteris Caune
26ad94d068
If the project has no integrations, show an appropriate message in the Details page, "Notification Methods" section. 2020-03-09 12:57:24 +02:00
Pēteris Caune
c8ebf73058
Merge pull request #340 from healthchecks/snyk-fix-51c0d13c6d0c4055f57c0eb711c34f20
[Snyk] Security upgrade django from 3.0.3 to 3.0.4
2020-03-09 10:17:35 +02:00
Pēteris Caune
6147451851
JS cleanup. 2020-03-09 10:16:39 +02:00
Pēteris Caune
3e25e5c242
Set the correct SMS limit when cancelling a paid plan. 2020-03-09 09:50:48 +02:00
snyk-bot
75cb95ffec fix: requirements.txt to reduce vulnerabilities
The following vulnerabilities are fixed by pinning transitive dependencies:
- https://snyk.io/vuln/SNYK-PYTHON-DJANGO-559326
2020-03-05 22:17:56 +00:00
Pēteris Caune
fcf11d5b4f
Reduce the number of SQL queries in the "Check Details" page. 2020-03-05 16:15:02 +02:00
Pēteris Caune
eb7f51f6f5
Focus the "name" input in the "Add Project" modal. 2020-03-05 16:05:06 +02:00
Pēteris Caune
00810ff123
Use Selectize.js for entering tags. Fixes #324 2020-03-05 15:49:42 +02:00
Pēteris Caune
35e476be59
Document more response codes. 2020-03-04 12:12:38 +02:00
Pēteris Caune
2e30d349aa
Tweak CSS for form controls in focused state. 2020-03-04 11:42:50 +02:00
Pēteris Caune
db9593c571
Unused, removing. 2020-03-02 16:43:47 +02:00
Pēteris Caune
ccba5e8731
Fix default values for timeout and grace parameters in API reference. 2020-03-02 13:50:27 +02:00
Pēteris Caune
dab0c4200e
API reference in Markdown 2020-03-02 13:37:29 +02:00
Pēteris Caune
516143de8a
Import hc.front.forms instead of importing each form individually 2020-03-02 10:12:57 +02:00
Pēteris Caune
22ef024885
Use secrets.token_urlsafe 2020-03-02 10:04:41 +02:00
Pēteris Caune
8bbf85a397
Remove Profile.current_project field. Fixes #336 2020-03-02 09:57:39 +02:00
Pēteris Caune
dd3820c0d5
_get_check_for_user and _get_channel_for_user are always be used with an authenticated user, so don't need to handle the unauthenticated case. 2020-03-01 22:45:33 +02:00
Pēteris Caune
4bcfba728e
Use unittest.mock 2020-03-01 22:30:12 +02:00
Pēteris Caune
d3ee9bae0e
Fix typo 2020-02-28 10:28:45 +02:00
Pēteris Caune
490362638f
Documentation: notes about resource limits 2020-02-27 17:51:22 +02:00
Pēteris Caune
dab15c3b8c
Link integration setup instructions from the welcome page (only the ones that don't require authentication: Slack, Pushover, PagerDuty Connect, Telegram) 2020-02-27 16:32:31 +02:00
Pēteris Caune
29e016d0fc
Update Telegram instructions. Fix redirect after login when adding Telegram integration. 2020-02-27 15:52:00 +02:00
Pēteris Caune
0c9c453ea0
Profile.current_project not used any more, remove last remaining references. cc: #336 2020-02-27 12:34:21 +02:00
Pēteris Caune
93b48ce720
In setup instructions, show an additional "log ina adn go to the Integrations" page for logged-out users 2020-02-27 12:16:42 +02:00
Pēteris Caune
9389408cbc
The "require_setting" decorator and more tests. 2020-02-27 11:35:18 +02:00
Pēteris Caune
dc373dc054
CSS counters for integration setup instructions. 2020-02-27 11:24:12 +02:00
Pēteris Caune
b5b5c58d77
Split "Add Pagerduty Connect" in three views for clarity. 2020-02-27 10:28:14 +02:00
Pēteris Caune
157711bc95
Reduce usage of Profile.current_project cc: #336 2020-02-26 10:56:17 +02:00
Pēteris Caune
6a0c90853b
request.project is now unused, removing 2020-02-26 10:37:19 +02:00
Pēteris Caune
9c3f7101db
Don't use request.project in the pricing page cc: #336 2020-02-26 10:27:45 +02:00
Pēteris Caune
bb808852d9
Reduce usage of request.project cc: #336 2020-02-25 15:39:54 +02:00
Pēteris Caune
318934697f
Remove last references of the hc-channels route. 2020-02-25 15:26:33 +02:00
Pēteris Caune
f2375f9f45
Don't redirect to /integrations/, redirect to /project/<uuid>/integrations/ 2020-02-25 15:19:20 +02:00
Pēteris Caune
7060d49306
The "Add Telegram" page shows a project picker. cc: #336 2020-02-25 14:51:39 +02:00
Pēteris Caune
acce0808ce
Project code in URL for the "Add Slack" page. cc: #336 2020-02-25 14:22:34 +02:00
Pēteris Caune
dee189be33
Project code in URL for the "Add Trello" page. cc: #336 2020-02-25 11:24:32 +02:00
Pēteris Caune
26757c6785
Clean up Pushover validation. 2020-02-25 11:05:52 +02:00
Pēteris Caune
f6f2b18c5d
Project code in URL for the "Add Pushover" page. cc: #336 2020-02-25 10:48:58 +02:00
Pēteris Caune
ea333f7ac1
Project code in URL for the "Add PagerDuty (Connect)" page. cc: #336 2020-02-25 10:14:42 +02:00
Pēteris Caune
f13ad875a1
Project code in URL for the "Add Discord" page. cc: #336 2020-02-25 09:57:11 +02:00
James Kirsop
d88f99a712 Changes to prototype this for testing with real data 2020-02-25 12:48:54 +11:00
Pēteris Caune
38bd84cc91
Project code in URL for the "Add Pushbullet" page. cc: #336 2020-02-21 17:31:17 +02:00
Pēteris Caune
44819cb555
Project code in URL for the "Add PagerDuty" page. cc: #336 2020-02-21 15:47:45 +02:00
Pēteris Caune
81f9a604e1
Project code in URL for the "Add Shell" page. cc: #336 2020-02-21 15:44:55 +02:00
Pēteris Caune
88f2a01182
Project code in URL for the "Add Apprise" page. cc: #336 2020-02-21 15:40:56 +02:00
Pēteris Caune
056134f2de
Project code in URL for the "Add WhatsApp" page. cc: #336 2020-02-21 15:33:42 +02:00
Pēteris Caune
9f5c133719
Project code in URL for the "Add VictorOps" page. cc: #336 2020-02-21 15:30:01 +02:00
Pēteris Caune
250935006d
Project code in URL for the "Add SMS" page. cc: #336 2020-02-21 15:26:06 +02:00
Pēteris Caune
f6a7d46058
Project code in URL for the "Add Prometheus" page. cc: #336 2020-02-21 15:22:10 +02:00
Pēteris Caune
5fb5b05f2e
Project code in URL for the "Add Pagertree" page. cc: #336 2020-02-21 15:18:15 +02:00
Pēteris Caune
1f950feee1
Fix Matrix test case. 2020-02-21 15:14:24 +02:00
Pēteris Caune
0ea2369dc0
Project code in URL for the "Add Pagerteam" page. cc: #336 2020-02-21 15:08:53 +02:00
Pēteris Caune
a6d497b21e
Project code in URL for the "Add OpsGenie" page. cc: #336 2020-02-21 15:03:26 +02:00
Pēteris Caune
d0b77febbc
Project code in URL for the "Add MS Teams" page. cc: #336 2020-02-21 14:58:22 +02:00
Pēteris Caune
70ff6c53e4
Project code in URL for the "Add Mattermost" page. cc: #336 2020-02-21 14:54:17 +02:00
Pēteris Caune
f8758e39ea
Project code in URL for the "Add Matrix" page. cc: #336 2020-02-21 14:44:44 +02:00
Pēteris Caune
59f5b7a5f5
Project code in URL for the "Add Webhook" page. cc: #336 2020-02-21 14:29:05 +02:00
Pēteris Caune
ea423e5420
Project code in URL for the "Integrations" and the "Add Email" pages. cc: #336 2020-02-21 14:15:13 +02:00
Pēteris Caune
9e82cbb412
Adding HealthChecksIOStatusReport in Third-Party resources. 2020-02-20 12:23:52 +02:00
Pēteris Caune
99bdc0ec8c
Tweak the integrations grid size in the welcome page. 2020-02-20 11:33:41 +02:00
Pēteris Caune
b5a4dada43
Add Prometheus in the welcome page. 2020-02-20 11:14:14 +02:00
Pēteris Caune
5e051d53f8
Validate channel identifiers before creating/updating a check. Fixes #335 2020-02-20 10:43:40 +02:00
Pēteris Caune
cde1f50ac2
API: update check's "alert_after" field when changing schedule 2020-02-19 12:45:33 +02:00
Pēteris Caune
fb527e4ed8
Security: check channel ownership when setting check's channels via API 2020-02-19 12:19:51 +02:00
Pēteris Caune
435659166c
Don't let SuspiciousOperation bubble up when validating channel ids in API 2020-02-19 11:43:42 +02:00
Pēteris Caune
7a0f3421dd
Setup instructions for Prometheus. 2020-02-18 16:48:01 +02:00
Pēteris Caune
3092eaf88d
Markdown with Pygments 2.4 and later wraps code in <code> tags (https://github.com/Python-Markdown/markdown/pull/862).
Reset CSS for code tags inside pre blocks.
2020-02-18 15:03:16 +02:00
Pēteris Caune
e52ac9af91
Put API key in the path (not query string) cc: #300 2020-02-14 16:39:31 +02:00
Pēteris Caune
12b946acf3
Experimental Prometheus metrics endpoint. cc: #300 2020-02-14 16:12:13 +02:00
Pēteris Caune
0ff4bd01e0
Improved UI to invite users from account's other projects. Fixes #258.
The team size limit is applied to the number of distinct users across all projects. Fixes #332.
2020-02-14 13:05:21 +02:00
Pēteris Caune
683dda9c5d
The "render_docs" command checks if markdown and pygments is installed. cc: #329 2020-02-14 10:16:43 +02:00
Pēteris Caune
82d61335b0
The "render_docs" command checks if markdown and pygments is installed. cc: #329 2020-02-14 10:14:29 +02:00
Pēteris Caune
174e5a7935
Update CHANGELOG for v1.13.0 2020-02-13 10:34:25 +02:00
Pēteris Caune
15b9611c5a
Show a warning in project's top navigation if the project has no configured integrations. Fixes #327 2020-02-13 10:29:01 +02:00
Pēteris Caune
c3608ac07c
Use t.me/username URL in the "Add Telegram" page. 2020-02-13 09:30:19 +02:00
Pēteris Caune
8ace6d5481
Merge pull request #328 from healthchecks/dependabot/pip/django-3.0.3
Bump django from 3.0.1 to 3.0.3
2020-02-13 09:25:25 +02:00
dependabot[bot]
ff383729cf
Bump django from 3.0.1 to 3.0.3
Bumps [django](https://github.com/django/django) from 3.0.1 to 3.0.3.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.0.1...3.0.3)

Signed-off-by: dependabot[bot] <support@github.com>
2020-02-11 21:12:35 +00:00
Pēteris Caune
c8ccd89af2
In hc.front.views.ping_details, if a ping does not exist, return a friendly message 2020-02-11 09:55:30 +02:00
Pēteris Caune
b0b6ee3149
In hc.front.views.ping_details, if a ping does not exist, return 404 instead of 500 2020-02-11 09:44:02 +02:00
Pēteris Caune
ccd30ac239
Stricter cron validation, reject schedules like "At midnight of February 31" 2020-02-07 11:38:50 +02:00
Pēteris Caune
4f6f1d9f66
Fix sendalerts crash loop when encountering a bad cron schedule 2020-02-07 10:36:45 +02:00
Pēteris Caune
ac4f1ca059
Log slow sendalerts.notify runs to stdout 2020-02-06 11:21:28 +02:00
Pēteris Caune
4a7074418a
Track the time spent sending notifications for each flip 2020-02-06 11:11:12 +02:00
Pēteris Caune
1b8460f39f
"Projects and Teams" in docs 2020-02-05 17:23:21 +02:00
Pēteris Caune
50280875cd
Typo 2020-02-05 11:32:01 +02:00
Pēteris Caune
9f2638bf72
The sendalerts commands measures notification dwell time and reports it over statsd protocol. Experimental, may go away in a future commit. 2020-02-05 11:25:06 +02:00
Pēteris Caune
5d9944873c
Don't trigger "down" notifications when changing schedule interactively in web UI 2020-02-05 10:31:20 +02:00
Pēteris Caune
6bc4948d00
Removing obsolete comment: the index is defined in hc.api.models.Check.Meta 2020-02-04 15:32:25 +02:00
Pēteris Caune
3048a20f9b
link rel="canonical" in the sign in page 2020-02-04 11:29:38 +02:00
Pēteris Caune
b1bffde3d6
Add SmartCronHelper 2020-02-04 11:26:49 +02:00
Pēteris Caune
e29b2387de
Illustrations in "Measuring script run time" page 2020-02-04 11:22:30 +02:00
Pēteris Caune
272360336b
"Configuring notifications" in docs 2020-02-03 17:41:06 +02:00
Pēteris Caune
1e721b8bcd
Docs: full width illustrations on smaller screens 2020-02-03 16:08:47 +02:00
Pēteris Caune
4bdc893fe0
Tweak footer height to avoid vertical scrollbar. 2020-02-03 16:03:16 +02:00
Pēteris Caune
5433cb1798
Fix README instructions on accessing Django admin (must log in first, then go to admin) 2020-02-03 11:28:39 +02:00
Pēteris Caune
b7d6f1bb30
link rel="canonical" on the welcome page 2020-02-03 11:17:24 +02:00
Pēteris Caune
f51a0a257e
Don't delete customer data in braintree when closing account.
Need customer data to stay in braintree until the end of each month for tax reports.
2020-02-03 11:11:21 +02:00
Pēteris Caune
b8cf428899
Merge pull request #325 from samyerkes/master
Updated default port in readme
2020-01-31 09:43:45 +02:00
Sam
b91f11588c
Merge pull request #1 from samyerkes/samyerkes-patch-1
Updated default port in readme
2020-01-29 20:56:20 -05:00
Sam
319d4528bb
Updated default port in readme
The default port after following the directions is actually 8000 instead of 8080.
2020-01-29 20:55:22 -05:00
Pēteris Caune
b8c0fd0eb9
Fix links to documentation. 2020-01-29 14:17:58 +02:00
Pēteris Caune
e2fe2edcc1
Title tags for documentation pages. 2020-01-29 13:54:54 +02:00
Pēteris Caune
564f69aca5
Adding shell example 2020-01-29 13:39:41 +02:00
Pēteris Caune
d29b0050a3
Fix endpoint address. 2020-01-29 12:45:00 +02:00
Pēteris Caune
dbd21c325d
Docs: "HTTP API" page 2020-01-29 12:43:35 +02:00
Pēteris Caune
d7de6476b7
Tweaking shell script examples 2020-01-28 16:44:32 +02:00
Pēteris Caune
a276c24dd3
Docs overhaul WIP 2020-01-28 14:07:06 +02:00
Pēteris Caune
74ab0d1931
Update CHANGELOG 2020-01-23 17:55:40 +02:00
Pēteris Caune
3e2ae02388
Added an example of capturing and submitting log output. Fixes #315 2020-01-23 17:53:23 +02:00
Pēteris Caune
f41c78e40f
Serve the introduction page at /docs/ 2020-01-23 16:58:28 +02:00
Pēteris Caune
50c8c153ea
Documentation in Markdown. 2020-01-23 16:04:15 +02:00
Pēteris Caune
7cf324872c
Replace the gear icon with three horizontal dots icon. Fixes #322.
Add a Pause button in the checks list. Fixes #312
2020-01-21 11:57:17 +02:00
Pēteris Caune
cdad632082
Show sub-second durations with higher precision, 2 digits after decimal point. Fixes #321 2020-01-17 14:41:41 +02:00
Pēteris Caune
77033760f9
Make sure Check.last_ping and Ping.created timestamps match exactly 2020-01-17 14:30:32 +02:00
Pēteris Caune
58a118c494
Make Ping.body size limit configurable. Fixes #301 2020-01-17 12:44:39 +02:00
Pēteris Caune
eae8d122b7
Update changelog. 2020-01-16 09:43:17 +02:00
Pēteris Caune
96797f6786
Merge pull request #320 from jerrykan/matrix_alias_length
Increase allowable length of Matrix room alias
2020-01-16 09:40:54 +02:00
John Kristensen
819aa227e9 Increase allowable length of Matrix room alias
The existing 40 character limit prevents using the integration will
Matrix servers that might have a fairly lengthy hostname (ie.
'matrix.internal.example.com' would only allow 12 characters for the
room name or ID, and room IDs are 19 characters long).

Increasing the `max_length` to `100` is still fairly arbitrary but it
does match up with the `max_length` of the `name` field of the `Channel`
model and should cover most instances.
2020-01-16 13:38:47 +11:00
Pēteris Caune
b8108906f4
hc.api.views.bounce updates Channel.last_error 2020-01-08 11:14:34 +02:00
Pēteris Caune
c521b44d20
hc.api.views.bounce handles transient email bounces (logs error, does not disable the integration) 2020-01-08 10:50:29 +02:00
Pēteris Caune
74ad152cc5
For superusers, show "Site Administration" in top navigation, note in README. Fixes #317 2020-01-07 12:15:09 +02:00
Pēteris Caune
c4edb415a2
Removing debug statement. 2020-01-07 11:47:53 +02:00
Pēteris Caune
012ad88bb3
createsuperuser management command requires an unique email address (#318) 2020-01-07 11:46:50 +02:00
Pēteris Caune
4ee2646539
Show a red "!" in project's top navigation if any integration is not working 2020-01-03 13:15:24 +02:00
Pēteris Caune
8e455965c4
Update changelog for v1.12.0 2020-01-02 12:38:11 +02:00
Pēteris Caune
52a178242b
2019 -> 2020. Cheers! 2020-01-02 10:10:20 +02:00
Pēteris Caune
18154dd6de
django-compressor==2.4, psycopg2==2.8.4 2020-01-02 10:08:44 +02:00
Pēteris Caune
3649c500d2
Don't allow adding email integrations with both "up" and "down" unchecked 2019-12-27 17:25:37 +02:00
Pēteris Caune
38ed309a3c
Don't allow adding webhook integrations with both URLs blank 2019-12-27 17:13:44 +02:00
Pēteris Caune
84a4de32cc
Remove legacy webhook formats (newline-separated fields and the post_data key) from the Channel model 2019-12-27 15:07:15 +02:00
Pēteris Caune
6ebae33579
Fix "Send Test Notification" for webhooks that only fire on checks going up 2019-12-27 14:36:32 +02:00
Pēteris Caune
be286518b7
For webhook integration, validate each header line separately 2019-12-27 13:56:33 +02:00
Pēteris Caune
057a6fe56b
Django 3.0.1 2019-12-19 15:59:36 +02:00
Pēteris Caune
830681d8f8
Specify encoding when reading CHANGELOG.md. Fixes #314 2019-12-19 09:59:10 +02:00
Pēteris Caune
0d2c6217d3
Auto-submit the unsubscribe confirmation form only if signature is more than 5 minutes old. Idea from https://stackoverflow.com/questions/59281750/strategies-to-prevent-email-scanners-from-activating-unsubscribe-links/59381066#59381066 2019-12-18 16:10:30 +02:00
Pēteris Caune
66c9fb33ad
Don't install django-compressor as editable package 2019-12-18 10:05:31 +02:00
Pēteris Caune
d9776e1340
Update pytz 2019-12-18 09:19:46 +02:00
Pēteris Caune
bffb51357e
Add desc to hc.api.schemas.check 2019-12-18 09:11:34 +02:00
Pēteris Caune
9697fc1b45
Merge pull request #313 from brammeleman/set-description
set/update the checks description through the API
2019-12-18 09:05:46 +02:00
Bram Daams
1b3d7e8c0a being able to set/update the description of a check when creating/updating using the api 2019-12-17 15:47:13 +01:00
Pēteris Caune
d6be955fa7
Silence stdout output from management commands during tests 2019-12-11 15:35:23 +02:00
Pēteris Caune
15ba415298
senddeletionnotices command skips profiles with recent last_active_date 2019-12-11 15:24:51 +02:00
Pēteris Caune
01bb03c889
django-compressor doesn't have a Django 3 compatible release yet. Use a development version temporarily. Details: https://github.com/django-compressor/django-compressor/issues/963 2019-12-11 13:31:55 +02:00
Pēteris Caune
b72979522b
Django 3 supports Python 3.6+. Adding the Py3.6 requirement to README. 2019-12-11 13:08:35 +02:00
Pēteris Caune
2a8e7ee766
Django 3.0 2019-12-11 13:05:25 +02:00
Pēteris Caune
eafff677d9
Don't auto-submit the unsubscribe form. Email security scanners like Office 365 Enterprise open links and *execute JS* causing users to automatically unsubscribe the first time they receive an email. Can't think of a sane fix for this :-( 2019-12-10 10:41:10 +02:00
Pēteris Caune
f7496fb8cf
Add List-Unsubscribe-Post email header 2019-12-10 09:44:51 +02:00
Pēteris Caune
0addbac7ba
Remove unused ask=1 parameters. 2019-12-10 09:27:30 +02:00
Pēteris Caune
8d81d27af3
Unsubscribe links serve a form, and require HTTP POST to actually unsubscribe 2019-12-10 09:14:54 +02:00
Pēteris Caune
4ee92a44ff
Unsubscribe is CSRF exempt. 2019-12-09 16:14:50 +02:00
Pēteris Caune
f9c61dad23
Fix List-Unsubscribe email header value: add angle brackets 2019-12-09 14:04:14 +02:00
Pēteris Caune
1cdb6e6d1d
Don't set CSRF cookie on first visit. Signup is exempt from CSRF protection. 2019-12-06 08:58:32 +02:00
Pēteris Caune
22d4d55340
Added support for Shields.io badges. cc: #304, #305 2019-12-05 12:27:37 +02:00
Pēteris Caune
838aee6bdd
Show Healthchecks version in Django admin header cc: #306 2019-12-03 17:41:58 +02:00
Pēteris Caune
5f47161e5e
staticfiles -> static 2019-12-03 09:59:36 +02:00
Pēteris Caune
87b232074c
Django 2.2.8 2019-12-02 12:30:52 +02:00
Pēteris Caune
7b32e9ef2c
Remove unused class="update-timeout-title" 2019-11-27 16:38:11 +02:00
Pēteris Caune
da095f2403
Merge branch 'master' of github.com:healthchecks/healthchecks 2019-11-27 16:34:09 +02:00
Pēteris Caune
3f19181028
"Filtering Rules" dialog, an option to require HTTP POST. Fixes #297 2019-11-27 16:33:36 +02:00
Pēteris Caune
87d75505fe
Merge pull request #307 from SuperSandro2000/patch-1
Add a note in README to run db migrations in production
2019-11-25 13:32:23 +02:00
Sandro
25f959c44b
Add hint to run db migration in production 2019-11-25 12:07:07 +01:00
Pēteris Caune
89a5fbb7f9
Optimize icons 2019-11-22 12:56:20 +02:00
Pēteris Caune
2893e370b6
Update CHANGELOG for release. 2019-11-22 12:03:50 +02:00
Pēteris Caune
1b005b6a9f
Update Changelog. 2019-11-22 11:43:47 +02:00
Pēteris Caune
5ab8486788
Update PagerDuty Connect setup illustrations. 2019-11-22 11:42:29 +02:00
Pēteris Caune
0349a3997b
PagerDuty event payload does not need the "vendor" key. 2019-11-22 11:29:09 +02:00
Pēteris Caune
f6d36b3491
Alternate flow for setting up PagerDuty integration, without using PD Connect 2019-11-22 11:17:14 +02:00
Pēteris Caune
d06721ab58
Rename "add_pd" to "add_pdc" (PagerDuty Connect). 2019-11-22 10:43:13 +02:00
Pēteris Caune
7c1b9c4b96
Rename "add_pd" to "add_pdc" (PagerDuty Connect). 2019-11-22 10:40:57 +02:00
Pēteris Caune
01955e4f99
Add MS Teams and Shell Commands to the list of integrations on Welcome page. 2019-11-21 16:01:41 +02:00
Pēteris Caune
98ba51f44f
Use hc.lib.string.replace for webhooks too.
hc.lib.string.replace only replaces placeholders that appear in the original template. It ignores any placeholders that "emerge" while doing string substitutions. This is done mainly to avoid unexpected behavior when check names or tags contain dollar signs.
2019-11-20 17:44:41 +02:00
Pēteris Caune
e4646205cb
Use channel.get_kind_display() in more places. 2019-11-20 17:31:36 +02:00
Pēteris Caune
fbba2b585e
Update PagerDuty logo in the icon font as well. 2019-11-20 17:10:41 +02:00
Pēteris Caune
5556bf3035
Update PagerDuty logo. 2019-11-20 16:46:31 +02:00
Pēteris Caune
c54c70cab7
Auto-focus the name field in the "Integration Details" modal. 2019-11-20 16:14:39 +02:00
Pēteris Caune
91c93b6a95
Add "Shell Commands" integration. Fixes #302 2019-11-20 16:01:03 +02:00
Pēteris Caune
8d81ea8f9d
Add "Shell Commands" integration. Fixes #302 2019-11-20 16:00:53 +02:00
Pēteris Caune
f74860bc0c
Add Profile.last_active_date field for more accurate inactive user detection 2019-11-19 16:29:38 +02:00
Pēteris Caune
494fd9ffb7
Improve alert summaries in ping log 2019-11-19 15:29:38 +02:00
Pēteris Caune
84bc6e7b2c
Fix typo. 2019-11-14 16:30:07 +02:00
Pēteris Caune
2b4de95141
Cleaner MS Teams setup illustrations. 2019-11-14 15:34:01 +02:00
Pēteris Caune
dc84b7be01
Add Microsoft Teams integration. Fixes #135 2019-11-14 15:19:40 +02:00
Pēteris Caune
046a643b13
Add python 3.8 to .travis.yml -- let's see if it will work... 2019-11-07 14:18:45 +02:00
Pēteris Caune
9cbd3bfc5a
In monthly reports, no downtime stats for the current month (month has just started) 2019-11-06 10:41:14 +02:00
Pēteris Caune
052700a642
Make log events fit better on mobile screens. 2019-11-05 10:45:39 +02:00
Pēteris Caune
87495a74c6
Update changelog. 2019-11-05 09:57:53 +02:00
Pēteris Caune
05855c1c69
Make the "Details" screen fit better on mobile screens. 2019-11-05 09:53:22 +02:00
Pēteris Caune
7904908625
Fix footer height on mobile. 2019-11-05 09:52:58 +02:00
Pēteris Caune
a464154151
On mobile, don't show the "Last Ping" column, but show the gear (Details) button. Fixes #286 2019-11-05 09:52:32 +02:00
Pēteris Caune
7db11fa7aa
Fix the senddeletionnotices command to take into account the new default SMS limit. 2019-10-30 22:12:25 +02:00
Pēteris Caune
c13ec18a27
5 SMS & WhatsApp sends/mo for free plans 2019-10-30 18:31:10 +02:00
Pēteris Caune
2848076d87
Update changelog for 1.10.0 release 2019-10-21 15:04:26 +03:00
Pēteris Caune
3f36d31cde
Display the error field in notifications admin list view, don't load all checks in details view. 2019-10-18 17:22:50 +03:00
Pēteris Caune
66a6de70c0
Send email notification when monthly SMS sending limit is reached. Fixes #292 2019-10-18 17:15:02 +03:00
Pēteris Caune
488ab2cce7
Add a "Create a Copy" function for cloning checks Fixes #288 2019-10-18 12:03:46 +03:00
Pēteris Caune
a5827c6458
Add link to borgmatic in the "Third-Party Resources" page 2019-10-17 11:49:10 +03:00
Pēteris Caune
82fb4ddece
Update OpsGenie logo 2019-10-14 21:14:36 +03:00
Pēteris Caune
01fc8e423b
Update OpsGenie screenshots. 2019-10-14 20:47:56 +03:00
Pēteris Caune
1dea8b6050
Add support for OpsGenie EU region. Fixes #294 2019-10-14 20:31:25 +03:00
Pēteris Caune
4625196ded
Autofocus the email field in the signup form, and submit on enter key 2019-10-12 20:22:28 +03:00
Pēteris Caune
163b020116
Signup form sets the "auto-login" cookie to avoid an extra click during first login 2019-10-12 20:14:57 +03:00
Pēteris Caune
2bb769f7bb
Send monthly reports on 1st of every month, not randomly during the month 2019-10-12 20:07:09 +03:00
Pēteris Caune
391921d8af
Revert deterministic username generation feature – it causes problems when users change their email address. See #290 for details. 2019-10-12 11:37:06 +03:00
Pēteris Caune
6cd4e494e8
Add go example to "manage.py pygmentize" command.
Make sure the Go snippet shows up in the welcome page and also in the check details page.
2019-10-07 15:10:36 +03:00
Pēteris Caune
ad731dfe0e
Merge pull request #293 from omurbekjk/feature/golang-example-added-for-code-snippets
feature: golang code snippet added
2019-10-07 15:00:08 +03:00
omurbekjk
fbc217ef35 feature: golang http get request changes to head 2019-10-07 16:55:09 +06:00
omurbekjk
3d9261c7c4 feature: golang code snippet added 2019-10-03 17:22:36 +06:00
Pēteris Caune
b0db5181d8
Don't validate plan_id if it has not changed from the old value (when updating payment method). 2019-10-02 17:28:20 +03:00
Pēteris Caune
f9ec5b482f
Upgrade to Django 2.2.6. Fixes #284 2019-10-01 17:17:36 +03:00
Pēteris Caune
41a0871452
Generate usernames as uuid3(const, email). Prevents multiple accts with the same email. Prevent double-clicking the submit button in signup form. Fixes #290 2019-09-30 16:40:45 +03:00
Pēteris Caune
335c73d6a2
Upgrade to psycopg2 2.8.3. Fixes #289 2019-09-30 16:08:37 +03:00
Pēteris Caune
ca5e19fd2d
Don't throw an exception if user's current project is unset. 2019-09-18 14:56:58 +03:00
Pēteris Caune
accdfb637b
Remove PDF invoice generation bits - these are unlikely to ever be useful in the open source version. 2019-09-15 18:39:32 +03:00
Pēteris Caune
34925f2cdf
Django compressor 2.2 -> 2.3 2019-09-12 11:01:45 +03:00
Pēteris Caune
0d2736059d
"Sign Up" link in top nav. 2019-09-12 11:01:21 +03:00
Pēteris Caune
4755e1c9da
Exclude sqlite from Travis builds. There's a Django bug that breaks sqlite (https://code.djangoproject.com/ticket/30754#ticket) and I don't want all builds to fail (and potentially mask other issues) until the upstream bugfix is released. 2019-09-09 14:53:21 +03:00
Pēteris Caune
5d8c5637b6
Wording tweaks 2019-09-09 14:47:05 +03:00
Pēteris Caune
6e4e7d737f
Merge pull request #287 from anymuster2/master
Adjusted Pushover notes for clarity on behaviour
2019-09-09 14:34:31 +03:00
anymuster2
4f2b5772df
Adjusted Pushover notes for clarity on behaviour 2019-09-09 21:10:16 +10:00
Pēteris Caune
7fffb95c96
load staticfiles -> load static 2019-09-09 10:55:38 +03:00
Pēteris Caune
c4bb20e3d5
Fixing typo. cc: #285 2019-09-05 08:17:04 +03:00
Pēteris Caune
69d4932194
Add the "Running in Production" section. cc: #283 2019-09-04 16:36:15 +03:00
Pēteris Caune
0d924f4627
Add the "Last Duration" field in the "My Checks" page. Add "last_duration" attribute to the Check API resource. Fixes #257 2019-09-03 13:46:41 +03:00
1092 changed files with 62143 additions and 16853 deletions

1
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1 @@
github: [healthchecks]

33
.github/workflows/coverage.yml vendored Normal file
View file

@ -0,0 +1,33 @@
name: Coverage
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
test:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: Install Dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y libcurl4-openssl-dev libpython3-dev libssl-dev
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install apprise braintree coverage coveralls minio
- name: Run Tests
env:
SECRET_KEY: dummy-key
run: coverage run --omit=*/tests/*,*/migrations/* --source=hc manage.py test
- name: Coveralls
run: coveralls --service=github
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

28
.github/workflows/mypy.yml vendored Normal file
View file

@ -0,0 +1,28 @@
name: Mypy
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
main:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: Install Dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y libcurl4-openssl-dev libpython3-dev libssl-dev
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install apprise braintree minio
pip install mypy==1.13.0 django-stubs types-braintree types-pycurl==7.45.2.20240311 types-Markdown types-pygments
touch hc/local_settings.py
- name: Run Mypy
run: mypy --strict hc

View file

@ -0,0 +1,44 @@
name: Publish Docker image
on:
release:
types: [published]
workflow_dispatch:
jobs:
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v4
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
with:
images: healthchecks/healthchecks
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
image: tonistiigi/binfmt:qemu-v8.1.5
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
file: docker/Dockerfile
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

52
.github/workflows/tests.yml vendored Normal file
View file

@ -0,0 +1,52 @@
name: Tests
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
test:
runs-on: ubuntu-20.04
strategy:
matrix:
db: [sqlite, postgres, mysql]
python-version: ["3.10", "3.11", "3.12"]
include:
- db: postgres
db_user: runner
db_password: ''
- db: mysql
db_user: root
db_password: root
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Start MySQL
if: matrix.db == 'mysql'
run: sudo systemctl start mysql.service
- name: Start PostgreSQL
if: matrix.db == 'postgres'
run: |
sudo systemctl start postgresql.service
sudo -u postgres createuser -s runner
- name: Install Dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y libcurl4-openssl-dev libpython3-dev libssl-dev
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install apprise minio mysqlclient
- name: Run Tests
env:
DB: ${{ matrix.db }}
DB_USER: ${{ matrix.db_user }}
DB_PASSWORD: ${{ matrix.db_password }}
SECRET_KEY: dummy-key
run: python manage.py test

4
.gitignore vendored
View file

@ -2,6 +2,8 @@ __pycache__/
*.pyc
.coverage
.env
.venv
.zed
hc.sqlite
hc/local_settings.py
static-collected
static-collected

View file

@ -1,21 +0,0 @@
dist: xenial
language: python
python:
- "3.5"
- "3.6"
- "3.7"
install:
- pip install -r requirements.txt
- pip install braintree coveralls mock mysqlclient reportlab apprise
env:
- DB=sqlite
- DB=mysql
- DB=postgres
services:
- mysql
addons:
postgresql: "9.6"
script:
- coverage run --omit=*/tests/* --source=hc manage.py test
after_success: coveralls
cache: pip

View file

@ -1,7 +1,782 @@
# Changelog
All notable changes to this project will be documented in this file.
## 1.9.0 - 2019-09-03
## v3.9 - 2024-12-20
### Improvements
- Change the default value of ALLOWED_HOSTS from "*" to the domain part of SITE_ROOT
### Bug Fixes
- Fix fetchstatus.py (again) to handle SITE_ROOT with a path (#1108)
## v3.8.2 - 2024-12-19
### Improvements
- Update notification templates to include failure reason (#1069)
### Bug Fixes
- Fix fetchstatus.py to handle SITE_ROOT with a path (#1107)
## v3.8.1 - 2024-12-13
### Improvements
- Update Dockerfile to use Python 3.13.1
- Improve Matrix notifications (include tags, period, last ping type etc.)
## v3.8 - 2024-12-09
### Improvements
- Rewrite the docker/fetchstatus.py script to reduce Docker container CPU use (#1071)
- Update Dockerfile to use Python 3.13
- Update CustomHeaderMiddleware to normalize email addresses to lower case (#1074)
- Add data migration to convert existing user account emails to lower case
- Update email alerts to mention failure reason (#1069)
- De-emphasize the unsubscribe link in email notifications
- In the checks list, move the "Add Check" button to the top of the page
- Implement filtering by status in the checks list page
- Increase ntfy.sh topic max length to 64
- Implement support for path in SITE_ROOT, e.g. SITE_ROOT=http://example.org/hc (#1091)
### Bug Fixes
- Improve recipient address validation in the smtp listener (#1077)
## v3.7 - 2024-10-21
### Improvements
- Increase outgoing webhook timeout from 10 to 30 seconds
- Remove `pruneflips` management command (now cleaned up automatically)
- Remove `prunenotifications` management command (now cleaned up automatically)
- Update settings.py to read SECURE_PROXY_SSL_HEADER from env vars
- Remove LINE Notify onboarding form (as LINE Notify is shutting down on Apr 1, 2025)
- Make slider labels clickable in the "Update Period & Grace" dialog (#1039)
- Update the Signal integration to retry on network errors
### Bug Fixes
- Update sqlite settings to avoid "Database is locked" errors (#1057)
- Fix API to gracefully handle too long slugs
## v3.6 - 2024-09-04
### Security
- Upgrade to Django 5.1.1 (it fixes a vulnerability in `urlize` which we do use)
### Improvements
- Implement concurrent sending and `--num-workers` argument in `manage.py sendalerts`
- Upgrade from psycopg2 to psycopg 3.x
## v3.5.2 - 2024-08-21
### Bug Fixes
- Fix the Docker healthcheck script to supply correct Host header (#1051)
## v3.5.1 - 2024-08-20
### Bug Fixes
- Fix the Dockerfile for building arm/v7 docker image
## v3.5 - 2024-08-20
Important: this Healthchecks release is using Django 5.1, which has dropped support
for PostgreSQL 12. Therefore, the PostgreSQL image in the sample `docker-compose.yml`
file has been updated from `postgres:12` to `postgres:16`. PostgreSQL does not
automatically upgrade its data files between major version upgrades, you will need
to do this manually. Instructions:
https://github.com/healthchecks/healthchecks/tree/master/docker#upgrading-database
### Improvements
- Improve performance of loading ping body previews (#1023)
- Implement MS Teams Workflows integration (#1024)
- Add "uuid" field in API responses when read/write key is used (#1007)
- Update timezone dropdowns to show frequently used timezones at the top
- Update the "Set Password" page to reject very weak passwords
- Implement search by slug in the checks list (#1048)
- Add support for $SLUG placeholder in webhook payloads (#1049)
- Update Dockerfile to use HEALTHCHECK instruction and report container health (#1045)
### Bug Fixes
- Fix Check.ping() to lock the check before updating (#1023)
- Fix AJAX views to better handle user logging out
## v3.4 - 2024-06-20
### Improvements
- Show status changes (flips) in check's log page (#447)
- Implement dynamic favicon in the projects overview page (#971)
- Add support for system theme (#978, @moraj-turing)
- Improve Opsgenie notifications (include description, schedule, link etc.)
- Update the Discord integration to disable channel on HTTP 404 responses
- Update email notifications to include the timestamps of status flips
- Update the Sign In page to hide "Email Link" option if SMTP is not configured (#922)
- Update Slack integration to use channel name as the integration name (#1003)
- Update Ping Details dialog to also show formatted datetimes (#975)
- Add data migration to update legacy timezones to current canonical timezones
### Bug Fixes
- Fix hc.front.views.docs_search to handle words "AND", "OR", "NOT" as queries
- Fix integrations to not disclose check's code in incident data
- Fix integrations to include oncalendar schedules in notifications
- Fix a bug in the log page that caused log events to sometimes load twice
## v3.3 - 2024-04-03
### Improvements
- Add support for $NAME_JSON and $BODY_JSON placeholders in webhook payloads
- Update the WhatsApp integration to use Twilio Content Templates
- Add auto-refresh functionality to the Log page (#957, @mickBoat00)
- Redesign the "Status Badges" page
- Add support for per-check status badges (#853)
- Add "Last ping subject" field in email notifications
- Change the signup flow to accept registered users (and sign them in instead)
- Implement event type filtering in the Log page (#873)
- Implement dynamic favicon in the "Checks" and "Details" pages (#971, @princekhunt)
### Bug Fixes
- Fix Gotify integration to handle Gotify server URLs with paths (#964)
- Update notification templates to handle cases where check's last ping value is null
- Make statsd metrics collection optional (to enable, set STATSD_HOST env var)
## v3.2 - 2024-02-09
### Improvements
- Update Opsgenie instructions
- Update Spike.sh instructions
- Add system check to validate settings.SITE_ROOT (#895)
- Add tooltips to tag buttons in the checks list screen (#911)
- Improve Email - Keywoard Filtering docs (@mmomjian)
- Split the grace time input field into value/unit input group (#945, @mickBoat00)
- Add a system check to warn about MariaDB UUID migration (#929)
### Bug Fixes
- Increase uWSGI buffer size to allow requests with large cookies (#925)
- Fix crash when processing one-shot OnCalendar schedules
- Fix the handling of ping bodies > 2.5MB (#931)
- Fix crash when inviting team member but SMTP is not configured (@marlenekoh)
## v3.1 - 2023-12-13
### Improvements
- Update logging configuration to write logs to database (to table `logs_record`)
- Improve Pushover notifications (include tags, period, last ping type etc.)
- Implement audo-submit in TOTP entry screen (#905)
- Update the Splunk On-Call integration to disable channel on HTTP 404 responses
- Update the Slack integration to disable channel when Slack returns 400 "invalid_token"
- Update the Pushover integration to disable channel when Pushover reports invalid user
- Update Twilio integrations to disable channel on "Invalid 'To' Phone Number"
- Update the Signal integration to disable channel on UNREGISTERED_FAILURE
- Upgrade to Django 5.0
- Add support for systemd's OnCalendar schedules (#919)
### Bug Fixes
- Fix "Ping Details" dialog to handle email bodies not yet uploaded to object storage
- Fix webauthn registration failure on Firefox with Bitwarden extension
- Fix webauthn registration failure on Firefox < 119 with Ed25519 keys
## v3.0.1 - 2023-10-30
### Bug Fixes
- Fix sending test notification to a group integration
- Fix the Login form to not perform form validation in GET requests
- Fix special character escaping in ntfy notifications
- Fix "Edit ntfy integration" page to fill the existing token in the form
- Fix "Delete Check" and "Update Check" API calls to handle concurrent deletes
- Fix Signal transport to handle JSON-RPC messages with no ids
- Fix DST handling in Check.get_grace_start()
## v3.0 - 2023-10-16
This release drops support of Python 3.9 and below. The minimum required Python
version is 3.10.
### Improvements
- Add Channel.last_notify_duration field, use it in "sendalerts" for prioritization
- Update Telegram integration to treat "bot was blocked by the user" as permanent error
- Add "Time Zone" field in notifications that use the "Schedule" field (#863)
- Add bold and monospace text formatting in Signal notifications
- Update hourly/daily email reminders to only show checks in the "down" state (#881)
- Add support for ntfy access tokens (#879)
- Improve ntfy notifications (include tags, period, last ping type etc.)
- Add an "Account closed." confirmation message after closing an account
- Add monthly uptime percentage display in Check Details page (#773)
- Increase the precision of calculated downtime duration in check's details and reports
- Increase bottom margin for modal windows to work around Mobile Safari issue (#899)
- New integration: notification group (#894)
### Bug Fixes
- Fix "senddeletionnotices" to recognize "Supporter" subscriptions
- Fix "createsuperuser" to reject already registered email addresses (#880)
- Fix hc.accounts.views.check_token to handle non-UUID usernames (#882)
- Fix time interval formatting in Check Details page, downtime summary table
- Fix HTML escaping issue in Project admin
## v2.10 - 2023-07-02
### Improvements
- Configure logging to log unhandled exceptions to console even when DEBUG=False (#835)
- Make hc.lib.emails raise exceptions when EMAIL_ settings are not set
- Decouple check's name from slug, allow users to set hand-picked slugs
- Add /api/v3/ (adds ability to specify slug when creating or updating checks)
- Update Dockerfile to use Debian Bookworm as the base
- Implement optional check auto-provisioning when pinging by slug (#626)
- Add support for the $EXITSTATUS placeholder in webhook payloads (#826)
- Add API support for filtering checks by slug (#844)
- Add support for Telegram topics (#852)
- For cron checks, switch to using check's (not browser's) timezone to format dates
- Upgrade to cronsim 2.5 (adds support for "LW" in the day-of-month field)
### Bug Fixes
- Fix DB connection timeouts in `manage.py smtpd` (#847)
## v2.9.2 - 2023-06-05
### Bug Fixes
- Fix a crash in `manage.py smtpd` when stdin is not attached (#840)
## v2.9.1 - 2023-06-05
### Bug Fixes
- Fix the GHA workflow for building arm/v7 docker image
## v2.9 - 2023-06-05
### Improvements
- Switch from CssAbsoluteFilter to CssRelativeFilter (#822)
- Add statsd metric collection in hc.lib.s3.get_object()
- Upgrade to cronsim 2.4
- Update Signal notification template to include more data
- Add Profile.deletion_scheduled_deleted field, and UI banner when it's set
- Add support for specifying MessagingServiceSid when sending SMS and WA messages
- Update the smtpd management command to use the aiosmtpd library
- Add Rocket.Chat integration (#463)
### Bug Fixes
- Fix a race condition when pinging and deleting checks at the same time
- Fix the checks list to preserve filters when changing sort order (#828)
## v2.8.1 - 2023-04-11
### Bug Fixes
- Fix django-compressor warning with github_actions.html
## v2.8 - 2023-04-11
### Improvements
- Add GitHub Actions examples
- Update the Dockerfile to use Python 3.11
- Update the Ping Details dialog to show the "HTML" tab by default (#801)
- Add a "Switch Project" menu in top navigation
- Update Trello onboarding form to allow longer Trello auth tokens (#806)
- Remove L10N markup from base.html, and associated translations
- Add Arduino usage example
- Upgrade to Django 4.2
- Add email fallback for Signal notifications that hit rate limit
- Make warnings about no backup second factor more assertive
- Add cron expression tester and sample expressions in the cron cheatsheet page
### Bug Fixes
- Fix notification query in the Log page
## v2.7 - 2023-03-06
### Improvements
- Add last ping body in Mattermost notifications (#785)
- Improve the error message about rejected private IPs
- Update Docker image's uwsgi.ini to use SMTPD_PORT env var (#791)
- Update Telegram notification template to include more data
- Add CSRF protection in the signup view
### Bug Fixes
- Fix URL validation to allow hostnames with no TLD ("http://example") (#782)
- Add handling for ProtocolError exceptions in hc.lib.s3.get_object
- Fix a race condition in Check.ping method
- Fix the SameSite and Secure attributes on the "auto-login" cookie
- Fix the "Test" button in the Integrations screen for read-only users
- Add form double submit protection when registering a WebAuthn key
## v2.6.1 - 2023-01-26
### Improvements
- Improve Prometheus docs, add section "Available Metrics"
### Bug Fixes
- Fix a crash in the "createsuperuser" management command (#779)
## v2.6 - 2023-01-23
### Improvements
- Improve layout in "My Checks" for checks with long ping URLs (#745)
- Add support for communicating with signal-cli over TCP (#732)
- Add /api/v2/ (changes the status reporting of checks in started state) (#633)
- Update settings.py to read the ADMINS setting from an environment variable
- Add "Start Keyword" filtering for inbound emails (#716)
- Add rate limiting by client IP in the signup and login views
### Bug Fixes
- Fix the Signal integration to handle unexpected RPC messages better (#763)
- Fix special character encoding in Signal notifications (#767)
- Fix project sort order to be case-insensitive everywhere in the UI (#768)
- Fix special character encoding in project invite emails
- Fix check transfer between same account's projects when at check limit
- Fix wording in the invite email when inviting read-only users
- Fix login and signup views to make email enumeration harder
## v2.5 - 2022-12-14
### Improvements
- Upgrade to fido2 1.1.0 and simplify hc.lib.webauthn
- Add handling for ipv4address:port values in the X-Forwarded-For header (#714)
- Add a form for submitting Signal CAPTCHA solutions
- Add Duration field in the Ping Details dialog (#720)
- Update Mattermost setup instructions
- Add support for specifying a run ID via a "rid" query parameter (#722)
- Add last ping body in Slack notifications (#735)
- Add ntfy integration (#728)
- Add ".txt" suffix to the filename when downloading ping body (#738)
- Add API support for fetching ping bodies (#737)
- Change "Settings - Email Reports" page to allow manual timezone selection
### Bug Fixes
- Fix the most recent ping lookup in the "Ping Details" dialog
- Fix binary data handling in the hc.front.views.ping_body view
- Fix downtime summaries in weekly reports (#736)
- Fix week, month boundary calculation to use user's timezone
## v2.4.1 - 2022-10-18
### Bug Fixes
- Fix the GHA workflow for building arm/v7 docker image
## v2.4 - 2022-10-18
### Improvements
- Add support for EMAIL_USE_SSL environment variable (#685)
- Switch from requests to pycurl
- Implement documentation search
- Add date filters in the Log page
- Upgrade to cronsim 2.3
- Add support for the $BODY placeholder in webhook payloads (#708)
- Implement the "Clear Events" function
- Add support for custom topics in Zulip notifications (#583)
### Bug Fixes
- Fix the handling of TooManyRedirects exceptions
- Fix MySQL 8 support in the Docker image (upgrade from buster to bullseye) (#717)
## v2.3 - 2022-08-05
### Improvements
- Update Dockerfile to start SMTP listener (#668)
- Implement the "Add Check" dialog
- Include last ping type in Slack, Mattermost, Discord notifications
- Upgrade to cron-descriptor 1.2.30
- Add "Filter by keywords in the message body" feature (#653)
- Upgrade to HiDPI screenshots in the documentation
- Add support for the $JSON placeholder in webhook payloads
- Add ping endpoints for "log" events
- Add the "Badges" page in docs
- Add support for multiple recipients in incoming email (#669)
- Upgrade to fido2 1.0.0, requests 2.28.1, segno 1.5.2
- Implement auto-refresh and running indicator in the My Projects page (#681)
- Upgrade to Django 4.1 and django-compressor 4.1
- Add API support for resuming paused checks (#687)
### Bug Fixes
- Fix the display of ignored pings with non-zero exit status
- Fix a race condition in the "Change Email" flow
- Fix grouping and sorting in the text version of the report/nag emails (#679)
- Fix the update_timeout and pause views to create flips (for downtime bookkeeping)
- Fix the checks list to preserve selected filters when adding/updating checks (#684)
- Fix duration calculation to skip "log" and "ign" events
## v2.2.1 - 2022-06-13
### Improvements
- Improve the text version of the alert email template
### Bug Fixes
- Fix the version number displayed in the footer
## v2.2 - 2022-06-13
### Improvements
- Add address verification step in the "Change Email" flow
- Reduce logging output from sendalerts and sendreports management commands (#656)
- Add Ctrl+C handler in sendalerts and sendreports management commands
- Add notes in docs about configuring uWSGI via UWSGI_ env vars (#656)
- Implement login link expiration (login links will now expire in 1 hour)
- Add Gotify integration (#270)
- Add API support for reading/writing the subject and subject_fail fields (#659)
- Add "Disabled" priority for Pushover notifications (#663)
### Bug Fixes
- Update hc.front.views.channels to handle empty strings in settings (#635)
- Add logic to handle ContentDecodingError exceptions
## v2.1 - 2022-05-10
### Improvements
- Add logic to alert ADMINS when Signal transport hits a CAPTCHA challenge
- Implement the "started" progress spinner in the details pages
- Add "hc_check_started" metric in the Prometheus metrics endpoint (#630)
- Add a management command for submitting Signal rate limit challenges
- Upgrade to django-compressor 4.0
- Update the C# snippet
- Increase max displayed duration from 24h to 72h (#644)
- Add "Ping-Body-Limit" response header in ping API responses
### Bug Fixes
- Fix unwanted localization in badge SVG generation (#629)
- Update email template to handle not yet uploaded ping bodies
- Add small delay in transports.Email.notify to allow ping body to upload
- Fix prunenotifications to handle checks with missing pings (#636)
- Fix "Send Test Notification" for integrations that only send "up" notifications
## v2.0.1 - 2022-03-18
### Bug Fixes
- Fix the GHA workflow for building arm/v7 docker image
## v2.0 - 2022-03-18
This release contains a backwards-incompatible change to the Signal integration
(hence the major version number bump). Healthchecks uses signal-cli to deliver
Signal notifications. In the past versions, Healthchecks interfaced with
signal-cli over DBus. Starting from this version, Healthchecks interfaces
with signal-cli using JSON RPC. Please see README for details on how to set
this up.
### Improvements
- Update Telegram integration to treat "group chat was deleted" as permanent error
- Update email bounce handler to mark email channels as disabled (#446)
- Update Signal integration to use JSON RPC over UNIX socket
- Update the "Add TOTP" form to display plaintext TOTP secret (#602)
- Improve PagerDuty notifications
- Add Ping.body_raw field for storing body as bytes
- Add support for storing ping bodies in S3-compatible object storage (#609)
- Add a "Download Original" link in the "Ping Details" dialog
### Bug Fixes
- Fix unwanted special character escaping in notification messages (#606)
- Fix JS error after copying a code snippet
- Make email non-editable in the "Invite Member" dialog when team limit reached
- Fix Telegram bot to handle TransportError exceptions
- Fix Signal integration to handle UNREGISTERED_FAILURE errors
- Fix unwanted localization of period and grace values in data- attributes (#617)
- Fix Mattermost integration to treat 404 as a transient error (#613)
## v1.25.0 - 2022-01-07
### Improvements
- Implement Pushover emergency alert cancellation when check goes up
- Add "The following checks are also down" section in Telegram notifications
- Add "The following checks are also down" section in Signal notifications
- Upgrade to django-compressor 3.0
- Add support for Telegram channels (#592)
- Implement Telegram group to supergroup migration (#132)
- Update the Slack integration to not retry when Slack returns 404
- Refactor transport classes to raise exceptions on delivery problems
- Add Channel.disabled field, for disabling integrations on permanent errors
- Upgrade to Django 4
- Bump the min. Python version from 3.6 to 3.8 (as required by Django 4)
### Bug Fixes
- Fix report templates to not show the "started" status (show UP or DOWN instead)
- Update Dockerfile to avoid running "pip wheel" more than once (#594)
## v1.24.1 - 2021-11-10
### Bug Fixes
- Fix Dockerfile for arm/v7 - install all dependencies from piwheels
## v1.24.0 - 2021-11-10
### Improvements
- Switch from croniter to cronsim
- Change outgoing webhook timeout to 10s, but cap the total time to 20s
- Implement automatic `api_ping` and `api_notification` pruning (#556)
- Update Dockerfile to install apprise (#581)
- Improve period and grace controls, allow up to 365 day periods (#281)
- Add SIGTERM handling in sendalerts and sendreports
- Remove the "welcome" landing page, direct users to the sign in form instead
### Bug Fixes
- Fix hc.api.views.ping to handle non-utf8 data in request body (#574)
- Fix a crash when hc.api.views.pause receives a single integer in request body
## v1.23.1 - 2021-10-13
### Bug Fixes
- Fix missing uwsgi dependencies in arm/v7 Docker image
## v1.23.0 - 2021-10-13
### Improvements
- Add /api/v1/badges/ endpoint (#552)
- Add ability to edit existing email, Signal, SMS, WhatsApp integrations
- Add new ping URL format: /{ping_key}/{slug} (#491)
- Reduce Docker image size by using slim base image and multi-stage Dockerfile
- Upgrade to Bootstrap 3.4.1
- Upgrade to jQuery 3.6.0
### Bug Fixes
- Add handling for non-latin-1 characters in webhook headers
- Fix dark mode bug in selectpicker widgets
- Fix a crash during login when user's profile does not exist (#77)
- Drop API support for GET, DELETE requests with a request body
- Add missing @csrf_exempt annotations in API views
- Fix the ping handler to reject status codes > 255
- Add 'schemaVersion' field in the shields.io endpoint (#566)
## v1.22.0 - 2021-08-06
### Improvements
- Use multicolor channel icons for better appearance in the dark mode
- Add SITE_LOGO_URL setting (#323)
- Add admin action to log in as any user
- Add a "Manager" role (#484)
- Add support for 2FA using TOTP (#354)
- Add Whitenoise (#548)
### Bug Fixes
- Fix dark mode styling issues in Cron Syntax Cheatsheet
- Fix a 403 when transferring a project to a read-only team member
- Security: fix allow_redirect function to reject absolute URLs
## v1.21.0 - 2021-07-02
### Improvements
- Increase "Success / Failure Keywords" field lengths to 200
- Django 3.2.4
- Improve the handling of unknown email addresses in the Sign In form
- Add support for "... is UP" SMS notifications
- Add an option for weekly reports (in addition to monthly)
- Implement PagerDuty Simple Install Flow, remove PD Connect
- Implement dark mode
### Bug Fixes
- Fix off-by-one-month error in monthly reports, downtime columns (#539)
## v1.20.0 - 2021-04-22
### Improvements
- Django 3.2
- Rename VictorOps -> Splunk On-Call
- Implement email body decoding in the "Ping Details" dialog
- Add a "Subject" field in the "Ping Details" dialog
- Improve HTML email display in the "Ping Details" dialog
- Add a link to check's details page in Slack notifications
- Replace details_url with cloaked_url in email and chat notifications
- In the "My Projects" page, show projects with failing checks first
### Bug Fixes
- Fix downtime summary to handle months when the check didn't exist yet (#472)
- Relax cron expression validation: accept all expressions that croniter accepts
- Fix sendalerts to clear Profile.next_nag_date if all checks up
- Fix the pause action to clear Profile.next_nag_date if all checks up
- Fix the "Email Reports" screen to clear Profile.next_nag_date if all checks up
- Fix the month boundary calculation in monthly reports (#497)
## v1.19.0 - 2021-02-03
### Improvements
- Add tighter parameter checks in hc.front.views.serve_doc
- Update OpsGenie instructions (#450)
- Update the email notification template to include more check and last ping details
- Improve the crontab snippet in the "Check Details" page (#465)
- Add Signal integration (#428)
- Change Zulip onboarding, ask for the zuliprc file (#202)
- Add a section in Docs about running self-hosted instances
- Add experimental Dockerfile and docker-compose.yml
- Add rate limiting for Pushover notifications (6 notifications / user / minute)
- Add support for disabling specific integration types (#471)
### Bug Fixes
- Fix unwanted HTML escaping in SMS and WhatsApp notifications
- Fix a crash when adding an integration for an empty Trello account
- Change icon CSS class prefix to 'ic-' to work around Fanboy's filter list
## v1.18.0 - 2020-12-09
### Improvements
- Add a tooltip to the 'confirmation link' label (#436)
- Update API to allow specifying channels by names (#440)
- When saving a phone number, remove any invisible unicode characters
- Update the read-only dashboard's CSS for better mobile support (#442)
- Reduce the number of SQL queries used in the "Get Checks" API call
- Add support for script's exit status in ping URLs (#429)
- Improve phone number sanitization: remove spaces and hyphens
- Change the "Test Integration" behavior for webhooks: don't retry failed requests
- Add retries to the the email sending logic
- Require confirmation codes (sent to email) before sensitive actions
- Implement WebAuthn two-factor authentication
- Implement badge mode (up/down vs up/late/down) selector (#282)
- Add Ping.exitstatus field, store client's reported exit status values (#455)
- Implement header-based authentication (#457)
- Add a "Lost password?" link with instructions in the Sign In page
### Bug Fixes
- Fix db field overflow when copying a check with a long name
## v1.17.0 - 2020-10-14
### Improvements
- Django 3.1
- Handle status callbacks from Twilio, show delivery failures in Integrations
- Removing unused /api/v1/notifications/{uuid}/bounce endpoint
- Less verbose output in the `senddeletionnotices` command
- Host a read-only dashboard (from github.com/healthchecks/dashboard/)
- LINE Notify integration (#412)
- Read-only team members
- API support for setting the allowed HTTP methods for making ping requests
### Bug Fixes
- Handle excessively long email addresses in the signup form
- Handle excessively long email addresses in the team member invite form
- Don't allow duplicate team memberships
- When copying a check, copy all fields from the "Filtering Rules" dialog (#417)
- Fix missing Resume button (#421)
- When decoding inbound emails, decode encoded headers (#420)
- Escape markdown in MS Teams notifications (#426)
- Set the "title" and "summary" fields in MS Teams notifications (#435)
## v1.16.0 - 2020-08-04
### Improvements
- Paused ping handling can be controlled via API (#376)
- Add "Get a list of checks's logged pings" API call (#371)
- The /api/v1/checks/ endpoint now accepts either UUID or `unique_key` (#370)
- Added /api/v1/checks/uuid/flips/ endpoint (#349)
- In the cron expression dialog, show a human-friendly version of the expression
- Indicate a started check with a progress spinner under status icon (#338)
- Added "Docs > Reliability Tips" page
- Spike.sh integration (#402)
- Updated Discord integration to use discord.com instead of discordapp.com
- Add "Failure Keyword" filtering for inbound emails (#396)
- Add support for multiple, comma-separated keywords (#396)
- New integration: phone calls (#403)
### Bug Fixes
- Removing Pager Team integration, project appears to be discontinued
- Sending a test notification updates Channel.last_error (#391)
- Handle HTTP 429 responses from Matrix server when joining a Matrix room
## v1.15.0 - 2020-06-04
### Improvements
- Rate limiting for Telegram notifications (10 notifications per chat per minute)
- Use Slack V2 OAuth flow
- Users can edit their existing webhook integrations (#176)
- Add a "Transfer Ownership" feature in Project Settings
- In checks list, the pause button asks for confirmation (#356)
- Added /api/v1/metrics/ endpoint, useful for monitoring the service itself
- Added "When paused, ignore pings" option in the Filtering Rules dialog (#369)
### Bug Fixes
- "Get a single check" API call now supports read-only API keys (#346)
- Don't escape HTML in the subject line of notification emails
- Don't let users clone checks if the account is at check limit
## v1.14.0 - 2020-03-23
### Improvements
- Improved UI to invite users from account's other projects (#258)
- Experimental Prometheus metrics endpoint (#300)
- Don't store user's current project in DB, put it explicitly in page URLs (#336)
- API reference in Markdown
- Use Selectize.js for entering tags (#324)
- Zulip integration (#202)
- OpsGenie integration returns more detailed error messages
- Telegram integration returns more detailed error messages
- Added the "Get a single check" API call (#337)
- Display project name in Slack notifications (#342)
### Bug Fixes
- The "render_docs" command checks if markdown and pygments is installed (#329)
- The team size limit is applied to the n. of distinct users across all projects (#332)
- API: don't let SuspiciousOperation bubble up when validating channel ids
- API security: check channel ownership when setting check's channels
- API: update check's "alert_after" field when changing schedule
- API: validate channel identifiers before creating/updating a check (#335)
- Fix redirect after login when adding Telegram integration
## v1.13.0 - 2020-02-13
### Improvements
- Show a red "!" in project's top navigation if any integration is not working
- createsuperuser management command requires an unique email address (#318)
- For superusers, show "Site Administration" in top navigation, note in README (#317)
- Make Ping.body size limit configurable (#301)
- Show sub-second durations with higher precision, 2 digits after decimal point (#321)
- Replace the gear icon with three horizontal dots icon (#322)
- Add a Pause button in the checks list (#312)
- Documentation in Markdown
- Added an example of capturing and submitting log output (#315)
- The sendalerts commands measures dwell time and reports it over statsd protocol
- Django 3.0.3
- Show a warning in top navigation if the project has no integrations (#327)
### Bug Fixes
- Increase the allowable length of Matrix room alias to 100 (#320)
- Make sure Check.last_ping and Ping.created timestamps match exactly
- Don't trigger "down" notifications when changing schedule interactively in web UI
- Fix sendalerts crash loop when encountering a bad cron schedule
- Stricter cron validation, reject schedules like "At midnight of February 31"
- In hc.front.views.ping_details, if a ping does not exist, return a friendly message
## v1.12.0 - 2020-01-02
### Improvements
- Django 3.0
- "Filtering Rules" dialog, an option to require HTTP POST (#297)
- Show Healthchecks version in Django admin header (#306)
- Added JSON endpoint for Shields.io (#304)
- `senddeletionnotices` command skips profiles with recent last_active_date
- The "Update Check" API call can update check's description (#311)
### Bug Fixes
- Don't set CSRF cookie on first visit. Signup is exempt from CSRF protection
- Fix List-Unsubscribe email header value: add angle brackets
- Unsubscribe links serve a form, and require HTTP POST to actually unsubscribe
- For webhook integration, validate each header line separately
- Fix "Send Test Notification" for webhooks that only fire on checks going up
- Don't allow adding webhook integrations with both URLs blank
- Don't allow adding email integrations with both "up" and "down" unchecked
## v1.11.0 - 2019-11-22
### Improvements
- In monthly reports, no downtime stats for the current month (month has just started)
- Add Microsoft Teams integration (#135)
- Add Profile.last_active_date field for more accurate inactive user detection
- Add "Shell Commands" integration (#302)
- PagerDuty integration works with or without PD_VENDOR_KEY (#303)
### Bug Fixes
- On mobile, "My Checks" page, always show the gear (Details) button (#286)
- Make log events fit better on mobile screens
## v1.10.0 - 2019-10-21
### Improvements
- Add the "Last Duration" field in the "My Checks" page (#257)
- Add "last_duration" attribute to the Check API resource (#257)
- Upgrade to psycopg2 2.8.3
- Add Go usage example
- Send monthly reports on 1st of every month, not randomly during the month
- Signup form sets the "auto-login" cookie to avoid an extra click during first login
- Autofocus the email field in the signup form, and submit on enter key
- Add support for OpsGenie EU region (#294)
- Update OpsGenie logo and setup illustrations
- Add a "Create a Copy" function for cloning checks (#288)
- Send email notification when monthly SMS sending limit is reached (#292)
### Bug Fixes
- Prevent double-clicking the submit button in signup form
- Upgrade to Django 2.2.6 fixes sqlite migrations (#284)
## v1.9.0 - 2019-09-03
### Improvements
- Show the number of downtimes and total downtime minutes in monthly reports (#104)
@ -11,11 +786,11 @@ All notable changes to this project will be documented in this file.
- Three choices in timezone switcher (UTC / check's timezone / browser's timezone) (#278)
- After adding a new check redirect to the "Check Details" page
## Bug Fixes
### Bug Fixes
- Fix javascript code to construct correct URLs when running from a subdirectory (#273)
- Don't show the "Sign Up" link in the login page if registration is closed (#280)
## 1.8.0 - 2019-07-08
## v1.8.0 - 2019-07-08
### Improvements
- Add the `prunetokenbucket` management command
@ -34,7 +809,7 @@ All notable changes to this project will be documented in this file.
- Fix `prunepings` and `prunepingsslow`, they got broken when adding Projects (#264)
## 1.7.0 - 2019-05-02
## v1.7.0 - 2019-05-02
### Improvements
- Add the EMAIL_USE_VERIFICATION configuration setting (#232)
@ -47,7 +822,8 @@ All notable changes to this project will be documented in this file.
- Show the Description section even if the description is missing. (#246)
- Include the description in email alerts. (#247)
## 1.6.0 - 2019-04-01
## v1.6.0 - 2019-04-01
### Improvements
- Add the "desc" field (check's description) to API responses
@ -63,7 +839,7 @@ All notable changes to this project will be documented in this file.
- Fix a "invalid time format" in front.views.status_single on Windows hosts
## 1.5.0 - 2019-02-04
## v1.5.0 - 2019-02-04
### Improvements
- Database schema: add uniqueness constraint to Check.code
@ -75,7 +851,7 @@ All notable changes to this project will be documented in this file.
- Add the "My Projects" page
## 1.4.0 - 2018-12-25
## v1.4.0 - 2018-12-25
### Improvements
- Set Pushover alert priorities for "down" and "up" events separately
@ -93,7 +869,7 @@ All notable changes to this project will be documented in this file.
- Validate and reject cron schedules with six components
## 1.3.0 - 2018-11-21
## v1.3.0 - 2018-11-21
### Improvements
- Load settings from environment variables
@ -113,7 +889,7 @@ All notable changes to this project will be documented in this file.
- During DST transition, handle ambiguous dates as pre-transition
## 1.2.0 - 2018-10-20
## v1.2.0 - 2018-10-20
### Improvements
- Content updates in the "Welcome" page.
@ -128,7 +904,7 @@ All notable changes to this project will be documented in this file.
- Fix hamburger menu button in "Login" page.
## 1.1.0 - 2018-08-20
## v1.1.0 - 2018-08-20
### Improvements
- A new "Check Details" page.

60
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,60 @@
# Contributing
I'm open to feature suggestions and happy to review code contributions.
If you are planning to contribute something larger than a small, straightforward
bugfix, please open an issue so we can discuss it first. Otherwise you are risking a
"no" or a "yes, but let's do it differently" to an already implemented feature.
## Code Style
* Format your Python code with [black](https://black.readthedocs.io/en/stable/).
* Prefer simplicity over cleverness.
* If you are fixing a bug or adding a feature, add a test. Run tests before
submitting pull requests.
## Adding Documentation
This project uses the Markdown format for documentation. Use the `render_docs`
management command to generate the HTML version of the documentation. To add a new
documentation page:
1. Create the appropriate .md file under `templates/docs`
2. Generate the HTML version with `./manage.py render_docs`
3. Add the page to the navigation in `/templates/front/docs_single.html`
## Developing a New Integration
Before starting work on a new integration, please open an issue and
discuss it first. We use several criteria when deciding whether to work on an
integration or accept a PR:
* Most important: is there substantial end-user (ideally, paying or would-be-paying
end user) interest, across GitHub issues, private emails, social media?
* Would it be fun to work on?
* Is the service we are integrating with developer-friendly? Does it have an open
and well-documented API? Can we develop and test the integration while avoiding
sales calls, contract signing, paid subscriptions?
* Does the new integration enable something that is otherwise not possible (or is
very inconvenient) via webhooks or email?
The best way to build a new integration is to pick a similar existing integration
as a starting point for the new integration and replicate every aspect of it.
You will need to make changes in the following files:
* Add a new class in `/hc/api/transports.py`.
* Add a new notification template in `/templates/integrations/`.
* Write testcases for the new transport class in `/hc/api/tests/test_notify_<kind>.py`.
* Update `CHANNEL_KINDS` in `/hc/api/models.py`.
* Update `Channel.transport()` in `/hc/api/models.py`.
* Create a view for provisioning the new integration in `/hc/front/views.py`.
* Write a HTML template for the new view in `/templates/front/add_<kind>.py`, and
prepare any supporting illustrations in `/static/img/integrations/`.
* Add a route for the new view in `/hc/front/urls.py`.
* Write testcases for the new view in `/hc/font/tests/test_add_<kind>.py`.
* Update `/templates/front/channels.html` add a new section in the list of available
integrations, make sure an existing integration is displayed nicely.
* Update `/templates/front/event_summary.html` to make sure notifications sent to the
new integration are displayed nicely.
* Add a logo in `/static/img/integrations/`.
* Update the icon font (it's a little tricky to do, I can take care of that).

693
README.md
View file

@ -1,304 +1,414 @@
# Healthchecks
[![Build Status](https://travis-ci.org/healthchecks/healthchecks.svg?branch=master)](https://travis-ci.org/healthchecks/healthchecks)
[![Tests](https://github.com/healthchecks/healthchecks/actions/workflows/tests.yml/badge.svg)](https://github.com/healthchecks/healthchecks/actions/workflows/tests.yml)
[![Coverage Status](https://coveralls.io/repos/healthchecks/healthchecks/badge.svg?branch=master&service=github)](https://coveralls.io/github/healthchecks/healthchecks?branch=master)
Healthchecks is a cron job monitoring service. It listens for HTTP requests
and email messages ("pings") from your cron jobs and scheduled tasks ("checks").
When a ping does not arrive on time, Healthchecks sends out alerts.
![Screenshot of Welcome page](/static/img/welcome.png?raw=true "Welcome Page")
![Screenshot of My Checks page](/static/img/my_checks.png?raw=true "My Checks Page")
![Screenshot of Period/Grace dialog](/static/img/period_grace.png?raw=true "Period/Grace Dialog")
![Screenshot of Cron dialog](/static/img/cron.png?raw=true "Cron Dialog")
![Screenshot of Integrations page](/static/img/channels.png?raw=true "Integrations Page")
healthchecks is a watchdog for your cron jobs. It's a web server that listens for pings from your cron jobs, plus a web interface.
It is live here: [http://healthchecks.io/](http://healthchecks.io/)
Healthchecks comes with a web dashboard, API, 25+ integrations for
delivering notifications, monthly email reports, WebAuthn 2FA support,
team management features: projects, team members, read-only access.
The building blocks are:
* Python 3
* Django 2
* Python 3.10+
* Django 5.1
* PostgreSQL or MySQL
Healthchecks is licensed under the BSD 3-clause license.
Healthchecks is available as a hosted service
at [https://healthchecks.io/](https://healthchecks.io/).
A [Dockerfile](https://github.com/healthchecks/healthchecks/tree/master/docker)
and [pre-built Docker images](https://hub.docker.com/r/healthchecks/healthchecks) are
available.
Screenshots:
The "My Checks" screen. Shows the status of all your cron jobs
in a live-updating dashboard.
![Screenshot of My Checks page](/static/img/my_checks.png?raw=true "My Checks Page")
Each check has configurable Period and Grace Time parameters. Period is the expected
time between pings. Grace Time specifies how long to wait before sending out alerts
when a job is running late.
![Screenshot of Period/Grace dialog](/static/img/period_grace.png?raw=true "Period/Grace Dialog")
Alternatively, you can define the expected schedules using a cron expressions.
Healthchecks uses the [cronsim](https://github.com/cuu508/cronsim) library to
parse and evaluate cron expressions.
![Screenshot of Cron dialog](/static/img/cron.png?raw=true "Cron Dialog")
Check details page, with a live-updating event log.
![Screenshot of Check Details page](/static/img/check_details.png?raw=true "Check Details Page")
Healthchecks provides status badges with public but hard-to-guess URLs.
You can use them in your READMEs, dashboards, or status pages.
![Screenshot of Badges page](/static/img/badges.png?raw=true "Status Badges")
## Setting Up for Development
These are instructions for setting up healthchecks Django app
in development environment.
To set up Healthchecks development environment:
* install dependencies (Debian/Ubuntu)
* Install dependencies (Debian/Ubuntu):
$ sudo apt-get update
$ sudo apt-get install -y gcc python3-dev python3-venv
```sh
sudo apt update
sudo apt install -y gcc python3-dev python3-venv libpq-dev libcurl4-openssl-dev libssl-dev
```
* prepare directory for project code and virtualenv:
* Prepare directory for project code and virtualenv. Feel free to use a
different location:
$ mkdir -p ~/webapps
$ cd ~/webapps
```sh
mkdir -p ~/webapps
cd ~/webapps
```
* prepare virtual environment
* Prepare virtual environment
(with virtualenv you get pip, we'll use it soon to install requirements):
$ python3 -m venv hc-venv
$ source hc-venv/bin/activate
```sh
python3 -m venv hc-venv
source hc-venv/bin/activate
pip3 install wheel # make sure wheel is installed in the venv
```
* check out project code:
* Check out project code:
$ git clone https://github.com/healthchecks/healthchecks.git
```sh
git clone https://github.com/healthchecks/healthchecks.git
```
* install requirements (Django, ...) into virtualenv:
* Install requirements (Django, ...) into virtualenv:
$ pip install -r healthchecks/requirements.txt
```sh
pip install -r healthchecks/requirements.txt
```
* healthchecks is configured to use a SQLite database by default. To use
PostgreSQL or MySQL database, create and edit `hc/local_settings.py` file.
There is a template you can copy and edit as needed:
* macOS only - pycurl needs to be reinstalled using the following method (assumes OpenSSL was installed using brew):
$ cd ~/webapps/healthchecks
$ cp hc/local_settings.py.example hc/local_settings.py
```sh
export PYCURL_VERSION=`cat requirements.txt | grep pycurl | cut -d '=' -f3`
export OPENSSL_LOCATION=`brew --prefix openssl`
export PYCURL_SSL_LIBRARY=openssl
export LDFLAGS=-L$OPENSSL_LOCATION/lib
export CPPFLAGS=-I$OPENSSL_LOCATION/include
pip uninstall -y pycurl
pip install pycurl==$PYCURL_VERSION --compile --no-cache-dir
```
* create database tables and the superuser account:
* Create database tables and a superuser account:
$ cd ~/webapps/healthchecks
$ ./manage.py migrate
$ ./manage.py createsuperuser
```sh
cd ~/webapps/healthchecks
./manage.py migrate
./manage.py createsuperuser
```
* run development server:
With the default configuration, Healthchecks stores data in a SQLite file
`hc.sqlite` in the checkout directory (`~/webapps/healthchecks`).
$ ./manage.py runserver
* Run tests:
The site should now be running at `http://localhost:8080`
To log into Django administration site as a super user,
visit `http://localhost:8080/admin`
```sh
./manage.py test
```
* Run development server:
```sh
./manage.py runserver
```
The site should now be running at `http://localhost:8000`.
To access Django administration site, log in as a superuser, then
visit `http://localhost:8000/admin/`
## Configuration
Site configuration is loaded from environment variables. This is
done in `hc/settings.py`. Additional configuration is loaded
from `hc/local_settings.py` file, if it exists. You can create this file
(should be right next to `settings.py` in the filesystem) and override
settings, or add extra settings as needed.
Healthchecks reads configuration from environment variables. See the
[full list of configuration parameters](https://healthchecks.io/docs/self_hosted_configuration/)
you can set via environment variables.
Configurations settings loaded from environment variables:
In addition, Healthchecks reads settings from the `hc/local_settings.py` file if it
exists. You can set or override any [standard Django setting](https://docs.djangoproject.com/en/5.1/ref/settings/)
in this file. You can copy the provided `hc/local_settings.py.example` as
`hc/local_settings.py` and use it as a starting point.
| Environment variable | Default value | Notes
| -------------------- | ------------- | ----- |
| [SECRET_KEY](https://docs.djangoproject.com/en/2.1/ref/settings/#secret-key) | `"---"`
| [DEBUG](https://docs.djangoproject.com/en/2.1/ref/settings/#debug) | `True` | Set to `False` for production
| [ALLOWED_HOSTS](https://docs.djangoproject.com/en/2.1/ref/settings/#allowed-hosts) | `*` | Separate multiple hosts with commas
| [DEFAULT_FROM_EMAIL](https://docs.djangoproject.com/en/2.1/ref/settings/#default-from-email) | `"healthchecks@example.org"`
| USE_PAYMENTS | `False`
| REGISTRATION_OPEN | `True`
| DB | `"sqlite"` | Set to `"postgres"` or `"mysql"`
| [DB_HOST](https://docs.djangoproject.com/en/2.1/ref/settings/#host) | `""` *(empty string)*
| [DB_PORT](https://docs.djangoproject.com/en/2.1/ref/settings/#port) | `""` *(empty string)*
| [DB_NAME](https://docs.djangoproject.com/en/2.1/ref/settings/#name) | `"hc"` (PostgreSQL, MySQL) or `"/path/to/project/hc.sqlite"` (SQLite) | For SQLite, specify the full path to the database file.
| [DB_USER](https://docs.djangoproject.com/en/2.1/ref/settings/#user) | `"postgres"` or `"root"`
| [DB_PASSWORD](https://docs.djangoproject.com/en/2.1/ref/settings/#password) | `""` *(empty string)*
| [DB_CONN_MAX_AGE](https://docs.djangoproject.com/en/2.1/ref/settings/#conn-max-age) | `0`
| DB_SSLMODE | `"prefer"` | PostgreSQL-specific, [details](https://blog.github.com/2018-10-21-october21-incident-report/)
| DB_TARGET_SESSION_ATTRS | `"read-write"` | PostgreSQL-specific, [details](https://www.postgresql.org/docs/10/static/libpq-connect.html#LIBPQ-CONNECT-TARGET-SESSION-ATTRS)
| EMAIL_HOST | `""` *(empty string)*
| EMAIL_PORT | `"587"`
| EMAIL_HOST_USER | `""` *(empty string)*
| EMAIL_HOST_PASSWORD | `""` *(empty string)*
| EMAIL_USE_TLS | `"True"`
| EMAIL_USE_VERIFICATION | `"True"`
| SITE_ROOT | `"http://localhost:8000"`
| SITE_NAME | `"Mychecks"`
| MASTER_BADGE_LABEL | `"Mychecks"`
| PING_ENDPOINT | `"http://localhost:8000/ping/"`
| PING_EMAIL_DOMAIN | `"localhost"`
| DISCORD_CLIENT_ID | `None`
| DISCORD_CLIENT_SECRET | `None`
| SLACK_CLIENT_ID | `None`
| SLACK_CLIENT_SECRET | `None`
| PUSHOVER_API_TOKEN | `None`
| PUSHOVER_SUBSCRIPTION_URL | `None`
| PUSHOVER_EMERGENCY_RETRY_DELAY | `300`
| PUSHOVER_EMERGENCY_EXPIRATION | `86400`
| PUSHBULLET_CLIENT_ID | `None`
| PUSHBULLET_CLIENT_SECRET | `None`
| TELEGRAM_BOT_NAME | `"ExampleBot"`
| TELEGRAM_TOKEN | `None`
| TWILIO_ACCOUNT | `None`
| TWILIO_AUTH | `None`
| TWILIO_FROM | `None`
| TWILIO_USE_WHATSAPP | `"False"`
| PD_VENDOR_KEY | `None`
| TRELLO_APP_KEY | `None`
| MATRIX_HOMESERVER | `None`
| MATRIX_USER_ID | `None`
| MATRIX_ACCESS_TOKEN | `None`
| APPRISE_ENABLED | `"False"`
If a setting is specified both as environment variable and in `hc/local_settings.py`,
the latter takes precedence.
## Accessing Administration Panel
Some useful settings keys to override are:
Healthchecks comes with Django's administration panel where you can perform
administrative tasks: delete user accounts, change passwords, increase limits for
specific users, inspect contents of database tables.
`SITE_ROOT` is used to build fully qualified URLs for pings, and for use in
emails and notifications. Example:
To access the administration panel,
```python
SITE_ROOT = "https://my-monitoring-project.com"
```
* if you haven't already, create a superuser account: `./manage.py createsuperuser`
* log into the site using superuser credentials
* in the top navigation, "Account" dropdown, select "Site Administration"
`SITE_NAME` has the default value of "Mychecks" and is used throughout
the templates. Replace it with your own name to personalize your installation.
Example:
```python
SITE_NAME = "My Monitoring Project"
```
`REGISTRATION_OPEN` controls whether site visitors can create new accounts.
Set it to `False` if you are setting up a private healthchecks instance, but
it needs to be publicly accessible (so, for example, your cloud services
can send pings).
If you close new user registration, you can still selectively invite users
to your team account.
`EMAIL_USE_VERIFICATION` enables/disables the sending of a verification
link when an email address is added to the list of notification methods.
Set it to `False` if you are setting up a private healthchecks instance where
you trust your users and want to avoid the extra verification step.
## Database Configuration
Database configuration is loaded from environment variables. If you
need to use a non-standard configuration, you can override the
database configuration in `hc/local_settings.py` like so:
```python
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'your-database-name-here',
'USER': 'your-database-user-here',
'PASSWORD': 'your-database-password-here',
'TEST': {'CHARSET': 'UTF8'},
'OPTIONS': {
... your custom options here ...
}
}
}
```
## Sending Emails
healthchecks must be able to send email messages, so it can send out login
links and alerts to users. Environment variables can be used to configure
SMTP settings, or your may put your SMTP server configuration in
`hc/local_settings.py` like so:
Healthchecks must be able to send email messages, so it can send out login
links and alerts to users. Specify your SMTP credentials using the following
environment variables:
```python
EMAIL_HOST = "your-smtp-server-here.com"
EMAIL_PORT = 587
EMAIL_HOST_USER = "username"
EMAIL_HOST_PASSWORD = "password"
EMAIL_USE_TLS = True
```
- Implicit TLS (*recommended*):
```python
DEFAULT_FROM_EMAIL = "valid-sender-address@example.org"
EMAIL_HOST = "your-smtp-server-here.com"
EMAIL_PORT = 465
EMAIL_HOST_USER = "smtp-username"
EMAIL_HOST_PASSWORD = "smtp-password"
EMAIL_USE_TLS = False
EMAIL_USE_SSL = True
```
Port 465 should be the preferred method according to [RFC8314 Section 3.3: Implicit TLS for SMTP Submission](https://tools.ietf.org/html/rfc8314#section-3.3). Be sure to use a TLS certificate and not an SSL one.
- Explicit TLS:
```python
DEFAULT_FROM_EMAIL = "valid-sender-address@example.org"
EMAIL_HOST = "your-smtp-server-here.com"
EMAIL_PORT = 587
EMAIL_HOST_USER = "smtp-username"
EMAIL_HOST_PASSWORD = "smtp-password"
EMAIL_USE_TLS = True
```
For more information, have a look at Django documentation,
[Sending Email](https://docs.djangoproject.com/en/1.10/topics/email/) section.
[Sending Email](https://docs.djangoproject.com/en/4.2/topics/email/) section.
## Receiving Emails
healthchecks comes with a `smtpd` management command, which starts up a
Healthchecks comes with a `smtpd` management command, which starts up a
SMTP listener service. With the command running, you can ping your
checks by sending email messages
to `your-uuid-here@my-monitoring-project.com` email addresses.
Start the SMTP listener on port 2525:
$ ./manage.py smtpd --port 2525
```sh
./manage.py smtpd --port 2525
```
Send a test email:
$ curl --url 'smtp://127.0.0.1:2525' \
--mail-from 'foo@example.org' \
--mail-rcpt '11111111-1111-1111-1111-111111111111@my-monitoring-project.com' \
-F '='
```sh
curl --url 'smtp://127.0.0.1:2525' \
--mail-from 'foo@example.org' \
--mail-rcpt '11111111-1111-1111-1111-111111111111@my-monitoring-project.com' \
-F '='
```
## Sending Alerts and Reports
## Sending Status Notifications
healtchecks comes with a `sendalerts` management command, which continuously
Healthchecks comes with a `sendalerts` management command, which continuously
polls database for any checks changing state, and sends out notifications as
needed. Within an activated virtualenv, you can manually run
the `sendalerts` command like so:
$ ./manage.py sendalerts
```sh
./manage.py sendalerts
```
In a production setup, you will want to run this command from a process
manager like [supervisor](http://supervisord.org/) or systemd.
manager like systemd or [supervisor](http://supervisord.org/).
Healthchecks also comes with a `sendreports` management command which
sends out monthly reports, weekly reports, and the daily or hourly reminders.
Run `sendreports` without arguments to run any due reports and reminders
and then exit:
```sh
./manage.py sendreports
```
Run it with the `--loop` argument to make it run continuously:
```sh
./manage.py sendreports --loop
```
## Database Cleanup
With time and use the healthchecks database will grow in size. You may
decide to prune old data: inactive user accounts, old checks not assigned
to users, records of outgoing email messages and records of received pings.
There are separate Django management commands for each task:
Healthchecks deletes old entries from `api_ping`, `api_flip`, and `api_notification`
tables automatically. By default, Healthchecks keeps the 100 most recent
pings for every check. You can set the limit higher to keep a longer history:
go to the Administration Panel, look up user's **Profile** and modify its
"Ping log limit" field.
* Remove old records from `api_ping` table. For each check, keep 100 most
recent pings:
Healthchecks also provides management commands for cleaning up
`auth_user` (user accounts) and `api_tokenbucket` (rate limiting records) tables,
and for removing stale objects from external object storage.
```
$ ./manage.py prunepings
```
* Remove user accounts that are older than 1 month and have never logged in:
* Remove old records of sent notifications. For each check, remove
notifications that are older than the oldest stored ping for same check.
```
$ ./manage.py prunenotifications
```
* Remove user accounts that match either of these conditions:
* Account was created more than 6 months ago, and user has never logged in.
These can happen when user enters invalid email address when signing up.
* Last login was more than 6 months ago, and the account has no checks.
Assume the user doesn't intend to use the account any more and would
probably *want* it removed.
```
$ ./manage.py pruneusers
```
```sh
./manage.py pruneusers
```
* Remove old records from the `api_tokenbucket` table. The TokenBucket
model is used for rate-limiting login attempts and similar operations.
Any records older than one day can be safely removed.
```
$ ./manage.py prunetokenbucket
```
```sh
./manage.py prunetokenbucket
```
* Remove old records from the `api_flip` table. The Flip
objects are used to track status changes of checks, and to calculate
downtime statistics month by month. Flip objects from more than 3 months
ago are not used and can be safely removed.
* Remove old objects from external object storage. When an user removes
a check, removes a project, or closes their account, Healthchecks
does not remove the associated objects from the external object
storage on the fly. Instead, you should run `pruneobjects` occasionally
(for example, once a month). This command first takes an inventory
of all checks in the database, and then iterates over top-level
keys in the object storage bucket, and deletes any that don't also
exist in the database.
```
$ ./manage.py pruneflips
```
```sh
./manage.py pruneobjects
```
When you first try these commands on your data, it is a good idea to
test them on a copy of your database, not on the live database right away.
In a production setup, you should also have regular, automated database
backups set up.
## Two-factor Authentication
Healthchecks optionally supports two-factor authentication using the WebAuthn
standard. To enable WebAuthn support, set the `RP_ID` (relying party identifier )
setting to a non-null value. Set its value to your site's domain without scheme
and without port. For example, if your site runs on `https://my-hc.example.org`,
set `RP_ID` to `my-hc.example.org`.
Note that WebAuthn requires HTTPS, even if running on localhost. To test WebAuthn
locally with a self-signed certificate, you can use the `runsslserver` command
from the `django-sslserver` package.
## External Authentication
Healthchecks supports external authentication by means of HTTP headers set by
reverse proxies or the WSGI server. This allows you to integrate it into your
existing authentication system (e.g., LDAP or OAuth) via an authenticating proxy.
When this option is enabled, **healthchecks will trust the header's value implicitly**,
so it is **very important** to ensure that attackers cannot set the value themselves
(and thus impersonate any user). How to do this varies by your chosen proxy,
but generally involves configuring it to strip out headers that normalize to the
same name as the chosen identity header.
To enable this feature, set the `REMOTE_USER_HEADER` value to a header you wish to
authenticate with. HTTP headers will be prefixed with `HTTP_` and have any dashes
converted to underscores. Headers without that prefix can be set by the WSGI server
itself only, which is more secure.
When `REMOTE_USER_HEADER` is set, Healthchecks will:
- assume the header contains user's email address
- look up and automatically log in the user with a matching email address
- automatically create an user account if it does not exist
- disable the default authentication methods (login link to email, password)
The header name in `REMOTE_USER_HEADER` must be specified in upper-case,
with any dashes replaced with underscores, and prefixed with `HTTP_`. For
example, if your authentication proxy sets a `X-Authenticated-User` request
header, you should set `REMOTE_USER_HEADER=HTTP_X_AUTHENTICATED_USER`.
**Note on using `local_settings.py`:**
When Healthchecks reads settings from environment variables and encounters
the `REMOTE_USER_HEADER` environment variable, it sets *two* settings,
`REMOTE_USER_HEADER` and `AUTHENTICATION_BACKENDS`. This logic has already run by the
time Healthchecks reads `local_settings.py`. Therefore, if you configure Healthchecks
using the `local_settings.py` file instead of environment variables, and specify
`REMOTE_USER_HEADER` there, you will also need a line which sets the other setting,
`AUTHENTICATION_BACKENDS`:
```
REMOTE_USER_HEADER = "HTTP_X_AUTHENTICATED_USER"
AUTHENTICATION_BACKENDS = ["hc.accounts.backends.CustomHeaderBackend"]
```
## External Object Storage
Healthchecks can optionally store large ping bodies in S3-compatible object
storage. To enable this feature, you will need to:
* ensure you have the [MinIO Python library](https://docs.min.io/docs/python-client-quickstart-guide.html) installed:
```bash
pip install minio
```
* configure the credentials for accessing object storage: `S3_ACCESS_KEY`,
`S3_SECRET_KEY`, `S3_ENDPOINT`, `S3_REGION` and `S3_BUCKET`.
Healthchecks will use external object storage for storing any request bodies that
exceed 100 bytes. If the size of a request body is 100 bytes or below, Healthchecks
will still store it in the database.
Healthchecks automatically removes old stored ping bodies from object
storage while uploading new data. However, Healthchecks does not automatically
clean up data when you delete checks, projects or entire user accounts.
Use the `pruneobjects` management command to remove data for checks that don't
exist any more.
When external object storage is not enabled (the credentials for accessing object
storage are not set), Healthchecks stores all ping bodies in the database.
If you enable external object storage, Healthchecks will still be able to
access the ping bodies already stored in the database. You don't need to migrate
them to the object storage. On the other hand, if you later decide to disable
external object storage, Healthchecks will not have access to the externally
stored ping bodies any more. And there is currently no script or management command
for migrating ping bodies from external object storage back to the database.
## Integrations
### Slack
Healthchecks supports two Slack integration setup flows: legacy and app-based.
The legacy flow does not require additional configuration and is used by default.
In this flow the user creates an incoming webhook URL on the Slack side, and
pastes the webhook URL in a form on the Healthchecks side.
In the app-based flow the user clicks an "Add to Slack" button in Healthchecks,
and gets transferred to a Slack-hosted dialog where they select the channel to
post notifications to. This flow uses OAuth2 behind the scenes. To enable this
flow, you will need to set up a Slack OAuth2 app:
* Create a new Slack app on https://api.slack.com/apps/
* Add at least one scope in the permissions section to be able to deploy the app in
your workspace (By example `incoming-webhook` for the `Bot Token Scopes`).
* Add a _redirect url_ in the format `SITE_ROOT/integrations/add_slack_btn/`.
For example, if your SITE_ROOT is `https://my-hc.example.org` then the redirect URL
would be `https://my-hc.example.org/integrations/add_slack_btn/`.
* Look up your Slack app for the Client ID and Client Secret. Put them
in `SLACK_CLIENT_ID` and `SLACK_CLIENT_SECRET` environment
variables. Once these variables are set, Healthchecks will switch from using
the legacy flow to using the app-based flow.
The legacy and app-based flows only affect the user experience during the initial
setup of Slack integrations. The contents of notifications posted to Slack are the same
regardless of the setup flow used.
### Discord
To enable Discord integration, you will need to:
* register a new application on https://discordapp.com/developers/applications/me
* register a new application on https://discord.com/developers/applications/me
* add a redirect URI to your Discord application. The URI format is
`SITE_ROOT/integrations/add_discord/`. For example, if you are running a
development server on `localhost:8000` then the redirect URI would be
@ -310,32 +420,68 @@ To enable Discord integration, you will need to:
### Pushover
To enable Pushover integration, you will need to:
Pushover integration works by creating an application on Pushover.net which
is then subscribed to by Healthchecks users. The registration workflow is as follows:
* register a new application on https://pushover.net/apps/build
* enable subscriptions in your application and make sure to enable the URL
subscription type
* put the application token and the subscription URL in
* On Healthchecks, the user adds a "Pushover" integration to a project
* Healthchecks redirects user's browser to a Pushover.net subscription page
* User approves adding the Healthchecks subscription to their Pushover account
* Pushover.net HTTP redirects back to Healthchecks with a subscription token
* Healthchecks saves the subscription token and uses it for sending Pushover
notifications
To enable the Pushover integration, you will need to:
* Register a new application on Pushover via https://pushover.net/apps/build.
* Within the Pushover 'application' configuration, enable subscriptions.
Make sure the subscription type is set to "URL". Also make sure the redirect
URL is configured to point back to the root of the Healthchecks instance
(e.g., `http://healthchecks.example.com/`).
* Put the Pushover application API Token and the Pushover subscription URL in
`PUSHOVER_API_TOKEN` and `PUSHOVER_SUBSCRIPTION_URL` environment
variables
variables. The Pushover subscription URL should look similar to
`https://pushover.net/subscribe/yourAppName-randomAlphaNumericData`.
### Signal
Healthchecks uses [signal-cli](https://github.com/AsamK/signal-cli) to send Signal
notifications. Healthcecks interacts with signal-cli over UNIX or TCP socket.
Healthchecks requires signal-cli version 0.11.2 or later.
To enable the Signal integration via UNIX socket:
* Set up and configure signal-cli to expose JSON RPC on an UNIX socket
([instructions](https://github.com/AsamK/signal-cli/wiki/JSON-RPC-service)).
Example: `signal-cli -a +xxxxxx daemon --socket /tmp/signal-cli-socket`
* Put the socket's location in the `SIGNAL_CLI_SOCKET` environment variable.
To enable the Signal integration via TCP socket:
* Set up and configure signal-cli to expose JSON RPC on a TCP socket.
Example: `signal-cli -a +xxxxxx daemon --tcp 127.0.0.1:7583`
* Put the socket's hostname and port in the `SIGNAL_CLI_SOCKET` environment variable
using "hostname:port" syntax, example: `127.0.0.1:7583`.
### Telegram
* Create a Telegram bot by talking to the
[BotFather](https://core.telegram.org/bots#6-botfather). Set the bot's name,
description, user picture, and add a "/start" command.
description, user picture, and add a "/start" command. To avoid user confusion,
please do not use the Healthchecks.io logo as your bot's user picture, use
your own logo.
* After creating the bot you will have the bot's name and token. Put them
in `TELEGRAM_BOT_NAME` and `TELEGRAM_TOKEN` environment variables.
* Run `settelegramwebhook` management command. This command tells Telegram
where to forward channel messages by invoking Telegram's
[setWebhook](https://core.telegram.org/bots/api#setwebhook) API call:
```
$ ./manage.py settelegramwebhook
```sh
./manage.py settelegramwebhook
Done, Telegram's webhook set to: https://my-monitoring-project.com/integrations/telegram/bot/
```
For this to work, your `SITE_ROOT` needs to be correct and use "https://"
For this to work, your `SITE_ROOT` must be correct and must use the "https://"
scheme.
### Apprise
@ -343,7 +489,108 @@ scheme.
To enable Apprise integration, you will need to:
* ensure you have apprise installed in your local environment:
```bash
pip install apprise
```
```bash
pip install apprise
```
* enable the apprise functionality by setting the `APPRISE_ENABLED` environment variable.
### Shell Commands
The "Shell Commands" integration runs user-defined local shell commands when checks
go up or down. This integration is disabled by default, and can be enabled by setting
the `SHELL_ENABLED` environment variable to `True`.
Note: be careful when using "Shell Commands" integration, and only enable it when
you fully trust the users of your Healthchecks instance. The commands will be executed
by the `manage.py sendalerts` process, and will run with the same system permissions as
the `sendalerts` process.
### Matrix
To enable the Matrix integration you will need to:
* Register a bot user (for posting notifications) in your preferred homeserver.
* Use the [Login API call](https://www.matrix.org/docs/guides/client-server-api#login)
to retrieve bot user's access token. You can run it as shown in the documentation,
using curl in command shell.
* Set the `MATRIX_` environment variables. Example:
```
MATRIX_HOMESERVER=https://matrix.org
MATRIX_USER_ID=@mychecks:matrix.org
MATRIX_ACCESS_TOKEN=[a long string of characters returned by the login call]
```
### PagerDuty Simple Install Flow
To enable PagerDuty [Simple Install Flow](https://developer.pagerduty.com/docs/app-integration-development/events-integration/),
* Register a PagerDuty app at [PagerDuty](https://pagerduty.com/) Developer Mode My Apps
* In the newly created app, add the "Events Integration" functionality
* Specify a Redirect URL: `https://your-domain.com/integrations/add_pagerduty/`
* Copy the displayed app_id value (PXXXXX) and put it in the `PD_APP_ID` environment
variable
## Running in Production
Here is a non-exhaustive list of pointers and things to check before launching a Healthchecks instance
in production.
* Environment variables, settings.py and local_settings.py.
* [DEBUG](https://docs.djangoproject.com/en/4.2/ref/settings/#debug). Make sure it is
set to `False`.
* [ALLOWED_HOSTS](https://docs.djangoproject.com/en/4.2/ref/settings/#allowed-hosts).
Make sure it contains the correct domain name you want to use.
* Server Errors. When DEBUG=False, Django will not show detailed error pages, and
will not print exception tracebacks to standard output. To receive exception
tracebacks in email, review and edit the
[ADMINS](https://docs.djangoproject.com/en/4.2/ref/settings/#admins) and
[SERVER_EMAIL](https://docs.djangoproject.com/en/4.2/ref/settings/#server-email)
settings. Consider setting up exception logging with [Sentry](https://sentry.io/for/django/).
* Management commands that need to be run during each deployment.
* `manage.py compress` creates combined JS and CSS bundles and
places them in the `static-collected` directory.
* `manage.py collectstatic` collects static files in the `static-collected`
directory.
* `manage.py migrate` applies any pending database schema changes
and data migrations.
* Processes that need to be running constantly.
* `manage.py runserver` is intended for development only.
**Do not use it in production**, instead consider using
[uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/) or
[gunicorn](https://gunicorn.org/).
An example of a minimal setup would be to install uWSGI using `pip3 install uwsgi`,
and to run `uwsgi --http :8000 --module hc.wsgi` from the project's root directory.
* `manage.py sendalerts` is the process that monitors checks and sends out
monitoring alerts. It must be always running, it must be started on reboot, and it
must be restarted if it itself crashes. On modern linux systems, a good option is
to [define a systemd service](https://github.com/healthchecks/healthchecks/issues/273#issuecomment-520560304)
for it.
* Static files. Healthchecks serves static files on its own, no configuration
required. It uses the [Whitenoise library](http://whitenoise.evans.io/en/stable/index.html)
for this.
* General
* Make sure the database is secured well and is getting backed up regularly
* Make sure the TLS certificates are secured well and are getting refreshed regularly
* Have monitoring in place to be sure the Healthchecks instance itself is operational
(is accepting pings, is sending out alerts, is not running out of resources).
## Docker Image
Healthchecks provides a reference Dockerfile and prebuilt Docker images for every
release. The Dockerfile lives in the [/docker/](https://github.com/healthchecks/healthchecks/tree/master/docker)
directory, and Docker images for amd64, arm/v7 and arm64 architectures are available
[on Docker Hub](https://hub.docker.com/r/healthchecks/healthchecks).
The Docker images:
* Use uWSGI as the web server. uWSGI is configured to perform database migrations
on startup, and to run `sendalerts`, `sendreports`, and `smtpd` in the background.
You do not need to run them separately.
* Ship with both PostgreSQL and MySQL database drivers.
* Serve static files using the whitenoise library.
* Have the apprise library preinstalled.
* Do *not* handle TLS termination. In a production setup, you will want to put
the Healthchecks container behind a reverse proxy or load balancer that handles TLS
termination.

5
SECURITY.md Normal file
View file

@ -0,0 +1,5 @@
# Security Policy
## Reporting a Vulnerability
To report a security vulnerability, please email contact@healthchecks.io.

79
docker/.env.example Normal file
View file

@ -0,0 +1,79 @@
ALLOWED_HOSTS=localhost
APPRISE_ENABLED=False
DB=postgres
DB_CONN_MAX_AGE=0
DB_HOST=db
DB_NAME=hc
DB_PASSWORD=fixme-postgres-password
DB_PORT=5432
DB_SSLMODE=prefer
DB_TARGET_SESSION_ATTRS=read-write
DB_USER=postgres
DEBUG=False
DEFAULT_FROM_EMAIL=healthchecks@example.org
DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET=
EMAIL_HOST=
EMAIL_HOST_PASSWORD=
EMAIL_HOST_USER=
EMAIL_PORT=587
EMAIL_USE_TLS=True
EMAIL_USE_VERIFICATION=True
INTEGRATIONS_ALLOW_PRIVATE_IPS=False
LINENOTIFY_CLIENT_ID=
LINENOTIFY_CLIENT_SECRET=
MASTER_BADGE_LABEL=Mychecks
MATRIX_ACCESS_TOKEN=
MATRIX_HOMESERVER=
MATRIX_USER_ID=
MATTERMOST_ENABLED=True
MSTEAMS_ENABLED=True
OPSGENIE_ENABLED=True
PAGERTREE_ENABLED=True
PD_APP_ID=
PD_ENABLED=True
PING_BODY_LIMIT=10000
PING_EMAIL_DOMAIN=localhost
PING_ENDPOINT=http://localhost:8000/ping/
PROMETHEUS_ENABLED=True
PUSHBULLET_CLIENT_ID=
PUSHBULLET_CLIENT_SECRET=
PUSHOVER_API_TOKEN=
PUSHOVER_EMERGENCY_EXPIRATION=86400
PUSHOVER_EMERGENCY_RETRY_DELAY=300
PUSHOVER_SUBSCRIPTION_URL=
REGISTRATION_OPEN=True
REMOTE_USER_HEADER=
ROCKETCHAT_ENABLED=True
RP_ID=
S3_ACCESS_KEY=
S3_BUCKET=
S3_ENDPOINT=
S3_REGION=
S3_SECRET_KEY=
S3_TIMEOUT=60
S3_SECURE=True
SECRET_KEY=---
SHELL_ENABLED=False
SIGNAL_CLI_SOCKET=
SITE_LOGO_URL=
SITE_NAME=Mychecks
SITE_ROOT=http://localhost:8000
SLACK_CLIENT_ID=
SLACK_CLIENT_SECRET=
SLACK_ENABLED=True
# SMTPD_PORT=
SPIKE_ENABLED=True
TELEGRAM_BOT_NAME=ExampleBot
TELEGRAM_TOKEN=
TRELLO_APP_KEY=
TWILIO_ACCOUNT=
TWILIO_AUTH=
TWILIO_FROM=
TWILIO_USE_WHATSAPP=False
USE_PAYMENTS=False
VICTOROPS_ENABLED=True
WEBHOOKS_ENABLED=True
WHATSAPP_DOWN_CONTENT_SID=
WHATSAPP_UP_CONTENT_SID=
ZULIP_ENABLED=True

49
docker/Dockerfile Normal file
View file

@ -0,0 +1,49 @@
FROM python:3.13.1-slim-bookworm AS builder
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
COPY requirements.txt /tmp
RUN \
apt-get update && \
apt-get install -y build-essential curl libpq-dev libmariadb-dev libffi-dev libpcre2-dev libssl-dev libcurl4-openssl-dev libpython3-dev pkg-config
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | bash -s -- --profile minimal -y
ENV PATH=$PATH:/root/.cargo/bin
RUN pip wheel --wheel-dir /wheels apprise uwsgi mysqlclient minio psycopg-c==3.2.3 -r /tmp/requirements.txt
COPY . /opt/healthchecks/
RUN \
rm -rf /opt/healthchecks/.git && \
rm -rf /opt/healthchecks/stuff
FROM python:3.13.0-slim-bookworm
RUN useradd --system hc
ENV PYTHONUNBUFFERED=1
WORKDIR /opt/healthchecks
RUN \
apt-get update && \
apt-get install -y libcurl4 libexpat1 libpq5 libmariadb3 libxml2 && \
rm -rf /var/apt/cache && \
rm -rf /var/lib/apt/lists
RUN --mount=type=bind,target=/wheels,source=/wheels,from=builder \
pip install --upgrade pip && \
pip install --no-cache /wheels/*
COPY --from=builder /opt/healthchecks/ /opt/healthchecks/
COPY docker/fetchstatus.py /opt/healthchecks/
RUN \
rm -f /opt/healthchecks/hc/local_settings.py && \
DEBUG=False SECRET_KEY=build-key ./manage.py collectstatic --noinput && \
DEBUG=False SECRET_KEY=build-key ./manage.py compress
RUN mkdir /data && chown hc /data
USER hc
ENV USE_GZIP_MIDDLEWARE=True
HEALTHCHECK --start-period=20s --start-interval=5s --interval=60s --retries=1 CMD ./fetchstatus.py
CMD [ "uwsgi", "/opt/healthchecks/docker/uwsgi.ini"]

150
docker/README.md Normal file
View file

@ -0,0 +1,150 @@
# Running with Docker
This is a sample configuration for running Healthchecks with
[Docker](https://www.docker.com) and [Docker Compose](https://docs.docker.com/compose/).
Note: For the sake of simplicity, the sample configuration starts a single database
node and a single web server node, both on the same host. It does not handle TLS
termination.
## Getting Started
* Copy `/docker/.env.example` to `/docker/.env` and add your configuration in it.
As a minimum, set the following fields:
* `ALLOWED_HOSTS` the domain name of your Healthchecks instance.
Example: `ALLOWED_HOSTS=hc.example.org`.
* `DEFAULT_FROM_EMAIL` the "From:" address for outbound emails.
* `EMAIL_HOST` the SMTP server.
* `EMAIL_HOST_PASSWORD` the SMTP password.
* `EMAIL_HOST_USER` the SMTP username.
* `SECRET_KEY` secures HTTP sessions, set to a random value.
* `SITE_ROOT` The base public URL of your Healthchecks instance. Example:
`SITE_ROOT=https://hc.example.org`.
* Create and start containers:
```sh
docker compose up
```
* Create a superuser:
```sh
docker compose run web /opt/healthchecks/manage.py createsuperuser
```
* Open [http://localhost:8000](http://localhost:8000) in your browser and log in with
the credentials from the previous step.
## uWSGI Configuration
The reference Dockerfile uses [uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/)
as the WSGI server. You can configure uWSGI by setting `UWSGI_...` environment
variables in `docker/.env`. For example, to disable HTTP request logging, set:
UWSGI_DISABLE_LOGGING=1
To adjust the number of uWSGI processes (for example, to save memory), set:
UWSGI_PROCESSES=2
Read more about configuring uWSGI in [uWSGI documentation](https://uwsgi-docs.readthedocs.io/en/latest/Configuration.html#environment-variables).
## SMTP Listener Configuration via `SMTPD_PORT`
Healthchecks comes with a `smtpd` management command, which runs a SMTP listener
service. With the command running, you can ping your checks by sending email messages
to `your-uuid-here@your-hc-domain.com` email addresses.
The container is configured to start the SMTP listener conditionally, based
on the value of the `SMTPD_PORT` environment value:
* If `SMTPD_PORT` environment variable is not set, the SMTP listener will not run.
* If `SMTPD_PORT` is set, the listener will run and listen on the specified port.
You may also need to edit `docker-compose.yml` to expose the listening port
(see the "ports" section under the "web" service in `docker-compose.yml`).
The conditional logic lives in uWSGI configuration file,
[uwsgi.ini](https://github.com/healthchecks/healthchecks/blob/master/docker/uwsgi.ini).
## TLS Termination and CSRF Protection
If you plan to expose your Healthchecks instance to the public internet, make sure you
put a TLS-terminating reverse proxy or load balancer in front of it.
**Important:** This Dockerfile uses uWSGI, which relies on the [X-Forwarded-Proto](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto)
header to determine if a request is secure or not. Without this information you
may run into HTTP 403 "CSRF verification failed." errors when using your Healthchecks
instance. See [this issue comment](https://github.com/healthchecks/healthchecks/discussions/851#discussioncomment-6293396)
for more information.
Make sure your TLS-terminating reverse proxy:
* Discards the `X-Forwarded-Proto` header sent by the end user.
* Sets the `X-Forwarded-Proto` header value to match the protocol of the original request
("http" or "https").
For example, in NGINX you can use the `$scheme` variable like so:
```
proxy_set_header X-Forwarded-Proto $scheme;
```
If you are using haproxy, you can do the same like so:
```
http-request set-header X-Forwarded-Proto https if { ssl_fc }
http-request set-header X-Forwarded-Proto http unless { ssl_fc }
```
## Upgrading Database
When you upgrade the database version in `docker-compose.yml` (for example,
from `postgres:12` to `postgres:16`), you will also need to upgrade your postgres
data directory. One way to do this is using the
[pgautoupgrade](https://hub.docker.com/r/pgautoupgrade/pgautoupgrade) container.
Steps:
* As the very first step, **take a full backup of your database**.
* Stop the `db` and `web` containers: `docker compose stop`
* Look up the name of the postgres data volume name using `docker volume ls`
* Run `pgautoupgrade` like so:
```
docker run --rm --name pgauto -it \
--mount type=volume,source=<pg-volume-name-here>,target=/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=password \
-e PGAUTO_ONESHOT=yes \
pgautoupgrade/pgautoupgrade:16-bookworm
```
* Update the `docker-compose.yml` file to use the `postgres:16` image
* Start containers: `docker compose up`
## Pre-built Images
Pre-built Docker images, built from the Dockerfile in this directory, are available
[on Docker Hub](https://hub.docker.com/r/healthchecks/healthchecks). The images are
built automatically for every new release.
The Docker images:
* Support amd64, arm/v7 and arm64 architectures.
* Use uWSGI as the web server. uWSGI is configured to perform database migrations
on startup, and to run `sendalerts`, `sendreports`, and `smtpd` in the background.
You do not need to run them separately. The SMTP listener (`manage.py smtpd`) is
started conditionally, [based on the value of the `SMTPD_PORT` environment variable](https://github.com/healthchecks/healthchecks/tree/master/docker#smtp-listener-configuration-via-smtpd_port).
* Ship with both PostgreSQL and MySQL database drivers.
* Serve static files using the whitenoise library.
* Have the apprise library preinstalled.
* Do *not* handle TLS termination. In a production setup, you will want to put
the Healthchecks container behind a reverse proxy or load balancer that handles TLS
termination.
To use a pre-built image for Healthchecks version X.Y, in the `docker-compose.yml` file
replace the "build" section with:
```text
image: healthchecks/healthchecks:vX.Y
```

29
docker/docker-compose.yml Normal file
View file

@ -0,0 +1,29 @@
volumes:
db-data:
services:
db:
image: postgres:16
volumes:
- db-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=$DB_NAME
- POSTGRES_PASSWORD=$DB_PASSWORD
web:
build:
context: ..
dockerfile: docker/Dockerfile
# To use a pre-built image, remove the above "build" section
# and uncomment the following line:
# image: healthchecks/healthchecks:latest
env_file:
- .env
ports:
- "8000:8000"
# To enable SMTP on port 2525, set SMTPD_PORT=2525 in .env
# and uncomment the following line:
# - "2525:2525"
depends_on:
- db
command: bash -c 'while !</dev/tcp/db/5432; do sleep 1; done; uwsgi /opt/healthchecks/docker/uwsgi.ini'

40
docker/fetchstatus.py Executable file
View file

@ -0,0 +1,40 @@
#!/usr/bin/env python
"""Probe the /api/v3/status/ endpoint and return status 0 if successful.
The /api/v3/status/ endpoint tests if the database connection is alive.
This script is intended to be used in the Dockerfile, in the
HEALTHCHECK instruction.
When making the HTTP request, we must pass a valid Host header and a valid
path (in case the app is not running at the root of the domnain). To
figure this out, we need to see `settings.SITE_ROOT`. Loading full
Django settings is a heavy operation so instead we replicate the logic that
settings.py uses for reading SITE_ROOT:
* Load it from `SITE_ROOT` environment variable
* if hc/local_settings.py exists, import it and read it from there
"""
from __future__ import annotations
import os
from urllib.parse import urlparse
from urllib.request import Request, urlopen
# Read SITE_ROOT from environment, same as settings.py would do:
SITE_ROOT = os.getenv("SITE_ROOT", "http://localhost:8000")
# If local_settings.py exists, load it from there
if os.path.exists("hc/local_settings.py"):
from hc import local_settings
SITE_ROOT = getattr(local_settings, "SITE_ROOT", SITE_ROOT)
parsed_site_root = urlparse(SITE_ROOT.removesuffix("/"))
url = f"http://localhost:8000{parsed_site_root.path}/api/v3/status/"
headers = {"Host": parsed_site_root.netloc}
with urlopen(Request(url, headers=headers)) as response:
assert response.status == 200
print("Status OK")

36
docker/uwsgi.ini Normal file
View file

@ -0,0 +1,36 @@
[uwsgi]
strict
master
die-on-term
http-socket = :8000
harakiri = 10
buffer-size = 32768
post-buffering = 16192
processes = 4
if-env = UWSGI_PROCESSES
processes = %(_)
endif =
auto-procname
enable-threads
threads = 1
chdir = /opt/healthchecks
module = hc.wsgi:application
thunder-lock
disable-write-exception
# workaround for https://github.com/unbit/uwsgi/issues/2299
max-fd = 10000
# compression
check-static = static-collected/
static-gzip-dir = static-collected/CACHE
# Note: manage.py migrate will also run system checks
hook-pre-app = exec:./manage.py migrate
# Use "--skip-checks" to avoid running same checks 3 times
attach-daemon = ./manage.py sendalerts --skip-checks
attach-daemon = ./manage.py sendreports --loop --skip-checks
if-env = SMTPD_PORT
attach-daemon = ./manage.py smtpd --port %(_) --skip-checks
endif =

View file

@ -1,248 +1,350 @@
from __future__ import annotations
from collections.abc import Iterable
from datetime import date, datetime
from typing import TypedDict
from django.contrib import admin
from django.contrib.admin import ModelAdmin
from django.contrib.auth import login as auth_login
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User
from django.db.models import Count, F
from django.db.models import Count, F, QuerySet
from django.http import HttpRequest, HttpResponseRedirect
from django.shortcuts import redirect
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.html import escape
from django.utils.safestring import mark_safe
from hc.accounts.models import Profile, Project
from django.utils.html import format_html
from django_stubs_ext import WithAnnotations
from hc.accounts.models import Credential, Profile, Project
Lookups = Iterable[tuple[str, str]]
class Fieldset:
name = None
fields = []
def _format_usage(num_checks: int, num_channels: int) -> str:
tmpl = ""
@classmethod
def tuple(cls):
return (cls.name, {"fields": cls.fields})
if num_checks == 0:
tmpl += "{} checks, "
elif num_checks == 1:
tmpl += "{} check, "
else:
tmpl += "<strong>{} checks</strong>, "
if num_channels == 0:
tmpl += "{} channels"
elif num_channels == 1:
tmpl += "{} channel"
else:
tmpl += "<strong>{} channels</strong>"
return format_html(tmpl, num_checks, num_channels)
class ProfileFieldset(Fieldset):
name = "User Profile"
fields = (
class NumChecksFilter(admin.SimpleListFilter):
title = "check count"
parameter_name = "num_checks"
def lookups(self, r: HttpRequest, model_admin: ModelAdmin[Profile]) -> Lookups:
return (
("10", "More than 10"),
("20", "More than 20"),
("50", "More than 50"),
("100", "More than 100"),
("500", "More than 500"),
("1000", "More than 1000"),
)
def queryset(
self, r: HttpRequest, qs: QuerySet[WithAnnotations[Profile, ProfileAnnotations]]
) -> QuerySet[WithAnnotations[Profile, ProfileAnnotations]]:
value = self.value()
if value:
qs = qs.filter(num_checks__gt=int(value))
return qs
class ProfileAnnotations(TypedDict):
num_checks: int
num_members: int
plan: str
@admin.register(Profile)
class ProfileAdmin(ModelAdmin[Profile]):
class Media:
css = {"all": ("css/admin/profiles.css",)}
readonly_fields = ("user", "email")
search_fields = ["id", "user__email"]
list_per_page = 30
list_select_related = ("user",)
list_display = (
"id",
"email",
"current_project",
"reports_allowed",
"checks",
"projects",
"date_joined",
"last_active",
"over_limit",
"deletion",
"invited",
"sms",
"reports",
)
list_filter = (
"check_limit",
NumChecksFilter,
"last_active_date",
"over_limit_date",
"deletion_scheduled_date",
"reports",
)
actions = (
"login",
"send_report",
"send_nag",
"remove_totp",
"schedule_for_deletion",
"unschedule_for_deletion",
)
_profile_fields = (
"tz",
"reports",
"next_report_date",
"nag_period",
"next_nag_date",
"deletion_notice_date",
"token",
"theme",
"sort",
)
class TeamFieldset(Fieldset):
name = "Team"
fields = (
_limits_fields = (
"team_limit",
"check_limit",
"ping_log_limit",
"sms_limit",
"sms_sent",
"last_sms_date",
"call_limit",
"calls_sent",
"last_call_date",
)
@admin.register(Profile)
class ProfileAdmin(admin.ModelAdmin):
class Media:
css = {"all": ("css/admin/profiles.css",)}
readonly_fields = ("user", "email")
raw_id_fields = ("current_project",)
search_fields = ["id", "user__email"]
list_per_page = 50
list_select_related = ("user",)
list_display = (
"id",
"email",
"engagement",
"date_joined",
"last_login",
"projects",
"invited",
"sms",
"reports_allowed",
)
list_filter = (
"user__date_joined",
"user__last_login",
"reports_allowed",
"check_limit",
_deletion_fields = (
"over_limit_date",
"deletion_notice_date",
"deletion_scheduled_date",
)
fieldsets = (ProfileFieldset.tuple(), TeamFieldset.tuple())
fieldsets = (
("User Profile", {"fields": _profile_fields}),
("Limits", {"fields": _limits_fields}),
("Deletion", {"fields": _deletion_fields}),
)
def get_queryset(self, request):
def get_queryset(self, request: HttpRequest) -> QuerySet[Profile]:
qs = super(ProfileAdmin, self).get_queryset(request)
qs = qs.prefetch_related("user__project_set")
qs = qs.annotate(num_members=Count("user__project__member", distinct=True))
qs = qs.annotate(num_checks=Count("user__project__check", distinct=True))
qs = qs.annotate(num_channels=Count("user__project__channel", distinct=True))
qs = qs.annotate(plan=F("user__subscription__plan_name"))
return qs
@mark_safe
def engagement(self, obj):
result = ""
if obj.num_checks == 0:
result += "0 checks, "
elif obj.num_checks == 1:
result += "1 check, "
else:
result += "<strong>%d checks</strong>, " % obj.num_checks
if obj.num_channels == 0:
result += "0 channels"
elif obj.num_channels == 1:
result += "1 channel, "
else:
result += "<strong>%d channels</strong>, " % obj.num_channels
return result
@mark_safe
def email(self, obj):
s = escape(obj.user.email)
def email(self, obj: WithAnnotations[Profile, ProfileAnnotations]) -> str:
if obj.plan:
return "<span title='%s'>%s</span>" % (obj.plan, s)
return format_html("{} <span>{}</span>", obj.user.email, obj.plan)
return s
return obj.user.email
def last_login(self, obj):
return obj.user.last_login
def date_joined(self, obj):
@admin.display(ordering="user__date_joined")
def date_joined(self, obj: Profile) -> datetime:
return obj.user.date_joined
@mark_safe
def projects(self, obj):
@admin.display(ordering="last_active_date")
def last_active(self, obj: Profile) -> date | None:
if obj.last_active_date:
return obj.last_active_date.date()
return None
@admin.display(ordering="over_limit_date")
def over_limit(self, obj: Profile) -> date | None:
if obj.over_limit_date:
return obj.over_limit_date.date()
return None
@admin.display(ordering="deletion_scheduled_date")
def deletion(self, obj: Profile) -> date | None:
if obj.deletion_scheduled_date:
return obj.deletion_scheduled_date.date()
return None
def projects(self, obj: Profile) -> str:
return render_to_string("admin/profile_list_projects.html", {"profile": obj})
def invited(self, obj):
return "%d of %d" % (obj.num_members, obj.team_limit)
def checks(self, obj: WithAnnotations[Profile, ProfileAnnotations]) -> str:
tmpl = "{} of {}"
if obj.num_checks > 1:
tmpl = "<b>%s</b>" % tmpl
return format_html(tmpl, obj.num_checks, obj.check_limit)
def sms(self, obj):
return "%d of %d" % (obj.sms_sent, obj.sms_limit)
def invited(self, obj: WithAnnotations[Profile, ProfileAnnotations]) -> str:
return f"{obj.num_members} of {obj.team_limit}"
def sms(self, obj: Profile) -> str:
return f"{obj.sms_sent} of {obj.sms_limit}"
def login(self, r: HttpRequest, qs: QuerySet[Profile]) -> HttpResponseRedirect:
profile = qs.get()
auth_login(r, profile.user, "hc.accounts.backends.EmailBackend")
return redirect("hc-index")
def send_report(self, request: HttpRequest, qs: QuerySet[Profile]) -> None:
for profile in qs:
profile.send_report()
self.message_user(request, f"{len(qs)} email(s) sent")
def send_nag(self, request: HttpRequest, qs: QuerySet[Profile]) -> None:
for profile in qs:
profile.send_report(nag=True)
self.message_user(request, f"{len(qs)} email(s) sent")
@admin.action(description="Remove TOTP")
def remove_totp(self, request: HttpRequest, qs: QuerySet[Profile]) -> None:
for profile in qs:
profile.totp = None
profile.totp_created = None
profile.save()
self.message_user(request, f"Removed TOTP for {len(qs)} profile(s)")
def schedule_for_deletion(self, r: HttpRequest, qs: QuerySet[Profile]) -> None:
for profile in qs:
profile.schedule_for_deletion()
self.message_user(r, f"{len(qs)} user(s) scheduled for deletion")
def unschedule_for_deletion(self, r: HttpRequest, qs: QuerySet[Profile]) -> None:
num_unscheduled = qs.update(deletion_scheduled_date=None)
self.message_user(r, f"{num_unscheduled} user(s) unscheduled for deletion")
class ProjectAnnotations(TypedDict):
num_checks: int
num_channels: int
num_members: int
@admin.register(Project)
class ProjectAdmin(admin.ModelAdmin):
class ProjectAdmin(ModelAdmin[Project]):
readonly_fields = ("code", "owner")
list_select_related = ("owner",)
list_display = ("id", "name_", "users", "engagement", "switch")
list_display = ("id", "name_", "users", "usage", "switch")
search_fields = ["id", "name", "owner__email"]
class Media:
css = {"all": ("css/admin/projects.css",)}
def get_queryset(self, request):
def get_queryset(self, request: HttpRequest) -> QuerySet[Project]:
qs = super(ProjectAdmin, self).get_queryset(request)
qs = qs.annotate(num_channels=Count("channel", distinct=True))
qs = qs.annotate(num_checks=Count("check", distinct=True))
qs = qs.annotate(num_members=Count("member", distinct=True))
return qs
def name_(self, obj):
def name_(self, obj: Project) -> str:
if obj.name:
return obj.name
return "Default Project for %s" % obj.owner.email
return f"Default Project for {obj.owner.email}"
@mark_safe
def users(self, obj):
def users(self, obj: WithAnnotations[Project, ProjectAnnotations]) -> str:
if obj.num_members == 0:
return obj.owner.email
else:
return render_to_string("admin/project_list_team.html", {"project": obj})
def email(self, obj):
return obj.owner.email
def usage(self, obj: WithAnnotations[Project, ProjectAnnotations]) -> str:
return _format_usage(obj.num_checks, obj.num_channels)
@mark_safe
def engagement(self, obj):
result = ""
if obj.num_checks == 0:
result += "0 checks, "
elif obj.num_checks == 1:
result += "1 check, "
else:
result += "<strong>%d checks</strong>, " % obj.num_checks
if obj.num_channels == 0:
result += "0 channels"
elif obj.num_channels == 1:
result += "1 channel, "
else:
result += "<strong>%d channels</strong>, " % obj.num_channels
return result
@mark_safe
def switch(self, obj):
def switch(self, obj: Project) -> str:
url = reverse("hc-checks", args=[obj.code])
return "<a href='%s'>Show Checks</a>" % url
return format_html("<a href='{}'>Show Checks</a>", url)
class HcUserAdmin(UserAdmin):
actions = ["send_report", "send_nag"]
class UserAnnotations(TypedDict):
num_checks: int
num_channels: int
last_active_date: datetime | None
class HcUserAdmin(UserAdmin[User]):
list_display = (
"id",
"email",
"engagement",
"usage",
"date_joined",
"last_login",
"last_active",
"is_staff",
)
list_display_links = ("id", "email")
list_filter = ("last_login", "date_joined", "is_staff", "is_active")
actions = ("activate", "deactivate")
ordering = ["-id"]
def get_queryset(self, request):
def get_queryset(self, request: HttpRequest) -> QuerySet[User]:
qs = super().get_queryset(request)
qs = qs.annotate(num_checks=Count("project__check", distinct=True))
qs = qs.annotate(num_channels=Count("project__channel", distinct=True))
qs = qs.annotate(last_active_date=F("profile__last_active_date"))
return qs
@mark_safe
def engagement(self, user):
result = ""
def last_active(
self, user: WithAnnotations[User, UserAnnotations]
) -> datetime | None:
assert (
isinstance(user.last_active_date, datetime) or user.last_active_date is None
)
return user.last_active_date
if user.num_checks == 0:
result += "0 checks, "
elif user.num_checks == 1:
result += "1 check, "
else:
result += "<strong>%d checks</strong>, " % user.num_checks
def usage(self, user: WithAnnotations[User, UserAnnotations]) -> str:
return _format_usage(user.num_checks, user.num_channels)
if user.num_channels == 0:
result += "0 channels"
elif user.num_channels == 1:
result += "1 channel, "
else:
result += "<strong>%d channels</strong>, " % user.num_channels
return result
def send_report(self, request, qs):
def activate(self, request: HttpRequest, qs: QuerySet[User]) -> None:
for user in qs:
user.profile.send_report()
user.is_active = True
user.save()
self.message_user(request, "%d email(s) sent" % qs.count())
self.message_user(request, f"{len(qs)} user(s) activated")
def send_nag(self, request, qs):
def deactivate(self, request: HttpRequest, qs: QuerySet[User]) -> None:
for user in qs:
user.profile.send_report(nag=True)
user.is_active = False
user.set_unusable_password()
user.save()
self.message_user(request, "%d email(s) sent" % qs.count())
self.message_user(request, f"{len(qs)} user(s) deactivated")
admin.site.unregister(User)
admin.site.register(User, HcUserAdmin)
@admin.register(Credential)
class CredentialAdmin(ModelAdmin[Credential]):
list_display = ("id", "created", "email", "name")
search_fields = ["id", "code", "name", "user__email"]
list_filter = ["created"]
readonly_fields = ("user",)
def email(self, obj: Credential) -> str:
return obj.user.email

View file

@ -1,11 +1,17 @@
from __future__ import annotations
from django.conf import settings
from django.contrib.auth.models import User
from django.http import HttpRequest
from hc.accounts.models import Profile
from hc.accounts.views import _make_user
class BasicBackend(object):
def get_user(self, user_id):
class BasicBackend:
def get_user(self, user_id: int) -> User | None:
try:
q = User.objects.select_related("profile", "profile__current_project")
q = User.objects.select_related("profile")
return q.get(pk=user_id)
except User.DoesNotExist:
@ -14,25 +20,73 @@ class BasicBackend(object):
# Authenticate against the token in user's profile.
class ProfileBackend(BasicBackend):
def authenticate(self, request=None, username=None, token=None):
def authenticate(
self,
request: HttpRequest,
username: str | None = None,
token: str | None = None,
) -> User | None:
if not token:
return None
try:
profiles = Profile.objects.select_related("user")
profile = profiles.get(user__username=username)
except Profile.DoesNotExist:
return None
if not profile.check_token(token, "login"):
if not profile.check_token(token):
return None
return profile.user
class EmailBackend(BasicBackend):
def authenticate(self, request=None, username=None, password=None):
def authenticate(
self,
request: HttpRequest,
username: str | None = None,
password: str | None = None,
) -> User | None:
if not password:
return None
try:
user = User.objects.get(email=username)
except User.DoesNotExist:
return None
if user.check_password(password):
return user
if not user.check_password(password):
return None
return user
class CustomHeaderBackend(BasicBackend):
"""
This backend works in conjunction with the ``CustomHeaderMiddleware``,
and is used when the server is handling authentication outside of Django.
"""
def authenticate(
self, request: HttpRequest, remote_user_email: str | None = None
) -> User | None:
"""
The email address passed as remote_user_email is considered trusted.
Return the User object with the given email address. Create a new User
if it does not exist.
"""
# This backend should only be used when header-based authentication is enabled
assert settings.REMOTE_USER_HEADER
# remote_user_email should have a value
assert remote_user_email
try:
user = User.objects.get(email=remote_user_email)
except User.DoesNotExist:
user = _make_user(remote_user_email)
return user

57
hc/accounts/decorators.py Normal file
View file

@ -0,0 +1,57 @@
from __future__ import annotations
import secrets
from functools import wraps
from typing import Any
from django.core.signing import SignatureExpired, TimestampSigner
from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect, render
from hc.api.models import TokenBucket
from hc.lib import emails
from hc.lib.typealias import ViewFunc
def _session_unsign(request: HttpRequest, key: str, max_age: int) -> str | None:
if key not in request.session:
return None
try:
return TimestampSigner().unsign(request.session[key], max_age=max_age)
except SignatureExpired:
return None
def require_sudo_mode(f: ViewFunc) -> ViewFunc:
@wraps(f)
def wrapper(request: HttpRequest, *args: Any, **kwds: Any) -> HttpResponse:
assert request.user.is_authenticated
# is sudo mode active and has not expired yet?
if _session_unsign(request, "sudo", 1800) == "active":
return f(request, *args, **kwds)
if not TokenBucket.authorize_sudo_code(request.user):
return render(request, "try_later.html")
# has the user submitted a code to enter sudo mode?
if "sudo_code" in request.POST:
ours = _session_unsign(request, "sudo_code", 900)
if ours and ours == request.POST["sudo_code"]:
request.session.pop("sudo_code")
request.session["sudo"] = TimestampSigner().sign("active")
return redirect(request.path)
if not _session_unsign(request, "sudo_code", 900):
code = "%06d" % secrets.randbelow(1000000)
request.session["sudo_code"] = TimestampSigner().sign(code)
emails.sudo_code(request.user.email, {"sudo_code": code})
ctx = {}
if "sudo_code" in request.POST:
ctx["wrong_code"] = True
return render(request, "accounts/sudo.html", ctx)
return wrapper

View file

@ -1,47 +1,86 @@
from __future__ import annotations
from datetime import timedelta as td
from typing import Any
from django import forms
from django.contrib.auth import authenticate
from django.contrib.auth.models import User
from django.http import HttpRequest
from pyotp.totp import TOTP
from hc.accounts.models import REPORT_CHOICES, Member
from hc.api.models import TokenBucket
from hc.lib.tz import all_timezones
class LowercaseEmailField(forms.EmailField):
def clean(self, value):
def clean(self, value: str) -> str:
value = super(LowercaseEmailField, self).clean(value)
return value.lower()
class AvailableEmailForm(forms.Form):
class SignupForm(forms.Form):
# Call it "identity" instead of "email"
# to avoid some of the dumber bots
identity = LowercaseEmailField(
error_messages={"required": "Please enter your email address."}
)
tz = forms.CharField(required=False)
def __init__(self, request: HttpRequest):
self.request = request
super(SignupForm, self).__init__(request.POST)
def clean_identity(self) -> str:
if not TokenBucket.authorize_auth_ip(self.request):
raise forms.ValidationError("Too many attempts, please try later.")
def clean_identity(self):
v = self.cleaned_data["identity"]
if User.objects.filter(email=v).exists():
raise forms.ValidationError(
"An account with this email address already exists."
)
assert isinstance(v, str)
if len(v) > 254:
raise forms.ValidationError("Address is too long.")
return v
def clean_tz(self) -> str | None:
assert isinstance(self.cleaned_data["tz"], str)
# Declare tz as "clean" only if we can find it in hc.lib.tz.all_timezones
if self.cleaned_data["tz"] in all_timezones:
return self.cleaned_data["tz"]
# Otherwise, return None, and *don't* throw a validation exception:
# If user's browser reports a timezone we don't recognize, we
# should ignore the timezone but still save the rest of the form.
return None
class EmailLoginForm(forms.Form):
# Call it "identity" instead of "email"
# to avoid some of the dumber bots
identity = LowercaseEmailField()
def clean_identity(self):
def __init__(self, request: HttpRequest | None = None):
self.request = request
super(EmailLoginForm, self).__init__(request.POST if request else None)
def clean_identity(self) -> str:
v = self.cleaned_data["identity"]
assert isinstance(v, str)
if not TokenBucket.authorize_login_email(v):
raise forms.ValidationError("Too many attempts, please try later.")
assert self.request
if not TokenBucket.authorize_auth_ip(self.request):
raise forms.ValidationError("Too many attempts, please try later.")
self.user: User | None
try:
self.user = User.objects.get(email=v)
except User.DoesNotExist:
raise forms.ValidationError("Incorrect email address.")
self.user = None
return v
@ -50,7 +89,7 @@ class PasswordLoginForm(forms.Form):
email = LowercaseEmailField()
password = forms.CharField()
def clean(self):
def clean(self) -> dict[str, Any]:
username = self.cleaned_data.get("email")
password = self.cleaned_data.get("password")
@ -66,10 +105,11 @@ class PasswordLoginForm(forms.Form):
class ReportSettingsForm(forms.Form):
reports_allowed = forms.BooleanField(required=False)
reports = forms.ChoiceField(choices=REPORT_CHOICES)
nag_period = forms.IntegerField(min_value=0, max_value=86400)
tz = forms.CharField()
def clean_nag_period(self):
def clean_nag_period(self) -> td:
seconds = self.cleaned_data["nag_period"]
if seconds not in (0, 3600, 86400):
@ -77,6 +117,18 @@ class ReportSettingsForm(forms.Form):
return td(seconds=seconds)
def clean_tz(self) -> str | None:
assert isinstance(self.cleaned_data["tz"], str)
# Declare tz as "clean" only if we can find it in hc.lib.tz.all_timezones
if self.cleaned_data["tz"] in all_timezones:
return self.cleaned_data["tz"]
# Otherwise, return None, and *don't* throw a validation exception:
# If user's browser reports a timezone we don't recognize, we
# should ignore the timezone but still save the rest of the form.
return None
class SetPasswordForm(forms.Form):
password = forms.CharField(min_length=8)
@ -86,8 +138,9 @@ class ChangeEmailForm(forms.Form):
error_css_class = "has-error"
email = LowercaseEmailField()
def clean_email(self):
def clean_email(self) -> str:
v = self.cleaned_data["email"]
assert isinstance(v, str)
if User.objects.filter(email=v).exists():
raise forms.ValidationError("%s is already registered" % v)
@ -95,7 +148,8 @@ class ChangeEmailForm(forms.Form):
class InviteTeamMemberForm(forms.Form):
email = LowercaseEmailField()
email = LowercaseEmailField(max_length=254)
role = forms.ChoiceField(choices=Member.Role.choices)
class RemoveTeamMemberForm(forms.Form):
@ -103,4 +157,33 @@ class RemoveTeamMemberForm(forms.Form):
class ProjectNameForm(forms.Form):
name = forms.CharField(max_length=200, required=True)
name = forms.CharField(max_length=60)
class TransferForm(forms.Form):
email = LowercaseEmailField()
class AddWebAuthnForm(forms.Form):
name = forms.CharField(max_length=100)
response = forms.CharField()
class WebAuthnForm(forms.Form):
response = forms.CharField()
class TotpForm(forms.Form):
error_css_class = "has-error"
code = forms.RegexField(regex=r"^\d{6}$")
def __init__(self, totp: TOTP, post: Any = None):
self.totp = totp
super(TotpForm, self).__init__(post)
def clean_code(self) -> str:
assert isinstance(self.cleaned_data["code"], str)
if not self.totp.verify(self.cleaned_data["code"], valid_window=1):
raise forms.ValidationError("The code you entered was incorrect.")
return self.cleaned_data["code"]

11
hc/accounts/http.py Normal file
View file

@ -0,0 +1,11 @@
from __future__ import annotations
from django.contrib.auth.models import User
from django.http import HttpRequest
from hc.accounts.models import Profile
class AuthenticatedHttpRequest(HttpRequest):
user: User
profile: Profile

View file

@ -0,0 +1,51 @@
from __future__ import annotations
from getpass import getpass
from typing import Any
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.core.management.base import BaseCommand
from hc.accounts.forms import LowercaseEmailField
from hc.accounts.views import _make_user
class Command(BaseCommand):
help = """Create a super-user account."""
def handle(self, **options: Any) -> str:
email = None
password = None
while not email:
raw = input("Email address:")
try:
email = LowercaseEmailField().clean(raw)
except ValidationError as e:
self.stderr.write("Error: " + " ".join(e.messages))
continue
if User.objects.filter(email=email).exists():
self.stderr.write(f"Error: email {email} is already taken")
email = None
continue
while not password:
p1 = getpass()
p2 = getpass("Password (again):")
if p1.strip() == "":
self.stderr.write("Error: Blank passwords aren't allowed.")
continue
if p1 != p2:
self.stderr.write("Error: Your passwords didn't match.")
continue
password = p1
user = _make_user(email)
user.set_password(password)
user.is_staff = True
user.is_superuser = True
user.save()
return "Superuser created successfully."

View file

@ -1,9 +1,13 @@
from datetime import timedelta
from __future__ import annotations
from datetime import timedelta as td
from typing import Any
from django.contrib.auth.models import User
from django.core.management.base import BaseCommand
from django.db.models import Count, F
from django.utils.timezone import now
from hc.accounts.models import Profile
@ -18,8 +22,8 @@ class Command(BaseCommand):
"""
def handle(self, *args, **options):
month_ago = now() - timedelta(days=30)
def handle(self, **options: Any) -> str:
month_ago = now() - td(days=30)
# Old accounts, never logged in, no team memberships
q = User.objects.order_by("id")
@ -31,12 +35,12 @@ class Command(BaseCommand):
self.stdout.write("Pruned %d never-logged-in user accounts." % count)
# Profiles scheduled for deletion
q = Profile.objects.order_by("id")
q = q.filter(deletion_notice_date__lt=month_ago)
pq = Profile.objects.order_by("id")
pq = pq.filter(deletion_notice_date__lt=month_ago)
# Exclude users who have logged in after receiving deletion notice
q = q.exclude(user__last_login__gt=F("deletion_notice_date"))
pq = pq.exclude(user__last_login__gt=F("deletion_notice_date"))
for profile in q:
for profile in pq:
self.stdout.write("Deleting inactive %s" % profile.user.email)
profile.user.delete()

View file

@ -1,60 +0,0 @@
from datetime import timedelta
import time
from django.conf import settings
from django.core.management.base import BaseCommand
from django.utils.timezone import now
from hc.accounts.models import Profile, Member
from hc.api.models import Ping
from hc.lib import emails
class Command(BaseCommand):
help = """Send deletion notices to inactive user accounts.
Conditions for sending the notice:
- deletion notice has not been sent recently
- last login more than a year ago
- none of the owned projects has invited team members
"""
def handle(self, *args, **options):
year_ago = now() - timedelta(days=365)
q = Profile.objects.order_by("id")
# Exclude accounts with logins in the last year_ago
q = q.exclude(user__last_login__gt=year_ago)
# Exclude accounts less than a year_ago old
q = q.exclude(user__date_joined__gt=year_ago)
# Exclude accounts with the deletion notice already sent
q = q.exclude(deletion_notice_date__gt=year_ago)
# Exclude paid accounts
q = q.exclude(sms_limit__gt=0)
sent = 0
for profile in q:
members = Member.objects.filter(project__owner_id=profile.user_id)
if members.exists():
print("Skipping %s, has team members" % profile)
continue
pings = Ping.objects
pings = pings.filter(owner__project__owner_id=profile.user_id)
pings = pings.filter(created__gt=year_ago)
if pings.exists():
print("Skipping %s, has pings in last year" % profile)
continue
self.stdout.write("Sending notice to %s" % profile.user.email)
profile.deletion_notice_date = now()
profile.save()
ctx = {"email": profile.user.email, "support_email": settings.SUPPORT_EMAIL}
emails.deletion_notice(profile.user.email, ctx)
# Throttle so we don't send too many emails at once:
time.sleep(1)
sent += 1
return "Done! Sent %d notices" % sent

View file

@ -0,0 +1,95 @@
from __future__ import annotations
import time
from datetime import timedelta as td
from typing import Any
from django.conf import settings
from django.contrib.auth.models import User
from django.core.management.base import BaseCommand
from django.db.models import QuerySet
from django.utils.timezone import now
from hc.accounts.models import Profile
from hc.api.models import Channel, Check, Flip
from hc.lib import emails
class Command(BaseCommand):
help = """Send warnings to accounts marked for deletion. """
def pause(self) -> None:
time.sleep(1)
def members(self, user: User) -> QuerySet[User]:
q = User.objects.filter(memberships__project__owner=user)
q = q.exclude(last_login=None)
return q.order_by("email")
def send_channel_notifications(
self, profile: Profile, skip_emails: list[str]
) -> None:
# Sending deletion notices to configured notification channels is
# a last ditch effort: only do this if 14 or fewer days are left.
assert profile.deletion_scheduled_date
delta = profile.deletion_scheduled_date - now()
if delta.days > 14:
return
formatted = profile.deletion_scheduled_date.strftime("%B %-d, %Y")
name = f"{settings.SITE_NAME} Account Deletion on {formatted}"
desc = (
f"The {settings.SITE_NAME} account registered to {profile.user.email} "
f"is scheduled for deletion on {formatted}. To keep the account, "
f"please contact {settings.SUPPORT_EMAIL} ASAP."
)
for channel in Channel.objects.filter(project__owner_id=profile.user_id):
if channel.kind == "email" and channel.email.value in skip_emails:
continue
dummy = Check(name=name, desc=desc, status="down", project=channel.project)
dummy.last_ping = now() - td(days=1)
dummy.n_pings = 1
dummy_flip = Flip(owner=dummy)
dummy_flip.created = now()
dummy_flip.old_status = "up"
dummy_flip.new_status = "down"
self.stdout.write(f" * Sending notification to {channel.kind}")
error = channel.notify(dummy_flip, is_test=True)
if error == "no-op":
# This channel may be configured to send "up" notifications only.
dummy.status = "up"
error = channel.notify(dummy_flip, is_test=True)
if error:
self.stdout.write(f" Error sending notification: {error}")
def handle(self, **options: Any) -> str:
q = Profile.objects.order_by("id")
q = q.filter(deletion_scheduled_date__gt=now())
sent = 0
for profile in q:
recipients = [profile.user.email]
# Include team members in the recipient list too:
for u in self.members(profile.user):
if u.email not in recipients:
recipients.append(u.email)
self.stdout.write(f"Sending notice to {recipients}")
ctx = {
"owner_email": profile.user.email,
"num_checks": profile.num_checks_used(),
"support_email": settings.SUPPORT_EMAIL,
"deletion_scheduled_date": profile.deletion_scheduled_date,
}
emails.deletion_scheduled(recipients, ctx)
self.send_channel_notifications(profile, skip_emails=recipients)
sent += 1
# Throttle so we don't send too many emails at once:
self.pause()
return f"Done!\nNotices sent: {sent}\n"

View file

@ -0,0 +1,90 @@
from __future__ import annotations
import time
from datetime import timedelta as td
from typing import Any
from django.conf import settings
from django.core.management.base import BaseCommand
from django.db.models import Q
from django.utils.timezone import now
from hc.accounts.models import Member, Profile
from hc.api.models import Ping
from hc.lib import emails
YEAR_AGO = now() - td(days=365)
def has_projects_with_active_members(profile: Profile) -> bool:
q = Member.objects.filter(project__owner_id=profile.user_id)
recent_signup = Q(user__date_joined__gt=YEAR_AGO)
recent_login = Q(user__last_login__gt=YEAR_AGO)
recent_activity = Q(user__profile__last_active_date__gt=YEAR_AGO)
q = q.filter(recent_signup | recent_login | recent_activity)
return q.exists()
class Command(BaseCommand):
help = """Send deletion notices to inactive user accounts.
Conditions for sending the notice:
- deletion notice has not been sent recently
- last login more than a year ago
- none of the owned projects has active team members
- none of the owned projects has pings in the last year
- is on a free plan
"""
def pause(self) -> None:
time.sleep(1)
def handle(self, **options: Any) -> str:
q = Profile.objects.order_by("id")
# Exclude accounts with logins in the last year
q = q.exclude(user__last_login__gt=YEAR_AGO)
# Exclude accounts less than a year old
q = q.exclude(user__date_joined__gt=YEAR_AGO)
# Exclude accounts with the deletion notice already sent
q = q.exclude(deletion_notice_date__gt=YEAR_AGO)
# Exclude accounts with activity in the last year
q = q.exclude(last_active_date__gt=YEAR_AGO)
# Exclude accounts with subscriptions
q = q.exclude(user__subscription__subscription_id__gt="")
sent = 0
skipped_has_team = 0
skipped_has_pings = 0
for profile in q:
if has_projects_with_active_members(profile):
# Don't send deletion notice: this account has team members
skipped_has_team += 1
continue
pings = Ping.objects.filter(owner__project__owner_id=profile.user_id)
pings = pings.filter(created__gt=YEAR_AGO)
if pings.exists():
# Don't send deletion notice: this account has pings in the last year
skipped_has_pings += 1
continue
self.stdout.write("Sending notice to %s" % profile.user.email)
profile.deletion_notice_date = now()
profile.save()
ctx = {"email": profile.user.email, "support_email": settings.SUPPORT_EMAIL}
emails.deletion_notice(profile.user.email, ctx)
sent += 1
# Throttle so we don't send too many emails at once:
self.pause()
return (
f"Done!\n"
f"* Notices sent: {sent}\n"
f"* Skipped (has team members): {skipped_has_team}\n"
f"* Skipped (has pings in the last year): {skipped_has_pings}\n"
)

View file

@ -1,17 +1,83 @@
from __future__ import annotations
from typing import Callable
from django.conf import settings
from django.contrib import auth
from django.contrib.auth.models import User
from django.core.exceptions import MiddlewareNotUsed
from django.http import HttpRequest, HttpResponse
from hc.accounts.models import Profile
MiddlewareFunc = Callable[[HttpRequest], HttpResponse]
class TeamAccessMiddleware(object):
def __init__(self, get_response):
class TeamAccessMiddleware:
def __init__(self, get_response: MiddlewareFunc) -> None:
self.get_response = get_response
def __call__(self, request):
def __call__(self, request: HttpRequest) -> HttpResponse:
if not request.user.is_authenticated:
return self.get_response(request)
profile = Profile.objects.for_user(request.user)
setattr(request, "profile", Profile.objects.for_user(request.user))
return self.get_response(request)
request.profile = profile
request.project = profile.current_project
class CustomHeaderMiddleware:
"""
Middleware for utilizing Web-server-provided authentication.
If request.user is not authenticated, then this middleware:
- looks for an email address in request.META[settings.REMOTE_USER_HEADER]
- looks up and automatically logs in the user with a matching email
"""
def __init__(self, get_response: MiddlewareFunc) -> None:
if not settings.REMOTE_USER_HEADER:
raise MiddlewareNotUsed()
self.get_response = get_response
def __call__(self, request: HttpRequest) -> HttpResponse:
assert settings.REMOTE_USER_HEADER
# Make sure AuthenticationMiddleware is installed
assert hasattr(request, "user")
email = request.META.get(settings.REMOTE_USER_HEADER)
if not email:
# If specified header doesn't exist or is empty then log out any
# authenticated user and return
if request.user.is_authenticated:
auth.logout(request)
return self.get_response(request)
# The email address from the external authentication system may be in
# in upper case or mixed case. Convert it to lower case, as we do
# elsewhere in the system (when registering new users, when inviting users
# into projects, when changing email address) to avoid naming conflicts.
email = email.lower()
# If the user is already authenticated and that user is the user we are
# getting passed in the headers, then the correct user is already
# persisted in the session and we don't need to continue.
if request.user.is_authenticated:
if request.user.email == email:
return self.get_response(request)
else:
# An authenticated user is associated with the request, but
# it does not match the authorized user in the header.
auth.logout(request)
# We are seeing this user for the first time in this session, attempt
# to authenticate the user.
if user := auth.authenticate(request, remote_user_email=email):
assert isinstance(user, User)
# User is valid. Set request.user and persist user in the session
# by logging the user in.
request.user = user
auth.login(request, user)
return self.get_response(request)

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
from django.conf import settings

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models

View file

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2016-01-04 20:26
from __future__ import unicode_literals
from django.db import migrations, models

View file

@ -1,6 +1,4 @@
# -*- 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

View file

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2016-05-09 08:01
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models

View file

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2016-05-09 10:34
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion

View file

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-05-07 13:04
from __future__ import unicode_literals
from django.db import migrations, models

View file

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-06-08 11:58
from __future__ import unicode_literals
from django.db import migrations, models

View file

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-07-14 17:34
from __future__ import unicode_literals
from django.db import migrations, models

View file

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-09-02 11:52
from __future__ import unicode_literals
from django.db import migrations, models

View file

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-09-12 14:24
from __future__ import unicode_literals
from django.db import migrations, models

View file

@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-10-14 10:02
from __future__ import unicode_literals
import datetime
from datetime import timedelta as td
from django.db import migrations, models
@ -16,11 +14,11 @@ class Migration(migrations.Migration):
name="nag_period",
field=models.DurationField(
choices=[
(datetime.timedelta(0), "Disabled"),
(datetime.timedelta(0, 3600), "Hourly"),
(datetime.timedelta(1), "Daily"),
(td(0), "Disabled"),
(td(0, 3600), "Hourly"),
(td(1), "Daily"),
],
default=datetime.timedelta(0),
default=td(0),
),
),
migrations.AddField(

View file

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-10-14 16:15
from __future__ import unicode_literals
from django.db import migrations

View file

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-12-27 15:30
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models

View file

@ -1,9 +1,14 @@
# Generated by Django 2.1.5 on 2019-01-11 14:49
from __future__ import annotations
from typing import Any
from django.apps.registry import Apps
from django.db import migrations
def create_projects(apps, schema_editor):
def create_projects(apps: Apps, schema_editor: Any) -> None:
Profile = apps.get_model("accounts", "Profile")
Project = apps.get_model("accounts", "Project")
Member = apps.get_model("accounts", "Member")
@ -27,7 +32,6 @@ def create_projects(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [("accounts", "0017_auto_20190112_1426")]
operations = [migrations.RunPython(create_projects, migrations.RunPython.noop)]

View file

@ -1,9 +1,14 @@
# Generated by Django 2.1.5 on 2019-01-12 19:50
from __future__ import annotations
from typing import Any
from django.apps.registry import Apps
from django.db import migrations
def set_badge_key(apps, schema_editor):
def set_badge_key(apps: Apps, schema_editor: Any) -> None:
Project = apps.get_model("accounts", "Project")
for project in Project.objects.select_related("owner").all():
project.badge_key = project.owner.username
@ -11,7 +16,6 @@ def set_badge_key(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [("accounts", "0019_project_badge_key")]
operations = [migrations.RunPython(set_badge_key, migrations.RunPython.noop)]

View file

@ -0,0 +1,23 @@
# Generated by Django 2.2.6 on 2019-11-19 13:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0027_profile_deletion_notice_date"),
]
operations = [
migrations.AddField(
model_name="profile",
name="last_active_date",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AlterField(
model_name="profile",
name="sms_limit",
field=models.IntegerField(default=5),
),
]

View file

@ -0,0 +1,17 @@
# Generated by Django 3.0.1 on 2020-03-02 07:56
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("accounts", "0028_auto_20191119_1346"),
]
operations = [
migrations.RemoveField(
model_name="profile",
name="current_project",
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 3.0.4 on 2020-04-11 13:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0029_remove_profile_current_project"),
]
operations = [
migrations.AddField(
model_name="member",
name="transfer_request_date",
field=models.DateTimeField(blank=True, null=True),
),
]

View file

@ -0,0 +1,28 @@
# Generated by Django 3.0.8 on 2020-08-03 14:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0030_member_transfer_request_date"),
]
operations = [
migrations.AddField(
model_name="profile",
name="call_limit",
field=models.IntegerField(default=0),
),
migrations.AddField(
model_name="profile",
name="calls_sent",
field=models.IntegerField(default=0),
),
migrations.AddField(
model_name="profile",
name="last_call_date",
field=models.DateTimeField(blank=True, null=True),
),
]

View file

@ -0,0 +1,19 @@
# Generated by Django 3.1 on 2020-08-19 07:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0031_auto_20200803_1413"),
]
operations = [
migrations.AddConstraint(
model_name="member",
constraint=models.UniqueConstraint(
fields=("user", "project"), name="accounts_member_no_duplicates"
),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 3.1 on 2020-08-24 16:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0032_auto_20200819_0757"),
]
operations = [
migrations.AddField(
model_name="member",
name="rw",
field=models.BooleanField(default=True),
),
]

View file

@ -0,0 +1,43 @@
# Generated by Django 3.1.2 on 2020-11-14 09:29
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("accounts", "0033_member_rw"),
]
operations = [
migrations.CreateModel(
name="Credential",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("code", models.UUIDField(default=uuid.uuid4, unique=True)),
("name", models.CharField(max_length=100)),
("created", models.DateTimeField(auto_now_add=True)),
("data", models.BinaryField()),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="credentials",
to=settings.AUTH_USER_MODEL,
),
),
],
),
]

View file

@ -0,0 +1,22 @@
# Generated by Django 3.2.2 on 2021-05-24 07:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0034_credential"),
]
operations = [
migrations.AddField(
model_name="profile",
name="reports",
field=models.CharField(
choices=[("off", "Off"), ("weekly", "Weekly"), ("monthly", "Monthly")],
default="monthly",
max_length=10,
),
),
]

View file

@ -0,0 +1,22 @@
# Generated by Django 3.2.2 on 2021-05-24 07:38
from __future__ import annotations
from typing import Any
from django.apps.registry import Apps
from django.db import migrations
def fill_reports_field(apps: Apps, schema_editor: Any) -> None:
Profile = apps.get_model("accounts", "Profile")
Profile.objects.filter(reports_allowed=False).update(reports="off")
Profile.objects.filter(reports_allowed=True).update(reports="monthly")
class Migration(migrations.Migration):
dependencies = [
("accounts", "0035_profile_reports"),
]
operations = [migrations.RunPython(fill_reports_field, migrations.RunPython.noop)]

View file

@ -0,0 +1,18 @@
# Generated by Django 3.2.2 on 2021-05-24 09:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0036_fill_profile_reports"),
]
operations = [
migrations.AddField(
model_name="profile",
name="tz",
field=models.CharField(default="UTC", max_length=36),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 3.2.4 on 2021-06-18 09:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0037_profile_tz"),
]
operations = [
migrations.AddField(
model_name="profile",
name="theme",
field=models.CharField(blank=True, max_length=10, null=True),
),
]

View file

@ -0,0 +1,17 @@
# Generated by Django 3.2.4 on 2021-06-29 11:35
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("accounts", "0038_profile_theme"),
]
operations = [
migrations.RemoveField(
model_name="profile",
name="reports_allowed",
),
]

View file

@ -0,0 +1,25 @@
# Generated by Django 3.2.4 on 2021-07-22 12:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0039_remove_profile_reports_allowed"),
]
operations = [
migrations.AddField(
model_name="member",
name="role",
field=models.CharField(
choices=[("r", "Read-only"), ("w", "Member")], default="w", max_length=1
),
),
migrations.AlterField(
model_name="member",
name="rw",
field=models.BooleanField(default=True, null=True),
),
]

View file

@ -0,0 +1,24 @@
# Generated by Django 3.2.4 on 2021-07-22 13:25
from __future__ import annotations
from typing import Any
from django.apps.registry import Apps
from django.db import migrations
def fill_member_role(apps: Apps, schema_editor: Any) -> None:
Member = apps.get_model("accounts", "Member")
Member.objects.filter(rw=False).update(role="r")
Member.objects.filter(rw=True).update(role="w")
class Migration(migrations.Migration):
dependencies = [
("accounts", "0040_auto_20210722_1244"),
]
operations = [
migrations.RunPython(fill_member_role, migrations.RunPython.noop),
]

View file

@ -0,0 +1,17 @@
# Generated by Django 3.2.4 on 2021-07-22 14:39
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("accounts", "0041_fill_role"),
]
operations = [
migrations.RemoveField(
model_name="member",
name="rw",
),
]

View file

@ -0,0 +1,19 @@
from django.db import migrations, models
from hc.accounts.models import Member
class Migration(migrations.Migration):
dependencies = [
("accounts", "0042_remove_member_rw"),
]
operations = [
migrations.AlterField(
model_name="member",
name="role",
field=models.CharField(
choices=Member.Role.choices, default=Member.Role.REGULAR, max_length=1
),
),
]

View file

@ -0,0 +1,23 @@
# Generated by Django 3.2.4 on 2021-07-30 09:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0043_add_role_manager"),
]
operations = [
migrations.AddField(
model_name="profile",
name="totp",
field=models.CharField(blank=True, max_length=32, null=True),
),
migrations.AddField(
model_name="profile",
name="totp_created",
field=models.DateTimeField(blank=True, null=True),
),
]

View file

@ -0,0 +1,23 @@
# Generated by Django 3.2.6 on 2021-09-08 12:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0044_auto_20210730_0942"),
]
operations = [
migrations.AddField(
model_name="project",
name="ping_key",
field=models.CharField(blank=True, max_length=128, null=True, unique=True),
),
migrations.AddField(
model_name="project",
name="show_slugs",
field=models.BooleanField(default=False),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 4.2 on 2023-04-28 11:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0045_auto_20210908_1257"),
]
operations = [
migrations.AddField(
model_name="profile",
name="deletion_scheduled_date",
field=models.DateTimeField(blank=True, null=True),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 4.2.2 on 2023-06-16 13:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0046_profile_deletion_scheduled_date"),
]
operations = [
migrations.AddField(
model_name="profile",
name="over_limit_date",
field=models.DateTimeField(blank=True, null=True),
),
]

View file

@ -0,0 +1,23 @@
# Generated by Django 4.2.4 on 2023-08-29 13:16
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("accounts", "0047_profile_over_limit_date"),
]
operations = [
migrations.AlterField(
model_name="profile",
name="user",
field=models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
]

View file

@ -0,0 +1,46 @@
# Generated by Django 5.1.1 on 2024-10-24 08:53
from __future__ import annotations
from typing import Any
from django.apps.registry import Apps
from django.core.management import CommandError
from django.db import migrations
from django.db.models.functions import Lower
def convert_emails(apps: Apps, schema_editor: Any) -> None:
User = apps.get_model("auth", "User")
# A queryset of users with non-lowercase email addresses
problematic_users = User.objects.exclude(email=Lower("email"))
# For each affected user, check if their normalized email address would
# conflict with another user's normalized email address.
# The situation we want to protect against is where before migration we have:
# * Alice@Example.Org
# * ALICE@EXAMPLE.ORG
# And after migration we have
# * alice@example.org
# * alice@example.org
# (Two accounts with the same email address).
for u in problematic_users:
q = User.objects.exclude(id=u.id).filter(email__iexact=u.email)
if conflicting_user := q.first():
raise CommandError(
f"Cannot convert {u.email} to lower case because of an existing "
f"account with a conflicting email address: {conflicting_user.email}"
)
# If no conflicts, go ahead and do a mass update
problematic_users.update(email=Lower("email"))
class Migration(migrations.Migration):
dependencies = [
("accounts", "0048_alter_profile_user"),
]
operations = [
migrations.RunPython(convert_emails, migrations.RunPython.noop),
]

View file

@ -1,211 +1,372 @@
from base64 import urlsafe_b64encode
from datetime import timedelta
import os
from __future__ import annotations
import random
import uuid
from datetime import date, datetime
from datetime import timedelta as td
from secrets import token_urlsafe
from typing import TYPE_CHECKING, Any
from urllib.parse import quote, urlencode
from zoneinfo import ZoneInfo
from django.conf import settings
from django.contrib.auth.hashers import check_password, make_password
from django.contrib.auth.models import User
from django.core.signing import TimestampSigner
from django.core.signing import BadSignature, TimestampSigner
from django.db import models
from django.db.models import Count, Q
from django.db.models import Q, QuerySet
from django.db.models.functions import Lower
from django.urls import reverse
from django.utils import timezone
from django.utils.timezone import now
from hc.lib import emails
from hc.lib.date import month_boundaries
from hc.lib.date import month_boundaries, week_boundaries
from hc.lib.signing import sign_bounce_id
from hc.lib.urls import absolute_reverse
if TYPE_CHECKING:
# Importing Check at runtime would cause a circular import, so only import it
# during type checking
from hc.api.models import Check
CheckQuerySet = QuerySet[Check]
NO_NAG = timedelta()
NO_NAG = td()
NAG_PERIODS = (
(NO_NAG, "Disabled"),
(timedelta(hours=1), "Hourly"),
(timedelta(days=1), "Daily"),
(td(hours=1), "Hourly"),
(td(days=1), "Daily"),
)
REPORT_CHOICES = (("off", "Off"), ("weekly", "Weekly"), ("monthly", "Monthly"))
# How long an account can be over limits before it is scheduled for deletion
OVER_LIMIT_GRACE = td(days=31)
# When scheduling for deletion, how many days in the future to schedule
DELETION_GRACE = td(days=31)
def month(dt):
""" For a given datetime, return the matching first-day-of-month date. """
def month(dt: datetime) -> date:
"""For a given datetime, return the matching first-day-of-month date."""
return dt.date().replace(day=1)
class ProfileManager(models.Manager):
def for_user(self, user):
class ProfileManager(models.Manager["Profile"]):
def for_user(self, user: User) -> Profile:
try:
return user.profile
except Profile.DoesNotExist:
profile = Profile(user=user)
if not settings.USE_PAYMENTS:
# If not using payments, set high limits
profile.check_limit = 500
profile.sms_limit = 500
profile.team_limit = 500
profile.check_limit = 10000
profile.sms_limit = 10000
profile.call_limit = 10000
profile.team_limit = 10000
profile.save()
return profile
class Profile(models.Model):
user = models.OneToOneField(User, models.CASCADE, blank=True, null=True)
user = models.OneToOneField(User, models.CASCADE)
next_report_date = models.DateTimeField(null=True, blank=True)
reports_allowed = models.BooleanField(default=True)
reports = models.CharField(max_length=10, default="monthly", choices=REPORT_CHOICES)
nag_period = models.DurationField(default=NO_NAG, choices=NAG_PERIODS)
next_nag_date = models.DateTimeField(null=True, blank=True)
ping_log_limit = models.IntegerField(default=100)
check_limit = models.IntegerField(default=20)
token = models.CharField(max_length=128, blank=True)
current_project = models.ForeignKey("Project", models.SET_NULL, null=True)
last_sms_date = models.DateTimeField(null=True, blank=True)
sms_limit = models.IntegerField(default=0)
sms_limit = models.IntegerField(default=5)
sms_sent = models.IntegerField(default=0)
last_call_date = models.DateTimeField(null=True, blank=True)
call_limit = models.IntegerField(default=0)
calls_sent = models.IntegerField(default=0)
team_limit = models.IntegerField(default=2)
sort = models.CharField(max_length=20, default="created")
# The date when "Inactive Account Notification" is sent
deletion_notice_date = models.DateTimeField(null=True, blank=True)
# Set manually by admin, causes an orange banner in web UI
deletion_scheduled_date = models.DateTimeField(null=True, blank=True)
# If the account is over its check limit, the date when it went over the limit
over_limit_date = models.DateTimeField(null=True, blank=True)
last_active_date = models.DateTimeField(null=True, blank=True)
tz = models.CharField(max_length=36, default="UTC")
theme = models.CharField(max_length=10, null=True, blank=True)
totp = models.CharField(max_length=32, null=True, blank=True)
totp_created = models.DateTimeField(null=True, blank=True)
objects = ProfileManager()
def __str__(self):
return "Profile for %s" % self.user.email
def __str__(self) -> str:
return f"Profile for {self.user.email}"
def notifications_url(self):
return settings.SITE_ROOT + reverse("hc-notifications")
def notifications_url(self) -> str:
return absolute_reverse("hc-notifications")
def reports_unsub_url(self):
def reports_unsub_url(self) -> str:
signer = TimestampSigner(salt="reports")
signed_username = signer.sign(self.user.username)
path = reverse("hc-unsubscribe-reports", args=[signed_username])
return settings.SITE_ROOT + path
return absolute_reverse("hc-unsubscribe-reports", args=[signed_username])
def prepare_token(self, salt):
token = urlsafe_b64encode(os.urandom(24)).decode()
self.token = make_password(token, salt)
def prepare_token(self) -> str:
token = token_urlsafe(24)
# Store a hashed transformation of the login token
self.token = make_password(token, "login")
self.save()
return token
# Sign the token so we can check its age later
return TimestampSigner().sign(token)
def check_token(self, token, salt):
return salt in self.token and check_password(token, self.token)
def check_token(self, token: str) -> bool:
try:
token = TimestampSigner().unsign(token, max_age=3600)
except BadSignature:
return False
def send_instant_login_link(self, inviting_project=None, redirect_url=None):
token = self.prepare_token("login")
path = reverse("hc-check-token", args=[self.user.username, token])
return "login" in self.token and check_password(token, self.token)
def send_instant_login_link(
self, membership: Member | None = None, redirect_url: str | None = None
) -> None:
token = self.prepare_token()
url = absolute_reverse("hc-check-token", args=[self.user.username, token])
if redirect_url:
path += "?next=%s" % redirect_url
url += "?next=%s" % redirect_url
ctx = {
"button_text": "Sign In",
"button_url": settings.SITE_ROOT + path,
"inviting_project": inviting_project,
"button_text": "Log In",
"button_url": url,
"membership": membership,
}
emails.login(self.user.email, ctx)
def send_set_password_link(self):
token = self.prepare_token("set-password")
path = reverse("hc-set-password", args=[token])
ctx = {"button_text": "Set Password", "button_url": settings.SITE_ROOT + path}
emails.set_password(self.user.email, ctx)
def send_change_email_link(self, new_email: str) -> None:
payload = {
"u": self.user.username,
"t": self.prepare_token(),
"e": new_email,
}
signed_payload = TimestampSigner().sign_object(payload)
url = absolute_reverse("hc-change-email-verify", args=[signed_payload])
def send_change_email_link(self):
token = self.prepare_token("change-email")
path = reverse("hc-change-email", args=[token])
ctx = {"button_text": "Change Email", "button_url": settings.SITE_ROOT + path}
emails.change_email(self.user.email, ctx)
ctx = {
"button_text": "Log In",
"button_url": url,
}
emails.login(new_email, ctx)
def projects(self):
""" Return a queryset of all projects we have access to. """
def send_transfer_request(self, project: Project) -> None:
token = self.prepare_token()
settings_path = reverse("hc-project-settings", args=[project.code])
url = absolute_reverse("hc-check-token", args=[self.user.username, token])
url += f"?next={settings_path}"
is_owner = Q(owner=self.user)
is_member = Q(member__user=self.user)
ctx = {
"button_text": "Project Settings",
"button_url": url,
"project": project,
}
emails.transfer_request(self.user.email, ctx)
def send_sms_limit_notice(self, transport: str) -> None:
ctx = {"transport": transport, "limit": self.sms_limit}
if self.sms_limit != 500 and settings.USE_PAYMENTS:
ctx["url"] = absolute_reverse("hc-pricing")
emails.sms_limit(self.user.email, ctx)
def send_call_limit_notice(self) -> None:
ctx: dict[str, Any] = {"limit": self.call_limit}
if self.call_limit != 500 and settings.USE_PAYMENTS:
ctx["url"] = absolute_reverse("hc-pricing")
emails.call_limit(self.user.email, ctx)
def projects(self) -> QuerySet[Project]:
"""Return a queryset of all projects we have access to."""
is_owner = Q(owner_id=self.user_id)
is_member = Q(member__user_id=self.user_id)
q = Project.objects.filter(is_owner | is_member)
return q.distinct().order_by("name")
return q.distinct().order_by(Lower("name"))
def annotated_projects(self):
""" Return all projects, annotated with 'n_down'. """
# Subquery for getting project ids
project_ids = self.projects().values("id")
# Main query with the n_down annotation.
# Must use the subquery, otherwise ORM gets confused by
# joins and group by's
q = Project.objects.filter(id__in=project_ids)
n_down = Count("check", filter=Q(check__status="down"))
q = q.annotate(n_down=n_down)
return q.order_by("name")
def checks_from_all_projects(self):
""" Return a queryset of checks from projects we have access to. """
project_ids = self.projects().values("id")
def checks_from_all_projects(self) -> CheckQuerySet:
"""Return a queryset of checks from projects we have access to."""
from hc.api.models import Check
return Check.objects.filter(project_id__in=project_ids)
return Check.objects.filter(project__in=self.projects())
def send_report(self, nag=False):
checks = self.checks_from_all_projects()
def send_report(self, nag: bool = False) -> bool:
q = self.checks_from_all_projects()
# Has there been a ping in last 6 months?
result = checks.aggregate(models.Max("last_ping"))
result = q.aggregate(models.Max("last_ping"))
last_ping = result["last_ping__max"]
six_months_ago = timezone.now() - timedelta(days=180)
six_months_ago = now() - td(days=180)
if last_ping is None or last_ping < six_months_ago:
return False
# Is there at least one check that is down?
num_down = checks.filter(status="down").count()
if nag and num_down == 0:
return False
# Sort checks by project. Need this because will group by project in
# template.
checks = checks.select_related("project")
checks = checks.order_by("project_id")
# list() executes the query, to avoid DB access while
# rendering the template
checks = list(checks)
# Sort checks by project. Need this because will group by project in template.
q = q.select_related("project").order_by("project_id")
# list() executes the query, to avoid DB access while rendering the template.
checks = list(q)
unsub_url = self.reports_unsub_url()
headers = {"List-Unsubscribe": unsub_url, "X-Bounce-Url": unsub_url}
ctx = {
"checks": checks,
headers = {
"X-Bounce-ID": sign_bounce_id("r.%s" % self.user.username),
"List-Unsubscribe": "<%s>" % unsub_url,
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click",
}
ctx: dict[str, Any] = {
"sort": self.sort,
"now": timezone.now(),
"unsub_link": unsub_url,
"notifications_url": self.notifications_url(),
"nag": nag,
"nag_period": self.nag_period.total_seconds(),
"num_down": num_down,
"month_boundaries": month_boundaries(),
"tz": self.tz,
}
emails.report(self.user.email, ctx, headers)
if not nag:
# For weekly and monthly reports, calculate the downtimes,
# throw away the current period, keep two previous periods
if self.reports == "weekly":
boundaries = week_boundaries(3, self.tz)
else:
boundaries = month_boundaries(3, self.tz)
for check in checks:
downtimes = check.downtimes_by_boundary(boundaries, self.tz)
# downtimes_by_boundary returns records in descending order,
# but the template will need them in ascending order:
downtimes.reverse()
setattr(check, "past_downtimes", downtimes[:-1])
# boundaries are in descending order, but the template
# will need them in ascending order:
boundaries.reverse()
ctx["checks"] = checks
ctx["boundaries"] = boundaries[:-1]
ctx["monthly_or_weekly"] = self.reports
emails.report(self.user.email, ctx, headers)
if nag:
# For nags, only show checks that are currently down
checks = [c for c in checks if c.get_status() == "down"]
if not checks:
return False
ctx["checks"] = checks
ctx["num_down"] = len(checks)
ctx["nag_period"] = self.nag_period.total_seconds()
emails.nag(self.user.email, ctx, headers)
return True
def sms_sent_this_month(self):
def sms_sent_this_month(self) -> int:
# IF last_sms_date was never set, we have not sent any messages yet.
if not self.last_sms_date:
return 0
# If last sent date is not from this month, we've sent 0 this month.
if month(timezone.now()) > month(self.last_sms_date):
if month(now()) > month(self.last_sms_date):
return 0
return self.sms_sent
def authorize_sms(self):
""" If monthly limit not exceeded, increase counter and return True """
def authorize_sms(self) -> bool:
"""If monthly limit not exceeded, increase counter and return True"""
sent_this_month = self.sms_sent_this_month()
if sent_this_month >= self.sms_limit:
return False
self.sms_sent = sent_this_month + 1
self.last_sms_date = timezone.now()
self.last_sms_date = now()
self.save()
return True
def calls_sent_this_month(self) -> int:
# IF last_call_date was never set, we have not made any phone calls yet.
if not self.last_call_date:
return 0
# If last sent date is not from this month, we've made 0 calls this month.
if month(now()) > month(self.last_call_date):
return 0
return self.calls_sent
def authorize_call(self) -> bool:
"""If monthly limit not exceeded, increase counter and return True"""
sent_this_month = self.calls_sent_this_month()
if sent_this_month >= self.call_limit:
return False
self.calls_sent = sent_this_month + 1
self.last_call_date = now()
self.save()
return True
def num_checks_used(self) -> int:
from hc.api.models import Check
return Check.objects.filter(project__owner_id=self.user_id).count()
def num_checks_available(self) -> int:
return self.check_limit - self.num_checks_used()
def can_accept(self, project: Project) -> bool:
return project.check_set.count() <= self.num_checks_available()
def update_next_nag_date(self) -> None:
any_down = self.checks_from_all_projects().filter(status="down").exists()
if any_down and self.next_nag_date is None and self.nag_period:
self.next_nag_date = now() + self.nag_period
self.save(update_fields=["next_nag_date"])
elif not any_down and self.next_nag_date:
self.next_nag_date = None
self.save(update_fields=["next_nag_date"])
def choose_next_report_date(self) -> datetime | None:
"""Calculate the target date for the next monthly/weekly report.
Monthly reports should get sent on 1st of each month, between
9AM and 11AM in user's timezone.
Weekly reports should get sent on Mondays, between
9AM and 11AM in user's timezone.
"""
if self.reports == "off":
return None
dt = now().astimezone(ZoneInfo(self.tz))
dt = dt.replace(hour=9, minute=0) + td(minutes=random.randrange(0, 120))
while True:
dt += td(days=1)
if self.reports == "monthly" and dt.day == 1:
return dt
elif self.reports == "weekly" and dt.weekday() == 0:
return dt
def is_past_over_limit_grace(self) -> bool:
"""Return True if this profile is over limits for 31 or more days."""
if not self.over_limit_date:
return False
return now() > self.over_limit_date + OVER_LIMIT_GRACE
def schedule_for_deletion(self) -> None:
self.deletion_scheduled_date = now() + DELETION_GRACE
self.save()
class Project(models.Model):
code = models.UUIDField(default=uuid.uuid4, unique=True)
@ -214,63 +375,120 @@ class Project(models.Model):
api_key = models.CharField(max_length=128, blank=True, db_index=True)
api_key_readonly = models.CharField(max_length=128, blank=True, db_index=True)
badge_key = models.CharField(max_length=150, unique=True)
ping_key = models.CharField(max_length=128, blank=True, null=True, unique=True)
show_slugs = models.BooleanField(default=False)
def __str__(self):
def __str__(self) -> str:
return self.name or self.owner.email
@property
def owner_profile(self):
def owner_profile(self) -> Profile:
return Profile.objects.for_user(self.owner)
def num_checks_available(self):
from hc.api.models import Check
def num_checks_available(self) -> int:
return self.owner_profile.num_checks_available()
num_used = Check.objects.filter(project__owner=self.owner).count()
return self.owner_profile.check_limit - num_used
def invite_suggestions(self) -> QuerySet[User]:
q = User.objects.filter(memberships__project__owner_id=self.owner_id)
q = q.exclude(memberships__project=self)
return q.distinct().order_by("email")
def set_api_keys(self):
self.api_key = urlsafe_b64encode(os.urandom(24)).decode()
self.api_key_readonly = urlsafe_b64encode(os.urandom(24)).decode()
self.save()
def can_invite_new_users(self) -> bool:
q = User.objects.filter(memberships__project__owner_id=self.owner_id)
used = q.distinct().count()
return used < self.owner_profile.team_limit
def can_invite(self):
return self.member_set.count() < self.owner_profile.team_limit
def invite(self, user: User, role: str) -> bool:
if Member.objects.filter(user=user, project=self).exists():
return False
def invite(self, user):
Member.objects.create(user=user, project=self)
if self.owner_id == user.id:
return False
# Switch the invited user over to the new team so they
# notice the new team on next visit:
user.profile.current_project = self
user.profile.save()
m = Member.objects.create(user=user, project=self, role=role)
checks_url = reverse("hc-checks", args=[self.code])
user.profile.send_instant_login_link(self)
if settings.EMAIL_HOST:
profile = Profile.objects.for_user(user)
profile.send_instant_login_link(membership=m, redirect_url=checks_url)
return True
def set_next_nag_date(self):
""" Set next_nag_date on profiles of all members of this project. """
def update_next_nag_dates(self) -> None:
"""Update next_nag_date on profiles of all members of this project."""
is_owner = Q(user=self.owner)
is_owner = Q(user_id=self.owner_id)
is_member = Q(user__memberships__project=self)
q = Profile.objects.filter(is_owner | is_member)
q = q.exclude(nag_period=NO_NAG)
# Exclude profiles with next_nag_date already set
q = q.filter(next_nag_date__isnull=True)
q = Profile.objects.filter(is_owner | is_member).exclude(nag_period=NO_NAG)
q.update(next_nag_date=timezone.now() + models.F("nag_period"))
for profile in q:
profile.update_next_nag_date()
def overall_status(self):
status = "up"
return None
def get_n_down(self) -> int:
result = 0
for check in self.check_set.all():
check_status = check.get_status(with_started=False)
if status == "up" and check_status == "grace":
status = "grace"
if check.get_status() == "down":
result += 1
if check_status == "down":
status = "down"
break
return status
return result
def have_channel_issues(self) -> bool:
errors = list(self.channel_set.values_list("last_error", flat=True))
# It's a problem if a project has no integrations at all
if len(errors) == 0:
return True
# It's a problem if any integration has a logged error
return True if max(errors) else False
def transfer_request(self) -> Member | None:
return self.member_set.filter(transfer_request_date__isnull=False).first()
def dashboard_url(self) -> str | None:
if not self.api_key_readonly:
return None
frag = urlencode({self.api_key_readonly: str(self)}, quote_via=quote)
return reverse("hc-dashboard") + "#" + frag
def checks_url(self) -> str:
return absolute_reverse("hc-checks", args=[self.code])
def get_absolute_url(self) -> str:
return reverse("hc-checks", args=[self.code])
class Member(models.Model):
class Role(models.TextChoices):
READONLY = "r", "Read-only"
REGULAR = "w", "Member"
MANAGER = "m", "Manager"
user = models.ForeignKey(User, models.CASCADE, related_name="memberships")
project = models.ForeignKey(Project, models.CASCADE)
transfer_request_date = models.DateTimeField(null=True, blank=True)
role = models.CharField(max_length=1, default=Role.REGULAR, choices=Role.choices)
class Meta:
constraints = [
models.UniqueConstraint(
fields=["user", "project"], name="accounts_member_no_duplicates"
)
]
def can_accept(self) -> bool:
return self.user.profile.can_accept(self.project)
@property
def is_rw(self) -> bool:
return self.role in (Member.Role.REGULAR, Member.Role.MANAGER)
class Credential(models.Model):
code = models.UUIDField(default=uuid.uuid4, unique=True)
name = models.CharField(max_length=100)
user = models.ForeignKey(User, models.CASCADE, related_name="credentials")
created = models.DateTimeField(auto_now_add=True)
data = models.BinaryField()

View file

@ -1,14 +1,11 @@
from __future__ import annotations
from hc.accounts.models import Project
from hc.test import BaseTestCase
class RemoveProjectTestCase(BaseTestCase):
def setUp(self):
super(RemoveProjectTestCase, self).setUp()
self.url = "/projects/%s/remove/" % self.project.code
def test_it_works(self):
class AddProjectTestCase(BaseTestCase):
def test_it_works(self) -> None:
self.client.login(username="alice@example.org", password="password")
r = self.client.post("/projects/add/", {"name": "My Second Project"})
@ -16,11 +13,7 @@ class RemoveProjectTestCase(BaseTestCase):
self.assertRedirects(r, "/projects/%s/checks/" % p.code)
self.assertEqual(str(p.code), p.badge_key)
# Alice's current project should be the just created one
self.profile.refresh_from_db()
self.assertEqual(self.profile.current_project, p)
def test_it_rejects_get(self):
def test_it_rejects_get(self) -> None:
self.client.login(username="alice@example.org", password="password")
r = self.client.get("/projects/add/")
self.assertEqual(r.status_code, 405)

View file

@ -0,0 +1,85 @@
from __future__ import annotations
from unittest.mock import Mock, patch
from hc.test import BaseTestCase
class AddTotpTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
self.url = "/accounts/two_factor/totp/"
def test_it_requires_sudo_mode(self) -> None:
self.client.login(username="alice@example.org", password="password")
r = self.client.get(self.url)
self.assertContains(r, "We have sent a confirmation code")
def test_it_shows_form(self) -> None:
self.client.login(username="alice@example.org", password="password")
self.set_sudo_flag()
r = self.client.get(self.url)
self.assertContains(r, "Enter the six-digit code")
# It should put a "totp_secret" key in the session:
self.assertIn("totp_secret", self.client.session)
@patch("hc.accounts.views.pyotp.totp.TOTP")
def test_it_adds_totp(self, mock_TOTP: Mock) -> None:
mock_TOTP.return_value.verify.return_value = True
self.client.login(username="alice@example.org", password="password")
self.set_sudo_flag()
payload = {"code": "000000"}
r = self.client.post(self.url, payload, follow=True)
self.assertRedirects(r, "/accounts/profile/")
self.assertContains(r, "Successfully set up the Authenticator app")
# totp_secret should be gone from the session:
self.assertNotIn("totp_secret", self.client.session)
self.profile.refresh_from_db()
self.assertTrue(self.profile.totp)
self.assertTrue(self.profile.totp_created)
@patch("hc.accounts.views.pyotp.totp.TOTP")
def test_it_handles_wrong_code(self, mock_TOTP: Mock) -> None:
mock_TOTP.return_value.verify.return_value = False
mock_TOTP.return_value.provisioning_uri.return_value = "test-uri"
self.client.login(username="alice@example.org", password="password")
self.set_sudo_flag()
payload = {"code": "000000"}
r = self.client.post(self.url, payload, follow=True)
self.assertContains(r, "The code you entered was incorrect.")
self.profile.refresh_from_db()
self.assertIsNone(self.profile.totp)
self.assertIsNone(self.profile.totp_created)
def test_it_checks_if_totp_already_configured(self) -> None:
self.profile.totp = "0" * 32
self.profile.save()
self.client.login(username="alice@example.org", password="password")
self.set_sudo_flag()
r = self.client.get(self.url)
self.assertEqual(r.status_code, 400)
@patch("hc.accounts.views.pyotp.totp.TOTP")
def test_it_handles_non_numeric_code(self, mock_TOTP: Mock) -> None:
mock_TOTP.return_value.verify.return_value = False
mock_TOTP.return_value.provisioning_uri.return_value = "test-uri"
self.client.login(username="alice@example.org", password="password")
self.set_sudo_flag()
payload = {"code": "AAAAAA"}
r = self.client.post(self.url, payload, follow=True)
self.assertContains(r, "Enter a valid value")

View file

@ -0,0 +1,94 @@
from __future__ import annotations
from unittest.mock import Mock, patch
from django.test.utils import override_settings
from hc.accounts.models import Credential
from hc.test import BaseTestCase
@override_settings(RP_ID="testserver")
class AddWebauthnTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
self.url = "/accounts/two_factor/webauthn/"
def test_it_requires_sudo_mode(self) -> None:
self.client.login(username="alice@example.org", password="password")
r = self.client.get(self.url)
self.assertContains(r, "We have sent a confirmation code")
@override_settings(RP_ID=None)
def test_it_requires_rp_id(self) -> None:
self.client.login(username="alice@example.org", password="password")
self.set_sudo_flag()
r = self.client.get(self.url)
self.assertEqual(r.status_code, 404)
def test_it_shows_form(self) -> None:
self.client.login(username="alice@example.org", password="password")
self.set_sudo_flag()
r = self.client.get(self.url)
self.assertContains(r, "Add Security Key")
# It should put a "state" key in the session:
self.assertIn("state", self.client.session)
@patch("hc.accounts.views.CreateHelper.verify")
def test_it_adds_credential(self, mock_verify: Mock) -> None:
mock_verify.return_value = b"dummy-credential-data"
self.client.login(username="alice@example.org", password="password")
self.set_sudo_flag()
session = self.client.session
session["state"] = "dummy state"
session.save()
payload = {"name": "My New Key", "response": "dummy response"}
r = self.client.post(self.url, payload, follow=True)
self.assertRedirects(r, "/accounts/profile/")
self.assertContains(r, "Added security key <strong>My New Key</strong>")
c = Credential.objects.get()
self.assertEqual(c.name, "My New Key")
# state should have been removed from the session
self.assertNotIn("state", self.client.session)
def test_it_handles_bad_response_json(self) -> None:
self.client.login(username="alice@example.org", password="password")
self.set_sudo_flag()
session = self.client.session
session["state"] = "dummy state"
session.save()
payload = {"name": "My New Key", "response": "this is not json"}
r = self.client.post(self.url, payload)
self.assertEqual(r.status_code, 400)
@patch("hc.accounts.views.logger")
@patch("hc.accounts.views.CreateHelper.verify")
def test_it_handles_verification_failure(self, verify: Mock, logger: Mock) -> None:
verify.side_effect = ValueError
self.client.login(username="alice@example.org", password="password")
self.set_sudo_flag()
session = self.client.session
session["state"] = "dummy state"
session.save()
payload = {"name": "My New Key", "response": "dummy response"}
r = self.client.post(self.url, payload, follow=True)
self.assertEqual(r.status_code, 400)
# It should log the verification failure
self.assertTrue(logger.exception.called)

View file

@ -1,17 +1,57 @@
from __future__ import annotations
from hc.payments.models import Subscription
from hc.test import BaseTestCase
class AccountsAdminTestCase(BaseTestCase):
def setUp(self):
super(AccountsAdminTestCase, self).setUp()
def setUp(self) -> None:
super().setUp()
self.alice.is_staff = True
self.alice.is_superuser = True
self.alice.save()
def test_it_shows_profiles(self):
def test_it_shows_profiles(self) -> None:
self.client.login(username="alice@example.org", password="password")
r = self.client.get("/admin/accounts/profile/")
self.assertContains(r, "alice@example.org")
self.assertContains(r, "bob@example.org")
def test_it_escapes_emails_when_showing_profiles(self) -> None:
self.bob.email = "bob&friends@example.org"
self.bob.save()
self.client.login(username="alice@example.org", password="password")
r = self.client.get("/admin/accounts/profile/")
# The amperstand should be escaped
self.assertNotContains(r, "bob&friends@example.org")
def test_it_escapes_emails_when_showing_profiles_with_subscriptions(self) -> None:
self.bob.email = "bob&friends@example.org"
self.bob.save()
self.sub = Subscription(user=self.bob)
self.sub.plan_name = "Custom Plan"
self.sub.save()
self.client.login(username="alice@example.org", password="password")
r = self.client.get("/admin/accounts/profile/")
# The amperstand should be escaped
self.assertNotContains(r, "bob&friends@example.org")
self.assertContains(r, "<span>Custom Plan</span>")
def test_it_shows_projects(self) -> None:
self.client.login(username="alice@example.org", password="password")
r = self.client.get("/admin/accounts/project/")
self.assertContains(r, "Alices Project")
self.assertContains(r, "Default Project for bob@example.org")
def test_it_escapes_emails_when_showing_projects(self) -> None:
self.bob.email = "bob&friends@example.org"
self.bob.save()
self.client.login(username="alice@example.org", password="password")
r = self.client.get("/admin/accounts/project/")
# The amperstand should be escaped
self.assertNotContains(r, "bob&friends@example.org")

View file

@ -1,39 +1,75 @@
from django.contrib.auth.hashers import make_password
from __future__ import annotations
from django.conf import settings
from django.core import mail
from django.core.mail import EmailMessage, EmailMultiAlternatives
from django.test.utils import override_settings
from hc.test import BaseTestCase
class ChangeEmailTestCase(BaseTestCase):
def test_it_shows_form(self):
self.profile.token = make_password("foo", "change-email")
self.profile.save()
def get_html(self, email: EmailMessage) -> str:
assert isinstance(email, EmailMultiAlternatives)
html, _ = email.alternatives[0]
assert isinstance(html, str)
return html
def test_it_requires_sudo_mode(self) -> None:
self.client.login(username="alice@example.org", password="password")
r = self.client.get("/accounts/change_email/foo/")
r = self.client.get("/accounts/change_email/")
self.assertContains(r, "We have sent a confirmation code")
def test_it_shows_form(self) -> None:
self.client.login(username="alice@example.org", password="password")
self.set_sudo_flag()
r = self.client.get("/accounts/change_email/")
self.assertContains(r, "Change Account's Email Address")
def test_it_changes_password(self):
self.profile.token = make_password("foo", "change-email")
self.profile.save()
@override_settings(SITE_ROOT="http://testserver", SESSION_COOKIE_SECURE=False)
def test_it_sends_link(self) -> None:
self.client.login(username="alice@example.org", password="password")
self.set_sudo_flag()
payload = {"email": "alice2@example.org"}
self.client.post("/accounts/change_email/foo/", payload)
r = self.client.post("/accounts/change_email/", payload, follow=True)
self.assertRedirects(r, "/accounts/change_email/")
self.assertContains(r, "One Last Step")
self.assertEqual(self.client.cookies["auto-login"].value, "1")
self.assertEqual(self.client.cookies["auto-login"]["samesite"], "Lax")
self.assertTrue(self.client.cookies["auto-login"]["httponly"])
self.assertFalse(self.client.cookies["auto-login"]["secure"])
# The email address should have not changed yet
self.alice.refresh_from_db()
self.assertEqual(self.alice.email, "alice2@example.org")
self.assertFalse(self.alice.has_usable_password())
self.assertEqual(self.alice.email, "alice@example.org")
self.assertTrue(self.alice.has_usable_password())
def test_it_requires_unique_email(self):
self.profile.token = make_password("foo", "change-email")
self.profile.save()
# And email should have been sent
self.assertEqual(len(mail.outbox), 1)
message = mail.outbox[0]
self.assertEqual(message.subject, f"Log in to {settings.SITE_NAME}")
html = self.get_html(message)
self.assertIn("http://testserver/accounts/change_email/", html)
@override_settings(SESSION_COOKIE_SECURE=True)
def test_it_sets_secure_autologin_cookie(self) -> None:
self.client.login(username="alice@example.org", password="password")
self.set_sudo_flag()
payload = {"email": "alice2@example.org"}
r = self.client.post("/accounts/change_email/", payload)
self.assertTrue(r.cookies["auto-login"]["secure"])
def test_it_requires_unique_email(self) -> None:
self.client.login(username="alice@example.org", password="password")
self.set_sudo_flag()
payload = {"email": "bob@example.org"}
r = self.client.post("/accounts/change_email/foo/", payload)
r = self.client.post("/accounts/change_email/", payload)
self.assertContains(r, "bob@example.org is already registered")
self.alice.refresh_from_db()

View file

@ -0,0 +1,80 @@
from __future__ import annotations
from unittest.mock import patch
from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import User
from django.core.signing import TimestampSigner
from hc.test import BaseTestCase
class ChangeEmailVerifyTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
self.profile.token = make_password("secret-token", "login")
self.profile.save()
self.checks_url = f"/projects/{self.project.code}/checks/"
def _url(self, expired: bool = False) -> str:
payload = {
"u": self.alice.username,
"t": TimestampSigner().sign("secret-token"),
"e": "alice+new@example.org",
}
if expired:
with patch("django.core.signing.TimestampSigner.timestamp") as mock_ts:
mock_ts.return_value = "1kHR5c"
signed = TimestampSigner().sign_object(payload)
else:
signed = TimestampSigner().sign_object(payload)
return f"/accounts/change_email/{signed}/"
def test_it_works(self) -> None:
r = self.client.post(self._url())
self.assertRedirects(r, self.checks_url)
# Alice's email should have been updated, and password cleared
self.alice.refresh_from_db()
self.assertEqual(self.alice.email, "alice+new@example.org")
self.assertFalse(self.alice.has_usable_password())
# After login, token should be blank
self.profile.refresh_from_db()
self.assertEqual(self.profile.token, "")
def test_it_handles_get(self) -> None:
r = self.client.get(self._url())
self.assertContains(r, "You are about to log into")
# Alice's email should have *not* been changed yet
self.alice.refresh_from_db()
self.assertEqual(self.alice.email, "alice@example.org")
def test_it_handles_get_with_cookie(self) -> None:
self.client.cookies["auto-login"] = "1"
r = self.client.get(self._url())
self.assertRedirects(r, self.checks_url)
def test_it_handles_expired_link(self) -> None:
r = self.client.post(self._url(expired=True))
self.assertContains(r, "The link you just used is incorrect.")
def test_it_handles_bad_payload(self) -> None:
r = self.client.post("/accounts/change_email/bad-payload/")
self.assertContains(r, "The link you just used is incorrect.")
def test_it_handles_unavailable_email(self) -> None:
# Make the target address unavailable
User.objects.create(email="alice+new@example.org")
r = self.client.post(self._url(), follow=True)
self.assertRedirects(r, "/accounts/login/")
self.assertContains(r, "incorrect or expired")
# Alice's email should have *not* been updated
self.alice.refresh_from_db()
self.assertEqual(self.alice.email, "alice@example.org")

View file

@ -1,21 +1,28 @@
from __future__ import annotations
from django.contrib.auth.hashers import make_password
from django.core.signing import TimestampSigner
from hc.accounts.models import Credential
from hc.test import BaseTestCase
class CheckTokenTestCase(BaseTestCase):
def setUp(self):
super(CheckTokenTestCase, self).setUp()
def setUp(self) -> None:
super().setUp()
self.profile.token = make_password("secret-token", "login")
self.profile.save()
signed_token = TimestampSigner().sign("secret-token")
self.url = f"/accounts/check_token/alice/{signed_token}/"
self.checks_url = "/projects/%s/checks/" % self.project.code
def test_it_shows_form(self):
r = self.client.get("/accounts/check_token/alice/secret-token/")
def test_it_shows_form(self) -> None:
r = self.client.get(self.url)
self.assertContains(r, "You are about to log in")
def test_it_redirects(self):
r = self.client.post("/accounts/check_token/alice/secret-token/")
def test_it_redirects(self) -> None:
r = self.client.post(self.url)
self.assertRedirects(r, self.checks_url)
@ -23,28 +30,58 @@ class CheckTokenTestCase(BaseTestCase):
self.profile.refresh_from_db()
self.assertEqual(self.profile.token, "")
def test_it_redirects_already_logged_in(self):
def test_it_handles_email_in_username(self) -> None:
# Healthchecks will generate usernames that look like UUIDs. But custom
# authentication backends like django-auth-ldap can also create User objects
# with non-UUID usernames. In this testcase we check if check_token works
# with an username that looks like an email address.
self.alice.username = "alice@example.org"
self.alice.save()
r = self.client.post(self.url.replace("alice", "alice@example.org"))
self.assertRedirects(r, self.checks_url)
def test_it_handles_get_with_cookie(self) -> None:
self.client.cookies["auto-login"] = "1"
r = self.client.get(self.url)
self.assertRedirects(r, self.checks_url)
def test_it_redirects_already_logged_in(self) -> None:
# Login
self.client.login(username="alice@example.org", password="password")
# Login again, when already authenticated
r = self.client.post("/accounts/check_token/alice/secret-token/")
r = self.client.post(self.url)
self.assertRedirects(r, self.checks_url)
def test_it_redirects_bad_login(self):
def test_it_redirects_bad_login(self) -> None:
# Login with a bad token
url = "/accounts/check_token/alice/invalid-token/"
r = self.client.post(url, follow=True)
self.assertRedirects(r, "/accounts/login/")
self.assertContains(r, "incorrect or expired")
def test_it_handles_next_parameter(self):
url = "/accounts/check_token/alice/secret-token/?next=/integrations/add_slack/"
def test_it_handles_next_parameter(self) -> None:
url = self.url + "?next=" + self.channels_url
r = self.client.post(url)
self.assertRedirects(r, "/integrations/add_slack/")
self.assertRedirects(r, self.channels_url)
def test_it_ignores_bad_next_parameter(self):
url = "/accounts/check_token/alice/secret-token/?next=/evil/"
def test_it_ignores_bad_next_parameter(self) -> None:
url = self.url + "?next=/evil/"
r = self.client.post(url)
self.assertRedirects(r, self.checks_url)
def test_it_redirects_to_webauthn_form(self) -> None:
Credential.objects.create(user=self.alice, name="Alices Key")
r = self.client.post(self.url)
self.assertRedirects(
r, "/accounts/login/two_factor/", fetch_redirect_response=False
)
# It should not log the user in yet
self.assertNotIn("_auth_user_id", self.client.session)
# Instead, it should set 2fa_user_id in the session
user_id, email, valid_until = self.client.session["2fa_user"]
self.assertEqual(user_id, self.alice.id)

View file

@ -1,46 +1,72 @@
from __future__ import annotations
from django.contrib.auth.models import User
from hc.api.models import Check
from hc.payments.models import Subscription
from hc.test import BaseTestCase
from mock import patch
class CloseAccountTestCase(BaseTestCase):
@patch("hc.payments.models.braintree")
def test_it_works(self, mock_braintree):
def test_it_requires_sudo_mode(self) -> None:
self.client.login(username="alice@example.org", password="password")
r = self.client.get("/accounts/close/")
self.assertContains(r, "We have sent a confirmation code")
def test_it_shows_confirmation_form(self) -> None:
self.client.login(username="alice@example.org", password="password")
self.set_sudo_flag()
r = self.client.get("/accounts/close/")
self.assertContains(r, "Close Account?")
self.assertContains(r, "1 project")
self.assertContains(r, "0 checks")
def test_it_works(self) -> None:
Check.objects.create(project=self.project, tags="foo a-B_1 baz@")
Subscription.objects.create(
user=self.alice, subscription_id="123", customer_id="fake-customer-id"
)
self.client.login(username="alice@example.org", password="password")
r = self.client.post("/accounts/close/")
self.assertEqual(r.status_code, 302)
self.set_sudo_flag()
payload = {"confirmation": "alice@example.org"}
r = self.client.post("/accounts/close/", payload, follow=True)
self.assertRedirects(r, "/accounts/login/")
self.assertContains(r, "Account closed.")
# Alice should be gone
alices = User.objects.filter(username="alice")
self.assertFalse(alices.exists())
# Bob's current team should now be None
self.bobs_profile.refresh_from_db()
self.assertIsNone(self.bobs_profile.current_project)
# Check should be gone
self.assertFalse(Check.objects.exists())
# Subscription should have been canceled
self.assertTrue(mock_braintree.Subscription.cancel.called)
# Braintree customer should have been deleted
self.assertTrue(mock_braintree.Customer.delete.called)
# Subscription should be gone
self.assertFalse(Subscription.objects.exists())
def test_partner_removal_works(self):
def test_it_requires_confirmation(self) -> None:
self.client.login(username="alice@example.org", password="password")
self.set_sudo_flag()
payload = {"confirmation": "incorrect"}
r = self.client.post("/accounts/close/", payload)
self.assertContains(r, "Close Account?")
self.assertContains(r, "has-error")
# Alice should be still present
self.alice.refresh_from_db()
self.profile.refresh_from_db()
def test_partner_removal_works(self) -> None:
self.client.login(username="bob@example.org", password="password")
r = self.client.post("/accounts/close/")
self.assertEqual(r.status_code, 302)
self.set_sudo_flag()
payload = {"confirmation": "bob@example.org"}
r = self.client.post("/accounts/close/", payload)
self.assertRedirects(r, "/accounts/login/")
# Alice should be still present
self.alice.refresh_from_db()
@ -49,8 +75,3 @@ class CloseAccountTestCase(BaseTestCase):
# Bob should be gone
bobs = User.objects.filter(username="bob")
self.assertFalse(bobs.exists())
def test_it_rejects_get(self):
self.client.login(username="bob@example.org", password="password")
r = self.client.get("/accounts/close/")
self.assertEqual(r.status_code, 405)

View file

@ -0,0 +1,32 @@
from __future__ import annotations
from unittest.mock import Mock, patch
from django.contrib.auth.models import User
from hc.accounts.management.commands.createsuperuser import Command
from hc.test import BaseTestCase
class CreateSuperuserTestCase(BaseTestCase):
def test_it_works(self) -> None:
cmd = Command(stdout=Mock())
with patch(cmd.__module__ + ".input") as mock_input:
with patch(cmd.__module__ + ".getpass") as mock_getpass:
mock_input.return_value = "superuser@example.org"
mock_getpass.return_value = "hunter2"
cmd.handle()
u = User.objects.get(email="superuser@example.org")
self.assertTrue(u.is_superuser)
def test_it_rejects_duplicate_email(self) -> None:
cmd = Command(stdout=Mock(), stderr=Mock())
with patch(cmd.__module__ + ".input") as mock_input:
with patch(cmd.__module__ + ".getpass") as mock_getpass:
mock_input.side_effect = ["alice@example.org", "alice2@example.org"]
mock_getpass.return_value = "hunter2"
cmd.handle()
u = User.objects.get(email="alice2@example.org")
self.assertTrue(u.is_superuser)

View file

@ -1,39 +1,101 @@
from __future__ import annotations
from django.conf import settings
from django.core import mail
from django.core.mail import EmailMessage, EmailMultiAlternatives
from django.test.utils import override_settings
from hc.accounts.models import Credential
from hc.api.models import Check, TokenBucket
from hc.test import BaseTestCase
class LoginTestCase(BaseTestCase):
def setUp(self):
super(LoginTestCase, self).setUp()
self.checks_url = "/projects/%s/checks/" % self.project.code
def setUp(self) -> None:
super().setUp()
self.checks_url = f"/projects/{self.project.code}/checks/"
def test_it_sends_link(self):
def get_html(self, email: EmailMessage) -> str:
assert isinstance(email, EmailMultiAlternatives)
html, _ = email.alternatives[0]
assert isinstance(html, str)
return html
def test_it_shows_form(self) -> None:
r = self.client.get("/accounts/login/")
self.assertContains(r, "magic-link-form")
# It should not show validation errors yet
self.assertNotContains(r, "This field is required")
@override_settings(EMAIL_HOST=None)
def test_it_handles_no_smtp(self) -> None:
r = self.client.get("/accounts/login/")
self.assertNotContains(r, "magic-link-form")
def test_it_redirects_authenticated_get(self) -> None:
self.client.login(username="alice@example.org", password="password")
r = self.client.get("/accounts/login/")
self.assertRedirects(r, self.checks_url)
@override_settings(
SITE_ROOT="http://testserver", SITE_LOGO_URL=None, SESSION_COOKIE_SECURE=False
)
def test_it_sends_link(self) -> None:
form = {"identity": "alice@example.org"}
r = self.client.post("/accounts/login/", form)
self.assertRedirects(r, "/accounts/login_link_sent/")
self.assertEqual(r.cookies["auto-login"].value, "1")
self.assertTrue(r.cookies["auto-login"]["httponly"])
self.assertEqual(r.cookies["auto-login"]["samesite"], "Lax")
self.assertFalse(r.cookies["auto-login"]["secure"])
# And email should have been sent
self.assertEqual(len(mail.outbox), 1)
subject = "Log in to %s" % settings.SITE_NAME
self.assertEqual(mail.outbox[0].subject, subject)
message = mail.outbox[0]
self.assertEqual(message.subject, f"Log in to {settings.SITE_NAME}")
html = self.get_html(message)
self.assertIn("http://testserver/static/img/logo.png", html)
self.assertIn("http://testserver/docs/", html)
def test_it_sends_link_with_next(self):
@override_settings(SESSION_COOKIE_SECURE=True)
def test_it_sets_secure_autologin_cookie(self) -> None:
form = {"identity": "alice@example.org"}
r = self.client.post("/accounts/login/", form)
self.assertTrue(r.cookies["auto-login"]["secure"])
@override_settings(SITE_LOGO_URL="https://example.org/logo.svg")
def test_it_uses_custom_logo(self) -> None:
self.client.post("/accounts/login/", {"identity": "alice@example.org"})
html = self.get_html(mail.outbox[0])
self.assertIn("https://example.org/logo.svg", html)
def test_it_sends_link_with_next(self) -> None:
form = {"identity": "alice@example.org"}
r = self.client.post("/accounts/login/?next=/integrations/add_slack/", form)
r = self.client.post("/accounts/login/?next=" + self.channels_url, form)
self.assertRedirects(r, "/accounts/login_link_sent/")
# The check_token link should have a ?next= query parameter:
self.assertEqual(len(mail.outbox), 1)
body = mail.outbox[0].body
self.assertTrue("/?next=/integrations/add_slack/" in body)
self.assertTrue("/?next=" + self.channels_url in body)
def test_it_handles_unknown_email(self) -> None:
form = {"identity": "surprise@example.org"}
r = self.client.post("/accounts/login/", form)
self.assertRedirects(r, "/accounts/login_link_sent/")
# It should send the same response and cookies as in normal login
self.assertEqual(r.cookies["auto-login"].value, "1")
# There should be no sent emails.
self.assertEqual(len(mail.outbox), 0)
@override_settings(SECRET_KEY="test-secret")
def test_it_rate_limits_emails(self):
def test_it_rate_limits_emails(self) -> None:
# "d60d..." is sha1("alice@example.orgtest-secret")
obj = TokenBucket(value="em-d60db3b2343e713a4de3e92d4eb417e4f05f06ab")
obj.tokens = 0
@ -47,28 +109,54 @@ class LoginTestCase(BaseTestCase):
# No email should have been sent
self.assertEqual(len(mail.outbox), 0)
def test_it_pops_bad_link_from_session(self):
def test_it_rate_limits_client_ips(self) -> None:
obj = TokenBucket(value="auth-ip-127.0.0.1")
obj.tokens = 0
obj.save()
form = {"identity": "alice@example.org"}
r = self.client.post("/accounts/login/", form)
self.assertContains(r, "Too many attempts")
# No email should have been sent
self.assertEqual(len(mail.outbox), 0)
def test_rate_limiter_uses_x_forwarded_for(self) -> None:
obj = TokenBucket(value="auth-ip-127.0.0.2")
obj.tokens = 0
obj.save()
form = {"identity": "alice@example.org"}
xff = "127.0.0.2:1234,127.0.0.3"
r = self.client.post("/accounts/login/", form, HTTP_X_FORWARDED_FOR=xff)
self.assertContains(r, "Too many attempts")
# No email should have been sent
self.assertEqual(len(mail.outbox), 0)
def test_it_pops_bad_link_from_session(self) -> None:
self.client.session["bad_link"] = True
self.client.get("/accounts/login/")
assert "bad_link" not in self.client.session
def test_it_ignores_case(self):
def test_it_ignores_case(self) -> None:
form = {"identity": "ALICE@EXAMPLE.ORG"}
r = self.client.post("/accounts/login/", form)
self.assertRedirects(r, "/accounts/login_link_sent/")
self.profile.refresh_from_db()
self.assertIn("login", self.profile.token)
self.assertTrue(self.profile.token)
def test_it_handles_password(self):
def test_it_handles_password(self) -> None:
form = {"action": "login", "email": "alice@example.org", "password": "password"}
r = self.client.post("/accounts/login/", form)
self.assertRedirects(r, self.checks_url)
@override_settings(SECRET_KEY="test-secret")
def test_it_rate_limits_password_attempts(self):
def test_it_rate_limits_password_attempts(self) -> None:
# "d60d..." is sha1("alice@example.orgtest-secret")
obj = TokenBucket(value="pw-d60db3b2343e713a4de3e92d4eb417e4f05f06ab")
obj.tokens = 0
@ -79,24 +167,30 @@ class LoginTestCase(BaseTestCase):
r = self.client.post("/accounts/login/", form)
self.assertContains(r, "Too many attempts")
def test_it_handles_password_login_with_redirect(self):
def test_it_handles_password_login_with_redirect(self) -> None:
check = Check.objects.create(project=self.project)
form = {"action": "login", "email": "alice@example.org", "password": "password"}
samples = ["/integrations/add_slack/", "/checks/%s/details/" % check.code]
samples = [self.channels_url, "/checks/%s/details/" % check.code]
for s in samples:
r = self.client.post("/accounts/login/?next=%s" % s, form)
self.assertRedirects(r, s)
def test_it_handles_bad_next_parameter(self):
def test_it_handles_bad_next_parameter(self) -> None:
form = {"action": "login", "email": "alice@example.org", "password": "password"}
r = self.client.post("/accounts/login/?next=/evil/", form)
self.assertRedirects(r, self.checks_url)
samples = [
"/evil/",
f"https://example.org/projects/{self.project.code}/checks/",
]
def test_it_handles_wrong_password(self):
for sample in samples:
r = self.client.post("/accounts/login/?next=" + sample, form)
self.assertRedirects(r, self.checks_url)
def test_it_handles_wrong_password(self) -> None:
form = {
"action": "login",
"email": "alice@example.org",
@ -107,6 +201,47 @@ class LoginTestCase(BaseTestCase):
self.assertContains(r, "Incorrect email or password")
@override_settings(REGISTRATION_OPEN=False)
def test_it_obeys_registration_open(self):
def test_it_obeys_registration_open(self) -> None:
r = self.client.get("/accounts/login/")
self.assertNotContains(r, "Create Your Account")
def test_it_redirects_to_webauthn_form(self) -> None:
Credential.objects.create(user=self.alice, name="Alices Key")
form = {"action": "login", "email": "alice@example.org", "password": "password"}
r = self.client.post("/accounts/login/", form)
self.assertRedirects(
r, "/accounts/login/two_factor/", fetch_redirect_response=False
)
# It should not log the user in yet
self.assertNotIn("_auth_user_id", self.client.session)
# Instead, it should set 2fa_user_id in the session
user_id, email, valid_until = self.client.session["2fa_user"]
self.assertEqual(user_id, self.alice.id)
def test_it_redirects_to_totp_form(self) -> None:
self.profile.totp = "0" * 32
self.profile.save()
form = {"action": "login", "email": "alice@example.org", "password": "password"}
r = self.client.post("/accounts/login/", form)
self.assertRedirects(
r, "/accounts/login/two_factor/totp/", fetch_redirect_response=False
)
# It should not log the user in yet
self.assertNotIn("_auth_user_id", self.client.session)
# Instead, it should set 2fa_user_id in the session
user_id, email, valid_until = self.client.session["2fa_user"]
self.assertEqual(user_id, self.alice.id)
def test_it_handles_missing_profile(self) -> None:
self.profile.delete()
form = {"action": "login", "email": "alice@example.org", "password": "password"}
r = self.client.post("/accounts/login/", form)
self.assertRedirects(r, self.checks_url)

View file

@ -0,0 +1,99 @@
from __future__ import annotations
import time
from unittest.mock import Mock, patch
from hc.api.models import TokenBucket
from hc.test import BaseTestCase
class LoginTotpTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
# This is the user we're trying to authenticate
session = self.client.session
session["2fa_user"] = [self.alice.id, self.alice.email, (time.time()) + 300]
session.save()
self.profile.totp = "0" * 32
self.profile.save()
self.url = "/accounts/login/two_factor/totp/"
self.checks_url = f"/projects/{self.project.code}/checks/"
def test_it_shows_form(self) -> None:
r = self.client.get(self.url)
self.assertContains(r, "Please enter the six-digit code")
def test_it_requires_unauthenticated_user(self) -> None:
self.client.login(username="alice@example.org", password="password")
r = self.client.get(self.url)
self.assertEqual(r.status_code, 400)
def test_it_requires_totp_secret(self) -> None:
self.profile.totp = None
self.profile.save()
r = self.client.get(self.url)
self.assertEqual(r.status_code, 400)
def test_it_rejects_changed_email(self) -> None:
session = self.client.session
session["2fa_user"] = [self.alice.id, "eve@example.org", int(time.time())]
session.save()
r = self.client.get(self.url)
self.assertEqual(r.status_code, 400)
def test_it_rejects_old_timestamp(self) -> None:
session = self.client.session
session["2fa_user"] = [self.alice.id, self.alice.email, int(time.time()) - 310]
session.save()
r = self.client.get(self.url)
self.assertRedirects(r, "/accounts/login/")
@patch("hc.accounts.views.pyotp.totp.TOTP")
def test_it_logs_in(self, mock_TOTP: Mock) -> None:
mock_TOTP.return_value.verify.return_value = True
r = self.client.post(self.url, {"code": "000000"})
self.assertRedirects(r, self.checks_url)
self.assertNotIn("2fa_user_id", self.client.session)
@patch("hc.accounts.views.pyotp.totp.TOTP")
def test_it_redirects_after_login(self, mock_TOTP: Mock) -> None:
mock_TOTP.return_value.verify.return_value = True
url = self.url + "?next=" + self.channels_url
r = self.client.post(url, {"code": "000000"})
self.assertRedirects(r, self.channels_url)
@patch("hc.accounts.views.pyotp.totp.TOTP")
def test_it_handles_authentication_failure(self, mock_TOTP: Mock) -> None:
mock_TOTP.return_value.verify.return_value = False
r = self.client.post(self.url, {"code": "000000"})
self.assertContains(r, "The code you entered was incorrect.")
def test_it_uses_rate_limiting(self) -> None:
obj = TokenBucket(value=f"totp-{self.alice.id}")
obj.tokens = 0
obj.save()
r = self.client.post(self.url, {"code": "000000"})
self.assertContains(r, "Too Many Requests")
@patch("hc.accounts.views.pyotp.totp.TOTP")
def test_it_rejects_used_code(self, mock_TOTP: Mock) -> None:
mock_TOTP.return_value.verify.return_value = True
obj = TokenBucket(value=f"totpc-{self.alice.id}-000000")
obj.tokens = 0
obj.save()
r = self.client.post(self.url, {"code": "000000"})
self.assertContains(r, "Too Many Requests")

View file

@ -0,0 +1,117 @@
from __future__ import annotations
import time
from unittest.mock import Mock, patch
from django.test.utils import override_settings
from hc.test import BaseTestCase
@override_settings(RP_ID="testserver")
class LoginWebAuthnTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
# This is the user we're trying to authenticate
session = self.client.session
session["2fa_user"] = [self.alice.id, self.alice.email, (time.time()) + 300]
session.save()
self.url = "/accounts/login/two_factor/"
self.checks_url = f"/projects/{self.project.code}/checks/"
def test_it_shows_form(self) -> None:
r = self.client.get(self.url)
self.assertContains(r, "Waiting for security key")
self.assertNotContains(r, "Use authenticator app")
# It should put a "state" key in the session:
self.assertIn("state", self.client.session)
def test_it_shows_totp_option(self) -> None:
self.profile.totp = "0" * 32
self.profile.save()
r = self.client.get(self.url)
self.assertContains(r, "Use authenticator app")
def test_it_preserves_next_parameter_in_totp_url(self) -> None:
self.profile.totp = "0" * 32
self.profile.save()
url = self.url + "?next=" + self.channels_url
r = self.client.get(url)
self.assertContains(r, "/login/two_factor/totp/?next=" + self.channels_url)
def test_it_requires_unauthenticated_user(self) -> None:
self.client.login(username="alice@example.org", password="password")
r = self.client.get(self.url)
self.assertEqual(r.status_code, 400)
def test_it_rejects_changed_email(self) -> None:
session = self.client.session
session["2fa_user"] = [self.alice.id, "eve@example.org", int(time.time())]
session.save()
r = self.client.get(self.url)
self.assertEqual(r.status_code, 400)
def test_it_rejects_old_timestamp(self) -> None:
session = self.client.session
session["2fa_user"] = [self.alice.id, self.alice.email, int(time.time()) - 310]
session.save()
r = self.client.get(self.url)
self.assertRedirects(r, "/accounts/login/")
@override_settings(RP_ID=None)
def test_it_requires_rp_id(self) -> None:
r = self.client.get(self.url)
self.assertEqual(r.status_code, 404)
@patch("hc.accounts.views.GetHelper.verify")
def test_it_logs_in(self, mock_verify: Mock) -> None:
mock_verify.return_value = True
session = self.client.session
session["state"] = "dummy-state"
session.save()
r = self.client.post(self.url, {"response": "dummy response"})
self.assertRedirects(r, self.checks_url)
self.assertNotIn("state", self.client.session)
self.assertNotIn("2fa_user_id", self.client.session)
@patch("hc.accounts.views.GetHelper.verify")
def test_it_redirects_after_login(self, mock_verify: Mock) -> None:
mock_verify.return_value = True
session = self.client.session
session["state"] = "dummy-state"
session.save()
url = self.url + "?next=" + self.channels_url
r = self.client.post(url, {"response": "dummy response"})
self.assertRedirects(r, self.channels_url)
def test_it_handles_bad_json(self) -> None:
session = self.client.session
session["state"] = "dummy-state"
session.save()
r = self.client.post(self.url, {"response": "this is not json"})
self.assertEqual(r.status_code, 400)
@patch("hc.accounts.views.GetHelper.verify")
def test_it_handles_authentication_failure(self, mock_verify: Mock) -> None:
mock_verify.return_value = False
session = self.client.session
session["state"] = "dummy-state"
session.save()
r = self.client.post(self.url, {"response": "this is not json"})
self.assertEqual(r.status_code, 400)

View file

@ -1,59 +1,118 @@
from __future__ import annotations
from datetime import timedelta as td
from django.utils.timezone import now
from hc.api.models import Check
from hc.test import BaseTestCase
class NotificationsTestCase(BaseTestCase):
def test_it_saves_reports_allowed_true(self):
self.profile.reports_allowed = False
url = "/accounts/profile/notifications/"
def _payload(self, **kwargs: str) -> dict[str, str]:
result = {"reports": "monthly", "nag_period": "0", "tz": "Europe/Riga"}
result.update(kwargs)
return result
def test_it_saves_reports_monthly(self) -> None:
self.profile.reports = "off"
self.profile.save()
self.client.login(username="alice@example.org", password="password")
form = {"reports_allowed": "on", "nag_period": "0"}
r = self.client.post("/accounts/profile/notifications/", form)
r = self.client.post(self.url, self._payload())
self.assertEqual(r.status_code, 200)
self.profile.refresh_from_db()
self.assertTrue(self.profile.reports_allowed)
self.assertIsNotNone(self.profile.next_report_date)
self.assertEqual(self.profile.reports, "monthly")
assert self.profile.next_report_date
self.assertEqual(self.profile.next_report_date.day, 1)
def test_it_saves_reports_allowed_false(self):
self.profile.reports_allowed = True
def test_it_saves_reports_weekly(self) -> None:
self.profile.reports = "off"
self.profile.save()
self.client.login(username="alice@example.org", password="password")
r = self.client.post(self.url, self._payload(reports="weekly"))
self.assertEqual(r.status_code, 200)
self.profile.refresh_from_db()
self.assertEqual(self.profile.reports, "weekly")
assert self.profile.next_report_date
self.assertEqual(self.profile.next_report_date.weekday(), 0)
def test_it_saves_reports_off(self) -> None:
self.profile.reports = "monthly"
self.profile.next_report_date = now()
self.profile.save()
self.client.login(username="alice@example.org", password="password")
form = {"nag_period": "0"}
r = self.client.post("/accounts/profile/notifications/", form)
r = self.client.post(self.url, self._payload(reports="off"))
self.assertEqual(r.status_code, 200)
self.profile.refresh_from_db()
self.assertFalse(self.profile.reports_allowed)
self.assertEqual(self.profile.reports, "off")
self.assertIsNone(self.profile.next_report_date)
def test_it_saves_hourly_nag_period(self):
def test_it_sets_next_nag_date_when_setting_hourly_nag_period(self) -> None:
Check.objects.create(project=self.project, status="down")
self.client.login(username="alice@example.org", password="password")
form = {"nag_period": "3600"}
r = self.client.post("/accounts/profile/notifications/", form)
r = self.client.post(self.url, self._payload(nag_period="3600"))
self.assertEqual(r.status_code, 200)
self.profile.refresh_from_db()
self.assertEqual(self.profile.nag_period.total_seconds(), 3600)
self.assertIsNotNone(self.profile.next_nag_date)
def test_it_does_not_save_nonstandard_nag_period(self):
def test_it_clears_next_nag_date_when_setting_hourly_nag_period(self) -> None:
self.profile.next_nag_date = now() + td(minutes=30)
self.profile.save()
self.client.login(username="alice@example.org", password="password")
r = self.client.post(self.url, self._payload(nag_period="3600"))
self.assertEqual(r.status_code, 200)
self.profile.refresh_from_db()
self.assertEqual(self.profile.nag_period.total_seconds(), 3600)
self.assertIsNone(self.profile.next_nag_date)
def test_it_does_not_save_nonstandard_nag_period(self) -> None:
self.profile.nag_period = td(seconds=3600)
self.profile.save()
self.client.login(username="alice@example.org", password="password")
form = {"nag_period": "1234"}
r = self.client.post("/accounts/profile/notifications/", form)
r = self.client.post(self.url, self._payload(nag_period="1234"))
self.assertEqual(r.status_code, 200)
self.profile.refresh_from_db()
self.assertEqual(self.profile.nag_period.total_seconds(), 3600)
def test_it_saves_tz(self) -> None:
self.client.login(username="alice@example.org", password="password")
r = self.client.post(self.url, self._payload())
self.assertEqual(r.status_code, 200)
self.profile.refresh_from_db()
self.assertEqual(self.profile.tz, "Europe/Riga")
def test_it_ignores_bad_tz(self) -> None:
self.profile.tz = "Europe/Riga"
self.profile.save()
self.client.login(username="alice@example.org", password="password")
r = self.client.post(self.url, self._payload(reports="weekly", tz="Foo/Bar"))
self.assertEqual(r.status_code, 200)
self.profile.refresh_from_db()
self.assertEqual(self.profile.reports, "weekly")
self.assertEqual(self.profile.tz, "Europe/Riga")

View file

@ -1,128 +1,148 @@
from datetime import timedelta as td
from django.core import mail
from __future__ import annotations
from django.conf import settings
from django.utils.timezone import now
from django.test.utils import override_settings
from hc.accounts.models import Credential
from hc.test import BaseTestCase
from hc.api.models import Check
class ProfileTestCase(BaseTestCase):
def test_it_sends_set_password_link(self):
def test_it_shows_profile_page(self) -> None:
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
r = self.client.get("/accounts/profile/")
self.assertContains(r, "Email and Password")
self.assertContains(r, "Change Password")
self.assertContains(r, "Set Up Authenticator App")
# profile.token should be set now
self.profile.refresh_from_db()
token = self.profile.token
self.assertTrue(len(token) > 10)
# And an email should have been sent
self.assertEqual(len(mail.outbox), 1)
expected_subject = "Set password on %s" % settings.SITE_NAME
self.assertEqual(mail.outbox[0].subject, expected_subject)
def test_it_sends_report(self):
check = Check(project=self.project, name="Test Check")
check.last_ping = now()
check.save()
sent = self.profile.send_report()
self.assertTrue(sent)
# And an email should have been sent
self.assertEqual(len(mail.outbox), 1)
message = mail.outbox[0]
self.assertEqual(message.subject, "Monthly Report")
self.assertIn("Test Check", message.body)
def test_it_skips_report_if_no_pings(self):
check = Check(project=self.project, name="Test Check")
check.save()
sent = self.profile.send_report()
self.assertFalse(sent)
self.assertEqual(len(mail.outbox), 0)
def test_it_skips_report_if_no_recent_pings(self):
check = Check(project=self.project, name="Test Check")
check.last_ping = now() - td(days=365)
check.save()
sent = self.profile.send_report()
self.assertFalse(sent)
self.assertEqual(len(mail.outbox), 0)
def test_it_sends_nag(self):
check = Check(project=self.project, name="Test Check")
check.status = "down"
check.last_ping = now()
check.save()
self.profile.nag_period = td(hours=1)
self.profile.save()
sent = self.profile.send_report(nag=True)
self.assertTrue(sent)
# And an email should have been sent
self.assertEqual(len(mail.outbox), 1)
message = mail.outbox[0]
self.assertEqual(message.subject, "Reminder: 1 check still down")
self.assertIn("Test Check", message.body)
def test_it_skips_nag_if_none_down(self):
check = Check(project=self.project, name="Test Check")
check.last_ping = now()
check.save()
self.profile.nag_period = td(hours=1)
self.profile.save()
sent = self.profile.send_report(nag=True)
self.assertFalse(sent)
self.assertEqual(len(mail.outbox), 0)
def test_it_sends_change_email_link(self):
self.client.login(username="alice@example.org", password="password")
form = {"change_email": "1"}
r = self.client.post("/accounts/profile/", form)
assert r.status_code == 302
# profile.token should be set now
self.profile.refresh_from_db()
token = self.profile.token
self.assertTrue(len(token) > 10)
# And an email should have been sent
self.assertEqual(len(mail.outbox), 1)
expected_subject = "Change email address on %s" % settings.SITE_NAME
self.assertEqual(mail.outbox[0].subject, expected_subject)
def test_leaving_works(self):
def test_leaving_works(self) -> None:
self.client.login(username="bob@example.org", password="password")
form = {"code": str(self.project.code), "leave_project": "1"}
r = self.client.post("/accounts/profile/", form)
self.assertContains(r, "Left project")
self.assertNotContains(r, "Alice's Project")
self.assertContains(r, "Left project <strong>Alices Project</strong>")
self.assertNotContains(r, "Member")
self.bobs_profile.refresh_from_db()
self.assertIsNone(self.bobs_profile.current_project)
self.assertFalse(self.bob.memberships.exists())
def test_leaving_checks_membership(self):
def test_leaving_checks_membership(self) -> None:
self.client.login(username="charlie@example.org", password="password")
form = {"code": str(self.project.code), "leave_project": "1"}
r = self.client.post("/accounts/profile/", form)
self.assertEqual(r.status_code, 400)
def test_it_shows_project_membership(self) -> None:
self.client.login(username="bob@example.org", password="password")
r = self.client.get("/accounts/profile/")
self.assertContains(r, "Alices Project")
self.assertContains(r, "Member")
def test_it_shows_readonly_project_membership(self) -> None:
self.bobs_membership.role = "r"
self.bobs_membership.save()
self.client.login(username="bob@example.org", password="password")
r = self.client.get("/accounts/profile/")
self.assertContains(r, "Alices Project")
self.assertContains(r, "Read-only")
def test_it_handles_no_projects(self) -> None:
self.project.delete()
self.client.login(username="alice@example.org", password="password")
r = self.client.get("/accounts/profile/")
self.assertContains(r, "You do not have any projects. Create one!")
@override_settings(RP_ID=None)
def test_it_hides_security_keys_bits_if_rp_id_not_set(self) -> None:
self.client.login(username="alice@example.org", password="password")
r = self.client.get("/accounts/profile/")
self.assertContains(r, "Two-factor Authentication")
self.assertNotContains(r, "Security keys")
self.assertNotContains(r, "Add Security Key")
@override_settings(RP_ID="testserver")
def test_it_handles_no_credentials(self) -> None:
self.client.login(username="alice@example.org", password="password")
r = self.client.get("/accounts/profile/")
self.assertContains(r, "Two-factor Authentication")
self.assertContains(r, "Your account does not have any configured two-factor")
@override_settings(RP_ID="testserver")
def test_it_shows_security_key(self) -> None:
Credential.objects.create(user=self.alice, name="Alices Key")
self.client.login(username="alice@example.org", password="password")
r = self.client.get("/accounts/profile/")
self.assertContains(r, "Alices Key")
# It should show a warning about Alices Key being the only second factor
s = """The key "Alices Key" is currently your only second factor."""
self.assertContains(r, s)
def test_it_handles_unusable_password(self) -> None:
self.alice.set_unusable_password()
self.alice.save()
# Authenticate using the ProfileBackend and a token:
token = self.profile.prepare_token()
self.client.login(username="alice", token=token)
r = self.client.get("/accounts/profile/")
self.assertContains(r, "Set Password")
self.assertNotContains(r, "Change Password")
@override_settings(RP_ID="testserver")
def test_it_shows_totp(self) -> None:
self.profile.totp = "0" * 32
self.profile.totp_created = "2020-01-01T00:00:00+00:00"
self.profile.save()
self.client.login(username="alice@example.org", password="password")
r = self.client.get("/accounts/profile/")
self.assertContains(r, "Enabled")
self.assertContains(r, "configured on Jan 1, 2020")
self.assertNotContains(r, "Set Up Authenticator App")
# It should show a warning about TOTP being the only second factor
s = "The Authenticator app is currently your only second factor."
self.assertContains(r, s)
self.assertContains(r, "or register a Security Key to be used")
def test_it_shows_no_warning_if_multiple_keys_are_registered(self) -> None:
Credential.objects.create(user=self.alice, name="Alices Key")
Credential.objects.create(user=self.alice, name="Alices Other Key")
self.client.login(username="alice@example.org", password="password")
r = self.client.get("/accounts/profile/")
self.assertNotContains(r, "is currently your only second factor.")
def test_it_shows_no_warning_if_key_and_totp_is_registered(self) -> None:
Credential.objects.create(user=self.alice, name="Alices Key")
self.profile.totp = "0" * 32
self.profile.totp_created = "2020-01-01T00:00:00+00:00"
self.profile.save()
self.client.login(username="alice@example.org", password="password")
r = self.client.get("/accounts/profile/")
self.assertNotContains(r, "is currently your only second factor.")
@override_settings(RP_ID=None)
def test_it_does_not_mention_security_key_if_rp_id_is_not_set(self) -> None:
self.profile.totp = "0" * 32
self.profile.totp_created = "2020-01-01T00:00:00+00:00"
self.profile.save()
self.client.login(username="alice@example.org", password="password")
r = self.client.get("/accounts/profile/")
self.assertNotContains(r, "or register a Security Key to be used")

View file

@ -0,0 +1,126 @@
from __future__ import annotations
from datetime import datetime
from datetime import timedelta as td
from datetime import timezone
from unittest.mock import Mock, patch
from django.core import mail
from django.core.mail import EmailMessage, EmailMultiAlternatives
from django.utils.timezone import now
from hc.api.models import Check
from hc.test import BaseTestCase
CURRENT_TIME = datetime(2020, 1, 15, tzinfo=timezone.utc)
MOCK_NOW = Mock(return_value=CURRENT_TIME)
class ProfileModelTestCase(BaseTestCase):
def get_html(self, email: EmailMessage) -> str:
assert isinstance(email, EmailMultiAlternatives)
html, _ = email.alternatives[0]
assert isinstance(html, str)
return html
@patch("hc.lib.date.now", MOCK_NOW)
def test_it_sends_report(self) -> None:
check = Check(project=self.project, name="Test Check")
check.last_ping = now()
check.save()
sent = self.profile.send_report()
self.assertTrue(sent)
# And an email should have been sent
self.assertEqual(len(mail.outbox), 1)
message = mail.outbox[0]
self.assertEqual(message.subject, "Monthly Report")
self.assertIn("Test Check", message.body)
html = self.get_html(message)
self.assertNotIn("Jan. 2020", html)
self.assertIn("Dec. 2019", html)
self.assertIn("Nov. 2019", html)
self.assertNotIn("Oct. 2019", html)
def test_it_skips_report_if_no_pings(self) -> None:
check = Check(project=self.project, name="Test Check")
check.save()
sent = self.profile.send_report()
self.assertFalse(sent)
self.assertEqual(len(mail.outbox), 0)
def test_it_skips_report_if_no_recent_pings(self) -> None:
check = Check(project=self.project, name="Test Check")
check.last_ping = now() - td(days=365)
check.save()
sent = self.profile.send_report()
self.assertFalse(sent)
self.assertEqual(len(mail.outbox), 0)
def test_it_sends_nag(self) -> None:
check = Check(project=self.project, name="Test Check")
check.status = "down"
check.last_ping = now()
check.save()
self.profile.nag_period = td(hours=1)
self.profile.save()
sent = self.profile.send_report(nag=True)
self.assertTrue(sent)
# And an email should have been sent
self.assertEqual(len(mail.outbox), 1)
message = mail.outbox[0]
self.assertEqual(message.subject, "Reminder: 1 check still down")
self.assertIn("Test Check", message.body)
def test_it_skips_nag_if_none_down(self) -> None:
check = Check(project=self.project, name="Test Check")
check.last_ping = now()
check.save()
self.profile.nag_period = td(hours=1)
self.profile.save()
sent = self.profile.send_report(nag=True)
self.assertFalse(sent)
self.assertEqual(len(mail.outbox), 0)
def test_it_sets_next_nag_date(self) -> None:
Check.objects.create(project=self.project, status="down")
self.profile.nag_period = td(hours=1)
self.profile.update_next_nag_date()
self.assertTrue(self.profile.next_nag_date)
def test_it_does_not_set_next_nag_date_if_no_nag_period(self) -> None:
Check.objects.create(project=self.project, status="down")
self.profile.update_next_nag_date()
self.assertIsNone(self.profile.next_nag_date)
def test_it_does_not_update_existing_next_nag_date(self) -> None:
Check.objects.create(project=self.project, status="down")
original_nag_date = now() - td(minutes=30)
self.profile.next_nag_date = original_nag_date
self.profile.nag_period = td(hours=1)
self.profile.update_next_nag_date()
self.assertEqual(self.profile.next_nag_date, original_nag_date)
def test_it_clears_next_nag_date(self) -> None:
self.profile.next_nag_date = now()
self.profile.update_next_nag_date()
self.assertIsNone(self.profile.next_nag_date)

View file

@ -1,71 +1,111 @@
from django.core import mail
from __future__ import annotations
from django.conf import settings
from django.contrib.auth.models import User
from django.core import mail
from django.core.mail import EmailMessage, EmailMultiAlternatives
from django.test.utils import override_settings
from hc.test import BaseTestCase
from hc.accounts.models import Member
from hc.accounts.models import Member, Project
from hc.api.models import TokenBucket
from hc.test import BaseTestCase
class ProjectTestCase(BaseTestCase):
def setUp(self):
super(ProjectTestCase, self).setUp()
def setUp(self) -> None:
super().setUp()
self.url = "/projects/%s/settings/" % self.project.code
def test_it_checks_access(self):
def get_html(self, email: EmailMessage) -> str:
assert isinstance(email, EmailMultiAlternatives)
html, _ = email.alternatives[0]
assert isinstance(html, str)
return html
def test_it_checks_access(self) -> None:
self.client.login(username="charlie@example.org", password="password")
r = self.client.get(self.url)
self.assertEqual(r.status_code, 404)
def test_it_allows_team_access(self):
def test_it_allows_team_access(self) -> None:
self.client.login(username="bob@example.org", password="password")
r = self.client.get(self.url)
self.assertContains(r, "Change Project Name")
def test_it_shows_api_keys(self):
def test_it_masks_keys_by_default(self) -> None:
self.project.api_key_readonly = "R" * 32
self.project.ping_key = "P" * 22
self.project.save()
self.client.login(username="alice@example.org", password="password")
form = {"show_api_keys": "1"}
r = self.client.get(self.url)
self.assertEqual(r.status_code, 200)
self.assertNotContains(r, "X" * 32)
self.assertNotContains(r, "R" * 32)
self.assertNotContains(r, "P" * 22)
def test_it_shows_keys(self) -> None:
self.project.api_key_readonly = "R" * 32
self.project.ping_key = "P" * 22
self.project.save()
self.client.login(username="alice@example.org", password="password")
form = {"show_keys": "1"}
r = self.client.post(self.url, form)
self.assertEqual(r.status_code, 200)
self.assertContains(r, "X" * 32)
self.assertContains(r, "R" * 32)
self.assertContains(r, "P" * 22)
self.assertContains(r, "Prometheus metrics endpoint")
def test_it_creates_api_key(self):
def test_it_creates_readonly_key(self) -> None:
self.client.login(username="alice@example.org", password="password")
form = {"create_api_keys": "1"}
form = {"create_key": "api_key_readonly"}
r = self.client.post(self.url, form)
self.assertEqual(r.status_code, 200)
self.project.refresh_from_db()
api_key = self.project.api_key
self.assertTrue(len(api_key) > 10)
self.assertFalse("b'" in api_key)
self.assertEqual(len(self.project.api_key_readonly), 32)
self.assertFalse("b'" in self.project.api_key_readonly)
def test_it_revokes_api_key(self):
self.project.api_key_readonly = "R" * 32
self.project.save()
def test_it_requires_rw_access_to_create_key(self) -> None:
self.bobs_membership.role = "r"
self.bobs_membership.save()
self.client.login(username="bob@example.org", password="password")
r = self.client.post(self.url, {"create_key": "api_key_readonly"})
self.assertEqual(r.status_code, 403)
def test_it_revokes_api_key(self) -> None:
self.client.login(username="alice@example.org", password="password")
form = {"revoke_api_keys": "1"}
r = self.client.post(self.url, form)
r = self.client.post(self.url, {"revoke_key": "api_key"})
self.assertEqual(r.status_code, 200)
self.project.refresh_from_db()
self.assertEqual(self.project.api_key, "")
self.assertEqual(self.project.api_key_readonly, "")
def test_it_adds_team_member(self):
def test_it_requires_rw_access_to_revoke_api_key(self) -> None:
self.bobs_membership.role = "r"
self.bobs_membership.save()
self.client.login(username="bob@example.org", password="password")
r = self.client.post(self.url, {"revoke_key": "api_key"})
self.assertEqual(r.status_code, 403)
def test_it_adds_team_member(self) -> None:
# Use "'" in the name to see if does or doesn't get escaped in email subject:
self.project.name = "Alice's Project"
self.project.save()
self.client.login(username="alice@example.org", password="password")
form = {"invite_team_member": "1", "email": "frank@example.org"}
form = {"invite_team_member": "1", "email": "frank@example.org", "role": "w"}
r = self.client.post(self.url, form)
self.assertEqual(r.status_code, 200)
@ -76,69 +116,226 @@ class ProjectTestCase(BaseTestCase):
project=self.project, user__email="frank@example.org"
)
profile = member.user.profile
self.assertEqual(profile.current_project, self.project)
# The read-write flag should be set
self.assertEqual(member.role, member.Role.REGULAR)
# The new user should not have their own project
self.assertFalse(member.user.project_set.exists())
# And an email should have been sent
subj = (
"You have been invited to join"
" Alice&#39;s Project on %s" % settings.SITE_NAME
message = mail.outbox[0]
subj = f"You have been invited to join Alice's Project on {settings.SITE_NAME}"
self.assertEqual(message.subject, subj)
html = self.get_html(message)
self.assertIn("You will be able to manage", message.body)
self.assertIn("You will be able to manage", html)
@override_settings(EMAIL_HOST=None)
def test_it_skips_invite_email_if_email_host_not_set(self) -> None:
self.client.login(username="alice@example.org", password="password")
form = {"invite_team_member": "1", "email": "frank@example.org", "role": "w"}
r = self.client.post(self.url, form)
self.assertEqual(r.status_code, 200)
self.assertEqual(self.project.member_set.count(), 2)
self.assertEqual(len(mail.outbox), 0)
def test_it_adds_readonly_team_member(self) -> None:
self.client.login(username="alice@example.org", password="password")
form = {"invite_team_member": "1", "email": "frank@example.org", "role": "r"}
r = self.client.post(self.url, form)
self.assertEqual(r.status_code, 200)
member = Member.objects.get(
project=self.project, user__email="frank@example.org"
)
self.assertEqual(mail.outbox[0].subject, subj)
self.assertEqual(member.role, member.Role.READONLY)
# And an email should have been sent
message = mail.outbox[0]
html = self.get_html(message)
self.assertIn("You will be able to view", message.body)
self.assertIn("You will be able to view", html)
def test_it_adds_manager_team_member(self) -> None:
self.client.login(username="alice@example.org", password="password")
form = {"invite_team_member": "1", "email": "frank@example.org", "role": "m"}
r = self.client.post(self.url, form)
self.assertEqual(r.status_code, 200)
member = Member.objects.get(
project=self.project, user__email="frank@example.org"
)
# The new user should have role manager
self.assertEqual(member.role, member.Role.MANAGER)
def test_it_adds_member_from_another_team(self) -> None:
# With team limit at zero, we should not be able to invite any new users
self.profile.team_limit = 0
self.profile.save()
# But Charlie will have an existing membership in another Alice's project
# so Alice *should* be able to invite Charlie:
p2 = Project.objects.create(owner=self.alice)
Member.objects.create(user=self.charlie, project=p2)
self.client.login(username="alice@example.org", password="password")
form = {"invite_team_member": "1", "email": "charlie@example.org", "role": "r"}
r = self.client.post(self.url, form)
self.assertEqual(r.status_code, 200)
q = Member.objects.filter(project=self.project, user=self.charlie)
self.assertEqual(q.count(), 1)
# And this should not have affected the rate limit:
tq = TokenBucket.objects.filter(value="invite-%d" % self.alice.id)
self.assertFalse(tq.exists())
def test_it_rejects_duplicate_membership(self) -> None:
self.client.login(username="alice@example.org", password="password")
form = {"invite_team_member": "1", "email": "bob@example.org", "role": "r"}
r = self.client.post(self.url, form)
self.assertContains(r, "bob@example.org is already a member")
# The number of memberships should have not increased
self.assertEqual(self.project.member_set.count(), 1)
def test_it_rejects_owner_as_a_member(self) -> None:
self.client.login(username="alice@example.org", password="password")
form = {"invite_team_member": "1", "email": "alice@example.org", "role": "r"}
r = self.client.post(self.url, form)
self.assertContains(r, "alice@example.org is already a member")
# The number of memberships should have not increased
self.assertEqual(self.project.member_set.count(), 1)
def test_it_rejects_too_long_email_addresses(self) -> None:
self.client.login(username="alice@example.org", password="password")
aaa = "a" * 300
form = {
"invite_team_member": "1",
"email": f"frank+{aaa}@example.org",
"role": "r",
}
r = self.client.post(self.url, form)
self.assertEqual(r.status_code, 200)
# No email should have been sent
self.assertEqual(len(mail.outbox), 0)
@override_settings(SECRET_KEY="test-secret")
def test_it_rate_limits_invites(self):
def test_it_rate_limits_invites(self) -> None:
obj = TokenBucket(value="invite-%d" % self.alice.id)
obj.tokens = 0
obj.save()
self.client.login(username="alice@example.org", password="password")
form = {"invite_team_member": "1", "email": "frank@example.org"}
form = {"invite_team_member": "1", "email": "frank@example.org", "role": "r"}
r = self.client.post(self.url, form)
self.assertContains(r, "Too Many Requests")
self.assertEqual(len(mail.outbox), 0)
def test_it_requires_owner_to_add_team_member(self):
def test_it_lets_manager_add_team_member(self) -> None:
# Bob is a manager:
self.bobs_membership.role = "m"
self.bobs_membership.save()
self.client.login(username="bob@example.org", password="password")
form = {"invite_team_member": "1", "email": "frank@example.org"}
form = {"invite_team_member": "1", "email": "frank@example.org", "role": "w"}
r = self.client.post(self.url, form)
self.assertEqual(r.status_code, 200)
Member.objects.get(project=self.project, user__email="frank@example.org")
def test_it_does_not_allow_regular_member_invite_team_members(self) -> None:
self.client.login(username="bob@example.org", password="password")
form = {"invite_team_member": "1", "email": "frank@example.org", "role": "w"}
r = self.client.post(self.url, form)
self.assertEqual(r.status_code, 403)
def test_it_checks_team_size(self):
def test_it_checks_team_size(self) -> None:
self.profile.team_limit = 0
self.profile.save()
self.client.login(username="alice@example.org", password="password")
form = {"invite_team_member": "1", "email": "frank@example.org"}
form = {"invite_team_member": "1", "email": "frank@example.org", "role": "r"}
r = self.client.post(self.url, form)
self.assertEqual(r.status_code, 403)
def test_it_removes_team_member(self):
def test_it_invites_user_with_email_as_username(self) -> None:
User.objects.create(username="frank@example.org", email="frank@example.org")
self.client.login(username="alice@example.org", password="password")
form = {"invite_team_member": "1", "email": "frank@example.org", "role": "w"}
r = self.client.post(self.url, form)
self.assertEqual(r.status_code, 200)
q = Member.objects.filter(project=self.project, user__email="frank@example.org")
self.assertEqual(q.count(), 1)
def test_it_lets_owner_remove_team_member(self) -> None:
self.client.login(username="alice@example.org", password="password")
form = {"remove_team_member": "1", "email": "bob@example.org"}
r = self.client.post(self.url, form)
self.assertEqual(r.status_code, 200)
self.assertEqual(Member.objects.count(), 0)
self.assertFalse(Member.objects.exists())
self.bobs_profile.refresh_from_db()
self.assertEqual(self.bobs_profile.current_project, None)
def test_it_lets_manager_remove_team_member(self) -> None:
# Bob is a manager:
self.bobs_membership.role = "m"
self.bobs_membership.save()
# Bob will try to remove this membership:
Member.objects.create(user=self.charlie, project=self.project)
self.client.login(username="bob@example.org", password="password")
form = {"remove_team_member": "1", "email": "charlie@example.org"}
r = self.client.post(self.url, form)
self.assertEqual(r.status_code, 200)
q = Member.objects.filter(user=self.charlie, project=self.project)
self.assertFalse(q.exists())
def test_it_does_not_allow_regular_member_remove_team_member(self) -> None:
# Bob will try to remove this membership:
Member.objects.create(user=self.charlie, project=self.project)
self.client.login(username="bob@example.org", password="password")
form = {"remove_team_member": "1", "email": "charlie@example.org"}
r = self.client.post(self.url, form)
self.assertEqual(r.status_code, 403)
def test_it_rejects_manager_remove_self(self) -> None:
self.bobs_membership.role = "m"
self.bobs_membership.save()
def test_it_requires_owner_to_remove_team_member(self):
self.client.login(username="bob@example.org", password="password")
form = {"remove_team_member": "1", "email": "bob@example.org"}
r = self.client.post(self.url, form)
self.assertEqual(r.status_code, 403)
self.assertEqual(r.status_code, 400)
def test_it_checks_membership_when_removing_team_member(self):
# The number of memberships should have not decreased
self.assertEqual(self.project.member_set.count(), 1)
def test_it_checks_membership_when_removing_team_member(self) -> None:
self.client.login(username="charlie@example.org", password="password")
url = "/projects/%s/settings/" % self.charlies_project.code
@ -146,10 +343,7 @@ class ProjectTestCase(BaseTestCase):
r = self.client.post(url, form)
self.assertEqual(r.status_code, 400)
self.profile.refresh_from_db()
self.assertIsNotNone(self.profile.current_project)
def test_it_sets_project_name(self):
def test_it_sets_project_name(self) -> None:
self.client.login(username="alice@example.org", password="password")
form = {"set_project_name": "1", "name": "Alpha Team"}
@ -158,3 +352,60 @@ class ProjectTestCase(BaseTestCase):
self.project.refresh_from_db()
self.assertEqual(self.project.name, "Alpha Team")
def test_it_requires_rw_access_to_set_project_name(self) -> None:
self.bobs_membership.role = "r"
self.bobs_membership.save()
self.client.login(username="bob@example.org", password="password")
form = {"set_project_name": "1", "name": "Alpha Team"}
r = self.client.post(self.url, form)
self.assertEqual(r.status_code, 403)
def test_it_shows_invite_suggestions(self) -> None:
p2 = Project.objects.create(owner=self.alice)
self.client.login(username="alice@example.org", password="password")
r = self.client.get("/projects/%s/settings/" % p2.code)
self.assertContains(r, "Add Users from Other Projects")
self.assertContains(r, "bob@example.org")
def test_it_requires_rw_access_to_update_project_name(self) -> None:
self.bobs_membership.role = "r"
self.bobs_membership.save()
self.client.login(username="bob@example.org", password="password")
form = {"set_project_name": "1", "name": "Alpha Team"}
r = self.client.post(self.url, form)
self.assertEqual(r.status_code, 403)
def test_it_hides_actions_for_readonly_users(self) -> None:
self.bobs_membership.role = "r"
self.bobs_membership.save()
self.client.login(username="bob@example.org", password="password")
r = self.client.get(self.url)
self.assertNotContains(r, "#set-project-name-modal", status_code=200)
self.assertNotContains(r, "Show API Keys")
@override_settings(PROMETHEUS_ENABLED=False)
def test_it_hides_prometheus_link_if_prometheus_not_enabled(self) -> None:
self.project.api_key_readonly = "R" * 32
self.project.save()
self.client.login(username="alice@example.org", password="password")
r = self.client.post(self.url, {"show_api_keys": "1"})
self.assertEqual(r.status_code, 200)
self.assertNotContains(r, "Prometheus metrics endpoint")
def test_it_requires_rw_access_to_show_api_key(self) -> None:
self.bobs_membership.role = "r"
self.bobs_membership.save()
self.client.login(username="bob@example.org", password="password")
r = self.client.post(self.url, {"show_keys": "1"})
self.assertEqual(r.status_code, 403)

View file

@ -1,10 +1,12 @@
from __future__ import annotations
from hc.accounts.models import Member, Project
from hc.api.models import Channel, Check
from hc.test import BaseTestCase
from hc.accounts.models import Project
from hc.api.models import Check
class ProjectModelTestCase(BaseTestCase):
def test_num_checks_available_handles_multiple_projects(self):
def test_num_checks_available_handles_multiple_projects(self) -> None:
# One check in Alice's primary project:
Check.objects.create(project=self.project)
@ -13,3 +15,37 @@ class ProjectModelTestCase(BaseTestCase):
Check.objects.create(project=p2)
self.assertEqual(self.project.num_checks_available(), 18)
def test_it_handles_zero_broken_channels(self) -> None:
Channel.objects.create(kind="webhook", last_error="", project=self.project)
self.assertFalse(self.project.have_channel_issues())
def test_it_handles_one_broken_channel(self) -> None:
Channel.objects.create(kind="webhook", last_error="x", project=self.project)
self.assertTrue(self.project.have_channel_issues())
def test_it_handles_no_channels(self) -> None:
# It's an issue if the project has no channels at all:
self.assertTrue(self.project.have_channel_issues())
def test_it_allows_third_user(self) -> None:
# Alice is the owner, and Bob is invited -- there is space for the third user:
self.assertTrue(self.project.can_invite_new_users())
def test_it_allows_same_user_in_multiple_projects(self) -> None:
p2 = Project.objects.create(owner=self.alice)
Member.objects.create(user=self.bob, project=p2)
# Bob's membership in two projects counts as one seat,
# one seat should be still free:
self.assertTrue(self.project.can_invite_new_users())
def test_it_checks_team_limit(self) -> None:
p2 = Project.objects.create(owner=self.alice)
Member.objects.create(user=self.charlie, project=p2)
# Alice and Bob are in one project, Charlie is in another,
# so no seats left:
self.assertFalse(self.project.can_invite_new_users())

View file

@ -1,7 +1,11 @@
from datetime import timedelta
from __future__ import annotations
from datetime import timedelta as td
from unittest.mock import Mock
from django.contrib.auth.models import User
from django.utils import timezone
from django.utils.timezone import now
from hc.accounts.management.commands.pruneusers import Command
from hc.accounts.models import Project
from hc.api.models import Check
@ -9,9 +13,9 @@ from hc.test import BaseTestCase
class PruneUsersTestCase(BaseTestCase):
year_ago = timezone.now() - timedelta(days=365)
year_ago = now() - td(days=365)
def test_it_removes_old_never_logged_in_users(self):
def test_it_removes_old_never_logged_in_users(self) -> None:
self.charlie.date_joined = self.year_ago
self.charlie.save()
@ -19,16 +23,17 @@ class PruneUsersTestCase(BaseTestCase):
charlies_project = Project.objects.create(owner=self.charlie)
Check(project=charlies_project).save()
Command().handle()
Command(stdout=Mock()).handle()
self.assertEqual(User.objects.filter(username="charlie").count(), 0)
self.assertEqual(Check.objects.count(), 0)
def test_it_leaves_team_members_alone(self):
def test_it_leaves_team_members_alone(self) -> None:
self.bob.date_joined = self.year_ago
self.bob.last_login = self.year_ago
self.bob.save()
Command().handle()
Command(stdout=Mock()).handle()
# Bob belongs to a team so should not get removed
self.assertEqual(User.objects.filter(username="bob").count(), 1)

View file

@ -0,0 +1,65 @@
from __future__ import annotations
from unittest.mock import patch
from django.contrib.auth.models import User
from django.test.utils import override_settings
from hc.test import BaseTestCase
@override_settings(
REMOTE_USER_HEADER="AUTH_USER",
AUTHENTICATION_BACKENDS=("hc.accounts.backends.CustomHeaderBackend",),
)
class RemoteUserHeaderTestCase(BaseTestCase):
@override_settings(REMOTE_USER_HEADER=None)
def test_it_does_nothing_when_not_configured(self) -> None:
r = self.client.get("/accounts/profile/", AUTH_USER="alice@example.org")
self.assertRedirects(r, "/accounts/login/?next=/accounts/profile/")
def test_it_logs_user_in(self) -> None:
r = self.client.get("/accounts/profile/", AUTH_USER="alice@example.org")
self.assertContains(r, "alice@example.org")
def test_it_forces_lowercase(self) -> None:
r = self.client.get("/accounts/profile/", AUTH_USER="USER@example.org")
# It should have created a new user account, the email should have been
# converted to lowercase
self.assertContains(r, "user@example.org")
def test_it_does_nothing_when_header_not_set(self) -> None:
r = self.client.get("/accounts/profile/")
self.assertRedirects(r, "/accounts/login/?next=/accounts/profile/")
def test_it_does_nothing_when_header_is_empty_string(self) -> None:
r = self.client.get("/accounts/profile/", AUTH_USER="")
self.assertRedirects(r, "/accounts/login/?next=/accounts/profile/")
def test_it_creates_user(self) -> None:
r = self.client.get("/accounts/profile/", AUTH_USER="dave@example.org")
self.assertContains(r, "dave@example.org")
q = User.objects.filter(email="dave@example.org")
self.assertTrue(q.exists())
def test_it_logs_out_another_user_when_header_is_empty_string(self) -> None:
self.client.login(remote_user_email="bob@example.org")
r = self.client.get("/accounts/profile/", AUTH_USER="")
self.assertRedirects(r, "/accounts/login/?next=/accounts/profile/")
def test_it_logs_out_another_user(self) -> None:
self.client.login(remote_user_email="bob@example.org")
r = self.client.get("/accounts/profile/", AUTH_USER="alice@example.org")
self.assertContains(r, "alice@example.org")
def test_it_handles_already_logged_in_user(self) -> None:
self.client.login(remote_user_email="alice@example.org")
with patch("hc.accounts.middleware.auth") as mock_auth:
r = self.client.get("/accounts/profile/", AUTH_USER="alice@example.org")
mock_auth.authenticate.assert_not_called()
self.assertContains(r, "alice@example.org")

View file

@ -0,0 +1,65 @@
from __future__ import annotations
from django.test.utils import override_settings
from hc.accounts.models import Credential
from hc.test import BaseTestCase
@override_settings(RP_ID="testserver")
class RemoveCredentialTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
self.c = Credential.objects.create(user=self.alice, name="Alices Key")
self.url = f"/accounts/two_factor/{self.c.code}/remove/"
def test_it_requires_sudo_mode(self) -> None:
self.client.login(username="alice@example.org", password="password")
r = self.client.get(self.url)
self.assertContains(r, "We have sent a confirmation code")
@override_settings(RP_ID=None)
def test_it_requires_rp_id(self) -> None:
self.client.login(username="alice@example.org", password="password")
self.set_sudo_flag()
r = self.client.get(self.url)
self.assertEqual(r.status_code, 404)
def test_it_shows_form(self) -> None:
self.client.login(username="alice@example.org", password="password")
self.set_sudo_flag()
r = self.client.get(self.url)
self.assertContains(r, "Remove Security Key")
self.assertContains(r, "Alices Key")
self.assertContains(r, "two-factor authentication will no longer be active")
def test_it_skips_warning_when_other_2fa_methods_exist(self) -> None:
self.profile.totp = "0" * 32
self.profile.save()
self.client.login(username="alice@example.org", password="password")
self.set_sudo_flag()
r = self.client.get(self.url)
self.assertNotContains(r, "two-factor authentication will no longer be active")
def test_it_removes_credential(self) -> None:
self.client.login(username="alice@example.org", password="password")
self.set_sudo_flag()
r = self.client.post(self.url, {"remove_credential": ""}, follow=True)
self.assertRedirects(r, "/accounts/profile/")
self.assertContains(r, "Removed security key <strong>Alices Key</strong>")
self.assertFalse(self.alice.credentials.exists())
def test_it_checks_owner(self) -> None:
self.client.login(username="charlie@example.org", password="password")
self.set_sudo_flag()
r = self.client.post(self.url, {"remove_credential": ""})
self.assertEqual(r.status_code, 400)

View file

@ -1,36 +1,34 @@
from __future__ import annotations
from hc.api.models import Check
from hc.test import BaseTestCase
class RemoveProjectTestCase(BaseTestCase):
def setUp(self):
super(RemoveProjectTestCase, self).setUp()
def setUp(self) -> None:
super().setUp()
self.url = "/projects/%s/remove/" % self.project.code
def test_it_works(self):
def test_it_works(self) -> None:
Check.objects.create(project=self.project, tags="foo a-B_1 baz@")
self.client.login(username="alice@example.org", password="password")
r = self.client.post(self.url)
self.assertRedirects(r, "/")
# Alice's current project should be not set
self.profile.refresh_from_db()
self.assertEqual(self.profile.current_project, None)
# Alice should not own any projects
self.assertFalse(self.alice.project_set.exists())
# Check should be gone
self.assertFalse(Check.objects.exists())
def test_it_rejects_get(self):
def test_it_rejects_get(self) -> None:
self.client.login(username="alice@example.org", password="password")
r = self.client.get(self.url)
self.assertEqual(r.status_code, 405)
def test_it_checks_access(self):
def test_it_checks_access(self) -> None:
self.client.login(username="bob@example.org", password="password")
r = self.client.post(self.url)
self.assertEqual(r.status_code, 404)

View file

@ -0,0 +1,48 @@
from __future__ import annotations
from hc.accounts.models import Credential
from hc.test import BaseTestCase
class RemoveCredentialTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
self.profile.totp = "0" * 32
self.profile.save()
self.url = "/accounts/two_factor/totp/remove/"
def test_it_requires_sudo_mode(self) -> None:
self.client.login(username="alice@example.org", password="password")
r = self.client.get(self.url)
self.assertContains(r, "We have sent a confirmation code")
def test_it_shows_form(self) -> None:
self.client.login(username="alice@example.org", password="password")
self.set_sudo_flag()
r = self.client.get(self.url)
self.assertContains(r, "Disable Authenticator App")
self.assertContains(r, "two-factor authentication will no longer be active")
def test_it_skips_warning_when_other_2fa_methods_exist(self) -> None:
self.c = Credential.objects.create(user=self.alice, name="Alices Key")
self.client.login(username="alice@example.org", password="password")
self.set_sudo_flag()
r = self.client.get(self.url)
self.assertNotContains(r, "two-factor authentication will no longer be active")
def test_it_removes_totp(self) -> None:
self.client.login(username="alice@example.org", password="password")
self.set_sudo_flag()
r = self.client.post(self.url, {"disable_totp": "1"}, follow=True)
self.assertRedirects(r, "/accounts/profile/")
self.assertContains(r, "Disabled the authenticator app.")
self.profile.refresh_from_db()
self.assertIsNone(self.profile.totp)
self.assertIsNone(self.profile.totp_created)

View file

@ -0,0 +1,127 @@
from __future__ import annotations
import re
from datetime import timedelta as td
from unittest.mock import Mock, patch
from django.core import mail
from django.test.utils import override_settings
from django.utils.timezone import now
from hc.accounts.management.commands.senddeletionscheduled import Command
from hc.accounts.models import Member, Project
from hc.api.models import Channel, Check
from hc.test import BaseTestCase
MOCK_SLEEP = Mock()
def counts(result: str) -> list[int]:
"""Extract integer values from command's return value."""
return [int(s) for s in re.findall(r"\d+", result)]
@override_settings(SITE_NAME="Mychecks")
@patch("hc.api.management.commands.sendreports.time.sleep", MOCK_SLEEP)
class SendDeletionScheduledTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
self.channel = Channel(project=self.project, kind="email")
self.channel.value = "alerts@example.org"
self.channel.email_verified = True
self.channel.save()
def test_it_sends_notice(self) -> None:
self.profile.deletion_scheduled_date = now() + td(days=31)
self.profile.save()
Check.objects.create(project=self.project)
Check.objects.create(project=self.project)
cmd = Command(stdout=Mock())
result = cmd.handle()
self.assertEqual(counts(result), [1])
email = mail.outbox[0]
self.assertEqual(email.subject, "Account Deletion Warning")
self.assertEqual(email.to[0], "alice@example.org")
self.assertIn("Owner: alice@example.org", email.body)
self.assertIn("Number of checks in the account: 2", email.body)
def test_it_sends_notice_to_team_members(self) -> None:
self.profile.deletion_scheduled_date = now() + td(days=31)
self.profile.save()
self.bob.last_login = now()
self.bob.save()
cmd = Command(stdout=Mock())
result = cmd.handle()
self.assertEqual(counts(result), [1])
self.assertEqual(mail.outbox[0].to, ["alice@example.org", "bob@example.org"])
def test_it_skips_profiles_with_deletion_scheduled_date_not_set(self) -> None:
cmd = Command(stdout=Mock())
result = cmd.handle()
self.assertEqual(counts(result), [0])
self.assertEqual(len(mail.outbox), 0)
def test_it_skips_profiles_with_deletion_scheduled_date_in_past(self) -> None:
self.profile.deletion_scheduled_date = now() - td(minutes=1)
self.profile.save()
cmd = Command(stdout=Mock())
result = cmd.handle()
self.assertEqual(counts(result), [0])
self.assertEqual(len(mail.outbox), 0)
def test_it_avoids_duplicate_recipients(self) -> None:
self.profile.deletion_scheduled_date = now() + td(days=31)
self.profile.save()
self.bob.last_login = now()
self.bob.save()
second_project = Project.objects.create(owner=self.alice)
Member.objects.create(
user=self.bob, project=second_project, role=Member.Role.REGULAR
)
cmd = Command(stdout=Mock())
cmd.handle()
# Bob should be listed as a recipient a single time, despite two memberships:
self.assertEqual(mail.outbox[0].to, ["alice@example.org", "bob@example.org"])
def test_it_notifies_channel(self) -> None:
self.profile.deletion_scheduled_date = now() + td(days=5)
self.profile.save()
cmd = Command(stdout=Mock())
cmd.handle()
self.assertEqual(mail.outbox[0].subject, "Account Deletion Warning")
s = "DOWN | Mychecks Account Deletion"
self.assertTrue(mail.outbox[1].subject.startswith(s))
def test_it_does_not_notify_channels_if_more_than_14_days_left(self) -> None:
self.profile.deletion_scheduled_date = now() + td(days=15, minutes=1)
self.profile.save()
cmd = Command(stdout=Mock())
cmd.handle()
self.assertEqual(len(mail.outbox), 1)
def test_it_skips_email_channels_of_team_members(self) -> None:
self.profile.deletion_scheduled_date = now() + td(days=5)
self.profile.save()
self.channel.value = "alice@example.org"
self.channel.save()
cmd = Command(stdout=Mock())
cmd.handle()
self.assertEqual(len(mail.outbox), 1)

View file

@ -0,0 +1,157 @@
from __future__ import annotations
import re
from datetime import timedelta as td
from unittest.mock import Mock, patch
from django.core import mail
from django.utils.timezone import now
from hc.accounts.management.commands.sendinactivitynotices import Command
from hc.accounts.models import Member
from hc.api.models import Check, Ping
from hc.payments.models import Subscription
from hc.test import BaseTestCase
MOCK_SLEEP = Mock()
def counts(result: str) -> list[int]:
"""Extract integer values from command's return value."""
return [int(s) for s in re.findall(r"\d+", result)]
@patch("hc.accounts.management.commands.sendinactivitynotices.time.sleep", MOCK_SLEEP)
class SendInactivityNoticesTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
# Make alice eligible for notice -- signed up more than 1 year ago
self.alice.date_joined = now() - td(days=500)
self.alice.save()
self.profile.sms_limit = 5
self.profile.save()
# remove members from alice's project
self.project.member_set.all().delete()
def test_it_sends_notice(self) -> None:
cmd = Command(stdout=Mock())
result = cmd.handle()
self.assertEqual(counts(result), [1, 0, 0])
self.profile.refresh_from_db()
self.assertTrue(self.profile.deletion_notice_date)
email = mail.outbox[0]
self.assertEqual(email.subject, "Inactive Account Notification")
def test_it_checks_last_login(self) -> None:
# alice has logged in recently:
self.alice.last_login = now() - td(days=15)
self.alice.save()
result = Command(stdout=Mock()).handle()
self.assertEqual(counts(result), [0, 0, 0])
self.profile.refresh_from_db()
self.assertIsNone(self.profile.deletion_notice_date)
def test_it_checks_date_joined(self) -> None:
# alice signed up recently:
self.alice.date_joined = now() - td(days=15)
self.alice.save()
result = Command(stdout=Mock()).handle()
self.assertEqual(counts(result), [0, 0, 0])
self.profile.refresh_from_db()
self.assertIsNone(self.profile.deletion_notice_date)
def test_it_checks_deletion_notice_date(self) -> None:
# alice has already received a deletion notice
self.profile.deletion_notice_date = now() - td(days=15)
self.profile.save()
result = Command(stdout=Mock()).handle()
self.assertEqual(counts(result), [0, 0, 0])
def test_it_checks_subscription(self) -> None:
# alice has a subscription
Subscription.objects.create(user=self.alice, subscription_id="abc123")
result = Command(stdout=Mock()).handle()
self.assertEqual(counts(result), [0, 0, 0])
self.profile.refresh_from_db()
self.assertIsNone(self.profile.deletion_notice_date)
def test_it_checks_recently_active_team_members(self) -> None:
# bob has access to alice's project
Member.objects.create(user=self.bob, project=self.project)
self.bobs_profile.last_active_date = now()
self.bobs_profile.save()
result = Command(stdout=Mock()).handle()
self.assertEqual(counts(result), [0, 1, 0])
self.profile.refresh_from_db()
self.assertIsNone(self.profile.deletion_notice_date)
def test_it_checks_recently_logged_in_team_members(self) -> None:
# bob has access to alice's project
Member.objects.create(user=self.bob, project=self.project)
self.bob.last_login = now()
self.bob.save()
result = Command(stdout=Mock()).handle()
self.assertEqual(counts(result), [0, 1, 0])
self.profile.refresh_from_db()
self.assertIsNone(self.profile.deletion_notice_date)
def test_it_checks_recently_signed_up_team_members(self) -> None:
# bob has access to alice's project
Member.objects.create(user=self.bob, project=self.project)
result = Command(stdout=Mock()).handle()
self.assertEqual(counts(result), [0, 1, 0])
self.profile.refresh_from_db()
self.assertIsNone(self.profile.deletion_notice_date)
def test_it_ignores_inactive_team_members(self) -> None:
# bob has access to alice's project, but is inactive
Member.objects.create(user=self.bob, project=self.project)
self.bob.date_joined = now() - td(days=366)
self.bob.save()
cmd = Command(stdout=Mock())
result = cmd.handle()
# both alice and bob are eligible for deletion
self.assertEqual(counts(result), [2, 0, 0])
self.profile.refresh_from_db()
self.assertTrue(self.profile.deletion_notice_date)
self.bobs_profile.refresh_from_db()
self.assertTrue(self.bobs_profile.deletion_notice_date)
def test_it_checks_recent_pings(self) -> None:
check = Check.objects.create(project=self.project)
Ping.objects.create(owner=check)
result = Command(stdout=Mock()).handle()
self.assertEqual(counts(result), [0, 0, 1])
self.profile.refresh_from_db()
self.assertIsNone(self.profile.deletion_notice_date)
def test_it_checks_last_active_date(self) -> None:
# alice has been browsing the site recently
self.profile.last_active_date = now() - td(days=15)
self.profile.save()
result = Command(stdout=Mock()).handle()
self.assertEqual(counts(result), [0, 0, 0])

View file

@ -1,46 +1,40 @@
from __future__ import annotations
from hc.test import BaseTestCase
class SetPasswordTestCase(BaseTestCase):
def test_it_shows_form(self):
token = self.profile.prepare_token("set-password")
def test_it_requires_sudo_mode(self) -> None:
self.client.login(username="alice@example.org", password="password")
r = self.client.get("/accounts/set_password/%s/" % token)
self.assertEqual(r.status_code, 200)
r = self.client.get("/accounts/set_password/")
self.assertContains(r, "We have sent a confirmation code")
def test_it_shows_form(self) -> None:
self.client.login(username="alice@example.org", password="password")
self.set_sudo_flag()
r = self.client.get("/accounts/set_password/")
self.assertContains(r, "Please pick a password")
def test_it_checks_token(self):
self.profile.prepare_token("set-password")
def test_it_sets_password(self) -> None:
self.client.login(username="alice@example.org", password="password")
self.set_sudo_flag()
# GET
r = self.client.get("/accounts/set_password/invalid-token/")
self.assertEqual(r.status_code, 400)
# POST
r = self.client.post("/accounts/set_password/invalid-token/")
self.assertEqual(r.status_code, 400)
def test_it_sets_password(self):
token = self.profile.prepare_token("set-password")
self.client.login(username="alice@example.org", password="password")
payload = {"password": "correct horse battery staple"}
r = self.client.post("/accounts/set_password/%s/" % token, payload)
self.assertEqual(r.status_code, 302)
r = self.client.post("/accounts/set_password/", payload)
self.assertRedirects(r, "/accounts/profile/")
old_password = self.alice.password
self.alice.refresh_from_db()
self.assertNotEqual(self.alice.password, old_password)
def test_post_checks_length(self):
token = self.profile.prepare_token("set-password")
def test_post_checks_length(self) -> None:
self.client.login(username="alice@example.org", password="password")
self.set_sudo_flag()
payload = {"password": "abc"}
r = self.client.post("/accounts/set_password/%s/" % token, payload)
r = self.client.post("/accounts/set_password/", payload)
self.assertEqual(r.status_code, 200)
old_password = self.alice.password

View file

@ -1,26 +1,42 @@
from __future__ import annotations
from django.conf import settings
from django.contrib.auth.models import User
from django.core import mail
from django.test import TestCase
from django.test.utils import override_settings
from hc.accounts.models import Project
from hc.api.models import Channel, Check
from django.conf import settings
from hc.accounts.models import Profile, Project
from hc.api.models import Channel, Check, TokenBucket
@override_settings(REGISTRATION_OPEN=True)
class SignupTestCase(TestCase):
def test_it_sends_link(self):
form = {"identity": "alice@example.org"}
@override_settings(USE_PAYMENTS=False, SESSION_COOKIE_SECURE=False)
def test_it_works(self) -> None:
form = {"identity": "alice@example.org", "tz": "Europe/Riga"}
r = self.client.post("/accounts/signup/", form)
self.assertContains(r, "Account created")
self.assertContains(r, "check your email")
self.assertEqual(r.cookies["auto-login"].value, "1")
self.assertEqual(r.cookies["auto-login"]["samesite"], "Lax")
self.assertTrue(r.cookies["auto-login"]["httponly"])
self.assertFalse(r.cookies["auto-login"]["secure"])
# An user should have been created
user = User.objects.get()
# A profile should have been created
profile = Profile.objects.get()
self.assertEqual(profile.check_limit, 10000)
self.assertEqual(profile.sms_limit, 10000)
self.assertEqual(profile.call_limit, 10000)
self.assertEqual(profile.tz, "Europe/Riga")
# And email sent
self.assertEqual(len(mail.outbox), 1)
subject = "Log in to %s" % settings.SITE_NAME
self.assertEqual(mail.outbox[0].subject, subject)
self.assertEqual(mail.outbox[0].subject, f"Log in to {settings.SITE_NAME}")
# A project should have been created
project = Project.objects.get()
@ -30,36 +46,110 @@ class SignupTestCase(TestCase):
# And check should be associated with the new user
check = Check.objects.get()
self.assertEqual(check.name, "My First Check")
self.assertEqual(check.slug, "my-first-check")
self.assertEqual(check.project, project)
# A channel should have been created
channel = Channel.objects.get()
self.assertEqual(channel.project, project)
@override_settings(SESSION_COOKIE_SECURE=True)
def test_it_sets_secure_autologin_cookie(self) -> None:
form = {"identity": "alice@example.org", "tz": "Europe/Riga"}
r = self.client.post("/accounts/signup/", form)
self.assertTrue(r.cookies["auto-login"]["secure"])
def test_it_requires_unauthenticated_user(self) -> None:
self.alice = User(username="alice", email="alice@example.org")
self.alice.set_password("password")
self.alice.save()
self.client.login(username="alice@example.org", password="password")
form = {"identity": "alice@example.org", "tz": "Europe/Riga"}
r = self.client.post("/accounts/signup/", form)
self.assertEqual(r.status_code, 403)
@override_settings(USE_PAYMENTS=True)
def test_it_sets_limits(self) -> None:
form = {"identity": "alice@example.org", "tz": ""}
self.client.post("/accounts/signup/", form)
profile = Profile.objects.get()
self.assertEqual(profile.check_limit, 20)
self.assertEqual(profile.sms_limit, 5)
self.assertEqual(profile.call_limit, 0)
@override_settings(REGISTRATION_OPEN=False)
def test_it_obeys_registration_open(self):
form = {"identity": "dan@example.org"}
def test_it_obeys_registration_open(self) -> None:
form = {"identity": "dan@example.org", "tz": ""}
r = self.client.post("/accounts/signup/", form)
self.assertEqual(r.status_code, 403)
def test_it_ignores_case(self):
form = {"identity": "ALICE@EXAMPLE.ORG"}
def test_it_ignores_case(self) -> None:
form = {"identity": "ALICE@EXAMPLE.ORG", "tz": ""}
self.client.post("/accounts/signup/", form)
# There should be exactly one user:
q = User.objects.filter(email="alice@example.org")
self.assertTrue(q.exists)
def test_it_checks_for_existing_users(self):
def test_it_handles_existing_users(self) -> None:
alice = User(username="alice", email="alice@example.org")
alice.save()
form = {"identity": "alice@example.org"}
form = {"identity": "alice@example.org", "tz": ""}
r = self.client.post("/accounts/signup/", form)
self.assertContains(r, "already exists")
# It should send the same response and cookies as in normal signup
self.assertContains(r, "check your email")
self.assertEqual(r.cookies["auto-login"].value, "1")
def test_it_checks_syntax(self):
form = {"identity": "alice at example org"}
# There should still be a single user
self.assertEqual(User.objects.count(), 1)
# It should send a normal sign-in email
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, f"Log in to {settings.SITE_NAME}")
def test_it_checks_syntax(self) -> None:
form = {"identity": "alice at example org", "tz": ""}
r = self.client.post("/accounts/signup/", form)
self.assertContains(r, "Enter a valid email address")
def test_it_checks_length(self) -> None:
aaa = "a" * 300
form = {"identity": f"alice+{aaa}@example.org", "tz": ""}
r = self.client.post("/accounts/signup/", form)
self.assertContains(r, "Address is too long.")
self.assertFalse(User.objects.exists())
@override_settings(USE_PAYMENTS=False)
def test_it_ignores_bad_tz(self) -> None:
form = {"identity": "alice@example.org", "tz": "Foo/Bar"}
r = self.client.post("/accounts/signup/", form)
self.assertContains(r, "check your email")
profile = Profile.objects.get()
self.assertEqual(profile.tz, "UTC")
def test_it_rate_limits_client_ips(self) -> None:
obj = TokenBucket(value="auth-ip-127.0.0.1")
obj.tokens = 0
obj.save()
form = {"identity": "alice@example.org", "tz": ""}
r = self.client.post("/accounts/signup/", form)
self.assertContains(r, "please try later")
def test_rate_limiter_uses_x_forwarded_for(self) -> None:
obj = TokenBucket(value="auth-ip-127.0.0.2")
obj.tokens = 0
obj.save()
form = {"identity": "alice@example.org", "tz": ""}
xff = "127.0.0.2:1234,127.0.0.3"
r = self.client.post("/accounts/signup/", form, HTTP_X_FORWARDED_FOR=xff)
self.assertContains(r, "please try later")

View file

@ -0,0 +1,22 @@
from __future__ import annotations
from django.test.utils import override_settings
from hc.test import BaseTestCase
@override_settings(REGISTRATION_OPEN=True)
class SignupCsrfTestCase(BaseTestCase):
def test_it_works(self) -> None:
r = self.client.get("/accounts/signup/csrf/")
self.assertTrue(r.cookies["csrftoken"])
def test_it_requires_unauthenticated_user(self) -> None:
self.client.login(username="alice@example.org", password="password")
r = self.client.get("/accounts/signup/csrf/")
self.assertEqual(r.status_code, 403)
@override_settings(REGISTRATION_OPEN=False)
def test_it_requires_registration_open(self) -> None:
r = self.client.get("/accounts/signup/csrf/")
self.assertEqual(r.status_code, 403)

View file

@ -0,0 +1,75 @@
from __future__ import annotations
from django.core import mail
from django.core.signing import TimestampSigner
from hc.accounts.models import Credential
from hc.api.models import TokenBucket
from hc.test import BaseTestCase
class SudoModeTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
self.c = Credential.objects.create(user=self.alice, name="Alices Key")
self.url = "/accounts/set_password/"
def test_it_sends_code(self) -> None:
self.client.login(username="alice@example.org", password="password")
r = self.client.get(self.url)
self.assertContains(r, "We have sent a confirmation code")
# A code should have been sent
self.assertEqual(len(mail.outbox), 1)
email = mail.outbox[0]
self.assertEqual(email.to[0], "alice@example.org")
self.assertIn("Confirmation code", email.subject)
def test_it_accepts_code(self) -> None:
self.client.login(username="alice@example.org", password="password")
session = self.client.session
session["sudo_code"] = TimestampSigner().sign("123456")
session.save()
r = self.client.post(self.url, {"sudo_code": "123456"})
self.assertRedirects(r, self.url)
# sudo mode should now be active
self.assertIn("sudo", self.client.session)
def test_it_rejects_incorrect_code(self) -> None:
self.client.login(username="alice@example.org", password="password")
session = self.client.session
session["sudo_code"] = TimestampSigner().sign("123456")
session.save()
r = self.client.post(self.url, {"sudo_code": "000000"})
self.assertContains(r, "Not a valid code.")
# sudo mode should *not* be active
self.assertNotIn("sudo", self.client.session)
def test_it_passes_through_if_sudo_mode_is_active(self) -> None:
self.client.login(username="alice@example.org", password="password")
session = self.client.session
session["sudo"] = TimestampSigner().sign("active")
session.save()
r = self.client.get(self.url)
self.assertContains(r, "Please pick a password")
def test_it_uses_rate_limiting(self) -> None:
self.client.login(username="alice@example.org", password="password")
obj = TokenBucket(value=f"sudo-{self.alice.id}")
obj.tokens = 0
obj.save()
r = self.client.get(self.url)
self.assertContains(r, "Too Many Requests")

View file

@ -1,10 +1,13 @@
from __future__ import annotations
from django.contrib.auth.models import User
from django.test import TestCase
from hc.accounts.models import Profile
class TeamAccessMiddlewareTestCase(TestCase):
def test_it_handles_missing_profile(self):
def test_it_handles_missing_profile(self) -> None:
user = User(username="ned", email="ned@example.org")
user.set_password("password")
user.save()

View file

@ -0,0 +1,172 @@
from __future__ import annotations
from django.core import mail
from django.utils.timezone import now
from hc.accounts.models import Member
from hc.api.models import Check
from hc.test import BaseTestCase
class TransferProjectTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
Check.objects.create(project=self.project)
self.url = "/projects/%s/settings/" % self.project.code
def test_transfer_project_works(self) -> None:
self.client.login(username="alice@example.org", password="password")
form = {"transfer_project": "1", "email": "bob@example.org"}
r = self.client.post(self.url, form)
self.assertContains(r, "Transfer initiated!")
self.bobs_membership.refresh_from_db()
self.assertIsNotNone(self.bobs_membership.transfer_request_date)
# Bob should receive an email notification
self.assertEqual(len(mail.outbox), 1)
body = mail.outbox[0].body
self.assertTrue("/?next=" + self.url in body)
def test_transfer_project_checks_ownership(self) -> None:
self.client.login(username="bob@example.org", password="password")
form = {"transfer_project": "1", "email": "bob@example.org"}
r = self.client.post(self.url, form)
self.assertEqual(r.status_code, 403)
def test_transfer_project_checks_membership(self) -> None:
self.client.login(username="alice@example.org", password="password")
form = {"transfer_project": "1", "email": "charlie@example.org"}
r = self.client.post(self.url, form)
self.assertEqual(r.status_code, 400)
def test_cancel_works(self) -> None:
self.bobs_membership.transfer_request_date = now()
self.bobs_membership.save()
self.client.login(username="alice@example.org", password="password")
r = self.client.post(self.url, {"cancel_transfer": "1"})
self.assertContains(r, "Transfer cancelled!")
self.bobs_membership.refresh_from_db()
self.assertIsNone(self.bobs_membership.transfer_request_date)
def test_cancel_checks_ownership(self) -> None:
self.bobs_membership.transfer_request_date = now()
self.bobs_membership.save()
self.client.login(username="bob@example.org", password="password")
r = self.client.post(self.url, {"cancel_transfer": "1"})
self.assertEqual(r.status_code, 403)
self.bobs_membership.refresh_from_db()
self.assertIsNotNone(self.bobs_membership.transfer_request_date)
def test_it_shows_transfer_request(self) -> None:
self.bobs_membership.transfer_request_date = now()
self.bobs_membership.save()
self.client.login(username="bob@example.org", password="password")
r = self.client.get(self.url)
self.assertContains(r, "would like to transfer")
self.assertNotContains(r, "upgrade your account first")
def test_it_shows_transfer_request_with_limit_notice(self) -> None:
self.bobs_membership.transfer_request_date = now()
self.bobs_membership.save()
self.bobs_profile.check_limit = 0
self.bobs_profile.save()
self.client.login(username="bob@example.org", password="password")
r = self.client.get(self.url)
self.assertContains(r, "upgrade your account first")
def test_accept_works(self) -> None:
self.bobs_membership.transfer_request_date = now()
self.bobs_membership.save()
self.client.login(username="bob@example.org", password="password")
r = self.client.post(self.url, {"accept_transfer": "1"})
self.assertContains(r, "You are now the owner of this project!")
self.project.refresh_from_db()
# Bob should now be the owner
self.assertEqual(self.project.owner, self.bob)
# Alice, the previous owner, should now be a member
m = Member.objects.get(project=self.project, user=self.alice)
self.assertEqual(m.role, Member.Role.REGULAR)
def test_accept_requires_a_transfer_request(self) -> None:
self.client.login(username="bob@example.org", password="password")
r = self.client.post(self.url, {"accept_transfer": "1"})
self.assertEqual(r.status_code, 403)
self.project.refresh_from_db()
# Alice should still be the owner
self.assertEqual(self.project.owner, self.alice)
def test_only_the_proposed_owner_can_accept(self) -> None:
self.bobs_membership.transfer_request_date = now()
self.bobs_membership.save()
self.client.login(username="alice@example.org", password="password")
r = self.client.post(self.url, {"accept_transfer": "1"})
self.assertEqual(r.status_code, 403)
def test_it_checks_limits(self) -> None:
self.bobs_membership.transfer_request_date = now()
self.bobs_membership.save()
self.bobs_profile.check_limit = 0
self.bobs_profile.save()
self.client.login(username="bob@example.org", password="password")
r = self.client.post(self.url, {"accept_transfer": "1"})
self.assertEqual(r.status_code, 400)
def test_reject_works(self) -> None:
self.bobs_membership.transfer_request_date = now()
self.bobs_membership.save()
self.client.login(username="bob@example.org", password="password")
r = self.client.post(self.url, {"reject_transfer": "1"})
self.assertEqual(r.status_code, 200)
self.project.refresh_from_db()
# Alice should still be the owner
self.assertEqual(self.project.owner, self.alice)
# The transfer_request_date should be cleared out
self.bobs_membership.refresh_from_db()
self.assertIsNone(self.bobs_membership.transfer_request_date)
def test_only_the_proposed_owner_can_reject(self) -> None:
self.bobs_membership.transfer_request_date = now()
self.bobs_membership.save()
self.client.login(username="alice@example.org", password="password")
r = self.client.post(self.url, {"reject_transfer": "1"})
self.assertEqual(r.status_code, 403)
def test_readonly_user_can_accept(self) -> None:
self.bobs_membership.transfer_request_date = now()
self.bobs_membership.role = "r"
self.bobs_membership.save()
self.client.login(username="bob@example.org", password="password")
self.client.post(self.url, {"accept_transfer": "1"})
self.project.refresh_from_db()
# Bob should now be the owner
self.assertEqual(self.project.owner, self.bob)
# Alice, the previous owner, should now be a *regular* member
m = Member.objects.get(user=self.alice, project=self.project)
self.assertEqual(m.role, "w")

View file

@ -1,12 +1,17 @@
from __future__ import annotations
import time
from datetime import timedelta as td
from unittest.mock import patch
from django.core import signing
from django.utils.timezone import now
from hc.test import BaseTestCase
class UnsubscribeReportsTestCase(BaseTestCase):
def test_it_unsubscribes(self):
def test_it_unsubscribes(self) -> None:
self.profile.next_report_date = now()
self.profile.nag_period = td(hours=1)
self.profile.next_nag_date = now()
@ -15,31 +20,46 @@ class UnsubscribeReportsTestCase(BaseTestCase):
sig = signing.TimestampSigner(salt="reports").sign("alice")
url = "/accounts/unsubscribe_reports/%s/" % sig
r = self.client.get(url)
r = self.client.post(url)
self.assertContains(r, "Unsubscribed")
self.profile.refresh_from_db()
self.assertFalse(self.profile.reports_allowed)
self.assertEqual(self.profile.reports, "off")
self.assertIsNone(self.profile.next_report_date)
self.assertEqual(self.profile.nag_period.total_seconds(), 0)
self.assertIsNone(self.profile.next_nag_date)
def test_bad_signature_gets_rejected(self):
def test_bad_signature_gets_rejected(self) -> None:
url = "/accounts/unsubscribe_reports/invalid/"
r = self.client.get(url)
self.assertContains(r, "Incorrect Link")
def test_post_works(self):
def test_it_serves_confirmation_form(self) -> None:
sig = signing.TimestampSigner(salt="reports").sign("alice")
url = "/accounts/unsubscribe_reports/%s/" % sig
r = self.client.get(url)
self.assertContains(r, "Please press the button below")
self.assertNotContains(r, "submit()")
def test_aged_signature_autosubmits(self) -> None:
with patch("django.core.signing.time") as mock_time:
mock_time.time.return_value = time.time() - 301
signer = signing.TimestampSigner(salt="reports")
sig = signer.sign("alice")
url = "/accounts/unsubscribe_reports/%s/" % sig
r = self.client.get(url)
self.assertContains(r, "Please press the button below")
self.assertContains(r, "submit()")
def test_it_handles_missing_user(self) -> None:
self.alice.delete()
sig = signing.TimestampSigner(salt="reports").sign("alice")
url = "/accounts/unsubscribe_reports/%s/" % sig
r = self.client.post(url)
self.assertContains(r, "Unsubscribed")
def test_it_serves_confirmation_form(self):
sig = signing.TimestampSigner(salt="reports").sign("alice")
url = "/accounts/unsubscribe_reports/%s/?ask=1" % sig
r = self.client.get(url)
self.assertContains(r, "Please press the button below")

View file

@ -1,26 +1,51 @@
from __future__ import annotations
from django.urls import path
from hc.accounts import views
urlpatterns = [
path("login/", views.login, name="hc-login"),
path("logout/", views.logout, name="hc-logout"),
path("signup/", views.signup, name="hc-signup"),
path("login_link_sent/", views.login_link_sent, name="hc-login-link-sent"),
path("link_sent/", views.link_sent, name="hc-link-sent"),
path("projects/add/", views.add_project, name="hc-add-project"),
path("projects/<uuid:code>/settings/", views.project, name="hc-project-settings"),
path(
"check_token/<slug:username>/<slug:token>/",
"projects/<uuid:code>/remove/", views.remove_project, name="hc-remove-project"
),
path("accounts/login/", views.login, name="hc-login"),
path("accounts/login/two_factor/", views.login_webauthn, name="hc-login-webauthn"),
path("accounts/login/two_factor/totp/", views.login_totp, name="hc-login-totp"),
path("accounts/logout/", views.logout, name="hc-logout"),
path("accounts/signup/csrf/", views.signup_csrf),
path("accounts/signup/", views.signup, name="hc-signup"),
path("accounts/login_link_sent/", views.login_link_sent, name="hc-login-link-sent"),
path(
"accounts/check_token/<str:username>/<str:token>/",
views.check_token,
name="hc-check-token",
),
path("profile/", views.profile, name="hc-profile"),
path("profile/notifications/", views.notifications, name="hc-notifications"),
path("close/", views.close, name="hc-close"),
path("accounts/profile/", views.profile, name="hc-profile"),
path("accounts/profile/appearance/", views.appearance, name="hc-appearance"),
path(
"unsubscribe_reports/<str:username>/",
"accounts/profile/notifications/", views.notifications, name="hc-notifications"
),
path("accounts/close/", views.close, name="hc-close"),
path(
"accounts/unsubscribe_reports/<str:signed_username>/",
views.unsubscribe_reports,
name="hc-unsubscribe-reports",
),
path("set_password/<slug:token>/", views.set_password, name="hc-set-password"),
path("change_email/done/", views.change_email_done, name="hc-change-email-done"),
path("change_email/<slug:token>/", views.change_email, name="hc-change-email"),
path("accounts/set_password/", views.set_password, name="hc-set-password"),
path("accounts/change_email/", views.change_email, name="hc-change-email"),
path(
"accounts/change_email/<str:signed_payload>/",
views.change_email_verify,
name="hc-change-email-verify",
),
path("accounts/two_factor/webauthn/", views.add_webauthn, name="hc-add-webauthn"),
path("accounts/two_factor/totp/", views.add_totp, name="hc-add-totp"),
path("accounts/two_factor/totp/remove/", views.remove_totp, name="hc-remove-totp"),
path(
"accounts/two_factor/<uuid:code>/remove/",
views.remove_credential,
name="hc-remove-credential",
),
]

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more