login changes

This commit is contained in:
Flatlogic Bot 2026-01-28 00:43:39 +00:00
parent 9d123496e2
commit 8e628f5334
9 changed files with 231 additions and 206 deletions

View File

@ -19,22 +19,9 @@
<h2 class="fw-bold text-masarx-primary mb-3">{{ platform_profile.name|default:"masarX" }}</h2> <h2 class="fw-bold text-masarx-primary mb-3">{{ platform_profile.name|default:"masarX" }}</h2>
{% endif %} {% endif %}
<h4 class="fw-bold">{% trans "Welcome Back" %}</h4> <h4 class="fw-bold">{% trans "Welcome Back" %}</h4>
<p class="text-muted small">{% trans "Please login to your account" %}</p> <p class="text-muted small">{% trans "Please login with your username and password" %}</p>
</div> </div>
<!-- Login Method Tabs -->
<ul class="nav nav-pills nav-fill mb-4 p-1 bg-light rounded-3" id="loginTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active fw-bold rounded-3" id="password-tab" data-bs-toggle="tab" data-bs-target="#password-login" type="button" role="tab" aria-controls="password-login" aria-selected="true">{% trans "Password" %}</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link fw-bold rounded-3" id="otp-tab" data-bs-toggle="tab" data-bs-target="#otp-login" type="button" role="tab" aria-controls="otp-login" aria-selected="false">{% trans "OTP Login" %}</button>
</li>
</ul>
<div class="tab-content" id="loginTabContent">
<!-- Password Login Tab -->
<div class="tab-pane fade show active" id="password-login" role="tabpanel" aria-labelledby="password-tab">
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{% for field in form %} {% for field in form %}
@ -58,39 +45,6 @@
<button type="submit" class="btn btn-masarx-primary w-100 py-2 fw-bold mb-3">{% trans "Login" %}</button> <button type="submit" class="btn btn-masarx-primary w-100 py-2 fw-bold mb-3">{% trans "Login" %}</button>
</form> </form>
</div>
<!-- OTP Login Tab -->
<div class="tab-pane fade" id="otp-login" role="tabpanel" aria-labelledby="otp-tab">
<div id="otp-step-1">
<div class="mb-3">
<label for="otp-identifier" class="form-label fw-medium">{% trans "Email or Phone Number" %}</label>
<input type="text" class="form-control" id="otp-identifier" placeholder="{% trans 'e.g. user@example.com or 96812345678' %}">
<div id="otp-identifier-error" class="text-danger small mt-1 d-none"></div>
</div>
<button type="button" class="btn btn-masarx-primary w-100 py-2 fw-bold mb-3" id="btn-send-otp">
<span id="btn-send-otp-text">{% trans "Send OTP" %}</span>
<span id="btn-send-otp-spinner" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
</button>
</div>
<div id="otp-step-2" class="d-none">
<div class="alert alert-info py-2 small" id="otp-sent-msg"></div>
<div class="mb-3">
<label for="otp-code" class="form-label fw-medium">{% trans "Enter OTP Code" %}</label>
<input type="text" class="form-control text-center letter-spacing-2" id="otp-code" placeholder="123456" maxlength="6">
<div id="otp-code-error" class="text-danger small mt-1 d-none"></div>
</div>
<button type="button" class="btn btn-masarx-primary w-100 py-2 fw-bold mb-3" id="btn-verify-otp">
<span id="btn-verify-otp-text">{% trans "Verify & Login" %}</span>
<span id="btn-verify-otp-spinner" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
</button>
<div class="text-center">
<button type="button" class="btn btn-link text-muted small text-decoration-none" id="btn-back-otp">{% trans "Back" %}</button>
</div>
</div>
</div>
</div>
<div class="text-center border-top pt-3 mt-2"> <div class="text-center border-top pt-3 mt-2">
<span class="text-muted small">{% trans "Don't have an account?" %}</span> <span class="text-muted small">{% trans "Don't have an account?" %}</span>
@ -105,15 +59,6 @@
</section> </section>
<style> <style>
.nav-pills .nav-link {
color: #6c757d;
border-radius: 8px;
padding: 10px 15px;
}
.nav-pills .nav-link.active {
background-color: var(--accent-orange);
color: white;
}
.form-control { .form-control {
border-radius: 8px; border-radius: 8px;
padding: 12px 15px; padding: 12px 15px;
@ -141,130 +86,5 @@
.hover-orange:hover { .hover-orange:hover {
color: var(--accent-orange) !important; color: var(--accent-orange) !important;
} }
.letter-spacing-2 {
letter-spacing: 4px;
font-size: 1.2rem;
}
</style> </style>
<script>
document.addEventListener('DOMContentLoaded', function() {
const btnSendOtp = document.getElementById('btn-send-otp');
const btnVerifyOtp = document.getElementById('btn-verify-otp');
const btnBackOtp = document.getElementById('btn-back-otp');
const otpStep1 = document.getElementById('otp-step-1');
const otpStep2 = document.getElementById('otp-step-2');
const inputIdentifier = document.getElementById('otp-identifier');
const inputCode = document.getElementById('otp-code');
const errorIdentifier = document.getElementById('otp-identifier-error');
const errorCode = document.getElementById('otp-code-error');
const msgSent = document.getElementById('otp-sent-msg');
let userId = null;
function showLoading(btnId, show) {
const btn = document.getElementById(btnId);
const textSpan = document.getElementById(btnId + '-text');
const spinnerSpan = document.getElementById(btnId + '-spinner');
if (show) {
btn.disabled = true;
textSpan.classList.add('d-none');
spinnerSpan.classList.remove('d-none');
} else {
btn.disabled = false;
textSpan.classList.remove('d-none');
spinnerSpan.classList.add('d-none');
}
}
function showError(element, message) {
element.innerText = message;
element.classList.remove('d-none');
}
function clearErrors() {
errorIdentifier.classList.add('d-none');
errorCode.classList.add('d-none');
}
btnSendOtp.addEventListener('click', function() {
const identifier = inputIdentifier.value.trim();
if (!identifier) {
showError(errorIdentifier, "{% trans 'Please enter your email or phone number.' %}");
return;
}
clearErrors();
showLoading('btn-send-otp', true);
fetch("{% url 'request_login_otp' %}", {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRFToken': '{{ csrf_token }}'
},
body: 'identifier=' + encodeURIComponent(identifier)
})
.then(response => response.json())
.then(data => {
showLoading('btn-send-otp', false);
if (data.success) {
userId = data.user_id;
msgSent.innerText = data.message;
otpStep1.classList.add('d-none');
otpStep2.classList.remove('d-none');
} else {
showError(errorIdentifier, data.message);
}
})
.catch(error => {
showLoading('btn-send-otp', false);
showError(errorIdentifier, "{% trans 'An error occurred. Please try again.' %}");
console.error('Error:', error);
});
});
btnVerifyOtp.addEventListener('click', function() {
const code = inputCode.value.trim();
if (!code) {
showError(errorCode, "{% trans 'Please enter the code.' %}");
return;
}
clearErrors();
showLoading('btn-verify-otp', true);
fetch("{% url 'verify_login_otp' %}", {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRFToken': '{{ csrf_token }}'
},
body: 'user_id=' + encodeURIComponent(userId) + '&code=' + encodeURIComponent(code)
})
.then(response => response.json())
.then(data => {
showLoading('btn-verify-otp', false);
if (data.success) {
window.location.href = data.redirect_url;
} else {
showError(errorCode, data.message);
}
})
.catch(error => {
showLoading('btn-verify-otp', false);
showError(errorCode, "{% trans 'An error occurred. Please try again.' %}");
console.error('Error:', error);
});
});
btnBackOtp.addEventListener('click', function() {
otpStep2.classList.add('d-none');
otpStep1.classList.remove('d-none');
inputCode.value = '';
clearErrors();
});
});
</script>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,36 @@
{% extends "base.html" %}
{% load i18n static %}
{% block content %}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-md-6 col-lg-5">
<div class="card shadow-sm border-0 rounded-3">
<div class="card-body p-4 text-center">
<h3 class="mb-4 fw-bold">{% trans "Two-Factor Authentication" %}</h3>
<p class="text-muted mb-4">{% trans "Please choose how you would like to receive your verification code." %}</p>
<form method="post">
{% csrf_token %}
<div class="d-grid gap-3">
<button type="submit" name="method" value="email" class="btn btn-outline-primary btn-lg py-3">
<i class="fas fa-envelope me-2"></i> {% trans "Send to Email" %}
</button>
<button type="submit" name="method" value="whatsapp" class="btn btn-outline-success btn-lg py-3">
<i class="fab fa-whatsapp me-2"></i> {% trans "Send to WhatsApp" %}
</button>
</div>
</form>
<div class="mt-4">
<a href="{% url 'login' %}" class="text-muted text-decoration-none">
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Login" %}
</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,38 @@
{% extends "base.html" %}
{% load i18n static %}
{% block content %}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-md-6 col-lg-5">
<div class="card shadow-sm border-0 rounded-3">
<div class="card-body p-4 text-center">
<h3 class="mb-3 fw-bold">{% trans "Verify OTP" %}</h3>
<p class="text-muted">{% trans "Enter the 6-digit code sent to your selected method." %}</p>
<form method="post" class="mt-4">
{% csrf_token %}
<div class="mb-4">
<input type="text" name="otp" class="form-control form-control-lg text-center tracking-widest fw-bold"
style="letter-spacing: 0.5em; font-size: 1.5rem;"
placeholder="------" maxlength="6" pattern="[0-9]*" inputmode="numeric" required autofocus>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary btn-lg">
{% trans "Verify & Login" %}
</button>
</div>
</form>
<div class="mt-4 d-flex justify-content-between">
<a href="{% url 'select_2fa_method' %}" class="text-muted small text-decoration-none">
<i class="fas fa-arrow-left me-1"></i> {% trans "Try another method" %}
</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -5,7 +5,9 @@ from . import api_views
urlpatterns = [ urlpatterns = [
path('', views.index, name='index'), 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'), path('logout/', auth_views.LogoutView.as_view(next_page='/'), name='logout'),
# Registration Flow # Registration Flow

View File

@ -1,3 +1,4 @@
from django.contrib.auth.views import LoginView
from django.shortcuts import render, redirect, get_object_or_404 from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth import login, authenticate, logout from django.contrib.auth import login, authenticate, logout
from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.forms import AuthenticationForm
@ -956,3 +957,104 @@ def cancel_parcel(request, parcel_id):
messages.success(request, _("Shipment cancelled successfully.")) messages.success(request, _("Shipment cancelled successfully."))
return redirect('dashboard') 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')

Binary file not shown.

View File

@ -1482,3 +1482,30 @@ msgid ""
"drivers)." "drivers)."
msgstr "يحدد ما إذا كان هذا المستخدم معتمداً لاستخدام المنصة (بشكل رئيسي للسائقين)." 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 "يرجى تسجيل الدخول باستخدام اسم المستخدم وكلمة المرور"