2022-10-17 13:52:15 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2023-11-17 14:06:39 +00:00
|
|
|
import logging
|
2022-10-17 13:52:15 +00:00
|
|
|
import time
|
2017-10-14 13:03:56 +00:00
|
|
|
from datetime import timedelta as td
|
2022-06-19 07:10:57 +00:00
|
|
|
from secrets import token_urlsafe
|
2022-05-26 18:39:53 +00:00
|
|
|
from urllib.parse import urlparse
|
2023-09-06 07:02:50 +00:00
|
|
|
from uuid import UUID, uuid4
|
2015-06-11 19:12:09 +00:00
|
|
|
|
2022-10-17 13:52:15 +00:00
|
|
|
import pyotp
|
|
|
|
import segno
|
2017-02-24 13:58:11 +00:00
|
|
|
from django.conf import settings
|
2015-12-15 00:27:24 +00:00
|
|
|
from django.contrib import messages
|
2022-10-17 13:52:15 +00:00
|
|
|
from django.contrib.auth import authenticate
|
2015-11-02 21:55:33 +00:00
|
|
|
from django.contrib.auth import login as auth_login
|
|
|
|
from django.contrib.auth import logout as auth_logout
|
2022-10-17 13:52:15 +00:00
|
|
|
from django.contrib.auth import update_session_auth_hash
|
2015-12-15 00:27:24 +00:00
|
|
|
from django.contrib.auth.decorators import login_required
|
2015-06-11 19:12:09 +00:00
|
|
|
from django.contrib.auth.models import User
|
2022-10-17 13:52:15 +00:00
|
|
|
from django.core.signing import BadSignature, SignatureExpired, TimestampSigner
|
|
|
|
from django.db import transaction
|
2022-12-22 09:39:20 +00:00
|
|
|
from django.db.models.functions import Lower
|
2023-09-05 15:31:35 +00:00
|
|
|
from django.http import (
|
|
|
|
HttpRequest,
|
|
|
|
HttpResponse,
|
|
|
|
HttpResponseBadRequest,
|
|
|
|
HttpResponseForbidden,
|
|
|
|
)
|
2023-02-14 12:20:27 +00:00
|
|
|
from django.middleware import csrf
|
2019-01-29 17:57:18 +00:00
|
|
|
from django.shortcuts import get_object_or_404, redirect, render
|
2022-10-17 13:52:15 +00:00
|
|
|
from django.urls import Resolver404, resolve, reverse
|
2017-10-14 13:03:56 +00:00
|
|
|
from django.utils.timezone import now
|
2023-02-14 12:20:27 +00:00
|
|
|
from django.views.decorators.csrf import csrf_exempt
|
2023-02-20 08:09:16 +00:00
|
|
|
from django.views.decorators.debug import sensitive_post_parameters
|
2017-03-16 17:39:30 +00:00
|
|
|
from django.views.decorators.http import require_POST
|
2022-10-17 13:52:15 +00:00
|
|
|
|
2020-04-13 09:16:39 +00:00
|
|
|
from hc.accounts import forms
|
2020-11-13 14:23:28 +00:00
|
|
|
from hc.accounts.decorators import require_sudo_mode
|
2023-10-21 15:16:48 +00:00
|
|
|
from hc.accounts.http import AuthenticatedHttpRequest
|
2022-10-17 13:52:15 +00:00
|
|
|
from hc.accounts.models import Credential, Member, Profile, Project
|
2019-04-25 18:28:40 +00:00
|
|
|
from hc.api.models import Channel, Check, TokenBucket
|
2022-12-01 14:12:32 +00:00
|
|
|
from hc.lib.tz import all_timezones
|
2022-06-19 08:30:37 +00:00
|
|
|
from hc.lib.webauthn import CreateHelper, GetHelper
|
2022-10-17 13:52:15 +00:00
|
|
|
from hc.payments.models import Subscription
|
2015-06-11 19:12:09 +00:00
|
|
|
|
2023-11-17 14:06:39 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
2020-08-26 13:38:29 +00:00
|
|
|
POST_LOGIN_ROUTES = (
|
2019-05-15 11:27:50 +00:00
|
|
|
"hc-checks",
|
|
|
|
"hc-details",
|
|
|
|
"hc-log",
|
2020-09-01 09:56:35 +00:00
|
|
|
"hc-channels",
|
2019-05-15 11:27:50 +00:00
|
|
|
"hc-add-slack",
|
|
|
|
"hc-add-pushover",
|
2020-02-27 13:52:00 +00:00
|
|
|
"hc-add-telegram",
|
2020-04-13 12:04:59 +00:00
|
|
|
"hc-project-settings",
|
2021-03-04 13:50:32 +00:00
|
|
|
"hc-uncloak",
|
2019-05-15 11:27:50 +00:00
|
|
|
)
|
2018-11-28 20:06:12 +00:00
|
|
|
|
|
|
|
|
2023-09-05 15:31:35 +00:00
|
|
|
def _allow_redirect(redirect_url: str | None) -> bool:
|
2020-02-27 13:52:00 +00:00
|
|
|
if not redirect_url:
|
|
|
|
return False
|
|
|
|
|
|
|
|
parsed = urlparse(redirect_url)
|
2021-08-06 10:34:40 +00:00
|
|
|
if parsed.netloc:
|
|
|
|
# Allow redirects only to relative URLs
|
|
|
|
return False
|
|
|
|
|
2018-11-28 20:06:12 +00:00
|
|
|
try:
|
2020-02-27 13:52:00 +00:00
|
|
|
match = resolve(parsed.path)
|
2018-11-28 20:06:12 +00:00
|
|
|
except Resolver404:
|
|
|
|
return False
|
|
|
|
|
2020-08-26 13:38:29 +00:00
|
|
|
return match.url_name in POST_LOGIN_ROUTES
|
2018-11-26 15:32:23 +00:00
|
|
|
|
2015-06-11 19:12:09 +00:00
|
|
|
|
2023-09-05 15:31:35 +00:00
|
|
|
def _make_user(email: str, tz: str | None = None, with_project: bool = True) -> User:
|
2023-09-06 07:02:50 +00:00
|
|
|
username = str(uuid4())[:30]
|
2015-06-18 15:39:03 +00:00
|
|
|
user = User(username=username, email=email)
|
2016-01-04 20:39:49 +00:00
|
|
|
user.set_unusable_password()
|
2015-06-18 15:39:03 +00:00
|
|
|
user.save()
|
2015-06-12 17:49:35 +00:00
|
|
|
|
2019-01-29 17:16:52 +00:00
|
|
|
project = None
|
|
|
|
if with_project:
|
|
|
|
project = Project(owner=user)
|
|
|
|
project.badge_key = user.username
|
|
|
|
project.save()
|
|
|
|
|
|
|
|
check = Check(project=project)
|
2023-06-14 13:52:45 +00:00
|
|
|
check.name = "My First Check"
|
|
|
|
check.slug = "my-first-check"
|
2019-01-29 17:16:52 +00:00
|
|
|
check.save()
|
|
|
|
|
|
|
|
channel = Channel(project=project)
|
|
|
|
channel.kind = "email"
|
|
|
|
channel.value = email
|
|
|
|
channel.email_verified = True
|
|
|
|
channel.save()
|
|
|
|
|
|
|
|
channel.checks.add(check)
|
2019-01-12 14:40:21 +00:00
|
|
|
|
2017-02-24 13:58:11 +00:00
|
|
|
# Ensure a profile gets created
|
2021-05-24 11:38:12 +00:00
|
|
|
profile = Profile.objects.for_user(user)
|
|
|
|
if tz:
|
|
|
|
profile.tz = tz
|
|
|
|
profile.save()
|
2016-05-09 12:35:13 +00:00
|
|
|
|
2018-06-14 20:42:39 +00:00
|
|
|
return user
|
2015-06-30 21:28:13 +00:00
|
|
|
|
|
|
|
|
2023-09-05 15:31:35 +00:00
|
|
|
def _redirect_after_login(request: HttpRequest) -> HttpResponse:
|
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 14:54:45 +00:00
|
|
|
"""Redirect to the URL indicated in ?next= query parameter."""
|
2018-11-26 15:32:23 +00:00
|
|
|
|
|
|
|
redirect_url = request.GET.get("next")
|
2023-09-05 15:31:35 +00:00
|
|
|
if redirect_url and _allow_redirect(redirect_url):
|
2018-11-26 15:32:23 +00:00
|
|
|
return redirect(redirect_url)
|
|
|
|
|
2023-09-05 15:31:35 +00:00
|
|
|
assert isinstance(request.user, User)
|
2019-01-29 17:05:32 +00:00
|
|
|
if request.user.project_set.count() == 1:
|
2023-09-05 15:31:35 +00:00
|
|
|
project = request.user.project_set.get()
|
2019-01-29 17:05:32 +00:00
|
|
|
return redirect("hc-checks", project.code)
|
|
|
|
|
2019-01-29 08:59:10 +00:00
|
|
|
return redirect("hc-index")
|
2018-11-26 15:32:23 +00:00
|
|
|
|
|
|
|
|
2023-09-05 15:31:35 +00:00
|
|
|
def _check_2fa(request: HttpRequest, user: User) -> HttpResponse:
|
2021-07-30 11:09:16 +00:00
|
|
|
have_keys = user.credentials.exists()
|
2021-08-18 07:32:10 +00:00
|
|
|
profile = Profile.objects.for_user(user)
|
|
|
|
if have_keys or profile.totp:
|
2020-11-19 14:21:31 +00:00
|
|
|
# We have verified user's password or token, and now must
|
|
|
|
# verify their security key. We store the following in user's session:
|
|
|
|
# - user.id, to look up the user in the login_webauthn view
|
|
|
|
# - user.email, to make sure email was not changed between the auth steps
|
|
|
|
# - timestamp, to limit the max time between the auth steps
|
|
|
|
request.session["2fa_user"] = [user.id, user.email, int(time.time())]
|
2020-11-15 19:39:49 +00:00
|
|
|
|
2021-07-30 11:09:16 +00:00
|
|
|
if have_keys:
|
|
|
|
path = reverse("hc-login-webauthn")
|
|
|
|
else:
|
|
|
|
path = reverse("hc-login-totp")
|
|
|
|
|
2020-11-15 19:39:49 +00:00
|
|
|
redirect_url = request.GET.get("next")
|
|
|
|
if _allow_redirect(redirect_url):
|
|
|
|
path += "?next=%s" % redirect_url
|
|
|
|
|
|
|
|
return redirect(path)
|
|
|
|
|
|
|
|
auth_login(request, user)
|
|
|
|
return _redirect_after_login(request)
|
|
|
|
|
|
|
|
|
2023-09-05 15:31:35 +00:00
|
|
|
def _new_key(nbytes: int = 24) -> str:
|
2021-09-09 11:55:17 +00:00
|
|
|
while True:
|
|
|
|
candidate = token_urlsafe(nbytes)
|
|
|
|
if candidate[0] not in "-_" and candidate[-1] not in "-_":
|
|
|
|
return candidate
|
|
|
|
|
|
|
|
|
2023-09-05 15:31:35 +00:00
|
|
|
def _set_autologin_cookie(response: HttpResponse) -> None:
|
2023-02-15 07:17:09 +00:00
|
|
|
# check_token looks for this cookie to decide if
|
|
|
|
# it needs to do the extra POST step.
|
|
|
|
response.set_cookie(
|
|
|
|
"auto-login",
|
|
|
|
"1",
|
|
|
|
max_age=300,
|
|
|
|
httponly=True,
|
|
|
|
samesite="Lax",
|
2023-02-15 07:20:00 +00:00
|
|
|
secure=bool(settings.SESSION_COOKIE_SECURE),
|
2023-02-15 07:17:09 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2023-02-20 08:09:16 +00:00
|
|
|
@sensitive_post_parameters()
|
2023-09-05 15:31:35 +00:00
|
|
|
def login(request: HttpRequest) -> HttpResponse:
|
2020-04-13 09:16:39 +00:00
|
|
|
form = forms.PasswordLoginForm()
|
2023-10-22 07:45:06 +00:00
|
|
|
magic_form = forms.EmailLoginForm()
|
2019-05-15 11:27:50 +00:00
|
|
|
if request.method == "POST":
|
2018-10-09 13:12:02 +00:00
|
|
|
if request.POST.get("action") == "login":
|
2020-04-13 09:16:39 +00:00
|
|
|
form = forms.PasswordLoginForm(request.POST)
|
2018-10-09 13:12:02 +00:00
|
|
|
if form.is_valid():
|
2023-09-05 15:31:35 +00:00
|
|
|
assert isinstance(form.user, User)
|
2020-11-15 19:39:49 +00:00
|
|
|
return _check_2fa(request, form.user)
|
2018-10-09 13:12:02 +00:00
|
|
|
|
|
|
|
else:
|
2023-01-23 12:35:45 +00:00
|
|
|
magic_form = forms.EmailLoginForm(request)
|
2018-10-09 13:12:02 +00:00
|
|
|
if magic_form.is_valid():
|
2018-11-26 15:32:23 +00:00
|
|
|
redirect_url = request.GET.get("next")
|
2020-08-26 13:38:29 +00:00
|
|
|
if not _allow_redirect(redirect_url):
|
2019-04-25 18:28:40 +00:00
|
|
|
redirect_url = None
|
2018-11-26 15:32:23 +00:00
|
|
|
|
2023-01-23 10:33:39 +00:00
|
|
|
if magic_form.user:
|
|
|
|
profile = Profile.objects.for_user(magic_form.user)
|
|
|
|
profile.send_instant_login_link(redirect_url=redirect_url)
|
2019-05-21 08:26:55 +00:00
|
|
|
|
2023-01-23 10:33:39 +00:00
|
|
|
response = redirect("hc-login-link-sent")
|
2023-02-15 07:17:09 +00:00
|
|
|
_set_autologin_cookie(response)
|
2019-05-21 08:26:55 +00:00
|
|
|
return response
|
2015-06-11 19:12:09 +00:00
|
|
|
|
2021-08-06 10:54:12 +00:00
|
|
|
if request.user.is_authenticated:
|
|
|
|
return _redirect_after_login(request)
|
|
|
|
|
2015-12-19 08:49:55 +00:00
|
|
|
bad_link = request.session.pop("bad_link", None)
|
2016-01-04 22:25:08 +00:00
|
|
|
ctx = {
|
2018-10-09 13:12:02 +00:00
|
|
|
"page": "login",
|
2016-01-04 22:25:08 +00:00
|
|
|
"form": form,
|
2018-10-09 13:12:02 +00:00
|
|
|
"magic_form": magic_form,
|
2019-05-15 11:27:50 +00:00
|
|
|
"bad_link": bad_link,
|
2019-08-26 07:55:41 +00:00
|
|
|
"registration_open": settings.REGISTRATION_OPEN,
|
2020-12-09 13:36:09 +00:00
|
|
|
"support_email": settings.SUPPORT_EMAIL,
|
2023-08-24 06:07:41 +00:00
|
|
|
"account_closed": "account-closed" in request.GET,
|
2024-05-10 08:04:21 +00:00
|
|
|
"use_magic_form": bool(settings.EMAIL_HOST),
|
2016-01-04 22:25:08 +00:00
|
|
|
}
|
2015-06-11 19:12:09 +00:00
|
|
|
return render(request, "accounts/login.html", ctx)
|
|
|
|
|
|
|
|
|
2022-08-08 12:16:24 +00:00
|
|
|
@require_POST
|
2023-09-06 07:02:50 +00:00
|
|
|
def logout(request: HttpRequest) -> HttpResponse:
|
2015-06-18 15:39:03 +00:00
|
|
|
auth_logout(request)
|
|
|
|
return redirect("hc-index")
|
|
|
|
|
|
|
|
|
2023-09-06 07:02:50 +00:00
|
|
|
def signup_csrf(request: HttpRequest) -> HttpResponse:
|
2023-02-14 07:15:46 +00:00
|
|
|
if not settings.REGISTRATION_OPEN or request.user.is_authenticated:
|
|
|
|
return HttpResponseForbidden()
|
|
|
|
|
2023-02-14 12:20:27 +00:00
|
|
|
return HttpResponse(csrf.get_token(request))
|
2023-02-14 07:15:46 +00:00
|
|
|
|
|
|
|
|
2018-10-12 07:55:15 +00:00
|
|
|
@require_POST
|
2023-09-06 07:02:50 +00:00
|
|
|
def signup(request: HttpRequest) -> HttpResponse:
|
2023-02-14 07:15:46 +00:00
|
|
|
if not settings.REGISTRATION_OPEN or request.user.is_authenticated:
|
2018-10-12 07:55:15 +00:00
|
|
|
return HttpResponseForbidden()
|
|
|
|
|
|
|
|
ctx = {}
|
2023-01-23 12:35:45 +00:00
|
|
|
form = forms.SignupForm(request)
|
2018-10-12 07:55:15 +00:00
|
|
|
if form.is_valid():
|
|
|
|
email = form.cleaned_data["identity"]
|
2024-03-15 15:30:06 +00:00
|
|
|
try:
|
|
|
|
user = User.objects.get(email=email)
|
|
|
|
# Sometimes existing users forget they already have an account.
|
|
|
|
# They use the signup form and are confused why no email arrives.
|
|
|
|
# To avoid this confusion, if we see the user account already exists,
|
|
|
|
# we will send them sign-in link even though they used the wrong form
|
|
|
|
# ("sign up" instead of "sign in").
|
|
|
|
except User.DoesNotExist:
|
|
|
|
# If the user does not exist, create a new user account.
|
2023-01-23 10:33:39 +00:00
|
|
|
tz = form.cleaned_data["tz"]
|
|
|
|
user = _make_user(email, tz)
|
2024-03-15 15:30:06 +00:00
|
|
|
|
|
|
|
profile = Profile.objects.for_user(user)
|
|
|
|
profile.send_instant_login_link()
|
2018-10-12 07:55:15 +00:00
|
|
|
else:
|
|
|
|
ctx = {"form": form}
|
|
|
|
|
2019-10-12 17:14:57 +00:00
|
|
|
response = render(request, "accounts/signup_result.html", ctx)
|
2023-01-23 10:33:39 +00:00
|
|
|
if "form" not in ctx:
|
2023-02-15 07:17:09 +00:00
|
|
|
_set_autologin_cookie(response)
|
2019-10-12 17:14:57 +00:00
|
|
|
|
|
|
|
return response
|
2018-10-12 07:55:15 +00:00
|
|
|
|
|
|
|
|
2023-09-06 07:02:50 +00:00
|
|
|
def login_link_sent(request: HttpRequest) -> HttpResponse:
|
2015-06-11 19:12:09 +00:00
|
|
|
return render(request, "accounts/login_link_sent.html")
|
|
|
|
|
|
|
|
|
2023-09-06 07:02:50 +00:00
|
|
|
def check_token(
|
|
|
|
request: HttpRequest, username: str, token: str, new_email: str | None = None
|
|
|
|
) -> HttpResponse:
|
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 14:54:45 +00:00
|
|
|
if request.user.is_authenticated:
|
|
|
|
auth_logout(request)
|
2015-11-28 08:35:02 +00:00
|
|
|
|
2016-07-28 18:41:28 +00:00
|
|
|
# Some email servers open links in emails to check for malicious content.
|
2019-05-21 08:26:55 +00:00
|
|
|
# To work around this, we sign user in if the method is POST
|
|
|
|
# *or* if the browser presents a cookie we had set when sending the login link.
|
2016-07-28 18:41:28 +00:00
|
|
|
#
|
2020-11-12 14:15:07 +00:00
|
|
|
# If the method is GET and the auto-login cookie isn't present, we serve
|
|
|
|
# a HTML form with a submit button.
|
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 14:54:45 +00:00
|
|
|
if request.method != "POST" and "auto-login" not in request.COOKIES:
|
|
|
|
return render(request, "accounts/check_token_submit.html")
|
2015-08-01 20:02:51 +00:00
|
|
|
|
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 14:54:45 +00:00
|
|
|
user = authenticate(username=username, token=token)
|
|
|
|
if user is not None and user.is_active:
|
2023-09-06 07:02:50 +00:00
|
|
|
assert isinstance(user, User)
|
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 14:54:45 +00:00
|
|
|
if new_email:
|
2022-07-21 12:14:51 +00:00
|
|
|
if User.objects.filter(email=new_email).exists():
|
|
|
|
request.session["bad_link"] = True
|
|
|
|
return redirect("hc-login")
|
|
|
|
|
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 14:54:45 +00:00
|
|
|
user.email = new_email
|
|
|
|
user.set_unusable_password()
|
|
|
|
user.save()
|
2016-07-28 18:41:28 +00:00
|
|
|
|
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 14:54:45 +00:00
|
|
|
user.profile.token = ""
|
|
|
|
user.profile.save()
|
|
|
|
return _check_2fa(request, user)
|
2015-06-11 20:44:49 +00:00
|
|
|
|
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 14:54:45 +00:00
|
|
|
request.session["bad_link"] = True
|
|
|
|
return redirect("hc-login")
|
2015-12-15 00:27:24 +00:00
|
|
|
|
|
|
|
|
|
|
|
@login_required
|
2023-09-05 15:31:35 +00:00
|
|
|
def profile(request: AuthenticatedHttpRequest) -> HttpResponse:
|
2017-08-30 19:42:45 +00:00
|
|
|
profile = request.profile
|
2015-12-15 00:27:24 +00:00
|
|
|
|
2020-11-14 09:45:09 +00:00
|
|
|
ctx = {
|
|
|
|
"page": "profile",
|
|
|
|
"profile": profile,
|
|
|
|
"my_projects_status": "default",
|
2020-11-16 12:16:06 +00:00
|
|
|
"2fa_status": "default",
|
2020-11-14 09:45:09 +00:00
|
|
|
"added_credential_name": request.session.pop("added_credential_name", ""),
|
|
|
|
"removed_credential_name": request.session.pop("removed_credential_name", ""),
|
2021-07-30 11:09:16 +00:00
|
|
|
"enabled_totp": request.session.pop("enabled_totp", False),
|
|
|
|
"disabled_totp": request.session.pop("disabled_totp", False),
|
2020-11-19 13:05:08 +00:00
|
|
|
"credentials": list(request.user.credentials.order_by("id")),
|
2021-07-30 11:09:16 +00:00
|
|
|
"use_webauthn": settings.RP_ID,
|
2020-11-14 09:45:09 +00:00
|
|
|
}
|
|
|
|
|
2021-07-30 11:09:16 +00:00
|
|
|
if ctx["added_credential_name"] or ctx["enabled_totp"]:
|
2020-11-16 12:16:06 +00:00
|
|
|
ctx["2fa_status"] = "success"
|
2020-11-14 09:45:09 +00:00
|
|
|
|
2021-07-30 11:09:16 +00:00
|
|
|
if ctx["removed_credential_name"] or ctx["disabled_totp"]:
|
2020-11-16 12:16:06 +00:00
|
|
|
ctx["2fa_status"] = "info"
|
2017-08-23 12:47:20 +00:00
|
|
|
|
2020-11-16 13:45:25 +00:00
|
|
|
if request.session.pop("changed_password", False):
|
|
|
|
ctx["changed_password"] = True
|
|
|
|
ctx["email_password_status"] = "success"
|
|
|
|
|
2020-11-16 13:33:29 +00:00
|
|
|
if request.method == "POST" and "leave_project" in request.POST:
|
|
|
|
code = request.POST["code"]
|
|
|
|
try:
|
|
|
|
project = Project.objects.get(code=code, member__user=request.user)
|
|
|
|
except Project.DoesNotExist:
|
|
|
|
return HttpResponseBadRequest()
|
2019-01-28 18:09:23 +00:00
|
|
|
|
2020-11-16 13:33:29 +00:00
|
|
|
Member.objects.filter(project=project, user=request.user).delete()
|
2019-01-28 18:09:23 +00:00
|
|
|
|
2020-11-16 13:33:29 +00:00
|
|
|
ctx["left_project"] = project
|
|
|
|
ctx["my_projects_status"] = "info"
|
2019-01-28 18:09:23 +00:00
|
|
|
|
2022-12-22 09:39:20 +00:00
|
|
|
ctx["ownerships"] = request.user.project_set.order_by(Lower("name"))
|
|
|
|
ctx["memberships"] = request.user.memberships.order_by(Lower("project__name"))
|
2019-01-22 13:44:54 +00:00
|
|
|
return render(request, "accounts/profile.html", ctx)
|
|
|
|
|
|
|
|
|
2019-01-28 18:09:23 +00:00
|
|
|
@login_required
|
|
|
|
@require_POST
|
2023-09-05 15:31:35 +00:00
|
|
|
def add_project(request: AuthenticatedHttpRequest) -> HttpResponse:
|
2020-04-13 09:16:39 +00:00
|
|
|
form = forms.ProjectNameForm(request.POST)
|
2019-01-28 18:09:23 +00:00
|
|
|
if not form.is_valid():
|
|
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
|
|
project = Project(owner=request.user)
|
2023-09-06 07:02:50 +00:00
|
|
|
project.code = project.badge_key = str(uuid4())
|
2019-01-28 18:09:23 +00:00
|
|
|
project.name = form.cleaned_data["name"]
|
|
|
|
project.save()
|
|
|
|
|
2019-01-29 08:59:10 +00:00
|
|
|
return redirect("hc-checks", project.code)
|
2019-01-28 18:09:23 +00:00
|
|
|
|
|
|
|
|
2019-01-22 13:44:54 +00:00
|
|
|
@login_required
|
2023-09-06 07:02:50 +00:00
|
|
|
def project(request: AuthenticatedHttpRequest, code: UUID) -> HttpResponse:
|
2020-08-26 12:04:12 +00:00
|
|
|
project = get_object_or_404(Project, code=code)
|
|
|
|
is_owner = project.owner_id == request.user.id
|
2019-04-02 08:51:35 +00:00
|
|
|
|
2020-08-26 12:04:12 +00:00
|
|
|
if request.user.is_superuser or is_owner:
|
2021-07-24 16:38:59 +00:00
|
|
|
is_manager = True
|
2020-08-26 12:04:12 +00:00
|
|
|
rw = True
|
|
|
|
else:
|
|
|
|
membership = get_object_or_404(Member, project=project, user=request.user)
|
2021-07-24 16:38:59 +00:00
|
|
|
is_manager = membership.role == Member.Role.MANAGER
|
2021-07-22 14:16:52 +00:00
|
|
|
rw = membership.is_rw
|
2019-01-22 13:44:54 +00:00
|
|
|
|
|
|
|
ctx = {
|
2019-01-28 18:09:23 +00:00
|
|
|
"page": "project",
|
2020-08-26 12:04:12 +00:00
|
|
|
"rw": rw,
|
2019-01-22 13:44:54 +00:00
|
|
|
"project": project,
|
2019-04-02 08:51:35 +00:00
|
|
|
"is_owner": is_owner,
|
2021-07-24 16:38:59 +00:00
|
|
|
"is_manager": is_manager,
|
2019-04-02 08:51:35 +00:00
|
|
|
"show_api_keys": "show_api_keys" in request.GET,
|
2021-01-29 13:05:42 +00:00
|
|
|
"enable_prometheus": settings.PROMETHEUS_ENABLED is True,
|
2019-01-22 13:44:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if request.method == "POST":
|
2021-09-09 11:55:17 +00:00
|
|
|
if "create_key" in request.POST:
|
2021-07-26 09:50:43 +00:00
|
|
|
if not rw:
|
|
|
|
return HttpResponseForbidden()
|
|
|
|
|
2021-09-09 11:55:17 +00:00
|
|
|
if request.POST["create_key"] == "api_key":
|
|
|
|
project.api_key = _new_key(24)
|
|
|
|
elif request.POST["create_key"] == "api_key_readonly":
|
|
|
|
project.api_key_readonly = _new_key(24)
|
|
|
|
elif request.POST["create_key"] == "ping_key":
|
|
|
|
project.ping_key = _new_key(16)
|
2019-01-17 14:02:57 +00:00
|
|
|
project.save()
|
2019-01-12 14:40:21 +00:00
|
|
|
|
2021-09-09 11:55:17 +00:00
|
|
|
ctx["key_created"] = True
|
2017-08-23 12:47:20 +00:00
|
|
|
ctx["api_status"] = "success"
|
2021-09-09 11:55:17 +00:00
|
|
|
ctx["show_keys"] = True
|
|
|
|
elif "revoke_key" in request.POST:
|
2021-07-26 09:50:43 +00:00
|
|
|
if not rw:
|
|
|
|
return HttpResponseForbidden()
|
|
|
|
|
2021-09-09 11:55:17 +00:00
|
|
|
if request.POST["revoke_key"] == "api_key":
|
|
|
|
project.api_key = ""
|
|
|
|
elif request.POST["revoke_key"] == "api_key_readonly":
|
|
|
|
project.api_key_readonly = ""
|
|
|
|
elif request.POST["revoke_key"] == "ping_key":
|
|
|
|
project.ping_key = None
|
2019-01-17 14:02:57 +00:00
|
|
|
project.save()
|
2019-01-12 14:40:21 +00:00
|
|
|
|
2021-09-09 11:55:17 +00:00
|
|
|
ctx["key_revoked"] = True
|
2017-08-23 12:47:20 +00:00
|
|
|
ctx["api_status"] = "info"
|
2021-09-09 11:55:17 +00:00
|
|
|
elif "show_keys" in request.POST:
|
2021-07-26 09:50:43 +00:00
|
|
|
if not rw:
|
|
|
|
return HttpResponseForbidden()
|
|
|
|
|
2021-09-09 11:55:17 +00:00
|
|
|
ctx["show_keys"] = True
|
2016-05-09 08:54:18 +00:00
|
|
|
elif "invite_team_member" in request.POST:
|
2021-07-24 16:38:59 +00:00
|
|
|
if not is_manager:
|
2016-05-14 09:51:10 +00:00
|
|
|
return HttpResponseForbidden()
|
|
|
|
|
2023-09-06 07:02:50 +00:00
|
|
|
invite_form = forms.InviteTeamMemberForm(request.POST)
|
|
|
|
if invite_form.is_valid():
|
|
|
|
email = invite_form.cleaned_data["email"]
|
2020-02-14 11:05:21 +00:00
|
|
|
|
2020-04-12 11:46:12 +00:00
|
|
|
invite_suggestions = project.invite_suggestions()
|
2020-02-14 11:05:21 +00:00
|
|
|
if not invite_suggestions.filter(email=email).exists():
|
|
|
|
# We're inviting a new user. Are we within team size limit?
|
|
|
|
if not project.can_invite_new_users():
|
|
|
|
return HttpResponseForbidden()
|
|
|
|
|
|
|
|
# And are we not hitting a rate limit?
|
|
|
|
if not TokenBucket.authorize_invite(request.user):
|
|
|
|
return render(request, "try_later.html")
|
|
|
|
|
2016-05-09 08:54:18 +00:00
|
|
|
try:
|
|
|
|
user = User.objects.get(email=email)
|
|
|
|
except User.DoesNotExist:
|
2019-01-29 17:16:52 +00:00
|
|
|
user = _make_user(email, with_project=False)
|
2016-05-09 08:54:18 +00:00
|
|
|
|
2023-09-06 07:02:50 +00:00
|
|
|
if project.invite(user, role=invite_form.cleaned_data["role"]):
|
2020-08-19 09:07:48 +00:00
|
|
|
ctx["team_member_invited"] = email
|
|
|
|
ctx["team_status"] = "success"
|
|
|
|
else:
|
|
|
|
ctx["team_member_duplicate"] = email
|
|
|
|
ctx["team_status"] = "info"
|
2017-08-23 12:47:20 +00:00
|
|
|
|
2016-05-09 08:54:18 +00:00
|
|
|
elif "remove_team_member" in request.POST:
|
2021-07-24 16:38:59 +00:00
|
|
|
if not is_manager:
|
2019-04-02 08:51:35 +00:00
|
|
|
return HttpResponseForbidden()
|
|
|
|
|
2023-09-06 07:02:50 +00:00
|
|
|
remove_form = forms.RemoveTeamMemberForm(request.POST)
|
|
|
|
if remove_form.is_valid():
|
|
|
|
q = User.objects.filter(
|
|
|
|
email=remove_form.cleaned_data["email"],
|
|
|
|
memberships__project=project,
|
|
|
|
)
|
2019-01-29 14:42:12 +00:00
|
|
|
farewell_user = q.first()
|
|
|
|
if farewell_user is None:
|
|
|
|
return HttpResponseBadRequest()
|
2016-05-09 08:54:18 +00:00
|
|
|
|
2021-07-24 16:38:59 +00:00
|
|
|
if farewell_user == request.user:
|
|
|
|
return HttpResponseBadRequest()
|
|
|
|
|
2019-05-15 11:27:50 +00:00
|
|
|
Member.objects.filter(project=project, user=farewell_user).delete()
|
2016-05-09 14:29:41 +00:00
|
|
|
|
2023-09-06 07:02:50 +00:00
|
|
|
ctx["team_member_removed"] = remove_form.cleaned_data["email"]
|
2017-08-23 12:47:20 +00:00
|
|
|
ctx["team_status"] = "info"
|
2019-01-22 13:44:54 +00:00
|
|
|
elif "set_project_name" in request.POST:
|
2021-07-26 09:50:43 +00:00
|
|
|
if not rw:
|
|
|
|
return HttpResponseForbidden()
|
|
|
|
|
2023-09-06 07:02:50 +00:00
|
|
|
name_form = forms.ProjectNameForm(request.POST)
|
|
|
|
if name_form.is_valid():
|
|
|
|
project.name = name_form.cleaned_data["name"]
|
2019-01-22 13:44:54 +00:00
|
|
|
project.save()
|
2019-01-12 14:40:21 +00:00
|
|
|
|
2019-01-22 13:44:54 +00:00
|
|
|
ctx["project_name_updated"] = True
|
|
|
|
ctx["project_name_status"] = "success"
|
|
|
|
|
2020-04-12 11:46:12 +00:00
|
|
|
elif "transfer_project" in request.POST:
|
|
|
|
if not is_owner:
|
|
|
|
return HttpResponseForbidden()
|
|
|
|
|
2023-09-06 07:02:50 +00:00
|
|
|
transfer_form = forms.TransferForm(request.POST)
|
|
|
|
if transfer_form.is_valid():
|
2020-04-13 12:19:37 +00:00
|
|
|
# Look up the proposed new owner
|
2023-09-06 07:02:50 +00:00
|
|
|
email = transfer_form.cleaned_data["email"]
|
2020-04-13 12:19:37 +00:00
|
|
|
try:
|
|
|
|
membership = project.member_set.filter(user__email=email).get()
|
|
|
|
except Member.DoesNotExist:
|
|
|
|
return HttpResponseBadRequest()
|
2020-04-12 11:46:12 +00:00
|
|
|
|
|
|
|
# Revoke any previous transfer requests
|
|
|
|
project.member_set.update(transfer_request_date=None)
|
|
|
|
|
|
|
|
# Initiate the new request
|
2020-04-13 12:19:37 +00:00
|
|
|
membership.transfer_request_date = now()
|
|
|
|
membership.save()
|
|
|
|
|
|
|
|
# Send an email notification
|
|
|
|
profile = Profile.objects.for_user(membership.user)
|
|
|
|
profile.send_transfer_request(project)
|
2020-04-12 11:46:12 +00:00
|
|
|
|
|
|
|
ctx["transfer_initiated"] = True
|
|
|
|
ctx["transfer_status"] = "success"
|
|
|
|
|
|
|
|
elif "cancel_transfer" in request.POST:
|
|
|
|
if not is_owner:
|
|
|
|
return HttpResponseForbidden()
|
|
|
|
|
|
|
|
project.member_set.update(transfer_request_date=None)
|
|
|
|
ctx["transfer_cancelled"] = True
|
|
|
|
ctx["transfer_status"] = "success"
|
|
|
|
|
|
|
|
elif "accept_transfer" in request.POST:
|
|
|
|
tr = project.transfer_request()
|
|
|
|
if not tr or tr.user != request.user:
|
|
|
|
return HttpResponseForbidden()
|
|
|
|
|
|
|
|
if not tr.can_accept():
|
|
|
|
return HttpResponseBadRequest()
|
|
|
|
|
2020-04-13 12:19:37 +00:00
|
|
|
with transaction.atomic():
|
|
|
|
# 1. Reuse the existing membership, and change its user
|
|
|
|
tr.user = project.owner
|
|
|
|
tr.transfer_request_date = None
|
2021-07-26 09:50:43 +00:00
|
|
|
# The previous owner becomes a regular member
|
|
|
|
# (not readonly, not manager):
|
|
|
|
tr.role = Member.Role.REGULAR
|
2020-04-13 12:19:37 +00:00
|
|
|
tr.save()
|
2020-04-12 11:46:12 +00:00
|
|
|
|
2020-04-13 12:19:37 +00:00
|
|
|
# 2. Change project's owner
|
|
|
|
project.owner = request.user
|
|
|
|
project.save()
|
2020-04-12 11:46:12 +00:00
|
|
|
|
|
|
|
ctx["is_owner"] = True
|
2021-07-24 16:38:59 +00:00
|
|
|
ctx["is_manager"] = True
|
2020-04-12 11:46:12 +00:00
|
|
|
messages.success(request, "You are now the owner of this project!")
|
|
|
|
|
|
|
|
elif "reject_transfer" in request.POST:
|
|
|
|
tr = project.transfer_request()
|
|
|
|
if not tr or tr.user != request.user:
|
|
|
|
return HttpResponseForbidden()
|
|
|
|
|
|
|
|
tr.transfer_request_date = None
|
|
|
|
tr.save()
|
|
|
|
|
2023-09-05 15:31:35 +00:00
|
|
|
mq = project.member_set.select_related("user").order_by("user__email")
|
|
|
|
ctx["memberships"] = list(mq)
|
2022-02-04 18:35:00 +00:00
|
|
|
ctx["can_invite_new_users"] = project.can_invite_new_users()
|
2019-01-22 13:44:54 +00:00
|
|
|
return render(request, "accounts/project.html", ctx)
|
2017-03-16 14:06:22 +00:00
|
|
|
|
|
|
|
|
|
|
|
@login_required
|
2023-09-05 15:31:35 +00:00
|
|
|
def notifications(request: AuthenticatedHttpRequest) -> HttpResponse:
|
2017-08-30 19:42:45 +00:00
|
|
|
profile = request.profile
|
2017-03-16 14:06:22 +00:00
|
|
|
|
2022-12-01 14:12:32 +00:00
|
|
|
ctx = {
|
|
|
|
"status": "default",
|
|
|
|
"page": "profile",
|
|
|
|
"profile": profile,
|
|
|
|
"timezones": all_timezones,
|
|
|
|
}
|
2017-10-14 19:04:11 +00:00
|
|
|
|
2017-03-16 14:06:22 +00:00
|
|
|
if request.method == "POST":
|
2020-04-13 09:16:39 +00:00
|
|
|
form = forms.ReportSettingsForm(request.POST)
|
2017-03-16 14:06:22 +00:00
|
|
|
if form.is_valid():
|
2021-05-24 11:04:05 +00:00
|
|
|
if form.cleaned_data["tz"]:
|
|
|
|
profile.tz = form.cleaned_data["tz"]
|
2021-05-24 10:44:34 +00:00
|
|
|
profile.reports = form.cleaned_data["reports"]
|
|
|
|
profile.next_report_date = profile.choose_next_report_date()
|
2017-10-14 13:03:56 +00:00
|
|
|
|
|
|
|
if profile.nag_period != form.cleaned_data["nag_period"]:
|
|
|
|
# Set the new nag period
|
|
|
|
profile.nag_period = form.cleaned_data["nag_period"]
|
2021-03-15 11:06:57 +00:00
|
|
|
# and update next_nag_date:
|
2017-10-14 13:03:56 +00:00
|
|
|
if profile.nag_period:
|
2021-03-15 11:06:57 +00:00
|
|
|
profile.update_next_nag_date()
|
2017-10-14 13:03:56 +00:00
|
|
|
else:
|
|
|
|
profile.next_nag_date = None
|
|
|
|
|
2017-03-16 14:06:22 +00:00
|
|
|
profile.save()
|
2017-10-14 19:04:11 +00:00
|
|
|
ctx["status"] = "info"
|
2017-03-16 14:06:22 +00:00
|
|
|
|
|
|
|
return render(request, "accounts/notifications.html", ctx)
|
|
|
|
|
|
|
|
|
2016-01-04 22:25:08 +00:00
|
|
|
@login_required
|
2023-02-20 08:09:16 +00:00
|
|
|
@sensitive_post_parameters()
|
2020-11-16 12:53:50 +00:00
|
|
|
@require_sudo_mode
|
2023-09-05 15:31:35 +00:00
|
|
|
def set_password(request: AuthenticatedHttpRequest) -> HttpResponse:
|
2016-01-04 22:25:08 +00:00
|
|
|
if request.method == "POST":
|
2020-04-13 09:16:39 +00:00
|
|
|
form = forms.SetPasswordForm(request.POST)
|
2016-01-04 22:25:08 +00:00
|
|
|
if form.is_valid():
|
|
|
|
password = form.cleaned_data["password"]
|
|
|
|
request.user.set_password(password)
|
|
|
|
request.user.save()
|
|
|
|
|
2017-08-30 19:42:45 +00:00
|
|
|
request.profile.token = ""
|
|
|
|
request.profile.save()
|
2016-01-04 22:25:08 +00:00
|
|
|
|
2020-11-16 12:29:52 +00:00
|
|
|
# update the session with the new password hash so that
|
|
|
|
# the user doesn't get logged out
|
|
|
|
update_session_auth_hash(request, request.user)
|
2016-01-04 22:25:08 +00:00
|
|
|
|
2020-11-16 13:45:25 +00:00
|
|
|
request.session["changed_password"] = True
|
2016-01-04 22:25:08 +00:00
|
|
|
return redirect("hc-profile")
|
|
|
|
|
2016-02-16 21:41:40 +00:00
|
|
|
return render(request, "accounts/set_password.html", {})
|
2016-01-04 22:25:08 +00:00
|
|
|
|
|
|
|
|
2017-08-23 12:47:20 +00:00
|
|
|
@login_required
|
2020-11-16 13:33:29 +00:00
|
|
|
@require_sudo_mode
|
2023-09-06 07:02:50 +00:00
|
|
|
def change_email(request: AuthenticatedHttpRequest) -> HttpResponse:
|
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 14:54:45 +00:00
|
|
|
if "sent" in request.session:
|
|
|
|
ctx = {"email": request.session.pop("sent")}
|
|
|
|
return render(request, "accounts/change_email_instructions.html", ctx)
|
|
|
|
|
2017-08-23 12:47:20 +00:00
|
|
|
if request.method == "POST":
|
2020-04-13 09:16:39 +00:00
|
|
|
form = forms.ChangeEmailForm(request.POST)
|
2017-08-23 12:47:20 +00:00
|
|
|
if form.is_valid():
|
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 14:54:45 +00:00
|
|
|
# The user has entered a valid-looking new email address.
|
|
|
|
# Send a special login link to the new address. When the user
|
|
|
|
# clicks the special login link, hc.accounts.views.change_email_verify
|
|
|
|
# unpacks the payload, and passes it to hc.accounts.views.check_token,
|
|
|
|
# which finally updates user's email address.
|
|
|
|
email = form.cleaned_data["email"]
|
|
|
|
request.profile.send_change_email_link(email)
|
|
|
|
request.session["sent"] = email
|
|
|
|
|
|
|
|
response = redirect(reverse("hc-change-email"))
|
|
|
|
# check_token looks for this cookie to decide if
|
|
|
|
# it needs to do the extra POST step.
|
2023-02-15 07:17:09 +00:00
|
|
|
_set_autologin_cookie(response)
|
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 14:54:45 +00:00
|
|
|
return response
|
2017-08-23 12:47:20 +00:00
|
|
|
else:
|
2020-04-13 09:16:39 +00:00
|
|
|
form = forms.ChangeEmailForm()
|
2017-08-23 12:47:20 +00:00
|
|
|
|
|
|
|
return render(request, "accounts/change_email.html", {"form": form})
|
|
|
|
|
|
|
|
|
2023-09-06 07:02:50 +00:00
|
|
|
def change_email_verify(request: HttpRequest, signed_payload: str) -> HttpResponse:
|
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 14:54:45 +00:00
|
|
|
try:
|
|
|
|
payload = TimestampSigner().unsign_object(signed_payload, max_age=900)
|
|
|
|
except BadSignature:
|
|
|
|
return render(request, "bad_link.html")
|
|
|
|
|
|
|
|
return check_token(request, payload["u"], payload["t"], payload["e"])
|
2017-08-23 12:47:20 +00:00
|
|
|
|
|
|
|
|
2018-10-24 07:06:51 +00:00
|
|
|
@csrf_exempt
|
2023-09-06 07:02:50 +00:00
|
|
|
def unsubscribe_reports(request: HttpRequest, signed_username: str) -> HttpResponse:
|
2019-12-18 14:10:30 +00:00
|
|
|
# Some email servers open links in emails to check for malicious content.
|
|
|
|
# To work around this, for GET requests we serve a confirmation form.
|
|
|
|
# If the signature is more than 5 minutes old, we also include JS code to
|
|
|
|
# auto-submit the form.
|
|
|
|
|
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 14:54:45 +00:00
|
|
|
signer = TimestampSigner(salt="reports")
|
2019-12-18 14:10:30 +00:00
|
|
|
# First, check the signature without looking at the timestamp:
|
2018-05-25 20:38:02 +00:00
|
|
|
try:
|
2019-12-18 14:10:30 +00:00
|
|
|
username = signer.unsign(signed_username)
|
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 14:54:45 +00:00
|
|
|
except BadSignature:
|
2018-05-25 20:38:02 +00:00
|
|
|
return render(request, "bad_link.html")
|
2015-12-15 00:27:24 +00:00
|
|
|
|
2019-12-18 14:10:30 +00:00
|
|
|
try:
|
2021-08-02 09:51:05 +00:00
|
|
|
user = User.objects.get(username=username)
|
|
|
|
except User.DoesNotExist:
|
|
|
|
# This is likely an old unsubscribe link, and the user account has already
|
|
|
|
# been deleted. Show the "Unsubscribed!" page nevertheless.
|
|
|
|
return render(request, "accounts/unsubscribed.html")
|
2019-12-18 14:10:30 +00:00
|
|
|
|
2019-12-10 07:14:54 +00:00
|
|
|
if request.method != "POST":
|
2021-08-02 09:51:05 +00:00
|
|
|
# Unsign again, now with max_age set,
|
|
|
|
# to see if the timestamp is older than 5 minutes
|
|
|
|
try:
|
|
|
|
autosubmit = False
|
|
|
|
username = signer.unsign(signed_username, max_age=300)
|
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 14:54:45 +00:00
|
|
|
except SignatureExpired:
|
2021-08-02 09:51:05 +00:00
|
|
|
autosubmit = True
|
|
|
|
|
|
|
|
ctx = {"autosubmit": autosubmit}
|
2019-12-18 14:10:30 +00:00
|
|
|
return render(request, "accounts/unsubscribe_submit.html", ctx)
|
2018-11-09 20:12:11 +00:00
|
|
|
|
2017-08-30 19:42:45 +00:00
|
|
|
profile = Profile.objects.for_user(user)
|
2021-05-24 08:20:28 +00:00
|
|
|
profile.reports = "off"
|
2017-12-29 16:03:42 +00:00
|
|
|
profile.next_report_date = None
|
2017-10-14 13:03:56 +00:00
|
|
|
profile.nag_period = td()
|
2017-12-29 16:03:42 +00:00
|
|
|
profile.next_nag_date = None
|
2017-08-30 19:42:45 +00:00
|
|
|
profile.save()
|
2015-12-15 00:27:24 +00:00
|
|
|
|
|
|
|
return render(request, "accounts/unsubscribed.html")
|
2016-05-09 12:35:13 +00:00
|
|
|
|
|
|
|
|
2017-03-16 17:39:30 +00:00
|
|
|
@login_required
|
2020-11-16 14:22:25 +00:00
|
|
|
@require_sudo_mode
|
2023-09-06 07:02:50 +00:00
|
|
|
def close(request: AuthenticatedHttpRequest) -> HttpResponse:
|
2017-03-16 17:39:30 +00:00
|
|
|
user = request.user
|
|
|
|
|
2020-11-16 14:22:25 +00:00
|
|
|
if request.method == "POST":
|
|
|
|
if request.POST.get("confirmation") == request.user.email:
|
|
|
|
# Cancel their subscription:
|
2022-12-01 07:16:19 +00:00
|
|
|
if sub := Subscription.objects.filter(user=user).first():
|
2020-11-16 14:22:25 +00:00
|
|
|
sub.cancel()
|
|
|
|
|
|
|
|
# Deleting user also deletes its profile, checks, channels etc.
|
|
|
|
user.delete()
|
2017-03-16 17:39:30 +00:00
|
|
|
|
2020-11-16 14:22:25 +00:00
|
|
|
request.session.flush()
|
2023-08-24 06:07:41 +00:00
|
|
|
path = reverse("hc-login") + "?account-closed"
|
|
|
|
return redirect(path)
|
2017-03-16 17:39:30 +00:00
|
|
|
|
2020-11-16 14:22:25 +00:00
|
|
|
ctx = {}
|
|
|
|
if "confirmation" in request.POST:
|
|
|
|
ctx["wrong_confirmation"] = True
|
2017-05-28 10:38:38 +00:00
|
|
|
|
2020-11-16 14:22:25 +00:00
|
|
|
return render(request, "accounts/close_account.html", ctx)
|
2019-01-28 18:09:23 +00:00
|
|
|
|
|
|
|
|
|
|
|
@require_POST
|
|
|
|
@login_required
|
2023-09-06 07:02:50 +00:00
|
|
|
def remove_project(request: AuthenticatedHttpRequest, code: str) -> HttpResponse:
|
2019-01-29 17:57:18 +00:00
|
|
|
project = get_object_or_404(Project, code=code, owner=request.user)
|
2024-04-16 13:36:49 +00:00
|
|
|
for check in project.check_set.all():
|
|
|
|
check.lock_and_delete()
|
2019-01-28 18:09:23 +00:00
|
|
|
project.delete()
|
2019-01-29 17:57:18 +00:00
|
|
|
return redirect("hc-index")
|
2020-11-12 14:15:07 +00:00
|
|
|
|
|
|
|
|
|
|
|
@login_required
|
2020-11-13 14:23:28 +00:00
|
|
|
@require_sudo_mode
|
2023-09-06 07:02:50 +00:00
|
|
|
def add_webauthn(request: AuthenticatedHttpRequest) -> HttpResponse:
|
2020-11-19 10:54:00 +00:00
|
|
|
if not settings.RP_ID:
|
|
|
|
return HttpResponse(status=404)
|
|
|
|
|
2022-06-19 08:30:37 +00:00
|
|
|
credentials = request.user.credentials.values_list("data", flat=True)
|
|
|
|
helper = CreateHelper(settings.RP_ID, credentials)
|
|
|
|
|
2020-11-12 14:15:07 +00:00
|
|
|
if request.method == "POST":
|
2021-07-30 11:09:16 +00:00
|
|
|
form = forms.AddWebAuthnForm(request.POST)
|
2020-11-12 15:08:23 +00:00
|
|
|
if not form.is_valid():
|
|
|
|
return HttpResponseBadRequest()
|
2020-11-12 14:15:07 +00:00
|
|
|
|
2022-06-19 08:30:37 +00:00
|
|
|
state = request.session["state"]
|
2023-11-17 14:06:39 +00:00
|
|
|
try:
|
|
|
|
credential_bytes = helper.verify(state, form.cleaned_data["response"])
|
2024-10-04 14:34:30 +00:00
|
|
|
except ValueError:
|
2023-11-17 14:06:39 +00:00
|
|
|
logger.exception("CreateHelper.verify failed, form: %s", form.cleaned_data)
|
2020-11-16 10:52:26 +00:00
|
|
|
return HttpResponseBadRequest()
|
2020-11-12 14:15:07 +00:00
|
|
|
|
|
|
|
c = Credential(user=request.user)
|
2020-11-16 10:52:26 +00:00
|
|
|
c.name = form.cleaned_data["name"]
|
2022-06-19 08:30:37 +00:00
|
|
|
c.data = credential_bytes
|
2020-11-12 14:15:07 +00:00
|
|
|
c.save()
|
|
|
|
|
2022-06-19 09:31:27 +00:00
|
|
|
request.session.pop("state")
|
2020-11-14 09:45:09 +00:00
|
|
|
request.session["added_credential_name"] = c.name
|
2020-11-12 15:08:23 +00:00
|
|
|
return redirect("hc-profile")
|
2020-11-12 14:15:07 +00:00
|
|
|
|
2022-06-19 08:30:37 +00:00
|
|
|
options, request.session["state"] = helper.prepare(request.user.email)
|
2022-06-19 07:10:57 +00:00
|
|
|
return render(request, "accounts/add_credential.html", {"options": options})
|
2020-11-14 09:45:09 +00:00
|
|
|
|
|
|
|
|
2021-07-30 11:09:16 +00:00
|
|
|
@login_required
|
|
|
|
@require_sudo_mode
|
2023-09-06 07:02:50 +00:00
|
|
|
def add_totp(request: AuthenticatedHttpRequest) -> HttpResponse:
|
2021-07-30 11:09:16 +00:00
|
|
|
if request.profile.totp:
|
|
|
|
# TOTP is already configured, refuse to continue
|
|
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
|
|
if "totp_secret" not in request.session:
|
|
|
|
request.session["totp_secret"] = pyotp.random_base32()
|
|
|
|
|
|
|
|
totp = pyotp.totp.TOTP(request.session["totp_secret"])
|
|
|
|
|
|
|
|
if request.method == "POST":
|
|
|
|
form = forms.TotpForm(totp, request.POST)
|
|
|
|
if form.is_valid():
|
|
|
|
request.profile.totp = request.session["totp_secret"]
|
|
|
|
request.profile.totp_created = now()
|
|
|
|
request.profile.save()
|
|
|
|
|
|
|
|
request.session["enabled_totp"] = True
|
|
|
|
request.session.pop("totp_secret")
|
|
|
|
return redirect("hc-profile")
|
|
|
|
else:
|
|
|
|
form = forms.TotpForm(totp)
|
|
|
|
|
|
|
|
uri = totp.provisioning_uri(name=request.user.email, issuer_name=settings.SITE_NAME)
|
|
|
|
qr_data_uri = segno.make(uri).png_data_uri(scale=8)
|
2022-01-24 13:17:48 +00:00
|
|
|
ctx = {
|
|
|
|
"form": form,
|
|
|
|
"qr_data_uri": qr_data_uri,
|
|
|
|
"secret": request.session["totp_secret"],
|
|
|
|
}
|
2021-07-30 11:09:16 +00:00
|
|
|
return render(request, "accounts/add_totp.html", ctx)
|
|
|
|
|
|
|
|
|
|
|
|
@login_required
|
|
|
|
@require_sudo_mode
|
2023-09-06 07:02:50 +00:00
|
|
|
def remove_totp(request: AuthenticatedHttpRequest) -> HttpResponse:
|
2021-07-30 11:09:16 +00:00
|
|
|
if request.method == "POST" and "disable_totp" in request.POST:
|
|
|
|
request.profile.totp = None
|
|
|
|
request.profile.totp_created = None
|
|
|
|
request.profile.save()
|
|
|
|
request.session["disabled_totp"] = True
|
|
|
|
return redirect("hc-profile")
|
|
|
|
|
|
|
|
ctx = {"is_last": not request.user.credentials.exists()}
|
|
|
|
return render(request, "accounts/remove_totp.html", ctx)
|
|
|
|
|
|
|
|
|
2020-11-14 09:45:09 +00:00
|
|
|
@login_required
|
|
|
|
@require_sudo_mode
|
2023-09-06 07:02:50 +00:00
|
|
|
def remove_credential(request: AuthenticatedHttpRequest, code: str) -> HttpResponse:
|
2020-11-19 10:54:00 +00:00
|
|
|
if not settings.RP_ID:
|
|
|
|
return HttpResponse(status=404)
|
|
|
|
|
2020-11-14 09:45:09 +00:00
|
|
|
try:
|
|
|
|
credential = Credential.objects.get(user=request.user, code=code)
|
|
|
|
except Credential.DoesNotExist:
|
|
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
|
|
if request.method == "POST" and "remove_credential" in request.POST:
|
|
|
|
request.session["removed_credential_name"] = credential.name
|
|
|
|
credential.delete()
|
|
|
|
return redirect("hc-profile")
|
|
|
|
|
2021-07-30 11:09:16 +00:00
|
|
|
if request.profile.totp:
|
|
|
|
is_last = False
|
|
|
|
else:
|
|
|
|
is_last = request.user.credentials.count() == 1
|
|
|
|
|
|
|
|
ctx = {"credential": credential, "is_last": is_last}
|
2020-11-14 09:45:09 +00:00
|
|
|
return render(request, "accounts/remove_credential.html", ctx)
|
2020-11-14 10:54:26 +00:00
|
|
|
|
|
|
|
|
2023-09-06 07:02:50 +00:00
|
|
|
def login_webauthn(request: HttpRequest) -> HttpResponse:
|
2020-11-19 10:54:00 +00:00
|
|
|
# We require RP_ID. Fail predicably if it is not set:
|
|
|
|
if not settings.RP_ID:
|
2023-06-08 07:49:57 +00:00
|
|
|
return HttpResponse(status=404)
|
2020-11-19 10:54:00 +00:00
|
|
|
|
2020-11-19 14:21:31 +00:00
|
|
|
# Expect an unauthenticated user
|
|
|
|
if request.user.is_authenticated:
|
|
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
|
|
if "2fa_user" not in request.session:
|
|
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
|
|
user_id, email, timestamp = request.session["2fa_user"]
|
|
|
|
if timestamp + 300 < time.time():
|
|
|
|
return redirect("hc-login")
|
|
|
|
|
|
|
|
try:
|
|
|
|
user = User.objects.get(id=user_id, email=email)
|
|
|
|
except User.DoesNotExist:
|
|
|
|
return HttpResponseBadRequest()
|
|
|
|
|
2022-06-19 08:30:37 +00:00
|
|
|
credentials = user.credentials.values_list("data", flat=True)
|
|
|
|
helper = GetHelper(settings.RP_ID, credentials)
|
2020-11-14 10:54:26 +00:00
|
|
|
|
|
|
|
if request.method == "POST":
|
2020-11-19 11:01:26 +00:00
|
|
|
form = forms.WebAuthnForm(request.POST)
|
2020-11-14 10:54:26 +00:00
|
|
|
if not form.is_valid():
|
|
|
|
return HttpResponseBadRequest()
|
|
|
|
|
2022-06-19 08:30:37 +00:00
|
|
|
if not helper.verify(request.session["state"], form.cleaned_data["response"]):
|
2020-11-16 10:52:26 +00:00
|
|
|
return HttpResponseBadRequest()
|
2020-11-14 10:54:26 +00:00
|
|
|
|
2020-11-16 10:52:26 +00:00
|
|
|
request.session.pop("state")
|
2020-11-19 14:21:31 +00:00
|
|
|
request.session.pop("2fa_user")
|
2020-11-15 19:39:49 +00:00
|
|
|
auth_login(request, user, "hc.accounts.backends.EmailBackend")
|
|
|
|
return _redirect_after_login(request)
|
2020-11-14 10:54:26 +00:00
|
|
|
|
2022-06-19 08:30:37 +00:00
|
|
|
options, request.session["state"] = helper.prepare()
|
2020-11-15 19:39:49 +00:00
|
|
|
|
2021-08-06 09:09:41 +00:00
|
|
|
totp_url = None
|
|
|
|
if user.profile.totp:
|
|
|
|
totp_url = reverse("hc-login-totp")
|
|
|
|
redirect_url = request.GET.get("next")
|
|
|
|
if _allow_redirect(redirect_url):
|
|
|
|
totp_url += "?next=%s" % redirect_url
|
|
|
|
|
2021-07-30 11:09:16 +00:00
|
|
|
ctx = {
|
2022-06-19 07:10:57 +00:00
|
|
|
"options": options,
|
2021-08-06 09:09:41 +00:00
|
|
|
"totp_url": totp_url,
|
2021-07-30 11:09:16 +00:00
|
|
|
}
|
2020-11-16 12:16:06 +00:00
|
|
|
return render(request, "accounts/login_webauthn.html", ctx)
|
2021-06-18 10:51:07 +00:00
|
|
|
|
|
|
|
|
2023-09-06 07:02:50 +00:00
|
|
|
def login_totp(request: HttpRequest) -> HttpResponse:
|
2021-07-30 11:09:16 +00:00
|
|
|
# Expect an unauthenticated user
|
|
|
|
if request.user.is_authenticated:
|
|
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
|
|
if "2fa_user" not in request.session:
|
|
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
|
|
user_id, email, timestamp = request.session["2fa_user"]
|
|
|
|
if timestamp + 300 < time.time():
|
|
|
|
return redirect("hc-login")
|
|
|
|
|
|
|
|
try:
|
|
|
|
user = User.objects.get(id=user_id, email=email)
|
|
|
|
except User.DoesNotExist:
|
|
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
|
|
if not user.profile.totp:
|
|
|
|
return HttpResponseBadRequest()
|
|
|
|
|
|
|
|
totp = pyotp.totp.TOTP(user.profile.totp)
|
|
|
|
if request.method == "POST":
|
2021-08-06 10:54:12 +00:00
|
|
|
# To guard against brute-forcing TOTP codes, we allow
|
|
|
|
# 96 attempts per user per 24h.
|
2021-07-30 15:17:21 +00:00
|
|
|
if not TokenBucket.authorize_totp_attempt(user):
|
2021-07-30 14:30:28 +00:00
|
|
|
return render(request, "try_later.html")
|
|
|
|
|
2021-07-30 11:09:16 +00:00
|
|
|
form = forms.TotpForm(totp, request.POST)
|
|
|
|
if form.is_valid():
|
2021-08-06 10:54:12 +00:00
|
|
|
# We blacklist an used TOTP code for 90 seconds,
|
|
|
|
# so an attacker cannot reuse a stolen code.
|
2021-07-30 15:17:21 +00:00
|
|
|
if not TokenBucket.authorize_totp_code(user, form.cleaned_data["code"]):
|
|
|
|
return render(request, "try_later.html")
|
|
|
|
|
2021-07-30 11:09:16 +00:00
|
|
|
request.session.pop("2fa_user")
|
|
|
|
auth_login(request, user, "hc.accounts.backends.EmailBackend")
|
|
|
|
return _redirect_after_login(request)
|
|
|
|
else:
|
|
|
|
form = forms.TotpForm(totp)
|
|
|
|
|
|
|
|
return render(request, "accounts/login_totp.html", {"form": form})
|
|
|
|
|
|
|
|
|
2021-06-18 10:51:07 +00:00
|
|
|
@login_required
|
2023-09-06 07:02:50 +00:00
|
|
|
def appearance(request: AuthenticatedHttpRequest) -> HttpResponse:
|
2021-06-18 10:51:07 +00:00
|
|
|
profile = request.profile
|
|
|
|
|
|
|
|
ctx = {
|
|
|
|
"page": "appearance",
|
|
|
|
"profile": profile,
|
|
|
|
"status": "default",
|
|
|
|
}
|
|
|
|
|
|
|
|
if request.method == "POST":
|
|
|
|
theme = request.POST.get("theme", "")
|
2024-04-15 07:42:16 +00:00
|
|
|
if theme in ("", "dark", "system"):
|
2021-06-18 10:51:07 +00:00
|
|
|
profile.theme = theme
|
|
|
|
profile.save()
|
|
|
|
ctx["status"] = "info"
|
|
|
|
|
|
|
|
return render(request, "accounts/appearance.html", ctx)
|