diff --git a/base/backends.py b/base/backends.py index bada9f045..ca5f043e1 100644 --- a/base/backends.py +++ b/base/backends.py @@ -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, diff --git a/base/context_processors.py b/base/context_processors.py index 0e874abfb..ae4940f17 100644 --- a/base/context_processors.py +++ b/base/context_processors.py @@ -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, diff --git a/base/methods.py b/base/methods.py index e4522de17..aa8b8f188 100644 --- a/base/methods.py +++ b/base/methods.py @@ -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)) diff --git a/base/middleware.py b/base/middleware.py index 63011ecf5..e29130d94 100644 --- a/base/middleware.py +++ b/base/middleware.py @@ -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) diff --git a/base/templates/base/auth/two_factor_auth.html b/base/templates/base/auth/two_factor_auth.html new file mode 100644 index 000000000..0aca6471b --- /dev/null +++ b/base/templates/base/auth/two_factor_auth.html @@ -0,0 +1,187 @@ +{% load static %} {% load i18n %} + + + + + + + Login - {{white_label_company_name}} Dashboard + + + + + + + + +
+
+ {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} +
+
+
+
+ {{white_label_company_name}} HRMS +
+

+ {% trans "Two Factor Authentication" %} +

+

+ {% trans "Enter the OTP send to your email: " %} + {{request.user.employee_get.get_mail}}. +

+
+ {% if request.session.otp_code %} +
+ {% csrf_token %} +
+
+
+ +
+ + +
+
+
+
+ +
+ {% else %} +
+ {% csrf_token %} + +
+ {% endif %} +
+
+ Horilla +
+
+ + + + + + + + + + diff --git a/horilla/horilla_apps.py b/horilla/horilla_apps.py index 3a0a26ab9..1dba2ec1f 100644 --- a/horilla/horilla_apps.py +++ b/horilla/horilla_apps.py @@ -60,3 +60,4 @@ SIDEBARS = [ WHITE_LABELLING = False NESTED_SUBORDINATE_VISIBILITY = False +TWO_FACTORS_AUTHENTICATION = False diff --git a/horilla/horilla_middlewares.py b/horilla/horilla_middlewares.py index 4275dfa4b..9a1b14b05 100644 --- a/horilla/horilla_middlewares.py +++ b/horilla/horilla_middlewares.py @@ -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() diff --git a/static/index/index.js b/static/index/index.js index b017b7245..6c2c9365f 100644 --- a/static/index/index.js +++ b/static/index/index.js @@ -684,6 +684,7 @@ $(document).on("htmx:beforeRequest", function (event, data) { "BiometricDeviceTestFormTarget", "reloadMessages", "infinite", + "OtpContainer" ]; var avoid_target_class = ["oh-badge--small"]; if (