From b0409d965bc93f8bd481aec1635e556ee14213da Mon Sep 17 00:00:00 2001 From: alorig <220087330+alorig@users.noreply.github.com> Date: Thu, 20 Nov 2025 21:29:14 +0500 Subject: [PATCH] refactor-upto-phase 6 --- backend/igny8_core/auth/middleware.py | 58 +++++- backend/igny8_core/auth/views.py | 21 +- frontend/src/App.tsx | 20 +- .../src/components/auth/ProtectedRoute.tsx | 22 ++- frontend/src/components/auth/SignInForm.tsx | 4 + frontend/src/pages/Sites/Builder/Wizard.tsx | 110 ++++++----- .../Builder/steps/BusinessDetailsStep.tsx | 187 ++++++++++-------- frontend/src/pages/Sites/Content.tsx | 32 +-- frontend/src/pages/Sites/DeploymentPanel.tsx | 101 +++++----- frontend/src/pages/Sites/PageManager.tsx | 41 ++-- frontend/src/pages/Sites/Settings.tsx | 26 ++- frontend/src/pages/Sites/SyncDashboard.tsx | 89 ++++----- frontend/src/store/authStore.ts | 49 +++-- refactor-plan/FINAL_REFACTOR_TASKS.md | 32 +-- 14 files changed, 478 insertions(+), 314 deletions(-) diff --git a/backend/igny8_core/auth/middleware.py b/backend/igny8_core/auth/middleware.py index 32846ca0..9628dc47 100644 --- a/backend/igny8_core/auth/middleware.py +++ b/backend/igny8_core/auth/middleware.py @@ -4,6 +4,7 @@ Extracts account from JWT token and injects into request context """ from django.utils.deprecation import MiddlewareMixin from django.http import JsonResponse +from django.contrib.auth import logout from rest_framework import status try: @@ -41,14 +42,19 @@ class AccountContextMiddleware(MiddlewareMixin): request.user = user # Get account from refreshed user user_account = getattr(user, 'account', None) - if user_account: - request.account = user_account - return None + validation_error = self._validate_account_and_plan(request, user) + if validation_error: + return validation_error + request.account = getattr(user, 'account', None) + return None except (AttributeError, UserModel.DoesNotExist, Exception): # If refresh fails, fallback to cached account try: user_account = getattr(request.user, 'account', None) if user_account: + validation_error = self._validate_account_and_plan(request, request.user) + if validation_error: + return validation_error request.account = user_account return None except (AttributeError, Exception): @@ -96,6 +102,9 @@ class AccountContextMiddleware(MiddlewareMixin): # Get user from DB (but don't set request.user - let DRF authentication handle that) # Only set request.account for account context user = User.objects.select_related('account', 'account__plan').get(id=user_id) + validation_error = self._validate_account_and_plan(request, user) + if validation_error: + return validation_error if account_id: # Verify account still exists try: @@ -119,4 +128,47 @@ class AccountContextMiddleware(MiddlewareMixin): request.account = None return None + + def _validate_account_and_plan(self, request, user): + """ + Ensure the authenticated user has an account and an active plan. + If not, logout the user (for session auth) and block the request. + """ + try: + account = getattr(user, 'account', None) + except Exception: + account = None + + if not account: + return self._deny_request( + request, + error='Account not configured for this user. Please contact support.', + status_code=status.HTTP_403_FORBIDDEN, + ) + + plan = getattr(account, 'plan', None) + if plan is None or getattr(plan, 'is_active', False) is False: + return self._deny_request( + request, + error='Active subscription required. Visit igny8.com/pricing to subscribe.', + status_code=status.HTTP_402_PAYMENT_REQUIRED, + ) + + return None + + def _deny_request(self, request, error, status_code): + """Logout session users (if any) and return a consistent JSON error.""" + try: + if hasattr(request, 'user') and request.user and request.user.is_authenticated: + logout(request) + except Exception: + pass + + return JsonResponse( + { + 'success': False, + 'error': error, + }, + status=status_code, + ) diff --git a/backend/igny8_core/auth/views.py b/backend/igny8_core/auth/views.py index 685c6930..aceb5e6c 100644 --- a/backend/igny8_core/auth/views.py +++ b/backend/igny8_core/auth/views.py @@ -926,13 +926,28 @@ class AuthViewSet(viewsets.GenericViewSet): ) if user.check_password(password): + # Ensure user has an account + account = getattr(user, 'account', None) + if account is None: + return error_response( + error='Account not configured for this user. Please contact support.', + status_code=status.HTTP_403_FORBIDDEN, + request=request, + ) + + # Ensure account has an active plan + plan = getattr(account, 'plan', None) + if plan is None or getattr(plan, 'is_active', False) is False: + return error_response( + error='Active subscription required. Visit igny8.com/pricing to subscribe.', + status_code=status.HTTP_402_PAYMENT_REQUIRED, + request=request, + ) + # Log the user in (create session for session authentication) from django.contrib.auth import login login(request, user) - # Get account from user - account = getattr(user, 'account', None) - # Generate JWT tokens access_token = generate_access_token(user, account) refresh_token = generate_refresh_token(user, account) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7bbda04a..eea88fbc 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,4 @@ -import { Suspense, lazy } from "react"; +import { Suspense, lazy, useEffect } from "react"; import { BrowserRouter as Router, Routes, Route, Navigate } from "react-router"; import { HelmetProvider } from "react-helmet-async"; import AppLayout from "./layout/AppLayout"; @@ -7,6 +7,7 @@ import ProtectedRoute from "./components/auth/ProtectedRoute"; import ModuleGuard from "./components/common/ModuleGuard"; import GlobalErrorDisplay from "./components/common/GlobalErrorDisplay"; import LoadingStateMonitor from "./components/common/LoadingStateMonitor"; +import { useAuthStore } from "./store/authStore"; // Auth pages - loaded immediately (needed for login) import SignIn from "./pages/AuthPages/SignIn"; @@ -137,6 +138,23 @@ const Tooltips = lazy(() => import("./pages/Settings/UiElements/Tooltips")); const Videos = lazy(() => import("./pages/Settings/UiElements/Videos")); export default function App() { + const { isAuthenticated, refreshUser, logout } = useAuthStore((state) => ({ + isAuthenticated: state.isAuthenticated, + refreshUser: state.refreshUser, + logout: state.logout, + })); + + useEffect(() => { + if (!isAuthenticated) { + return; + } + + refreshUser().catch((error) => { + console.warn('Session validation failed:', error); + logout(); + }); + }, [isAuthenticated, refreshUser, logout]); + return ( <> diff --git a/frontend/src/components/auth/ProtectedRoute.tsx b/frontend/src/components/auth/ProtectedRoute.tsx index 125a235b..3c7ef4a8 100644 --- a/frontend/src/components/auth/ProtectedRoute.tsx +++ b/frontend/src/components/auth/ProtectedRoute.tsx @@ -4,6 +4,8 @@ import { useAuthStore } from "../../store/authStore"; import { useErrorHandler } from "../../hooks/useErrorHandler"; import { trackLoading } from "../common/LoadingStateMonitor"; +const PRICING_URL = "https://igny8.com/pricing"; + interface ProtectedRouteProps { children: ReactNode; } @@ -13,7 +15,7 @@ interface ProtectedRouteProps { * Redirects to /signin if user is not authenticated */ export default function ProtectedRoute({ children }: ProtectedRouteProps) { - const { isAuthenticated, loading } = useAuthStore(); + const { isAuthenticated, loading, user, logout } = useAuthStore(); const location = useLocation(); const { addError } = useErrorHandler('ProtectedRoute'); const [showError, setShowError] = useState(false); @@ -24,6 +26,24 @@ export default function ProtectedRoute({ children }: ProtectedRouteProps) { trackLoading('auth-loading', loading); }, [loading]); + // Validate account + plan whenever auth/user changes + useEffect(() => { + if (!isAuthenticated) { + return; + } + + if (!user?.account) { + setErrorMessage('This user is not linked to an account. Please contact support.'); + logout(); + return; + } + + if (!user.account.plan) { + logout(); + window.location.href = PRICING_URL; + } + }, [isAuthenticated, user, logout]); + // Immediate check on mount: if loading is true, reset it immediately useEffect(() => { if (loading) { diff --git a/frontend/src/components/auth/SignInForm.tsx b/frontend/src/components/auth/SignInForm.tsx index eeb995a2..54e545b0 100644 --- a/frontend/src/components/auth/SignInForm.tsx +++ b/frontend/src/components/auth/SignInForm.tsx @@ -32,6 +32,10 @@ export default function SignInForm() { const from = (location.state as any)?.from?.pathname || "/"; navigate(from, { replace: true }); } catch (err: any) { + if (err?.code === 'PLAN_REQUIRED') { + window.location.href = 'https://igny8.com/pricing'; + return; + } setError(err.message || "Login failed. Please check your credentials."); } }; diff --git a/frontend/src/pages/Sites/Builder/Wizard.tsx b/frontend/src/pages/Sites/Builder/Wizard.tsx index f16a7d47..731c6aa8 100644 --- a/frontend/src/pages/Sites/Builder/Wizard.tsx +++ b/frontend/src/pages/Sites/Builder/Wizard.tsx @@ -8,13 +8,14 @@ import { import Button from "../../../components/ui/button/Button"; import PageMeta from "../../../components/common/PageMeta"; import SiteAndSectorSelector from "../../../components/common/SiteAndSectorSelector"; +import PageHeader from "../../../components/common/PageHeader"; import Alert from "../../../components/ui/alert/Alert"; import { - Loader2, - PlayCircle, - RefreshCw, - Wand2, -} from "lucide-react"; + GridIcon, + ArrowLeftIcon, + ArrowRightIcon, + BoltIcon, +} from "../../../icons"; import { useSiteStore } from "../../../store/siteStore"; import { useSectorStore } from "../../../store/sectorStore"; import { useBuilderStore } from "../../../store/builderStore"; @@ -289,29 +290,41 @@ export default function SiteBuilderWizard() { } }; + const renderPrimaryIcon = () => { + if (isSubmitting) { + return ( + + + + ); + } + if (isLastStep) { + return ; + } + return undefined; + }; + return (
-
-

- Sites / Create Site -

-

- Site Builder -

-

- Create a new site using IGNY8’s AI-powered wizard. Align the estate, - strategy, and tone before publishing. + , color: "purple" }} + hideSiteSector + /> +

+

+ Use the AI-powered wizard to capture business context, brand direction, and tone before generating blueprints.

+
-
@@ -390,13 +403,7 @@ export default function SiteBuilderWizard() { tone="brand" disabled={missingContext || isSubmitting} onClick={handlePrimary} - startIcon={ - isSubmitting ? ( - - ) : isLastStep ? ( - - ) : undefined - } + startIcon={renderPrimaryIcon()} > {isLastStep ? "Generate structure" : "Next"} @@ -421,28 +428,29 @@ export default function SiteBuilderWizard() { {pages.length}
- - + +
) : ( diff --git a/frontend/src/pages/Sites/Builder/steps/BusinessDetailsStep.tsx b/frontend/src/pages/Sites/Builder/steps/BusinessDetailsStep.tsx index 4bb4260c..0dc72860 100644 --- a/frontend/src/pages/Sites/Builder/steps/BusinessDetailsStep.tsx +++ b/frontend/src/pages/Sites/Builder/steps/BusinessDetailsStep.tsx @@ -10,10 +10,10 @@ import { useState, useEffect } from 'react'; import { useBuilderWorkflowStore } from '../../../../store/builderWorkflowStore'; import { fetchSiteBlueprintById, updateSiteBlueprint, SiteBlueprint } from '../../../../services/api'; import { Card, CardDescription, CardTitle } from '../../../../components/ui/card'; -import ButtonWithTooltip from '../../../../components/ui/button/ButtonWithTooltip'; +import Button from '../../../../components/ui/button/Button'; import Input from '../../../../components/form/input/InputField'; import Alert from '../../../../components/ui/alert/Alert'; -import { Loader2 } from 'lucide-react'; +import { BoltIcon, GridIcon } from '../../../../icons'; import type { BuilderFormData, SiteBuilderMetadata } from '../../../../types/siteBuilder'; // Stage 1 Wizard props @@ -115,11 +115,13 @@ function BusinessDetailsStepStage2({ blueprintId }: { blueprintId: number }) { const canProceed = formData.name.trim().length > 0; return ( - - Business Details - - Tell us about your business and site type to get started. - + +
+ Business details + + Tell us about your business and hosting preference to keep blueprints organized. + +
{error && ( @@ -128,62 +130,58 @@ function BusinessDetailsStepStage2({ blueprintId }: { blueprintId: number }) { )}
-
- - setFormData({ ...formData, name: e.target.value })} - placeholder="My Awesome Site" - required - /> +
+
+ + setFormData({ ...formData, name: e.target.value })} + placeholder="Acme Robotics" + required + /> +
+
+ + +
- +