adsasdasd

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-08 11:51:00 +00:00
parent affa783a4f
commit da3b45d1c7
14 changed files with 1763 additions and 19 deletions

View File

@@ -21,6 +21,21 @@ class AccountModelViewSet(viewsets.ModelViewSet):
user = getattr(self.request, 'user', None)
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
# Bypass filtering for superusers - they can see everything
if getattr(user, 'is_superuser', False):
return queryset
# Bypass filtering for developers
if hasattr(user, 'role') and user.role == 'developer':
return queryset
# Bypass filtering for system account users
try:
if hasattr(user, 'is_system_account_user') and user.is_system_account_user():
return queryset
except Exception:
pass
try:
account = getattr(self.request, 'account', None)
if not account and hasattr(self.request, 'user') and self.request.user and hasattr(self.request.user, 'is_authenticated') and self.request.user.is_authenticated:
@@ -239,6 +254,29 @@ class SiteSectorModelViewSet(AccountModelViewSet):
# Check if user is authenticated and is a proper User instance (not AnonymousUser)
if user and hasattr(user, 'is_authenticated') and user.is_authenticated and hasattr(user, 'get_accessible_sites'):
# Bypass site filtering for superusers and developers
# They already got unfiltered queryset from parent AccountModelViewSet
if getattr(user, 'is_superuser', False) or (hasattr(user, 'role') and user.role == 'developer'):
# No site filtering for superuser/developer
# But still apply query param filters if provided
try:
query_params = getattr(self.request, 'query_params', None)
if query_params is None:
query_params = getattr(self.request, 'GET', {})
site_id = query_params.get('site_id') or query_params.get('site')
except AttributeError:
site_id = None
if site_id:
try:
site_id_int = int(site_id) if site_id else None
if site_id_int:
queryset = queryset.filter(site_id=site_id_int)
except (ValueError, TypeError):
pass
return queryset
try:
# Get user's accessible sites
accessible_sites = user.get_accessible_sites()

View File

@@ -26,11 +26,27 @@ class HasTenantAccess(permissions.BasePermission):
"""
Permission class that requires user to belong to the tenant/account
Ensures tenant isolation
Superusers, developers, and system account users bypass this check.
"""
def has_permission(self, request, view):
if not request.user or not request.user.is_authenticated:
return False
# Bypass for superusers
if getattr(request.user, 'is_superuser', False):
return True
# Bypass for developers
if hasattr(request.user, 'role') and request.user.role == 'developer':
return True
# Bypass for system account users
try:
if hasattr(request.user, 'is_system_account_user') and request.user.is_system_account_user():
return True
except Exception:
pass
# Get account from request (set by middleware)
account = getattr(request, 'account', None)
@@ -58,11 +74,20 @@ class IsViewerOrAbove(permissions.BasePermission):
"""
Permission class that requires viewer, editor, admin, or owner role
For read-only operations
Superusers and developers bypass this check.
"""
def has_permission(self, request, view):
if not request.user or not request.user.is_authenticated:
return False
# Bypass for superusers
if getattr(request.user, 'is_superuser', False):
return True
# Bypass for developers
if hasattr(request.user, 'role') and request.user.role == 'developer':
return True
# Check user role
if hasattr(request.user, 'role'):
role = request.user.role
@@ -77,11 +102,20 @@ class IsEditorOrAbove(permissions.BasePermission):
"""
Permission class that requires editor, admin, or owner role
For content operations
Superusers and developers bypass this check.
"""
def has_permission(self, request, view):
if not request.user or not request.user.is_authenticated:
return False
# Bypass for superusers
if getattr(request.user, 'is_superuser', False):
return True
# Bypass for developers
if hasattr(request.user, 'role') and request.user.role == 'developer':
return True
# Check user role
if hasattr(request.user, 'role'):
role = request.user.role
@@ -96,11 +130,20 @@ class IsAdminOrOwner(permissions.BasePermission):
"""
Permission class that requires admin or owner role only
For settings, keys, billing operations
Superusers and developers bypass this check.
"""
def has_permission(self, request, view):
if not request.user or not request.user.is_authenticated:
return False
# Bypass for superusers
if getattr(request.user, 'is_superuser', False):
return True
# Bypass for developers
if hasattr(request.user, 'role') and request.user.role == 'developer':
return True
# Check user role
if hasattr(request.user, 'role'):
role = request.user.role

View File

