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>
|
<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 -->
|
<form method="post">
|
||||||
<ul class="nav nav-pills nav-fill mb-4 p-1 bg-light rounded-3" id="loginTab" role="tablist">
|
{% csrf_token %}
|
||||||
<li class="nav-item" role="presentation">
|
{% for field in form %}
|
||||||
<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>
|
<div class="mb-3">
|
||||||
</li>
|
<label class="form-label fw-medium">{{ field.label }}</label>
|
||||||
<li class="nav-item" role="presentation">
|
{{ field }}
|
||||||
<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>
|
{% if field.errors %}
|
||||||
</li>
|
<div class="text-danger small mt-1">{{ field.errors }}</div>
|
||||||
</ul>
|
{% endif %}
|
||||||
|
</div>
|
||||||
<div class="tab-content" id="loginTabContent">
|
{% endfor %}
|
||||||
<!-- Password Login Tab -->
|
|
||||||
<div class="tab-pane fade show active" id="password-login" role="tabpanel" aria-labelledby="password-tab">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<form method="post">
|
<div class="form-check">
|
||||||
{% csrf_token %}
|
<!-- Optional: Remember Me logic could go here later -->
|
||||||
{% for field in form %}
|
</div>
|
||||||
<div class="mb-3">
|
<a href="{% url 'password_reset' %}" class="text-decoration-none text-muted small hover-orange">
|
||||||
<label class="form-label fw-medium">{{ field.label }}</label>
|
{% trans "Forgot Password?" %}
|
||||||
{{ field }}
|
</a>
|
||||||
{% 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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- OTP Login Tab -->
|
<button type="submit" class="btn btn-masarx-primary w-100 py-2 fw-bold mb-3">{% trans "Login" %}</button>
|
||||||
<div class="tab-pane fade" id="otp-login" role="tabpanel" aria-labelledby="otp-tab">
|
</form>
|
||||||
<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>
|
||||||
|
{% endblock %}
|
||||||
<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 %}
|
|
||||||
|
|||||||
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 = [
|
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
|
||||||
@ -84,4 +86,4 @@ urlpatterns = [
|
|||||||
# Root-level Aliases (for apps hardcoded to /shipments/)
|
# Root-level Aliases (for apps hardcoded to /shipments/)
|
||||||
path('shipments/', api_views.ParcelListCreateView.as_view(), name='root_shipment_list'),
|
path('shipments/', api_views.ParcelListCreateView.as_view(), name='root_shipment_list'),
|
||||||
path('shipments/<int:pk>/', api_views.ParcelDetailView.as_view(), name='root_shipment_detail'),
|
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.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
|
||||||
@ -955,4 +956,105 @@ def cancel_parcel(request, parcel_id):
|
|||||||
parcel.save()
|
parcel.save()
|
||||||
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.
@ -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 "يرجى تسجيل الدخول باستخدام اسم المستخدم وكلمة المرور"
|
||||||
Loading…
x
Reference in New Issue
Block a user