feat: implement advanced access control system with user blocking, rate limiting, and enhanced login security; update UI components for improved user experience and documentation

This commit is contained in:
2025-10-29 09:07:37 -03:00
parent d1715f358a
commit 6b14059fde
33 changed files with 6450 additions and 1202 deletions

View File

@@ -12,6 +12,40 @@
let salvando = $state(false);
let mensagem = $state<{ tipo: "success" | "error"; texto: string } | null>(null);
let busca = $state("");
let filtroRole = $state("");
function mostrarMensagem(tipo: "success" | "error", texto: string) {
mensagem = { tipo, texto };
setTimeout(() => {
mensagem = null;
}, 3000);
}
const dadosFiltrados = $derived.by(() => {
if (!matrizQuery.data) return [];
let resultado = matrizQuery.data;
// Filtrar por role
if (filtroRole) {
resultado = resultado.filter(r => r.role._id === filtroRole);
}
// Filtrar por busca
if (busca.trim()) {
const buscaLower = busca.toLowerCase();
resultado = resultado.map(roleData => ({
...roleData,
permissoes: roleData.permissoes.filter(p =>
p.menuNome.toLowerCase().includes(buscaLower) ||
p.menuPath.toLowerCase().includes(buscaLower)
)
})).filter(roleData => roleData.permissoes.length > 0);
}
return resultado;
});
async function atualizarPermissao(
roleId: Id<"roles">,
@@ -71,12 +105,9 @@
podeGravar,
});
mensagem = { tipo: "success", texto: "Permissão atualizada com sucesso!" };
setTimeout(() => {
mensagem = null;
}, 3000);
mostrarMensagem("success", "Permissão atualizada com sucesso!");
} catch (e: any) {
mensagem = { tipo: "error", texto: e.message || "Erro ao atualizar permissão" };
mostrarMensagem("error", e.message || "Erro ao atualizar permissão");
} finally {
salvando = false;
}
@@ -86,19 +117,16 @@
try {
salvando = true;
await client.mutation(api.menuPermissoes.inicializarPermissoesRole, { roleId });
mensagem = { tipo: "success", texto: "Permissões inicializadas!" };
setTimeout(() => {
mensagem = null;
}, 3000);
mostrarMensagem("success", "Permissões inicializadas!");
} catch (e: any) {
mensagem = { tipo: "error", texto: e.message || "Erro ao inicializar permissões" };
mostrarMensagem("error", e.message || "Erro ao inicializar permissões");
} finally {
salvando = false;
}
}
</script>
<ProtectedRoute allowedRoles={["admin", "ti"]} maxLevel={1}>
<ProtectedRoute allowedRoles={["ti_master", "admin"]} maxLevel={1}>
<!-- Breadcrumb -->
<div class="text-sm breadcrumbs mb-4">
<ul>
@@ -154,20 +182,119 @@
</div>
{/if}
<!-- Filtros e Busca -->
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Busca por menu -->
<div class="form-control">
<label class="label" for="busca">
<span class="label-text font-semibold">Buscar Menu</span>
</label>
<div class="relative">
<input
id="busca"
type="text"
placeholder="Digite o nome ou caminho do menu..."
class="input input-bordered w-full pr-10"
bind:value={busca}
/>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 absolute right-3 top-3.5 text-base-content/40"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</div>
</div>
<!-- Filtro por perfil -->
<div class="form-control">
<label class="label" for="filtroRole">
<span class="label-text font-semibold">Filtrar por Perfil</span>
</label>
<select
id="filtroRole"
class="select select-bordered w-full"
bind:value={filtroRole}
>
<option value="">Todos os perfis</option>
{#if matrizQuery.data}
{#each matrizQuery.data as roleData}
<option value={roleData.role._id}>
{roleData.role.descricao} ({roleData.role.nome})
</option>
{/each}
{/if}
</select>
</div>
</div>
{#if busca || filtroRole}
<div class="flex items-center gap-2 mt-2">
<span class="text-sm text-base-content/60">Filtros ativos:</span>
{#if busca}
<div class="badge badge-primary gap-2">
Busca: {busca}
<button
class="btn btn-ghost btn-xs"
onclick={() => (busca = "")}
aria-label="Limpar busca"
>
</button>
</div>
{/if}
{#if filtroRole}
<div class="badge badge-secondary gap-2">
Perfil filtrado
<button
class="btn btn-ghost btn-xs"
onclick={() => (filtroRole = "")}
aria-label="Limpar filtro"
>
</button>
</div>
{/if}
</div>
{/if}
</div>
</div>
<!-- Informações sobre o sistema de permissões -->
<div class="alert alert-info mb-6">
<div class="alert alert-info mb-6 shadow-lg">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<h3 class="font-bold">Como funciona:</h3>
<ul class="text-sm mt-2 space-y-1">
<li><strong>Acessar:</strong> Permite visualizar o menu e entrar na página</li>
<li><strong>Consultar:</strong> Permite visualizar dados (requer "Acessar")</li>
<li><strong>Gravar:</strong> Permite criar, editar e excluir dados (requer "Consultar")</li>
<li><strong>Admin e TI:</strong> Têm acesso total automático a todos os recursos</li>
<li><strong>Dashboard e Solicitar Acesso:</strong> São públicos para todos os usuários</li>
</ul>
<h3 class="font-bold text-lg">Como funciona o sistema de permissões:</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
<div>
<h4 class="font-semibold text-sm">Tipos de Permissão:</h4>
<ul class="text-sm mt-1 space-y-1">
<li><strong>Acessar:</strong> Visualizar menu e acessar página</li>
<li><strong>Consultar:</strong> Ver dados (requer "Acessar")</li>
<li><strong>Gravar:</strong> Criar/editar/excluir (requer "Consultar")</li>
</ul>
</div>
<div>
<h4 class="font-semibold text-sm">Perfis Especiais:</h4>
<ul class="text-sm mt-1 space-y-1">
<li><strong>Admin e TI:</strong> Acesso total automático</li>
<li><strong>Dashboard:</strong> Público para todos</li>
<li><strong>Perfil Customizado:</strong> Permissões personalizadas</li>
</ul>
</div>
</div>
</div>
</div>
@@ -184,19 +311,60 @@
<span>Erro ao carregar permissões: {matrizQuery.error.message}</span>
</div>
{:else if matrizQuery.data}
{#each matrizQuery.data as roleData}
{#if dadosFiltrados.length === 0}
<div class="card bg-base-100 shadow-xl">
<div class="card-body items-center text-center">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-16 w-16 text-base-content/30"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
<h3 class="text-xl font-bold mt-4">Nenhum resultado encontrado</h3>
<p class="text-base-content/60">
{busca ? `Não foram encontrados menus com "${busca}"` : "Nenhuma permissão corresponde aos filtros aplicados"}
</p>
<button
class="btn btn-primary btn-sm mt-4"
onclick={() => {
busca = "";
filtroRole = "";
}}
>
Limpar Filtros
</button>
</div>
</div>
{/if}
{#each dadosFiltrados as roleData}
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<div class="flex items-center justify-between mb-4">
<div>
<h2 class="card-title text-xl">
{roleData.role.nome}
<div class="badge badge-primary">Nível {roleData.role.nivel}</div>
<div class="flex items-center justify-between mb-4 flex-wrap gap-4">
<div class="flex-1 min-w-[200px]">
<div class="flex items-center gap-3 mb-2">
<h2 class="card-title text-2xl">{roleData.role.descricao}</h2>
<div class="badge badge-lg badge-primary">Nível {roleData.role.nivel}</div>
{#if roleData.role.nivel <= 1}
<div class="badge badge-success">Acesso Total</div>
<div class="badge badge-lg badge-success gap-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Acesso Total
</div>
{/if}
</h2>
<p class="text-sm text-base-content/60 mt-1">{roleData.role.descricao}</p>
</div>
<p class="text-sm text-base-content/60">
<span class="font-mono bg-base-200 px-2 py-1 rounded">{roleData.role.nome}</span>
</p>
</div>
{#if roleData.role.nivel > 1}
@@ -214,13 +382,35 @@
</div>
{#if roleData.role.nivel <= 1}
<div class="alert alert-success">
<div class="alert alert-success shadow-md">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Esta função possui acesso total ao sistema automaticamente.</span>
<div>
<h3 class="font-bold">Perfil Administrativo</h3>
<div class="text-sm">Este perfil possui acesso total ao sistema automaticamente, sem necessidade de configuração manual.</div>
</div>
</div>
{:else}
<div class="stats stats-vertical lg:stats-horizontal shadow mb-4 w-full">
<div class="stat">
<div class="stat-title">Total de Menus</div>
<div class="stat-value text-primary">{roleData.permissoes.length}</div>
</div>
<div class="stat">
<div class="stat-title">Com Acesso</div>
<div class="stat-value text-info">{roleData.permissoes.filter(p => p.podeAcessar).length}</div>
</div>
<div class="stat">
<div class="stat-title">Pode Consultar</div>
<div class="stat-value text-success">{roleData.permissoes.filter(p => p.podeConsultar).length}</div>
</div>
<div class="stat">
<div class="stat-title">Pode Gravar</div>
<div class="stat-value text-warning">{roleData.permissoes.filter(p => p.podeGravar).length}</div>
</div>
</div>
<div class="overflow-x-auto">
<table class="table table-zebra table-sm">
<thead class="bg-base-200">