diff --git a/.env.example b/.env.example index ed40286..61ef4c0 100644 --- a/.env.example +++ b/.env.example @@ -8,3 +8,5 @@ DB_USER= DB_PASS= DB_HOST= DB_PORT= +EMAIL_SENDER= +EMAILOFFICE_KEY= diff --git a/accounts/management/commands/remove_duplicates.py b/accounts/management/commands/remove_duplicates.py new file mode 100644 index 0000000..e5d6d45 --- /dev/null +++ b/accounts/management/commands/remove_duplicates.py @@ -0,0 +1,39 @@ +#აქ ანუ რადგან რეალურ დათაბეიზში არის შემთხვევები როდესაც ორ მომხმარებელს +#აქვს ერთნაირი ნომერი ვასუფთავებთ ეგეთების ნომრებს გარდა ყველაზე ბოლო ვინც დაემატა + + +from django.core.management.base import BaseCommand +from django.contrib.auth import get_user_model +from django.db.models import Count, Max + + +User = get_user_model() + +class Command(BaseCommand): + help = 'Remove duplicate user entries and keep only the latest ones based on primary key, prioritizing phone number, then email, and finally username, ignoring users with associated enrollments' + + def handle(self, *args, **kwargs): + # Step 1: Delete duplicates based on phone number + phone_duplicates = User.objects.values('phone_number').annotate(count=Count('id')).filter(count__gt=1) + for duplicate in phone_duplicates: + users_to_update = User.objects.filter(phone_number=duplicate['phone_number']).order_by('-id')[1:] + for i, user in enumerate(users_to_update, start=1): + # Generate a unique phone number + phone_number = self.generate_unique_phone_number() + # Update the user's phone number + user.phone_number = phone_number + user.save() + + self.stdout.write(self.style.SUCCESS('Duplicate users removed successfully')) + + def generate_unique_phone_number(self): + # Start with 1 + counter = 1 + while True: + # Generate the phone number with leading zeros + phone_number = f"{counter:09d}" + # Check if a user with this phone number already exists + if not User.objects.filter(phone_number=phone_number).exists(): + return phone_number + # Increment the counter if the phone number is not unique + counter += 1 \ No newline at end of file diff --git a/accounts/migrations/0013_alter_bitcampuser_phone_number.py b/accounts/migrations/0013_alter_bitcampuser_phone_number.py new file mode 100644 index 0000000..3a02a16 --- /dev/null +++ b/accounts/migrations/0013_alter_bitcampuser_phone_number.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.4 on 2024-05-15 09:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0012_authverificationcode_created_at'), + ] + + operations = [ + migrations.AlterField( + model_name='bitcampuser', + name='phone_number', + field=models.CharField(max_length=16, unique=True), + ), + ] diff --git a/accounts/migrations/0014_alter_bitcampuser_phone_number.py b/accounts/migrations/0014_alter_bitcampuser_phone_number.py new file mode 100644 index 0000000..100a2fc --- /dev/null +++ b/accounts/migrations/0014_alter_bitcampuser_phone_number.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.4 on 2024-05-15 09:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0013_alter_bitcampuser_phone_number'), + ] + + operations = [ + migrations.AlterField( + model_name='bitcampuser', + name='phone_number', + field=models.CharField(max_length=16, null=True, unique=True), + ), + ] diff --git a/accounts/models.py b/accounts/models.py index c396d9d..ed15c35 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -27,7 +27,9 @@ class BitCampUser(AbstractUser): ) phone_number = models.CharField( - max_length=16 + max_length=16, + unique=True, + null=True ) email = models.EmailField( @@ -37,7 +39,15 @@ class BitCampUser(AbstractUser): ) def __str__(self): - return self.phone_number + if self.username: + return self.username + elif self.phone_number: + return self.phone_number + elif self.email: + return self.email + else: + return "" + class AuthVerificationCode(models.Model): user_id = models.ForeignKey( diff --git a/accounts/tests.py b/accounts/tests.py index c287b2d..09bfef9 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -2,13 +2,13 @@ from django.test import TestCase from io import StringIO from accounts.models import BitCampUser - +from accounts.models import Enrollment +from django.contrib.auth import get_user_model # if you want to test it then comment this in : views.py 97,98 and also views.py in 89 ,90 # # if not request.data.get("phone_number").isdigit() or len(request.data.get("phone_number")) != 9: # return Response({"error": "Invalid phone number format"}, status=status.HTTP_400_BAD_REQUEST) - class UpdatePhoneNumbersTestCase(TestCase): def setUp(self): # Create BitCampUser objects with different phone numbers @@ -203,3 +203,85 @@ def tearDown(self): self.user24.delete() self.user25.delete() +User = get_user_model() + +class RemoveDuplicateUsersTestCase(TestCase): + def setUp(self): + # Create some duplicate users + self.user1 = User.objects.create(username='user1', email='user1@example.com', phone_number='123456789') + self.user2 = User.objects.create(username='user2', email='user2@example.com', phone_number='987654321') + self.user3 = User.objects.create(username='user3', email='user3@example.com', phone_number='123456789') + self.user4 = User.objects.create(username='user4', email='user4@example.com', phone_number='111111111') + self.user5 = User.objects.create(username='user5', email='user5@example.com', phone_number='111111111') + + def test_remove_duplicate_users(self): + # Ensure there are duplicates + self.assertEqual(User.objects.count(), 5) + + # Call the management command + call_command('remove_duplicates') + + # Check that duplicates are updated + self.assertEqual(User.objects.count(), 5) + + # Check that the phone number is set to an empty string for duplicate users with more than one entry + self.assertEqual(User.objects.get(id=self.user1.id).phone_number, "000000002") + self.assertEqual(User.objects.get(id=self.user3.id).phone_number, "123456789") + + # Check that the email is not changed for duplicate users + self.assertEqual(User.objects.get(id=self.user1.id).email, 'user1@example.com') + self.assertEqual(User.objects.get(id=self.user3.id).email, 'user3@example.com') + + # Check that the phone number is not changed for the latest user + self.assertEqual(User.objects.get(id=self.user2.id).phone_number, '987654321') + + # Check that the username is not changed for any user + self.assertEqual(User.objects.get(id=self.user1.id).username, 'user1') + self.assertEqual(User.objects.get(id=self.user2.id).username, 'user2') + self.assertEqual(User.objects.get(id=self.user3.id).username, 'user3') + + # Check that users without duplicates are not affected + self.assertEqual(User.objects.get(id=self.user4.id).phone_number, '000000001') + self.assertEqual(User.objects.get(id=self.user5.id).phone_number, '111111111') + self.assertEqual(User.objects.get(id=self.user4.id).email, 'user4@example.com') + self.assertEqual(User.objects.get(id=self.user5.id).email, 'user5@example.com') + self.assertEqual(User.objects.get(id=self.user4.id).username, 'user4') + self.assertEqual(User.objects.get(id=self.user5.id).username, 'user5') + def test_no_duplicates(self): + # Create users without duplicates + User.objects.create(username='user6', email='user6@example.com', phone_number='666666666') + User.objects.create(username='user7', email='user7@example.com', phone_number='777777777') + + # Call the management command + call_command('remove_duplicates') + + # Check that no changes are made for users without duplicates + self.assertEqual(User.objects.get(username='user6').phone_number, '666666666') + self.assertEqual(User.objects.get(username='user7').phone_number, '777777777') + + def test_multiple_duplicates(self): + # Create multiple duplicates for one user + User.objects.create(username='user8', email='user8@example.com', phone_number='888888888') + User.objects.create(username='user9', email='user9@example.com', phone_number='888888888') + User.objects.create(username='user10', email='user10@example.com', phone_number='888888888') + + # Call the management command + call_command('remove_duplicates') + + # Check that the phone number is updated correctly for multiple duplicates + self.assertEqual(User.objects.get(username='user8').phone_number, '000000004') + self.assertEqual(User.objects.get(username='user9').phone_number, '000000003') + self.assertEqual(User.objects.get(username='user10').phone_number, '888888888') + + def test_no_duplicates_exist(self): + # Call the management command when no duplicates exist + call_command('remove_duplicates') + + # Check that no changes are made + self.assertEqual(User.objects.get(username='user4').phone_number, '000000001') + self.assertEqual(User.objects.get(username='user5').phone_number, '111111111') + self.assertEqual(User.objects.get(username='user4').email, 'user4@example.com') + self.assertEqual(User.objects.get(username='user5').email, 'user5@example.com') + self.assertEqual(User.objects.get(username='user4').username, 'user4') + self.assertEqual(User.objects.get(username='user5').username, 'user5') + \ No newline at end of file diff --git a/accounts/urls.py b/accounts/urls.py index c00a4d2..ff31819 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -9,6 +9,7 @@ path("auth/profile/update", views.UpdateUser.as_view(), name="updateuser"), path("auth/connect-to-discord", views.Discord.as_view(), name="connect-to-discord"), path("user", views.RegByNum.as_view(), name="register by phone number"), + path("user/email", views.RegByEmail.as_view(), name="register by Email"), path("enroll", views.NewEnroll.as_view(), name="enrollment"), path("enrollments", views.MyEnrolls.as_view(), name="myenrollments"), path('enrollments//check-payze-subscription-status', views.CheckPayzeSubscriptionStatusView.as_view(), name='check-payze-status'), diff --git a/accounts/views.py b/accounts/views.py index 886c9a7..da93783 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -17,11 +17,15 @@ from django.conf import settings import logging from bitcamp import settings as bitcamp_settings + import time from django.http import JsonResponse import json +from postmarker.core import PostmarkClient +import re + logger = logging.getLogger(__name__) @@ -86,6 +90,13 @@ class RegByNum(APIView): @extend_schema(responses=serializers.BitCampUserSerializer) def post(self, request, **kwargs): # I am not using any AI for this so we're gonna have to go in blind + + try: + if request.data.get("email"): + return RegByEmail.post(RegByEmail, request) + except: + pass + try: if request.data.get("code") and request.data.get("phone_number"): @@ -120,13 +131,12 @@ def post(self, request, **kwargs): }, status=status.HTTP_201_CREATED) except: pass - # We are expecting the phone number as username serializer = serializers.BitCampUserSerializer( data=request.data, partial=True ) - + if serializer.is_valid(): serializer.save() user = serializer.instance @@ -199,7 +209,158 @@ def post(self, request, **kwargs): "token" : token.key }, status=status.HTTP_202_ACCEPTED) return Response({}, status=status.HTTP_400_BAD_REQUEST) + +class RegByEmail(APIView): + @extend_schema(responses=serializers.BitCampUserSerializer) + def post(self, request, **kwargs): + try: + email= request.data.get("email") + + if not self.validate_email(email): + return Response({"error": "Invalid email format"}, status=status.HTTP_400_BAD_REQUEST) + + if request.data.get("code") and request.data.get("email"): + email= request.data.get("email") + + return LogByEmail.post(LogByEmail, request) + except: + pass + + try: + email= request.data.get("email") + + if not self.validate_email(email): + return Response({"error": "Invalid email format"}, status=status.HTTP_400_BAD_REQUEST) + user = models.BitCampUser.objects.get( + email=request.data.get("email") + ) + if not user: + raise Exception + + authcode = self.generatecode(self) + models.AuthVerificationCode.objects.create( + user_id=user, + verification_code=authcode + ) + + + if not self.sendcode(self, authcode, user.email): + return Response({"error": "Failed to send SMS code"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + return Response({ + "message": "Code was generated and sent to the email", + }, status=status.HTTP_201_CREATED) + except: + pass + serializer = serializers.BitCampUserSerializer( + data=request.data, + partial=True + ) + if serializer.is_valid(): + serializer.save() + user = serializer.instance + # We just set the username as the phone_number + user.username = user.email + try: + user.set_password(request.data["password"]) + except: + # Dont try this at home kids + password = SignupUser.randompass(self) + user.set_password(password) + user.save() + + authcode = self.generatecode(self) + models.AuthVerificationCode.objects.create( + user_id=user, + verification_code=authcode + ) + + if not self.sendcode(self,authcode, user.email): + return Response({"error": "Failed to send SMS code"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + return Response({ + "message": "Code was generated and sent to the email", + }, status=status.HTTP_201_CREATED) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + + + def sendcode(self, code, destination): + postmark = PostmarkClient(server_token=settings.EMAILOFFICE_KEY) + message_body = f""" + + +

