import base64 import binascii from datetime import timedelta as td from django import forms from django.core.exceptions import ValidationError from django.contrib.auth import authenticate from django.contrib.auth.models import User from hc.accounts.models import REPORT_CHOICES, Member from hc.api.models import TokenBucket import pytz class LowercaseEmailField(forms.EmailField): def clean(self, value): value = super(LowercaseEmailField, self).clean(value) return value.lower() class Base64Field(forms.CharField): def to_python(self, value): if value is None: return None try: return base64.b64decode(value.encode()) except binascii.Error: raise ValidationError(message="Cannot decode base64") 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 clean_identity(self): v = self.cleaned_data["identity"] if len(v) > 254: raise forms.ValidationError("Address is too long.") if User.objects.filter(email=v).exists(): raise forms.ValidationError( "An account with this email address already exists." ) return v def clean_tz(self): # Declare tz as "clean" only if we can find it in pytz.all_timezones if self.cleaned_data["tz"] in pytz.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. class EmailLoginForm(forms.Form): # Call it "identity" instead of "email" # to avoid some of the dumber bots identity = LowercaseEmailField() def clean_identity(self): v = self.cleaned_data["identity"] if not TokenBucket.authorize_login_email(v): raise forms.ValidationError("Too many attempts, please try later.") try: self.user = User.objects.get(email=v) except User.DoesNotExist: raise forms.ValidationError("Unknown email address.") return v class PasswordLoginForm(forms.Form): email = LowercaseEmailField() password = forms.CharField() def clean(self): username = self.cleaned_data.get("email") password = self.cleaned_data.get("password") if username and password: if not TokenBucket.authorize_login_password(username): raise forms.ValidationError("Too many attempts, please try later.") self.user = authenticate(username=username, password=password) if self.user is None or not self.user.is_active: raise forms.ValidationError("Incorrect email or password.") return self.cleaned_data class ReportSettingsForm(forms.Form): reports = forms.ChoiceField(choices=REPORT_CHOICES) nag_period = forms.IntegerField(min_value=0, max_value=86400) tz = forms.CharField() def clean_nag_period(self): seconds = self.cleaned_data["nag_period"] if seconds not in (0, 3600, 86400): raise forms.ValidationError("Bad nag_period: %d" % seconds) return td(seconds=seconds) def clean_tz(self): # Declare tz as "clean" only if we can find it in pytz.all_timezones if self.cleaned_data["tz"] in pytz.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. class SetPasswordForm(forms.Form): password = forms.CharField(min_length=8) class ChangeEmailForm(forms.Form): error_css_class = "has-error" email = LowercaseEmailField() def clean_email(self): v = self.cleaned_data["email"] if User.objects.filter(email=v).exists(): raise forms.ValidationError("%s is already registered" % v) return v class InviteTeamMemberForm(forms.Form): email = LowercaseEmailField(max_length=254) role = forms.ChoiceField(choices=Member.Role.choices) class RemoveTeamMemberForm(forms.Form): email = LowercaseEmailField() class ProjectNameForm(forms.Form): name = forms.CharField(max_length=60) class TransferForm(forms.Form): email = LowercaseEmailField() class AddWebAuthnForm(forms.Form): name = forms.CharField(max_length=100) client_data_json = Base64Field() attestation_object = Base64Field() class WebAuthnForm(forms.Form): credential_id = Base64Field() client_data_json = Base64Field() authenticator_data = Base64Field() signature = Base64Field() class TotpForm(forms.Form): error_css_class = "has-error" code = forms.RegexField(regex=r"^\d{6}$") def __init__(self, totp, post=None, files=None): self.totp = totp super(TotpForm, self).__init__(post, files) def clean_code(self): 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"]