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:
Claude 2025-07-16 13:11:06 +05:30
parent 49b5b2a6aa
commit d36cf9a79d
15 changed files with 1910 additions and 2 deletions

13
.env.email.example Normal file
View 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

View 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"],
},
),
]

View File

@ -1,6 +1,9 @@
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.db import models from django.db import models
from decimal import Decimal from decimal import Decimal
import uuid
from django.utils import timezone
from datetime import timedelta
class User(AbstractUser): class User(AbstractUser):
@ -55,3 +58,29 @@ class User(AbstractUser):
description=description, description=description,
stripe_session_id=stripe_session_id 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}"

View File

@ -8,4 +8,6 @@ urlpatterns = [
path('register/', views.register_view, name='register'), path('register/', views.register_view, name='register'),
path('logout/', views.logout_view, name='logout'), path('logout/', views.logout_view, name='logout'),
path('profile/', views.profile_view, name='profile'), 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'),
] ]

View File

@ -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 import login, authenticate, logout
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.forms import UserCreationForm from django.contrib.auth.forms import UserCreationForm
from django.http import JsonResponse 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): def login_view(request):
@ -105,3 +108,89 @@ def profile_view(request):
} }
return render(request, 'authentication/profile.html', context) 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
View 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()

View 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.

View File

@ -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(',') 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 # Application definition
@ -256,6 +262,16 @@ N8N_WEBHOOK_SOCIAL_ADS = config('N8N_WEBHOOK_SOCIAL_ADS', default='')
# OpenWeather API # OpenWeather API
OPENWEATHER_API_KEY = config('OPENWEATHER_API_KEY', default='') 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 # Security settings
CSRF_TRUSTED_ORIGINS = [origin.strip() for origin in config('CSRF_TRUSTED_ORIGINS', default='').split(',') if origin.strip()] CSRF_TRUSTED_ORIGINS = [origin.strip() for origin in config('CSRF_TRUSTED_ORIGINS', default='').split(',') if origin.strip()]

View 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 %}

View File

@ -311,6 +311,7 @@
<!-- Auth Links --> <!-- Auth Links -->
<div class="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> <p>Don't have an account? <a href="{% url 'authentication:register' %}">Create account</a></p>
</div> </div>

View 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
View 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
View 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
View 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
View 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()