diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc
index c895336..d628931 100644
Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ
diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc
index 9911f8c..aaf076f 100644
Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ
diff --git a/core/templates/core/login.html b/core/templates/core/login.html
index 09f4d96..03b6c07 100644
--- a/core/templates/core/login.html
+++ b/core/templates/core/login.html
@@ -19,78 +19,32 @@
-
-
+
+
{% trans "Don't have an account?" %}
@@ -105,15 +59,6 @@
-
-
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/core/templates/core/select_2fa_method.html b/core/templates/core/select_2fa_method.html
new file mode 100644
index 0000000..fac553e
--- /dev/null
+++ b/core/templates/core/select_2fa_method.html
@@ -0,0 +1,36 @@
+{% extends "base.html" %}
+{% load i18n static %}
+
+{% block content %}
+
+
+
+
+
+
{% trans "Two-Factor Authentication" %}
+
{% trans "Please choose how you would like to receive your verification code." %}
+
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/core/templates/core/verify_2fa_otp.html b/core/templates/core/verify_2fa_otp.html
new file mode 100644
index 0000000..77da6bc
--- /dev/null
+++ b/core/templates/core/verify_2fa_otp.html
@@ -0,0 +1,38 @@
+{% extends "base.html" %}
+{% load i18n static %}
+
+{% block content %}
+
+
+
+
+
+
{% trans "Verify OTP" %}
+
{% trans "Enter the 6-digit code sent to your selected method." %}
+
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/core/urls.py b/core/urls.py
index 685628e..b737738 100644
--- a/core/urls.py
+++ b/core/urls.py
@@ -5,7 +5,9 @@ from . import api_views
urlpatterns = [
path('', views.index, name='index'),
- path('login/', auth_views.LoginView.as_view(template_name='core/login.html'), name='login'),
+ path('login/', views.CustomLoginView.as_view(), name='login'),
+ path('login/select-method/', views.select_2fa_method, name='select_2fa_method'),
+ path('login/verify-2fa/', views.verify_2fa_otp, name='verify_2fa_otp'),
path('logout/', auth_views.LogoutView.as_view(next_page='/'), name='logout'),
# Registration Flow
@@ -84,4 +86,4 @@ urlpatterns = [
# Root-level Aliases (for apps hardcoded to /shipments/)
path('shipments/', api_views.ParcelListCreateView.as_view(), name='root_shipment_list'),
path('shipments/
/', api_views.ParcelDetailView.as_view(), name='root_shipment_detail'),
-]
\ No newline at end of file
+]
diff --git a/core/views.py b/core/views.py
index a5d3156..aea9bc3 100644
--- a/core/views.py
+++ b/core/views.py
@@ -1,3 +1,4 @@
+from django.contrib.auth.views import LoginView
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth import login, authenticate, logout
from django.contrib.auth.forms import AuthenticationForm
@@ -955,4 +956,105 @@ def cancel_parcel(request, parcel_id):
parcel.save()
messages.success(request, _("Shipment cancelled successfully."))
- return redirect('dashboard')
\ No newline at end of file
+ return redirect('dashboard')
+class CustomLoginView(LoginView):
+ template_name = 'core/login.html'
+
+ def form_valid(self, form):
+ # Authenticate checks are done by the form
+ user = form.get_user()
+
+ # Store user ID in session for 2FA step
+ self.request.session['pre_2fa_user_id'] = user.id
+ self.request.session.set_expiry(600) # 10 minutes expiry for this session part
+
+ return redirect('select_2fa_method')
+
+def select_2fa_method(request):
+ user_id = request.session.get('pre_2fa_user_id')
+ if not user_id:
+ return redirect('login')
+
+ try:
+ user = User.objects.get(id=user_id)
+ except User.DoesNotExist:
+ return redirect('login')
+
+ if request.method == 'POST':
+ method = request.POST.get('method')
+ code = ''.join(random.choices(string.digits, k=6))
+
+ # Invalidate old login OTPs
+ OTPVerification.objects.filter(user=user, purpose='login').delete()
+ OTPVerification.objects.create(user=user, code=code, purpose='login')
+
+ if method == 'email':
+ if user.email:
+ try:
+ send_html_email(
+ subject=_("Your Login OTP"),
+ message=f"Your verification code is: {code}",
+ recipient_list=[user.email],
+ title=_("Login Verification")
+ )
+ messages.success(request, _("OTP sent to your email."))
+ return redirect('verify_2fa_otp')
+ except Exception as e:
+ messages.error(request, _("Failed to send email: ") + str(e))
+ else:
+ messages.error(request, _("No email address associated with this account."))
+
+ elif method == 'whatsapp':
+ if hasattr(user, 'profile') and user.profile.phone_number:
+ if send_whatsapp_message(user.profile.phone_number, f"Your login verification code is: {code}"):
+ messages.success(request, _("OTP sent to your WhatsApp."))
+ return redirect('verify_2fa_otp')
+ else:
+ messages.error(request, _("Failed to send WhatsApp message. Please check the logs."))
+ else:
+ messages.error(request, _("No phone number found for this account."))
+
+ return render(request, 'core/select_2fa_method.html')
+
+def verify_2fa_otp(request):
+ user_id = request.session.get('pre_2fa_user_id')
+ if not user_id:
+ return redirect('login')
+
+ try:
+ user = User.objects.get(id=user_id)
+ except User.DoesNotExist:
+ return redirect('login')
+
+ if request.method == 'POST':
+ code = request.POST.get('otp')
+
+ try:
+ # Find the most recent valid login OTP for this user
+ otp_record = OTPVerification.objects.filter(
+ user=user,
+ purpose='login',
+ is_verified=False
+ ).latest('created_at')
+
+ if otp_record.code == code and otp_record.is_valid():
+ otp_record.is_verified = True
+ otp_record.save()
+
+ # ACTUAL LOGIN HAPPENS HERE
+ login(request, user)
+
+ # Clean up session
+ if 'pre_2fa_user_id' in request.session:
+ del request.session['pre_2fa_user_id']
+ request.session.set_expiry(None)
+
+ messages.success(request, _("Logged in successfully."))
+ return redirect('dashboard')
+ else:
+ messages.error(request, _("Invalid or expired OTP."))
+
+ except OTPVerification.DoesNotExist:
+ messages.error(request, _("No valid OTP found. Please request a new one."))
+
+ return render(request, 'core/verify_2fa_otp.html')
diff --git a/locale/ar/LC_MESSAGES/django.mo b/locale/ar/LC_MESSAGES/django.mo
index 7f90fed..84972a7 100644
Binary files a/locale/ar/LC_MESSAGES/django.mo and b/locale/ar/LC_MESSAGES/django.mo differ
diff --git a/locale/ar/LC_MESSAGES/django.po b/locale/ar/LC_MESSAGES/django.po
index 0d64c13..c415808 100644
--- a/locale/ar/LC_MESSAGES/django.po
+++ b/locale/ar/LC_MESSAGES/django.po
@@ -1482,3 +1482,30 @@ msgid ""
"drivers)."
msgstr "يحدد ما إذا كان هذا المستخدم معتمداً لاستخدام المنصة (بشكل رئيسي للسائقين)."
+
+msgid "Two-Factor Authentication"
+msgstr "المصادقة الثنائية"
+
+msgid "Please choose how you would like to receive your verification code."
+msgstr "يرجى اختيار الطريقة التي تود استلام رمز التحقق عبرها."
+
+msgid "Send to Email"
+msgstr "إرسال إلى البريد الإلكتروني"
+
+msgid "Send to WhatsApp"
+msgstr "إرسال إلى واتساب"
+
+msgid "Verify OTP"
+msgstr "التحقق من الرمز"
+
+msgid "Enter the 6-digit code sent to your selected method."
+msgstr "أدخل الرمز المكون من 6 أرقام المرسل إلى الطريقة التي اخترتها."
+
+msgid "Verify & Login"
+msgstr "تحقق ودخول"
+
+msgid "Try another method"
+msgstr "جرب طريقة أخرى"
+
+msgid "Please login with your username and password"
+msgstr "يرجى تسجيل الدخول باستخدام اسم المستخدم وكلمة المرور"
\ No newline at end of file