From 2172d9a937face5ef63bec099932f9fc5ca7b90a Mon Sep 17 00:00:00 2001 From: deyvisonwanderley Date: Tue, 9 Dec 2025 07:41:19 -0300 Subject: [PATCH] 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 --- .../(dashboard)/ti/usuarios/+page.svelte | 179 +++++++++++++++++- packages/backend/convex/usuarios.ts | 135 +++++++++---- 2 files changed, 275 insertions(+), 39 deletions(-) diff --git a/apps/web/src/routes/(dashboard)/ti/usuarios/+page.svelte b/apps/web/src/routes/(dashboard)/ti/usuarios/+page.svelte index dfd915f..91bb961 100644 --- a/apps/web/src/routes/(dashboard)/ti/usuarios/+page.svelte +++ b/apps/web/src/routes/(dashboard)/ti/usuarios/+page.svelte @@ -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(null); + let senhaCopiada = $state(false); let processando = $state(false); let mensagem = $state(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'} +
  • + +
  • +
  • {#if usuario.bloqueado} + +

    + ✓ Esta senha foi enviada por email para {usuarioSelecionado.email} +

    +

    + O usuário precisará alterar a senha no próximo login. +

    + + + {:else} +
    + + + O usuário precisará alterar a senha no próximo login. Todas as sessões ativas serão encerradas. + +
    + {/if} + + + + + + + {/if} diff --git a/packages/backend/convex/usuarios.ts b/packages/backend/convex/usuarios.ts index f8bc3f9..1b5ec65 100644 --- a/packages/backend/convex/usuarios.ts +++ b/packages/backend/convex/usuarios.ts @@ -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 {