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

View File

@ -46,7 +46,9 @@ ROOT_URLCONF = 'booking_system.urls'
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [], 'DIRS': [
os.path.join(BASE_DIR, 'templates'),
],
'APP_DIRS': True, 'APP_DIRS': True,
'OPTIONS': { 'OPTIONS': {
'context_processors': [ 'context_processors': [
@ -95,12 +97,35 @@ SIMPLE_JWT = {
} }
# HIPAA Email Configuration
EMAIL_ENCRYPTION_KEY = os.getenv('EMAIL_ENCRYPTION_KEY')
# Stripe Configuration # Stripe Configuration
STRIPE_PUBLISHABLE_KEY = os.getenv('STRIPE_PUBLISHABLE_KEY') STRIPE_PUBLISHABLE_KEY = os.getenv('STRIPE_PUBLISHABLE_KEY')
STRIPE_SECRET_KEY = os.getenv('STRIPE_SECRET_KEY') STRIPE_SECRET_KEY = os.getenv('STRIPE_SECRET_KEY')
STRIPE_WEBHOOK_SECRET = os.getenv('STRIPE_WEBHOOK_SECRET') 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 Configuration
JITSI_BASE_URL = os.getenv('JITSI_BASE_URL', 'https://meet.jit.si') 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 import django.db.models.deletion
from django.conf import settings from django.conf import settings
@ -25,6 +25,11 @@ class Migration(migrations.Migration):
('is_staff', models.BooleanField(default=False)), ('is_staff', models.BooleanField(default=False)),
('is_superuser', models.BooleanField(default=False)), ('is_superuser', models.BooleanField(default=False)),
('is_active', models.BooleanField(default=True)), ('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)), ('phone_number', models.CharField(blank=True, max_length=20)),
('last_login', models.DateTimeField(auto_now=True)), ('last_login', models.DateTimeField(auto_now=True)),
('date_joined', models.DateTimeField(auto_now_add=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_staff = models.BooleanField(default=False)
is_superuser = models.BooleanField(default=False) is_superuser = models.BooleanField(default=False)
is_active = models.BooleanField(default=True) 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) phone_number = models.CharField(max_length=20, blank=True)
last_login = models.DateTimeField(auto_now=True) last_login = models.DateTimeField(auto_now=True)
date_joined = models.DateTimeField(auto_now_add=True) date_joined = models.DateTimeField(auto_now_add=True)
@ -21,6 +26,9 @@ class CustomUser(AbstractBaseUser, PermissionsMixin):
def __str__(self): def __str__(self):
return self.email return self.email
def get_full_name(self):
return f"{self.first_name} {self.last_name}"
class UserProfile(models.Model): class UserProfile(models.Model):
user = models.OneToOneField(CustomUser, on_delete=models.CASCADE, related_name='profile') user = models.OneToOneField(CustomUser, on_delete=models.CASCADE, related_name='profile')
bio = models.TextField(max_length=500, blank=True) bio = models.TextField(max_length=500, blank=True)

View File

@ -10,15 +10,10 @@ class UserProfileSerializer(serializers.ModelSerializer):
class UserRegistrationSerializer(serializers.ModelSerializer): class UserRegistrationSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True, required=True, validators=[validate_password]) password = serializers.CharField(write_only=True, required=True, validators=[validate_password])
password2 = serializers.CharField(write_only=True, required=True) password2 = serializers.CharField(write_only=True, required=True)
profile = UserProfileSerializer(read_only=True)
class Meta: class Meta:
model = CustomUser model = CustomUser
fields = ['email', 'password', 'password2', 'first_name', 'last_name', 'profile'] fields = ('email', 'first_name', 'last_name', 'phone_number', 'password', 'password2')
extra_kwargs = {
'first_name': {'required': True},
'last_name': {'required': True}
}
def validate(self, attrs): def validate(self, attrs):
if attrs['password'] != attrs['password2']: if attrs['password'] != attrs['password2']:
@ -27,17 +22,35 @@ class UserRegistrationSerializer(serializers.ModelSerializer):
def create(self, validated_data): def create(self, validated_data):
validated_data.pop('password2') validated_data.pop('password2')
user = CustomUser.objects.create_user( password = validated_data.pop('password')
email=validated_data['email'],
password=validated_data['password'], user = CustomUser.objects.create_user(**validated_data)
first_name=validated_data['first_name'], user.set_password(password)
last_name=validated_data['last_name'], user.is_active = True
) user.isVerified = False
user.save()
return user return user
class UserSerializer(serializers.ModelSerializer): class ForgotPasswordSerializer(serializers.Serializer):
profile = UserProfileSerializer(read_only=True) 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):
class Meta: class Meta:
model = CustomUser 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 from . import views
urlpatterns = [ urlpatterns = [
path('', views.api_root, name='api-root'),
path('register/', views.register_user, name='register'), path('register/', views.register_user, name='register'),
path('login/', views.login_user, name='login'), 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('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('profile/', views.get_user_profile, name='profile'), path('profile/', views.get_user_profile, name='profile'),
path('profile/update/', views.update_user_profile, name='update_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 rest_framework_simplejwt.tokens import RefreshToken
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from .models import CustomUser, UserProfile 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']) @api_view(['POST'])
@permission_classes([AllowAny]) @permission_classes([AllowAny])
@ -14,20 +198,126 @@ def register_user(request):
if serializer.is_valid(): if serializer.is_valid():
user = serializer.save() user = serializer.save()
# Create user profile
UserProfile.objects.create(user=user) UserProfile.objects.create(user=user)
# Generate tokens otp = generate_otp()
refresh = RefreshToken.for_user(user) 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({ return Response({
'user': UserSerializer(user).data, 'user': UserSerializer(user).data,
'refresh': str(refresh), 'otp_sent': email_sent,
'access': str(refresh.access_token), 'otp_expires_in': 10
}, status=status.HTTP_201_CREATED) }, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 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']) @api_view(['POST'])
@permission_classes([AllowAny]) @permission_classes([AllowAny])
def login_user(request): def login_user(request):
@ -37,11 +327,27 @@ def login_user(request):
user = authenticate(request, email=email, password=password) user = authenticate(request, email=email, password=password)
if user is not None: 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) refresh = RefreshToken.for_user(user)
return Response({ return Response({
'user': UserSerializer(user).data, 'user': UserSerializer(user).data,
'refresh': str(refresh), 'refresh': str(refresh),
'access': str(refresh.access_token), 'access': str(refresh.access_token),
'message': 'Login successful'
}) })
else: else:
return Response( return Response(
@ -49,6 +355,166 @@ def login_user(request):
status=status.HTTP_401_UNAUTHORIZED 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']) @api_view(['GET'])
@permission_classes([IsAuthenticated]) @permission_classes([IsAuthenticated])
def get_user_profile(request): def get_user_profile(request):