adding email otp

This commit is contained in:
Flatlogic Bot 2026-01-25 02:29:45 +00:00
parent 8bddfce275
commit b2cb32384e
13 changed files with 211 additions and 38 deletions

Binary file not shown.

View File

@ -1,7 +1,7 @@
from django import forms
from .models import Truck, Shipment, Bid, Profile, Country, OTPCode, City, TruckType, AppSetting, ContactMessage
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from django.contrib.auth.models import User
class UserRegistrationForm(UserCreationForm):
@ -11,6 +11,7 @@ class UserRegistrationForm(UserCreationForm):
country_code = forms.ChoiceField(choices=[], widget=forms.Select(attrs={'class': 'form-select'}))
phone_number = forms.CharField(max_length=20, required=True, widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': '123456789'}))
subscription_plan = forms.ChoiceField(choices=[('MONTHLY', _('Monthly')), ('ANNUAL', _('Annual'))], required=False, widget=forms.Select(attrs={'class': 'form-select'}), label=_('Subscription Plan'))
otp_method = forms.ChoiceField(choices=[("whatsapp", _("WhatsApp")), ("email", _("Email"))], initial="whatsapp", widget=forms.RadioSelect, label=_("Receive verification code via"))
accept_terms = forms.BooleanField(required=True, widget=forms.CheckboxInput(attrs={'class': 'form-check-input'}), label=_('I have read and agree to the Terms and Conditions and Privacy Policy'))
class Meta(UserCreationForm.Meta):
@ -199,3 +200,18 @@ class ContactForm(forms.ModelForm):
'subject': forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Subject')}),
'message': forms.Textarea(attrs={'class': 'form-control', 'rows': 5, 'placeholder': _('Your Message')}),
}
class CustomLoginForm(AuthenticationForm):
otp_method = forms.ChoiceField(
choices=[("whatsapp", _("WhatsApp")), ("email", _("Email"))],
initial="whatsapp",
widget=forms.RadioSelect,
label=_("Receive verification code via")
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields.values():
if not isinstance(field.widget, (forms.RadioSelect, forms.CheckboxInput)):
field.widget.attrs.update({"class": "form-control"})

40
core/mail.py Normal file
View File

@ -0,0 +1,40 @@
import logging
from django.core.mail import send_mail
from django.conf import settings
from django.utils.translation import gettext_lazy as _
logger = logging.getLogger(__name__)
def send_otp_email(email, code):
"""
Sends an OTP code via email.
"""
subject = _("Your verification code for MASAR CARGO")
message = _("Your verification code for MASAR CARGO is: %(code)s") % {"code": code}
from_email = settings.DEFAULT_FROM_EMAIL
recipient_list = [email]
try:
send_mail(subject, message, from_email, recipient_list)
logger.info(f"OTP email sent to {email}")
return True
except Exception as e:
logger.exception(f"Exception while sending OTP email: {str(e)}")
return False
def send_contact_message(name, email, message):
"""
Sends a contact message to the admin.
"""
subject = _("New contact message from %(name)s") % {"name": name}
full_message = f"Name: {name}\nEmail: {email}\n\nMessage:\n{message}"
from_email = settings.DEFAULT_FROM_EMAIL
recipient_list = settings.CONTACT_EMAIL_TO
try:
send_mail(subject, full_message, from_email, recipient_list)
logger.info(f"Contact message from {email} sent to admins")
return True
except Exception as e:
logger.exception(f"Exception while sending contact message: {str(e)}")
return False

View File

@ -0,0 +1,23 @@
# Generated by Django 5.2.7 on 2026-01-25 02:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0026_remove_banner_admin_phone_and_more'),
]
operations = [
migrations.AddField(
model_name='otpcode',
name='email',
field=models.EmailField(blank=True, max_length=254, null=True),
),
migrations.AlterField(
model_name='otpcode',
name='phone_number',
field=models.CharField(blank=True, max_length=20, null=True),
),
]

View File

