Compare commits

..

2 Commits

Author SHA1 Message Date
9d9858ef69 Merge pull request 'feat: add HIPAA-compliant email and OTP authentication system' (#1) from main into feature/authentication_and_authorization
Reviewed-on: https://gitea.blackbusinesslabs.com/ATTUNE-HEART-THERAPY/alternative-backend-service/pulls/1
2025-11-22 02:23:15 +00:00
f06b5120e9 feat: add HIPAA-compliant email and OTP authentication system
Add comprehensive HIPAA compliance features and OTP-based authentication:

- Configure HIPAA email settings with AES-256 encryption standard
- Add secure portal URL and BAA verification configuration
- Implement OTP verification for user registration and password reset
- Add user model fields for email verification and password reset OTPs
- Configure templates directory in Django settings
- Add authentication flow endpoints with detailed documentation
- Update dependencies to support new security features
- Reorganize .gitignore for better structure

These changes ensure HIPAA compliance for healthcare data handling
with 6-year audit retention, secure email communications, and
multi-factor authentication capabilities.
2025-11-22 02:19:44 +00:00
17 changed files with 1602 additions and 28 deletions

4
.gitignore vendored
View File

@ -18,8 +18,6 @@ media
*.py[cod]
*$py.class
meetings
# C extensions
*.so
@ -121,6 +119,8 @@ ipython_config.py
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
meetings
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/

View File

@ -46,7 +46,9 @@ ROOT_URLCONF = 'booking_system.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'DIRS': [
os.path.join(BASE_DIR, 'templates'),
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
@ -95,12 +97,35 @@ SIMPLE_JWT = {
}
# HIPAA Email Configuration
EMAIL_ENCRYPTION_KEY = os.getenv('EMAIL_ENCRYPTION_KEY')
# Stripe Configuration
STRIPE_PUBLISHABLE_KEY = os.getenv('STRIPE_PUBLISHABLE_KEY')
STRIPE_SECRET_KEY = os.getenv('STRIPE_SECRET_KEY')
STRIPE_WEBHOOK_SECRET = os.getenv('STRIPE_WEBHOOK_SECRET')
# HIPAA Compliance Settings
HIPAA_EMAIL_CONFIG = {
'ENCRYPTION_STANDARD': 'AES-256',
'REQUIRE_SECURE_PORTAL': True,
'AUDIT_RETENTION_DAYS': 365 * 6,
'AUTO_DELETE_UNREAD_DAYS': 30,
'REQUIRE_BAA': True,
}
# Secure Portal URL
SECURE_PORTAL_URL = os.getenv('SECURE_PORTAL_URL', 'https://secure.yourdomain.com')
# Business Associate Agreement Verification
BAA_VERIFICATION = {
'EMAIL_PROVIDER': os.getenv('EMAIL_PROVIDER'),
'BAA_SIGNED': os.getenv('BAA_SIGNED', 'False').lower() == 'true',
'BAA_EXPIRY': os.getenv('BAA_EXPIRY'),
}
# Jitsi Configuration
JITSI_BASE_URL = os.getenv('JITSI_BASE_URL', 'https://meet.jit.si')

Binary file not shown.

View File

@ -0,0 +1,100 @@
{% extends "emails/base.html" %}
{% block title %}New Therapy Booking Request - Action Required{% endblock %}
{% block content %}
<div class="email-header" style="background: linear-gradient(135deg, #dc2626, #ea580c);">
<h1>📋 NEW THERAPY BOOKING REQUEST</h1>
</div>
<div class="email-body">
<div class="urgent-badge">⏰ ACTION REQUIRED - Please respond within 24 hours</div>
<div class="section">
<h2 class="section-title">Patient Information</h2>
<div class="info-card">
<div class="info-item">
<span class="info-label">Full Name:</span>
<span class="info-value">{{ booking.full_name }}</span>
</div>
<div class="info-item">
<span class="info-label">Email:</span>
<span class="info-value">{{ booking.email }}</span>
</div>
<div class="info-item">
<span class="info-label">Phone:</span>
<span class="info-value">{{ booking.phone }}</span>
</div>
<div class="info-item">
<span class="info-label">Submitted:</span>
<span class="info-value">{{ booking.created_at|date:"F d, Y" }} at {{ booking.created_at|time:"g:i A" }}</span>
</div>
</div>
</div>
<div class="section">
<h2 class="section-title">Appointment Details</h2>
<div class="info-card">
<div class="info-item">
<span class="info-label">Appointment Type:</span>
<span class="info-value">{{ booking.get_appointment_type_display }}</span>
</div>
<div class="info-item">
<span class="info-label">Preferred Date:</span>
<span class="info-value">{{ booking.preferred_date|date:"l, F d, Y" }}</span>
</div>
<div class="info-item">
<span class="info-label">Preferred Time:</span>
<span class="info-value">{{ booking.preferred_time }}</span>
</div>
<div class="info-item">
<span class="info-label">Session Fee:</span>
<span class="info-value">${{ booking.amount }}</span>
</div>
</div>
</div>
{% if booking.additional_message %}
<div class="section">
<h2 class="section-title">Patient's Message</h2>
<div class="info-card">
<div style="background: white; padding: 16px; border-radius: 6px; border-left: 4px solid #10b981;">
{{ booking.additional_message }}
</div>
</div>
</div>
{% endif %}
<div class="section">
<h2 class="section-title">Required Actions</h2>
<div class="steps">
<div class="step">
<strong>Review Patient Information</strong><br>
Assess clinical appropriateness and availability.
</div>
<div class="step">
<strong>Contact Patient</strong><br>
Reach out within 24 hours to confirm appointment details.
</div>
<div class="step">
<strong>Confirm Booking</strong><br>
Update booking status and send confirmation email.
</div>
</div>
</div>
<div style="text-align: center; margin: 30px 0;">
<a href="https://attunehearttherapy.com/admin" class="button" style="background: linear-gradient(135deg, #dc2626, #ea580c);">
📊 Manage This Booking
</a>
</div>
</div>
<div class="footer">
<p><strong>Attune Heart Therapy - Admin Portal</strong></p>
<div class="contact-info">
Booking ID: {{ booking.id }}<br>
Received: {{ booking.created_at|date:"Y-m-d H:i" }}
</div>
</div>
{% endblock %}

196
templates/emails/base.html Normal file
View File

@ -0,0 +1,196 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Attune Heart Therapy{% endblock %}</title>
<style>
/* Reset styles for email compatibility */
body, table, td, div, p, a {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333333;
background-color: #f8f9fa;
margin: 0;
padding: 20px 0;
}
.email-container {
max-width: 600px;
margin: 0 auto;
background: #ffffff;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.email-header {
background: linear-gradient(135deg, #ec4899, #8b5cf6);
color: white;
padding: 30px 40px;
text-align: center;
}
.email-header h1 {
font-size: 28px;
font-weight: 600;
margin: 0;
}
.email-body {
padding: 40px;
}
.greeting {
font-size: 18px;
color: #374151;
margin-bottom: 24px;
}
.section {
margin-bottom: 30px;
}
.section-title {
font-size: 20px;
font-weight: 600;
color: #1f2937;
margin-bottom: 16px;
border-bottom: 2px solid #f3f4f6;
padding-bottom: 8px;
}
.info-card {
background: #f8fafc;
border-left: 4px solid #8b5cf6;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
}
.info-item {
margin-bottom: 12px;
display: flex;
}
.info-label {
font-weight: 600;
color: #4b5563;
min-width: 140px;
}
.info-value {
color: #1f2937;
flex: 1;
}
.button {
display: inline-block;
background: linear-gradient(135deg, #ec4899, #8b5cf6);
color: white;
text-decoration: none;
padding: 14px 32px;
border-radius: 8px;
font-weight: 600;
text-align: center;
margin: 20px 0;
}
.steps {
counter-reset: step-counter;
margin: 20px 0;
}
.step {
margin-bottom: 16px;
padding-left: 40px;
position: relative;
}
.step:before {
counter-increment: step-counter;
content: counter(step-counter);
background: #8b5cf6;
color: white;
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
position: absolute;
left: 0;
top: 0;
}
.urgent-badge {
background: #fee2e2;
color: #dc2626;
padding: 8px 16px;
border-radius: 20px;
font-size: 14px;
font-weight: 600;
display: inline-block;
margin-bottom: 16px;
}
.footer {
background: #f8fafc;
padding: 30px 40px;
text-align: center;
border-top: 1px solid #e5e7eb;
}
.footer p {
color: #6b7280;
margin-bottom: 8px;
}
.contact-info {
color: #4b5563;
font-size: 14px;
margin-top: 16px;
}
/* Responsive design */
@media only screen and (max-width: 600px) {
.email-body {
padding: 30px 20px;
}
.email-header {
padding: 20px;
}
.email-header h1 {
font-size: 24px;
}
.info-item {
flex-direction: column;
}
.info-label {
min-width: auto;
margin-bottom: 4px;
}
}
</style>
</head>
<body>
<div class="email-container">
{% block content %}{% endblock %}
</div>
</body>
</html>

View File

@ -0,0 +1,145 @@
{% extends "emails/base.html" %}
{% block title %}Appointment Confirmed - Attune Heart Therapy{% endblock %}
{% block content %}
<div class="email-header">
<h1>✅ Your Appointment is Confirmed!</h1>
</div>
<div class="email-body">
<div class="greeting">
Dear <strong>{{ booking.full_name }}</strong>,
</div>
<div class="section">
<p>We're delighted to confirm your <strong>{{ booking.get_appointment_type_display }}</strong> appointment. Your healing journey begins now, and we're honored to walk this path with you.</p>
</div>
<div class="section">
<h2 class="section-title">Appointment Details</h2>
<div class="info-card">
<div class="info-item">
<span class="info-label">Appointment Type:</span>
<span class="info-value">{{ booking.get_appointment_type_display }}</span>
</div>
<div class="info-item">
<span class="info-label">Date & Time:</span>
<span class="info-value">{{ booking.confirmed_datetime|date:"l, F d, Y" }} at {{ booking.confirmed_datetime|time:"g:i A" }}</span>
</div>
<div class="info-item">
<span class="info-label">Duration:</span>
<span class="info-value">
{% if booking.appointment_type == 'initial-consultation' %}90 minutes
{% elif booking.appointment_type == 'individual-therapy' %}60 minutes
{% elif booking.appointment_type == 'family-therapy' %}90 minutes
{% elif booking.appointment_type == 'couples-therapy' %}75 minutes
{% elif booking.appointment_type == 'group-therapy' %}90 minutes
{% elif booking.appointment_type == 'follow-up' %}45 minutes
{% else %}60 minutes{% endif %}
</span>
</div>
<div class="info-item">
<span class="info-label">Therapist:</span>
<span class="info-value">{{ booking.assigned_therapist.get_full_name }}</span>
</div>
{% if booking.payment_status == 'paid' %}
<div class="info-item">
<span class="info-label">Payment Status:</span>
<span class="info-value" style="color: #10b981; font-weight: 600;">✅ Paid</span>
</div>
{% endif %}
</div>
</div>
<div class="section">
<h2 class="section-title">Join Your Session</h2>
<div class="info-card" style="background: linear-gradient(135deg, #f0f9ff, #e0f2fe); border-left-color: #0ea5e9;">
<div style="text-align: center; padding: 10px 0;">
<div style="font-size: 16px; font-weight: 600; color: #0369a1; margin-bottom: 15px;">
📅 Secure Video Session
</div>
<a href="{{ booking.jitsi_meet_url }}" class="button" style="font-size: 16px; padding: 16px 40px;">
🎥 Join Video Session
</a>
<div style="margin-top: 15px; font-size: 14px; color: #64748b;">
Or copy this link:<br>
<span style="word-break: break-all; color: #0ea5e9;">{{ booking.jitsi_meet_url }}</span>
</div>
</div>
</div>
</div>
<div class="section">
<h2 class="section-title">Preparing for Your Session</h2>
<div class="steps">
<div class="step">
<strong>Test Your Technology</strong><br>
Please test your camera, microphone, and internet connection before the session.
</div>
<div class="step">
<strong>Find a Quiet Space</strong><br>
Choose a private, comfortable location where you won't be interrupted.
</div>
<div class="step">
<strong>Join Early</strong><br>
Please join 5-10 minutes before your scheduled time to ensure everything is working.
</div>
<div class="step">
<strong>Browser Recommendation</strong><br>
Use Chrome, Firefox, or Safari for the best video experience.
</div>
</div>
</div>
{% if booking.payment_status != 'paid' %}
<div class="section">
<h2 class="section-title">Payment Information</h2>
<div class="info-card" style="background: #fffbeb; border-left-color: #f59e0b;">
<div style="text-align: center;">
<div style="color: #d97706; font-weight: 600; margin-bottom: 10px;">
💳 Payment Required
</div>
<p>Your session fee of <strong>${{ booking.amount }}</strong> is pending. Please complete your payment before the session.</p>
<a href="https://attunehearttherapy.com/payment/{{ booking.id }}" class="button" style="background: linear-gradient(135deg, #f59e0b, #d97706);">
Complete Payment
</a>
</div>
</div>
</div>
{% endif %}
<div class="section">
<h2 class="section-title">Need to Reschedule?</h2>
<p>If you need to reschedule or cancel your appointment, please contact us at least 24 hours in advance:</p>
<div style="text-align: center; margin: 20px 0;">
<a href="tel:+19548073027" class="button" style="background: linear-gradient(135deg, #64748b, #475569);">
📞 Call (954) 807-3027
</a>
</div>
</div>
<div class="section">
<div style="background: #f0fdf4; padding: 20px; border-radius: 8px; border-left: 4px solid #10b981;">
<p style="color: #065f46; font-style: italic; margin: 0;">
"The privilege of a lifetime is to become who you truly are."<br>
<span style="font-size: 14px;">- Carl Jung</span>
</p>
</div>
</div>
</div>
<div class="footer">
<p><strong>Attune Heart Therapy</strong></p>
<p>Compassionate Care for Your Healing Journey</p>
<div class="contact-info">
📞 (954) 807-3027<br>
✉️ hello@attunehearttherapy.com<br>
🌐 attunehearttherapy.com
</div>
<p style="font-size: 12px; color: #9ca3af; margin-top: 15px;">
Confirmation ID: {{ booking.id }}<br>
Sent: {{ booking.confirmed_datetime|date:"Y-m-d H:i" }}
</p>
</div>
{% endblock %}

View File

@ -0,0 +1,179 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Email Verification</title>
<style>
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap");
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f8fafc;
}
.email-container {
max-width: 600px;
margin: 0 auto;
background: #ffffff;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.email-header {
background-image: linear-gradient(to right, #e11d48, #db2777, #f97316);
padding: 40px 30px;
text-align: center;
color: white;
}
.email-header h1 {
font-size: 28px;
font-weight: 700;
margin-bottom: 8px;
}
.email-header p {
font-size: 16px;
opacity: 0.9;
font-weight: 400;
}
.email-body {
padding: 40px 30px;
}
.greeting {
font-size: 18px;
font-weight: 600;
color: #1f2937;
margin-bottom: 20px;
}
.otp-container {
background: #f8fafc;
border: 2px dashed #e2e8f0;
border-radius: 8px;
padding: 30px;
text-align: center;
margin: 30px 0;
}
.otp-code {
font-size: 42px;
font-weight: 700;
color: #1f2937;
letter-spacing: 8px;
font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.email-footer {
background-image: linear-gradient(to right, #e11d48, #db2777, #f97316);
padding: 30px;
text-align: center;
border-top: 1px solid #e2e8f0;
}
.company-name {
font-size: 18px;
font-weight: 700;
color: #fff;
margin-bottom: 8px;
}
.support-info {
color: #fff;
font-size: 14px;
margin: 15px 0;
}
.copyright {
color: #fff;
font-size: 12px;
margin-top: 20px;
}
@media (max-width: 600px) {
.email-container {
margin: 10px;
border-radius: 8px;
}
.email-header,
.email-body,
.email-footer {
padding: 25px 20px;
}
.otp-code {
font-size: 32px;
letter-spacing: 6px;
}
}
</style>
</head>
<body>
<div class="email-container">
<!-- Header -->
<div class="email-header">
<h1>Verify Your Email Address</h1>
<p>Secure your account with one-time password</p>
</div>
<!-- Body -->
<div class="email-body">
<p class="greeting">Hello {{ user_name }},</p>
<p>
Thank you for registering with us! To complete your registration and
secure your account, please use the following verification code:
</p>
<!-- OTP Display -->
<div class="otp-container">
<div class="otp-code">{{ otp }}</div>
</div>
<div>
<strong>Important:</strong> This code will expire in
<strong>{{ expiry_minutes }} minutes</strong> for security reasons.
</div>
<p>
If you didn't request this code, please ignore this email or contact
our support team immediately.
</p>
</div>
<!-- Footer -->
<div class="email-footer">
<div class="company-name">{{ company_name }}</div>
<p class="support-info">
Need help? Contact our support team at
<a
href="mailto:{{ support_email }}"
style="color: #fff; text-decoration: none"
>{{ support_email }}</a
>
</p>
<p class="copyright">
© {{ current_year }} {{ company_name }}. All rights reserved.
</p>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,170 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Password Reset</title>
<style>
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap");
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f8fafc;
}
.email-container {
max-width: 600px;
margin: 0 auto;
background: #ffffff;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.email-header {
background-image: linear-gradient(to right, #e11d48, #db2777, #f97316);
padding: 40px 30px;
text-align: center;
color: white;
}
.email-header h1 {
font-size: 28px;
font-weight: 700;
margin-bottom: 8px;
}
.email-header p {
font-size: 16px;
opacity: 0.9;
font-weight: 400;
}
.email-body {
padding: 40px 30px;
}
.greeting {
font-size: 18px;
font-weight: 600;
color: #1f2937;
margin-bottom: 20px;
}
.otp-container {
background: #fff5f5;
border: 2px dashed #fed7d7;
border-radius: 8px;
padding: 30px;
text-align: center;
margin: 30px 0;
}
.otp-code {
font-size: 42px;
font-weight: 700;
color: #c53030;
letter-spacing: 8px;
font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
}
.email-footer {
background-image: linear-gradient(to right, #e11d48, #db2777, #f97316);
padding: 30px;
text-align: center;
border-top: 1px solid #e2e8f0;
}
.company-name {
font-size: 18px;
font-weight: 700;
color: #fff;
margin-bottom: 8px;
}
.support-info {
color: #fff;
font-size: 14px;
margin: 15px 0;
}
.copyright {
color: #fff;
font-size: 12px;
margin-top: 20px;
}
@media (max-width: 600px) {
.email-container {
margin: 10px;
border-radius: 8px;
}
.email-header,
.email-body,
.email-footer {
padding: 25px 20px;
}
.otp-code {
font-size: 32px;
letter-spacing: 6px;
}
}
</style>
</head>
<body>
<div class="email-container">
<div class="email-header">
<h1>Password Reset Request</h1>
<p>Secure your account with verification code</p>
</div>
<div class="email-body">
<p class="greeting">Hello {{ user_name }},</p>
<p>
We received a request to reset your password for your account. Use the
verification code below to proceed with resetting your password:
</p>
<div class="otp-container">
<div class="otp-code">{{ otp }}</div>
</div>
<div class="expiry-notice">
<strong> Important:</strong> This code will expire in
<strong>{{ expiry_minutes }} minutes</strong> for security reasons.
</div>
<div>
<strong>Note:</strong> This password reset request was initiated from
our system. If this wasn't you, your account might be at risk.
</div>
</div>
<div class="email-footer">
<div class="company-name">{{ company_name }}</div>
<p class="support-info">
Need help? Contact our support team at
<a
href="mailto:{{ support_email }}"
style="color: #fff; text-decoration: none"
>{{ support_email }}</a
>
</p>
<p class="copyright">
© {{ current_year }} {{ company_name }}. All rights reserved.
</p>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,86 @@
{% extends "emails/base.html" %}
{% block title %}Booking Request Received - Attune Heart Therapy{% endblock %}
{% block content %}
<div class="email-header">
<h1>🎉 Thank You for Your Booking Request!</h1>
</div>
<div class="email-body">
<div class="greeting">
Dear <strong>{{ booking.full_name }}</strong>,
</div>
<div class="section">
<p>We have received your request for a <strong>{{ booking.get_appointment_type_display }}</strong> appointment and we're excited to support you on your healing journey.</p>
</div>
<div class="section">
<h2 class="section-title">Your Request Details</h2>
<div class="info-card">
<div class="info-item">
<span class="info-label">Appointment Type:</span>
<span class="info-value">{{ booking.get_appointment_type_display }}</span>
</div>
<div class="info-item">
<span class="info-label">Preferred Date:</span>
<span class="info-value">{{ booking.preferred_date|date:"l, F d, Y" }}</span>
</div>
<div class="info-item">
<span class="info-label">Preferred Time:</span>
<span class="info-value">{{ booking.preferred_time }}</span>
</div>
{% if booking.additional_message %}
<div class="info-item">
<span class="info-label">Your Message:</span>
<span class="info-value">{{ booking.additional_message }}</span>
</div>
{% endif %}
</div>
</div>
<div class="section">
<h2 class="section-title">What Happens Next?</h2>
<div class="steps">
<div class="step">
<strong>Review Process</strong><br>
Our clinical team will review your request to ensure we're the right fit for your needs.
</div>
<div class="step">
<strong>Confirmation</strong><br>
We'll contact you within 24 hours to confirm your appointment details.
</div>
<div class="step">
<strong>Session Preparation</strong><br>
You'll receive a confirmed date, time, and secure video meeting link.
</div>
</div>
</div>
<div class="section">
<h2 class="section-title">Need Immediate Assistance?</h2>
<p>If you have any questions or need to modify your request, please don't hesitate to contact us:</p>
<div style="text-align: center; margin: 25px 0;">
<a href="tel:+19548073027" class="button">📞 Call Us: (954) 807-3027</a>
</div>
</div>
<div class="section">
<p style="color: #6b7280; font-style: italic;">
"The journey of a thousand miles begins with a single step."<br>
- Lao Tzu
</p>
</div>
</div>
<div class="footer">
<p><strong>Attune Heart Therapy</strong></p>
<p>Healing Hearts, Transforming Lives</p>
<div class="contact-info">
📞 (954) 807-3027<br>
✉️ hello@attunehearttherapy.com<br>
🌐 attunehearttherapy.com
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,112 @@
{% extends "emails/base.html" %}
{% block title %}Payment Confirmed - Attune Heart Therapy{% endblock %}
{% block content %}
<div class="email-header" style="background: linear-gradient(135deg, #10b981, #059669);">
<h1>💳 Payment Confirmed!</h1>
</div>
<div class="email-body">
<div class="greeting">
Dear <strong>{{ booking.full_name }}</strong>,
</div>
<div class="section">
<p>Thank you for your payment! Your <strong>{{ booking.get_appointment_type_display }}</strong> appointment is now fully confirmed and we're looking forward to our session.</p>
</div>
<div class="section">
<h2 class="section-title">Payment Details</h2>
<div class="info-card" style="background: linear-gradient(135deg, #f0fdf4, #dcfce7); border-left-color: #10b981;">
<div class="info-item">
<span class="info-label">Amount Paid:</span>
<span class="info-value" style="color: #059669; font-size: 18px; font-weight: 700;">${{ booking.amount }}</span>
</div>
<div class="info-item">
<span class="info-label">Payment Date:</span>
<span class="info-value">{{ booking.paid_at|date:"F d, Y" }} at {{ booking.paid_at|time:"g:i A" }}</span>
</div>
<div class="info-item">
<span class="info-label">Appointment:</span>
<span class="info-value">{{ booking.get_appointment_type_display }}</span>
</div>
<div class="info-item">
<span class="info-label">Session Date:</span>
<span class="info-value">{{ booking.confirmed_datetime|date:"l, F d, Y" }} at {{ booking.confirmed_datetime|time:"g:i A" }}</span>
</div>
</div>
</div>
<div class="section">
<h2 class="section-title">Your Session Details</h2>
<div class="info-card">
<div class="info-item">
<span class="info-label">Video Meeting:</span>
<span class="info-value">
<a href="{{ booking.jitsi_meet_url }}" style="color: #0ea5e9; text-decoration: none;">
{{ booking.jitsi_meet_url }}
</a>
</span>
</div>
<div class="info-item">
<span class="info-label">Therapist:</span>
<span class="info-value">{{ booking.assigned_therapist.get_full_name }}</span>
</div>
<div class="info-item">
<span class="info-label">Duration:</span>
<span class="info-value">
{% if booking.appointment_type == 'initial-consultation' %}90 minutes
{% elif booking.appointment_type == 'individual-therapy' %}60 minutes
{% elif booking.appointment_type == 'family-therapy' %}90 minutes
{% elif booking.appointment_type == 'couples-therapy' %}75 minutes
{% elif booking.appointment_type == 'group-therapy' %}90 minutes
{% elif booking.appointment_type == 'follow-up' %}45 minutes
{% else %}60 minutes{% endif %}
</span>
</div>
</div>
</div>
<div style="text-align: center; margin: 30px 0;">
<a href="{{ booking.jitsi_meet_url }}" class="button" style="background: linear-gradient(135deg, #10b981, #059669);">
🎥 Join Video Session
</a>
</div>
<div class="section">
<div style="background: #f0f9ff; padding: 20px; border-radius: 8px; text-align: center;">
<h3 style="color: #0369a1; margin-bottom: 10px;">📋 Receipt</h3>
<p style="margin: 5px 0;">Payment ID: {{ booking.stripe_payment_intent_id|default:booking.id }}</p>
<p style="margin: 5px 0;">Date: {{ booking.paid_at|date:"Y-m-d" }}</p>
<p style="margin: 5px 0;">Amount: ${{ booking.amount }}</p>
<p style="margin: 5px 0; font-size: 12px; color: #64748b;">
This email serves as your receipt for tax purposes.
</p>
</div>
</div>
<div class="section">
<p>If you have any questions about your payment or appointment, please don't hesitate to contact us.</p>
<div style="text-align: center; margin: 20px 0;">
<a href="tel:+19548073027" class="button" style="background: linear-gradient(135deg, #64748b, #475569);">
📞 Call (954) 807-3027
</a>
</div>
</div>
</div>
<div class="footer">
<p><strong>Attune Heart Therapy</strong></p>
<p>Thank you for trusting us with your care</p>
<div class="contact-info">
📞 (954) 807-3027<br>
✉️ hello@attunehearttherapy.com<br>
🌐 attunehearttherapy.com
</div>
<p style="font-size: 12px; color: #9ca3af; margin-top: 15px;">
Payment ID: {{ booking.stripe_payment_intent_id|default:booking.id }}<br>
Processed: {{ booking.paid_at|date:"Y-m-d H:i" }}
</p>
</div>
{% endblock %}

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.8 on 2025-11-13 00:35
# Generated by Django 5.2.8 on 2025-11-22 02:11
import django.db.models.deletion
from django.conf import settings
@ -25,6 +25,11 @@ class Migration(migrations.Migration):
('is_staff', models.BooleanField(default=False)),
('is_superuser', models.BooleanField(default=False)),
('is_active', models.BooleanField(default=True)),
('isVerified', models.BooleanField(default=False)),
('verify_otp', models.CharField(blank=True, max_length=6, null=True)),
('verify_otp_expiry', models.DateTimeField(blank=True, null=True)),
('forgot_password_otp', models.CharField(blank=True, max_length=6, null=True)),
('forgot_password_otp_expiry', models.DateTimeField(blank=True, null=True)),
('phone_number', models.CharField(blank=True, max_length=20)),
('last_login', models.DateTimeField(auto_now=True)),
('date_joined', models.DateTimeField(auto_now_add=True)),

View File

@ -9,6 +9,11 @@ class CustomUser(AbstractBaseUser, PermissionsMixin):
is_staff = models.BooleanField(default=False)
is_superuser = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
isVerified = models.BooleanField(default=False)
verify_otp = models.CharField(max_length=6, blank=True, null=True)
verify_otp_expiry = models.DateTimeField(null=True, blank=True)
forgot_password_otp = models.CharField(max_length=6, blank=True, null=True)
forgot_password_otp_expiry = models.DateTimeField(null=True, blank=True)
phone_number = models.CharField(max_length=20, blank=True)
last_login = models.DateTimeField(auto_now=True)
date_joined = models.DateTimeField(auto_now_add=True)
@ -20,6 +25,9 @@ class CustomUser(AbstractBaseUser, PermissionsMixin):
def __str__(self):
return self.email
def get_full_name(self):
return f"{self.first_name} {self.last_name}"
class UserProfile(models.Model):
user = models.OneToOneField(CustomUser, on_delete=models.CASCADE, related_name='profile')

View File

@ -10,34 +10,47 @@ class UserProfileSerializer(serializers.ModelSerializer):
class UserRegistrationSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True, required=True, validators=[validate_password])
password2 = serializers.CharField(write_only=True, required=True)
profile = UserProfileSerializer(read_only=True)
class Meta:
model = CustomUser
fields = ['email', 'password', 'password2', 'first_name', 'last_name', 'profile']
extra_kwargs = {
'first_name': {'required': True},
'last_name': {'required': True}
}
fields = ('email', 'first_name', 'last_name', 'phone_number', 'password', 'password2')
def validate(self, attrs):
if attrs['password'] != attrs['password2']:
raise serializers.ValidationError({"password": "Password fields didn't match."})
return attrs
def create(self, validated_data):
validated_data.pop('password2')
user = CustomUser.objects.create_user(
email=validated_data['email'],
password=validated_data['password'],
first_name=validated_data['first_name'],
last_name=validated_data['last_name'],
)
password = validated_data.pop('password')
user = CustomUser.objects.create_user(**validated_data)
user.set_password(password)
user.is_active = True
user.isVerified = False
user.save()
return user
class ForgotPasswordSerializer(serializers.Serializer):
email = serializers.EmailField(required=True)
class VerifyPasswordResetOTPSerializer(serializers.Serializer):
email = serializers.EmailField(required=True)
otp = serializers.CharField(required=True, max_length=6)
class ResetPasswordSerializer(serializers.Serializer):
email = serializers.EmailField(required=True)
otp = serializers.CharField(required=True, max_length=6)
new_password = serializers.CharField(required=True, write_only=True, validators=[validate_password])
confirm_password = serializers.CharField(required=True, write_only=True)
def validate(self, attrs):
if attrs['new_password'] != attrs['confirm_password']:
raise serializers.ValidationError({"password": "Password fields didn't match."})
return attrs
class UserSerializer(serializers.ModelSerializer):
profile = UserProfileSerializer(read_only=True)
class Meta:
model = CustomUser
fields = ['id', 'email', 'first_name', 'last_name', 'phone_number', 'profile']
fields = ('id', 'email', 'first_name', 'last_name', 'phone_number', 'isVerified', 'date_joined')

View File

@ -3,8 +3,20 @@ from rest_framework_simplejwt.views import TokenRefreshView
from . import views
urlpatterns = [
path('', views.api_root, name='api-root'),
path('register/', views.register_user, name='register'),
path('login/', views.login_user, name='login'),
path('verify-otp/', views.verify_otp, name='verify-otp'),
path('resend-otp/', views.resend_otp, name='resend-otp'),
path('forgot-password/', views.forgot_password, name='forgot-password'),
path('verify-password-reset-otp/', views.verify_password_reset_otp, name='verify-password-reset-otp'),
path('reset-password/', views.reset_password, name='reset-password'),
path('resend-password-reset-otp/', views.resend_password_reset_otp, name='resend-password-reset-otp'),
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('profile/', views.get_user_profile, name='profile'),
path('profile/update/', views.update_user_profile, name='update_profile'),

57
users/utils.py Normal file
View File

@ -0,0 +1,57 @@
# utils/otp_utils.py
import random
from django.utils import timezone
from datetime import timedelta
from django.conf import settings
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils.html import strip_tags
def generate_otp():
return str(random.randint(100000, 999999))
def send_otp_via_email(email, otp, user_name=None, context='registration'):
try:
context_data = {
'user_name': user_name or 'User',
'otp': otp,
'expiry_minutes': 10,
'company_name': 'Attune Heart Therapy',
'support_email': 'hello@attunehearttherapy.com',
'current_year': timezone.now().year,
'email_context': context
}
if context == 'password_reset':
template_name = 'emails/password_reset_otp.html'
subject = 'Password Reset Request - Verification Code'
else:
template_name = 'emails/otp_verification.html'
subject = 'Your Verification Code - Secure Your Account'
html_content = render_to_string(template_name, context_data)
text_content = strip_tags(html_content)
from_email = settings.DEFAULT_FROM_EMAIL
to_email = [email]
email_msg = EmailMultiAlternatives(
subject,
text_content,
from_email,
to_email
)
email_msg.attach_alternative(html_content, "text/html")
email_msg.send()
return True
except Exception as e:
print(f"Error sending email OTP: {e}")
return False
def is_otp_expired(otp_expiry):
if otp_expiry and timezone.now() < otp_expiry:
return False
return True

View File

@ -5,7 +5,191 @@ from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework_simplejwt.tokens import RefreshToken
from django.contrib.auth import authenticate
from .models import CustomUser, UserProfile
from .serializers import UserRegistrationSerializer, UserSerializer
from .serializers import UserRegistrationSerializer, UserSerializer, ResetPasswordSerializer, ForgotPasswordSerializer, VerifyPasswordResetOTPSerializer
from .utils import send_otp_via_email, is_otp_expired, generate_otp
from django.utils import timezone
from datetime import timedelta
from rest_framework.reverse import reverse
@api_view(['GET'])
@permission_classes([AllowAny])
def api_root(request, format=None):
"""
# Authentication API Documentation
Welcome to the Authentication API. This service provides complete user authentication functionality including registration, email verification, login, and password reset using OTP.
## Base URL
```
{{ request.build_absolute_uri }}
```
## Quick Start
1. **Register** a new user account
2. **Verify** email with OTP sent to your email
3. **Login** with your credentials
4. Use the **access token** for authenticated requests
## API Endpoints
"""
endpoints = {
'documentation': {
'description': 'This API documentation',
'url': request.build_absolute_uri(),
'methods': ['GET']
},
'register': {
'description': 'Register a new user and send verification OTP',
'url': request.build_absolute_uri('register/'),
'methods': ['POST'],
'required_fields': ['email', 'first_name', 'last_name', 'password', 'password2'],
'example_request': {
'email': 'user@example.com',
'first_name': 'John',
'last_name': 'Doe',
'phone_number': '+1234567890',
'password': 'SecurePassword123',
'password2': 'SecurePassword123'
}
},
'verify_otp': {
'description': 'Verify email address using OTP',
'url': request.build_absolute_uri('verify-otp/'),
'methods': ['POST'],
'required_fields': ['email', 'otp'],
'example_request': {
'email': 'user@example.com',
'otp': '123456'
}
},
'login': {
'description': 'Authenticate user and return JWT tokens',
'url': request.build_absolute_uri('login/'),
'methods': ['POST'],
'required_fields': ['email', 'password'],
'example_request': {
'email': 'user@example.com',
'password': 'SecurePassword123'
}
},
'resend_otp': {
'description': 'Resend OTP for email verification or password reset',
'url': request.build_absolute_uri('resend-otp/'),
'methods': ['POST'],
'required_fields': ['email'],
'optional_fields': ['context (registration/password_reset)'],
'example_request': {
'email': 'user@example.com',
'context': 'registration'
}
},
'forgot_password': {
'description': 'Initiate password reset process',
'url': request.build_absolute_uri('forgot-password/'),
'methods': ['POST'],
'required_fields': ['email'],
'example_request': {
'email': 'user@example.com'
}
},
'verify_password_reset_otp': {
'description': 'Verify OTP for password reset',
'url': request.build_absolute_uri('verify-password-reset-otp/'),
'methods': ['POST'],
'required_fields': ['email', 'otp'],
'example_request': {
'email': 'user@example.com',
'otp': '123456'
}
},
'reset_password': {
'description': 'Reset password after OTP verification',
'url': request.build_absolute_uri('reset-password/'),
'methods': ['POST'],
'required_fields': ['email', 'otp', 'new_password', 'confirm_password'],
'example_request': {
'email': 'user@example.com',
'otp': '123456',
'new_password': 'NewSecurePassword123',
'confirm_password': 'NewSecurePassword123'
}
},
'token_refresh': {
'description': 'Refresh access token using refresh token',
'url': request.build_absolute_uri('token/refresh/'),
'methods': ['POST'],
'required_fields': ['refresh'],
'example_request': {
'refresh': 'your_refresh_token_here'
}
}
}
return Response({
'message': 'Authentication API',
'version': '1.0.0',
'endpoints': endpoints,
'authentication_flows': {
'registration_flow': [
'1. POST /register/ - Register user and send OTP',
'2. POST /verify-otp/ - Verify email with OTP',
'3. POST /login/ - Login with credentials'
],
'password_reset_flow': [
'1. POST /forgot-password/ - Request password reset OTP',
'2. POST /verify-password-reset-otp/ - Verify OTP',
'3. POST /reset-password/ - Set new password'
],
'login_flow_unverified': [
'1. POST /login/ - Returns email not verified error',
'2. POST /resend-otp/ - Resend verification OTP',
'3. POST /verify-otp/ - Verify email',
'4. POST /login/ - Successful login'
]
},
'specifications': {
'otp': {
'length': 6,
'expiry_minutes': 10,
'delivery_method': 'email'
},
'tokens': {
'access_token_lifetime': '5 minutes',
'refresh_token_lifetime': '24 hours'
},
'password_requirements': [
'Minimum 8 characters',
'Cannot be entirely numeric',
'Cannot be too common',
'Should include uppercase, lowercase, and numbers'
]
},
'error_handling': {
'common_status_codes': {
'200': 'Success',
'201': 'Created',
'400': 'Bad Request (validation errors)',
'401': 'Unauthorized (invalid credentials)',
'403': 'Forbidden (unverified email, inactive account)',
'404': 'Not Found',
'500': 'Internal Server Error'
},
'error_response_format': {
'error': 'Error description',
'message': 'User-friendly message'
}
},
'security_notes': [
'Always use HTTPS in production',
'Store tokens securely (httpOnly cookies recommended)',
'Implement token refresh logic',
'Validate all inputs on frontend and backend',
'Handle token expiration gracefully'
]
})
@api_view(['POST'])
@permission_classes([AllowAny])
@ -14,20 +198,126 @@ def register_user(request):
if serializer.is_valid():
user = serializer.save()
# Create user profile
UserProfile.objects.create(user=user)
# Generate tokens
refresh = RefreshToken.for_user(user)
otp = generate_otp()
user.verify_otp = otp
user.verify_otp_expiry = timezone.now() + timedelta(minutes=10)
user.save()
user_name = f"{user.first_name} {user.last_name}".strip() or user.email
email_sent = send_otp_via_email(user.email, otp, user_name, 'registration')
if not email_sent:
return Response({
'error': 'Failed to send OTP. Please try again later.'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return Response({
'user': UserSerializer(user).data,
'refresh': str(refresh),
'access': str(refresh.access_token),
'otp_sent': email_sent,
'otp_expires_in': 10
}, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@api_view(['POST'])
@permission_classes([AllowAny])
def verify_otp(request):
email = request.data.get('email')
otp = request.data.get('otp')
if not email or not otp:
return Response({
'error': 'Email and OTP are required'
}, status=status.HTTP_400_BAD_REQUEST)
try:
user = CustomUser.objects.get(email=email)
if user.isVerified:
return Response({
'error': 'User is already verified'
}, status=status.HTTP_400_BAD_REQUEST)
if (user.verify_otp == otp and
not is_otp_expired(user.verify_otp_expiry)):
user.isVerified = True
user.verify_otp = None
user.verify_otp_expiry = None
user.save()
refresh = RefreshToken.for_user(user)
return Response({
'message': 'Email verified successfully',
'verified': True,
}, status=status.HTTP_200_OK)
else:
return Response({
'error': 'Invalid or expired OTP'
}, status=status.HTTP_400_BAD_REQUEST)
except CustomUser.DoesNotExist:
return Response({
'error': 'User not found'
}, status=status.HTTP_404_NOT_FOUND)
@api_view(['POST'])
@permission_classes([AllowAny])
def resend_otp(request):
email = request.data.get('email')
context = request.data.get('context', 'registration')
if not email:
return Response({
'error': 'Email is required'
}, status=status.HTTP_400_BAD_REQUEST)
try:
user = CustomUser.objects.get(email=email)
if user.isVerified and context == 'registration':
return Response({
'error': 'Already verified',
'message': 'Your email is already verified. You can login now.'
}, status=status.HTTP_400_BAD_REQUEST)
otp = generate_otp()
if context == 'password_reset':
user.forgot_password_otp = otp
user.forgot_password_otp_expiry = timezone.now() + timedelta(minutes=10)
else:
user.verify_otp = otp
user.verify_otp_expiry = timezone.now() + timedelta(minutes=10)
user.save()
user_name = f"{user.first_name} {user.last_name}".strip() or user.email
email_sent = send_otp_via_email(user.email, otp, user_name, context)
if not email_sent:
return Response({
'error': 'Failed to send OTP',
'message': 'Please try again later.'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return Response({
'message': f'OTP resent to your email successfully',
'otp_sent': email_sent,
'otp_expires_in': 10,
'context': context
}, status=status.HTTP_200_OK)
except CustomUser.DoesNotExist:
return Response({
'error': 'User not found',
'message': 'No account found with this email address.'
}, status=status.HTTP_404_NOT_FOUND)
@api_view(['POST'])
@permission_classes([AllowAny])
def login_user(request):
@ -37,11 +327,27 @@ def login_user(request):
user = authenticate(request, email=email, password=password)
if user is not None:
if not user.isVerified:
return Response({
'error': 'Email not verified',
'message': 'Please verify your email address before logging in.',
'email': user.email,
'can_resend_otp': True
}, status=status.HTTP_403_FORBIDDEN)
if not user.is_active:
return Response({
'error': 'Account deactivated',
'message': 'Your account has been deactivated. Please contact support.'
}, status=status.HTTP_403_FORBIDDEN)
refresh = RefreshToken.for_user(user)
return Response({
'user': UserSerializer(user).data,
'refresh': str(refresh),
'access': str(refresh.access_token),
'message': 'Login successful'
})
else:
return Response(
@ -49,6 +355,166 @@ def login_user(request):
status=status.HTTP_401_UNAUTHORIZED
)
@api_view(['POST'])
@permission_classes([AllowAny])
def forgot_password(request):
serializer = ForgotPasswordSerializer(data=request.data)
if serializer.is_valid():
email = serializer.validated_data['email']
try:
user = CustomUser.objects.get(email=email)
if not user.isVerified:
return Response({
'error': 'Email not verified',
'message': 'Please verify your email address first.'
}, status=status.HTTP_400_BAD_REQUEST)
if not user.is_active:
return Response({
'error': 'Account deactivated',
'message': 'Your account has been deactivated.'
}, status=status.HTTP_400_BAD_REQUEST)
otp = generate_otp()
user.forgot_password_otp = otp
user.forgot_password_otp_expiry = timezone.now() + timedelta(minutes=10)
user.save()
user_name = f"{user.first_name} {user.last_name}".strip() or user.email
email_sent = send_otp_via_email(user.email, otp, user_name, 'password_reset')
if not email_sent:
return Response({
'error': 'Failed to send OTP',
'message': 'Please try again later.'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return Response({
'message': 'Password reset OTP sent to your email',
'otp_sent': True,
'otp_expires_in': 10,
'email': user.email
}, status=status.HTTP_200_OK)
except CustomUser.DoesNotExist:
return Response({
'message': 'If the email exists, a password reset OTP has been sent.'
}, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@api_view(['POST'])
@permission_classes([AllowAny])
def verify_password_reset_otp(request):
serializer = VerifyPasswordResetOTPSerializer(data=request.data)
if serializer.is_valid():
email = serializer.validated_data['email']
otp = serializer.validated_data['otp']
try:
user = CustomUser.objects.get(email=email)
if (user.forgot_password_otp == otp and
not is_otp_expired(user.forgot_password_otp_expiry)):
return Response({
'message': 'OTP verified successfully',
'verified': True,
'email': user.email
}, status=status.HTTP_200_OK)
else:
return Response({
'error': 'Invalid or expired OTP'
}, status=status.HTTP_400_BAD_REQUEST)
except CustomUser.DoesNotExist:
return Response({
'error': 'User not found'
}, status=status.HTTP_404_NOT_FOUND)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@api_view(['POST'])
@permission_classes([AllowAny])
def reset_password(request):
serializer = ResetPasswordSerializer(data=request.data)
if serializer.is_valid():
email = serializer.validated_data['email']
otp = serializer.validated_data['otp']
new_password = serializer.validated_data['new_password']
try:
user = CustomUser.objects.get(email=email)
if (user.forgot_password_otp == otp and
not is_otp_expired(user.forgot_password_otp_expiry)):
# Set new password
user.set_password(new_password)
user.forgot_password_otp = None
user.forgot_password_otp_expiry = None
user.save()
return Response({
'message': 'Password reset successfully',
'success': True
}, status=status.HTTP_200_OK)
else:
return Response({
'error': 'Invalid or expired OTP'
}, status=status.HTTP_400_BAD_REQUEST)
except CustomUser.DoesNotExist:
return Response({
'error': 'User not found'
}, status=status.HTTP_404_NOT_FOUND)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@api_view(['POST'])
@permission_classes([AllowAny])
def resend_password_reset_otp(request):
serializer = ForgotPasswordSerializer(data=request.data)
if serializer.is_valid():
email = serializer.validated_data['email']
try:
user = CustomUser.objects.get(email=email)
otp = generate_otp()
user.forgot_password_otp = otp
user.forgot_password_otp_expiry = timezone.now() + timedelta(minutes=10)
user.save()
user_name = f"{user.first_name} {user.last_name}".strip() or user.email
email_sent = send_otp_via_email(user.email, otp, user_name, 'password_reset')
if not email_sent:
return Response({
'error': 'Failed to send OTP',
'message': 'Please try again later.'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return Response({
'message': 'Password reset OTP resent to your email',
'otp_sent': True,
'otp_expires_in': 10
}, status=status.HTTP_200_OK)
except CustomUser.DoesNotExist:
return Response({
'message': 'If the email exists, a password reset OTP has been sent.'
}, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def get_user_profile(request):