adding email otp
This commit is contained in:
parent
8bddfce275
commit
b2cb32384e
Binary file not shown.
BIN
core/__pycache__/mail.cpython-311.pyc
Normal file
BIN
core/__pycache__/mail.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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
40
core/mail.py
Normal 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
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
@ -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')
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user