mirror of
https://github.com/thecyberlearn/quantum-ai-v2.git
synced 2026-08-18 17:12:59 +00:00
Fix email URLs to use Railway domain instead of localhost
- Add SITE_URL configuration that auto-detects Railway environment - Update forgot password view to use correct site URL in emails - Ensure password reset links work on both local and Railway deployments 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
49b5b2a6aa
commit
d36cf9a79d
13
.env.email.example
Normal file
13
.env.email.example
Normal file
@ -0,0 +1,13 @@
|
||||
# Email Configuration
|
||||
EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
|
||||
EMAIL_HOST=smtp.gmail.com
|
||||
EMAIL_PORT=587
|
||||
EMAIL_USE_TLS=True
|
||||
EMAIL_HOST_USER=your-email@gmail.com
|
||||
EMAIL_HOST_PASSWORD=your-app-password
|
||||
DEFAULT_FROM_EMAIL=NetCop <your-email@gmail.com>
|
||||
|
||||
# Other settings (copy from existing if you have them)
|
||||
SECRET_KEY=your-secret-key-here
|
||||
DEBUG=True
|
||||
ALLOWED_HOSTS=localhost,127.0.0.1
|
||||
48
authentication/migrations/0003_passwordresettoken.py
Normal file
48
authentication/migrations/0003_passwordresettoken.py
Normal file
@ -0,0 +1,48 @@
|
||||
# Generated by Django 5.2.4 on 2025-07-16 06:48
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentication", "0002_alter_user_options_alter_user_created_at_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="PasswordResetToken",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"token",
|
||||
models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("expires_at", models.DateTimeField()),
|
||||
("is_used", models.BooleanField(default=False)),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="password_reset_tokens",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"ordering": ["-created_at"],
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -1,6 +1,9 @@
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
from decimal import Decimal
|
||||
import uuid
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
@ -55,3 +58,29 @@ class User(AbstractUser):
|
||||
description=description,
|
||||
stripe_session_id=stripe_session_id
|
||||
)
|
||||
|
||||
|
||||
class PasswordResetToken(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='password_reset_tokens')
|
||||
token = models.UUIDField(default=uuid.uuid4, unique=True, editable=False)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
expires_at = models.DateTimeField()
|
||||
is_used = models.BooleanField(default=False)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.expires_at:
|
||||
self.expires_at = timezone.now() + timedelta(hours=1)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def is_valid(self):
|
||||
return not self.is_used and timezone.now() < self.expires_at
|
||||
|
||||
def mark_as_used(self):
|
||||
self.is_used = True
|
||||
self.save()
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"Password reset token for {self.user.email}"
|
||||
|
||||
@ -8,4 +8,6 @@ urlpatterns = [
|
||||
path('register/', views.register_view, name='register'),
|
||||
path('logout/', views.logout_view, name='logout'),
|
||||
path('profile/', views.profile_view, name='profile'),
|
||||
path('forgot-password/', views.forgot_password_view, name='forgot_password'),
|
||||
path('reset-password/<uuid:token>/', views.reset_password_view, name='reset_password'),
|
||||
]
|
||||
@ -1,10 +1,13 @@
|
||||
from django.shortcuts import render, redirect
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.contrib.auth import login, authenticate, logout
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.forms import UserCreationForm
|
||||
from django.http import JsonResponse
|
||||
from .models import User
|
||||
from django.core.mail import send_mail
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
from .models import User, PasswordResetToken
|
||||
|
||||
|
||||
def login_view(request):
|
||||
@ -105,3 +108,89 @@ def profile_view(request):
|
||||
}
|
||||
|
||||
return render(request, 'authentication/profile.html', context)
|
||||
|
||||
|
||||
def forgot_password_view(request):
|
||||
"""Forgot password view - request password reset"""
|
||||
if request.method == 'POST':
|
||||
email = request.POST.get('email')
|
||||
|
||||
try:
|
||||
user = User.objects.get(email=email)
|
||||
|
||||
# Create password reset token
|
||||
reset_token = PasswordResetToken.objects.create(user=user)
|
||||
|
||||
# Build reset URL using correct site URL
|
||||
reset_path = reverse('authentication:reset_password', kwargs={'token': reset_token.token})
|
||||
reset_url = f"{settings.SITE_URL}{reset_path}"
|
||||
|
||||
# Send email
|
||||
subject = 'Password Reset Request'
|
||||
message = f'''
|
||||
Hello {user.username},
|
||||
|
||||
You requested a password reset for your NetCop account.
|
||||
|
||||
Click the link below to reset your password:
|
||||
{reset_url}
|
||||
|
||||
This link will expire in 1 hour.
|
||||
|
||||
If you didn't request this reset, please ignore this email.
|
||||
|
||||
Best regards,
|
||||
NetCop Team
|
||||
'''
|
||||
|
||||
try:
|
||||
send_mail(
|
||||
subject,
|
||||
message,
|
||||
settings.DEFAULT_FROM_EMAIL,
|
||||
[email],
|
||||
fail_silently=False,
|
||||
)
|
||||
messages.success(request, 'Password reset instructions have been sent to your email.')
|
||||
except Exception as e:
|
||||
messages.error(request, 'Failed to send reset email. Please try again later.')
|
||||
|
||||
except User.DoesNotExist:
|
||||
# Don't reveal if email exists or not for security
|
||||
messages.success(request, 'If an account with that email exists, password reset instructions have been sent.')
|
||||
|
||||
return render(request, 'authentication/forgot_password.html')
|
||||
|
||||
|
||||
def reset_password_view(request, token):
|
||||
"""Reset password view - using token from email"""
|
||||
reset_token = get_object_or_404(PasswordResetToken, token=token)
|
||||
|
||||
if not reset_token.is_valid():
|
||||
messages.error(request, 'This password reset link has expired or is invalid.')
|
||||
return redirect('authentication:forgot_password')
|
||||
|
||||
if request.method == 'POST':
|
||||
password1 = request.POST.get('password1')
|
||||
password2 = request.POST.get('password2')
|
||||
|
||||
if password1 != password2:
|
||||
messages.error(request, 'Passwords do not match.')
|
||||
return render(request, 'authentication/reset_password.html', {'token': token})
|
||||
|
||||
if len(password1) < 8:
|
||||
messages.error(request, 'Password must be at least 8 characters long.')
|
||||
return render(request, 'authentication/reset_password.html', {'token': token})
|
||||
|
||||
# Reset password
|
||||
user = reset_token.user
|
||||
user.set_password(password1)
|
||||
user.save()
|
||||
|
||||
# Mark token as used
|
||||
reset_token.mark_as_used()
|
||||
|
||||
messages.success(request, 'Your password has been reset successfully. You can now log in.')
|
||||
return redirect('authentication:login')
|
||||
|
||||
return render(request, 'authentication/reset_password.html', {'token': token})
|
||||
|
||||
107
debug_email.py
Normal file
107
debug_email.py
Normal file
@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import django
|
||||
from django.conf import settings
|
||||
|
||||
# Setup Django
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'netcop_hub.settings')
|
||||
django.setup()
|
||||
|
||||
from django.core.mail import send_mail
|
||||
from authentication.models import User, PasswordResetToken
|
||||
import traceback
|
||||
|
||||
def debug_email_sending():
|
||||
print("=" * 60)
|
||||
print("🔍 DEBUGGING EMAIL CONFIGURATION")
|
||||
print("=" * 60)
|
||||
|
||||
# Check Django settings
|
||||
print(f"EMAIL_BACKEND: {settings.EMAIL_BACKEND}")
|
||||
print(f"EMAIL_HOST: {settings.EMAIL_HOST}")
|
||||
print(f"EMAIL_PORT: {settings.EMAIL_PORT}")
|
||||
print(f"EMAIL_USE_TLS: {settings.EMAIL_USE_TLS}")
|
||||
print(f"EMAIL_HOST_USER: {settings.EMAIL_HOST_USER}")
|
||||
print(f"EMAIL_HOST_PASSWORD: {'*' * len(settings.EMAIL_HOST_PASSWORD) if settings.EMAIL_HOST_PASSWORD else 'NOT SET'}")
|
||||
print(f"DEFAULT_FROM_EMAIL: {settings.DEFAULT_FROM_EMAIL}")
|
||||
print()
|
||||
|
||||
# Test basic email sending
|
||||
print("📧 Testing basic email sending...")
|
||||
try:
|
||||
send_mail(
|
||||
'Test Email from NetCop',
|
||||
'This is a test email to verify email configuration.',
|
||||
settings.DEFAULT_FROM_EMAIL,
|
||||
[settings.EMAIL_HOST_USER], # Send to yourself
|
||||
fail_silently=False,
|
||||
)
|
||||
print("✅ Basic email test PASSED")
|
||||
except Exception as e:
|
||||
print(f"❌ Basic email test FAILED: {e}")
|
||||
print(f"Error details: {traceback.format_exc()}")
|
||||
return False
|
||||
|
||||
# Test forgot password flow
|
||||
print("\n🔑 Testing forgot password flow...")
|
||||
try:
|
||||
# Get a test user
|
||||
user = User.objects.filter(email=settings.EMAIL_HOST_USER).first()
|
||||
if not user:
|
||||
user = User.objects.first()
|
||||
|
||||
if not user:
|
||||
print("❌ No users found in database")
|
||||
return False
|
||||
|
||||
print(f"Using test user: {user.email}")
|
||||
|
||||
# Create password reset token
|
||||
reset_token = PasswordResetToken.objects.create(user=user)
|
||||
print(f"✅ Password reset token created: {reset_token.token}")
|
||||
|
||||
# Build reset URL
|
||||
reset_url = f"http://localhost:8000/auth/reset-password/{reset_token.token}/"
|
||||
|
||||
# Send reset email
|
||||
subject = 'Password Reset Request - DEBUG TEST'
|
||||
message = f'''
|
||||
Hello {user.username},
|
||||
|
||||
This is a DEBUG TEST of the password reset functionality.
|
||||
|
||||
Click the link below to reset your password:
|
||||
{reset_url}
|
||||
|
||||
This link will expire in 1 hour.
|
||||
|
||||
If you didn't request this reset, please ignore this email.
|
||||
|
||||
Best regards,
|
||||
NetCop Team (DEBUG MODE)
|
||||
'''
|
||||
|
||||
send_mail(
|
||||
subject,
|
||||
message,
|
||||
settings.DEFAULT_FROM_EMAIL,
|
||||
[user.email],
|
||||
fail_silently=False,
|
||||
)
|
||||
|
||||
print("✅ Password reset email sent successfully!")
|
||||
print(f"📧 Email sent to: {user.email}")
|
||||
print(f"🔗 Reset URL: {reset_url}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Password reset test FAILED: {e}")
|
||||
print(f"Error details: {traceback.format_exc()}")
|
||||
return False
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("✅ ALL TESTS PASSED - Check your Gmail inbox!")
|
||||
print("=" * 60)
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
debug_email_sending()
|
||||
676
docs/WALLET_STRIPE_IMPLEMENTATION.md
Normal file
676
docs/WALLET_STRIPE_IMPLEMENTATION.md
Normal file
@ -0,0 +1,676 @@
|
||||
# 💳 NetCop Wallet/Stripe Implementation Guide
|
||||
|
||||
## 🎯 Overview
|
||||
This guide documents how to implement a professional wallet system with Stripe Payment Intents API in the NetCop Django project. The system provides real-time payment processing **without requiring webhooks** for basic functionality.
|
||||
|
||||
## ✨ Key Features
|
||||
- **Professional wallet topup interface** with Stripe Elements
|
||||
- **Real-time payment processing** with Payment Intents API
|
||||
- **Loading states and progress indicators** for better UX
|
||||
- **Webhook-free operation** for development and testing
|
||||
- **AED currency support** matching NetCop pricing
|
||||
- **Balance checking** before agent usage
|
||||
- **Transaction history** with copy/download functionality
|
||||
|
||||
## 🚫 No Webhooks Required
|
||||
|
||||
### Why No Webhooks Needed:
|
||||
- **Payment Intents API** provides immediate payment status
|
||||
- **Frontend confirmation** happens in real-time after card processing
|
||||
- **Direct database updates** via confirmed payment status
|
||||
- **Duplicate prevention** through payment metadata checking
|
||||
|
||||
### Payment Flow (Webhook-Free):
|
||||
1. User selects topup amount → Frontend creates Payment Intent
|
||||
2. Stripe Elements processes card securely → Returns success/failure
|
||||
3. Frontend confirms payment status → Backend updates wallet immediately
|
||||
4. User sees updated balance → Can use agents with sufficient funds
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Implementation Steps
|
||||
|
||||
### 1. Environment Configuration
|
||||
|
||||
Add to `.env` file:
|
||||
```bash
|
||||
# Stripe Configuration (No webhook secret required for basic functionality)
|
||||
STRIPE_PUBLISHABLE_KEY=pk_test_your_publishable_key_here
|
||||
STRIPE_SECRET_KEY=sk_test_your_secret_key_here
|
||||
# STRIPE_WEBHOOK_SECRET=whsec_... (optional for production)
|
||||
```
|
||||
|
||||
Add to `netcop_hub/settings.py`:
|
||||
```python
|
||||
# Stripe Configuration
|
||||
STRIPE_SECRET_KEY = config('STRIPE_SECRET_KEY', default='')
|
||||
STRIPE_PUBLISHABLE_KEY = config('STRIPE_PUBLISHABLE_KEY', default='')
|
||||
STRIPE_WEBHOOK_SECRET = config('STRIPE_WEBHOOK_SECRET', default='')
|
||||
```
|
||||
|
||||
### 2. User Model Enhancement
|
||||
|
||||
Update `authentication/models.py` to add wallet balance:
|
||||
```python
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
from decimal import Decimal
|
||||
|
||||
class User(AbstractUser):
|
||||
email = models.EmailField(unique=True)
|
||||
wallet_balance = models.DecimalField(
|
||||
max_digits=10,
|
||||
decimal_places=2,
|
||||
default=Decimal('0.00'),
|
||||
help_text="User wallet balance in AED"
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
USERNAME_FIELD = 'email'
|
||||
REQUIRED_FIELDS = ['username']
|
||||
|
||||
def has_sufficient_balance(self, amount):
|
||||
"""Check if user has sufficient balance for a transaction"""
|
||||
return self.wallet_balance >= Decimal(str(amount))
|
||||
|
||||
def deduct_balance(self, amount, description=""):
|
||||
"""Deduct amount from wallet balance"""
|
||||
if self.has_sufficient_balance(amount):
|
||||
self.wallet_balance -= Decimal(str(amount))
|
||||
self.save()
|
||||
|
||||
# Create transaction record
|
||||
from wallet.models import WalletTransaction
|
||||
WalletTransaction.objects.create(
|
||||
user=self,
|
||||
amount=-Decimal(str(amount)),
|
||||
type='agent_usage',
|
||||
description=description
|
||||
)
|
||||
return True
|
||||
return False
|
||||
|
||||
def add_balance(self, amount, description=""):
|
||||
"""Add amount to wallet balance"""
|
||||
self.wallet_balance += Decimal(str(amount))
|
||||
self.save()
|
||||
|
||||
# Create transaction record
|
||||
from wallet.models import WalletTransaction
|
||||
WalletTransaction.objects.create(
|
||||
user=self,
|
||||
amount=Decimal(str(amount)),
|
||||
type='top_up',
|
||||
description=description
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Wallet Models
|
||||
|
||||
Update `wallet/models.py`:
|
||||
```python
|
||||
from django.db import models
|
||||
from django.contrib.auth import get_user_model
|
||||
import uuid
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
class WalletTransaction(models.Model):
|
||||
TRANSACTION_TYPES = [
|
||||
('top_up', 'Top Up'),
|
||||
('agent_usage', 'Agent Usage'),
|
||||
('refund', 'Refund'),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='wallet_transactions')
|
||||
amount = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
type = models.CharField(max_length=20, choices=TRANSACTION_TYPES)
|
||||
description = models.TextField()
|
||||
stripe_payment_intent_id = models.CharField(max_length=200, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.email} - {self.amount} AED ({self.type})"
|
||||
```
|
||||
|
||||
### 4. Wallet Views (Payment Intents API)
|
||||
|
||||
Create `wallet/views.py`:
|
||||
```python
|
||||
import stripe
|
||||
import json
|
||||
from django.conf import settings
|
||||
from django.shortcuts import render
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.http import JsonResponse
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.utils import timezone
|
||||
from .models import WalletTransaction
|
||||
from decimal import Decimal
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
User = get_user_model()
|
||||
stripe.api_key = settings.STRIPE_SECRET_KEY
|
||||
|
||||
@login_required
|
||||
def topup(request):
|
||||
"""Professional wallet topup page"""
|
||||
return render(request, "wallet/topup.html", {
|
||||
'stripe_publishable_key': settings.STRIPE_PUBLISHABLE_KEY,
|
||||
'user_balance': request.user.wallet_balance
|
||||
})
|
||||
|
||||
@login_required
|
||||
@csrf_exempt
|
||||
def create_payment_intent(request):
|
||||
"""Create Stripe Payment Intent for wallet topup"""
|
||||
if request.method == "POST":
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
amount = int(data.get("amount"))
|
||||
|
||||
if amount < 1:
|
||||
return JsonResponse({"error": "Amount must be at least 1 AED"}, status=400)
|
||||
|
||||
# Create Payment Intent
|
||||
intent = stripe.PaymentIntent.create(
|
||||
amount=amount * 100, # Convert to fils (AED cents)
|
||||
currency='aed',
|
||||
metadata={
|
||||
'user_id': request.user.id,
|
||||
'amount': amount,
|
||||
'email': request.user.email
|
||||
},
|
||||
description=f"NetCop wallet top-up for {request.user.email}"
|
||||
)
|
||||
|
||||
return JsonResponse({
|
||||
'client_secret': intent.client_secret,
|
||||
'amount': amount
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({"error": str(e)}, status=400)
|
||||
|
||||
return JsonResponse({"error": "Invalid request method"}, status=405)
|
||||
|
||||
@login_required
|
||||
@csrf_exempt
|
||||
def confirm_payment(request):
|
||||
"""Confirm payment and update wallet balance"""
|
||||
if request.method == "POST":
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
payment_intent_id = data.get("payment_intent_id")
|
||||
|
||||
# Retrieve payment intent from Stripe
|
||||
intent = stripe.PaymentIntent.retrieve(payment_intent_id)
|
||||
|
||||
if intent.status == 'succeeded':
|
||||
user_id = int(intent.metadata['user_id'])
|
||||
amount = Decimal(intent.metadata['amount'])
|
||||
|
||||
# Verify this is the correct user
|
||||
if user_id != request.user.id:
|
||||
return JsonResponse({"error": "Unauthorized"}, status=403)
|
||||
|
||||
# Check for duplicate processing
|
||||
existing_transaction = WalletTransaction.objects.filter(
|
||||
stripe_payment_intent_id=payment_intent_id
|
||||
).first()
|
||||
|
||||
if not existing_transaction:
|
||||
# Update user balance using model method
|
||||
request.user.add_balance(
|
||||
amount=amount,
|
||||
description=f"Wallet top-up via Stripe - {amount} AED"
|
||||
)
|
||||
|
||||
# Update the transaction with Stripe ID
|
||||
latest_transaction = WalletTransaction.objects.filter(
|
||||
user=request.user,
|
||||
type='top_up',
|
||||
amount=amount
|
||||
).first()
|
||||
if latest_transaction:
|
||||
latest_transaction.stripe_payment_intent_id = payment_intent_id
|
||||
latest_transaction.save()
|
||||
|
||||
return JsonResponse({
|
||||
"success": True,
|
||||
"message": f"Successfully added {amount} AED to your wallet",
|
||||
"new_balance": str(request.user.wallet_balance)
|
||||
})
|
||||
else:
|
||||
return JsonResponse({"error": "Payment not completed"}, status=400)
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({"error": str(e)}, status=400)
|
||||
|
||||
return JsonResponse({"error": "Invalid request method"}, status=405)
|
||||
|
||||
@login_required
|
||||
def transaction_history(request):
|
||||
"""View transaction history"""
|
||||
transactions = request.user.wallet_transactions.all()[:50]
|
||||
return render(request, "wallet/history.html", {
|
||||
'transactions': transactions,
|
||||
'current_balance': request.user.wallet_balance
|
||||
})
|
||||
```
|
||||
|
||||
### 5. Wallet URLs
|
||||
|
||||
Create `wallet/urls.py`:
|
||||
```python
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'wallet'
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.topup, name='topup'),
|
||||
path('create-payment-intent/', views.create_payment_intent, name='create_payment_intent'),
|
||||
path('confirm-payment/', views.confirm_payment, name='confirm_payment'),
|
||||
path('history/', views.transaction_history, name='history'),
|
||||
]
|
||||
```
|
||||
|
||||
### 6. Professional Topup Template
|
||||
|
||||
Create `templates/wallet/topup.html`:
|
||||
```html
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Top Up Wallet - NetCop Hub{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div style="max-width: 600px; margin: var(--space-xl) auto; padding: 0 var(--space-md);">
|
||||
<div class="card">
|
||||
<div style="text-align: center; margin-bottom: var(--space-xl);">
|
||||
<h1 style="color: var(--text-primary); margin-bottom: var(--space-sm);">💰 Top Up Wallet</h1>
|
||||
<p style="color: var(--text-secondary);">Add funds to your wallet to use AI agents</p>
|
||||
</div>
|
||||
|
||||
<div style="background: var(--bg-accent); padding: var(--space-md); border-radius: var(--radius); margin-bottom: var(--space-lg); text-align: center;">
|
||||
<p style="color: var(--text-secondary); margin-bottom: var(--space-xs);">Current Balance</p>
|
||||
<p style="font-size: var(--text-xl); font-weight: 600; color: var(--success-green);">
|
||||
{{ user_balance|floatformat:2 }} AED
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Amount Selection -->
|
||||
<div style="margin-bottom: var(--space-lg);">
|
||||
<label style="display: block; font-weight: 500; color: var(--text-primary); margin-bottom: var(--space-sm);">Select Amount (AED)</label>
|
||||
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-sm); margin-bottom: var(--space-md);">
|
||||
<button type="button" class="btn btn-secondary amount-btn" data-amount="50">50 AED</button>
|
||||
<button type="button" class="btn btn-secondary amount-btn" data-amount="100">100 AED</button>
|
||||
<button type="button" class="btn btn-secondary amount-btn" data-amount="200">200 AED</button>
|
||||
</div>
|
||||
<input type="number"
|
||||
id="amount"
|
||||
placeholder="Enter custom amount"
|
||||
class="form-input"
|
||||
style="width: 100%;"
|
||||
min="1"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<!-- Payment Form -->
|
||||
<form id="payment-form">
|
||||
<div style="margin-bottom: var(--space-lg);">
|
||||
<label style="display: block; font-weight: 500; color: var(--text-primary); margin-bottom: var(--space-sm);">💳 Card Information</label>
|
||||
<div id="card-element" style="border: 1px solid var(--border-color); border-radius: var(--radius); padding: var(--space-md); background: var(--bg-primary);">
|
||||
<!-- Stripe Elements will create form elements here -->
|
||||
</div>
|
||||
<div id="card-errors" style="color: var(--error-red); font-size: var(--text-sm); margin-top: var(--space-sm);" role="alert"></div>
|
||||
</div>
|
||||
|
||||
<button id="submit-payment"
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
style="width: 100%; font-size: var(--text-base);">
|
||||
<span id="button-text">🚀 Add to Wallet</span>
|
||||
<div id="spinner" style="display: none;">
|
||||
<span style="display: inline-block; width: 16px; height: 16px; border: 2px solid #ffffff; border-radius: 50%; border-top-color: transparent; animation: spin 1s linear infinite; margin-right: var(--space-xs);"></span>
|
||||
Processing...
|
||||
</div>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Success/Error Messages -->
|
||||
<div id="payment-result" style="margin-top: var(--space-lg); display: none;">
|
||||
<div id="success-message" style="background: #f0fdf4; border: 1px solid #bbf7d0; color: #166534; padding: var(--space-md); border-radius: var(--radius); display: none;">
|
||||
<strong>✅ Success!</strong> <span id="success-text"></span>
|
||||
</div>
|
||||
<div id="error-message" style="background: #fef2f2; border: 1px solid #fecaca; color: #dc2626; padding: var(--space-md); border-radius: var(--radius); display: none;">
|
||||
<strong>❌ Error:</strong> <span id="error-text"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stripe.js -->
|
||||
<script src="https://js.stripe.com/v3/"></script>
|
||||
<script>
|
||||
// CSS for spinner animation
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Initialize Stripe
|
||||
const stripe = Stripe('{{ stripe_publishable_key }}');
|
||||
const elements = stripe.elements();
|
||||
|
||||
// Create card element
|
||||
const cardElement = elements.create('card', {
|
||||
style: {
|
||||
base: {
|
||||
fontSize: '16px',
|
||||
color: '#1f2937',
|
||||
fontFamily: 'Inter, system-ui, sans-serif',
|
||||
'::placeholder': {
|
||||
color: '#9ca3af',
|
||||
},
|
||||
},
|
||||
invalid: {
|
||||
color: '#dc2626',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
cardElement.mount('#card-element');
|
||||
|
||||
// Handle real-time validation errors
|
||||
cardElement.addEventListener('change', ({error}) => {
|
||||
const displayError = document.getElementById('card-errors');
|
||||
if (error) {
|
||||
displayError.textContent = error.message;
|
||||
} else {
|
||||
displayError.textContent = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Amount selection buttons
|
||||
document.querySelectorAll('.amount-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const amount = this.dataset.amount;
|
||||
document.getElementById('amount').value = amount;
|
||||
|
||||
// Update button styles
|
||||
document.querySelectorAll('.amount-btn').forEach(b => {
|
||||
b.classList.remove('btn-primary');
|
||||
b.classList.add('btn-secondary');
|
||||
});
|
||||
this.classList.remove('btn-secondary');
|
||||
this.classList.add('btn-primary');
|
||||
});
|
||||
});
|
||||
|
||||
// Payment form submission
|
||||
const form = document.getElementById('payment-form');
|
||||
form.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const amount = parseInt(document.getElementById('amount').value);
|
||||
if (!amount || amount < 1) {
|
||||
showError('Please enter a valid amount');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// Create Payment Intent
|
||||
const response = await fetch('/wallet/create-payment-intent/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ amount: amount }),
|
||||
});
|
||||
|
||||
const { client_secret, error } = await response.json();
|
||||
|
||||
if (error) {
|
||||
showError(error);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Confirm payment with Stripe
|
||||
const { error: stripeError, paymentIntent } = await stripe.confirmCardPayment(client_secret, {
|
||||
payment_method: {
|
||||
card: cardElement,
|
||||
}
|
||||
});
|
||||
|
||||
if (stripeError) {
|
||||
showError(stripeError.message);
|
||||
setLoading(false);
|
||||
} else if (paymentIntent.status === 'succeeded') {
|
||||
// Confirm payment on server
|
||||
const confirmResponse = await fetch('/wallet/confirm-payment/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ payment_intent_id: paymentIntent.id }),
|
||||
});
|
||||
|
||||
const confirmResult = await confirmResponse.json();
|
||||
|
||||
if (confirmResult.success) {
|
||||
showSuccess(`${confirmResult.message}. New balance: ${confirmResult.new_balance} AED`);
|
||||
// Reset form
|
||||
form.reset();
|
||||
cardElement.clear();
|
||||
document.getElementById('amount').value = '';
|
||||
// Reset amount buttons
|
||||
document.querySelectorAll('.amount-btn').forEach(b => {
|
||||
b.classList.remove('btn-primary');
|
||||
b.classList.add('btn-secondary');
|
||||
});
|
||||
// Reload page after 2 seconds to show updated balance
|
||||
setTimeout(() => window.location.reload(), 2000);
|
||||
} else {
|
||||
showError(confirmResult.error || 'Payment confirmation failed');
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (error) {
|
||||
showError('Network error: ' + error.message);
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
function setLoading(loading) {
|
||||
const button = document.getElementById('submit-payment');
|
||||
const buttonText = document.getElementById('button-text');
|
||||
const spinner = document.getElementById('spinner');
|
||||
|
||||
if (loading) {
|
||||
button.disabled = true;
|
||||
buttonText.style.display = 'none';
|
||||
spinner.style.display = 'inline-block';
|
||||
} else {
|
||||
button.disabled = false;
|
||||
buttonText.style.display = 'inline-block';
|
||||
spinner.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function showSuccess(message) {
|
||||
const resultDiv = document.getElementById('payment-result');
|
||||
const successDiv = document.getElementById('success-message');
|
||||
const errorDiv = document.getElementById('error-message');
|
||||
const successText = document.getElementById('success-text');
|
||||
|
||||
successText.textContent = message;
|
||||
successDiv.style.display = 'block';
|
||||
errorDiv.style.display = 'none';
|
||||
resultDiv.style.display = 'block';
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
const resultDiv = document.getElementById('payment-result');
|
||||
const successDiv = document.getElementById('success-message');
|
||||
const errorDiv = document.getElementById('error-message');
|
||||
const errorText = document.getElementById('error-text');
|
||||
|
||||
errorText.textContent = message;
|
||||
errorDiv.style.display = 'block';
|
||||
successDiv.style.display = 'none';
|
||||
resultDiv.style.display = 'block';
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
### 7. Update Navigation
|
||||
|
||||
Update `templates/base.html` to include wallet balance in navigation:
|
||||
```html
|
||||
<!-- In the user info section -->
|
||||
{% if user.is_authenticated %}
|
||||
<p class="user-welcome">Welcome, {{ user.username }}!</p>
|
||||
<a href="{% url 'wallet:topup' %}" class="balance" data-wallet-balance>💰 {{ user.wallet_balance|floatformat:2 }} AED</a>
|
||||
<div class="auth-links">
|
||||
<a href="{% url 'wallet:topup' %}">Wallet</a>
|
||||
<a href="{% url 'authentication:logout' %}">Logout</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
### 8. Update Main URLs
|
||||
|
||||
Add wallet URLs to `netcop_hub/urls.py`:
|
||||
```python
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('auth/', include('authentication.urls')),
|
||||
path('wallet/', include('wallet.urls')), # Add this line
|
||||
# ... other URLs
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing Guide
|
||||
|
||||
### 1. Database Migration
|
||||
```bash
|
||||
python manage.py makemigrations
|
||||
python manage.py migrate
|
||||
```
|
||||
|
||||
### 2. Test with Stripe Test Cards
|
||||
- **Successful payment**: `4242 4242 4242 4242`
|
||||
- **Requires authentication**: `4000 0025 0000 3155`
|
||||
- **Declined card**: `4000 0000 0000 9995`
|
||||
|
||||
### 3. Testing Checklist
|
||||
- [ ] User can access wallet topup page
|
||||
- [ ] Amount selection buttons work
|
||||
- [ ] Card form validates properly
|
||||
- [ ] Payment processing shows loading states
|
||||
- [ ] Successful payments update balance immediately
|
||||
- [ ] Failed payments show error messages
|
||||
- [ ] Balance displays in navigation
|
||||
- [ ] Transaction history is recorded
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Advanced Features (Optional)
|
||||
|
||||
### Agent Integration
|
||||
Update agent views to check wallet balance:
|
||||
```python
|
||||
@login_required
|
||||
def use_agent(request, agent_slug):
|
||||
agent = get_object_or_404(BaseAgent, slug=agent_slug)
|
||||
|
||||
if not request.user.has_sufficient_balance(agent.price):
|
||||
return JsonResponse({
|
||||
'error': f'Insufficient balance. Need {agent.price} AED.',
|
||||
'redirect_url': reverse('wallet:topup')
|
||||
}, status=400)
|
||||
|
||||
# Deduct balance before processing
|
||||
request.user.deduct_balance(
|
||||
amount=agent.price,
|
||||
description=f"Used {agent.name} agent"
|
||||
)
|
||||
|
||||
# Process agent request...
|
||||
```
|
||||
|
||||
### Transaction History Page
|
||||
Create `templates/wallet/history.html`:
|
||||
```html
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<h2>Transaction History</h2>
|
||||
<p>Current Balance: <strong>{{ current_balance }} AED</strong></p>
|
||||
|
||||
<div class="transaction-list">
|
||||
{% for transaction in transactions %}
|
||||
<div class="transaction-item">
|
||||
<span class="amount">{{ transaction.amount }} AED</span>
|
||||
<span class="type">{{ transaction.get_type_display }}</span>
|
||||
<span class="date">{{ transaction.created_at|date:"M d, Y H:i" }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
### Common Issues:
|
||||
1. **Stripe keys not working**: Verify test keys are correct in `.env`
|
||||
2. **Payment not confirming**: Check browser console for JavaScript errors
|
||||
3. **Balance not updating**: Ensure user model has wallet_balance field
|
||||
4. **CSS not loading**: Run `python manage.py collectstatic`
|
||||
|
||||
### Debug Mode:
|
||||
Add to views.py for debugging:
|
||||
```python
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# In payment views:
|
||||
logger.info(f"Payment Intent created: {intent.id}")
|
||||
logger.info(f"User {request.user.id} balance updated: {request.user.wallet_balance}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Production Checklist
|
||||
|
||||
Before deploying to production:
|
||||
- [ ] Switch to live Stripe keys
|
||||
- [ ] Set up webhook endpoints (optional but recommended)
|
||||
- [ ] Enable HTTPS for secure payments
|
||||
- [ ] Set DEBUG=False in settings
|
||||
- [ ] Configure proper error logging
|
||||
- [ ] Test with real payment amounts
|
||||
- [ ] Set up monitoring for failed payments
|
||||
|
||||
---
|
||||
|
||||
This implementation provides a complete, professional wallet system with Stripe integration that works without webhooks for development and testing, while being easily extensible for production use.
|
||||
@ -42,6 +42,12 @@ DEBUG = config('DEBUG', default=True, cast=bool)
|
||||
|
||||
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='localhost,127.0.0.1,testserver,netcop.up.railway.app').split(',')
|
||||
|
||||
# Site URL configuration for emails
|
||||
if config('RAILWAY_ENVIRONMENT', default=''):
|
||||
SITE_URL = 'https://netcop.up.railway.app'
|
||||
else:
|
||||
SITE_URL = config('SITE_URL', default='http://localhost:8000')
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
@ -256,6 +262,16 @@ N8N_WEBHOOK_SOCIAL_ADS = config('N8N_WEBHOOK_SOCIAL_ADS', default='')
|
||||
# OpenWeather API
|
||||
OPENWEATHER_API_KEY = config('OPENWEATHER_API_KEY', default='')
|
||||
|
||||
# Email Configuration
|
||||
EMAIL_BACKEND = config('EMAIL_BACKEND', default='django.core.mail.backends.console.EmailBackend')
|
||||
EMAIL_HOST = config('EMAIL_HOST', default='smtp.gmail.com')
|
||||
EMAIL_PORT = config('EMAIL_PORT', default=587, cast=int)
|
||||
EMAIL_USE_TLS = config('EMAIL_USE_TLS', default=True, cast=bool)
|
||||
EMAIL_HOST_USER = config('EMAIL_HOST_USER', default='')
|
||||
EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD', default='')
|
||||
EMAIL_FILE_PATH = config('EMAIL_FILE_PATH', default='/tmp/app-messages')
|
||||
DEFAULT_FROM_EMAIL = config('DEFAULT_FROM_EMAIL', default='NetCop <noreply@netcop.com>')
|
||||
|
||||
# Security settings
|
||||
CSRF_TRUSTED_ORIGINS = [origin.strip() for origin in config('CSRF_TRUSTED_ORIGINS', default='').split(',') if origin.strip()]
|
||||
|
||||
|
||||
289
templates/authentication/forgot_password.html
Normal file
289
templates/authentication/forgot_password.html
Normal file
@ -0,0 +1,289 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Forgot Password{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
/* Override main-container for full-width sections */
|
||||
.main-container {
|
||||
max-width: none;
|
||||
padding: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* Forgot password page background */
|
||||
.forgot-password-page {
|
||||
min-height: calc(100vh - 80px); /* Account for header height */
|
||||
background: var(--gradient-hero);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: clamp(20px, 5vw, 40px);
|
||||
}
|
||||
|
||||
/* Forgot password container */
|
||||
.forgot-password-container {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: clamp(16px, 4vw, 24px);
|
||||
padding: clamp(32px, 8vw, 48px);
|
||||
box-shadow: 0 20px 60px rgba(30, 64, 175, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(20px);
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Decorative elements */
|
||||
.forgot-password-container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: var(--gradient-primary);
|
||||
border-top-right-radius: clamp(16px, 4vw, 24px);
|
||||
border-bottom-left-radius: clamp(16px, 4vw, 24px);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.forgot-password-container::after {
|
||||
content: '🔑';
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
right: 24px;
|
||||
font-size: 24px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Header section */
|
||||
.forgot-password-header {
|
||||
text-align: center;
|
||||
margin-bottom: clamp(24px, 6vw, 32px);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.forgot-password-title {
|
||||
font-size: clamp(24px, 6vw, 32px);
|
||||
font-weight: 700;
|
||||
color: var(--primary-blue);
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.forgot-password-subtitle {
|
||||
font-size: clamp(14px, 3.5vw, 16px);
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Form styles */
|
||||
.forgot-password-form {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: clamp(16px, 4vw, 20px);
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 8px;
|
||||
font-size: clamp(14px, 3.5vw, 16px);
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: clamp(14px, 4vw, 18px) clamp(16px, 4vw, 20px);
|
||||
border: 2px solid var(--border-medium);
|
||||
border-radius: clamp(8px, 2vw, 12px);
|
||||
font-size: clamp(14px, 3.5vw, 16px);
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
min-height: 48px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-blue);
|
||||
box-shadow: 0 0 0 3px rgba(64, 224, 208, 0.1);
|
||||
}
|
||||
|
||||
/* Reset button */
|
||||
.reset-btn {
|
||||
width: 100%;
|
||||
background: var(--gradient-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: clamp(16px, 4vw, 20px);
|
||||
border-radius: clamp(8px, 2vw, 12px);
|
||||
font-size: clamp(16px, 4vw, 18px);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
min-height: 56px;
|
||||
margin-bottom: clamp(20px, 5vw, 24px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.reset-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(30, 64, 175, 0.3);
|
||||
background: var(--gradient-success);
|
||||
}
|
||||
|
||||
.reset-btn:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 4px 12px rgba(30, 64, 175, 0.2);
|
||||
}
|
||||
|
||||
/* Messages */
|
||||
.messages {
|
||||
margin-bottom: clamp(16px, 4vw, 20px);
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 12px 16px;
|
||||
border-radius: clamp(8px, 2vw, 12px);
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
font-size: clamp(14px, 3.5vw, 16px);
|
||||
}
|
||||
|
||||
.message.success {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: var(--success-dark);
|
||||
border: 1px solid rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: var(--error-red);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.message.info {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: var(--primary-blue);
|
||||
border: 1px solid rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
/* Auth links */
|
||||
.forgot-password-page .forgot-password-container .auth-links {
|
||||
text-align: center;
|
||||
padding-top: clamp(16px, 4vw, 20px);
|
||||
border-top: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.forgot-password-page .forgot-password-container .auth-links p {
|
||||
margin: 8px 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: clamp(14px, 3.5vw, 16px);
|
||||
}
|
||||
|
||||
.forgot-password-page .forgot-password-container .auth-links a {
|
||||
color: var(--primary-blue);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.forgot-password-page .forgot-password-container .auth-links a:hover {
|
||||
color: var(--primary-blue);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 480px) {
|
||||
.forgot-password-page {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.forgot-password-container {
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="forgot-password-page">
|
||||
<div class="forgot-password-container">
|
||||
<!-- Header -->
|
||||
<div class="forgot-password-header">
|
||||
<h1 class="forgot-password-title">Forgot Password</h1>
|
||||
<p class="forgot-password-subtitle">Enter your email address and we'll send you a link to reset your password</p>
|
||||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
{% if messages %}
|
||||
<div class="messages">
|
||||
{% for message in messages %}
|
||||
<div class="message {{ message.tags }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Forgot Password Form -->
|
||||
<form method="post" class="forgot-password-form">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email" class="form-label">Email Address</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
class="form-input"
|
||||
placeholder="Enter your email address"
|
||||
required
|
||||
autocomplete="email"
|
||||
>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="reset-btn">
|
||||
📧 Send Reset Instructions
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Auth Links -->
|
||||
<div class="auth-links">
|
||||
<p>Remember your password? <a href="{% url 'authentication:login' %}">Sign in</a></p>
|
||||
<p>Don't have an account? <a href="{% url 'authentication:register' %}">Create account</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// Focus on email field when page loads
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const emailField = document.getElementById('email');
|
||||
if (emailField) {
|
||||
emailField.focus();
|
||||
}
|
||||
|
||||
// Add loading state to button on form submission
|
||||
const resetForm = document.querySelector('.forgot-password-form');
|
||||
const resetBtn = document.querySelector('.reset-btn');
|
||||
|
||||
if (resetForm && resetBtn) {
|
||||
resetForm.addEventListener('submit', function() {
|
||||
resetBtn.innerHTML = '⏳ Sending instructions...';
|
||||
resetBtn.disabled = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -311,6 +311,7 @@
|
||||
|
||||
<!-- Auth Links -->
|
||||
<div class="auth-links">
|
||||
<p><a href="{% url 'authentication:forgot_password' %}">Forgot your password?</a></p>
|
||||
<p>Don't have an account? <a href="{% url 'authentication:register' %}">Create account</a></p>
|
||||
</div>
|
||||
|
||||
|
||||
365
templates/authentication/reset_password.html
Normal file
365
templates/authentication/reset_password.html
Normal file
@ -0,0 +1,365 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Reset Password{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
/* Override main-container for full-width sections */
|
||||
.main-container {
|
||||
max-width: none;
|
||||
padding: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* Reset password page background */
|
||||
.reset-password-page {
|
||||
min-height: calc(100vh - 80px); /* Account for header height */
|
||||
background: var(--gradient-hero);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: clamp(20px, 5vw, 40px);
|
||||
}
|
||||
|
||||
/* Reset password container */
|
||||
.reset-password-container {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: clamp(16px, 4vw, 24px);
|
||||
padding: clamp(32px, 8vw, 48px);
|
||||
box-shadow: 0 20px 60px rgba(30, 64, 175, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(20px);
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Decorative elements */
|
||||
.reset-password-container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: var(--gradient-primary);
|
||||
border-top-right-radius: clamp(16px, 4vw, 24px);
|
||||
border-bottom-left-radius: clamp(16px, 4vw, 24px);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.reset-password-container::after {
|
||||
content: '🔒';
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
right: 24px;
|
||||
font-size: 24px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Header section */
|
||||
.reset-password-header {
|
||||
text-align: center;
|
||||
margin-bottom: clamp(24px, 6vw, 32px);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.reset-password-title {
|
||||
font-size: clamp(24px, 6vw, 32px);
|
||||
font-weight: 700;
|
||||
color: var(--primary-blue);
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.reset-password-subtitle {
|
||||
font-size: clamp(14px, 3.5vw, 16px);
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Form styles */
|
||||
.reset-password-form {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: clamp(16px, 4vw, 20px);
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 8px;
|
||||
font-size: clamp(14px, 3.5vw, 16px);
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: clamp(14px, 4vw, 18px) clamp(16px, 4vw, 20px);
|
||||
border: 2px solid var(--border-medium);
|
||||
border-radius: clamp(8px, 2vw, 12px);
|
||||
font-size: clamp(14px, 3.5vw, 16px);
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
min-height: 48px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-blue);
|
||||
box-shadow: 0 0 0 3px rgba(64, 224, 208, 0.1);
|
||||
}
|
||||
|
||||
/* Reset button */
|
||||
.reset-btn {
|
||||
width: 100%;
|
||||
background: var(--gradient-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: clamp(16px, 4vw, 20px);
|
||||
border-radius: clamp(8px, 2vw, 12px);
|
||||
font-size: clamp(16px, 4vw, 18px);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
min-height: 56px;
|
||||
margin-bottom: clamp(20px, 5vw, 24px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.reset-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(30, 64, 175, 0.3);
|
||||
background: var(--gradient-success);
|
||||
}
|
||||
|
||||
.reset-btn:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 4px 12px rgba(30, 64, 175, 0.2);
|
||||
}
|
||||
|
||||
/* Messages */
|
||||
.messages {
|
||||
margin-bottom: clamp(16px, 4vw, 20px);
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 12px 16px;
|
||||
border-radius: clamp(8px, 2vw, 12px);
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
font-size: clamp(14px, 3.5vw, 16px);
|
||||
}
|
||||
|
||||
.message.success {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: var(--success-dark);
|
||||
border: 1px solid rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: var(--error-red);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.message.info {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: var(--primary-blue);
|
||||
border: 1px solid rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
/* Auth links */
|
||||
.reset-password-page .reset-password-container .auth-links {
|
||||
text-align: center;
|
||||
padding-top: clamp(16px, 4vw, 20px);
|
||||
border-top: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.reset-password-page .reset-password-container .auth-links p {
|
||||
margin: 8px 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: clamp(14px, 3.5vw, 16px);
|
||||
}
|
||||
|
||||
.reset-password-page .reset-password-container .auth-links a {
|
||||
color: var(--primary-blue);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.reset-password-page .reset-password-container .auth-links a:hover {
|
||||
color: var(--primary-blue);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Password requirements */
|
||||
.password-requirements {
|
||||
background: rgba(64, 224, 208, 0.05);
|
||||
border-radius: clamp(8px, 2vw, 12px);
|
||||
padding: clamp(12px, 3vw, 16px);
|
||||
margin-top: 8px;
|
||||
border: 1px solid rgba(64, 224, 208, 0.2);
|
||||
}
|
||||
|
||||
.password-requirements h4 {
|
||||
font-size: clamp(13px, 3vw, 14px);
|
||||
font-weight: 600;
|
||||
color: var(--primary-blue);
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.password-requirements ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.password-requirements li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin: 4px 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: clamp(12px, 3vw, 13px);
|
||||
}
|
||||
|
||||
.password-requirements li::before {
|
||||
content: '•';
|
||||
color: var(--primary-blue);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 480px) {
|
||||
.reset-password-page {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.reset-password-container {
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="reset-password-page">
|
||||
<div class="reset-password-container">
|
||||
<!-- Header -->
|
||||
<div class="reset-password-header">
|
||||
<h1 class="reset-password-title">Reset Password</h1>
|
||||
<p class="reset-password-subtitle">Enter your new password below</p>
|
||||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
{% if messages %}
|
||||
<div class="messages">
|
||||
{% for message in messages %}
|
||||
<div class="message {{ message.tags }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Reset Password Form -->
|
||||
<form method="post" class="reset-password-form">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password1" class="form-label">New Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password1"
|
||||
name="password1"
|
||||
class="form-input"
|
||||
placeholder="Enter your new password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
minlength="8"
|
||||
>
|
||||
<div class="password-requirements">
|
||||
<h4>Password Requirements:</h4>
|
||||
<ul>
|
||||
<li>At least 8 characters long</li>
|
||||
<li>Mix of letters, numbers, and symbols recommended</li>
|
||||
<li>Should not be commonly used</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password2" class="form-label">Confirm New Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password2"
|
||||
name="password2"
|
||||
class="form-input"
|
||||
placeholder="Confirm your new password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
minlength="8"
|
||||
>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="reset-btn">
|
||||
🔒 Reset My Password
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Auth Links -->
|
||||
<div class="auth-links">
|
||||
<p>Remember your password? <a href="{% url 'authentication:login' %}">Sign in</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// Focus on password field when page loads
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const passwordField = document.getElementById('password1');
|
||||
if (passwordField) {
|
||||
passwordField.focus();
|
||||
}
|
||||
|
||||
// Add loading state to button on form submission
|
||||
const resetForm = document.querySelector('.reset-password-form');
|
||||
const resetBtn = document.querySelector('.reset-btn');
|
||||
|
||||
if (resetForm && resetBtn) {
|
||||
resetForm.addEventListener('submit', function() {
|
||||
resetBtn.innerHTML = '⏳ Resetting password...';
|
||||
resetBtn.disabled = true;
|
||||
});
|
||||
}
|
||||
|
||||
// Password confirmation validation
|
||||
const password1 = document.getElementById('password1');
|
||||
const password2 = document.getElementById('password2');
|
||||
|
||||
function validatePasswords() {
|
||||
if (password2.value && password1.value !== password2.value) {
|
||||
password2.setCustomValidity('Passwords do not match');
|
||||
} else {
|
||||
password2.setCustomValidity('');
|
||||
}
|
||||
}
|
||||
|
||||
if (password1 && password2) {
|
||||
password1.addEventListener('input', validatePasswords);
|
||||
password2.addEventListener('input', validatePasswords);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
45
test_email.py
Normal file
45
test_email.py
Normal file
@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import django
|
||||
from django.conf import settings
|
||||
|
||||
# Setup Django
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'netcop_hub.settings')
|
||||
django.setup()
|
||||
|
||||
from django.core.mail import send_mail
|
||||
from authentication.models import User
|
||||
|
||||
# Test email sending
|
||||
def test_email():
|
||||
print("Testing email configuration...")
|
||||
|
||||
# Check settings
|
||||
print(f"EMAIL_BACKEND: {settings.EMAIL_BACKEND}")
|
||||
print(f"EMAIL_FILE_PATH: {getattr(settings, 'EMAIL_FILE_PATH', 'Not set')}")
|
||||
|
||||
# Send test email
|
||||
try:
|
||||
send_mail(
|
||||
'Test Email',
|
||||
'This is a test email from Django.',
|
||||
settings.DEFAULT_FROM_EMAIL,
|
||||
['test@example.com'],
|
||||
fail_silently=False,
|
||||
)
|
||||
print("✅ Email sent successfully!")
|
||||
print("📁 Check /tmp/app-messages/ for the email file")
|
||||
|
||||
# List files in email directory
|
||||
import glob
|
||||
files = glob.glob('/tmp/app-messages/*')
|
||||
if files:
|
||||
print(f"📧 Email files: {files}")
|
||||
else:
|
||||
print("⚠️ No email files found")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error sending email: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_email()
|
||||
79
test_gmail_reset.py
Normal file
79
test_gmail_reset.py
Normal file
@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import django
|
||||
from django.conf import settings
|
||||
|
||||
# Setup Django
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'netcop_hub.settings')
|
||||
django.setup()
|
||||
|
||||
from django.core.mail import send_mail
|
||||
from authentication.models import User, PasswordResetToken
|
||||
import traceback
|
||||
|
||||
def test_gmail_reset():
|
||||
print("🔍 Testing forgot password with Gmail address")
|
||||
print("=" * 50)
|
||||
|
||||
gmail_address = settings.EMAIL_HOST_USER # thecyberlearn@gmail.com
|
||||
|
||||
# Create or update user with Gmail address
|
||||
user, created = User.objects.update_or_create(
|
||||
email=gmail_address,
|
||||
defaults={
|
||||
'username': 'gmail_user',
|
||||
'is_active': True,
|
||||
}
|
||||
)
|
||||
|
||||
if created:
|
||||
print(f"✅ Created new user: {user.email}")
|
||||
user.set_password('temppassword123')
|
||||
user.save()
|
||||
else:
|
||||
print(f"✅ Using existing user: {user.email}")
|
||||
|
||||
# Create password reset token
|
||||
reset_token = PasswordResetToken.objects.create(user=user)
|
||||
|
||||
# Build reset URL
|
||||
reset_url = f"http://localhost:8000/auth/reset-password/{reset_token.token}/"
|
||||
|
||||
# Send reset email
|
||||
subject = 'NetCop Password Reset Request'
|
||||
message = f'''
|
||||
Hello {user.username},
|
||||
|
||||
You requested a password reset for your NetCop account.
|
||||
|
||||
Click the link below to reset your password:
|
||||
{reset_url}
|
||||
|
||||
This link will expire in 1 hour.
|
||||
|
||||
If you didn't request this reset, please ignore this email.
|
||||
|
||||
Best regards,
|
||||
NetCop Team
|
||||
'''
|
||||
|
||||
try:
|
||||
send_mail(
|
||||
subject,
|
||||
message,
|
||||
settings.DEFAULT_FROM_EMAIL,
|
||||
[user.email],
|
||||
fail_silently=False,
|
||||
)
|
||||
|
||||
print(f"✅ Password reset email sent to: {user.email}")
|
||||
print(f"🔗 Reset URL: {reset_url}")
|
||||
print("\n📧 CHECK YOUR GMAIL INBOX!")
|
||||
print("Note: Check spam folder if not in inbox")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to send email: {e}")
|
||||
print(f"Error details: {traceback.format_exc()}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_gmail_reset()
|
||||
77
test_user_reset.py
Normal file
77
test_user_reset.py
Normal file
@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import django
|
||||
from django.conf import settings
|
||||
|
||||
# Setup Django
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'netcop_hub.settings')
|
||||
django.setup()
|
||||
|
||||
from django.core.mail import send_mail
|
||||
from authentication.models import User, PasswordResetToken
|
||||
import traceback
|
||||
|
||||
def test_user_reset():
|
||||
print("🔍 Testing forgot password with user: amitrana01@gmail.com")
|
||||
print("=" * 60)
|
||||
|
||||
target_email = "amitrana01@gmail.com"
|
||||
|
||||
# Check if user exists
|
||||
try:
|
||||
user = User.objects.get(email=target_email)
|
||||
print(f"✅ Found user: {user.username} ({user.email})")
|
||||
except User.DoesNotExist:
|
||||
print(f"❌ User with email {target_email} not found")
|
||||
return False
|
||||
|
||||
# Create password reset token
|
||||
reset_token = PasswordResetToken.objects.create(user=user)
|
||||
print(f"✅ Password reset token created: {reset_token.token}")
|
||||
|
||||
# Build reset URL
|
||||
reset_url = f"http://localhost:8000/auth/reset-password/{reset_token.token}/"
|
||||
|
||||
# Send reset email (same as in the actual view)
|
||||
subject = 'Password Reset Request'
|
||||
message = f'''
|
||||
Hello {user.username},
|
||||
|
||||
You requested a password reset for your NetCop account.
|
||||
|
||||
Click the link below to reset your password:
|
||||
{reset_url}
|
||||
|
||||
This link will expire in 1 hour.
|
||||
|
||||
If you didn't request this reset, please ignore this email.
|
||||
|
||||
Best regards,
|
||||
NetCop Team
|
||||
'''
|
||||
|
||||
try:
|
||||
send_mail(
|
||||
subject,
|
||||
message,
|
||||
settings.DEFAULT_FROM_EMAIL,
|
||||
[user.email],
|
||||
fail_silently=False,
|
||||
)
|
||||
|
||||
print(f"✅ Password reset email sent successfully!")
|
||||
print(f"📧 Email sent to: {user.email}")
|
||||
print(f"🔗 Reset URL: {reset_url}")
|
||||
print(f"⏰ Token expires in: 1 hour")
|
||||
print("\n📧 CHECK THE INBOX FOR: amitrana01@gmail.com")
|
||||
print("Note: Check spam folder if not in inbox")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to send email: {e}")
|
||||
print(f"Error details: {traceback.format_exc()}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_user_reset()
|
||||
72
test_web_form.py
Normal file
72
test_web_form.py
Normal file
@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python
|
||||
import requests
|
||||
import re
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
def test_web_form():
|
||||
print("🔍 Testing forgot password web form submission")
|
||||
print("=" * 50)
|
||||
|
||||
base_url = "http://localhost:8000"
|
||||
session = requests.Session()
|
||||
|
||||
# Step 1: Get the forgot password page
|
||||
print("1. Getting forgot password page...")
|
||||
response = session.get(f"{base_url}/auth/forgot-password/")
|
||||
|
||||
if response.status_code != 200:
|
||||
print(f"❌ Failed to load forgot password page: {response.status_code}")
|
||||
return False
|
||||
|
||||
# Step 2: Extract CSRF token
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
csrf_token = soup.find('input', {'name': 'csrfmiddlewaretoken'})
|
||||
|
||||
if not csrf_token:
|
||||
print("❌ CSRF token not found")
|
||||
return False
|
||||
|
||||
csrf_value = csrf_token.get('value')
|
||||
print(f"✅ CSRF token extracted: {csrf_value[:20]}...")
|
||||
|
||||
# Step 3: Submit the form
|
||||
print("2. Submitting forgot password form...")
|
||||
form_data = {
|
||||
'csrfmiddlewaretoken': csrf_value,
|
||||
'email': 'amitrana01@gmail.com'
|
||||
}
|
||||
|
||||
response = session.post(f"{base_url}/auth/forgot-password/", data=form_data)
|
||||
|
||||
if response.status_code != 200:
|
||||
print(f"❌ Form submission failed: {response.status_code}")
|
||||
return False
|
||||
|
||||
# Step 4: Check for success message
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
messages = soup.find_all('div', class_='message')
|
||||
|
||||
if messages:
|
||||
for msg in messages:
|
||||
print(f"📧 Message: {msg.get_text().strip()}")
|
||||
if 'sent' in msg.get_text().lower() or 'instructions' in msg.get_text().lower():
|
||||
print("✅ Success message found!")
|
||||
return True
|
||||
|
||||
print("⚠️ No success message found in response")
|
||||
|
||||
# Check if there are any error messages
|
||||
error_messages = soup.find_all('div', class_='message error')
|
||||
if error_messages:
|
||||
for msg in error_messages:
|
||||
print(f"❌ Error: {msg.get_text().strip()}")
|
||||
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
test_web_form()
|
||||
except Exception as e:
|
||||
print(f"❌ Test failed with error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
Loading…
Reference in New Issue
Block a user