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 django import forms
from .models import Truck, Shipment, Bid, Profile, Country, OTPCode, City, TruckType, AppSetting, ContactMessage from .models import Truck, Shipment, Bid, Profile, Country, OTPCode, City, TruckType, AppSetting, ContactMessage
from django.utils.translation import gettext_lazy as _ 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 from django.contrib.auth.models import User
class UserRegistrationForm(UserCreationForm): class UserRegistrationForm(UserCreationForm):
@ -11,6 +11,7 @@ class UserRegistrationForm(UserCreationForm):
country_code = forms.ChoiceField(choices=[], widget=forms.Select(attrs={'class': 'form-select'})) 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'})) 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')) 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')) 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): class Meta(UserCreationForm.Meta):
@ -199,3 +200,18 @@ class ContactForm(forms.ModelForm):
'subject': forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Subject')}), 'subject': forms.TextInput(attrs={'class': 'form-control', 'placeholder': _('Subject')}),
'message': forms.Textarea(attrs={'class': 'form-control', 'rows': 5, 'placeholder': _('Your Message')}), '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}" return f"{self.user.username} - {self.role}"
class OTPCode(models.Model): 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) code = models.CharField(max_length=6)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
is_used = models.BooleanField(default=False) 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 return not self.is_used and (timezone.now() - self.created_at).total_seconds() < 600
@staticmethod @staticmethod
def generate_code(phone_number): def generate_code(phone_number=None, email=None):
code = ''.join(random.choices(string.digits, k=6)) 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): class Truck(models.Model):
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='trucks') owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='trucks')

View File

@ -18,13 +18,41 @@
{% csrf_token %} {% csrf_token %}
<div class="mb-3"> <div class="mb-3">
<label class="form-label">{% trans "Username" %}</label> <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>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">{% trans "Password" %}</label> <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> </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> </form>
<div class="text-center mt-3"> <div class="text-center mt-3">
<p>{% trans "Don't have an account?" %} <a href="{% url 'register' %}">{% trans "Register" %}</a></p> <p>{% trans "Don't have an account?" %} <a href="{% url 'register' %}">{% trans "Register" %}</a></p>

View File

@ -47,6 +47,28 @@
</div> </div>
{% elif field.name == 'phone_number' %} {% elif field.name == 'phone_number' %}
{# Handled above #} {# 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' %} {% elif field.name == 'subscription_plan' %}
{% if subscription_enabled %} {% if subscription_enabled %}
<div class="mb-3"> <div class="mb-3">

View File

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

View File

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