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>
|
||||
|
||||
{(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>
|
||||
{(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"
|
||||
|
|
|
|||
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 { 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.')
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
console.error("Action failed", error);
|
||||
alert("Erro ao processar solicitação.");
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue