melhorando o codigo de restricao de acesso

This commit is contained in:
Gabriel Santana de Menezes 2026-05-07 08:29:52 -04:00
parent e0a3697e5b
commit b7803fef05
5 changed files with 130 additions and 77 deletions

View file

@ -251,10 +251,13 @@ export function AppShell() {
)} )}
</div> </div>
<NavLink to="/projects" className={navLinkClassName}> {(activeOrg?.roles.includes("OWNER") ||
<FolderKanban size={18} /> activeOrg?.roles.includes("ADMIN")) && (
Projetos <NavLink to="/projects" className={navLinkClassName}>
</NavLink> <FolderKanban size={18} />
Projetos
</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>
<NavLink to="/projects" className={navLinkClassName}> {(activeOrg?.roles.includes("OWNER") ||
<FolderKanban size={18} /> activeOrg?.roles.includes("ADMIN")) && (
Projetos <NavLink to="/projects" className={navLinkClassName}>
</NavLink> <FolderKanban size={18} />
Projetos
</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"

View 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}</>;
}

View file

@ -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>
) );
} };

View file

@ -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 */}

View file

@ -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",