Skip to content

Commit 830c0ea

Browse files
committed
Merged from master
2 parents 0779b5d + fdaede4 commit 830c0ea

File tree

11 files changed

+176
-53
lines changed

11 files changed

+176
-53
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- Email sign up ✉️
1212
- Email verification 📨
1313
- Forgot password 🤔
14+
- Ip block on failed login tries & ip blocklist ✋ (Optional)
1415
- Dark mode 🌚 🌝 Light mode (Optional)
1516

1617
## Development

app/settings.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from pathlib import Path
1414

1515
from django.contrib.messages import constants as message_constants
16+
from django.utils import timezone
1617

1718
from .hackathon_variables import *
1819

@@ -47,6 +48,7 @@
4748
'django.contrib.sessions',
4849
'django.contrib.messages',
4950
'django.contrib.staticfiles',
51+
'captcha',
5052
'django_tables2',
5153
'django_filters',
5254
'django_jwt',
@@ -60,6 +62,7 @@
6062
'application',
6163
'review',
6264
'event',
65+
'axes',
6366
]
6467

6568
MIDDLEWARE = [
@@ -129,6 +132,22 @@
129132
},
130133
]
131134

135+
AUTHENTICATION_BACKENDS = [
136+
# AxesStandaloneBackend should be the first backend in the AUTHENTICATION_BACKENDS list.
137+
'axes.backends.AxesStandaloneBackend',
138+
139+
# Django ModelBackend is the default authentication backend.
140+
'django.contrib.auth.backends.ModelBackend',
141+
]
142+
143+
# django-axes configuration
144+
AXES_USERNAME_FORM_FIELD = 'user.models.User.USERNAME_FIELD'
145+
AXES_COOLOFF_TIME = timezone.timedelta(minutes=5)
146+
AXES_FAILURE_LIMIT = os.environ.get('AXES_FAILURE_LIMIT', 3)
147+
AXES_ENABLED = os.environ.get('AXES_ENABLED', not DEBUG)
148+
AXES_IP_BLACKLIST = os.environ.get('AXES_IP_BLACKLIST', '').split(',')
149+
SILENCED_SYSTEM_CHECKS = ['axes.W002']
150+
132151

133152
# Internationalization
134153
# https://docs.djangoproject.com/en/4.0/topics/i18n/
@@ -201,9 +220,16 @@
201220
message_constants.ERROR: 'danger',
202221
}
203222

204-
# Google recaptcha
205-
GOOGLE_RECAPTCHA_SECRET_KEY = os.environ.get('GOOGLE_RECAPTCHA_SECRET_KEY', '')
206-
GOOGLE_RECAPTCHA_SITE_KEY = os.environ.get('GOOGLE_RECAPTCHA_SITE_KEY', '')
223+
# Google Recaptcha configuration
224+
RECAPTCHA_PUBLIC_KEY = os.environ.get('RECAPTCHA_PUBLIC_KEY', '')
225+
RECAPTCHA_PRIVATE_KEY = os.environ.get('RECAPTCHA_PRIVATE_KEY', '')
226+
RECAPTCHA_WIDGET = os.environ.get('RECAPTCHA_WIDGET', 'ReCaptchaV2Checkbox')
227+
RECAPTCHA_REGISTER = True
228+
RECAPTCHA_LOGIN = False
229+
try:
230+
RECAPTCHA_REQUIRED_SCORE = float(os.environ.get('RECAPTCHA_REQUIRED_SCORE', "0.85"))
231+
except ValueError:
232+
RECAPTCHA_REQUIRED_SCORE = 0.85
207233

208234
# Login tries
209235
LOGIN_TRIES = 1000 if DEBUG else 4