პატივცემულო ბითქემფის მომხმარებელო,

+

თქვენს ანგარიშზე ავტორიზაციის კოდი:

+

{code}

+

გთხოვთ შეიყვანოთ კოდი საიტზე მითითებულ ადგილას

+

პატივისცემით,
ბითქემფის გუნდი

+ + + """ + try: + response = postmark.emails.send( + From=settings.EMAIL_SENDER, + To=destination, + Subject='ელ. ფოსტით ავტორიზაცია', + HtmlBody=message_body + ) + if "Message" in response and response["Message"] == "OK": + return True + else: + return False + except Exception as e: + print(f"Error sending email: {e}") + return False + def generatecode(self): + # This is pure high quality code generator written by yours truly + # Epic Python one liner + return "".join(random.sample(list("0123456789"), 6)) # BTW letters say BitCamp + # I know it had to be only numbers but common, letters make it more safe (: + # Its called a verification CODE for a reason + # It would have been a verification NUMBER if there were only numbers + + def validate_email(email): + # Regular expression pattern for validating email addresses + email_pattern = re.compile(r'^[\w\.-]+@[a-zA-Z0-9-]+\.[a-zA-Z]{2,}(\.[a-zA-Z]{2,})?$') + # Check if the email matches the pattern + if email_pattern.match(email): + return True + else: + return False + + +class LogByEmail(APIView): + @extend_schema(responses=serializers.BitCampUserSerializer) + def post(self, request, **kwargs): + if request.data.get("code"): + try: + verification = models.AuthVerificationCode.objects.get(verification_code=request.data.get("code")) + if djtimezone.now() - verification.created_at > timedelta(minutes=1): + return Response({"error": "Verification code expired"}, status=status.HTTP_400_BAD_REQUEST) + except Exception as error: + print(error) + return Response({"error": "Invalid code"}, status=status.HTTP_400_BAD_REQUEST) + + user = verification.user_id + + + try: + if not user.email == request.data.get("email"): + return Response({"error": "Invalid email"}, status=status.HTTP_400_BAD_REQUEST) + except: + return Response({"error": "Invalid email"}, status=status.HTTP_400_BAD_REQUEST) + + token, created = Token.objects.get_or_create(user=user) + + return Response({ + "token" : token.key + }, status=status.HTTP_202_ACCEPTED) + return Response({}, status=status.HTTP_400_BAD_REQUEST) + + class CurrentUser(APIView): authentication_classes = [TokenAuthentication] permission_classes = [IsAuthenticated] diff --git a/bitcamp/settings.py b/bitcamp/settings.py index 65f1177..6b854bd 100644 --- a/bitcamp/settings.py +++ b/bitcamp/settings.py @@ -4,9 +4,11 @@ load_dotenv() PAYZE_API_KEY = os.environ.get('PAYZE_API_KEY') - SMSOFFICE_KEY = os.environ.get("SMSOFFICE_KEY") +EMAILOFFICE_KEY=os.environ.get("EMAILOFFICE_KEY") +EMAIL_SENDER=os.environ.get("EMAIL_SENDER") + BASE_DIR = Path(__file__).resolve().parent.parent SECRET_KEY = os.environ["SECRET_KEY"]