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

@@ -0,0 +1,308 @@
<script lang="ts">
import { useQuery, useConvexClient } from "convex-svelte";
import { api } from "@sgse-app/backend/convex/_generated/api";
import { authStore } from "$lib/stores/auth.svelte";
import UserStatusBadge from "$lib/components/ti/UserStatusBadge.svelte";
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
import { goto } from "$app/navigation";
const client = useConvexClient();
const usuarios = useQuery(api.usuarios.listar, {});
let filtroNome = $state("");
let filtroStatus = $state<"todos" | "ativo" | "bloqueado" | "inativo">("todos");
let usuarioSelecionado = $state<any>(null);
let modalAberto = $state(false);
let modalAcao = $state<"bloquear" | "desbloquear" | "reset">("bloquear");
let motivo = $state("");
let processando = $state(false);
// Usuários filtrados
const usuariosFiltrados = $derived.by(() => {
if (!usuarios?.data || !Array.isArray(usuarios.data)) return [];
return usuarios.data.filter(u => {
const matchNome = !filtroNome ||
u.nome.toLowerCase().includes(filtroNome.toLowerCase()) ||
u.matricula.includes(filtroNome) ||
u.email?.toLowerCase().includes(filtroNome.toLowerCase());
const matchStatus = filtroStatus === "todos" ||
(filtroStatus === "ativo" && u.ativo && !u.bloqueado) ||
(filtroStatus === "bloqueado" && u.bloqueado) ||
(filtroStatus === "inativo" && !u.ativo);
return matchNome && matchStatus;
});
});
const stats = $derived.by(() => {
if (!usuarios?.data || !Array.isArray(usuarios.data)) return null;
return {
total: usuarios.data.length,
ativos: usuarios.data.filter(u => u.ativo && !u.bloqueado).length,
bloqueados: usuarios.data.filter(u => u.bloqueado).length,
inativos: usuarios.data.filter(u => !u.ativo).length
};
});
function abrirModal(usuario: any, acao: typeof modalAcao) {
usuarioSelecionado = usuario;
modalAcao = acao;
motivo = "";
modalAberto = true;
}
function fecharModal() {
modalAberto = false;
usuarioSelecionado = null;
motivo = "";
}
async function executarAcao() {
if (!usuarioSelecionado) return;
if (!authStore.usuario) {
alert("Usuário não autenticado");
return;
}
processando = true;
try {
if (modalAcao === "bloquear") {
await client.mutation(api.usuarios.bloquearUsuario, {
usuarioId: usuarioSelecionado._id as Id<"usuarios">,
motivo,
bloqueadoPorId: authStore.usuario._id as Id<"usuarios">
});
} else if (modalAcao === "desbloquear") {
await client.mutation(api.usuarios.desbloquearUsuario, {
usuarioId: usuarioSelecionado._id as Id<"usuarios">,
desbloqueadoPorId: authStore.usuario._id as Id<"usuarios">
});
} else if (modalAcao === "reset") {
await client.mutation(api.usuarios.resetarSenhaUsuario, {
usuarioId: usuarioSelecionado._id as Id<"usuarios">,
resetadoPorId: authStore.usuario._id as Id<"usuarios">
});
}
fecharModal();
} catch (error) {
console.error("Erro ao executar ação:", error);
alert("Erro ao executar ação. Veja o console.");
} finally {
processando = false;
}
}
</script>
<div class="container mx-auto px-4 py-6 max-w-7xl">
<!-- Header -->
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-3xl font-bold text-base-content">Gestão de Usuários</h1>
<p class="text-base-content/60 mt-1">Gerenciar usuários do sistema</p>
</div>
<a href="/ti/usuarios/criar" class="btn btn-primary">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
Criar Usuário
</a>
</div>
<!-- Stats -->
{#if stats}
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div class="stat bg-base-100 shadow rounded-lg">
<div class="stat-title">Total</div>
<div class="stat-value text-primary">{stats.total}</div>
</div>
<div class="stat bg-base-100 shadow rounded-lg">
<div class="stat-title">Ativos</div>
<div class="stat-value text-success">{stats.ativos}</div>
</div>
<div class="stat bg-base-100 shadow rounded-lg">
<div class="stat-title">Bloqueados</div>
<div class="stat-value text-error">{stats.bloqueados}</div>
</div>
<div class="stat bg-base-100 shadow rounded-lg">
<div class="stat-title">Inativos</div>
<div class="stat-value text-warning">{stats.inativos}</div>
</div>
</div>
{/if}
<!-- Filtros -->
<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">
<div class="form-control">
<label class="label">
<span class="label-text">Buscar por nome, matrícula ou email</span>
</label>
<input
type="text"
bind:value={filtroNome}
placeholder="Digite para buscar..."
class="input input-bordered"
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Filtrar por status</span>
</label>
<select bind:value={filtroStatus} class="select select-bordered">
<option value="todos">Todos</option>
<option value="ativo">Ativos</option>
<option value="bloqueado">Bloqueados</option>
<option value="inativo">Inativos</option>
</select>
</div>
</div>
</div>
</div>
<!-- Tabela -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title mb-4">
Usuários ({usuariosFiltrados.length})
</h2>
<div class="overflow-x-auto">
<table class="table table-zebra">
<thead>
<tr>
<th>Matrícula</th>
<th>Nome</th>
<th>Email</th>
<th>Status</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{#each usuariosFiltrados as usuario}
<tr>
<td class="font-mono">{usuario.matricula}</td>
<td>{usuario.nome}</td>
<td>{usuario.email || "-"}</td>
<td>
<UserStatusBadge ativo={usuario.ativo} bloqueado={usuario.bloqueado} />
</td>
<td>
<div class="flex gap-2">
{#if usuario.bloqueado}
<button
class="btn btn-sm btn-success"
onclick={() => abrirModal(usuario, "desbloquear")}
>
<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="M8 11V7a4 4 0 118 0m-4 8v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2z" />
</svg>
Desbloquear
</button>
{:else}
<button
class="btn btn-sm btn-error"
onclick={() => abrirModal(usuario, "bloquear")}
>
<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="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
Bloquear
</button>
{/if}
<button
class="btn btn-sm btn-warning"
onclick={() => abrirModal(usuario, "reset")}
>
<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="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
</svg>
Reset Senha
</button>
</div>
</td>
</tr>
{:else}
<tr>
<td colspan="5" class="text-center py-8 text-base-content/60">
Nenhum usuário encontrado
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Modal -->
{#if modalAberto}
<div class="modal modal-open">
<div class="modal-box">
<h3 class="font-bold text-lg mb-4">
{modalAcao === "bloquear" ? "Bloquear Usuário" :
modalAcao === "desbloquear" ? "Desbloquear Usuário" :
"Resetar Senha"}
</h3>
<div class="mb-4">
<p class="text-base-content/80">
<strong>Usuário:</strong> {usuarioSelecionado?.nome} ({usuarioSelecionado?.matricula})
</p>
</div>
{#if modalAcao === "bloquear"}
<div class="form-control mb-4">
<label class="label">
<span class="label-text">Motivo do bloqueio *</span>
</label>
<textarea
bind:value={motivo}
class="textarea textarea-bordered"
placeholder="Digite o motivo..."
rows="3"
></textarea>
</div>
{/if}
{#if modalAcao === "reset"}
<div class="alert alert-info mb-4">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
<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"></path>
</svg>
<span>Uma senha temporária será gerada automaticamente.</span>
</div>
{/if}
<div class="modal-action">
<button
class="btn btn-ghost"
onclick={fecharModal}
disabled={processando}
>
Cancelar
</button>
<button
class="btn btn-primary"
onclick={executarAcao}
disabled={processando || (modalAcao === "bloquear" && !motivo.trim())}
>
{#if processando}
<span class="loading loading-spinner loading-sm"></span>
{/if}
Confirmar
</button>
</div>
</div>
<div class="modal-backdrop" onclick={fecharModal}></div>
</div>
{/if}