app/static/css/main.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,7 @@ footer {
147147
.bg-light .bg-contrast {
148148
background-color: rgba(0, 0, 0, 0.075);
149149
}
150+
151+
.g-recaptcha {
152+
display: inline-block;
153+
}

requirements.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,21 @@ cryptography==37.0.2
88
Deprecated==1.2.13
99
Django==4.0.7
1010
django-appconf==1.0.5
11+
django-axes==5.39.0
1112
django-bootstrap5==21.3
1213
django-colorfield==0.7.2
1314
django-compressor==4.1
1415
django-cors-headers==3.13.0
1516
django-crontab==0.7.1
1617
django-filter==22.1
18+
django-ipware==4.0.2
1719
django-jwt-oidc==0.3.9
1820
django-libsass==0.9
21+
django-recaptcha==3.0.0
1922
django-tables2==2.4.1
2023
flake8==4.0.1
2124
idna==3.3
22-
jwcrypto==1.3.1
25+
jwcrypto==1.4
2326
libsass==0.21.0
2427
mccabe==0.6.1
2528
Pillow==9.2.0

review/views.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,9 @@ def get_context_data(self, **kwargs):
121121
context = super().get_context_data(**kwargs)
122122
application = self.get_application()
123123
if application is not None:
124-
details = {_('Full Name'): application.user.get_full_name(), _('Status'): application.get_status_display(),
125-
_('Promotion'): application.promotional_code.name}
124+
details = {_('Full Name'): application.user.get_full_name(), _('Status'): application.get_status_display()}
125+
if application.promotional_code:
126+
details[_('Promotion')] = application.promotional_code.name
126127
ApplicationForm = self.get_form(application.type)
127128
for name, value in application.form_data.items():
128129
if isinstance(value, FileField):

user/admin.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from django.contrib.auth.models import Group
44

55
from user.forms import UserChangeForm, UserCreationForm
6-
from user.models import User, BlockedUser, LoginRequest
6+
from user.models import User, BlockedUser
77

88

99
class UserAdmin(BaseUserAdmin):
@@ -45,5 +45,4 @@ class GroupAdmin(BaseGroupAdmin):
4545
admin.site.register(User, UserAdmin)
4646
admin.site.unregister(Group)
4747
admin.site.register(Group, GroupAdmin)
48-
admin.site.register(LoginRequest)
4948
admin.site.register(BlockedUser)

user/forms.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import re
22

3+
from captcha.fields import ReCaptchaField
4+
from captcha import widgets as captcha_widgets
35
from django import forms
46
from django.conf import settings
57
from django.contrib.auth import password_validation
@@ -8,10 +10,34 @@
810
from django.utils.safestring import mark_safe
911

1012
from app.mixins import BootstrapFormMixin
13+
from app.utils import get_theme
1114
from user.models import User
1215
from django.utils.translation import gettext_lazy as _
1316

1417

18+
class RecaptchaForm(forms.Form):
19+
@classmethod
20+
def active(cls):
21+
return getattr(settings, 'RECAPTCHA_PUBLIC_KEY', False) and getattr(settings, 'RECAPTCHA_PRIVATE_KEY', False)
22+
23+
def __init__(self, *args, **kwargs):
24+
request = kwargs.pop('request', None)
25+
widget_setting = getattr(settings, 'RECAPTCHA_WIDGET', 'ReCaptchaV2Checkbox')
26+
widget_class = getattr(captcha_widgets, widget_setting, captcha_widgets.ReCaptchaV2Checkbox)
27+
if widget_class == captcha_widgets.ReCaptchaBase or not issubclass(widget_class, captcha_widgets.ReCaptchaBase):
28+
widget_class = captcha_widgets.ReCaptchaV2Checkbox
29+
theme = get_theme(request) if request is not None else 'light'
30+
self.base_fields['captcha'] = ReCaptchaField(
31+
widget=widget_class(attrs={'data-theme': theme}),
32+
error_messages={'required': _('You must pass the reCAPTCHA challenge!')})
33+
super().__init__(*args, **kwargs)
34+
35+
captcha = ReCaptchaField(
36+
widget=captcha_widgets.ReCaptchaV2Checkbox(),
37+
error_messages={'required': _('You must pass the reCAPTCHA challenge!')}
38+
)
39+
40+
1541
class LoginForm(BootstrapFormMixin, forms.Form):
1642
bootstrap_field_info = {'': {'fields': [{'name': 'email', 'space': 12}, {'name': 'password', 'space': 12}]}}
1743

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Generated by Django 4.0.7 on 2022-10-01 14:04
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('user', '0005_user_qr_code_alter_user_diet'),
10+
]
11+
12+
operations = [
13+
migrations.DeleteModel(
14+
name='LoginRequest',
15+
),
16+
]

user/models.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -222,18 +222,6 @@ def get_users_with_permissions(cls, perms):
222222
Q(is_superuser=True)).distinct()
223223

224224

225-
class LoginRequest(models.Model):
226-
ip = models.CharField(max_length=30)
227-
latest_request = models.DateTimeField()
228-
login_tries = models.IntegerField(default=1)
229-
230-
def __str__(self):
231-
return self.ip
232-
233-
def reset_tries(self):
234-
self.login_tries = 1
235-
236-
237225
class BlockedUser(models.Model):
238226
full_name = models.CharField(max_length=100)
239227
email = models.EmailField(max_length=100)

user/templates/auth.html

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,34 @@
88
{% include 'components/tabs.html' %}
99
{% endif %}
1010
<div class="bg-{{ theme }} text-{% if theme == 'dark' %}white{% else %}black{% endif %}">
11-
<form method="post">
12-
{% csrf_token %}
13-
<div class="content p-4">
14-
{% include 'components/bootstrap5_form.html' %}
15-
{% if auth == 'register' %}
16-
{% if captcha_site_key %}
17-
<script src='https://www.google.com/recaptcha/api.js'></script>
18-
<div class="g-recaptcha" data-sitekey="{{ captcha_site_key }}"></div>
11+
{% if not blocked_message %}
12+
<form method="post">
13+
{% csrf_token %}
14+
<div class="content p-4">
15+
{% include 'components/bootstrap5_form.html' %}
16+
{% if auth != 'register' %}
17+
<p><a class="text-{% if theme == 'dark' %}white{% else %}black{% endif %}" style="text-decoration: none" href="{% url 'forgot_password' %}">{% translate 'Forgot your password?' %}</a></p>
1918
{% endif %}
20-
{% else %}
21-
<p><a class="text-{% if theme == 'dark' %}white{% else %}black{% endif %}" style="text-decoration: none" href="{% url 'forgot_password' %}">{% translate 'Forgot your password?' %}</a></p>
22-
{% endif %}
23-
<div class="row justify-content-around">
24-
<div class="col-12 col-lg-6 d-grid d-md-block">
25-
<button type="submit" class="btn btn-primary col-12">{{ auth|title }}</button>
19+
{% for field in recaptcha_form %}
20+
<div class="text-center mb-1">
21+
{{ field }}
22+
</div>
23+
{% endfor %}
24+
<div class="row justify-content-around">
25+
<div class="col-12 col-lg-6 d-grid d-md-block">
26+
<button type="submit" class="btn btn-primary col-12">{{ auth|title }}</button>
27+
</div>
2628
</div>
2729
</div>
30+
</form>
31+
{% else %}
32+
<div class="p-3">
33+
<div class="alert alert-danger alert-dismissible fade show" role="alert">
34+
{{ blocked_message }}
35+
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
36+
</div>
2837
</div>
29-
</form>
38+
{% endif %}
3039
</div>
3140
</div>
3241
</div>

0 commit comments

Comments
 (0)