Fixing PLans page

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-08 14:12:08 +00:00
parent da3b45d1c7
commit 144e955b92
24 changed files with 1992 additions and 1105 deletions

View File

@@ -0,0 +1,82 @@
"""
Management command to clean up expired and orphaned sessions
Helps prevent session contamination and reduces DB bloat
"""
from django.core.management.base import BaseCommand
from django.contrib.sessions.models import Session
from django.contrib.auth import get_user_model
from datetime import datetime, timedelta
User = get_user_model()
class Command(BaseCommand):
help = 'Clean up expired sessions and detect session contamination'
def add_arguments(self, parser):
parser.add_argument(
'--dry-run',
action='store_true',
help='Show what would be deleted without actually deleting',
)
parser.add_argument(
'--days',
type=int,
default=7,
help='Delete sessions older than X days (default: 7)',
)
def handle(self, *args, **options):
dry_run = options['dry_run']
days = options['days']
cutoff_date = datetime.now() - timedelta(days=days)
# Get all sessions
all_sessions = Session.objects.all()
expired_sessions = Session.objects.filter(expire_date__lt=datetime.now())
old_sessions = Session.objects.filter(expire_date__lt=cutoff_date)
self.stdout.write(f"\n📊 Session Statistics:")
self.stdout.write(f" Total sessions: {all_sessions.count()}")
self.stdout.write(f" Expired sessions: {expired_sessions.count()}")
self.stdout.write(f" Sessions older than {days} days: {old_sessions.count()}")
# Count sessions by user
user_sessions = {}
for session in all_sessions:
try:
data = session.get_decoded()
user_id = data.get('_auth_user_id')
if user_id:
user = User.objects.get(id=user_id)
key = f"{user.username} ({user.account.slug if user.account else 'no-account'})"
user_sessions[key] = user_sessions.get(key, 0) + 1
except:
pass
if user_sessions:
self.stdout.write(f"\n📈 Active sessions by user:")
for user_key, count in sorted(user_sessions.items(), key=lambda x: x[1], reverse=True)[:10]:
indicator = "⚠️ " if count > 20 else " "
self.stdout.write(f"{indicator}{user_key}: {count} sessions")
# Delete expired sessions
if expired_sessions.exists():
if dry_run:
self.stdout.write(self.style.WARNING(f"\n[DRY RUN] Would delete {expired_sessions.count()} expired sessions"))
else:
count = expired_sessions.delete()[0]
self.stdout.write(self.style.SUCCESS(f"\n✓ Deleted {count} expired sessions"))
else:
self.stdout.write(f"\n✓ No expired sessions to clean")
# Detect potential contamination
warnings = []
for user_key, count in user_sessions.items():
if count > 50:
warnings.append(f"User '{user_key}' has {count} active sessions (potential proliferation)")
if warnings:
self.stdout.write(self.style.WARNING(f"\n⚠️ Contamination Warnings:"))
for warning in warnings:
self.stdout.write(self.style.WARNING(f" {warning}"))
self.stdout.write(f"\n💡 Consider running: python manage.py clearsessions")

View File

@@ -35,13 +35,11 @@ class AccountContextMiddleware(MiddlewareMixin):
# This ensures changes to account/plan are reflected immediately without re-login
try:
from .models import User as UserModel
# Refresh user from DB with account and plan relationships to get latest data
# This is important so account/plan changes are reflected immediately
user = UserModel.objects.select_related('account', 'account__plan').get(id=request.user.id)
# Update request.user with fresh data
request.user = user
# Get account from refreshed user
user_account = getattr(user, 'account', None)
# CRITICAL FIX: Never mutate request.user - it causes session contamination
# Instead, just read the current user and set request.account
# Django's session middleware already sets request.user correctly
user = request.user # Use the user from session, don't overwrite it
validation_error = self._validate_account_and_plan(request, user)
if validation_error:
return validation_error

View File