@ -93,7 +93,8 @@ class Profile(models.Model):
return f"{self.user.username} - {self.role}"
class OTPCode(models.Model):
phone_number = models.CharField(max_length=20)
phone_number = models.CharField(max_length=20, null=True, blank=True)
email = models.EmailField(null=True, blank=True)
code = models.CharField(max_length=6)
created_at = models.DateTimeField(auto_now_add=True)
is_used = models.BooleanField(default=False)
@ -103,9 +104,9 @@ class OTPCode(models.Model):
return not self.is_used and (timezone.now() - self.created_at).total_seconds() < 600
@staticmethod
def generate_code(phone_number):
def generate_code(phone_number=None, email=None):
code = ''.join(random.choices(string.digits, k=6))
return OTPCode.objects.create(phone_number=phone_number, code=code)
return OTPCode.objects.create(phone_number=phone_number, email=email, code=code)
class Truck(models.Model):
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='trucks')

View File

@ -18,13 +18,41 @@
{% csrf_token %}
<div class="mb-3">
<label class="form-label">{% trans "Username" %}</label>
<input type="text" name="username" class="form-control" required>
{{ form.username }}
{% if form.username.errors %}
{% for error in form.username.errors %}
<div class="text-danger small">{{ error }}</div>
{% endfor %}
{% endif %}
</div>
<div class="mb-3">
<label class="form-label">{% trans "Password" %}</label>
<input type="password" name="password" class="form-control" required>
{{ form.password }}
{% if form.password.errors %}
{% for error in form.password.errors %}
<div class="text-danger small">{{ error }}</div>
{% endfor %}
{% endif %}
</div>
<button type="submit" class="btn btn-primary w-100 py-2">{% trans "Login" %}</button>
<div class="mb-3">
<label class="form-label d-block">{{ form.otp_method.label }}</label>
<div class="btn-group w-100" role="group">
{% for radio in form.otp_method %}
<input type="radio" class="btn-check" name="{{ form.otp_method.name }}" id="login_{{ radio.id_for_label }}" value="{{ radio.data.value }}" {% if radio.data.selected %}checked{% endif %}>
<label class="btn btn-outline-primary" for="login_{{ radio.id_for_label }}">
{% if radio.data.value == 'whatsapp' %}
<i class="fa-brands fa-whatsapp me-1"></i>
{% else %}
<i class="fa-solid fa-envelope me-1"></i>
{% endif %}
{{ radio.choice_label }}
</label>
{% endfor %}
</div>
</div>
<button type="submit" class="btn btn-primary w-100 py-2 mt-3">{% trans "Login" %}</button>
</form>
<div class="text-center mt-3">
<p>{% trans "Don't have an account?" %} <a href="{% url 'register' %}">{% trans "Register" %}</a></p>

View File

