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 @@

{{ platform_profile.name|default:"masarX" }}

{% endif %}

{% trans "Welcome Back" %}

-

{% trans "Please login to your account" %}

+

{% trans "Please login with your username and password" %}

- - - -
- -
-
- {% csrf_token %} - {% for field in form %} -
- - {{ field }} - {% if field.errors %} -
{{ field.errors }}
- {% endif %} -
- {% endfor %} - - - - -
+
+ {% csrf_token %} + {% for field in form %} +
+ + {{ field }} + {% if field.errors %} +
{{ field.errors }}
+ {% endif %} +
+ {% endfor %} + + - -
-
-
- - -
-
- -
- -
-
-
- - -
-
- -
- -
-
-
-
+ +
{% 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." %}

+ +
+ {% csrf_token %} +
+ + + +
+
+ + +
+
+
+
+
+{% 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." %}

+ +
+ {% csrf_token %} +
+ +
+ +
+ +
+
+ + +
+
+
+
+
+{% 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