diff --git a/breathecode/authenticate/actions.py b/breathecode/authenticate/actions.py index 23f25a8b7..c476e621e 100644 --- a/breathecode/authenticate/actions.py +++ b/breathecode/authenticate/actions.py @@ -902,3 +902,117 @@ def get_app(pk: str | int) -> App: raise Exception('App not found') return app + + +def accept_invite_action(data={}, token=None, lang='en'): + from breathecode.payments.models import Invoice, Bag, Plan + + password1 = data.get('password', None) + password2 = data.get('repeat_password', None) + + invite = UserInvite.objects.filter(token=str(token), status='PENDING', email__isnull=False).first() + if invite is None: + raise Exception( + translation(lang, + en='Invalid or expired invitation ' + str(token), + es='Invitación inválida o expirada ' + str(token))) + + first_name = data.get('first_name', None) + last_name = data.get('last_name', None) + if first_name is None or first_name == '' or last_name is None or last_name == '': + raise Exception(translation(lang, en='Invalid first or last name', es='Nombre o apellido inválido')) + + if password1 != password2: + raise Exception(translation(lang, en='Passwords don\'t match', es='Las contraseñas no coinciden')) + + if not password1: + raise Exception(translation(lang, en='Password is empty', es='La contraseña está vacía')) + + user = User.objects.filter(email=invite.email).first() + if user is None: + user = User(email=invite.email, first_name=first_name, last_name=last_name, username=invite.email) + user.save() + user.set_password(password1) + user.save() + + if invite.academy is not None: + profile = ProfileAcademy.objects.filter(email=invite.email, academy=invite.academy).first() + if profile is None: + role = invite.role + if not role: + role = Role.objects.filter(slug='student').first() + + if not role: + raise Exception( + translation(lang, + en='Unexpected error occurred with invite, please contact the ' + 'staff of 4geeks', + es='Ocurrió un error inesperado con la invitación, por favor ' + 'contacta al staff de 4geeks')) + + profile = ProfileAcademy(email=invite.email, + academy=invite.academy, + role=role, + first_name=first_name, + last_name=last_name) + + if invite.first_name is not None and invite.first_name != '': + profile.first_name = invite.first_name + if invite.last_name is not None and invite.last_name != '': + profile.last_name = invite.last_name + + profile.user = user + profile.status = 'ACTIVE' + profile.save() + + if invite.cohort is not None: + role = 'student' + if invite.role is not None and invite.role.slug != 'student': + role = invite.role.slug.upper() + + cu = CohortUser.objects.filter(user=user, cohort=invite.cohort).first() + if cu is None: + cu = CohortUser(user=user, cohort=invite.cohort, role=role.upper()) + cu.save() + + plan = Plan.objects.filter(cohort_set__cohorts=invite.cohort, invites=invite).first() + + if plan and invite.user and invite.cohort.academy.main_currency and ( + invite.cohort.available_as_saas == True or + (invite.cohort.available_as_saas == None and invite.cohort.academy.available_as_saas == True)): + utc_now = timezone.now() + + bag = Bag() + bag.chosen_period = 'NO_SET' + bag.status = 'PAID' + bag.type = 'INVITED' + bag.how_many_installments = 1 + bag.academy = invite.cohort.academy + bag.user = user + bag.is_recurrent = False + bag.was_delivered = False + bag.token = None + bag.currency = invite.cohort.academy.main_currency + bag.expires_at = None + + bag.save() + + bag.plans.add(plan) + bag.selected_cohorts.add(invite.cohort) + + invoice = Invoice(amount=0, + paid_at=utc_now, + user=invite.user, + bag=bag, + academy=bag.academy, + status='FULFILLED', + currency=bag.academy.main_currency) + invoice.save() + + payment_tasks.build_plan_financing.delay(bag.id, invoice.id, is_free=True) + + invite.status = 'ACCEPTED' + invite.is_email_validated = True + invite.save() + + return invite diff --git a/breathecode/authenticate/serializers.py b/breathecode/authenticate/serializers.py index a0b845ff3..1234905c7 100644 --- a/breathecode/authenticate/serializers.py +++ b/breathecode/authenticate/serializers.py @@ -1291,7 +1291,7 @@ def validate(self, data: dict[str, str]): now = str(timezone.now()) if not self.instance: - data['token'] = hashlib.sha1((now + data['email']).encode('UTF-8')).hexdigest() + data['token'] = hashlib.sha512((data['email']).encode('UTF-8') + os.urandom(64)).hexdigest() return data diff --git a/breathecode/authenticate/tests/urls/tests_member_invite_token.py b/breathecode/authenticate/tests/urls/tests_member_invite_token.py index babe78f1e..00018586b 100644 --- a/breathecode/authenticate/tests/urls/tests_member_invite_token.py +++ b/breathecode/authenticate/tests/urls/tests_member_invite_token.py @@ -6,6 +6,7 @@ from unittest.mock import MagicMock, patch from django.template import loader from django.urls.base import reverse_lazy +import pytest from rest_framework import status from django.http.request import HttpRequest from random import randint @@ -139,6 +140,28 @@ def salt(self): return 'salt' +def post_serializer(data={}): + return { + 'created_at': ..., + 'email': None, + 'id': 0, + 'sent_at': None, + 'status': 'PENDING', + **data, + } + + +created_at = None + + +@pytest.fixture(autouse=True) +def setup(monkeypatch, utc_now): + global created_at + created_at = utc_now + monkeypatch.setattr('django.utils.timezone.now', MagicMock(return_value=utc_now)) + yield + + class AuthenticateTestSuite(AuthTestCase): """Authentication test suite""" """ @@ -1020,3 +1043,88 @@ def test_member_invite_token__post__with_cohort__without_role(self): ]) self.assertEqual(self.bc.database.list_of('admissions.CohortUser'), []) + + """ + 🔽🔽🔽 POST JSON password is empty, UserInvite with email + """ + + @patch('django.template.loader.render_to_string', MagicMock(side_effect=render_to_string_mock)) + @patch('django.contrib.auth.hashers.get_hasher', MagicMock(side_effect=GetHasherMock)) + @patch('django.db.models.signals.pre_delete.send', MagicMock(return_value=None)) + @patch('breathecode.admissions.signals.student_edu_status_updated.send', MagicMock(return_value=None)) + def test__post__json__password_is_empty(self): + user_invite = {'email': 'user@dotdotdotdot.dot'} + model = self.bc.database.create(user_invite=user_invite) + url = reverse_lazy('authenticate:member_invite_token', kwargs={'token': model.user_invite.token}) + + data = {'first_name': 'abc', 'last_name': 'xyz'} + response = self.client.post(url, data, format='json') + + json = response.json() + expected = {'detail': 'Password is empty', 'status_code': 400} + + self.assertEqual(json, expected) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), [ + self.bc.format.to_dict(model.user_invite), + ]) + + self.assertEqual(self.bc.database.list_of('auth.User'), []) + self.assertEqual(self.bc.database.list_of('authenticate.ProfileAcademy'), []) + self.assertEqual(self.bc.database.list_of('admissions.CohortUser'), []) + + """ + 🔽🔽🔽 POST JSON with first name, last name and passwords, UserInvite with email + """ + + @patch('django.template.loader.render_to_string', MagicMock(side_effect=render_to_string_mock)) + @patch('django.contrib.auth.hashers.get_hasher', MagicMock(side_effect=GetHasherMock)) + @patch('django.db.models.signals.pre_delete.send', MagicMock(return_value=None)) + @patch('breathecode.admissions.signals.student_edu_status_updated.send', MagicMock(return_value=None)) + def test__post__json__with_first_name_last_name_and_passwords(self): + user_invite = {'email': 'user@dotdotdotdot.dot'} + model = self.bc.database.create(user_invite=user_invite) + url = reverse_lazy('authenticate:member_invite_token', kwargs={'token': model.user_invite.token}) + data = { + 'first_name': 'abc', + 'last_name': 'xyz', + 'password': '^3^3uUppppp', + 'repeat_password': '^3^3uUppppp', + } + response = self.client.post(url, data, format='json') + + json = response.json() + expected = post_serializer({ + 'id': 1, + 'created_at': self.bc.datetime.to_iso_string(created_at), + 'status': 'ACCEPTED', + 'email': 'user@dotdotdotdot.dot', + }) + + self.assertEqual(json, expected) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), + [{ + **self.bc.format.to_dict(model.user_invite), + 'status': 'ACCEPTED', + 'is_email_validated': True, + }]) + + user_db = [ + x for x in self.bc.database.list_of('auth.User') if x['date_joined'] and x.pop('date_joined') + ] + self.assertEqual(user_db, [{ + 'email': 'user@dotdotdotdot.dot', + 'first_name': 'abc', + 'id': 1, + 'is_active': True, + 'is_staff': False, + 'is_superuser': False, + 'last_login': None, + 'last_name': 'xyz', + 'password': CSRF_TOKEN, + 'username': 'user@dotdotdotdot.dot' + }]) + + self.assertEqual(self.bc.database.list_of('authenticate.ProfileAcademy'), []) + self.assertEqual(self.bc.database.list_of('admissions.CohortUser'), []) diff --git a/breathecode/authenticate/tests/urls/tests_subscribe.py b/breathecode/authenticate/tests/urls/tests_subscribe.py index 54dc7751b..8d30df0f4 100644 --- a/breathecode/authenticate/tests/urls/tests_subscribe.py +++ b/breathecode/authenticate/tests/urls/tests_subscribe.py @@ -3,11 +3,13 @@ """ import hashlib from datetime import datetime +import os import random from unittest.mock import MagicMock, call, patch from django.urls.base import reverse_lazy from django.utils import timezone +import pytest from rest_framework import status from breathecode.notify import actions as notify_actions @@ -122,6 +124,15 @@ def put_serializer(user_invite, cohort=None, syllabus=None, plans=[], data={}): } +b = os.urandom(64) + + +@pytest.fixture(autouse=True) +def setup(monkeypatch): + monkeypatch.setattr('os.urandom', lambda _: b) + yield + + class SubscribeTestSuite(AuthTestCase): """Test /v1/auth/subscribe""" """ @@ -180,8 +191,7 @@ def test_task__post__without_user_invite(self): 'role_id': None, 'sent_at': None, 'status': 'ACCEPTED', - 'token': hashlib.sha1( - (str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'token': hashlib.sha512('pokemon@potato.io'.encode('UTF-8') + b).hexdigest(), 'process_message': '', 'process_status': 'DONE', 'syllabus_id': None, @@ -422,7 +432,7 @@ def test_task__post__with_user_invite(self): 'role_id': None, 'sent_at': None, 'status': 'ACCEPTED', - 'token': hashlib.sha1((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'token': hashlib.sha512(('pokemon@potato.io').encode('UTF-8') + b).hexdigest(), 'process_message': '', 'process_status': 'DONE', 'syllabus_id': None, @@ -456,8 +466,8 @@ def test_task__post__with_user_invite(self): 'verify_email', 'pokemon@potato.io', { 'SUBJECT': '4Geeks - Validate account', - 'LINK': ('http://localhost:8000/v1/auth/password/' + hashlib.sha1( - (str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest()) + 'LINK': ('http://localhost:8000/v1/auth/password/' + + hashlib.sha512('pokemon@potato.io'.encode('UTF-8') + b).hexdigest()) }) ]) @@ -518,7 +528,7 @@ def test_task__post__does_not_get_in_waiting_list_using_a_plan(self): 'status': 'WAITING_LIST', 'process_message': '', 'process_status': 'PENDING', - 'token': hashlib.sha1((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'token': hashlib.sha512(('pokemon@potato.io').encode('UTF-8') + b).hexdigest(), 'syllabus_id': None, **data, } @@ -582,7 +592,7 @@ def test_task__post__get_in_waiting_list_using_a_plan(self): 'role_id': None, 'sent_at': None, 'status': 'ACCEPTED', - 'token': hashlib.sha1((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'token': hashlib.sha512(('pokemon@potato.io').encode('UTF-8') + b).hexdigest(), 'process_message': '', 'process_status': 'DONE', 'syllabus_id': None, @@ -594,7 +604,7 @@ def test_task__post__get_in_waiting_list_using_a_plan(self): self.assertEqual(self.bc.database.list_of('payments.Plan'), [plan_db_item(model.plan, data={})]) self.bc.check.queryset_with_pks(model.plan.invites.all(), [2]) - token = hashlib.sha1((str(now) + data['email']).encode('UTF-8')).hexdigest() + token = hashlib.sha512('pokemon@potato.io'.encode('UTF-8') + b).hexdigest() self.bc.check.calls(notify_actions.send_email_message.call_args_list, [ call('verify_email', 'pokemon@potato.io', { @@ -704,7 +714,7 @@ def test__post__course_without_syllabus(self): self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), [ user_invite_db_item( data={ - 'token': hashlib.sha1((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'token': hashlib.sha512(('pokemon@potato.io').encode('UTF-8') + b).hexdigest(), 'process_status': 'DONE', 'status': 'ACCEPTED', 'academy_id': 1, @@ -730,7 +740,7 @@ def test__post__course_without_syllabus(self): self.bc.check.queryset_with_pks(model.course.invites.all(), [1]) self.assertEqual(self.bc.database.list_of('payments.Plan'), []) - token = hashlib.sha1((str(now) + data['email']).encode('UTF-8')).hexdigest() + token = hashlib.sha512('pokemon@potato.io'.encode('UTF-8') + b).hexdigest() self.bc.check.calls(notify_actions.send_email_message.call_args_list, [ call('verify_email', 'pokemon@potato.io', { @@ -783,7 +793,7 @@ def test__post__course_and_syllabus(self): self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), [ user_invite_db_item( data={ - 'token': hashlib.sha1((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'token': hashlib.sha512(('pokemon@potato.io').encode('UTF-8') + b).hexdigest(), 'process_status': 'DONE', 'status': 'ACCEPTED', 'academy_id': 1, @@ -810,7 +820,7 @@ def test__post__course_and_syllabus(self): self.bc.check.queryset_with_pks(model.course.invites.all(), [1]) self.assertEqual(self.bc.database.list_of('payments.Plan'), []) - token = hashlib.sha1((str(now) + data['email']).encode('UTF-8')).hexdigest() + token = hashlib.sha512('pokemon@potato.io'.encode('UTF-8') + b).hexdigest() self.bc.check.calls(notify_actions.send_email_message.call_args_list, [ call('verify_email', 'pokemon@potato.io', { @@ -913,7 +923,7 @@ def test__post__course_and_syllabus__waiting_list(self): self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), [ user_invite_db_item( data={ - 'token': hashlib.sha1((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'token': hashlib.sha512(('pokemon@potato.io').encode('UTF-8') + b).hexdigest(), 'process_status': 'PENDING', 'status': 'WAITING_LIST', 'academy_id': 1, @@ -976,7 +986,7 @@ def test__post__with_other_invite__course_and_syllabus__waiting_list(self): self.bc.format.to_dict(model.user_invite), user_invite_db_item( data={ - 'token': hashlib.sha1((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'token': hashlib.sha512(('pokemon@potato.io').encode('UTF-8') + b).hexdigest(), 'process_status': 'PENDING', 'status': 'WAITING_LIST', 'academy_id': 1, @@ -1039,7 +1049,7 @@ def test__post__with_other_invite__plan__waiting_list(self): self.bc.format.to_dict(model.user_invite), user_invite_db_item( data={ - 'token': hashlib.sha1((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'token': hashlib.sha512(('pokemon@potato.io').encode('UTF-8') + b).hexdigest(), 'process_status': 'PENDING', 'status': 'WAITING_LIST', 'academy_id': None, @@ -1102,7 +1112,7 @@ def test__post__with_other_invite__cohort__waiting_list(self): self.assertEqual(json, expected) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - token = hashlib.sha1((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest() + token = hashlib.sha512(('pokemon@potato.io').encode('UTF-8') + b).hexdigest() self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), [ self.bc.format.to_dict(model.user_invite), user_invite_db_item( @@ -1196,7 +1206,7 @@ def test__post__with_other_invite__syllabus__waiting_list(self): self.assertEqual(json, expected) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - token = hashlib.sha1((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest() + token = hashlib.sha512(('pokemon@potato.io').encode('UTF-8') + b).hexdigest() self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), [ self.bc.format.to_dict(model.user_invite), user_invite_db_item( @@ -1314,25 +1324,25 @@ def test_task__put__with_user_invite__cohort_as_none(self): self.assertEqual(json, expected) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), - [{ - 'user_id': 1, - 'academy_id': None, - 'author_id': None, - 'cohort_id': None, - 'id': 1, - 'is_email_validated': False, - 'role_id': None, - 'sent_at': None, - 'status': 'ACCEPTED', - 'token': hashlib.sha1( - (str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), - 'process_message': '', - 'process_status': 'DONE', - 'token': token, - 'syllabus_id': None, - **data, - }]) + self.assertEqual( + self.bc.database.list_of('authenticate.UserInvite'), + [{ + 'user_id': 1, + 'academy_id': None, + 'author_id': None, + 'cohort_id': None, + 'id': 1, + 'is_email_validated': False, + 'role_id': None, + 'sent_at': None, + 'status': 'ACCEPTED', + 'token': hashlib.sha512((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'process_message': '', + 'process_status': 'DONE', + 'token': token, + 'syllabus_id': None, + **data, + }]) user_db = self.bc.database.list_of('auth.User') for item in user_db: @@ -1400,28 +1410,28 @@ def test_task__put__with_user_invite__cohort_not_found(self): self.assertEqual(json, expected) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), - [{ - 'user_id': None, - 'academy_id': None, - 'author_id': None, - 'cohort_id': None, - 'id': 1, - 'role_id': None, - 'sent_at': None, - 'is_email_validated': False, - 'status': 'WAITING_LIST', - 'token': hashlib.sha1( - (str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), - 'process_message': '', - 'process_status': 'PENDING', - 'token': token, - 'email': 'pokemon@potato.io', - 'first_name': None, - 'last_name': None, - 'phone': '', - 'syllabus_id': None, - }]) + self.assertEqual( + self.bc.database.list_of('authenticate.UserInvite'), + [{ + 'user_id': None, + 'academy_id': None, + 'author_id': None, + 'cohort_id': None, + 'id': 1, + 'role_id': None, + 'sent_at': None, + 'is_email_validated': False, + 'status': 'WAITING_LIST', + 'token': hashlib.sha512((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'process_message': '', + 'process_status': 'PENDING', + 'token': token, + 'email': 'pokemon@potato.io', + 'first_name': None, + 'last_name': None, + 'phone': '', + 'syllabus_id': None, + }]) self.assertEqual(self.bc.database.list_of('marketing.Course'), []) self.assertEqual(self.bc.database.list_of('payments.Plan'), []) @@ -1472,25 +1482,25 @@ def test_task__put__with_user_invite__cohort_found(self): self.assertEqual(response.status_code, status.HTTP_200_OK) del data['cohort'] - self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), - [{ - 'user_id': 1, - 'academy_id': 1, - 'author_id': None, - 'cohort_id': 1, - 'id': 1, - 'is_email_validated': False, - 'role_id': None, - 'sent_at': None, - 'status': 'ACCEPTED', - 'token': hashlib.sha1( - (str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), - 'process_message': '', - 'process_status': 'DONE', - 'token': token, - 'syllabus_id': None, - **data, - }]) + self.assertEqual( + self.bc.database.list_of('authenticate.UserInvite'), + [{ + 'user_id': 1, + 'academy_id': 1, + 'author_id': None, + 'cohort_id': 1, + 'id': 1, + 'is_email_validated': False, + 'role_id': None, + 'sent_at': None, + 'status': 'ACCEPTED', + 'token': hashlib.sha512((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'process_message': '', + 'process_status': 'DONE', + 'token': token, + 'syllabus_id': None, + **data, + }]) self.assertEqual(self.bc.database.list_of('marketing.Course'), []) self.assertEqual(self.bc.database.list_of('payments.Plan'), []) @@ -1567,25 +1577,25 @@ def test_task__put__with_user_invite__cohort_found__academy_available_as_saas__u self.assertEqual(response.status_code, status.HTTP_200_OK) del data['cohort'] - self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), - [{ - 'user_id': 1, - 'academy_id': 1, - 'author_id': None, - 'cohort_id': 1, - 'id': 1, - 'is_email_validated': False, - 'role_id': None, - 'sent_at': None, - 'status': 'ACCEPTED', - 'token': hashlib.sha1( - (str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), - 'process_message': '', - 'process_status': 'DONE', - 'token': token, - 'syllabus_id': None, - **data, - }]) + self.assertEqual( + self.bc.database.list_of('authenticate.UserInvite'), + [{ + 'user_id': 1, + 'academy_id': 1, + 'author_id': None, + 'cohort_id': 1, + 'id': 1, + 'is_email_validated': False, + 'role_id': None, + 'sent_at': None, + 'status': 'ACCEPTED', + 'token': hashlib.sha512((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'process_message': '', + 'process_status': 'DONE', + 'token': token, + 'syllabus_id': None, + **data, + }]) user_db = self.bc.database.list_of('auth.User') for item in user_db: @@ -1663,25 +1673,25 @@ def test_task__put__with_user_invite__cohort_found__academy_available_as_saas__u self.assertEqual(response.status_code, status.HTTP_200_OK) del data['cohort'] - self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), - [{ - 'user_id': 1, - 'academy_id': 1, - 'author_id': 1, - 'cohort_id': 1, - 'id': 1, - 'is_email_validated': False, - 'role_id': None, - 'sent_at': None, - 'status': 'ACCEPTED', - 'token': hashlib.sha1( - (str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), - 'process_message': '', - 'process_status': 'DONE', - 'token': token, - 'syllabus_id': None, - **data, - }]) + self.assertEqual( + self.bc.database.list_of('authenticate.UserInvite'), + [{ + 'user_id': 1, + 'academy_id': 1, + 'author_id': 1, + 'cohort_id': 1, + 'id': 1, + 'is_email_validated': False, + 'role_id': None, + 'sent_at': None, + 'status': 'ACCEPTED', + 'token': hashlib.sha512((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'process_message': '', + 'process_status': 'DONE', + 'token': token, + 'syllabus_id': None, + **data, + }]) self.assertEqual(self.bc.database.list_of('marketing.Course'), []) self.assertEqual(self.bc.database.list_of('payments.Plan'), []) @@ -1725,28 +1735,28 @@ def test_task__put__with_user_invite__syllabus_not_found(self): self.assertEqual(json, expected) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), - [{ - 'user_id': None, - 'academy_id': None, - 'author_id': None, - 'cohort_id': None, - 'is_email_validated': False, - 'id': 1, - 'role_id': None, - 'sent_at': None, - 'status': 'WAITING_LIST', - 'token': hashlib.sha1( - (str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), - 'process_message': '', - 'process_status': 'PENDING', - 'token': token, - 'email': 'pokemon@potato.io', - 'first_name': None, - 'last_name': None, - 'phone': '', - 'syllabus_id': None, - }]) + self.assertEqual( + self.bc.database.list_of('authenticate.UserInvite'), + [{ + 'user_id': None, + 'academy_id': None, + 'author_id': None, + 'cohort_id': None, + 'is_email_validated': False, + 'id': 1, + 'role_id': None, + 'sent_at': None, + 'status': 'WAITING_LIST', + 'token': hashlib.sha512((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'process_message': '', + 'process_status': 'PENDING', + 'token': token, + 'email': 'pokemon@potato.io', + 'first_name': None, + 'last_name': None, + 'phone': '', + 'syllabus_id': None, + }]) self.assertEqual(self.bc.database.list_of('marketing.Course'), []) self.assertEqual(self.bc.database.list_of('payments.Plan'), []) @@ -1810,7 +1820,7 @@ def test_task__put__with_user_invite__syllabus_found(self): 'is_email_validated': False, 'sent_at': None, 'status': 'ACCEPTED', - 'token': hashlib.sha1((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'token': hashlib.sha512(('pokemon@potato.io').encode('UTF-8') + b).hexdigest(), 'process_message': '', 'process_status': 'DONE', 'token': token, @@ -1899,25 +1909,25 @@ def test_task__put__with_user_invite__syllabus_found__academy_available_as_saas_ self.assertEqual(response.status_code, status.HTTP_200_OK) del data['syllabus'] - self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), - [{ - 'user_id': 1, - 'academy_id': 1, - 'author_id': None, - 'cohort_id': None, - 'id': 1, - 'role_id': None, - 'is_email_validated': False, - 'sent_at': None, - 'status': 'ACCEPTED', - 'token': hashlib.sha1( - (str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), - 'process_message': '', - 'process_status': 'DONE', - 'token': token, - 'syllabus_id': 1, - **data, - }]) + self.assertEqual( + self.bc.database.list_of('authenticate.UserInvite'), + [{ + 'user_id': 1, + 'academy_id': 1, + 'author_id': None, + 'cohort_id': None, + 'id': 1, + 'role_id': None, + 'is_email_validated': False, + 'sent_at': None, + 'status': 'ACCEPTED', + 'token': hashlib.sha512((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'process_message': '', + 'process_status': 'DONE', + 'token': token, + 'syllabus_id': 1, + **data, + }]) user_db = self.bc.database.list_of('auth.User') for item in user_db: @@ -2001,25 +2011,25 @@ def test_task__put__with_user_invite__syllabus_found__academy_available_as_saas_ self.assertEqual(response.status_code, status.HTTP_200_OK) del data['syllabus'] - self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), - [{ - 'user_id': 1, - 'academy_id': 1, - 'author_id': 1, - 'cohort_id': None, - 'id': 1, - 'is_email_validated': False, - 'role_id': None, - 'sent_at': None, - 'status': 'ACCEPTED', - 'token': hashlib.sha1( - (str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), - 'process_message': '', - 'process_status': 'DONE', - 'token': token, - 'syllabus_id': 1, - **data, - }]) + self.assertEqual( + self.bc.database.list_of('authenticate.UserInvite'), + [{ + 'user_id': 1, + 'academy_id': 1, + 'author_id': 1, + 'cohort_id': None, + 'id': 1, + 'is_email_validated': False, + 'role_id': None, + 'sent_at': None, + 'status': 'ACCEPTED', + 'token': hashlib.sha512((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'process_message': '', + 'process_status': 'DONE', + 'token': token, + 'syllabus_id': 1, + **data, + }]) self.assertEqual(self.bc.database.list_of('marketing.Course'), []) self.assertEqual(self.bc.database.list_of('payments.Plan'), []) @@ -2069,28 +2079,28 @@ def test_task__put__plan_does_not_exist(self): self.assertEqual(json, expected) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) # del data['cohort'] - self.assertEqual(self.bc.database.list_of('authenticate.UserInvite'), - [{ - 'user_id': None, - 'academy_id': 1, - 'author_id': None, - 'cohort_id': 1, - 'id': 1, - 'is_email_validated': False, - 'role_id': None, - 'sent_at': None, - 'status': 'WAITING_LIST', - 'email': 'pokemon@potato.io', - 'first_name': None, - 'last_name': None, - 'phone': '', - 'token': hashlib.sha1( - (str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), - 'process_message': '', - 'process_status': 'PENDING', - 'token': token, - 'syllabus_id': None, - }]) + self.assertEqual( + self.bc.database.list_of('authenticate.UserInvite'), + [{ + 'user_id': None, + 'academy_id': 1, + 'author_id': None, + 'cohort_id': 1, + 'id': 1, + 'is_email_validated': False, + 'role_id': None, + 'sent_at': None, + 'status': 'WAITING_LIST', + 'email': 'pokemon@potato.io', + 'first_name': None, + 'last_name': None, + 'phone': '', + 'token': hashlib.sha512((str(now) + 'pokemon@potato.io').encode('UTF-8')).hexdigest(), + 'process_message': '', + 'process_status': 'PENDING', + 'token': token, + 'syllabus_id': None, + }]) self.assertEqual(self.bc.database.list_of('marketing.Course'), []) self.assertEqual(self.bc.database.list_of('payments.Plan'), []) diff --git a/breathecode/authenticate/views.py b/breathecode/authenticate/views.py index 3fdb66cdd..5a0f98280 100644 --- a/breathecode/authenticate/views.py +++ b/breathecode/authenticate/views.py @@ -44,9 +44,9 @@ from breathecode.utils.shorteners import C from breathecode.utils.views import (private_view, render_message, set_query_parameter) -from .actions import (generate_academy_token, get_app, get_user_language, resend_invite, reset_password, - set_gitpod_user_expiration, update_gitpod_users, sync_organization_members, - get_github_scopes, accept_invite) +from .actions import (accept_invite_action, generate_academy_token, get_app, get_user_language, resend_invite, + reset_password, set_gitpod_user_expiration, update_gitpod_users, + sync_organization_members, get_github_scopes, accept_invite) from .authentication import ExpiringTokenAuthentication from .forms import (InviteForm, LoginForm, PasswordChangeCustomForm, PickPasswordForm, ResetPasswordForm, SyncGithubUsersForm) @@ -1572,11 +1572,12 @@ def render_user_invite(request, token): @api_view(['GET', 'POST']) @permission_classes([AllowAny]) def render_invite(request, token, member_id=None): - from breathecode.payments.models import Invoice, Bag, Plan _dict = request.POST.copy() _dict['token'] = token _dict['callback'] = request.GET.get('callback', '') + lang = get_user_language(request) + if request.method == 'GET': invite = UserInvite.objects.filter(token=token, status='PENDING').first() if invite is None: @@ -1603,124 +1604,29 @@ def render_invite(request, token, member_id=None): 'form': form, }) - if request.method == 'POST': - form = InviteForm(_dict) - password1 = request.POST.get('password', None) - password2 = request.POST.get('repeat_password', None) - - invite = UserInvite.objects.filter(token=str(token), status='PENDING', email__isnull=False).first() - if invite is None: - messages.error(request, 'Invalid or expired invitation ' + str(token)) - return render(request, 'form_invite.html', {'form': form}) + if request.method == 'POST' and request.META.get('CONTENT_TYPE') == 'application/json': + _dict = request.data + _dict['token'] = token + _dict['callback'] = request.GET.get('callback', '') - first_name = request.POST.get('first_name', None) - last_name = request.POST.get('last_name', None) - if first_name is None or first_name == '' or last_name is None or last_name == '': - messages.error(request, 'Invalid first or last name') - return render(request, 'form_invite.html', { - 'form': form, - }) + try: + invite = accept_invite_action(_dict, token, lang) + except Exception as e: + raise ValidationException(str(e)) - if password1 != password2: - messages.error(request, 'Passwords don\'t match') - return render(request, 'form_invite.html', { - 'form': form, - }) + serializer = UserInviteShortSerializer(invite, many=False) + return Response(serializer.data) - if not password1: - messages.error(request, 'Password is empty') + if request.method == 'POST': + try: + accept_invite_action(_dict, token, lang) + except Exception as e: + form = InviteForm(_dict) + messages.error(request, str(e)) return render(request, 'form_invite.html', { 'form': form, }) - user = User.objects.filter(email=invite.email).first() - if user is None: - user = User(email=invite.email, first_name=first_name, last_name=last_name, username=invite.email) - user.save() - user.set_password(password1) - user.save() - - if invite.academy is not None: - profile = ProfileAcademy.objects.filter(email=invite.email, academy=invite.academy).first() - if profile is None: - role = invite.role - if not role: - role = Role.objects.filter(slug='student').first() - - if not role: - messages.error( - request, 'Unexpected error occurred with invite, please contact the ' - 'staff of 4geeks') - return render(request, 'form_invite.html', { - 'form': form, - }) - - profile = ProfileAcademy(email=invite.email, - academy=invite.academy, - role=role, - first_name=first_name, - last_name=last_name) - - if invite.first_name is not None and invite.first_name != '': - profile.first_name = invite.first_name - if invite.last_name is not None and invite.last_name != '': - profile.last_name = invite.last_name - - profile.user = user - profile.status = 'ACTIVE' - profile.save() - - if invite.cohort is not None: - role = 'student' - if invite.role is not None and invite.role.slug != 'student': - role = invite.role.slug.upper() - - cu = CohortUser.objects.filter(user=user, cohort=invite.cohort).first() - if cu is None: - cu = CohortUser(user=user, cohort=invite.cohort, role=role.upper()) - cu.save() - - plan = Plan.objects.filter(cohort_set__cohorts=invite.cohort, invites=invite).first() - - if plan and invite.user and invite.cohort.academy.main_currency and ( - invite.cohort.available_as_saas == True or - (invite.cohort.available_as_saas == None - and invite.cohort.academy.available_as_saas == True)): - utc_now = timezone.now() - - bag = Bag() - bag.chosen_period = 'NO_SET' - bag.status = 'PAID' - bag.type = 'INVITED' - bag.how_many_installments = 1 - bag.academy = invite.cohort.academy - bag.user = user - bag.is_recurrent = False - bag.was_delivered = False - bag.token = None - bag.currency = invite.cohort.academy.main_currency - bag.expires_at = None - - bag.save() - - bag.plans.add(plan) - bag.selected_cohorts.add(invite.cohort) - - invoice = Invoice(amount=0, - paid_at=utc_now, - user=invite.user, - bag=bag, - academy=bag.academy, - status='FULFILLED', - currency=bag.academy.main_currency) - invoice.save() - - payment_tasks.build_plan_financing.delay(bag.id, invoice.id, is_free=True) - - invite.status = 'ACCEPTED' - invite.is_email_validated = True - invite.save() - callback = request.POST.get('callback', None) if callback: uri = callback[0] if isinstance(callback, list) else callback