diff --git a/backend/igny8_core/auth/middleware.py b/backend/igny8_core/auth/middleware.py index a9bf50b3..ef359562 100644 --- a/backend/igny8_core/auth/middleware.py +++ b/backend/igny8_core/auth/middleware.py @@ -31,6 +31,14 @@ 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: diff --git a/backend/igny8_core/auth/serializers.py b/backend/igny8_core/auth/serializers.py index 9ab5f1fb..17baf0f6 100644 --- a/backend/igny8_core/auth/serializers.py +++ b/backend/igny8_core/auth/serializers.py @@ -288,14 +288,27 @@ class RegisterSerializer(serializers.Serializer): from igny8_core.business.billing.models import CreditTransaction with transaction.atomic(): - # ALWAYS assign the existing free plan for simple signup - # No fallbacks: if free plan is misconfigured, surface error immediately - try: - plan = Plan.objects.get(slug='free', is_active=True) - except Plan.DoesNotExist: - raise serializers.ValidationError({ - "plan": "Free plan not configured. Please contact support." - }) + plan_slug = validated_data.get('plan_slug') + paid_plans = ['starter', 'growth', 'scale'] + + if plan_slug and plan_slug in paid_plans: + try: + plan = Plan.objects.get(slug=plan_slug, is_active=True) + except Plan.DoesNotExist: + raise serializers.ValidationError({ + "plan": f"Plan '{plan_slug}' not available. Please contact support." + }) + account_status = 'pending_payment' + initial_credits = 0 + else: + try: + plan = Plan.objects.get(slug='free', is_active=True) + except Plan.DoesNotExist: + raise serializers.ValidationError({ + "plan": "Free plan not configured. Please contact support." + }) + account_status = 'trial' + initial_credits = plan.get_effective_credits_per_month() # Generate account name if not provided account_name = validated_data.get('account_name') @@ -338,32 +351,31 @@ class RegisterSerializer(serializers.Serializer): slug = f"{base_slug}-{counter}" counter += 1 - # Get trial credits from plan - trial_credits = plan.get_effective_credits_per_month() - - # Create account with trial status and credits seeded + # Create account with status and credits seeded (0 for paid pending) account = Account.objects.create( name=account_name, slug=slug, owner=user, plan=plan, - credits=trial_credits, # CRITICAL: Seed initial credits - status='trial' # CRITICAL: Set as trial account + credits=initial_credits, + status=account_status, + payment_method=validated_data.get('payment_method') or 'bank_transfer', ) - # Log initial credit transaction for transparency - CreditTransaction.objects.create( - account=account, - transaction_type='subscription', - amount=trial_credits, - balance_after=trial_credits, - description=f'Free plan credits from {plan.name}', - metadata={ - 'plan_slug': plan.slug, - 'registration': True, - 'trial': True - } - ) + # Log initial credit transaction only for free/trial accounts with credits + if initial_credits > 0: + CreditTransaction.objects.create( + account=account, + transaction_type='subscription', + amount=initial_credits, + balance_after=initial_credits, + description=f'Free plan credits from {plan.name}', + metadata={ + 'plan_slug': plan.slug, + 'registration': True, + 'trial': True + } + ) # Update user to reference the new account user.account = account diff --git a/backend/igny8_core/auth/views.py b/backend/igny8_core/auth/views.py index df276371..817cefcc 100644 --- a/backend/igny8_core/auth/views.py +++ b/backend/igny8_core/auth/views.py @@ -476,7 +476,7 @@ class SiteViewSet(AccountModelViewSet): """ViewSet for managing Sites.""" serializer_class = SiteSerializer permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsEditorOrAbove] - authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication] + authentication_classes = [JWTAuthentication] def get_permissions(self): """Allow normal users (viewer) to create sites, but require editor+ for other operations.""" @@ -721,7 +721,7 @@ class SectorViewSet(AccountModelViewSet): """ViewSet for managing Sectors.""" serializer_class = SectorSerializer permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsEditorOrAbove] - authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication] + authentication_classes = [JWTAuthentication] def get_queryset(self): """Return sectors from sites accessible to the current user.""" diff --git a/backend/igny8_core/modules/billing/views.py b/backend/igny8_core/modules/billing/views.py index acaa2ea2..b3fcb0f2 100644 --- a/backend/igny8_core/modules/billing/views.py +++ b/backend/igny8_core/modules/billing/views.py @@ -34,7 +34,7 @@ class CreditBalanceViewSet(viewsets.ViewSet): Unified API Standard v1.0 compliant """ permission_classes = [IsAuthenticatedAndActive, HasTenantAccess] - authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication] + authentication_classes = [JWTAuthentication] throttle_scope = 'billing' throttle_classes = [DebugScopedRateThrottle] @@ -98,7 +98,7 @@ class CreditUsageViewSet(AccountModelViewSet): queryset = CreditUsageLog.objects.all() serializer_class = CreditUsageLogSerializer permission_classes = [IsAuthenticatedAndActive, HasTenantAccess] - authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication] + authentication_classes = [JWTAuthentication] pagination_class = CustomPageNumberPagination throttle_scope = 'billing' throttle_classes = [DebugScopedRateThrottle] @@ -389,7 +389,7 @@ class CreditTransactionViewSet(AccountModelViewSet): queryset = CreditTransaction.objects.all() serializer_class = CreditTransactionSerializer permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner] - authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication] + authentication_classes = [JWTAuthentication] pagination_class = CustomPageNumberPagination throttle_scope = 'billing' throttle_classes = [DebugScopedRateThrottle] @@ -409,7 +409,7 @@ class CreditTransactionViewSet(AccountModelViewSet): class BillingOverviewViewSet(viewsets.ViewSet): """User-facing billing overview API""" permission_classes = [IsAuthenticatedAndActive] - authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication] + authentication_classes = [JWTAuthentication] def account_balance(self, request): """Get account balance with subscription info""" @@ -445,7 +445,7 @@ class BillingOverviewViewSet(viewsets.ViewSet): class AdminBillingViewSet(viewsets.ViewSet): """Admin-only billing management API""" permission_classes = [IsAuthenticatedAndActive, permissions.IsAdminUser] - authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication] + authentication_classes = [JWTAuthentication] def stats(self, request): """Get system-wide billing statistics""" diff --git a/backend/igny8_core/modules/system/settings_views.py b/backend/igny8_core/modules/system/settings_views.py index 57a0b0f0..c4d8d47f 100644 --- a/backend/igny8_core/modules/system/settings_views.py +++ b/backend/igny8_core/modules/system/settings_views.py @@ -36,7 +36,7 @@ class SystemSettingsViewSet(AccountModelViewSet): queryset = SystemSettings.objects.all() serializer_class = SystemSettingsSerializer permission_classes = [IsAuthenticatedAndActive, HasTenantAccess] - authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication] + authentication_classes = [JWTAuthentication] pagination_class = CustomPageNumberPagination throttle_scope = 'system' throttle_classes = [DebugScopedRateThrottle] @@ -87,7 +87,7 @@ class AccountSettingsViewSet(AccountModelViewSet): queryset = AccountSettings.objects.all() serializer_class = AccountSettingsSerializer permission_classes = [IsAuthenticatedAndActive, HasTenantAccess] - authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication] + authentication_classes = [JWTAuthentication] pagination_class = CustomPageNumberPagination throttle_scope = 'system' throttle_classes = [DebugScopedRateThrottle] @@ -147,7 +147,7 @@ class UserSettingsViewSet(AccountModelViewSet): queryset = UserSettings.objects.all() serializer_class = UserSettingsSerializer permission_classes = [IsAuthenticatedAndActive, HasTenantAccess] - authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication] + authentication_classes = [JWTAuthentication] pagination_class = CustomPageNumberPagination throttle_scope = 'system' throttle_classes = [DebugScopedRateThrottle] @@ -213,7 +213,7 @@ class ModuleSettingsViewSet(AccountModelViewSet): queryset = ModuleSettings.objects.all() serializer_class = ModuleSettingsSerializer permission_classes = [IsAuthenticatedAndActive, HasTenantAccess] - authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication] + authentication_classes = [JWTAuthentication] pagination_class = CustomPageNumberPagination throttle_scope = 'system' throttle_classes = [DebugScopedRateThrottle] @@ -301,7 +301,7 @@ class ModuleEnableSettingsViewSet(AccountModelViewSet): """ queryset = ModuleEnableSettings.objects.all() serializer_class = ModuleEnableSettingsSerializer - authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication] + authentication_classes = [JWTAuthentication] throttle_scope = 'system' throttle_classes = [DebugScopedRateThrottle] @@ -466,7 +466,7 @@ class AISettingsViewSet(AccountModelViewSet): queryset = AISettings.objects.all() serializer_class = AISettingsSerializer permission_classes = [IsAuthenticatedAndActive, HasTenantAccess] - authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication] + authentication_classes = [JWTAuthentication] pagination_class = CustomPageNumberPagination throttle_scope = 'system' throttle_classes = [DebugScopedRateThrottle] diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 222d4e7c..e9319c02 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -13,6 +13,7 @@ import { useAuthStore } from "./store/authStore"; // Auth pages - loaded immediately (needed for login) import SignIn from "./pages/AuthPages/SignIn"; import SignUp from "./pages/AuthPages/SignUp"; +import Payment from "./pages/Payment"; import NotFound from "./pages/OtherPage/NotFound"; // Lazy load all other pages - only loads when navigated to @@ -186,6 +187,7 @@ export default function App() { {/* Auth Routes - Public */} } /> } /> + } /> {/* Protected Routes - Require Authentication */} { + const params = new URLSearchParams(window.location.search); + const planSlug = params.get("plan"); + const paidPlans = ["starter", "growth", "scale"]; + if (planSlug && paidPlans.includes(planSlug)) { + navigate(`/payment?plan=${planSlug}`, { replace: true }); + } + }, [navigate]); + const handleChange = (e: React.ChangeEvent) => { const { name, value } = e.target; setFormData((prev) => ({ ...prev, [name]: value })); @@ -79,7 +88,7 @@ export default function SignUpForm() { Start Your Free Trial

