132 lines
4.4 KiB
Python
132 lines
4.4 KiB
Python
""" Forms for the landing pages """
|
|
from django import forms
|
|
from django.contrib.auth.password_validation import validate_password
|
|
from django.core.exceptions import ValidationError
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
import pyotp
|
|
|
|
from bookwyrm import models
|
|
from bookwyrm.settings import DOMAIN
|
|
from bookwyrm.settings import TWO_FACTOR_LOGIN_VALIDITY_WINDOW
|
|
from .custom_form import CustomForm
|
|
|
|
|
|
# pylint: disable=missing-class-docstring
|
|
class LoginForm(CustomForm):
|
|
class Meta:
|
|
model = models.User
|
|
fields = ["localname", "password"]
|
|
help_texts = {f: None for f in fields}
|
|
widgets = {
|
|
"password": forms.PasswordInput(),
|
|
}
|
|
|
|
def infer_username(self):
|
|
"""Users may enter their localname, username, or email"""
|
|
localname = self.data.get("localname")
|
|
if "@" in localname: # looks like an email address to me
|
|
try:
|
|
return models.User.objects.get(email=localname).username
|
|
except models.User.DoesNotExist: # maybe it's a full username?
|
|
return localname
|
|
return f"{localname}@{DOMAIN}"
|
|
|
|
def add_invalid_password_error(self):
|
|
"""We don't want to be too specific about this"""
|
|
# pylint: disable=attribute-defined-outside-init
|
|
self.non_field_errors = _("Username or password are incorrect")
|
|
|
|
|
|
class RegisterForm(CustomForm):
|
|
class Meta:
|
|
model = models.User
|
|
fields = ["localname", "email", "password"]
|
|
help_texts = {f: None for f in fields}
|
|
widgets = {"password": forms.PasswordInput()}
|
|
|
|
def clean(self):
|
|
"""Check if the username is taken"""
|
|
cleaned_data = super().clean()
|
|
localname = cleaned_data.get("localname").strip()
|
|
try:
|
|
validate_password(cleaned_data.get("password"))
|
|
except ValidationError as err:
|
|
self.add_error("password", err)
|
|
if models.User.objects.filter(localname=localname).first():
|
|
self.add_error("localname", _("User with this username already exists"))
|
|
|
|
|
|
class InviteRequestForm(CustomForm):
|
|
def clean(self):
|
|
"""make sure the email isn't in use by a registered user"""
|
|
cleaned_data = super().clean()
|
|
email = cleaned_data.get("email")
|
|
if email and models.User.objects.filter(email=email).exists():
|
|
self.add_error("email", _("A user with this email already exists."))
|
|
|
|
class Meta:
|
|
model = models.InviteRequest
|
|
fields = ["email", "answer"]
|
|
|
|
|
|
class PasswordResetForm(CustomForm):
|
|
confirm_password = forms.CharField(widget=forms.PasswordInput)
|
|
|
|
class Meta:
|
|
model = models.User
|
|
fields = ["password"]
|
|
widgets = {
|
|
"password": forms.PasswordInput(),
|
|
}
|
|
|
|
def clean(self):
|
|
"""Make sure the passwords match and are valid"""
|
|
cleaned_data = super().clean()
|
|
new_password = cleaned_data.get("password")
|
|
confirm_password = self.data.get("confirm_password")
|
|
|
|
if new_password != confirm_password:
|
|
self.add_error("confirm_password", _("Password does not match"))
|
|
|
|
try:
|
|
validate_password(new_password)
|
|
except ValidationError as err:
|
|
self.add_error("password", err)
|
|
|
|
|
|
class Confirm2FAForm(CustomForm):
|
|
otp = forms.CharField(
|
|
max_length=6, min_length=6, widget=forms.TextInput(attrs={"autofocus": True})
|
|
)
|
|
|
|
class Meta:
|
|
model = models.User
|
|
fields = ["otp_secret", "hotp_count"]
|
|
|
|
def clean_otp(self):
|
|
"""Check otp matches"""
|
|
otp = self.data.get("otp")
|
|
totp = pyotp.TOTP(self.instance.otp_secret)
|
|
|
|
if not totp.verify(otp, valid_window=TWO_FACTOR_LOGIN_VALIDITY_WINDOW):
|
|
|
|
if self.instance.hotp_secret:
|
|
# maybe it's a backup code?
|
|
hotp = pyotp.HOTP(self.instance.hotp_secret)
|
|
hotp_count = (
|
|
self.instance.hotp_count
|
|
if self.instance.hotp_count is not None
|
|
else 0
|
|
)
|
|
|
|
if not hotp.verify(otp, hotp_count):
|
|
self.add_error("otp", _("Incorrect code"))
|
|
|
|
# increment the user hotp_count
|
|
else:
|
|
self.instance.hotp_count = hotp_count + 1
|
|
self.instance.save(broadcast=False, update_fields=["hotp_count"])
|
|
|
|
else:
|
|
self.add_error("otp", _("Incorrect code"))
|