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:
@@ -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}>
|
||||||
|
|||||||
@@ -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(() => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user