From 43c475e4c15646970b3d475e1fa03ff9b2b406ec Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Fri, 23 Jan 2026 12:44:51 +0000 Subject: [PATCH] dem16 --- core/__pycache__/forms.cpython-311.pyc | Bin 8784 -> 10440 bytes core/__pycache__/models.cpython-311.pyc | Bin 13252 -> 14701 bytes core/__pycache__/urls.cpython-311.pyc | Bin 1811 -> 1986 bytes core/__pycache__/views.cpython-311.pyc | Bin 15179 -> 20815 bytes core/forms.py | 23 ++- ...0009_otpcode_alter_profile_phone_number.py | 28 ++++ ...alter_profile_phone_number.cpython-311.pyc | Bin 0 -> 1437 bytes core/models.py | 22 ++- core/templates/registration/verify_otp.html | 43 ++++++ core/urls.py | 6 +- core/views.py | 137 ++++++++++++++++-- 11 files changed, 239 insertions(+), 20 deletions(-) create mode 100644 core/migrations/0009_otpcode_alter_profile_phone_number.py create mode 100644 core/migrations/__pycache__/0009_otpcode_alter_profile_phone_number.cpython-311.pyc create mode 100644 core/templates/registration/verify_otp.html diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index af0d53c604fcc945278602fc6daf9b1c45aca390..950dabf6e634ce66c3d8fd4416a0e5134264c8be 100644 GIT binary patch delta 3234 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 diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index be16afb7a208fd25c637d2d77d69a4a7cc9b3e1e..e198ee695167dae7e2009452ccf94d226c6bc48d 100644 GIT binary patch delta 3826 zcmaJ^YitzP6`tAmd%a$J*K5Pt;9bLFYHVZ7qddyXyp2oT5L}1tvNK+fy}Pr%GsfVO z*hx`AQBaL;5=zyk6aj_cwj|C&YK4?UZIeHxYq}EFs8z{N)JB3PYNbe3d(Iv2VjJ2q z{(SFu&%JZcH|Lyt`EBZR;`zDT?cm@!kyfPNEI#k4;{S1e*?|=TCs@<2jJwCpbI`V> zJv|=w*W2SYaJx9cewq^;B0o?@zrhE+KE^qLbIo##?QZCKN)~=*;RUXoiP2V8lH zt7M!XxQY@NU|c0|ff5&FToAabA{VbRaH3T-i^j>&H3P@pgkSv;s!wy*pn0=Ma6O^_ zXghRi*iwtl z20Gp7cQ~<$;G}oV@9FPx()+FuAE29-4$~@Y&LKD_J=b8(n?^ceeP^c!tTk)0UzU=h zLVVDBU>wL0QkLPYG&C$~rtEM!J@umPJA5FDOBs?$&5+9~ zxa+#QIufQ|+FSUgw88NbznETkq#GhIqVPCO!hoa!VAR`1kYmz#8?lLi!YZH9e>tzXA|H0= z{d=eVdkg-(^r~x39s4k7l!V{XgRbE7#XHosqGn9sCk@wN|2NSG_Vkz`UW@~#VqY*m zVPRnw){=#3(!5{|8?&&NEG(1O1r`p$cAD!MbBZoZumvIO790?UPR)9-yKB2Fh@=%( zB5r^O7J%*>yyi*_t8zRmkHiy#&_T+8PL_Ot4rT=U`h^N`fjh+wz+e4wadNY&>#&M3 z1EuttzYmyHl#3gM+HuK5hwOgpwR=IgK{ge(Y3P)mg9Tz{%_=GJVMP=OI<6UEbDASb z#Dppe@r0_mRas4>l1ufrtrO#mn`{NN1(i*r(sgiSLI$Wm6bkVICHGX(wCERaJ|Hly&DSN4QEY%@djQUyXTGG^+s=P{7}eyqto7f1@FGRZ6A}l zGv=Ai?t`%6-+XKZ}O zj4wJ7DEOkc+VZ}2)4p{D-@5U=GoB@9`wE^V*Q0q)^R%bA;AtM;_0VK6KhHnpX0Z)B zHqKK1&|om{XZ(Ktkr9Z~y>ls?u%BmwkglowF44rd&<-KuP;wICMTD2x zxt+o$!ltRSRsS*Q2X|`fT+OcyyhL}08;XIThSyqHVP8$(4%bv-eVdI1Y+=17hv;YF zZ}Yq8rrLVnTt#uLfezJH@@MFo+NRmFHMCH!5dA}K$oW0A)`g;vYNIW9%xabqkg#7R zKf%E_5zZj(hA$0Lkdtu%a+LfO2U2ubUD!Ftf9B={4k4&h=jtYS-OUNSpG<9B`j8Jv zINl5}>N?ohCz86Y8F@IX5-;5uX)9xat~-x*hv5DcqL(9wTlPR#vnl;jE+c~V6HeoT z&TW!VjQ5LlMKrck50~H*{^Q!05HN5E6N2p{-PF@q9mb0!Vc!so*QkAuQnxjUIm9j^G-KRC-!PWZ*BjgU zN)|1(IN=k3x9Ee$4f@Kp5ZocUZRNJjpTYo?#LZ~La`&m9J#e@`p(8m6=B(K$86V~V7t{N`5o>F;VSL^ z+HE)c%GizZkj*;C)Ndk?JAz^~iUo|bS#Y?f~*UxCY8<9Aa6TBOfyD;)L z{b$ogsEMmr{eH(CoMzFi{Rt(Zs)Q79rb$VFrtG@=cx@wLpYo4jK&AoaT+cT(2hI0@ zdY>L?KFm~()zA-`zdy!Qwc(O3VIYxB$xXwmlvdbHjU2_5;y7qNfTf=OuS0)p7Y<=D zW2w&Kn*|7yg!w@5ePj^=69+y1%C-?sis7`lnfw*HJO^2(9H(!#sF4|NNq+vn8O}Y< leo75m-rCdV=JAYuI-c!&HT{TH^1C>2;D>GD;;^YA)$@ zy36#ZeMHx^?h+kdE)cqd)%Wyu-{o5)dWLjSF9h_1TwHqvo9DN|`1Qd~1GZy@SqD&55yWG9k&NoIZVTq!3q2)^~PBO9-K|tGd6a>i(*#8#qUYrdxBqrBX6*YXDpgCL2z&!*$nID0M$#Q4N{# zREi3ac1KN}Xp6%I_h7Y?4re*lE{rQ#K9fDp3!(_WxgX#*Lx(36DjL2cPR0{Q3c+uK zCp{sm)+@d=IK9mkOQ#%wo!&arPTYDk9QAqNwAaPlGu$Dt)%xv=CqP?m#572Ke{G_^ zE;v^EpYNZwEC27??pw?4gOsm*VYzbulI7OTKwVYuxNatGlT{}W<_5~?jJ^FofM^q3^7j?IE1#gl1? zoe9;NAQB}+UQo0&KZr{lKu92rA*2wNoRI!AB0wQX`*@;i;7cFLbYrt-V@(D#P@CAy`yi-5Ni`8jb;NvKQuKq z#_&|ArsGm_bW9;uizP-@Q<${x9vP8HuqVyBT^US8+sm0S3qn$L=AyaSHtMZf3QCfq z64`o_VnLlVAT&p}XzI9&sY9N{O_U%oN!XsT6v=A{OrckCGytDBFYkGd4pmDcn`VjB z>XhkmeHEZrdo zdlCwkpA)(qUVt^(0Jh+z=aXG-d1`nOg0T(STLbh&JzW1TC8K)Mg_BrMxvOg6o0yNg z0KdoT>ovuSUaj+82{yI*+E||{#Xh6$?VeZb+xS)lfOD-k94ztp80E-S*xPnAh<6eV z8*diXoE<(aB@|gTXv&pV`dO$+HSWsNwxIoWU@K#9WT%}KHme5~INKt@<(8!E4@*}%D~_k6Zt^L$;{7fM U;HORn&a6F{uUQx84EXAQ0l<{9 diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 4be52ff58b77684253a4aafd16a480e478a9dd81..fddd74705a416f62f44c89cd26a7a115b2c74a0b 100644 GIT binary patch delta 445 zcmbQtcZgqoIWI340}vd|Db8eIWng#=;=q6il<|4eMD+@0<}8kh#}t7S=VU%cWp1!G zkb)Gxi5Eoq`BPc4xM3O>@Jw8zyg8oHka6MzR({1)<}6W|REiQ%wWcyqNDMBdvW9IL zGXukFAclZ+fhh45)nEoqwaK%X91|tVQj0RvDs}Tq3UrH7(=&@piV{mQ^Yip?i9=-K zfim&PGF5Cj`RSQJ1*~AYN)X8esEh!T*3D0t`WP7{CKt1~>lT4Lc8e#uw74WcHy*6G zhzBIZ<&&SDUs@8DnOc5}J+U~x49J-LghhkB2;`F@@yVR50bEKzCL<6RpPQV_TIh0t zQ+Yw?6;-DTo=F#2k}t9(UtvkU!NSqt(jhUy^op?R3ik^vCKp*uuCSPZg)eZ*%t*Z= kXL7;X_X3OGMHas+EPk6mu?jKrD+=;6HE@Gqkpj?s07V9U2LJ#7 delta 274 zcmX@aKbcQ`IWI340}xCIDb5UIVPJR+;=q6ql<_%lqIv}{OBM%AVgcu52}Wf`u89{! z`MFbBvcN`wBq0(L&r30KPkg1w%bdjn(~=?pRJ=KmQJRq}oi9o>MKPE`Q)%)(CPz-T zoc#36JpEg&n-iG(7#YPTzh`mRy2a@O5)I2tEx*N{SR7vlVsHV)@=HrVf<+*Mi+F*= zeq$!FA3{k6gIpfY`BB{0*k{%7KbY= X4x1OU2{H1^^7At_aD!lx98f0!dBH*- diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index ea8c7f4359ffcb9853e6679c6b5fea71540b3520..be364c3471f8249f9bf73f5e50ea144fede760ed 100644 GIT binary patch delta 8414 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 In$X4m7X*9X5dZ)H delta 3497 zcmb7GYit}>6`s5A_3nCYukE$Her9d&PMkOz#?c&dl!S z&a9J^hz)8`4J1H%(^4t{Ri(B>`~YF0!XHF_ps1=4c4bs+qzWPh351Z`KY$`taLye+ z;ztCGX3ySx&bjxV@4W8({%!K%Lg9NpUx5HmkF2Gv&36kc$eVW~?Rg?df@D+NYC)_3 z#&*e}cvNr9tNLO-wJ=tw`eS~zC|1N}PNi5aiIu2sI{?L7~3VUQm59(>S3Js-4bFAQX$X-vR(4u65a(He}abc zCl&^7(H?FiaEteFn}A!ghuaL?z#eV{xTUv*=3U1{fnByoS!9!>toV>fNjvmbW3$!*pUs3sNcU$zPR?{1K{Z0tc*nEfhHWF_+s_Nc#>eGn-sx?zpG zxX*yx*c3boaDjV|pyvP=@RUH|4J&SYwuuDUaHy6&j5e{6Za*7$7FqN<_9$A#BBGxZ zu@mA+_S3Rz5@vVAh{Lc=PE4I=51j|tJN{PoN7q2U#BivxK9`Yb0Jf!JAciZY#TODv zN-}I(VnH^XS(>?&Qe?xMROCcjmf})EPZ-u&S*MM-stHLm5+{;UAcj-RC6ltI86-|^ z$gsF2$Dy{5LTx`QUkP=thB_CAZjY{oBdg(K zABQ_X3U~gpb|rjvHGFn)Xw_T)Y=JlMozZWOeq;Rh_~Q5{-s0~@-Z-}6t%oJc-umiK zyrrw&>NhUDlUR~hm7$VM<1^Q}|EL$W z3Q@revC?u`nQ|uvk!4S2a%r7AU38g_M&}w%^N4=Q@T%A2+FUBD%4uDrL%3Nl*8u)Z zO44X8%rs0-3~Rq(9a>^`D5M8AB3yZT=KGp{7plR z!b;AJZza%0kPgC6`#Y=cX=eZKD`4F%4gQlpa?z5SI(Ms)*X*&kPNZ;!d3OizwYjxgS98o^6z7 zl|Uatfbo=ckRQZP-&H9E+!n4!UxahM#I7It2cXNgy-ynZ@L&wxeL`dKcit$zlv9*= zb`E-5Je^Z#WXiga&gZ$++rb*cb=;(FVBBhqlHr8BP3Su1eluKI1+q7u&d>^6wKH&{ z-3-^4QG~uSJejL$NM~+=VO6zR4UTBS_-)p{FOh|=vmnP!P?um;8h+XX_T+d!yVL$P zNWN@aM{J{SOWTRVMP0(~IJ^S2xHV=8 zu*U-pdCok83jU|uz*C3&0`HA&@Knd?epQcK7}kWM?6|Y9q59SlyZVqHAXe~eB`!gG zNGTMH;sf|;KNi@PzH;{S!Phn_H!<|O#W9V$Mc8M<6@d&44bSj+eDZ~fp%(_GrXaS_ zkq)A<_{hK*?jsV4r;9-o7RSLY*~Z4Bo5f9AkZ=f2`C3xRNwVR{W#PjG+BN%hq%_Y% z$9>1a=TLtE$vz#7_sxwqW;h@w7{cqQEFs|$gbpS=6~BT6eEwv09>7;Xv|9PUl#Ux*Mx@Uo$s2^wfw)mYr^pIe|y)2@G^f{ heB$}=y3I~btP5MDu$T;!b>S)6yi`4TgxC<`e*jC6Ep-3@ diff --git a/core/forms.py b/core/forms.py index e14a845..0e82b0d 100644 --- a/core/forms.py +++ b/core/forms.py @@ -1,5 +1,5 @@ from django import forms -from .models import Truck, Shipment, Bid, Profile, Country +from .models import Truck, Shipment, Bid, Profile, Country, OTPCode from django.utils.translation import gettext_lazy as _ from django.contrib.auth.forms import UserCreationForm from django.contrib.auth.models import User @@ -9,7 +9,7 @@ class UserRegistrationForm(UserCreationForm): confirm_email = forms.EmailField(required=True, widget=forms.EmailInput(attrs={'class': 'form-control'}), label=_("Confirm Email")) role = forms.ChoiceField(choices=Profile.ROLE_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=False, 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'})) class Meta(UserCreationForm.Meta): model = User @@ -33,6 +33,20 @@ class UserRegistrationForm(UserCreationForm): if not isinstance(field.widget, (forms.CheckboxInput, forms.Select)): field.widget.attrs.update({'class': 'form-control'}) + def clean_email(self): + email = self.cleaned_data.get('email') + if User.objects.filter(email=email).exists(): + raise forms.ValidationError(_("This email is already in use.")) + return email + + def clean_phone_number(self): + phone_number = self.cleaned_data.get('phone_number') + country_code = self.cleaned_data.get('country_code') + # We check uniqueness of phone_number in Profile + if Profile.objects.filter(phone_number=phone_number).exists(): + raise forms.ValidationError(_("This phone number is already in use.")) + return phone_number + def clean(self): cleaned_data = super().clean() email = cleaned_data.get("email") @@ -43,6 +57,9 @@ class UserRegistrationForm(UserCreationForm): return cleaned_data +class OTPVerifyForm(forms.Form): + otp_code = forms.CharField(max_length=6, widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': '123456'})) + class TruckForm(forms.ModelForm): class Meta: model = Truck @@ -98,4 +115,4 @@ class BidForm(forms.ModelForm): # Only allow bidding with approved trucks self.fields['truck'].queryset = Truck.objects.filter(owner=user, is_approved=True) if not self.fields['truck'].queryset.exists(): - self.fields['truck'].help_text = _("You must have an approved truck to place a bid.") + self.fields['truck'].help_text = _("You must have an approved truck to place a bid.") \ No newline at end of file diff --git a/core/migrations/0009_otpcode_alter_profile_phone_number.py b/core/migrations/0009_otpcode_alter_profile_phone_number.py new file mode 100644 index 0000000..3a031e9 --- /dev/null +++ b/core/migrations/0009_otpcode_alter_profile_phone_number.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2.7 on 2026-01-23 12:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0008_country'), + ] + + operations = [ + migrations.CreateModel( + name='OTPCode', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('phone_number', models.CharField(max_length=20)), + ('code', models.CharField(max_length=6)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('is_used', models.BooleanField(default=False)), + ], + ), + migrations.AlterField( + model_name='profile', + name='phone_number', + field=models.CharField(max_length=20, null=True, unique=True), + ), + ] diff --git a/core/migrations/__pycache__/0009_otpcode_alter_profile_phone_number.cpython-311.pyc b/core/migrations/__pycache__/0009_otpcode_alter_profile_phone_number.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b29a4750dec4088a3bb18bbfa7831ba616592360 GIT binary patch literal 1437 zcmZuxJ#5=X6h2ZEB~nrl|wQKoI^AcNB)aP8Po;G#*|#OR>qKBtsXnMEutZ#$BZ2*fP=uPKqqeu>?uHjzM~|!iF4FFy?6J$pZngCzZD89 zg7K5(nm?ov`kNc`k)A0hw?X*@5kv$F4TQEZ5Zj`FM06Js@kc~bROo~6bR2^A{LE^n zi1Y|)>0`dBiIF-md(7}m$95xm;1FuL-I-(Zmzxibs4|KQEn|E;~tO8_?3$Ccg)@@(A453MatT?!KP|q9a!5m2Xt!Y?>LS{4SQ;- ztCmM+K-C%2*<0`jI)Rwzbz9$d22}Su&>UZ<&uAAiF#V~+9=k)MOLg8s{lB+IhhBb; z9dF3F#RiX!XWZcb_v89-mvQD3!4>x(5PN4&EF+~Er^Jf9_jRlyrSY>GW-m`lAN*Ci z7*wzA?}XKBN7A2HgEkIq7F6AF)eWm|Q1m9HM$o+R>q6MPar7`~u8f;2VRI!Yu1-qT zpr#!xgf;EOw?S=bTw4ljOF{A0q;x50+&tI`8#iB8f<|lHXoZbdP`nH2g1Uat3hVk& zI;by?>&sz%IVgS^=j6(TIISq^D+J<{i@1W6%e=}|D&*c<@e4D@8x<> z-WZoR!tzF(k+>I#Q!e6au`KU>#UJ+RoR9l(;}z03?4EO-bdr82EuJ#=35O-v&p>;f xy*<4&%S;E8e>mLlK*T9Q5P(HN3(&{sf3dtE$T2!E_SLX>^%dVZeaCK+_h0hag2Mm+ literal 0 HcmV?d00001 diff --git a/core/models.py b/core/models.py index e063def..4f11329 100644 --- a/core/models.py +++ b/core/models.py @@ -4,6 +4,9 @@ from django.db.models.signals import post_save from django.dispatch import receiver from django.utils.translation import gettext_lazy as _ from django.utils.translation import get_language +from django.utils import timezone +import random +import string class Country(models.Model): name = models.CharField(_('Country Name'), max_length=100) @@ -26,7 +29,7 @@ class Profile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='SHIPPER') country_code = models.CharField(max_length=5, blank=True, default="966") - phone_number = models.CharField(max_length=20, blank=True) + phone_number = models.CharField(max_length=20, unique=True, null=True) # Changed to unique and nullable for migration safety @property def full_phone_number(self): @@ -40,6 +43,21 @@ class Profile(models.Model): def __str__(self): return f"{self.user.username} - {self.role}" +class OTPCode(models.Model): + phone_number = models.CharField(max_length=20) + code = models.CharField(max_length=6) + created_at = models.DateTimeField(auto_now_add=True) + is_used = models.BooleanField(default=False) + + def is_valid(self): + # Valid for 10 minutes + return not self.is_used and (timezone.now() - self.created_at).total_seconds() < 600 + + @staticmethod + def generate_code(phone_number): + code = ''.join(random.choices(string.digits, k=6)) + return OTPCode.objects.create(phone_number=phone_number, code=code) + class Truck(models.Model): owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='trucks') @@ -166,4 +184,4 @@ def save_user_profile(sender, instance, **kwargs): if hasattr(instance, 'profile'): instance.profile.save() else: - Profile.objects.create(user=instance) + Profile.objects.create(user=instance) \ No newline at end of file diff --git a/core/templates/registration/verify_otp.html b/core/templates/registration/verify_otp.html new file mode 100644 index 0000000..0b0fb5f --- /dev/null +++ b/core/templates/registration/verify_otp.html @@ -0,0 +1,43 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} +
+
+
+
+
+

{% trans "Verify Your Account" %}

+

+ {% 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 %} +

+
+ {% csrf_token %} + {% for field in form %} +
+ + {{ field }} + {% if field.errors %} + {% for error in field.errors %} +
{{ error }}
+ {% endfor %} + {% endif %} +
+ {% endfor %} +
+ +
+
+
+ {% trans "Didn't receive the code?" %} {% trans "Try again" %} +
+
+
+
+
+
+{% endblock %} diff --git a/core/urls.py b/core/urls.py index 4a39602..597f03a 100644 --- a/core/urls.py +++ b/core/urls.py @@ -5,7 +5,9 @@ from . import views urlpatterns = [ path("", views.home, name="home"), path("register/", views.register, name="register"), - path("login/", auth_views.LoginView.as_view(), name="login"), + path("verify-otp-registration/", views.verify_otp_registration, name="verify_otp_registration"), + path("login/", views.custom_login, name="login"), + path("verify-otp-login/", views.verify_otp_login, name="verify_otp_login"), path("logout/", auth_views.LogoutView.as_view(), name="logout"), path("dashboard/", views.dashboard, name="dashboard"), path("truck/register/", views.truck_register, name="truck_register"), @@ -17,4 +19,4 @@ urlpatterns = [ path("shipment//", views.shipment_detail, name="shipment_detail"), path("shipment//bid/", views.place_bid, name="place_bid"), path("bid//accept/", views.accept_bid, name="accept_bid"), -] \ No newline at end of file +] diff --git a/core/views.py b/core/views.py index 55b07ac..1bda90a 100644 --- a/core/views.py +++ b/core/views.py @@ -1,14 +1,15 @@ from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth.decorators import login_required -from django.contrib.auth import login, authenticate +from django.contrib.auth import login, authenticate, logout from django.utils import timezone -from .models import Profile, Truck, Shipment, Bid, Message -from .forms import TruckForm, ShipmentForm, BidForm, UserRegistrationForm +from .models import Profile, Truck, Shipment, Bid, Message, OTPCode +from .forms import TruckForm, ShipmentForm, BidForm, UserRegistrationForm, OTPVerifyForm from django.contrib import messages 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 django.contrib.auth.forms import AuthenticationForm def home(request): """Render the landing screen for MASAR CARGO.""" @@ -21,21 +22,131 @@ def register(request): if request.method == 'POST': form = UserRegistrationForm(request.POST) if form.is_valid(): - user = form.save() - profile = user.profile - profile.role = form.cleaned_data.get('role') - profile.phone_number = form.cleaned_data.get('phone_number') - profile.country_code = form.cleaned_data.get('country_code') - profile.save() - login(request, user) - messages.success(request, _("Registration successful. Welcome!")) - return redirect('dashboard') + # Store data in session to be used after OTP verification + registration_data = { + 'username': form.cleaned_data['username'], + 'email': form.cleaned_data['email'], + 'password': form.data['password1'], # We need raw password to create user later + 'role': form.cleaned_data['role'], + 'phone_number': form.cleaned_data['phone_number'], + 'country_code': form.cleaned_data['country_code'], + } + 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 = f"Your verification code for MASAR CARGO is: {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: form = UserRegistrationForm() return render(request, 'registration/register.html', {'form': form}) +def verify_otp_registration(request): + registration_data = request.session.get('registration_data') + if not registration_data: + return redirect('register') + + if request.method == 'POST': + form = OTPVerifyForm(request.POST) + 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() + + if otp_record and otp_record.is_valid(): + otp_record.is_used = True + otp_record.save() + + # Create user + user = User.objects.create_user( + username=registration_data['username'], + email=registration_data['email'], + password=registration_data['password'] + ) + profile = user.profile + profile.role = registration_data['role'] + profile.phone_number = registration_data['phone_number'] + profile.country_code = registration_data['country_code'] + profile.save() + + login(request, user) + del request.session['registration_data'] + messages.success(request, _("Registration successful. Welcome!")) + return redirect('dashboard') + else: + messages.error(request, _("Invalid or expired verification code.")) + else: + form = OTPVerifyForm() + + return render(request, 'registration/verify_otp.html', {'form': form, 'purpose': 'registration'}) + +def custom_login(request): + if request.method == 'POST': + form = AuthenticationForm(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') + + # Store user ID in session temporarily + request.session['pre_otp_user_id'] = user.id + + # Send OTP + full_phone = profile.full_phone_number + otp = OTPCode.generate_code(full_phone) + msg = f"Your login verification code for MASAR CARGO is: {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: + # 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.")) + else: + messages.error(request, _("Invalid username or password.")) + else: + form = AuthenticationForm() + return render(request, 'registration/login.html', {'form': form}) + +def verify_otp_login(request): + user_id = request.session.get('pre_otp_user_id') + if not user_id: + return redirect('login') + + user = get_object_or_404(User, id=user_id) + profile = user.profile + + if request.method == 'POST': + 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_record and otp_record.is_valid(): + otp_record.is_used = True + otp_record.save() + + login(request, user) + del request.session['pre_otp_user_id'] + messages.success(request, _("Logged in successfully!")) + return redirect('dashboard') + else: + messages.error(request, _("Invalid or expired verification code.")) + else: + form = OTPVerifyForm() + + return render(request, 'registration/verify_otp.html', {'form': form, 'purpose': 'login'}) + @login_required def dashboard(request): profile, created = Profile.objects.get_or_create(user=request.user) @@ -234,4 +345,4 @@ def accept_bid(request, bid_id): send_whatsapp_message(owner_phone, msg) messages.success(request, _("Bid accepted! Shipment is now in progress.")) - return redirect('shipment_detail', shipment_id=bid.shipment.id) + return redirect('shipment_detail', shipment_id=bid.shipment.id) \ No newline at end of file