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>
<NavLink to="/projects" className={navLinkClassName}>
<FolderKanban size={18} />
Projetos
</NavLink>
{(activeOrg?.roles.includes("OWNER") ||
activeOrg?.roles.includes("ADMIN")) && (
<NavLink to="/projects" className={navLinkClassName}>
<FolderKanban size={18} />
Projetos
</NavLink>
)}
{/* Organizações Dropdown */}
<div className="relative">
@ -318,10 +321,8 @@ export function AppShell() {
<Clock size={18} />
Minhas Solicitações
</NavLink>
{user?.organizations.some(
(org) =>
org.roles.includes("OWNER") ||
org.roles.includes("ADMIN"),
{activeOrg?.roles.some(
(role) => role === "OWNER" || role === "ADMIN",
) && (
<NavLink
to="/admin"
@ -427,10 +428,13 @@ export function AppShell() {
</>
)}
</div>
<NavLink to="/projects" className={navLinkClassName}>
<FolderKanban size={18} />
Projetos
</NavLink>
{(activeOrg?.roles.includes("OWNER") ||
activeOrg?.roles.includes("ADMIN")) && (
<NavLink to="/projects" className={navLinkClassName}>
<FolderKanban size={18} />
Projetos
</NavLink>
)}
{/* Mobile Organizações Group */}
<div className="relative">
@ -479,10 +483,8 @@ export function AppShell() {
<Clock size={18} />
Pedidos
</NavLink>
{user?.organizations.some(
(org) =>
org.roles.includes("OWNER") ||
org.roles.includes("ADMIN"),
{activeOrg?.roles.some(
(role) => role === "OWNER" || role === "ADMIN",
) && (
<NavLink
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 { motion } from 'framer-motion'
import { Users, Check, X, ShieldAlert, Mail } from 'lucide-react'
import { organizationService, type JoinRequest } from '../services/organizationService'
import { useAuth } from '../contexts/AuthContext'
import React, { useEffect, useState } from "react";
import { motion } from "framer-motion";
import { Users, Check, X, ShieldAlert, Mail } from "lucide-react";
import {
organizationService,
type JoinRequest,
} from "../services/organizationService";
import { useAuth } from "../contexts/AuthContext";
export const OrganizationAdminPage: React.FC = () => {
const { user } = useAuth()
const [requests, setRequests] = useState<JoinRequest[]>([])
const [isLoading, setIsLoading] = useState(true)
const { activeOrg } = useAuth();
const [requests, setRequests] = useState<JoinRequest[]>([]);
const [isLoading, setIsLoading] = useState(true);
// For this simplified version, we manage the first organization where the user is OWNER
const orgToManage = user?.organizations.find(org => org.roles.includes('OWNER') || org.roles.includes('ADMIN'))
const orgToManage = activeOrg!;
useEffect(() => {
if (orgToManage) {
loadRequests()
loadRequests();
}
}, [orgToManage])
}, [orgToManage]);
const loadRequests = async () => {
if (!orgToManage) return
if (!orgToManage) return;
try {
const data = await organizationService.listPendingRequests(orgToManage.id)
setRequests(data)
const data = await organizationService.listPendingRequests(
orgToManage.id,
);
setRequests(data);
} catch (error) {
console.error('Failed to load requests', error)
console.error("Failed to load requests", error);
} finally {
setIsLoading(false)
setIsLoading(false);
}
}
};
const handleAction = async (userId: number, approved: boolean) => {
if (!orgToManage) return
if (!orgToManage) return;
try {
if (approved) {
await organizationService.approveRequest(orgToManage.id, userId)
await organizationService.approveRequest(orgToManage.id, userId);
} 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) {
console.error('Action failed', error)
alert('Erro ao processar solicitação.')
console.error("Action failed", error);
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 (
<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">
<Users size={20} />
</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">
{requests.length} novos
</span>
@ -102,12 +96,17 @@ export const OrganizationAdminPage: React.FC = () => {
>
<div className="flex items-center gap-4">
<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}
className="w-12 h-12 rounded-full border border-border-soft object-cover"
/>
<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">
<Mail size={14} />
{request.email}
@ -137,5 +136,5 @@ export const OrganizationAdminPage: React.FC = () => {
</div>
</div>
</div>
)
}
);
};

View file

@ -72,23 +72,6 @@ export const ProjectsPage: React.FC = () => {
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 (
<div className="space-y-12 pb-20">
{/* Header Section */}

View file

@ -13,6 +13,7 @@ import { MyRequestsPage } from "./pages/MyRequestsPage";
import { ProjectsPage } from "./pages/ProjectsPage";
import { OrganizationsPage } from "./pages/OrganizationsPage";
import { DashboardPage } from "./pages/DashboardPage";
import { RoleGate } from "./components/RoleGate";
export const router = createBrowserRouter([
{
@ -53,7 +54,11 @@ export const router = createBrowserRouter([
},
{
path: "admin",
element: <OrganizationAdminPage />,
element: (
<RoleGate allowedRoles={["OWNER", "ADMIN"]} message="Apenas administradores e proprietários podem gerenciar solicitações.">
<OrganizationAdminPage />
</RoleGate>
),
},
{
path: "requests",
@ -61,7 +66,11 @@ export const router = createBrowserRouter([
},
{
path: "projects",
element: <ProjectsPage />,
element: (
<RoleGate allowedRoles={["OWNER", "ADMIN"]} message="Apenas administradores e proprietários podem gerenciar projetos.">
<ProjectsPage />
</RoleGate>
),
},
{
path: "organizations",