ensure password reset and account verification works as well as support
This commit is contained in:
@@ -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")
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
146
dta_service/core/templates/emails/base_email.html
Normal file
146
dta_service/core/templates/emails/base_email.html
Normal file
@@ -0,0 +1,146 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{% block title %}Email{% endblock %}</title>
|
||||
<!--
|
||||
NOTE: For best compatibility, modern email clients prefer inline styles.
|
||||
This template uses a combination of inline styles and classes.
|
||||
For production, you may want to use a tool to pre-process the classes into inline styles.
|
||||
The styles are designed to mimic Material-UI's design language:
|
||||
- Clean, modern typography (sans-serif)
|
||||
- Elevated card-like container with rounded corners and a subtle shadow
|
||||
- Primary color for call-to-action buttons
|
||||
-->
|
||||
<style type="text/css">
|
||||
body,
|
||||
html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
"Helvetica Neue", Arial, sans-serif, "Apple Color Emoji",
|
||||
"Segoe UI Emoji", "Segoe UI Symbol";
|
||||
background-color: #2d4a4aff;
|
||||
color: #050f24;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
color: #050f24;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
line-height: 1.6;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.content p {
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
padding: 12px 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #ffffff !important;
|
||||
background-color: #2d4a4aff;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #6f757e;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #27d095;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body style="margin: 0; padding: 0; background-color: #2d4a4aff">
|
||||
<div class="container" style="width: 100%; max-width: 600px; margin: 0 auto; padding: 20px">
|
||||
<div class="card" style="
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
padding: 24px;
|
||||
">
|
||||
<div class="header" style="
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
text-align: center;
|
||||
">
|
||||
<h1 style="
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
color: #050f24;
|
||||
">
|
||||
{% block header_title %}Django App{% endblock %}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="content" style="
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
line-height: 1.6;
|
||||
font-size: 16px;
|
||||
">
|
||||
{% block content %}
|
||||
<!-- Content will be inserted here by child templates -->
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
<div class="footer" style="
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #6f757e;
|
||||
line-height: 1.5;
|
||||
">
|
||||
<p>This email was sent by Ditch the Agent.</p>
|
||||
<p>Please do not reply to this email.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,160 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{% block title %}Email{% endblock %}</title>
|
||||
<!--
|
||||
NOTE: For best compatibility, modern email clients prefer inline styles.
|
||||
This template uses a combination of inline styles and classes.
|
||||
For production, you may want to use a tool to pre-process the classes into inline styles.
|
||||
The styles are designed to mimic Material-UI's design language:
|
||||
- Clean, modern typography (sans-serif)
|
||||
- Elevated card-like container with rounded corners and a subtle shadow
|
||||
- Primary color for call-to-action buttons
|
||||
-->
|
||||
<style type="text/css">
|
||||
body,
|
||||
html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
"Helvetica Neue", Arial, sans-serif, "Apple Color Emoji",
|
||||
"Segoe UI Emoji", "Segoe UI Symbol";
|
||||
background-color: #2d4a4aff;
|
||||
color: #050f24;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
color: #050f24;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
line-height: 1.6;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.content p {
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
padding: 12px 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #ffffff !important;
|
||||
background-color: #2d4a4aff;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #6f757e;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #27d095;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; background-color: #2d4a4aff">
|
||||
<div
|
||||
class="container"
|
||||
style="width: 100%; max-width: 600px; margin: 0 auto; padding: 20px"
|
||||
>
|
||||
<div
|
||||
class="card"
|
||||
style="
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
padding: 24px;
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="header"
|
||||
style="
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
<h1
|
||||
style="
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
color: #050f24;
|
||||
"
|
||||
>
|
||||
{% block header_title %}Django App{% endblock %}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="content"
|
||||
style="
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
line-height: 1.6;
|
||||
font-size: 16px;
|
||||
"
|
||||
>
|
||||
{% block content %}
|
||||
<!-- Content will be inserted here by child templates -->
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="footer"
|
||||
style="
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #6f757e;
|
||||
line-height: 1.5;
|
||||
"
|
||||
>
|
||||
<p>This email was sent by Ditch the Agent.</p>
|
||||
<p>Please do not reply to this email.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,16 +1,16 @@
|
||||
{% extends 'emails/base_email.html' %} {% block header_title %}Password Reset
|
||||
Request{% endblock %} {% block content %}
|
||||
<p>Hello {{ user.first_name|default:user.username }},</p>
|
||||
<p>Hello {{ display_name }},</p>
|
||||
<p>
|
||||
We received a request to reset the password for your account. If you did not
|
||||
make this request, you can safely ignore this email.
|
||||
</p>
|
||||
<p>To reset your password, please click the link below:</p>
|
||||
<p>To reset your password, please click the link below or enter the code:</p>
|
||||
<p style="text-align: center; font-size: 24px; font-weight: bold; letter-spacing: 5px;">
|
||||
{{ code }}
|
||||
</p>
|
||||
<div style="text-align: center; margin: 24px 0">
|
||||
<a
|
||||
href="{{ reset_link }}"
|
||||
class="button"
|
||||
style="
|
||||
<a href="{{ reset_link }}" class="button" style="
|
||||
display: inline-block;
|
||||
padding: 12px 24px;
|
||||
font-size: 16px;
|
||||
@@ -20,8 +20,7 @@ Request{% endblock %} {% block content %}
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
">
|
||||
Reset Password
|
||||
</a>
|
||||
</div>
|
||||
@@ -30,9 +29,7 @@ Request{% endblock %} {% block content %}
|
||||
into your web browser:
|
||||
</p>
|
||||
<p>
|
||||
<a href="{{ reset_link }}" style="word-break: break-all"
|
||||
>{{ reset_link }}</a
|
||||
>
|
||||
<a href="{{ reset_link }}" style="word-break: break-all">{{ reset_link }}</a>
|
||||
</p>
|
||||
<p>This link will expire in a few hours for security reasons.</p>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -3,14 +3,13 @@ The Agent!{% endblock %} {% block content %}
|
||||
<p>Hello {{ display_name }},</p>
|
||||
<p>Thank you for registering with us. We're excited to have you on board!</p>
|
||||
<p>
|
||||
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:
|
||||
</p>
|
||||
<p style="text-align: center; font-size: 24px; font-weight: bold; letter-spacing: 5px;">
|
||||
{{ code }}
|
||||
</p>
|
||||
<div style="text-align: center; margin: 24px 0">
|
||||
<a
|
||||
href="{{ activation_link }}"
|
||||
class="button"
|
||||
style="
|
||||
<a href="{{ verify_link }}" class="button" style="
|
||||
display: inline-block;
|
||||
padding: 12px 24px;
|
||||
font-size: 16px;
|
||||
@@ -20,9 +19,8 @@ The Agent!{% endblock %} {% block content %}
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
Confirm Account
|
||||
">
|
||||
Verify Account
|
||||
</a>
|
||||
</div>
|
||||
<p>
|
||||
@@ -30,8 +28,6 @@ The Agent!{% endblock %} {% block content %}
|
||||
into your web browser:
|
||||
</p>
|
||||
<p>
|
||||
<a href="{{ activation_link }}" style="word-break: break-all"
|
||||
>{{ activation_link }}</a
|
||||
>
|
||||
<a href="{{ verify_link }}" style="word-break: break-all">{{ verify_link }}</a>
|
||||
</p>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
27
dta_service/core/tests/reproduce_issue.py
Normal file
27
dta_service/core/tests/reproduce_issue.py
Normal file
@@ -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")
|
||||
65
dta_service/core/tests/test_support_message_fix.py
Normal file
65
dta_service/core/tests/test_support_message_fix.py
Normal file
@@ -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)
|
||||
39
dta_service/core/tests/verify_serializer_update.py
Normal file
39
dta_service/core/tests/verify_serializer_update.py
Normal file
@@ -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")
|
||||
@@ -7,6 +7,7 @@ from .user import (
|
||||
PasswordResetRequestView,
|
||||
PasswordResetConfirmView,
|
||||
CheckPasscodeView,
|
||||
ResendRegistrationEmailView
|
||||
)
|
||||
from .property_owner import PropertyOwnerViewSet
|
||||
from .vendor import VendorViewSet
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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")),
|
||||
|
||||
Reference in New Issue
Block a user