melhorando o codigo de restricao de acesso
This commit is contained in:
parent
e0a3697e5b
commit
b7803fef05
5 changed files with 130 additions and 77 deletions
|
|
@ -251,10 +251,13 @@ export function AppShell() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{(activeOrg?.roles.includes("OWNER") ||
|
||||||
|
activeOrg?.roles.includes("ADMIN")) && (
|
||||||
<NavLink to="/projects" className={navLinkClassName}>
|
<NavLink to="/projects" className={navLinkClassName}>
|
||||||
<FolderKanban size={18} />
|
<FolderKanban size={18} />
|
||||||
Projetos
|
Projetos
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Organizações Dropdown */}
|
{/* Organizações Dropdown */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|
@ -318,10 +321,8 @@ export function AppShell() {
|
||||||
<Clock size={18} />
|
<Clock size={18} />
|
||||||
Minhas Solicitações
|
Minhas Solicitações
|
||||||
</NavLink>
|
</NavLink>
|
||||||
{user?.organizations.some(
|
{activeOrg?.roles.some(
|
||||||
(org) =>
|
(role) => role === "OWNER" || role === "ADMIN",
|
||||||
org.roles.includes("OWNER") ||
|
|
||||||
org.roles.includes("ADMIN"),
|
|
||||||
) && (
|
) && (
|
||||||
<NavLink
|
<NavLink
|
||||||
to="/admin"
|
to="/admin"
|
||||||
|
|
@ -427,10 +428,13 @@ export function AppShell() {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{(activeOrg?.roles.includes("OWNER") ||
|
||||||
|
activeOrg?.roles.includes("ADMIN")) && (
|
||||||
<NavLink to="/projects" className={navLinkClassName}>
|
<NavLink to="/projects" className={navLinkClassName}>
|
||||||
<FolderKanban size={18} />
|
<FolderKanban size={18} />
|
||||||
Projetos
|
Projetos
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Mobile Organizações Group */}
|
{/* Mobile Organizações Group */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|
@ -479,10 +483,8 @@ export function AppShell() {
|
||||||
<Clock size={18} />
|
<Clock size={18} />
|
||||||
Pedidos
|
Pedidos
|
||||||
</NavLink>
|
</NavLink>
|
||||||
{user?.organizations.some(
|
{activeOrg?.roles.some(
|
||||||
(org) =>
|
(role) => role === "OWNER" || role === "ADMIN",
|
||||||
org.roles.includes("OWNER") ||
|
|
||||||
org.roles.includes("ADMIN"),
|
|
||||||
) && (
|
) && (
|
||||||
<NavLink
|
<NavLink
|
||||||
to="/admin"
|
to="/admin"
|
||||||
|
|
|
||||||
60
frontend/src/components/RoleGate.tsx
Normal file
60
frontend/src/components/RoleGate.tsx
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
import React from "react";
|
||||||
|
import { ShieldCheck, Building2 } from "lucide-react";
|
||||||
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
|
|
||||||
|
interface RoleGateProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
allowedRoles: string[];
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RoleGate({ children, allowedRoles, message }: RoleGateProps) {
|
||||||
|
const { activeOrg, isLoading } = useAuth();
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-[60vh] items-center justify-center">
|
||||||
|
<div className="h-10 w-10 animate-spin rounded-full border-4 border-brand border-t-transparent" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!activeOrg) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center min-h-[60vh] text-center p-6">
|
||||||
|
<div className="w-20 h-20 bg-surface-muted rounded-[2rem] flex items-center justify-center mb-6 text-text-muted">
|
||||||
|
<Building2 size={40} />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-black text-text-main mb-2">
|
||||||
|
Nenhuma Organização Ativa
|
||||||
|
</h2>
|
||||||
|
<p className="text-text-muted max-w-sm">
|
||||||
|
Selecione uma organização no menu superior para gerenciar seus
|
||||||
|
recursos.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAuthorized = activeOrg.roles.some((role) =>
|
||||||
|
allowedRoles.includes(role),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isAuthorized) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center min-h-[60vh] text-center p-6">
|
||||||
|
<div className="w-20 h-20 bg-danger/10 text-danger rounded-[2rem] flex items-center justify-center mb-6">
|
||||||
|
<ShieldCheck size={40} />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-black text-text-main mb-2">
|
||||||
|
Acesso Restrito
|
||||||
|
</h2>
|
||||||
|
<p className="text-text-muted max-w-sm">
|
||||||
|
{message || "Você não tem permissão para acessar esta área nesta organização."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
|
@ -1,61 +1,53 @@
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from "react";
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from "framer-motion";
|
||||||
import { Users, Check, X, ShieldAlert, Mail } from 'lucide-react'
|
import { Users, Check, X, ShieldAlert, Mail } from "lucide-react";
|
||||||
import { organizationService, type JoinRequest } from '../services/organizationService'
|
import {
|
||||||
import { useAuth } from '../contexts/AuthContext'
|
organizationService,
|
||||||
|
type JoinRequest,
|
||||||
|
} from "../services/organizationService";
|
||||||
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
|
|
||||||
export const OrganizationAdminPage: React.FC = () => {
|
export const OrganizationAdminPage: React.FC = () => {
|
||||||
const { user } = useAuth()
|
const { activeOrg } = useAuth();
|
||||||
const [requests, setRequests] = useState<JoinRequest[]>([])
|
const [requests, setRequests] = useState<JoinRequest[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
// For this simplified version, we manage the first organization where the user is OWNER
|
const orgToManage = activeOrg!;
|
||||||
const orgToManage = user?.organizations.find(org => org.roles.includes('OWNER') || org.roles.includes('ADMIN'))
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (orgToManage) {
|
if (orgToManage) {
|
||||||
loadRequests()
|
loadRequests();
|
||||||
}
|
}
|
||||||
}, [orgToManage])
|
}, [orgToManage]);
|
||||||
|
|
||||||
const loadRequests = async () => {
|
const loadRequests = async () => {
|
||||||
if (!orgToManage) return
|
if (!orgToManage) return;
|
||||||
try {
|
try {
|
||||||
const data = await organizationService.listPendingRequests(orgToManage.id)
|
const data = await organizationService.listPendingRequests(
|
||||||
setRequests(data)
|
orgToManage.id,
|
||||||
|
);
|
||||||
|
setRequests(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load requests', error)
|
console.error("Failed to load requests", error);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false)
|
setIsLoading(false);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleAction = async (userId: number, approved: boolean) => {
|
const handleAction = async (userId: number, approved: boolean) => {
|
||||||
if (!orgToManage) return
|
if (!orgToManage) return;
|
||||||
try {
|
try {
|
||||||
if (approved) {
|
if (approved) {
|
||||||
await organizationService.approveRequest(orgToManage.id, userId)
|
await organizationService.approveRequest(orgToManage.id, userId);
|
||||||
} else {
|
} else {
|
||||||
await organizationService.rejectRequest(orgToManage.id, userId)
|
await organizationService.rejectRequest(orgToManage.id, userId);
|
||||||
}
|
}
|
||||||
setRequests(prev => prev.filter(r => r.userId !== userId))
|
setRequests((prev) => prev.filter((r) => r.userId !== userId));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Action failed', error)
|
console.error("Action failed", error);
|
||||||
alert('Erro ao processar solicitação.')
|
alert("Erro ao processar solicitação.");
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!orgToManage) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center justify-center py-20 text-center">
|
|
||||||
<div className="w-16 h-16 bg-danger/10 text-danger rounded-2xl flex items-center justify-center mb-6">
|
|
||||||
<ShieldAlert size={32} />
|
|
||||||
</div>
|
|
||||||
<h2 className="text-2xl font-black text-text-main mb-2">Acesso Negado</h2>
|
|
||||||
<p className="text-text-muted max-w-xs">Você não tem permissão para gerenciar nenhuma organização.</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||||
|
|
@ -76,7 +68,9 @@ export const OrganizationAdminPage: React.FC = () => {
|
||||||
<div className="w-10 h-10 bg-brand/10 text-brand rounded-xl flex items-center justify-center">
|
<div className="w-10 h-10 bg-brand/10 text-brand rounded-xl flex items-center justify-center">
|
||||||
<Users size={20} />
|
<Users size={20} />
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-xl font-bold text-text-main">Solicitações Pendentes</h2>
|
<h2 className="text-xl font-bold text-text-main">
|
||||||
|
Solicitações Pendentes
|
||||||
|
</h2>
|
||||||
<span className="ml-auto bg-brand text-white text-xs font-bold px-3 py-1 rounded-full">
|
<span className="ml-auto bg-brand text-white text-xs font-bold px-3 py-1 rounded-full">
|
||||||
{requests.length} novos
|
{requests.length} novos
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -102,12 +96,17 @@ export const OrganizationAdminPage: React.FC = () => {
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<img
|
<img
|
||||||
src={request.photo || `https://ui-avatars.com/api/?name=${request.name}&background=random`}
|
src={
|
||||||
|
request.photo ||
|
||||||
|
`https://ui-avatars.com/api/?name=${request.name}&background=random`
|
||||||
|
}
|
||||||
alt={request.name}
|
alt={request.name}
|
||||||
className="w-12 h-12 rounded-full border border-border-soft object-cover"
|
className="w-12 h-12 rounded-full border border-border-soft object-cover"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-bold text-text-main text-lg">{request.name}</h3>
|
<h3 className="font-bold text-text-main text-lg">
|
||||||
|
{request.name}
|
||||||
|
</h3>
|
||||||
<div className="flex items-center gap-2 text-text-muted text-sm font-medium">
|
<div className="flex items-center gap-2 text-text-muted text-sm font-medium">
|
||||||
<Mail size={14} />
|
<Mail size={14} />
|
||||||
{request.email}
|
{request.email}
|
||||||
|
|
@ -137,5 +136,5 @@ export const OrganizationAdminPage: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -72,23 +72,6 @@ export const ProjectsPage: React.FC = () => {
|
||||||
p.name.toLowerCase().includes(searchQuery.toLowerCase()),
|
p.name.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!activeOrg) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center justify-center min-h-[60vh] text-center p-6">
|
|
||||||
<div className="w-20 h-20 bg-surface-muted rounded-[2rem] flex items-center justify-center mb-6 text-text-muted">
|
|
||||||
<Building2 size={40} />
|
|
||||||
</div>
|
|
||||||
<h2 className="text-2xl font-black text-text-main mb-2">
|
|
||||||
Nenhuma Organização Ativa
|
|
||||||
</h2>
|
|
||||||
<p className="text-text-muted max-w-sm">
|
|
||||||
Selecione uma organização no menu superior para gerenciar seus
|
|
||||||
projetos.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-12 pb-20">
|
<div className="space-y-12 pb-20">
|
||||||
{/* Header Section */}
|
{/* Header Section */}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import { MyRequestsPage } from "./pages/MyRequestsPage";
|
||||||
import { ProjectsPage } from "./pages/ProjectsPage";
|
import { ProjectsPage } from "./pages/ProjectsPage";
|
||||||
import { OrganizationsPage } from "./pages/OrganizationsPage";
|
import { OrganizationsPage } from "./pages/OrganizationsPage";
|
||||||
import { DashboardPage } from "./pages/DashboardPage";
|
import { DashboardPage } from "./pages/DashboardPage";
|
||||||
|
import { RoleGate } from "./components/RoleGate";
|
||||||
|
|
||||||
export const router = createBrowserRouter([
|
export const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
|
|
@ -53,7 +54,11 @@ export const router = createBrowserRouter([
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "admin",
|
path: "admin",
|
||||||
element: <OrganizationAdminPage />,
|
element: (
|
||||||
|
<RoleGate allowedRoles={["OWNER", "ADMIN"]} message="Apenas administradores e proprietários podem gerenciar solicitações.">
|
||||||
|
<OrganizationAdminPage />
|
||||||
|
</RoleGate>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "requests",
|
path: "requests",
|
||||||
|
|
@ -61,7 +66,11 @@ export const router = createBrowserRouter([
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "projects",
|
path: "projects",
|
||||||
element: <ProjectsPage />,
|
element: (
|
||||||
|
<RoleGate allowedRoles={["OWNER", "ADMIN"]} message="Apenas administradores e proprietários podem gerenciar projetos.">
|
||||||
|
<ProjectsPage />
|
||||||
|
</RoleGate>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "organizations",
|
path: "organizations",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue