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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user