- No credit card required. 2,000 AI credits to get started. + No credit card required. 100 AI credits to get started.

diff --git a/frontend/src/marketing/pages/Pricing.tsx b/frontend/src/marketing/pages/Pricing.tsx index e88d4d31..df5c1a4e 100644 --- a/frontend/src/marketing/pages/Pricing.tsx +++ b/frontend/src/marketing/pages/Pricing.tsx @@ -43,6 +43,7 @@ const Pricing: React.FC = () => { const tiers = [ { name: "Starter", + slug: "starter", price: "$89", cadence: "per month", description: "For small teams starting workflows.", @@ -67,6 +68,7 @@ const Pricing: React.FC = () => { }, { name: "Growth", + slug: "growth", price: "$139", cadence: "per month", description: "For teams automating multiple workflows.", @@ -93,6 +95,7 @@ const Pricing: React.FC = () => { }, { name: "Scale", + slug: "scale", price: "$229", cadence: "per month", description: "For publishers and large orgs needing deeper control.", @@ -305,14 +308,14 @@ const Pricing: React.FC = () => { {/* CTA Button */}
diff --git a/frontend/src/pages/Payment.tsx b/frontend/src/pages/Payment.tsx new file mode 100644 index 00000000..3175567a --- /dev/null +++ b/frontend/src/pages/Payment.tsx @@ -0,0 +1,89 @@ +import { useEffect, useMemo, useState } from "react"; +import { useLocation, useNavigate, Link } from "react-router-dom"; + +const PLAN_COPY: Record = { + starter: { name: "Starter", price: "$89/mo", credits: "1,000 credits/month" }, + growth: { name: "Growth", price: "$139/mo", credits: "2,000 credits/month" }, + scale: { name: "Scale", price: "$229/mo", credits: "4,000 credits/month" }, +}; + +export default function Payment() { + const location = useLocation(); + const navigate = useNavigate(); + const [contactEmail, setContactEmail] = useState(""); + const [note, setNote] = useState(""); + + const planSlug = useMemo(() => new URLSearchParams(location.search).get("plan") || "", [location.search]); + const plan = planSlug ? PLAN_COPY[planSlug] : null; + + useEffect(() => { + if (!plan) { + navigate("/pricing", { replace: true }); + } + }, [plan, navigate]); + + return ( +
+
+
+
+

Confirm your plan

+

Complete your subscription

+
+ + Change plan + +
+ + {plan && ( +
+

{plan.name}

+

{plan.price}

+

{plan.credits}

+

+ Payment is completed offline (bank transfer). Submit your email below and we will send payment instructions. +

+
+ )} + +
+ +