@@ -22,9 +22,22 @@ class DebugScopedRateThrottle(ScopedRateThrottle):
def allow_request(self, request, view):
"""
Check if request should be throttled.
Only bypasses for DEBUG mode or public requests.
Enforces per-account throttling for all authenticated users.
Bypasses for: DEBUG mode, superusers, developers, system accounts, and public requests.
Enforces per-account throttling for regular users.
"""
# Bypass for superusers and developers
if request.user and hasattr(request.user, 'is_authenticated') and request.user.is_authenticated:
if getattr(request.user, 'is_superuser', False):
return True
if hasattr(request.user, 'role') and request.user.role == 'developer':
return True
# Bypass for system account users
try:
if hasattr(request.user, 'is_system_account_user') and request.user.is_system_account_user():
return True
except Exception:
pass
# Check if throttling should be bypassed
debug_bypass = getattr(settings, 'DEBUG', False)
env_bypass = getattr(settings, 'IGNY8_DEBUG_THROTTLE', False)

View File

@@ -31,14 +31,6 @@ class AccountContextMiddleware(MiddlewareMixin):
# First, try to get user from Django session (cookie-based auth)
# This handles cases where frontend uses credentials: 'include' with session cookies
if hasattr(request, 'user') and request.user and request.user.is_authenticated:
# Block superuser access via session on non-admin routes (JWT required)
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
if request.user.is_superuser and not auth_header.startswith('Bearer '):
logout(request)
return JsonResponse(
{'success': False, 'error': 'Session authentication not allowed for API. Use JWT.'},
status=status.HTTP_403_FORBIDDEN,
)
# User is authenticated via session - refresh from DB to get latest account/plan data
# This ensures changes to account/plan are reflected immediately without re-login
try:
@@ -141,7 +133,23 @@ class AccountContextMiddleware(MiddlewareMixin):
"""
Ensure the authenticated user has an account and an active plan.
Uses shared validation helper for consistency.
Bypasses validation for superusers, developers, and system accounts.
"""
# Bypass validation for superusers
if getattr(user, 'is_superuser', False):
return None
# Bypass validation for developers
if hasattr(user, 'role') and user.role == 'developer':
return None
# Bypass validation for system account users
try:
if hasattr(user, 'is_system_account_user') and user.is_system_account_user():
return None
except Exception:
pass
from .utils import validate_account_and_plan
is_valid, error_message, http_status = validate_account_and_plan(user)

View File

