Skip to content

Commit c6b022c

Browse files
Add a honeypot field to some forms
1 parent ca0b33d commit c6b022c

File tree

7 files changed

+96
-18
lines changed

7 files changed

+96
-18
lines changed

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ CACHE_URL=""
9393
# The default e-mail address of your site.
9494
EMAIL_ADDRESS="<user>@${DOMAIN}"
9595

96+
# The name and email address of the site's admin.
97+
ADMIN="<name>,${EMAIL_ADDRESS}"
98+
9699
# The time zone of your server. You can find it with this command:
97100
# timedatectl | awk '/Time zone/{print $3}'
98101
TIME_ZONE=UTC

MangAdventure/settings.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import re
88
from importlib.util import find_spec
99
from pathlib import Path
10+
from urllib.parse import urlsplit
1011

1112
from yaenv import Env
1213

@@ -27,8 +28,7 @@
2728
#: See :setting:`ALLOWED_HOSTS`.
2829
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', [
2930
'127.0.0.1', '0.0.0.0', 'localhost', '[::1]',
30-
# From https://stackoverflow.com/questions/9626535/#36609868
31-
env['DOMAIN'].split('//')[-1].split('/')[0].split('?')[0]
31+
urlsplit(env['DOMAIN']).hostname
3232
])
3333

3434
#: | A boolean that turns debug mode on/off. See :setting:`DEBUG`.
@@ -40,6 +40,9 @@
4040
#: | **SECURITY WARNING: this must be kept secret!**
4141
SECRET_KEY = env.secret('SECRET_KEY')
4242

43+
#: A list of site administrators. See :setting:`ADMINS`.
44+
ADMINS = [tuple(env.list('ADMIN', []))] if 'ADMIN' in env else []
45+
4346
#: The ID of the current site. See :setting:`SITE_ID`.
4447
SITE_ID = 1
4548

@@ -167,8 +170,7 @@
167170
re.compile(r'^/api'),
168171
]
169172

170-
LOGS_DIR = BASE_DIR / 'logs'
171-
LOGS_DIR.mkdir(exist_ok=True)
173+
(LOGS_DIR := BASE_DIR / 'logs').mkdir(exist_ok=True)
172174

