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:
308
apps/web/src/routes/(dashboard)/ti/usuarios/+page.svelte
Normal file
308
apps/web/src/routes/(dashboard)/ti/usuarios/+page.svelte
Normal 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}
|
||||
|
||||
Reference in New Issue
Block a user