@@ -135,6 +135,7 @@ def validate_account_and_plan(user_or_account):
"""
Validate account exists and has active plan.
Allows trial, active, and pending_payment statuses.
Bypasses validation for superusers, developers, and system accounts.
Args:
user_or_account: User or Account instance
@@ -145,6 +146,22 @@ def validate_account_and_plan(user_or_account):
from rest_framework import status
from .models import User, Account
# Bypass validation for superusers
if isinstance(user_or_account, User):
if getattr(user_or_account, 'is_superuser', False):
return (True, None, None)
# Bypass validation for developers
if hasattr(user_or_account, 'role') and user_or_account.role == 'developer':
return (True, None, None)
# Bypass validation for system account users
try:
if hasattr(user_or_account, 'is_system_account_user') and user_or_account.is_system_account_user():
return (True, None, None)
except Exception:
pass
# Extract account from user or use directly
if isinstance(user_or_account, User):
try:
@@ -153,6 +170,12 @@ def validate_account_and_plan(user_or_account):
account = None
elif isinstance(user_or_account, Account):
account = user_or_account
# Check if account is a system account
try:
if hasattr(account, 'is_system_account') and account.is_system_account():
return (True, None, None)
except Exception:
pass
else:
return (False, 'Invalid object type', status.HTTP_400_BAD_REQUEST)

View File

@@ -1,7 +1,13 @@
"""Billing routes including bank transfer confirmation and credit endpoints."""
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import BillingViewSet
from .views import (
BillingViewSet,
InvoiceViewSet,
PaymentViewSet,
CreditPackageViewSet,
AccountPaymentMethodViewSet,
)
from igny8_core.modules.billing.views import (
CreditBalanceViewSet,
CreditUsageViewSet,
@@ -14,6 +20,11 @@ router.register(r'admin', BillingViewSet, basename='billing-admin')
router.register(r'credits/balance', CreditBalanceViewSet, basename='credit-balance')
router.register(r'credits/usage', CreditUsageViewSet, basename='credit-usage')
router.register(r'credits/transactions', CreditTransactionViewSet, basename='credit-transactions')
# User-facing billing endpoints
router.register(r'invoices', InvoiceViewSet, basename='invoices')
router.register(r'payments', PaymentViewSet, basename='payments')
router.register(r'credit-packages', CreditPackageViewSet, basename='credit-packages')
router.register(r'payment-methods', AccountPaymentMethodViewSet, basename='payment-methods')
urlpatterns = [
path('', include(router.urls)),

View File

@@ -3,14 +3,19 @@ Billing Views - Payment confirmation and management
"""
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from django.db import transaction
from django.utils import timezone
from django.http import HttpResponse
from datetime import timedelta
from igny8_core.api.response import success_response, error_response
from igny8_core.api.permissions import IsAdminOrOwner
from igny8_core.api.response import success_response, error_response, paginated_response
from igny8_core.api.permissions import IsAdminOrOwner, IsAuthenticatedAndActive, HasTenantAccess
from igny8_core.api.base import AccountModelViewSet
from igny8_core.api.pagination import CustomPageNumberPagination
from igny8_core.auth.models import Account, Subscription
from igny8_core.business.billing.services.credit_service import CreditService
from igny8_core.business.billing.models import CreditTransaction
from igny8_core.business.billing.services.invoice_service import InvoiceService
from igny8_core.business.billing.models import CreditTransaction, Invoice, Payment, CreditPackage, AccountPaymentMethod
import logging
logger = logging.getLogger(__name__)
@@ -166,3 +171,242 @@ class BillingViewSet(viewsets.GenericViewSet):
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
request=request
)
class InvoiceViewSet(AccountModelViewSet):
"""ViewSet for user-facing invoices"""
queryset = Invoice.objects.all().select_related('account')
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
pagination_class = CustomPageNumberPagination
def get_queryset(self):
"""Filter invoices by account"""
queryset = super().get_queryset()
if hasattr(self.request, 'account') and self.request.account:
queryset = queryset.filter(account=self.request.account)
return queryset.order_by('-invoice_date', '-created_at')
def list(self, request):
"""List invoices for current account"""
queryset = self.get_queryset()
# Filter by status if provided
status_param = request.query_params.get('status')
if status_param:
queryset = queryset.filter(status=status_param)
paginator = self.pagination_class()
page = paginator.paginate_queryset(queryset, request)
# Serialize invoice data
results = []
for invoice in page:
results.append({
'id': invoice.id,
'invoice_number': invoice.invoice_number,
'status': invoice.status,
'total_amount': str(invoice.total),
'subtotal': str(invoice.subtotal),
'tax_amount': str(invoice.tax),
'currency': invoice.currency,
'invoice_date': invoice.invoice_date.isoformat(),
'due_date': invoice.due_date.isoformat(),
'paid_at': invoice.paid_at.isoformat() if invoice.paid_at else None,
'line_items': invoice.line_items,
'billing_email': invoice.billing_email,
'notes': invoice.notes,
'created_at': invoice.created_at.isoformat(),
})
paginated_data = paginator.get_paginated_response({'results': results}).data
return paginated_response(paginated_data, request=request)
def retrieve(self, request, pk=None):
"""Get invoice detail"""
try:
invoice = self.get_queryset().get(pk=pk)
data = {
'id': invoice.id,
'invoice_number': invoice.invoice_number,
'status': invoice.status,
'total_amount': str(invoice.total),
'subtotal': str(invoice.subtotal),
'tax_amount': str(invoice.tax),
'currency': invoice.currency,
'invoice_date': invoice.invoice_date.isoformat(),
'due_date': invoice.due_date.isoformat(),
'paid_at': invoice.paid_at.isoformat() if invoice.paid_at else None,
'line_items': invoice.line_items,
'billing_email': invoice.billing_email,
'notes': invoice.notes,
'created_at': invoice.created_at.isoformat(),
}
return success_response(data=data, request=request)
except Invoice.DoesNotExist:
return error_response(error='Invoice not found', status_code=404, request=request)
@action(detail=True, methods=['get'])
def download_pdf(self, request, pk=None):
"""Download invoice PDF"""
try:
invoice = self.get_queryset().get(pk=pk)
pdf_bytes = InvoiceService.generate_pdf(invoice)
response = HttpResponse(pdf_bytes, content_type='application/pdf')
response['Content-Disposition'] = f'attachment; filename="invoice-{invoice.invoice_number}.pdf"'
return response
except Invoice.DoesNotExist:
return error_response(error='Invoice not found', status_code=404, request=request)
class PaymentViewSet(AccountModelViewSet):
"""ViewSet for user-facing payments"""
queryset = Payment.objects.all().select_related('account', 'invoice')
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
pagination_class = CustomPageNumberPagination
def get_queryset(self):
"""Filter payments by account"""
queryset = super().get_queryset()
if hasattr(self.request, 'account') and self.request.account:
queryset = queryset.filter(account=self.request.account)
return queryset.order_by('-created_at')
def list(self, request):
"""List payments for current account"""
queryset = self.get_queryset()
# Filter by status if provided
status_param = request.query_params.get('status')
if status_param:
queryset = queryset.filter(status=status_param)
# Filter by invoice if provided
invoice_id = request.query_params.get('invoice_id')
if invoice_id:
queryset = queryset.filter(invoice_id=invoice_id)
paginator = self.pagination_class()
page = paginator.paginate_queryset(queryset, request)
# Serialize payment data
results = []
for payment in page:
results.append({
'id': payment.id,
'invoice_id': payment.invoice_id,
'invoice_number': payment.invoice.invoice_number if payment.invoice else None,
'amount': str(payment.amount),
'currency': payment.currency,
'status': payment.status,
'payment_method': payment.payment_method,
'created_at': payment.created_at.isoformat(),
'processed_at': payment.processed_at.isoformat() if payment.processed_at else None,
'manual_reference': payment.manual_reference,
'manual_notes': payment.manual_notes,
})
paginated_data = paginator.get_paginated_response({'results': results}).data
return paginated_response(paginated_data, request=request)
@action(detail=False, methods=['post'])
def manual(self, request):
"""Submit manual payment for approval"""
invoice_id = request.data.get('invoice_id')
amount = request.data.get('amount')
payment_method = request.data.get('payment_method', 'bank_transfer')
reference = request.data.get('reference', '')
notes = request.data.get('notes', '')
if not amount:
return error_response(error='Amount is required', status_code=400, request=request)
try:
account = request.account
invoice = None
if invoice_id:
invoice = Invoice.objects.get(id=invoice_id, account=account)
payment = Payment.objects.create(
account=account,
invoice=invoice,
amount=amount,
currency='USD',
payment_method=payment_method,
status='pending_approval',
manual_reference=reference,
manual_notes=notes,
)
return success_response(
data={'id': payment.id, 'status': payment.status},
message='Manual payment submitted for approval',
status_code=201,
request=request
)
except Invoice.DoesNotExist:
return error_response(error='Invoice not found', status_code=404, request=request)
class CreditPackageViewSet(viewsets.ReadOnlyModelViewSet):
"""ViewSet for credit packages (read-only for users)"""
queryset = CreditPackage.objects.filter(is_active=True).order_by('sort_order')
permission_classes = [IsAuthenticatedAndActive]
pagination_class = CustomPageNumberPagination
def list(self, request):
"""List available credit packages"""
queryset = self.get_queryset()
paginator = self.pagination_class()
page = paginator.paginate_queryset(queryset, request)
results = []
for package in page:
results.append({
'id': package.id,
'name': package.name,
'slug': package.slug,
'credits': package.credits,
'price': str(package.price),
'discount_percentage': package.discount_percentage,
'is_featured': package.is_featured,
'description': package.description,
'display_order': package.sort_order,
})
paginated_data = paginator.get_paginated_response({'results': results}).data
return paginated_response(paginated_data, request=request)
class AccountPaymentMethodViewSet(AccountModelViewSet):
"""ViewSet for account payment methods"""
queryset = AccountPaymentMethod.objects.all()
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
pagination_class = CustomPageNumberPagination
def get_queryset(self):
"""Filter payment methods by account"""
queryset = super().get_queryset()
if hasattr(self.request, 'account') and self.request.account:
queryset = queryset.filter(account=self.request.account)
return queryset.order_by('-is_default', 'type')
def list(self, request):
"""List payment methods for current account"""
queryset = self.get_queryset()
paginator = self.pagination_class()
page = paginator.paginate_queryset(queryset, request)
results = []
for method in page:
results.append({
'id': str(method.id),
'type': method.type,
'display_name': method.display_name,
'is_default': method.is_default,
'is_enabled': method.is_enabled if hasattr(method, 'is_enabled') else True,
'instructions': method.instructions,
})
paginated_data = paginator.get_paginated_response({'results': results}).data
return paginated_response(paginated_data, request=request)

View File

@@ -22,6 +22,8 @@ urlpatterns = [
path('account_balance/', BillingOverviewViewSet.as_view({'get': 'account_balance'}), name='account-balance'),
# Canonical credit balance endpoint
path('credits/balance/', CreditBalanceViewSet.as_view({'get': 'list'}), name='credit-balance-canonical'),
# Alias for frontend compatibility (transactions/balance/)
path('transactions/balance/', CreditBalanceViewSet.as_view({'get': 'list'}), name='transactions-balance'),
# Explicit list endpoints
path('transactions/', CreditTransactionViewSet.as_view({'get': 'list'}), name='transactions'),
path('usage/', CreditUsageViewSet.as_view({'get': 'list'}), name='usage'),