[UPDT] GENERAL: Updated 403 page for fail2ban system

This commit is contained in:
Horilla
2025-05-08 14:52:23 +05:30
parent c517ac7a64
commit d9a4dbc703
2 changed files with 225 additions and 42 deletions

View File

@@ -10,7 +10,8 @@ from django.contrib.auth.signals import user_login_failed
from django.db.models import Max, Q from django.db.models import Max, Q
from django.db.models.signals import m2m_changed, post_migrate, post_save from django.db.models.signals import m2m_changed, post_migrate, post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.shortcuts import redirect from django.http import Http404
from django.shortcuts import redirect, render
from base.models import Announcement, PenaltyAccounts from base.models import Announcement, PenaltyAccounts
from horilla.methods import get_horilla_model_class from horilla.methods import get_horilla_model_class
@@ -150,65 +151,92 @@ def log_login_failed(sender, credentials, request, **kwargs):
}, },
} }
# This section is for giving the maxtry and bantime
FAIL2BAN_MAX_RETRY = 3 # Same as maxretry in jail.local
FAIL2BAN_BAN_TIME = 300 # Same as bantime in jail.local (in seconds)
""" """
# Checking that the file is created or not to initiate the ban functions. # Checking that the file is created or not to initiate the ban functions.
if not FAIL2BAN_LOG_ENABLED: if not FAIL2BAN_LOG_ENABLED:
return return
max_attempts = getattr(settings, "FAIL2BAN_MAX_RETRY", 3)
ban_duration = getattr(settings, "FAIL2BAN_BAN_TIME", 300)
username = credentials.get("username", "unknown") username = credentials.get("username", "unknown")
ip = request.META.get("REMOTE_ADDR", "unknown") ip = request.META.get("REMOTE_ADDR", "unknown")
session_key = (
request.session.session_key or request.session._get_or_create_session_key()
)
# Track the number of failed attempts for the user in the current session # Check if currently banned
session_key = request.session.session_key if session_key in ban_time and ban_time[session_key] > time.time():
if not session_key:
request.session.create()
# Initialize failed attempts for this session if not already
if session_key not in failed_attempts:
failed_attempts[session_key] = 0
# Initialize ban time if not already set
if session_key not in ban_time:
ban_time[session_key] = 0
failed_attempts[session_key] += 1
# Log the failed attempt
logger.warning(f"Invalid login attempt for user '{username}' from {ip}")
# Set maximum allowed attempts
max_attempts = 3
attempts_left = max_attempts - failed_attempts[session_key]
# If the user is banned, show banned message and GIF
if ban_time.get(session_key, 0) > time.time():
banned_until = time.strftime("%H:%M", time.localtime(ban_time[session_key])) banned_until = time.strftime("%H:%M", time.localtime(ban_time[session_key]))
messages.info( messages.info(
request, f"You are banned until {banned_until}. Please try again later." request, f"You are banned until {banned_until}. Please try again later."
) )
return redirect("/") return redirect("/")
# If failed attempts are above the limit, ban the user for 5 minutes # If ban expired, reset counters
if session_key in ban_time and ban_time[session_key] <= time.time():
del ban_time[session_key]
if session_key in failed_attempts:
del failed_attempts[session_key]
# Initialize tracking if needed
if session_key not in failed_attempts:
failed_attempts[session_key] = 0
failed_attempts[session_key] += 1
attempts_left = max_attempts - failed_attempts[session_key]
logger.warning(f"Invalid login attempt for user '{username}' from {ip}")
if failed_attempts[session_key] >= max_attempts: if failed_attempts[session_key] >= max_attempts:
# Ban user for 5 minutes (300 seconds) ban_time[session_key] = time.time() + ban_duration
ban_time[session_key] = time.time() + 300
messages.info( messages.info(
request, request,
"You have exceeded the maximum attempts. You are banned for 5 minutes.", f"You have been banned for {ban_duration // 60} minutes due to multiple failed login attempts.",
) )
return redirect("/") # Redirect or show a banned page return redirect("/")
# Display message showing remaining attempts messages.info(
if attempts_left > 0: request,
message = ( f"You have {attempts_left} login attempt(s) left before a temporary ban.",
f"You have {attempts_left} attempts left before being temporarily banned." )
)
else:
message = (
"You have exceeded the maximum attempts. You will be banned for 5 minutes."
)
# Add a message to the session to show to the user
messages.info(request, message)
return redirect("login") return redirect("login")
class Fail2BanMiddleware:
"""
Middleware to force password change for new employees.
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
session_key = request.session.session_key
if not session_key:
request.session.create()
# Check ban and enforce it
if session_key in ban_time and ban_time[session_key] > time.time():
banned_until = time.strftime("%H:%M", time.localtime(ban_time[session_key]))
messages.info(
request, f"You are banned until {banned_until}. Please try again later."
)
return render(request, "403.html")
# If ban expired, clear counters
if session_key in ban_time and ban_time[session_key] <= time.time():
del ban_time[session_key]
if session_key in failed_attempts:
del failed_attempts[session_key]
return self.get_response(request)
settings.MIDDLEWARE.append("base.signals.Fail2BanMiddleware")

155
templates/403.html Normal file
View File

@@ -0,0 +1,155 @@
{% load static %}
<style>
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
* {
margin: 0;
padding: 0;
font-family: "Poppins", sans-serif;
}
.hr-error {
text-align: center;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 2rem;
}
.hr-error_container {
display: flex;
justify-content: space-around;
align-items: center;
max-width: 1180px;
margin-left: auto;
margin-right: auto;
width: 100%;
padding-top: 3rem;
padding-bottom: 3rem;
}
.hr-404 {
font-size: 7rem;
line-height: 7.2rem;
margin-bottom: 0;
}
.hr-error_msg {
font-weight: bold;
font-size: 2rem;
color: #e54f38;
}
.hr-error_btn {
background-color: hsl(8deg, 77%, 56%);
border: 2px solid hsl(8deg, 77%, 56%);
color: hsl(0deg, 0%, 100%);
border-radius: 35px;
padding: 0.5rem 2rem;
text-decoration: none;
margin-top: 2rem;
gap: 0.8rem;
display: inline-flex;
transition: all 0.4s ease-in-out;
align-items: center;
font-size: 1rem;
}
.hr-error_btn:hover {
color: hsl(0deg, 0%, 100%);
background-color: hsl(8deg, 77%, 45%);
border: 2px solid hsl(8deg, 77%, 45%);
}
.hr-error_img img {
width: 100%;
height: 100%;
}
.hr-error_bg {
position: relative;
background-color: #e54f38;
width: 340px;
height: 340px;
border-radius: 50%;
box-shadow: rgb(0 0 0 / 26%) 0px 4px 4px;
}
.hr-error_bg img {
position: absolute;
top: 40px;
left: 0;
}
.hr-bg-img--error{
position: relative;
max-width: 320px;
}
@media (max-width:768px) {
.hr-error_container {
flex-direction: column-reverse;
text-align: center;
}
.hr-error_bg {
position: relative;
background-color: #e54f38;
width: 320px;
height: 320px;
}
.hr-error_content {
padding: 2rem 0;
}
}
</style>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Horilla</title>
<link rel="icon" type="image/png" sizes="32x32" href="{% if white_label_company.icon %}{{white_label_company.icon.url}} {% else %}{% static 'favicons/favicon-32x32.png' %}{% endif %}">
<link rel="icon" type="image/png" sizes="16x16" href="{% if white_label_company.icon %}{{white_label_company.icon.url}} {% else %}{% static 'favicons/favicon-16x16.png' %}{% endif %}">
</head>
<body>
<section class="hr-error_container">
<div class="hr-error">
<div class="hr-error_img hr-error_bg">
<img src="{% static "/images/ui/404_error_image.png" %}">
</div>
<div class="oh-wrapper">
<h1 class="hr-404">403</h1>
<br>
{% for message in messages %}
<div class="oh-alert-container">
<div class="oh-alert oh-alert--animated hr-error_msg {{message.tags}}">
{% if forloop.counter == 2 %}
{% else %}
<div class="alert alert-warning">
{{ message }}
</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
</section>
<script type="module" src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.esm.js"></script>
<script nomodule src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.js"></script>
</body>
</html>