diff --git a/dta_service/core/admin.py b/dta_service/core/admin.py index f74bbf2..3b9ac48 100644 --- a/dta_service/core/admin.py +++ b/dta_service/core/admin.py @@ -27,6 +27,7 @@ from core.models import ( SupportCase, SupportMessage, FAQ, + OneTimePasscode, ) @@ -428,3 +429,11 @@ admin.site.register(PropertySaleInfo, PropertySaleInfoAdmin) admin.site.register(PropertyTaxInfo, PropertyTaxInfoAdmin) admin.site.register(PropertyWalkScoreInfo, PropertyWalkScoreInfoAdmin) admin.site.register(SchoolInfo, SchoolInfoAdmin) + + +@admin.register(OneTimePasscode) +class OneTimePasscodeAdmin(admin.ModelAdmin): + list_display = ("user", "code", "purpose", "created_at", "expires_at", "used") + list_filter = ("purpose", "used") + search_fields = ("user__email", "code") + diff --git a/dta_service/core/serializers/support.py b/dta_service/core/serializers/support.py index dbe0d9f..08ab979 100644 --- a/dta_service/core/serializers/support.py +++ b/dta_service/core/serializers/support.py @@ -18,10 +18,22 @@ class SupportMessageSerializer(serializers.ModelSerializer): "user_first_name", "user_last_name", ] - read_only_fields = ["created_at", "updated_at", "user", "support_case"] + read_only_fields = ["created_at", "updated_at", "user"] + + def validate_support_case(self, value): + user = self.context["request"].user + if user.user_type != "support_agent" and value.user != user: + raise serializers.ValidationError( + "You cannot add a message to a support case that does not belong to you." + ) + return value class SupportCaseListSerializer(serializers.ModelSerializer): + user_email = serializers.EmailField(source="user.email", read_only=True) + user_first_name = serializers.CharField(source="user.first_name", read_only=True) + user_last_name = serializers.CharField(source="user.last_name", read_only=True) + class Meta: model = SupportCase fields = [ @@ -30,11 +42,22 @@ class SupportCaseListSerializer(serializers.ModelSerializer): "description", "category", "status", - "user", + "user_email", + "user_first_name", + "user_last_name", + "created_at", "updated_at", ] read_only_fields = ["created_at", "updated_at", "user"] + def validate_status(self, value): + user = self.context["request"].user + if value == "closed" and user.user_type != "support_agent": + raise serializers.ValidationError( + "You cannot close a support case unless you are a support agent." + ) + return value + class SupportCaseDetailSerializer(serializers.ModelSerializer): messages = SupportMessageSerializer(many=True, read_only=True) @@ -49,6 +72,8 @@ class SupportCaseDetailSerializer(serializers.ModelSerializer): "status", "user", "messages", + "created_at", + "updated_at", ] read_only_fields = ["created_at", "updated_at", "user"] diff --git a/dta_service/core/services/email/user.py b/dta_service/core/services/email/user.py index f39b041..bacae60 100644 --- a/dta_service/core/services/email/user.py +++ b/dta_service/core/services/email/user.py @@ -22,6 +22,7 @@ class UserEmailService(BaseEmailService): "display_name": user.first_name if user.first_name else user.email, "code": code, "expiration_minutes": settings.OTC_EXPIRATION_MINUTES, + "verify_link": f"{settings.FRONTEND_URL}/authentication/verify-email", } self.send_email( "Account Created", "user_registration_email", context, user.email @@ -33,6 +34,7 @@ class UserEmailService(BaseEmailService): "display_name": user.first_name if user.first_name else user.email, "code": code, "expiration_minutes": settings.OTC_EXPIRATION_MINUTES, + "reset_link": f"{settings.FRONTEND_URL}/authentication/reset-password", } self.send_email("Password Reset", "password_reset_email", context, user.email) diff --git a/dta_service/core/templates/emails/base_email.html b/dta_service/core/templates/emails/base_email.html new file mode 100644 index 0000000..e4610c9 --- /dev/null +++ b/dta_service/core/templates/emails/base_email.html @@ -0,0 +1,146 @@ + + + + + + + + {% block title %}Email{% endblock %} + + + + + +
+
+
+

+ {% block header_title %}Django App{% endblock %} +

+
+ +
+ {% block content %} + + {% endblock %} +
+ + +
+
+ + + \ No newline at end of file diff --git a/dta_service/core/templates/emails/base_template.html b/dta_service/core/templates/emails/base_template.html deleted file mode 100644 index addc517..0000000 --- a/dta_service/core/templates/emails/base_template.html +++ /dev/null @@ -1,160 +0,0 @@ - - - - - - - {% block title %}Email{% endblock %} - - - - -
-
-
-

- {% block header_title %}Django App{% endblock %} -

-
- -
- {% block content %} - - {% endblock %} -
- - -
-
- - diff --git a/dta_service/core/templates/emails/password_reset_email.html b/dta_service/core/templates/emails/password_reset_email.html index 1c5eda7..59f059a 100644 --- a/dta_service/core/templates/emails/password_reset_email.html +++ b/dta_service/core/templates/emails/password_reset_email.html @@ -1,16 +1,16 @@ {% extends 'emails/base_email.html' %} {% block header_title %}Password Reset Request{% endblock %} {% block content %} -

Hello {{ user.first_name|default:user.username }},

+

Hello {{ display_name }},

We received a request to reset the password for your account. If you did not make this request, you can safely ignore this email.

-

To reset your password, please click the link below:

+

To reset your password, please click the link below or enter the code:

+

+ {{ code }} +

- + "> Reset Password
@@ -30,9 +29,7 @@ Request{% endblock %} {% block content %} into your web browser:

- {{ reset_link }} + {{ reset_link }}

This link will expire in a few hours for security reasons.

-{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/dta_service/core/templates/emails/user_registration_email.html b/dta_service/core/templates/emails/user_registration_email.html index 5340fa0..5a8e2f6 100644 --- a/dta_service/core/templates/emails/user_registration_email.html +++ b/dta_service/core/templates/emails/user_registration_email.html @@ -3,14 +3,13 @@ The Agent!{% endblock %} {% block content %}

Hello {{ display_name }},

Thank you for registering with us. We're excited to have you on board!

- Please confirm your email address by clicking the button below to activate - your account: + Please confirm your email address by entering the code below at the link: +

+

+ {{ code }}

- - Confirm Account + "> + Verify Account

@@ -30,8 +28,6 @@ The Agent!{% endblock %} {% block content %} into your web browser:

- {{ activation_link }} + {{ verify_link }}

-{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/dta_service/core/tests/reproduce_issue.py b/dta_service/core/tests/reproduce_issue.py new file mode 100644 index 0000000..6017d1a --- /dev/null +++ b/dta_service/core/tests/reproduce_issue.py @@ -0,0 +1,27 @@ +from django.test import TestCase +from django.contrib.auth import get_user_model +from rest_framework.test import APIClient +from rest_framework import status +from core.models import SupportCase + +User = get_user_model() + +class SupportCaseReproductionTests(TestCase): + def setUp(self): + self.client = APIClient() + self.user = User.objects.create_user( + email="user@example.com", password="password", user_type="property_owner" + ) + self.case = SupportCase.objects.create( + user=self.user, title="Case 1", description="Desc 1" + ) + + def test_user_can_close_own_case(self): + self.client.force_authenticate(user=self.user) + data = {"status": "closed"} + response = self.client.patch(f"/api/support/cases/{self.case.id}/", data) + + # If this passes (400 Bad Request), it confirms the fix that regular users cannot close cases + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.case.refresh_from_db() + self.assertNotEqual(self.case.status, "closed") diff --git a/dta_service/core/tests/test_support_message_fix.py b/dta_service/core/tests/test_support_message_fix.py new file mode 100644 index 0000000..a355aec --- /dev/null +++ b/dta_service/core/tests/test_support_message_fix.py @@ -0,0 +1,65 @@ +from django.test import TestCase +from django.contrib.auth import get_user_model +from rest_framework.test import APIClient +from rest_framework import status +from core.models import SupportCase, SupportMessage + +User = get_user_model() + + +class SupportMessageFixTests(TestCase): + def setUp(self): + self.client = APIClient() + self.user = User.objects.create_user( + email="user@example.com", password="password", user_type="property_owner" + ) + self.other_user = User.objects.create_user( + email="other@example.com", password="password", user_type="property_owner" + ) + self.support_agent = User.objects.create_user( + email="agent@example.com", password="password", user_type="support_agent" + ) + + self.case = SupportCase.objects.create( + user=self.user, title="My Case", description="Help" + ) + self.other_case = SupportCase.objects.create( + user=self.other_user, title="Other Case", description="Help other" + ) + + def test_create_message_success(self): + """Test that a user can create a message for their own case.""" + self.client.force_authenticate(user=self.user) + data = { + "user": self.user.id, + "text": "My reply", + "support_case": self.case.id, + } + response = self.client.post("/api/support/messages/", data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(SupportMessage.objects.count(), 1) + self.assertEqual(SupportMessage.objects.first().support_case, self.case) + + def test_create_message_fail_other_case(self): + """Test that a user cannot create a message for another user's case.""" + self.client.force_authenticate(user=self.user) + data = { + "user": self.user.id, + "text": "Intruder reply", + "support_case": self.other_case.id, + } + response = self.client.post("/api/support/messages/", data) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("You cannot add a message to a support case that does not belong to you.", str(response.data)) + + def test_support_agent_can_reply_to_any_case(self): + """Test that a support agent can create a message for any case.""" + self.client.force_authenticate(user=self.support_agent) + data = { + "user": self.support_agent.id, + "text": "Agent reply", + "support_case": self.case.id, + } + response = self.client.post("/api/support/messages/", data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(SupportMessage.objects.count(), 1) diff --git a/dta_service/core/tests/verify_serializer_update.py b/dta_service/core/tests/verify_serializer_update.py new file mode 100644 index 0000000..7cb6123 --- /dev/null +++ b/dta_service/core/tests/verify_serializer_update.py @@ -0,0 +1,39 @@ +from django.test import TestCase +from django.contrib.auth import get_user_model +from core.models import SupportCase +from core.serializers import SupportCaseListSerializer + +User = get_user_model() + +class SupportCaseSerializerTests(TestCase): + def setUp(self): + self.user = User.objects.create_user( + email="testuser@example.com", + password="password", + user_type="property_owner", + first_name="Test", + last_name="User", + ) + self.case = SupportCase.objects.create( + user=self.user, title="Test Case", description="Test Description" + ) + + def test_serializer_fields(self): + serializer = SupportCaseListSerializer(self.case) + data = serializer.data + + # Verify user_email is present and correct + self.assertIn("user_email", data) + self.assertEqual(data["user_email"], "testuser@example.com") + + # Verify created_at is present + self.assertIn("created_at", data) + + # Verify user ID is NOT present + self.assertNotIn("user", data) + + # Verify user name fields are present + self.assertIn("user_first_name", data) + self.assertEqual(data["user_first_name"], "Test") + self.assertIn("user_last_name", data) + self.assertEqual(data["user_last_name"], "User") diff --git a/dta_service/core/views/__init__.py b/dta_service/core/views/__init__.py index 3a23bda..2833791 100644 --- a/dta_service/core/views/__init__.py +++ b/dta_service/core/views/__init__.py @@ -7,6 +7,7 @@ from .user import ( PasswordResetRequestView, PasswordResetConfirmView, CheckPasscodeView, + ResendRegistrationEmailView ) from .property_owner import PropertyOwnerViewSet from .vendor import VendorViewSet diff --git a/dta_service/core/views/user.py b/dta_service/core/views/user.py index 5aab6bd..677588d 100644 --- a/dta_service/core/views/user.py +++ b/dta_service/core/views/user.py @@ -46,7 +46,8 @@ class UserRegisterView(generics.CreateAPIView): # Send registration email with OTC try: - EmailService.send_registration_email(user) + email_service = EmailService() + email_service.send_registration_email(user) except Exception as e: print(e) @@ -101,6 +102,7 @@ class LogoutView(APIView): class PasswordResetRequestView(APIView): permission_classes = [permissions.AllowAny] + authentication_classes = () def post(self, request): serializer = PasswordResetRequestSerializer(data=request.data) @@ -115,6 +117,7 @@ class PasswordResetRequestView(APIView): class PasswordResetConfirmView(APIView): permission_classes = [permissions.AllowAny] + authentication_classes = () def post(self, request): serializer = PasswordResetConfirmSerializer(data=request.data) @@ -129,6 +132,7 @@ class PasswordResetConfirmView(APIView): class CheckPasscodeView(APIView): permission_classes = [permissions.AllowAny] + authentication_classes = () def post(self, request): email = request.data.get("email") @@ -175,3 +179,28 @@ class CheckPasscodeView(APIView): return Response( {"valid": False, "detail": "Invalid code."}, status=status.HTTP_200_OK ) + + +class ResendRegistrationEmailView(APIView): + permission_classes = [permissions.AllowAny] + authentication_classes = () + + def post(self, request): + email = request.data.get("email") + + if not email: + return Response( + {"detail": "Email is required."}, status=status.HTTP_400_BAD_REQUEST + ) + + try: + user = User.objects.get(email=email) + email_service = EmailService() + email_service.send_registration_email(user) + return Response( + {"detail": "Registration email sent."}, status=status.HTTP_200_OK + ) + except User.DoesNotExist: + return Response( + {"detail": "User not found."}, status=status.HTTP_404_NOT_FOUND + ) diff --git a/dta_service/dta_service/urls.py b/dta_service/dta_service/urls.py index 1e58df9..1baf809 100644 --- a/dta_service/dta_service/urls.py +++ b/dta_service/dta_service/urls.py @@ -36,6 +36,7 @@ from core.views import ( PropertyCompsProxyView, MLSDetailProxyView, CheckPasscodeView, + ResendRegistrationEmailView, ) from django.conf import settings from django.conf.urls.static import static @@ -80,6 +81,11 @@ urlpatterns = [ CheckPasscodeView.as_view(), name="check_passcode", ), + path( + "api/resend-registration-email/", + ResendRegistrationEmailView.as_view(), + name="resend_registration_email", + ), # API endpoints path("api/attorney/", include("core.urls.attorney")), path("api/document/", include("core.urls.document")),