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:
@@ -1,6 +1,8 @@
|
||||
import { v } from "convex/values";
|
||||
import { mutation, query } from "./_generated/server";
|
||||
import { hashPassword } from "./auth/utils";
|
||||
import { hashPassword, generateToken } from "./auth/utils";
|
||||
import { registrarAtividade } from "./logsAtividades";
|
||||
import { Id } from "./_generated/dataModel";
|
||||
|
||||
/**
|
||||
* Criar novo usuário (apenas TI)
|
||||
@@ -76,6 +78,8 @@ export const listar = query({
|
||||
nome: v.string(),
|
||||
email: v.string(),
|
||||
ativo: v.boolean(),
|
||||
bloqueado: v.optional(v.boolean()),
|
||||
motivoBloqueio: v.optional(v.string()),
|
||||
primeiroAcesso: v.boolean(),
|
||||
ultimoAcesso: v.optional(v.number()),
|
||||
criadoEm: v.number(),
|
||||
@@ -141,6 +145,8 @@ export const listar = query({
|
||||
nome: usuario.nome,
|
||||
email: usuario.email,
|
||||
ativo: usuario.ativo,
|
||||
bloqueado: usuario.bloqueado,
|
||||
motivoBloqueio: usuario.motivoBloqueio,
|
||||
primeiroAcesso: usuario.primeiroAcesso,
|
||||
ultimoAcesso: usuario.ultimoAcesso,
|
||||
criadoEm: usuario.criadoEm,
|
||||
@@ -569,3 +575,362 @@ export const uploadFotoPerfil = mutation({
|
||||
},
|
||||
});
|
||||
|
||||
// ==================== GESTÃO AVANÇADA DE USUÁRIOS (TI_MASTER) ====================
|
||||
|
||||
/**
|
||||
* Bloquear usuário (apenas TI_MASTER)
|
||||
*/
|
||||
export const bloquearUsuario = mutation({
|
||||
args: {
|
||||
usuarioId: v.id("usuarios"),
|
||||
motivo: v.string(),
|
||||
bloqueadoPorId: v.id("usuarios"),
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({ sucesso: v.literal(true) }),
|
||||
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" };
|
||||
}
|
||||
|
||||
// Atualizar usuário como bloqueado
|
||||
await ctx.db.patch(args.usuarioId, {
|
||||
bloqueado: true,
|
||||
motivoBloqueio: args.motivo,
|
||||
dataBloqueio: Date.now(),
|
||||
atualizadoEm: Date.now(),
|
||||
});
|
||||
|
||||
// Registrar no histórico de bloqueios
|
||||
await ctx.db.insert("bloqueiosUsuarios", {
|
||||
usuarioId: args.usuarioId,
|
||||
motivo: args.motivo,
|
||||
bloqueadoPor: args.bloqueadoPorId,
|
||||
dataInicio: Date.now(),
|
||||
ativo: true,
|
||||
});
|
||||
|
||||
// Desativar todas as sessões ativas do usuário
|
||||
const sessoes = await ctx.db
|
||||
.query("sessoes")
|
||||
.withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId))
|
||||
.filter((q) => q.eq(q.field("ativo"), true))
|
||||
.collect();
|
||||
|
||||
for (const sessao of sessoes) {
|
||||
await ctx.db.patch(sessao._id, { ativo: false });
|
||||
}
|
||||
|
||||
// Log de atividade
|
||||
await registrarAtividade(
|
||||
ctx,
|
||||
args.bloqueadoPorId,
|
||||
"bloquear",
|
||||
"usuarios",
|
||||
JSON.stringify({ usuarioId: args.usuarioId, motivo: args.motivo }),
|
||||
args.usuarioId
|
||||
);
|
||||
|
||||
return { sucesso: true as const };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Desbloquear usuário (apenas TI_MASTER)
|
||||
*/
|
||||
export const desbloquearUsuario = mutation({
|
||||
args: {
|
||||
usuarioId: v.id("usuarios"),
|
||||
desbloqueadoPorId: v.id("usuarios"),
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({ sucesso: v.literal(true) }),
|
||||
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" };
|
||||
}
|
||||
|
||||
// Atualizar usuário como desbloqueado
|
||||
await ctx.db.patch(args.usuarioId, {
|
||||
bloqueado: false,
|
||||
motivoBloqueio: undefined,
|
||||
dataBloqueio: undefined,
|
||||
tentativasLogin: 0,
|
||||
ultimaTentativaLogin: undefined,
|
||||
atualizadoEm: Date.now(),
|
||||
});
|
||||
|
||||
// Fechar bloqueios ativos
|
||||
const bloqueiosAtivos = await ctx.db
|
||||
.query("bloqueiosUsuarios")
|
||||
.withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId))
|
||||
.filter((q) => q.eq(q.field("ativo"), true))
|
||||
.collect();
|
||||
|
||||
for (const bloqueio of bloqueiosAtivos) {
|
||||
await ctx.db.patch(bloqueio._id, {
|
||||
ativo: false,
|
||||
dataFim: Date.now(),
|
||||
desbloqueadoPor: args.desbloqueadoPorId,
|
||||
});
|
||||
}
|
||||
|
||||
// Log de atividade
|
||||
await registrarAtividade(
|
||||
ctx,
|
||||
args.desbloqueadoPorId,
|
||||
"desbloquear",
|
||||
"usuarios",
|
||||
JSON.stringify({ usuarioId: args.usuarioId }),
|
||||
args.usuarioId
|
||||
);
|
||||
|
||||
return { sucesso: true as const };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 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" };
|
||||
}
|
||||
|
||||
// Gerar senha temporária se não foi fornecida
|
||||
const senhaTemporaria = args.novaSenhaTemporaria || gerarSenhaTemporaria();
|
||||
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(),
|
||||
});
|
||||
|
||||
// Log de atividade
|
||||
await registrarAtividade(
|
||||
ctx,
|
||||
args.resetadoPorId,
|
||||
"resetar_senha",
|
||||
"usuarios",
|
||||
JSON.stringify({ usuarioId: args.usuarioId }),
|
||||
args.usuarioId
|
||||
);
|
||||
|
||||
return { sucesso: true as const, senhaTemporaria };
|
||||
},
|
||||
});
|
||||
|
||||
// Helper para gerar senha temporária
|
||||
function gerarSenhaTemporaria(): string {
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%";
|
||||
let senha = "";
|
||||
for (let i = 0; i < 12; i++) {
|
||||
senha += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return senha;
|
||||
}
|
||||
|
||||
/**
|
||||
* Editar dados de usuário (apenas TI_MASTER)
|
||||
*/
|
||||
export const editarUsuario = mutation({
|
||||
args: {
|
||||
usuarioId: v.id("usuarios"),
|
||||
nome: v.optional(v.string()),
|
||||
email: v.optional(v.string()),
|
||||
roleId: v.optional(v.id("roles")),
|
||||
setor: v.optional(v.string()),
|
||||
editadoPorId: v.id("usuarios"),
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({ sucesso: v.literal(true) }),
|
||||
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" };
|
||||
}
|
||||
|
||||
// Verificar se email já existe (se estiver mudando)
|
||||
if (args.email && args.email !== usuario.email) {
|
||||
const emailExistente = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_email", (q) => q.eq("email", args.email!))
|
||||
.first();
|
||||
|
||||
if (emailExistente) {
|
||||
return { sucesso: false as const, erro: "E-mail já cadastrado" };
|
||||
}
|
||||
}
|
||||
|
||||
// Atualizar campos fornecidos
|
||||
const updates: any = {
|
||||
atualizadoEm: Date.now(),
|
||||
};
|
||||
|
||||
if (args.nome !== undefined) updates.nome = args.nome;
|
||||
if (args.email !== undefined) updates.email = args.email;
|
||||
if (args.roleId !== undefined) updates.roleId = args.roleId;
|
||||
if (args.setor !== undefined) updates.setor = args.setor;
|
||||
|
||||
await ctx.db.patch(args.usuarioId, updates);
|
||||
|
||||
// Log de atividade
|
||||
await registrarAtividade(
|
||||
ctx,
|
||||
args.editadoPorId,
|
||||
"editar",
|
||||
"usuarios",
|
||||
JSON.stringify(updates),
|
||||
args.usuarioId
|
||||
);
|
||||
|
||||
return { sucesso: true as const };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Desativar usuário logicamente (soft delete - apenas TI_MASTER)
|
||||
*/
|
||||
export const excluirUsuarioLogico = mutation({
|
||||
args: {
|
||||
usuarioId: v.id("usuarios"),
|
||||
excluidoPorId: v.id("usuarios"),
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({ sucesso: v.literal(true) }),
|
||||
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" };
|
||||
}
|
||||
|
||||
// Marcar como inativo
|
||||
await ctx.db.patch(args.usuarioId, {
|
||||
ativo: false,
|
||||
atualizadoEm: Date.now(),
|
||||
});
|
||||
|
||||
// Desativar todas as sessões
|
||||
const sessoes = await ctx.db
|
||||
.query("sessoes")
|
||||
.withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId))
|
||||
.filter((q) => q.eq(q.field("ativo"), true))
|
||||
.collect();
|
||||
|
||||
for (const sessao of sessoes) {
|
||||
await ctx.db.patch(sessao._id, { ativo: false });
|
||||
}
|
||||
|
||||
// Log de atividade
|
||||
await registrarAtividade(
|
||||
ctx,
|
||||
args.excluidoPorId,
|
||||
"excluir",
|
||||
"usuarios",
|
||||
JSON.stringify({ usuarioId: args.usuarioId }),
|
||||
args.usuarioId
|
||||
);
|
||||
|
||||
return { sucesso: true as const };
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Criar usuário completo com permissões (TI_MASTER)
|
||||
*/
|
||||
export const criarUsuarioCompleto = mutation({
|
||||
args: {
|
||||
matricula: v.string(),
|
||||
nome: v.string(),
|
||||
email: v.string(),
|
||||
roleId: v.id("roles"),
|
||||
setor: v.optional(v.string()),
|
||||
senhaInicial: v.optional(v.string()),
|
||||
criadoPorId: v.id("usuarios"),
|
||||
enviarEmailBoasVindas: v.optional(v.boolean()),
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({ sucesso: v.literal(true), usuarioId: v.id("usuarios"), senhaTemporaria: v.string() }),
|
||||
v.object({ sucesso: v.literal(false), erro: v.string() })
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
// Verificar se matrícula já existe
|
||||
const existente = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_matricula", (q) => q.eq("matricula", args.matricula))
|
||||
.first();
|
||||
|
||||
if (existente) {
|
||||
return { sucesso: false as const, erro: "Matrícula já cadastrada" };
|
||||
}
|
||||
|
||||
// Verificar se email já existe
|
||||
const emailExistente = await ctx.db
|
||||
.query("usuarios")
|
||||
.withIndex("by_email", (q) => q.eq("email", args.email))
|
||||
.first();
|
||||
|
||||
if (emailExistente) {
|
||||
return { sucesso: false as const, erro: "E-mail já cadastrado" };
|
||||
}
|
||||
|
||||
// Gerar senha inicial se não fornecida
|
||||
const senhaTemporaria = args.senhaInicial || gerarSenhaTemporaria();
|
||||
const senhaHash = await hashPassword(senhaTemporaria);
|
||||
|
||||
// Criar usuário
|
||||
const usuarioId = await ctx.db.insert("usuarios", {
|
||||
matricula: args.matricula,
|
||||
senhaHash,
|
||||
nome: args.nome,
|
||||
email: args.email,
|
||||
roleId: args.roleId,
|
||||
setor: args.setor,
|
||||
ativo: true,
|
||||
primeiroAcesso: true,
|
||||
criadoEm: Date.now(),
|
||||
atualizadoEm: Date.now(),
|
||||
});
|
||||
|
||||
// Log de atividade
|
||||
await registrarAtividade(
|
||||
ctx,
|
||||
args.criadoPorId,
|
||||
"criar",
|
||||
"usuarios",
|
||||
JSON.stringify({ usuarioId, matricula: args.matricula, nome: args.nome }),
|
||||
usuarioId
|
||||
);
|
||||
|
||||
// TODO: Se enviarEmailBoasVindas = true, enfileirar email
|
||||
// Isso será implementado quando criarmos o sistema de emails
|
||||
|
||||
return { sucesso: true as const, usuarioId, senhaTemporaria };
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user