Phase 0: Add sidebar filtering and route guards for modules

- Updated AppSidebar to filter out disabled modules from navigation
- Added ModuleGuard to all module routes (planner, writer, thinker, automation)
- Modules now dynamically appear/disappear based on enable settings
- Routes are protected and redirect to settings if module is disabled
This commit is contained in:
IGNY8 VPS (Salman)
2025-11-16 18:48:23 +00:00
parent dbe8da589f
commit 9b3fb25bc9
2 changed files with 155 additions and 89 deletions

View File

@@ -4,6 +4,7 @@ import { HelmetProvider } from "react-helmet-async";
import AppLayout from "./layout/AppLayout"; import AppLayout from "./layout/AppLayout";
import { ScrollToTop } from "./components/common/ScrollToTop"; import { ScrollToTop } from "./components/common/ScrollToTop";
import ProtectedRoute from "./components/auth/ProtectedRoute"; import ProtectedRoute from "./components/auth/ProtectedRoute";
import ModuleGuard from "./components/common/ModuleGuard";
import GlobalErrorDisplay from "./components/common/GlobalErrorDisplay"; import GlobalErrorDisplay from "./components/common/GlobalErrorDisplay";
import LoadingStateMonitor from "./components/common/LoadingStateMonitor"; import LoadingStateMonitor from "./components/common/LoadingStateMonitor";
@@ -133,90 +134,122 @@ export default function App() {
{/* Planner Module */} {/* Planner Module */}
<Route path="/planner" element={ <Route path="/planner" element={
<Suspense fallback={null}> <Suspense fallback={null}>
<PlannerDashboard /> <ModuleGuard module="planner">
<PlannerDashboard />
</ModuleGuard>
</Suspense> </Suspense>
} /> } />
<Route path="/planner/keywords" element={ <Route path="/planner/keywords" element={
<Suspense fallback={null}> <Suspense fallback={null}>
<Keywords /> <ModuleGuard module="planner">
<Keywords />
</ModuleGuard>
</Suspense> </Suspense>
} /> } />
<Route path="/planner/clusters" element={ <Route path="/planner/clusters" element={
<Suspense fallback={null}> <Suspense fallback={null}>
<Clusters /> <ModuleGuard module="planner">
<Clusters />
</ModuleGuard>
</Suspense> </Suspense>
} /> } />
<Route path="/planner/ideas" element={ <Route path="/planner/ideas" element={
<Suspense fallback={null}> <Suspense fallback={null}>
<Ideas /> <ModuleGuard module="planner">
<Ideas />
</ModuleGuard>
</Suspense> </Suspense>
} /> } />
{/* Writer Module */} {/* Writer Module */}
<Route path="/writer" element={ <Route path="/writer" element={
<Suspense fallback={null}> <Suspense fallback={null}>
<ModuleGuard module="writer">
<WriterDashboard /> <WriterDashboard />
</Suspense> </ModuleGuard>
</Suspense>
} /> } />
<Route path="/writer/tasks" element={ <Route path="/writer/tasks" element={
<Suspense fallback={null}> <Suspense fallback={null}>
<ModuleGuard module="writer">
<Tasks /> <Tasks />
</Suspense> </ModuleGuard>
</Suspense>
} /> } />
{/* Writer Content Routes - Order matters: list route must come before detail route */} {/* Writer Content Routes - Order matters: list route must come before detail route */}
<Route path="/writer/content" element={ <Route path="/writer/content" element={
<Suspense fallback={null}> <Suspense fallback={null}>
<ModuleGuard module="writer">
<Content /> <Content />
</Suspense> </ModuleGuard>
</Suspense>
} /> } />
{/* Content detail view - matches /writer/content/:id (e.g., /writer/content/10) */} {/* Content detail view - matches /writer/content/:id (e.g., /writer/content/10) */}
<Route path="/writer/content/:id" element={ <Route path="/writer/content/:id" element={
<Suspense fallback={null}> <Suspense fallback={null}>
<ModuleGuard module="writer">
<ContentView /> <ContentView />
</Suspense> </ModuleGuard>
</Suspense>
} /> } />
<Route path="/writer/drafts" element={<Navigate to="/writer/content" replace />} /> <Route path="/writer/drafts" element={<Navigate to="/writer/content" replace />} />
<Route path="/writer/images" element={ <Route path="/writer/images" element={
<Suspense fallback={null}> <Suspense fallback={null}>
<ModuleGuard module="writer">
<Images /> <Images />
</Suspense> </ModuleGuard>
</Suspense>
} /> } />
<Route path="/writer/published" element={ <Route path="/writer/published" element={
<Suspense fallback={null}> <Suspense fallback={null}>
<ModuleGuard module="writer">
<Published /> <Published />
</Suspense> </ModuleGuard>
</Suspense>
} /> } />
{/* Thinker Module */} {/* Thinker Module */}
<Route path="/thinker" element={ <Route path="/thinker" element={
<Suspense fallback={null}> <Suspense fallback={null}>
<ModuleGuard module="thinker">
<ThinkerDashboard /> <ThinkerDashboard />
</Suspense> </ModuleGuard>
</Suspense>
} /> } />
<Route path="/thinker/prompts" element={ <Route path="/thinker/prompts" element={
<Suspense fallback={null}> <Suspense fallback={null}>
<ModuleGuard module="thinker">
<Prompts /> <Prompts />
</Suspense> </ModuleGuard>
</Suspense>
} /> } />
<Route path="/thinker/author-profiles" element={ <Route path="/thinker/author-profiles" element={
<Suspense fallback={null}> <Suspense fallback={null}>
<ModuleGuard module="thinker">
<AuthorProfiles /> <AuthorProfiles />
</Suspense> </ModuleGuard>
</Suspense>
} /> } />
<Route path="/thinker/profile" element={ <Route path="/thinker/profile" element={
<Suspense fallback={null}> <Suspense fallback={null}>
<ModuleGuard module="thinker">
<ThinkerProfile /> <ThinkerProfile />
</Suspense> </ModuleGuard>
</Suspense>
} /> } />
<Route path="/thinker/strategies" element={ <Route path="/thinker/strategies" element={
<Suspense fallback={null}> <Suspense fallback={null}>
<ModuleGuard module="thinker">
<Strategies /> <Strategies />
</Suspense> </ModuleGuard>
</Suspense>
} /> } />
<Route path="/thinker/image-testing" element={ <Route path="/thinker/image-testing" element={
<Suspense fallback={null}> <Suspense fallback={null}>
<ModuleGuard module="thinker">
<ImageTesting /> <ImageTesting />
</Suspense> </ModuleGuard>
</Suspense>
} /> } />
{/* Billing Module */} {/* Billing Module */}
@@ -256,8 +289,10 @@ export default function App() {
{/* Other Pages */} {/* Other Pages */}
<Route path="/automation" element={ <Route path="/automation" element={
<Suspense fallback={null}> <Suspense fallback={null}>
<ModuleGuard module="automation">
<AutomationDashboard /> <AutomationDashboard />
</Suspense> </ModuleGuard>
</Suspense>
} /> } />
<Route path="/schedules" element={ <Route path="/schedules" element={
<Suspense fallback={null}> <Suspense fallback={null}>

View File

@@ -20,6 +20,8 @@ import { useSidebar } from "../context/SidebarContext";
import SidebarWidget from "./SidebarWidget"; import SidebarWidget from "./SidebarWidget";
import { APP_VERSION } from "../config/version"; import { APP_VERSION } from "../config/version";
import { useAuthStore } from "../store/authStore"; import { useAuthStore } from "../store/authStore";
import { useSettingsStore } from "../store/settingsStore";
import { isModuleEnabled } from "../config/modules.config";
import ApiStatusIndicator from "../components/sidebar/ApiStatusIndicator"; import ApiStatusIndicator from "../components/sidebar/ApiStatusIndicator";
type NavItem = { type NavItem = {
@@ -38,12 +40,19 @@ const AppSidebar: React.FC = () => {
const { isExpanded, isMobileOpen, isHovered, setIsHovered } = useSidebar(); const { isExpanded, isMobileOpen, isHovered, setIsHovered } = useSidebar();
const location = useLocation(); const location = useLocation();
const { user } = useAuthStore(); const { user } = useAuthStore();
const { moduleEnableSettings, isModuleEnabled: checkModuleEnabled } = useSettingsStore();
// Show admin menu only for users in aws-admin account // Show admin menu only for users in aws-admin account
const isAwsAdminAccount = Boolean( const isAwsAdminAccount = Boolean(
user?.account?.slug === 'aws-admin' || user?.account?.slug === 'aws-admin' ||
user?.role === 'developer' // Also show for developers as fallback user?.role === 'developer' // Also show for developers as fallback
); );
// Helper to check if module is enabled
const moduleEnabled = (moduleName: string): boolean => {
if (!moduleEnableSettings) return true; // Default to enabled if not loaded
return checkModuleEnabled(moduleName);
};
const [openSubmenu, setOpenSubmenu] = useState<{ const [openSubmenu, setOpenSubmenu] = useState<{
sectionIndex: number; sectionIndex: number;
@@ -60,77 +69,98 @@ const AppSidebar: React.FC = () => {
); );
// Define menu sections with useMemo to prevent recreation on every render // Define menu sections with useMemo to prevent recreation on every render
const menuSections: MenuSection[] = useMemo(() => [ // Filter out disabled modules based on module enable settings
{ const menuSections: MenuSection[] = useMemo(() => {
label: "OVERVIEW", const workflowItems: NavItem[] = [
items: [ {
{ icon: <PlugInIcon />,
icon: <GridIcon />, name: "Setup",
name: "Dashboard", subItems: [
path: "/", { name: "Sites", path: "/settings/sites" },
}, { name: "Keywords Opportunities", path: "/planner/keyword-opportunities" },
{ ],
icon: <DocsIcon />, },
name: "Industry / Sectors", ];
path: "/reference/industries",
}, // Add Planner if enabled
], if (moduleEnabled('planner')) {
}, workflowItems.push({
{ icon: <ListIcon />,
label: "WORKFLOWS", name: "Planner",
items: [ subItems: [
{ { name: "Dashboard", path: "/planner" },
icon: <PlugInIcon />, { name: "Keywords", path: "/planner/keywords" },
name: "Setup", { name: "Clusters", path: "/planner/clusters" },
subItems: [ { name: "Ideas", path: "/planner/ideas" },
{ name: "Sites", path: "/settings/sites" }, ],
{ name: "Keywords Opportunities", path: "/planner/keyword-opportunities" }, });
], }
},
{ // Add Writer if enabled
icon: <ListIcon />, if (moduleEnabled('writer')) {
name: "Planner", workflowItems.push({
subItems: [ icon: <TaskIcon />,
{ name: "Dashboard", path: "/planner" }, name: "Writer",
{ name: "Keywords", path: "/planner/keywords" }, subItems: [
{ name: "Clusters", path: "/planner/clusters" }, { name: "Dashboard", path: "/writer" },
{ name: "Ideas", path: "/planner/ideas" }, { name: "Tasks", path: "/writer/tasks" },
], { name: "Content", path: "/writer/content" },
}, { name: "Images", path: "/writer/images" },
{ { name: "Published", path: "/writer/published" },
icon: <TaskIcon />, ],
name: "Writer", });
subItems: [ }
{ name: "Dashboard", path: "/writer" },
{ name: "Tasks", path: "/writer/tasks" }, // Add Thinker if enabled
{ name: "Content", path: "/writer/content" }, if (moduleEnabled('thinker')) {
{ name: "Images", path: "/writer/images" }, workflowItems.push({
{ name: "Published", path: "/writer/published" }, icon: <BoltIcon />,
], name: "Thinker",
}, subItems: [
{ { name: "Dashboard", path: "/thinker" },
icon: <BoltIcon />, { name: "Prompts", path: "/thinker/prompts" },
name: "Thinker", { name: "Author Profiles", path: "/thinker/author-profiles" },
subItems: [ { name: "Strategies", path: "/thinker/strategies" },
{ name: "Dashboard", path: "/thinker" }, { name: "Image Testing", path: "/thinker/image-testing" },
{ name: "Prompts", path: "/thinker/prompts" }, ],
{ name: "Author Profiles", path: "/thinker/author-profiles" }, });
{ name: "Strategies", path: "/thinker/strategies" }, }
{ name: "Image Testing", path: "/thinker/image-testing" },
], // Add Automation if enabled
}, if (moduleEnabled('automation')) {
{ workflowItems.push({
icon: <BoltIcon />, icon: <BoltIcon />,
name: "Automation", name: "Automation",
path: "/automation", path: "/automation",
}, });
{ }
icon: <TimeIcon />,
name: "Schedules", workflowItems.push({
path: "/schedules", icon: <TimeIcon />,
}, name: "Schedules",
], path: "/schedules",
}, });
return [
{
label: "OVERVIEW",
items: [
{
icon: <GridIcon />,
name: "Dashboard",
path: "/",
},
{
icon: <DocsIcon />,
name: "Industry / Sectors",
path: "/reference/industries",
},
],
},
{
label: "WORKFLOWS",
items: workflowItems,
},
{ {
label: "ACCOUNT & SETTINGS", label: "ACCOUNT & SETTINGS",
items: [ items: [
@@ -165,7 +195,8 @@ const AppSidebar: React.FC = () => {
}, },
], ],
}, },
], []); ];
}, [moduleEnableSettings, moduleEnabled]);
// Admin section - only shown for users in aws-admin account // Admin section - only shown for users in aws-admin account
const adminSection: MenuSection = useMemo(() => ({ const adminSection: MenuSection = useMemo(() => ({