login changes
This commit is contained in:
parent
9d123496e2
commit
8e628f5334
Binary file not shown.
Binary file not shown.
@ -19,78 +19,32 @@
|
||||
<h2 class="fw-bold text-masarx-primary mb-3">{{ platform_profile.name|default:"masarX" }}</h2>
|
||||
{% endif %}
|
||||
<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>
|
||||
|
||||
<!-- 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">
|
||||
{% csrf_token %}
|
||||
{% for field in form %}
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-medium">{{ field.label }}</label>
|
||||
{{ field }}
|
||||
{% if field.errors %}
|
||||
<div class="text-danger small mt-1">{{ field.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div class="form-check">
|
||||
<!-- Optional: Remember Me logic could go here later -->
|
||||
</div>
|
||||
<a href="{% url 'password_reset' %}" class="text-decoration-none text-muted small hover-orange">
|
||||
{% trans "Forgot Password?" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-masarx-primary w-100 py-2 fw-bold mb-3">{% trans "Login" %}</button>
|
||||
</form>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% for field in form %}
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-medium">{{ field.label }}</label>
|
||||
{{ field }}
|
||||
{% if field.errors %}
|
||||
<div class="text-danger small mt-1">{{ field.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div class="form-check">
|
||||
<!-- Optional: Remember Me logic could go here later -->
|
||||
</div>
|
||||
<a href="{% url 'password_reset' %}" class="text-decoration-none text-muted small hover-orange">
|
||||
{% trans "Forgot Password?" %}
|
||||
</a>
|
||||
</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>
|
||||
<button type="submit" class="btn btn-masarx-primary w-100 py-2 fw-bold mb-3">{% trans "Login" %}</button>
|
||||
</form>
|
||||
|
||||
<div class="text-center border-top pt-3 mt-2">
|
||||
<span class="text-muted small">{% trans "Don't have an account?" %}</span>
|
||||
@ -105,15 +59,6 @@
|
||||
</section>
|
||||
|
||||
<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 {
|
||||
border-radius: 8px;
|
||||
padding: 12px 15px;
|
||||
@ -141,130 +86,5 @@
|
||||
.hover-orange:hover {
|
||||
color: var(--accent-orange) !important;
|
||||
}
|
||||
.letter-spacing-2 {
|
||||
letter-spacing: 4px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
</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 %}
|
||||
|
||||
36
core/templates/core/select_2fa_method.html
Normal file
36
core/templates/core/select_2fa_method.html
Normal 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 %}
|
||||
38
core/templates/core/verify_2fa_otp.html
Normal file
38
core/templates/core/verify_2fa_otp.html
Normal 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 %}
|
||||
@ -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/<int:pk>/', api_views.ParcelDetailView.as_view(), name='root_shipment_detail'),
|
||||
]
|
||||
]
|
||||
|
||||
104
core/views.py
104
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')
|
||||
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.
@ -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 "يرجى تسجيل الدخول باستخدام اسم المستخدم وكلمة المرور"
|
||||
Loading…
x
Reference in New Issue
Block a user