@@ -42,14 +42,31 @@ export const Navigation: React.FC<NavigationProps> = ({
|
|||||||
const currentTheme = getCurrentTheme();
|
const currentTheme = getCurrentTheme();
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ path: "/templates", label: "Шаблоны", icon: FaCode },
|
{ path: "/templates", label: "Шаблоны", icon: FaCode, requireView: true },
|
||||||
{ path: "/add-agents", label: "Деплой", icon: FaRocket },
|
{
|
||||||
{ path: "/registration", label: "Регистрация", icon: FaKey },
|
path: "/add-agents",
|
||||||
{ path: "/logs", label: "Логи", icon: FaFileAlt },
|
label: "Деплой",
|
||||||
|
icon: FaRocket,
|
||||||
|
requireManageAgent: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/registration",
|
||||||
|
label: "Регистрация",
|
||||||
|
icon: FaKey,
|
||||||
|
requireManageAgent: true,
|
||||||
|
},
|
||||||
|
{ path: "/logs", label: "Логи", icon: FaFileAlt, requireView: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
const isActive = (path: string) => location.pathname === path;
|
const isActive = (path: string) => location.pathname === path;
|
||||||
|
|
||||||
|
// Filter nav items based on user permissions
|
||||||
|
const filteredNavItems = navItems.filter((item) => {
|
||||||
|
if (item.requireView && !user?.permission_view) return false;
|
||||||
|
if (item.requireManageAgent && !user?.permission_manage_agent) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (e: MouseEvent) => {
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
if (
|
if (
|
||||||
@@ -77,42 +94,37 @@ export const Navigation: React.FC<NavigationProps> = ({
|
|||||||
|
|
||||||
const renderNavItems = (showLabels: boolean, iconSize: number) => (
|
const renderNavItems = (showLabels: boolean, iconSize: number) => (
|
||||||
<div className="flex items-center gap-1 whitespace-nowrap">
|
<div className="flex items-center gap-1 whitespace-nowrap">
|
||||||
{navItems
|
{filteredNavItems.map((item) => {
|
||||||
.filter((item) => {
|
const Icon = item.icon;
|
||||||
if ((item as any).adminOnly && !user?.permission_admin) return false;
|
const active = isActive(item.path);
|
||||||
return true;
|
return (
|
||||||
})
|
<button
|
||||||
.map((item) => {
|
key={item.path}
|
||||||
const Icon = item.icon;
|
onClick={() => navigate(item.path)}
|
||||||
const active = isActive(item.path);
|
className="flex items-center gap-1.5 px-3 py-2 rounded-lg font-medium transition-all flex-shrink-0"
|
||||||
return (
|
style={{
|
||||||
<button
|
backgroundColor: active ? "var(--accent)" : "transparent",
|
||||||
key={item.path}
|
color: active ? "var(--accent-text)" : "var(--text-secondary)",
|
||||||
onClick={() => navigate(item.path)}
|
}}
|
||||||
className="flex items-center gap-1.5 px-3 py-2 rounded-lg font-medium transition-all flex-shrink-0"
|
onMouseEnter={(e) => {
|
||||||
style={{
|
if (!active) {
|
||||||
backgroundColor: active ? "var(--accent)" : "transparent",
|
e.currentTarget.style.backgroundColor = "var(--bg-secondary)";
|
||||||
color: active ? "var(--accent-text)" : "var(--text-secondary)",
|
e.currentTarget.style.color = "var(--text-primary)";
|
||||||
}}
|
}
|
||||||
onMouseEnter={(e) => {
|
}}
|
||||||
if (!active) {
|
onMouseLeave={(e) => {
|
||||||
e.currentTarget.style.backgroundColor = "var(--bg-secondary)";
|
if (!active) {
|
||||||
e.currentTarget.style.color = "var(--text-primary)";
|
e.currentTarget.style.backgroundColor = "transparent";
|
||||||
}
|
e.currentTarget.style.color = "var(--text-secondary)";
|
||||||
}}
|
}
|
||||||
onMouseLeave={(e) => {
|
}}
|
||||||
if (!active) {
|
title={item.label}
|
||||||
e.currentTarget.style.backgroundColor = "transparent";
|
>
|
||||||
e.currentTarget.style.color = "var(--text-secondary)";
|
<Icon size={iconSize} />
|
||||||
}
|
{showLabels && <span className="text-xs">{item.label}</span>}
|
||||||
}}
|
</button>
|
||||||
title={item.label}
|
);
|
||||||
>
|
})}
|
||||||
<Icon size={iconSize} />
|
|
||||||
{showLabels && <span className="text-xs">{item.label}</span>}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -375,14 +387,31 @@ export const BottomNav: React.FC = () => {
|
|||||||
const { user } = useAuthStore();
|
const { user } = useAuthStore();
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ path: "/templates", label: "Шаблоны", icon: FaCode },
|
{ path: "/templates", label: "Шаблоны", icon: FaCode, requireView: true },
|
||||||
{ path: "/add-agents", label: "Деплой", icon: FaRocket },
|
{
|
||||||
{ path: "/registration", label: "Регистрация", icon: FaKey },
|
path: "/add-agents",
|
||||||
{ path: "/logs", label: "Логи", icon: FaFileAlt },
|
label: "Деплой",
|
||||||
|
icon: FaRocket,
|
||||||
|
requireManageAgent: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/registration",
|
||||||
|
label: "Регистрация",
|
||||||
|
icon: FaKey,
|
||||||
|
requireManageAgent: true,
|
||||||
|
},
|
||||||
|
{ path: "/logs", label: "Логи", icon: FaFileAlt, requireView: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
const isActive = (path: string) => location.pathname === path;
|
const isActive = (path: string) => location.pathname === path;
|
||||||
|
|
||||||
|
// Filter nav items based on user permissions
|
||||||
|
const filteredNavItems = navItems.filter((item) => {
|
||||||
|
if (item.requireView && !user?.permission_view) return false;
|
||||||
|
if (item.requireManageAgent && !user?.permission_manage_agent) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex-shrink-0 border-t"
|
className="flex-shrink-0 border-t"
|
||||||
@@ -392,32 +421,24 @@ export const BottomNav: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-around px-2 py-2">
|
<div className="flex items-center justify-around px-2 py-2">
|
||||||
{navItems
|
{filteredNavItems.map((item) => {
|
||||||
.filter((item) => {
|
const Icon = item.icon;
|
||||||
if ((item as any).adminOnly && !user?.permission_admin)
|
const active = isActive(item.path);
|
||||||
return false;
|
return (
|
||||||
return true;
|
<button
|
||||||
})
|
key={item.path}
|
||||||
.map((item) => {
|
onClick={() => navigate(item.path)}
|
||||||
const Icon = item.icon;
|
className="flex items-center justify-center p-3 rounded-lg transition-all"
|
||||||
const active = isActive(item.path);
|
style={{
|
||||||
return (
|
backgroundColor: active ? "var(--accent)" : "transparent",
|
||||||
<button
|
color: active ? "var(--accent-text)" : "var(--text-secondary)",
|
||||||
key={item.path}
|
}}
|
||||||
onClick={() => navigate(item.path)}
|
title={item.label}
|
||||||
className="flex items-center justify-center p-3 rounded-lg transition-all"
|
>
|
||||||
style={{
|
<Icon size={20} />
|
||||||
backgroundColor: active ? "var(--accent)" : "transparent",
|
</button>
|
||||||
color: active
|
);
|
||||||
? "var(--accent-text)"
|
})}
|
||||||
: "var(--text-secondary)",
|
|
||||||
}}
|
|
||||||
title={item.label}
|
|
||||||
>
|
|
||||||
<Icon size={20} />
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,42 @@
|
|||||||
import { useAuthStore } from "@/modules/auth/store/useAuthStore";
|
|
||||||
import { Navigate } from "react-router-dom";
|
import { Navigate } from "react-router-dom";
|
||||||
|
import { useAuthStore } from "@/modules/auth/store/useAuthStore";
|
||||||
|
|
||||||
export const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
|
interface ProtectedRouteProps {
|
||||||
const { isAuthenticated } = useAuthStore();
|
children: React.ReactNode;
|
||||||
|
requireView?: boolean;
|
||||||
|
requireManageAgent?: boolean;
|
||||||
|
requireAdmin?: boolean;
|
||||||
|
fallbackPath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// if (!isAuthenticated) {
|
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
||||||
// return <Navigate to="/auth" replace />;
|
children,
|
||||||
// }
|
requireView = false,
|
||||||
|
requireManageAgent = false,
|
||||||
|
requireAdmin = false,
|
||||||
|
fallbackPath = "/",
|
||||||
|
}) => {
|
||||||
|
const { user, isAuthenticated } = useAuthStore();
|
||||||
|
|
||||||
|
if (!isAuthenticated && user?.token) {
|
||||||
|
// User is authenticated based on token
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return <Navigate to="/auth" replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requireView && !user.permission_view) {
|
||||||
|
return <Navigate to={fallbackPath} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requireManageAgent && !user.permission_manage_agent) {
|
||||||
|
return <Navigate to={fallbackPath} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requireAdmin && !user.permission_admin) {
|
||||||
|
return <Navigate to={fallbackPath} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { LogsPage } from "@/pages/logs.page";
|
|||||||
import { GraphsPage } from "@/pages/graphs.page";
|
import { GraphsPage } from "@/pages/graphs.page";
|
||||||
import { DashboardPage } from "@/pages/dashboard.page";
|
import { DashboardPage } from "@/pages/dashboard.page";
|
||||||
import { AgentDashboardPage } from "@/pages/agent-dashboard.page";
|
import { AgentDashboardPage } from "@/pages/agent-dashboard.page";
|
||||||
|
import { ProtectedRoute } from "./helper/protected.route";
|
||||||
|
|
||||||
export const mockGraphData: GraphData = {
|
export const mockGraphData: GraphData = {
|
||||||
nodes: [
|
nodes: [
|
||||||
@@ -122,18 +123,82 @@ export const Routing = () => {
|
|||||||
<Route path="/register" element={<RegisterPage />} />
|
<Route path="/register" element={<RegisterPage />} />
|
||||||
|
|
||||||
<Route element={<DefaultLayout />}>
|
<Route element={<DefaultLayout />}>
|
||||||
<Route path="/" element={<TemplatesPage />} />
|
{/* Routes requiring 'view' permission */}
|
||||||
<Route path="/add-agents" element={<AddAgentsPage />} />
|
<Route
|
||||||
<Route path="/registration" element={<RegistrationTokenPage />} />
|
path="/"
|
||||||
<Route path="/logs" element={<LogsPage />} />
|
element={
|
||||||
<Route path="/admin" element={<AdminPage />} />
|
<ProtectedRoute requireView>
|
||||||
<Route path="/IDE" element={<IDEPage />} />
|
<TemplatesPage />
|
||||||
<Route path="/templates" element={<TemplatesPage />} />
|
</ProtectedRoute>
|
||||||
<Route path="/graphs" element={<GraphsPage />} />
|
}
|
||||||
{/* <Route path="/dashboard" element={<DashboardPage />} /> */}
|
/>
|
||||||
|
<Route
|
||||||
|
path="/logs"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute requireView>
|
||||||
|
<LogsPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/graphs"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute requireView>
|
||||||
|
<GraphsPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/dashboard/:agentLabel"
|
path="/dashboard/:agentLabel"
|
||||||
element={<AgentDashboardPage />}
|
element={
|
||||||
|
<ProtectedRoute requireView>
|
||||||
|
<AgentDashboardPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Routes requiring 'manage_agent' permission */}
|
||||||
|
<Route
|
||||||
|
path="/add-agents"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute requireManageAgent>
|
||||||
|
<AddAgentsPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/registration"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute requireManageAgent>
|
||||||
|
<RegistrationTokenPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/templates"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute requireView>
|
||||||
|
<TemplatesPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/IDE"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute requireView>
|
||||||
|
<IDEPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Admin route requiring 'admin' permission */}
|
||||||
|
<Route
|
||||||
|
path="/admin"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute requireAdmin>
|
||||||
|
<AdminPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { RunScriptModal } from "../modules/ide/components/RunScriptModal";
|
|||||||
import { AddInterpreterModal } from "../modules/ide/components/AddInterpreterModal";
|
import { AddInterpreterModal } from "../modules/ide/components/AddInterpreterModal";
|
||||||
import type { FileNode } from "../modules/ide";
|
import type { FileNode } from "../modules/ide";
|
||||||
import { scriptsApi } from "../modules/ide/api/scripts.api";
|
import { scriptsApi } from "../modules/ide/api/scripts.api";
|
||||||
|
import { useAuthStore } from "@/modules/auth/store/useAuthStore";
|
||||||
|
|
||||||
const convertTreeToFileNode = (data: any[]): FileNode => {
|
const convertTreeToFileNode = (data: any[]): FileNode => {
|
||||||
const convertItem = (item: any): FileNode => {
|
const convertItem = (item: any): FileNode => {
|
||||||
@@ -43,6 +44,8 @@ const convertTreeToFileNode = (data: any[]): FileNode => {
|
|||||||
|
|
||||||
export const TemplatesPage = () => {
|
export const TemplatesPage = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { user } = useAuthStore();
|
||||||
|
const canManageAgent = user?.permission_manage_agent;
|
||||||
const [files, setFiles] = useState<FileNode | null>(null);
|
const [files, setFiles] = useState<FileNode | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [runModal, setRunModal] = useState<{
|
const [runModal, setRunModal] = useState<{
|
||||||
@@ -127,33 +130,35 @@ export const TemplatesPage = () => {
|
|||||||
Add Interpreter
|
Add Interpreter
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Open in Editor button */}
|
{/* Open in Editor button — только с правом manage_agent */}
|
||||||
<button
|
{canManageAgent && (
|
||||||
onClick={() => navigate("/ide")}
|
<button
|
||||||
style={{
|
onClick={() => navigate("/ide")}
|
||||||
display: "flex",
|
style={{
|
||||||
alignItems: "center",
|
display: "flex",
|
||||||
gap: "8px",
|
alignItems: "center",
|
||||||
padding: "6px 16px",
|
gap: "8px",
|
||||||
backgroundColor: "#0e639c",
|
padding: "6px 16px",
|
||||||
border: "none",
|
backgroundColor: "#0e639c",
|
||||||
borderRadius: "4px",
|
border: "none",
|
||||||
color: "#ffffff",
|
borderRadius: "4px",
|
||||||
cursor: "pointer",
|
color: "#ffffff",
|
||||||
fontSize: "12px",
|
cursor: "pointer",
|
||||||
fontWeight: 500,
|
fontSize: "12px",
|
||||||
transition: "all 0.15s",
|
fontWeight: 500,
|
||||||
}}
|
transition: "all 0.15s",
|
||||||
onMouseEnter={(e) => {
|
}}
|
||||||
e.currentTarget.style.backgroundColor = "#1177bb";
|
onMouseEnter={(e) => {
|
||||||
}}
|
e.currentTarget.style.backgroundColor = "#1177bb";
|
||||||
onMouseLeave={(e) => {
|
}}
|
||||||
e.currentTarget.style.backgroundColor = "#0e639c";
|
onMouseLeave={(e) => {
|
||||||
}}
|
e.currentTarget.style.backgroundColor = "#0e639c";
|
||||||
>
|
}}
|
||||||
<FiEdit3 size={14} />
|
>
|
||||||
Open Editor
|
<FiEdit3 size={14} />
|
||||||
</button>
|
Open Editor
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* File Picker (terminal встроен внутрь) */}
|
{/* File Picker (terminal встроен внутрь) */}
|
||||||
|
|||||||
Reference in New Issue
Block a user