[ADD] BASE: 2 Factor Authentication via email (beta)
This commit is contained in:
@@ -245,7 +245,9 @@ def new_init(
|
||||
if request and request.user and request.user.is_authenticated:
|
||||
user_id = request.user.pk
|
||||
reply_to = cache.get(f"reply_to{user_id}") if not reply_to else reply_to
|
||||
from_email = cache.get(f"dynamic_display_name{user_id}")
|
||||
|
||||
if not from_email:
|
||||
from_email = cache.get(f"dynamic_display_name{user_id}")
|
||||
|
||||
message_init(
|
||||
self,
|
||||
|
||||
@@ -85,6 +85,10 @@ def update_selected_company(request):
|
||||
This method is used to update the selected company on the session
|
||||
"""
|
||||
company_id = request.GET.get("company_id")
|
||||
user = request.user.employee_get
|
||||
user_company = getattr(
|
||||
getattr(user, "employee_work_info", None), "company_id", None
|
||||
)
|
||||
request.session["selected_company"] = company_id
|
||||
company = (
|
||||
AllCompany()
|
||||
@@ -103,21 +107,13 @@ def update_selected_company(request):
|
||||
if re.match(pattern, previous_path):
|
||||
employee_id = get_last_section(previous_path)
|
||||
employee = Employee.objects.filter(id=employee_id).first()
|
||||
|
||||
if (
|
||||
not EmployeeWorkInformation.objects.filter(
|
||||
employee_id=employee_id
|
||||
).exists()
|
||||
or employee.employee_work_info.company_id != company
|
||||
):
|
||||
emp_company = getattr(
|
||||
getattr(employee, "employee_work_info", None), "company_id", None
|
||||
)
|
||||
if emp_company != company:
|
||||
text = "Other Company"
|
||||
if (
|
||||
company_id
|
||||
== request.user.employee_get.employee_work_info.company_id
|
||||
):
|
||||
if company_id == user_company:
|
||||
text = "My Company"
|
||||
if company_id == "all":
|
||||
text = "All companies"
|
||||
company = {
|
||||
"company": company.company,
|
||||
"icon": company.icon.url,
|
||||
@@ -134,11 +130,13 @@ def update_selected_company(request):
|
||||
"""
|
||||
)
|
||||
|
||||
text = "Other Company"
|
||||
if company_id == request.user.employee_get.employee_work_info.company_id:
|
||||
text = "My Company"
|
||||
if company_id == "all":
|
||||
text = "All companies"
|
||||
elif company_id == user_company:
|
||||
text = "My Company"
|
||||
else:
|
||||
text = "Other Company"
|
||||
|
||||
company = {
|
||||
"company": company.company,
|
||||
"icon": company.icon.url,
|
||||
|
||||
@@ -1007,3 +1007,12 @@ def template_pdf(template, context={}, html=False, filename="payslip.pdf"):
|
||||
return response
|
||||
except Exception as e:
|
||||
return HttpResponse(f"Error generating PDF: {str(e)}", status=500)
|
||||
|
||||
|
||||
def generate_otp():
|
||||
"""
|
||||
Function to generate a random 6-digit OTP (One-Time Password).
|
||||
Returns:
|
||||
str: A 6-digit random OTP as a string.
|
||||
"""
|
||||
return str(random.randint(100000, 999999))
|
||||
|
||||
@@ -3,10 +3,12 @@ middleware.py
|
||||
"""
|
||||
|
||||
from django.apps import apps
|
||||
from django.contrib import messages
|
||||
from django.core.cache import cache
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import redirect
|
||||
|
||||
from base.backends import ConfiguredEmailBackend
|
||||
from base.context_processors import AllCompany
|
||||
from base.horilla_company_manager import HorillaCompanyManager
|
||||
from base.models import Company, ShiftRequest, WorkTypeRequest
|
||||
@@ -16,6 +18,7 @@ from employee.models import (
|
||||
EmployeeBankDetails,
|
||||
EmployeeWorkInformation,
|
||||
)
|
||||
from horilla.horilla_apps import TWO_FACTORS_AUTHENTICATION
|
||||
from horilla.horilla_settings import APPS
|
||||
from horilla.methods import get_horilla_model_class
|
||||
from horilla_documents.models import DocumentRequest
|
||||
@@ -55,12 +58,23 @@ class CompanyMiddleware:
|
||||
"""
|
||||
Set the company session data based on the company ID.
|
||||
"""
|
||||
user = request.user.employee_get
|
||||
user_company_id = getattr(
|
||||
getattr(user, "employee_work_info", None), "company_id", None
|
||||
)
|
||||
if company_id and request.session.get("selected_company") != "all":
|
||||
if company_id == "all":
|
||||
text = "All companies"
|
||||
elif company_id == user_company_id:
|
||||
text = "My Company"
|
||||
else:
|
||||
text = "Other Company"
|
||||
|
||||
request.session["selected_company"] = str(company_id.id)
|
||||
request.session["selected_company_instance"] = {
|
||||
"company": company_id.company,
|
||||
"icon": company_id.icon.url,
|
||||
"text": "My company",
|
||||
"text": text,
|
||||
"id": company_id.id,
|
||||
}
|
||||
else:
|
||||
@@ -189,3 +203,37 @@ class ForcePasswordChangeMiddleware:
|
||||
return redirect("change-password")
|
||||
|
||||
return self.get_response(request)
|
||||
|
||||
|
||||
class TwoFactorAuthMiddleware:
|
||||
"""
|
||||
Middleware to enforce two-factor authentication for specific users.
|
||||
"""
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
excluded_paths = [
|
||||
"/change-password",
|
||||
"/login",
|
||||
"/logout",
|
||||
"/two-factor",
|
||||
"/send-otp",
|
||||
]
|
||||
|
||||
if request.path.rstrip("/") in excluded_paths:
|
||||
return self.get_response(request)
|
||||
|
||||
if TWO_FACTORS_AUTHENTICATION:
|
||||
try:
|
||||
if ConfiguredEmailBackend().configuration is not None:
|
||||
if hasattr(request, "user") and request.user.is_authenticated:
|
||||
if not request.session.get("otp_code_verified", False):
|
||||
return redirect("/two-factor")
|
||||
else:
|
||||
return self.get_response(request)
|
||||
except Exception as e:
|
||||
return self.get_response(request)
|
||||
|
||||
return self.get_response(request)
|
||||
|
||||
187
base/templates/base/auth/two_factor_auth.html
Normal file
187
base/templates/base/auth/two_factor_auth.html
Normal file
@@ -0,0 +1,187 @@
|
||||
{% load static %} {% load i18n %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Login - {{white_label_company_name}} Dashboard</title>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="{% if white_label_company.icon %}{{white_label_company.icon.url}} {% else %}{% static 'favicons/apple-touch-icon.png' %}{% endif %}"
|
||||
/>
|
||||
<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 %}"
|
||||
/>
|
||||
<link rel="stylesheet" href="{% static '/build/css/style.min.css' %}" />
|
||||
<link rel="manifest" href="{% static 'build/manifest.json' %}" />
|
||||
</head>
|
||||
<style>
|
||||
.oh-opacity-0 {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
|
||||
<div id="main">
|
||||
<div class="oh-alert-container">
|
||||
{% for message in messages %}
|
||||
<div class="oh-alert oh-alert--animated {{ message.tags }}">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<main class="oh-auth">
|
||||
<div
|
||||
class="oh-onboarding-card"
|
||||
style="max-height: 790px; max-width: 975px"
|
||||
id="ohAuthCard"
|
||||
>
|
||||
<div class="oh-onboarding-card__header">
|
||||
<span class="oh-onboarding-card__company-name"
|
||||
>{{white_label_company_name}} HRMS</span
|
||||
>
|
||||
</div>
|
||||
<h1
|
||||
class="oh-onboarding-card__title oh-onboarding-card__title--h2 text-center my-3"
|
||||
>
|
||||
{% trans "Two Factor Authentication" %}
|
||||
</h1>
|
||||
<p class="text-muted text-center">
|
||||
{% trans "Enter the OTP send to your email: " %}
|
||||
<b>{{request.user.employee_get.get_mail}}</b>.
|
||||
</p>
|
||||
<div id="OtpContainer" >
|
||||
{% if request.session.otp_code %}
|
||||
<form
|
||||
class="oh-form-group"
|
||||
method="POST"
|
||||
action="{% url 'two-factor' %}"
|
||||
>
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
<div class="col-12 col-sm-12 col-md-12 col-lg-12">
|
||||
<div class="oh-input-group">
|
||||
<label class="oh-label" for="password"
|
||||
>{% trans "Enter OTP" %}</label
|
||||
>
|
||||
<div class="oh-password-input-container">
|
||||
<input
|
||||
type="password"
|
||||
id="otp"
|
||||
name="otp"
|
||||
class="oh-input oh-input--password w-100"
|
||||
placeholder="Enter OTP"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="oh-btn oh-btn--transparent oh-password-input--toggle"
|
||||
>
|
||||
<ion-icon
|
||||
class="oh-passowrd-input__show-icon"
|
||||
title="Show Password"
|
||||
name="eye-outline"
|
||||
></ion-icon>
|
||||
<ion-icon
|
||||
class="oh-passowrd-input__hide-icon d-none"
|
||||
title="Hide Password"
|
||||
name="eye-off-outline"
|
||||
></ion-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="oh-modal__dialog-footer p-0 mt-3 justify-content-between">
|
||||
<div class="mt-3 oh-text--secondary oh-opacity-0" id="otp-timer">
|
||||
<span class=""> {% trans "Resend OTP in" %} <span class="time fw-bold">20</span></span>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="oh-btn oh-btn--secondary-outline"
|
||||
role="button"
|
||||
>
|
||||
{% trans "Authenticate" %}
|
||||
<ion-icon
|
||||
class="ms-2"
|
||||
name="arrow-forward-outline"
|
||||
></ion-icon>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<form
|
||||
class="oh-form-group"
|
||||
hx-post="{% url 'send-otp' %}"
|
||||
hx-target="#OtpContainer"
|
||||
hx-select="#OtpContainer"
|
||||
hx-swap="outerHTML"
|
||||
>
|
||||
{% csrf_token %}
|
||||
<div class="oh-modal__dialog-footer p-0 mt-3 justify-content-center">
|
||||
<button
|
||||
type="submit"
|
||||
class="oh-btn oh-btn--secondary-outline m-2"
|
||||
role="button"
|
||||
>
|
||||
{% trans "Send OTP" %}
|
||||
<ion-icon
|
||||
class="ms-2"
|
||||
name="arrow-forward-outline"
|
||||
></ion-icon>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<img src={% if white_label_company.icon %}
|
||||
"{{white_label_company.icon.url}}" {% else %} "{% static 'images/ui/auth-logo.png' %}" {% endif %} alt="Horilla" />
|
||||
</main>
|
||||
</div>
|
||||
<script src="{% static '/build/js/web.frontend.min.js' %}"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
|
||||
<script src="{% static 'htmx/htmx.min.js' %}"></script>
|
||||
<script src="{% static 'build/js/hxSelect2.js' %}"></script>
|
||||
<script src="{% static '/index/index.js' %}"></script>
|
||||
<script type="module" src="{% static 'images/ionicons/ui_icons/ionicons/ionicons.esm.js' %}"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@10"></script>
|
||||
<script>
|
||||
$(document).on("htmx:beforeRequest", function (event, data) {
|
||||
var response = event.detail.xhr.response;
|
||||
var target = $(event.detail.elt.getAttribute("hx-target"));
|
||||
if (!target.closest("form").length) {
|
||||
target.html();
|
||||
}
|
||||
});
|
||||
setTimeout(function () {
|
||||
var timer = $("#otp-timer");
|
||||
timer.removeClass("oh-opacity-0");
|
||||
|
||||
let counter = 20;
|
||||
let $timeSpan = timer.find(".time");
|
||||
let resendUrl = "{% url 'send-otp' %}";
|
||||
|
||||
let interval = setInterval(function () {
|
||||
counter--;
|
||||
if (counter <= 0) {
|
||||
clearInterval(interval);
|
||||
$timeSpan.html(`<a href='${resendUrl}' class="oh-text--secondary">{% trans "Resend" %}`);
|
||||
} else {
|
||||
$timeSpan.text(`${counter}`);
|
||||
}
|
||||
}, 1000);
|
||||
}, 5000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -60,3 +60,4 @@ SIDEBARS = [
|
||||
|
||||
WHITE_LABELLING = False
|
||||
NESTED_SUBORDINATE_VISIBILITY = False
|
||||
TWO_FACTORS_AUTHENTICATION = False
|
||||
|
||||
@@ -17,6 +17,7 @@ MIDDLEWARE.append("horilla.horilla_middlewares.ThreadLocalMiddleware")
|
||||
MIDDLEWARE.append("accessibility.middlewares.AccessibilityMiddleware")
|
||||
MIDDLEWARE.append("accessibility.middlewares.AccessibilityMiddleware")
|
||||
MIDDLEWARE.append("base.middleware.ForcePasswordChangeMiddleware")
|
||||
MIDDLEWARE.append("base.middleware.TwoFactorAuthMiddleware")
|
||||
_thread_locals = threading.local()
|
||||
|
||||
|
||||
|
||||
@@ -684,6 +684,7 @@ $(document).on("htmx:beforeRequest", function (event, data) {
|
||||
"BiometricDeviceTestFormTarget",
|
||||
"reloadMessages",
|
||||
"infinite",
|
||||
"OtpContainer"
|
||||
];
|
||||
var avoid_target_class = ["oh-badge--small"];
|
||||
if (
|
||||
|
||||
Reference in New Issue
Block a user