feat: add password reset functionality for users, including modal interface for generating temporary passwords and copying to clipboard; enhance backend mutation for secure password management and email notifications

This commit is contained in:
2025-12-09 07:41:19 -03:00
parent 4110b12724
commit 2172d9a937
2 changed files with 275 additions and 39 deletions

View File

@@ -18,7 +18,9 @@
Check,
UserPlus,
Search,
MoreVertical
MoreVertical,
Copy,
CheckCircle2
} from 'lucide-svelte';
type AvisoUsuario = {
@@ -189,6 +191,9 @@
let buscaFuncionario = $state('');
let modalExcluirAberto = $state(false);
let modalResetarSenhaAberto = $state(false);
let novaSenhaGerada = $state<string | null>(null);
let senhaCopiada = $state(false);
let processando = $state(false);
let mensagem = $state<Mensagem | null>(null);
@@ -525,6 +530,68 @@
usuarioSelecionado = null;
}
function abrirModalResetarSenha(usuario: Usuario) {
usuarioSelecionado = usuario;
novaSenhaGerada = null;
modalResetarSenhaAberto = true;
}
function fecharModalResetarSenha() {
modalResetarSenhaAberto = false;
novaSenhaGerada = null;
senhaCopiada = false;
usuarioSelecionado = null;
}
async function copiarSenha() {
if (!novaSenhaGerada) return;
try {
await navigator.clipboard.writeText(novaSenhaGerada);
senhaCopiada = true;
setTimeout(() => {
senhaCopiada = false;
}, 2000);
} catch (error) {
console.error('Erro ao copiar senha:', error);
}
}
async function resetarSenha() {
if (!usuarioSelecionado || !currentUser?.data) {
console.error('Erro: usuarioSelecionado ou currentUser não definido');
mostrarMensagem('error', 'Erro: dados do usuário não encontrados');
return;
}
try {
processando = true;
console.log('Iniciando reset de senha para:', usuarioSelecionado._id);
const resultado = await client.mutation(api.usuarios.resetarSenhaUsuario, {
usuarioId: usuarioSelecionado._id,
resetadoPorId: currentUser.data._id
});
console.log('Resultado do reset:', resultado);
if (resultado && resultado.sucesso) {
novaSenhaGerada = resultado.senhaTemporaria;
mostrarMensagem('success', 'Senha resetada com sucesso! Email enviado ao usuário.');
} else {
const erroMsg = resultado?.erro || 'Erro ao resetar senha';
console.error('Erro no reset:', erroMsg);
mostrarMensagem('error', erroMsg);
}
} catch (error) {
console.error('Erro ao resetar senha:', error);
const errorMessage = error instanceof Error ? error.message : 'Erro desconhecido ao resetar senha';
mostrarMensagem('error', errorMessage);
} finally {
processando = false;
}
}
async function excluirUsuario() {
if (!usuarioSelecionado) {
return;
@@ -912,6 +979,17 @@
{usuario.funcionario ? 'Alterar Funcionário' : 'Associar Funcionário'}
</button>
</li>
<li>
<button
type="button"
onclick={() => abrirModalResetarSenha(usuario)}
disabled={processando}
class="text-warning"
>
Resetar Senha
</button>
</li>
<div class="divider my-0"></div>
<li>
{#if usuario.bloqueado}
<button
@@ -1133,4 +1211,103 @@
</div>
</div>
{/if}
<!-- Modal Resetar Senha -->
{#if modalResetarSenhaAberto && usuarioSelecionado}
<div class="modal modal-open">
<div class="modal-box">
<h3 class="mb-4 text-lg font-bold">Resetar Senha do Usuário</h3>
<div class="mb-4">
<p class="text-base-content/80 mb-2">
<strong>Usuário:</strong>
{usuarioSelecionado.nome} ({usuarioSelecionado.matricula})
</p>
<p class="text-base-content/70 mb-4 text-sm">
Uma nova senha temporária será gerada e enviada por email ao usuário.
</p>
{#if novaSenhaGerada}
<div class="alert alert-success mb-4">
<CheckCircle class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
<div class="flex-1 w-full">
<p class="font-semibold text-base">Senha resetada com sucesso!</p>
<p class="mt-3 text-sm font-medium">
<strong>Nova senha temporária:</strong>
</p>
<div class="bg-base-200 border-base-300 mt-2 flex items-center justify-between gap-2 rounded-lg border p-4">
<code class="text-xl font-mono font-bold select-all">{novaSenhaGerada}</code>
<button
type="button"
class="btn btn-sm btn-ghost shrink-0"
onclick={copiarSenha}
aria-label="Copiar senha"
title="Copiar senha"
>
{#if senhaCopiada}
<CheckCircle2 class="h-5 w-5 text-success" strokeWidth={2} />
{:else}
<Copy class="h-5 w-5" strokeWidth={2} />
{/if}
</button>
</div>
<p class="mt-3 text-xs text-base-content/70">
✓ Esta senha foi enviada por email para <strong>{usuarioSelecionado.email}</strong>
</p>
<p class="mt-1 text-xs text-base-content/60">
O usuário precisará alterar a senha no próximo login.
</p>
</div>
</div>
{:else}
<div class="alert alert-warning">
<AlertTriangle class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
<span>
O usuário precisará alterar a senha no próximo login. Todas as sessões ativas serão encerradas.
</span>
</div>
{/if}
</div>
<div class="modal-action">
{#if novaSenhaGerada}
<button type="button" class="btn btn-primary" onclick={fecharModalResetarSenha}>
Fechar
</button>
{:else}
<button
type="button"
class="btn"
onclick={fecharModalResetarSenha}
disabled={processando}
>
Cancelar
</button>
<button
type="button"
class="btn btn-warning"
onclick={resetarSenha}
disabled={processando}
>
{#if processando}
<span class="loading loading-spinner loading-sm"></span>
{/if}
Resetar Senha
</button>
{/if}
</div>
</div>
<div class="modal-backdrop">
<button
type="button"
onclick={fecharModalResetarSenha}
onkeydown={(e) => e.key === 'Escape' && fecharModalResetarSenha()}
aria-label="Fechar modal"
class="sr-only"
>
Fechar
</button>
</div>
</div>
{/if}
</ProtectedRoute>

View File

@@ -841,48 +841,107 @@ export const desbloquearUsuario = mutation({
/**
* Resetar senha de usuário (apenas TI_MASTER)
*/
// export const resetarSenhaUsuario = mutation({
// args: {
// usuarioId: v.id("usuarios"),
// resetadoPorId: v.id("usuarios"),
// novaSenhaTemporaria: v.optional(v.string()), // Se não fornecer, gera automática
// },
// returns: v.union(
// v.object({ sucesso: v.literal(true), senhaTemporaria: v.string() }),
// v.object({ sucesso: v.literal(false), erro: v.string() })
// ),
// handler: async (ctx, args) => {
// const usuario = await ctx.db.get(args.usuarioId);
// if (!usuario) {
// return { sucesso: false as const, erro: "Usuário não encontrado" };
// }
export const resetarSenhaUsuario = mutation({
args: {
usuarioId: v.id('usuarios'),
resetadoPorId: v.id('usuarios'),
novaSenhaTemporaria: v.optional(v.string()) // Se não fornecer, gera automática
},
returns: v.union(
v.object({ sucesso: v.literal(true), senhaTemporaria: v.string() }),
v.object({ sucesso: v.literal(false), erro: v.string() })
),
handler: async (ctx, args) => {
const usuario = await ctx.db.get(args.usuarioId);
if (!usuario) {
return { sucesso: false as const, erro: 'Usuário não encontrado' };
}
// // Gerar senha temporária se não foi fornecida
// const senhaTemporaria = args.novaSenhaTemporaria || gerarSenhaTemporaria();
// const senhaHash = await hashPassword(senhaTemporaria);
// Verificar permissão (apenas TI_MASTER)
const resetadoPor = await ctx.db.get(args.resetadoPorId);
if (!resetadoPor) {
return { sucesso: false as const, erro: 'Usuário que está resetando não encontrado' };
}
// // Atualizar usuário
// await ctx.db.patch(args.usuarioId, {
// senhaHash,
// primeiroAcesso: true, // Força mudança de senha no próximo login
// tentativasLogin: 0,
// ultimaTentativaLogin: undefined,
// atualizadoEm: Date.now(),
// });
// Buscar a role do usuário
if (!resetadoPor.roleId) {
return { sucesso: false as const, erro: 'Usuário não possui role definida' };
}
// // Log de atividade
// await registrarAtividade(
// ctx,
// args.resetadoPorId,
// "resetar_senha",
// "usuarios",
// JSON.stringify({ usuarioId: args.usuarioId }),
// args.usuarioId
// );
const role = await ctx.db.get(resetadoPor.roleId);
if (!role) {
return { sucesso: false as const, erro: 'Role do usuário não encontrada' };
}
// return { sucesso: true as const, senhaTemporaria };
// },
// });
// Permitir TI_MASTER, TI_USUARIO e ADMIN
const rolesPermitidas = ['ti_master', 'ti_usuario', 'admin'];
if (!rolesPermitidas.includes(role.nome)) {
return {
sucesso: false as const,
erro: 'Apenas usuários de TI ou administradores podem resetar senhas'
};
}
// Gerar senha temporária se não foi fornecida
const senhaTemporaria = args.novaSenhaTemporaria || gerarSenhaTemporaria();
try {
// Fazer hash da senha
const { hashPassword } = await import('./auth/utils');
const senhaHash = await hashPassword(senhaTemporaria);
// Atualizar usuário
await ctx.db.patch(args.usuarioId, {
senhaHash,
primeiroAcesso: true, // Força mudança de senha no próximo login
tentativasLogin: 0,
ultimaTentativaLogin: undefined,
atualizadoEm: Date.now()
});
// Desativar todas as sessões ativas
const sessoes = await ctx.db
.query('sessoes')
.withIndex('by_usuario', (q) => q.eq('usuarioId', args.usuarioId))
.collect();
for (const sessao of sessoes) {
await ctx.db.patch(sessao._id, { ativo: false });
}
// Enviar email com a nova senha usando template
try {
await ctx.scheduler.runAfter(0, api.email.enviarEmailComTemplate, {
destinatario: usuario.email,
destinatarioId: args.usuarioId,
templateCodigo: 'SENHA_RESETADA',
variaveis: {
senha: senhaTemporaria
},
enviadoPor: args.resetadoPorId
});
} catch (emailError) {
console.error('Erro ao agendar envio de email:', emailError);
// Não falhar a mutation se o email falhar, apenas logar o erro
}
// Log de atividade
await registrarAtividade(
ctx,
args.resetadoPorId,
'resetar_senha',
'usuarios',
JSON.stringify({ usuarioId: args.usuarioId }),
args.usuarioId
);
return { sucesso: true as const, senhaTemporaria };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return { sucesso: false as const, erro: `Erro ao resetar senha: ${errorMessage}` };
}
}
});
// Helper para gerar senha temporária
function gerarSenhaTemporaria(): string {