@ -47,6 +47,28 @@
</div>
{% elif field.name == 'phone_number' %}
{# Handled above #}
{% elif field.name == 'otp_method' %}
<div class="mb-3">
<label class="form-label d-block">{{ field.label }}</label>
<div class="btn-group w-100" role="group">
{% for radio in field %}
<input type="radio" class="btn-check" name="{{ field.name }}" id="{{ radio.id_for_label }}" value="{{ radio.data.value }}" {% if radio.data.selected %}checked{% endif %}>
<label class="btn btn-outline-primary" for="{{ radio.id_for_label }}">
{% if radio.data.value == 'whatsapp' %}
<i class="fa-brands fa-whatsapp me-1"></i>
{% else %}
<i class="fa-solid fa-envelope me-1"></i>
{% endif %}
{{ radio.choice_label }}
</label>
{% endfor %}
</div>
{% if field.errors %}
{% for error in field.errors %}
<div class="text-danger small">{{ error }}</div>
{% endfor %}
{% endif %}
</div>
{% elif field.name == 'subscription_plan' %}
{% if subscription_enabled %}
<div class="mb-3">

View File

@ -14,10 +14,18 @@
<div class="card-body p-5">
<h2 class="text-center mb-4">{% trans "Verify Your Account" %}</h2>
<p class="text-muted text-center mb-4">
{% if purpose == 'registration' %}
{% trans "We have sent a verification code to your WhatsApp number. Please enter it below to complete your registration." %}
{% if otp_method == 'email' %}
{% if purpose == 'registration' %}
{% trans "We have sent a verification code to your email address. Please enter it below to complete your registration." %}
{% else %}
{% trans "We have sent a verification code to your email address. Please enter it below to log in." %}
{% endif %}
{% else %}
{% trans "We have sent a verification code to your WhatsApp number. Please enter it below to log in." %}
{% if purpose == 'registration' %}
{% trans "We have sent a verification code to your WhatsApp number. Please enter it below to complete your registration." %}
{% else %}
{% trans "We have sent a verification code to your WhatsApp number. Please enter it below to log in." %}
{% endif %}
{% endif %}
</p>
<form method="post">

View File

@ -10,6 +10,7 @@ from .models import (
AppSetting, Banner, HomeSection, Transaction, ContactMessage, Testimonial, WhatsAppConfig
)
from .forms import (
CustomLoginForm,
TruckForm, ShipmentForm, BidForm, UserRegistrationForm,
OTPVerifyForm, ShipperOfferForm, RenewSubscriptionForm, AppSettingForm,
ContactForm
@ -19,6 +20,7 @@ from django.utils.translation import gettext as _
from django.db.models import Q
from django.contrib.auth.models import User
from .whatsapp import send_whatsapp_message
from .mail import send_otp_email, send_contact_message
from django.contrib.auth.forms import AuthenticationForm
from django.core.mail import send_mail
from django.conf import settings
@ -70,23 +72,35 @@ def register(request):
registration_data = {
'username': form.cleaned_data['username'],
'email': form.cleaned_data['email'],
'password': form.data['password1'], # We need raw password to create user later
'password': form.data['password1'],
'role': form.cleaned_data['role'],
'phone_number': form.cleaned_data['phone_number'],
'country_code': form.cleaned_data['country_code'],
'subscription_plan': form.cleaned_data.get('subscription_plan', 'NONE'),
'otp_method': form.cleaned_data.get('otp_method', 'whatsapp'),
}
request.session['registration_data'] = registration_data
# Send OTP
full_phone = f"{registration_data['country_code']}{registration_data['phone_number']}"
otp = OTPCode.generate_code(full_phone)
msg = _("Your verification code for MASAR CARGO is: %(code)s") % {"code": otp.code}
if send_whatsapp_message(full_phone, msg):
messages.info(request, _("A verification code has been sent to your WhatsApp."))
return redirect('verify_otp_registration')
email = registration_data['email']
otp_method = registration_data.get('otp_method', 'whatsapp')
if otp_method == 'email':
otp = OTPCode.generate_code(email=email)
if send_otp_email(email, otp.code):
messages.info(request, _("A verification code has been sent to your email."))
return redirect('verify_otp_registration')
else:
messages.error(request, _("Failed to send verification code to your email."))
else:
messages.error(request, _("Failed to send verification code. Please check your phone number."))
otp = OTPCode.generate_code(phone_number=full_phone)
msg = _("Your verification code for MASAR CARGO is: %(code)s") % {"code": otp.code}
if send_whatsapp_message(full_phone, msg):
messages.info(request, _("A verification code has been sent to your WhatsApp."))
return redirect('verify_otp_registration')
else:
messages.error(request, _("Failed to send verification code. Please check your phone number."))
else:
messages.error(request, _("Please correct the errors below."))
else:
@ -108,7 +122,13 @@ def verify_otp_registration(request):
if form.is_valid():
code = form.cleaned_data['otp_code']
full_phone = f"{registration_data['country_code']}{registration_data['phone_number']}"
otp_record = OTPCode.objects.filter(phone_number=full_phone, code=code, is_used=False).last()
email = registration_data['email']
otp_method = registration_data.get('otp_method', 'whatsapp')
if otp_method == 'email':
otp_record = OTPCode.objects.filter(email=email, code=code, is_used=False).last()
else:
otp_record = OTPCode.objects.filter(phone_number=full_phone, code=code, is_used=False).last()
if otp_record and otp_record.is_valid():
otp_record.is_used = True
@ -141,40 +161,52 @@ def verify_otp_registration(request):
else:
form = OTPVerifyForm()
return render(request, 'registration/verify_otp.html', {'form': form, 'purpose': 'registration'})
return render(request, 'registration/verify_otp.html', {'form': form, 'purpose': 'registration', 'otp_method': registration_data.get('otp_method', 'whatsapp')})
def custom_login(request):
if request.method == 'POST':
form = AuthenticationForm(request, data=request.POST)
form = CustomLoginForm(request, data=request.POST)
if form.is_valid():
user = form.get_user()
profile = user.profile
if not profile.phone_number:
messages.error(request, _("Your account does not have a phone number. Please contact admin."))
return redirect('login')
otp_method = form.cleaned_data.get('otp_method', 'whatsapp')
# Store user ID in session temporarily
# Store user ID and method in session temporarily
request.session['pre_otp_user_id'] = user.id
request.session['pre_otp_method'] = otp_method
# Send OTP
full_phone = profile.full_phone_number
otp = OTPCode.generate_code(full_phone)
msg = _("Your login verification code for MASAR CARGO is: %(code)s") % {"code": otp.code}
if send_whatsapp_message(full_phone, msg):
messages.info(request, _("A verification code has been sent to your WhatsApp."))
return redirect('verify_otp_login')
if otp_method == 'email':
if not user.email:
messages.error(request, _("Your account does not have an email address. Please use WhatsApp or contact admin."))
return redirect('login')
otp = OTPCode.generate_code(email=user.email)
if send_otp_email(user.email, otp.code):
messages.info(request, _("A verification code has been sent to your email."))
return redirect('verify_otp_login')
else:
messages.error(request, _("Failed to send verification code to your email."))
else:
# If WhatsApp fails, maybe allow login but warn? Or strictly enforce?
# For now, strictly enforce
messages.error(request, _("Failed to send verification code. Please check your connection."))
if not profile.phone_number:
messages.error(request, _("Your account does not have a phone number. Please contact admin."))
return redirect('login')
full_phone = profile.full_phone_number
otp = OTPCode.generate_code(phone_number=full_phone)
msg = _("Your login verification code for MASAR CARGO is: %(code)s") % {"code": otp.code}
if send_whatsapp_message(full_phone, msg):
messages.info(request, _("A verification code has been sent to your WhatsApp."))
return redirect('verify_otp_login')
else:
messages.error(request, _("Failed to send verification code. Please check your connection."))
else:
messages.error(request, _("Invalid username or password."))
else:
form = AuthenticationForm()
form = CustomLoginForm()
return render(request, 'registration/login.html', {'form': form})
def verify_otp_login(request):
user_id = request.session.get('pre_otp_user_id')
otp_method = request.session.get('pre_otp_method', 'whatsapp')
if not user_id:
return redirect('login')
@ -185,8 +217,11 @@ def verify_otp_login(request):
form = OTPVerifyForm(request.POST)
if form.is_valid():
code = form.cleaned_data['otp_code']
full_phone = profile.full_phone_number
otp_record = OTPCode.objects.filter(phone_number=full_phone, code=code, is_used=False).last()
if otp_method == 'email':
otp_record = OTPCode.objects.filter(email=user.email, code=code, is_used=False).last()
else:
full_phone = profile.full_phone_number
otp_record = OTPCode.objects.filter(phone_number=full_phone, code=code, is_used=False).last()
if otp_record and otp_record.is_valid():
otp_record.is_used = True
@ -202,7 +237,7 @@ def verify_otp_login(request):
else:
form = OTPVerifyForm()
return render(request, 'registration/verify_otp.html', {'form': form, 'purpose': 'login'})
return render(request, 'registration/verify_otp.html', {'form': form, 'purpose': 'login', 'otp_method': otp_method})
@login_required
def dashboard(request):