feat: Enhances registration validation and cleanup
Adds rules for username and email validation, preventing the use of blocked keywords or patterns. Implements a cleanup routine for expired IP, email, and username blocks, ensuring outdated data is removed. Introduces IP rate-limiting in registration views to mitigate abuse and server strain. Updates UI text for improved user clarity. These changes aim to improve security, reduce spam registrations, and enhance user experience.
This commit is contained in:
parent
64cab4d53c
commit
9c6555b348
5 changed files with 121 additions and 14 deletions
|
@ -1,12 +1,16 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
|
|
||||||
from .models import UserRegistration
|
from .models import UserRegistration, EmailBlock, IPBlock, UsernameRule
|
||||||
|
|
||||||
admin.site.site_header = "Synapse Registration Administration"
|
admin.site.site_header = "Synapse Registration Administration"
|
||||||
admin.site.site_title = "Synapse Registration Administration"
|
admin.site.site_title = "Synapse Registration Administration"
|
||||||
admin.site.index_title = "Welcome to the Synapse Registration Administration"
|
admin.site.index_title = "Welcome to the Synapse Registration Administration"
|
||||||
|
|
||||||
|
admin.site.register(EmailBlock)
|
||||||
|
admin.site.register(IPBlock)
|
||||||
|
admin.site.register(UsernameRule)
|
||||||
|
|
||||||
admin.site.unregister(Group)
|
admin.site.unregister(Group)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .models import EmailBlock, UsernameRule, UserRegistration
|
||||||
|
|
||||||
|
|
||||||
class UsernameForm(forms.Form):
|
class UsernameForm(forms.Form):
|
||||||
|
@ -25,9 +30,18 @@ class UsernameForm(forms.Form):
|
||||||
):
|
):
|
||||||
self.add_error(
|
self.add_error(
|
||||||
"username",
|
"username",
|
||||||
"Username can only contain the characters a-z, 0-9, ., _, =, -, and /.",
|
"Sorry, your username can only contain the characters a-z, 0-9, ., _, =, -, and /.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for rule in UsernameRule.objects.filter(expires__gt=timezone.now()):
|
||||||
|
regex = re.compile(rule.regex)
|
||||||
|
|
||||||
|
if regex.match(username):
|
||||||
|
self.add_error(
|
||||||
|
"username", "Sorry, the provided username cannot be used."
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
cleaned_data["username"] = username
|
cleaned_data["username"] = username
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
|
@ -39,6 +53,26 @@ class EmailForm(forms.Form):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super().clean()
|
||||||
|
email = cleaned_data.get("email")
|
||||||
|
|
||||||
|
if UserRegistration.objects.filter(email=email).exists():
|
||||||
|
self.add_error(
|
||||||
|
"email", "You have recently registered with this email address."
|
||||||
|
)
|
||||||
|
|
||||||
|
for rule in EmailBlock.objects.filter(expires__gt=timezone.now()):
|
||||||
|
regex = re.compile(rule.regex)
|
||||||
|
|
||||||
|
if regex.match(email):
|
||||||
|
self.add_error(
|
||||||
|
"email", "Sorry, the provided email address/domain is blocked."
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
class RegistrationForm(forms.Form):
|
class RegistrationForm(forms.Form):
|
||||||
password1 = forms.CharField(
|
password1 = forms.CharField(
|
||||||
|
@ -58,7 +92,7 @@ class RegistrationForm(forms.Form):
|
||||||
widget=forms.Textarea(
|
widget=forms.Textarea(
|
||||||
attrs={
|
attrs={
|
||||||
"class": "textarea",
|
"class": "textarea",
|
||||||
"placeholder": "Why do you want to join our server? If you were referred by a current member, who referred you? If you found us through a different means, how did you find us?",
|
"placeholder": "Please tell us a little bit about yourself. Why do you want to join our server? If you were referred by a current member, who referred you? If you found us through a different means, how did you find us?",
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
from ...models import UserRegistration
|
from ...models import UserRegistration, IPBlock, EmailBlock, UsernameRule
|
||||||
|
|
||||||
from datetime import timedelta, datetime
|
from datetime import timedelta, datetime
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = "Clean up old user registrations"
|
help = "Clean up old user registrations and blocks"
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
# Remove all registrations that are still in the "started" state after 48 hours
|
# Remove all registrations that are still in the "started" state after 48 hours
|
||||||
|
@ -17,10 +17,24 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
# Remove all registrations that are denied or approved after 30 days
|
# Remove all registrations that are denied or approved after 30 days
|
||||||
UserRegistration.objects.filter(
|
UserRegistration.objects.filter(
|
||||||
status__in=[UserRegistration.STATUS_DENIED, UserRegistration.STATUS_APPROVED],
|
status__in=[
|
||||||
|
UserRegistration.STATUS_DENIED,
|
||||||
|
UserRegistration.STATUS_APPROVED,
|
||||||
|
],
|
||||||
timestamp__lt=datetime.now() - timedelta(days=30),
|
timestamp__lt=datetime.now() - timedelta(days=30),
|
||||||
).delete()
|
).delete()
|
||||||
|
|
||||||
self.stdout.write(
|
self.stdout.write(
|
||||||
self.style.SUCCESS("Successfully cleaned up old user registrations")
|
self.style.SUCCESS("Successfully cleaned up old user registrations")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Remove all IP blocks that have expired
|
||||||
|
IPBlock.objects.filter(expires__lt=datetime.now()).delete()
|
||||||
|
|
||||||
|
# Remove all email blocks that have expired
|
||||||
|
EmailBlock.objects.filter(expires__lt=datetime.now()).delete()
|
||||||
|
|
||||||
|
# Remove all username rules that have expired
|
||||||
|
UsernameRule.objects.filter(expires__lt=datetime.now()).delete()
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS("Successfully cleaned up old blocks"))
|
||||||
|
|
|
@ -29,3 +29,39 @@ class UserRegistration(models.Model):
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.username
|
return self.username
|
||||||
|
|
||||||
|
|
||||||
|
class IPBlock(models.Model):
|
||||||
|
network = models.GenericIPAddressField()
|
||||||
|
netmask = models.SmallIntegerField(default=-1)
|
||||||
|
reason = models.TextField(null=True, blank=True)
|
||||||
|
timestamp = models.DateTimeField(auto_now_add=True)
|
||||||
|
expires = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.netmask == -1:
|
||||||
|
self.netmask = 32 if self.network.version == 4 else 128
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.network}/{self.netmask}"
|
||||||
|
|
||||||
|
|
||||||
|
class EmailBlock(models.Model):
|
||||||
|
regex = models.CharField(max_length=1024)
|
||||||
|
reason = models.TextField(null=True, blank=True)
|
||||||
|
timestamp = models.DateTimeField(auto_now_add=True)
|
||||||
|
expires = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.email
|
||||||
|
|
||||||
|
|
||||||
|
class UsernameRule(models.Model):
|
||||||
|
regex = models.CharField(max_length=1024)
|
||||||
|
reason = models.TextField(null=True, blank=True)
|
||||||
|
timestamp = models.DateTimeField(auto_now_add=True)
|
||||||
|
expires = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.regex
|
||||||
|
|
|
@ -3,15 +3,31 @@ from django.shortcuts import render, redirect, get_object_or_404
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.core.mail import send_mail
|
from django.core.mail import send_mail
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from .forms import UsernameForm, EmailForm, RegistrationForm
|
from .forms import UsernameForm, EmailForm, RegistrationForm
|
||||||
from .models import UserRegistration
|
from .models import UserRegistration, IPBlock
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from secrets import token_urlsafe
|
from secrets import token_urlsafe
|
||||||
from datetime import datetime, timedelta
|
from datetime import timedelta
|
||||||
from smtplib import SMTPRecipientsRefused
|
from smtplib import SMTPRecipientsRefused
|
||||||
|
from ipaddress import ip_network
|
||||||
|
|
||||||
|
|
||||||
|
class RateLimitMixin:
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
if not settings.TRUST_PROXY:
|
||||||
|
ip_address = request.META.get("REMOTE_ADDR")
|
||||||
|
else:
|
||||||
|
ip_address = request.META.get("HTTP_X_FORWARDED_FOR")
|
||||||
|
|
||||||
|
for block in IPBlock.objects.filter(expiry__gt=timezone.now()):
|
||||||
|
if ip_network(ip_address) in ip_network(f"{block.network}/{block.netmask}"):
|
||||||
|
return render(request, "registration/ratelimit.html", status=429)
|
||||||
|
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class LandingPageView(TemplateView):
|
class LandingPageView(TemplateView):
|
||||||
|
@ -22,7 +38,7 @@ class ErrorPageView(TemplateView):
|
||||||
template_name = "error_page.html"
|
template_name = "error_page.html"
|
||||||
|
|
||||||
|
|
||||||
class CheckUsernameView(FormView):
|
class CheckUsernameView(RateLimitMixin, FormView):
|
||||||
template_name = "registration/username_form.html"
|
template_name = "registration/username_form.html"
|
||||||
form_class = UsernameForm
|
form_class = UsernameForm
|
||||||
success_url = reverse_lazy("email_input")
|
success_url = reverse_lazy("email_input")
|
||||||
|
@ -42,7 +58,7 @@ class CheckUsernameView(FormView):
|
||||||
return self.form_invalid(form)
|
return self.form_invalid(form)
|
||||||
|
|
||||||
|
|
||||||
class EmailInputView(FormView):
|
class EmailInputView(RateLimitMixin, FormView):
|
||||||
template_name = "registration/email_form.html"
|
template_name = "registration/email_form.html"
|
||||||
form_class = EmailForm
|
form_class = EmailForm
|
||||||
|
|
||||||
|
@ -69,7 +85,7 @@ class EmailInputView(FormView):
|
||||||
if (
|
if (
|
||||||
UserRegistration.objects.filter(
|
UserRegistration.objects.filter(
|
||||||
ip_address=ip_address,
|
ip_address=ip_address,
|
||||||
timestamp__gte=datetime.now() - timedelta(hours=24),
|
timestamp__gte=timezone.now() - timedelta(hours=24),
|
||||||
).count()
|
).count()
|
||||||
>= 5
|
>= 5
|
||||||
):
|
):
|
||||||
|
@ -98,12 +114,15 @@ class EmailInputView(FormView):
|
||||||
return render(self.request, "registration/email_sent.html")
|
return render(self.request, "registration/email_sent.html")
|
||||||
|
|
||||||
except SMTPRecipientsRefused:
|
except SMTPRecipientsRefused:
|
||||||
form.add_error("email", "Your email address is invalid, not accepting mail, or blocked by our mail server.")
|
form.add_error(
|
||||||
|
"email",
|
||||||
|
"Your email address is invalid, not accepting mail, or blocked by our mail server.",
|
||||||
|
)
|
||||||
registration.delete()
|
registration.delete()
|
||||||
return self.form_invalid(form)
|
return self.form_invalid(form)
|
||||||
|
|
||||||
|
|
||||||
class VerifyEmailView(View):
|
class VerifyEmailView(RateLimitMixin, View):
|
||||||
def get(self, request, token):
|
def get(self, request, token):
|
||||||
try:
|
try:
|
||||||
registration = UserRegistration.objects.get(token=token)
|
registration = UserRegistration.objects.get(token=token)
|
||||||
|
@ -123,7 +142,7 @@ class VerifyEmailView(View):
|
||||||
return redirect("complete_registration")
|
return redirect("complete_registration")
|
||||||
|
|
||||||
|
|
||||||
class CompleteRegistrationView(FormView):
|
class CompleteRegistrationView(RateLimitMixin, FormView):
|
||||||
template_name = "registration/complete_registration.html"
|
template_name = "registration/complete_registration.html"
|
||||||
form_class = RegistrationForm
|
form_class = RegistrationForm
|
||||||
success_url = reverse_lazy("registration_complete")
|
success_url = reverse_lazy("registration_complete")
|
||||||
|
|
Loading…
Reference in a new issue