173175
#: Logging configuration dictionary. See :setting:`LOGGING`.
174176
LOGGING = {
@@ -183,11 +185,11 @@
183185
'verbose': {
184186
'format': '{levelname} {asctime} {pathname}'
185187
' {funcName}:{lineno} {message}',
186-
'style': '{',
188+
'style': '{'
187189
},
188190
'simple': {
189191
'format': '{asctime} {module} {funcName} {message}',
190-
'style': '{',
192+
'style': '{'
191193
},
192194
},
193195
'handlers': {
@@ -196,13 +198,13 @@
196198
'class': 'logging.FileHandler',
197199
'filters': ['require_debug_true'],
198200
'filename': LOGS_DIR / 'debug.log',
199-
'formatter': 'verbose',
201+
'formatter': 'verbose'
200202
},
201203
'error': {
202204
'level': 'ERROR',
203205
'class': 'logging.FileHandler',
204206
'filename': LOGS_DIR / 'errors.log',
205-
'formatter': 'simple',
207+
'formatter': 'simple'
206208
},
207209
'query': {
208210
'level': 'DEBUG',
@@ -338,6 +340,13 @@
338340
#: See :auth:`ACCOUNT_EMAIL_VERIFICATION <configuration.html>`.
339341
ACCOUNT_EMAIL_VERIFICATION = 'mandatory'
340342

343+
#: Override some of the builtin forms.
344+
#: See :auth:`ACCOUNT_FORMS <configuration.html>`.
345+
ACCOUNT_FORMS = {
346+
'signup': 'users.forms.RegistrationForm',
347+
'reset_password': 'users.forms.PasswordResetForm'
348+
}
349+
341350
#: The social account adapter class to use.
342351
#: See :auth:`SOCIALACCOUNT_ADAPTER <configuration.html>`.
343352
SOCIALACCOUNT_ADAPTER = 'users.adapters.SocialAccountAdapter'

MangAdventure/templates/account/password_reset.html

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@ <h1 class="text-shadow alter-fg">Password Reset</h1>
1515
<form method="POST" action="{% url 'account_reset_password' %}">
1616
{% csrf_token %}
1717
{% for item in form %}
18-
<div class="field">
19-
<label for="{{ item.name }}">{{ item.label }}</label>
20-
<input type="{{ item.field.widget.input_type }}" class="input" name="{{ item.name }}"
21-
id="{{ item.name }}" placeholder="{{ item.field.widget.attrs.placeholder }}" required>
18+
<div class="field{% if item.name == 'email2' %} no-display{% endif %}">
19+
<input type="{{ item.field.widget.input_type }}" class="input"
20+
name="{{ item.name }}" id="{{ item.name }}"
21+
placeholder="{{ item.field.widget.attrs.placeholder }}"
22+
{% if item.field.required %}required{% endif %}>
2223
{% if item.errors %}
2324
{% for error in item.errors %}
2425
<p class="error">{{ error|escape }}</p>

MangAdventure/templates/account/signup.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ <h1 class="text-shadow alter-bg">Sign up</h1>
1414
<form method="POST" action="{% url 'account_signup' %}">
1515
{% csrf_token %}
1616
{% for item in form %}
17-
<div class="field">
17+
<div class="field{% if item.name == 'email2' %} no-display{% endif %}">
1818
<label for="{{ item.name }}">{{ item.label }}</label>
1919
<input type="{{ item.field.widget.input_type }}" class="input" name="{{ item.name }}"
2020
id="{{ item.name }}" placeholder="{{ item.field.widget.attrs.placeholder }}"
21-
required{% if 'password' in item.name %} autocomplete="off"{% endif %}>
21+
{% if 'password' in item.name %}autocomplete="off"{% endif %}
22+
{% if item.field.required %}required{% endif %}>
2223
{% if item.errors %}
2324
{% for error in item.errors %}
2425
<p class="error">{{ error|escape }}</p>

docs/changelog.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Changelog
44
v0.9.5
55
^^^^^^
66

7+
* Added a honeypot field to some forms
78
* Added last login date to the user list
89
* Removed Reddit OAuth provider
910
* Reduced prefetched pages from 3 to 2

reader/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
def capture_exception(_): pass # noqa: E704
4848

4949
_update_lock = Lock()
50-
_logger = getLogger('django.db')
50+
_logger = getLogger('django.db.models')
5151

5252

5353
def _cover_uploader(obj: Series, name: str) -> str:

users/forms.py

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,85 @@
11
"""Form models for the users app."""
22

3-
from typing import cast
3+
from importlib.util import find_spec
4+
from typing import Optional, cast
45

56
from django import forms
67
from django.contrib.auth.models import User
78
from django.contrib.auth.validators import UnicodeUsernameValidator
89

10+
from allauth.account.forms import ResetPasswordForm, SignupForm
11+
912
from MangAdventure.validators import FileSizeValidator
1013

1114
from .models import UserProfile
1215

16+
if find_spec('sentry_sdk'): # pragma: no cover
17+
from sentry_sdk import capture_message, configure_scope
18+
19+
def _log_honeypot(message: str, username: Optional[str], email: str):
20+
with configure_scope() as scope:
21+
scope.set_tag('username', username)
22+
scope.set_tag('email', email)
23+
capture_message(message, 'warning', scope)
24+
else: # pragma: no cover
25+
from django.core.mail import mail_admins
26+
27+
def _log_honeypot(message: str, username: Optional[str], email: str):
28+
body = f'Username: {username or "N/A"}\nE-mail: {email}'
29+
mail_admins(message, body, fail_silently=True)
30+
31+
32+
class RegistrationForm(SignupForm): # pragma: no cover
33+
"""Registration form with a honeypot field."""
34+
email2 = forms.EmailField(
35+
label='Email (again)',
36+
required=False,
37+
widget=forms.EmailInput(
38+
attrs={
39+
'placeholder': 'Email address confirmation'
40+
}
41+
)
42+
)
43+
44+
def clean(self):
45+
result = super().clean()
46+
if self.cleaned_data.get('email2'):
47+
msg = 'Possible spam bot detected'
48+
username = self.cleaned_data['username']
49+
email = self.cleaned_data['email']
50+
_log_honeypot(msg, username, email)
51+
raise forms.ValidationError('Nope!')
52+
return result
53+
54+
55+
class PasswordResetForm(ResetPasswordForm): # pragma: no cover
56+
"""Password reset form with a honeypot field."""
57+
email2 = forms.EmailField(
58+
label='Email (again)',
59+
required=False,
60+
widget=forms.EmailInput(
61+
attrs={
62+
'placeholder': 'Email address confirmation'
63+
}
64+
)
65+
)
66+
67+
def clean(self):
68+
result = super().clean()
69+
if self.cleaned_data.get('email2'):
70+
msg = 'Possible spam bot detected'
71+
email = self.cleaned_data['email']
72+
_log_honeypot(msg, None, email)
73+
raise forms.ValidationError('Nope!')
74+
return result
75+
1376

1477
class UserProfileForm(forms.ModelForm):
1578
"""Form used for editing a :class:`~users.models.UserProfile` model."""
1679
#: The user's e-mail address.
1780
email = forms.EmailField(
1881
max_length=254, min_length=5, label='E-mail',
19-
widget=forms.TextInput(attrs={
82+
widget=forms.EmailInput(attrs={
2083
'placeholder': 'E-mail address'
2184
})
2285
)
@@ -166,4 +229,4 @@ class Meta:
166229
)
167230

168231

169-
__all__ = ['UserProfileForm']
232+
__all__ = ['RegistrationForm', 'PasswordResetForm', 'UserProfileForm']

0 commit comments

Comments
 (0)