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.