@@ -0,0 +1,26 @@
# Generated by Django 5.2.8 on 2025-12-08 13:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('igny8_core_auth', '0007_add_payment_method_fields'),
]
operations = [
migrations.RemoveIndex(
model_name='account',
name='auth_acc_payment_idx',
),
migrations.RemoveIndex(
model_name='subscription',
name='auth_sub_payment_idx',
),
migrations.AddField(
model_name='plan',
name='is_internal',
field=models.BooleanField(default=False, help_text='Internal-only plan (Free/Internal) - hidden from public plan listings'),
),
]

View File

@@ -0,0 +1,36 @@
# Generated manually
from django.db import migrations, models
import django.core.validators
class Migration(migrations.Migration):
dependencies = [
('igny8_core_auth', '0008_add_plan_is_internal'),
]
operations = [
migrations.AddField(
model_name='plan',
name='annual_discount_percent',
field=models.DecimalField(
decimal_places=2,
default=15.0,
help_text='Annual subscription discount percentage (default 15%)',
max_digits=5,
validators=[
django.core.validators.MinValueValidator(0),
django.core.validators.MaxValueValidator(100)
]
),
),
migrations.AddField(
model_name='plan',
name='is_featured',
field=models.BooleanField(
default=False,
help_text='Highlight this plan as popular/recommended'
),
),
]

View File

@@ -154,8 +154,17 @@ class Plan(models.Model):
slug = models.SlugField(unique=True, max_length=255)
price = models.DecimalField(max_digits=10, decimal_places=2)
billing_cycle = models.CharField(max_length=20, choices=BILLING_CYCLE_CHOICES, default='monthly')
annual_discount_percent = models.DecimalField(
max_digits=5,
decimal_places=2,
default=15.00,
validators=[MinValueValidator(0), MaxValueValidator(100)],
help_text="Annual subscription discount percentage (default 15%)"
)
is_featured = models.BooleanField(default=False, help_text="Highlight this plan as popular/recommended")
features = models.JSONField(default=list, blank=True, help_text="Plan features as JSON array (e.g., ['ai_writer', 'image_gen', 'auto_publish'])")
is_active = models.BooleanField(default=True)
is_internal = models.BooleanField(default=False, help_text="Internal-only plan (Free/Internal) - hidden from public plan listings")
created_at = models.DateTimeField(auto_now_add=True)
# Account Management Limits (kept - not operation limits)

View File

@@ -10,7 +10,8 @@ class PlanSerializer(serializers.ModelSerializer):
class Meta:
model = Plan
fields = [
'id', 'name', 'slug', 'price', 'billing_cycle', 'features', 'is_active',
'id', 'name', 'slug', 'price', 'billing_cycle', 'annual_discount_percent',
'is_featured', 'features', 'is_active',
'max_users', 'max_sites', 'max_industries', 'max_author_profiles',
'included_credits', 'extra_credit_price', 'allow_credit_topup',
'auto_credit_topup_threshold', 'auto_credit_topup_amount',

View File

@@ -440,9 +440,10 @@ class SiteUserAccessViewSet(AccountModelViewSet):
class PlanViewSet(viewsets.ReadOnlyModelViewSet):
"""
ViewSet for listing active subscription plans.
Excludes internal-only plans (Free/Internal) from public listings.
Unified API Standard v1.0 compliant
"""
queryset = Plan.objects.filter(is_active=True)
queryset = Plan.objects.filter(is_active=True, is_internal=False)
serializer_class = PlanSerializer
permission_classes = [permissions.AllowAny]
pagination_class = CustomPageNumberPagination
@@ -450,6 +451,16 @@ class PlanViewSet(viewsets.ReadOnlyModelViewSet):
throttle_scope = None
throttle_classes: list = []
def list(self, request, *args, **kwargs):
"""Override list to return paginated response with unified format"""
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return success_response(data={'results': serializer.data}, request=request)
def retrieve(self, request, *args, **kwargs):
"""Override retrieve to return unified format"""
try: