Files
sgse-app/packages/backend/convex/usuarios.ts

1111 lines
29 KiB
TypeScript

import { v } from 'convex/values';
import { mutation, query } from './_generated/server';
import { api } from './_generated/api';
import { registrarAtividade } from './logsAtividades';
import { Id, Doc } from './_generated/dataModel';
import type { QueryCtx } from './_generated/server';
import { createAuthUser, getCurrentUserFunction } from './auth';
/**
* Helper para obter a matrícula do usuário (do funcionário se houver)
*/
async function obterMatriculaUsuario(
ctx: QueryCtx,
usuario: Doc<'usuarios'>
): Promise<string | undefined> {
if (usuario.funcionarioId) {
const funcionario = await ctx.db.get(usuario.funcionarioId);
return funcionario?.matricula;
}
return undefined;
}
/**
* Associar funcionário a um usuário
*/
export const associarFuncionario = mutation({
args: {
usuarioId: v.id('usuarios'),
funcionarioId: v.id('funcionarios')
},
returns: v.object({ sucesso: v.boolean() }),
handler: async (ctx, args) => {
// Verificar se o funcionário existe
const funcionario = await ctx.db.get(args.funcionarioId);
if (!funcionario) {
throw new Error('Funcionário não encontrado');
}
// Verificar se o funcionário já está associado a outro usuário
const usuarioExistente = await ctx.db
.query('usuarios')
.withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', args.funcionarioId))
.first();
if (usuarioExistente && usuarioExistente._id !== args.usuarioId) {
const matricula = await obterMatriculaUsuario(ctx, usuarioExistente);
throw new Error(
`Este funcionário já está associado ao usuário: ${
usuarioExistente.nome
}${matricula ? ` (${matricula})` : ''}`
);
}
// Associar funcionário ao usuário
await ctx.db.patch(args.usuarioId, {
funcionarioId: args.funcionarioId
});
return { sucesso: true };
}
});
/**
* Desassociar funcionário de um usuário
*/
export const desassociarFuncionario = mutation({
args: {
usuarioId: v.id('usuarios')
},
returns: v.object({ sucesso: v.boolean() }),
handler: async (ctx, args) => {
await ctx.db.patch(args.usuarioId, {
funcionarioId: undefined
});
return { sucesso: true };
}
});
/**
* Criar novo usuário (apenas TI)
*/
export const criar = mutation({
args: {
nome: v.string(),
email: v.string(),
roleId: v.id('roles'),
funcionarioId: v.optional(v.id('funcionarios')),
senhaInicial: v.string()
},
returns: v.union(
v.object({ sucesso: v.literal(true), usuarioId: v.id('usuarios') }),
v.object({ sucesso: v.literal(false), erro: v.string() })
),
handler: async (ctx, args) => {
// 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' };
}
const senhaTemporaria = args.senhaInicial;
const authUserId = await createAuthUser(ctx, {
nome: args.nome,
email: args.email,
password: senhaTemporaria
});
// Criar usuário
const usuarioId = await ctx.db.insert('usuarios', {
authId: authUserId,
nome: args.nome,
email: args.email,
funcionarioId: args.funcionarioId,
roleId: args.roleId,
ativo: true,
primeiroAcesso: true,
criadoEm: Date.now(),
atualizadoEm: Date.now()
});
return { sucesso: true as const, usuarioId };
}
});
/**
* Listar todos os usuários com filtros
*/
export const listar = query({
args: {
setor: v.optional(v.string()),
matricula: v.optional(v.string()),
ativo: v.optional(v.boolean())
},
handler: async (ctx, args) => {
let usuarios = await ctx.db.query('usuarios').collect();
// Filtrar por matrícula (buscar no funcionário)
if (args.matricula) {
const usuariosComMatricula = await Promise.all(
usuarios.map(async (u) => {
const matricula = await obterMatriculaUsuario(ctx, u);
return { usuario: u, matricula };
})
);
usuarios = usuariosComMatricula
.filter(({ matricula }) => matricula?.includes(args.matricula!))
.map(({ usuario }) => usuario);
}
// Filtrar por ativo
if (args.ativo !== undefined) {
usuarios = usuarios.filter((u) => u.ativo === args.ativo);
}
// Buscar roles e funcionários
const resultado = [];
const usuariosSemRole: Array<{
nome: string;
matricula: string;
roleId: Id<'roles'>;
}> = [];
for (const usuario of usuarios) {
try {
const role = await ctx.db.get(usuario.roleId);
// Se a role não existe, criar uma role de erro mas ainda incluir o usuário
if (!role) {
const matricula = await obterMatriculaUsuario(ctx, usuario);
usuariosSemRole.push({
nome: usuario.nome,
matricula: matricula || 'N/A',
roleId: usuario.roleId
});
// Filtrar por setor - se filtro está ativo e role não existe, pular
if (args.setor) {
continue;
}
// Incluir usuário com role de erro
let funcionario = undefined;
if (usuario.funcionarioId) {
try {
const func = await ctx.db.get(usuario.funcionarioId);
if (func) {
funcionario = {
_id: func._id,
nome: func.nome,
matricula: func.matricula,
descricaoCargo: func.descricaoCargo,
simboloTipo: func.simboloTipo
};
}
} catch (error) {
console.error(
`Erro ao buscar funcionário ${usuario.funcionarioId} para usuário ${usuario._id}:`,
error
);
}
}
const matriculaUsuario = await obterMatriculaUsuario(ctx, usuario);
// Criar role de erro (sem _creationTime pois a role não existe)
resultado.push({
_id: usuario._id,
matricula: matriculaUsuario,
nome: usuario.nome,
email: usuario.email,
ativo: usuario.ativo,
bloqueado: usuario.bloqueado,
motivoBloqueio: usuario.motivoBloqueio,
primeiroAcesso: usuario.primeiroAcesso,
ultimoAcesso: usuario.ultimoAcesso,
criadoEm: usuario.criadoEm,
role: {
_id: usuario.roleId,
descricao: 'Perfil não encontrado' as const,
nome: 'erro_role_ausente' as const,
nivel: 999 as const,
erro: true as const
},
funcionario,
avisos: [
{
tipo: 'erro' as const,
mensagem: `Perfil de acesso (ID: ${usuario.roleId}) não encontrado. Este usuário precisa ter seu perfil reatribuído.`
}
]
});
continue;
}
// Filtrar por setor
if (args.setor && role.setor !== args.setor) {
continue;
}
// Buscar funcionário associado
let funcionario = undefined;
if (usuario.funcionarioId) {
try {
const func = await ctx.db.get(usuario.funcionarioId);
if (func) {
funcionario = {
_id: func._id,
nome: func.nome,
matricula: func.matricula,
descricaoCargo: func.descricaoCargo,
simboloTipo: func.simboloTipo
};
}
} catch (error) {
console.error(
`Erro ao buscar funcionário ${usuario.funcionarioId} para usuário ${usuario._id}:`,
error
);
}
}
// Construir objeto role - incluir _creationTime se existir (campo automático do Convex)
const roleObj = {
_id: role._id,
descricao: role.descricao,
nome: role.nome,
nivel: role.nivel,
...(role.criadoPor !== undefined && { criadoPor: role.criadoPor }),
...(role.customizado !== undefined && {
customizado: role.customizado
}),
...(role.editavel !== undefined && { editavel: role.editavel }),
...(role.setor !== undefined && { setor: role.setor })
};
const matriculaUsuario = await obterMatriculaUsuario(ctx, usuario);
resultado.push({
_id: usuario._id,
matricula: matriculaUsuario,
nome: usuario.nome,
email: usuario.email,
ativo: usuario.ativo,
bloqueado: usuario.bloqueado,
motivoBloqueio: usuario.motivoBloqueio,
primeiroAcesso: usuario.primeiroAcesso,
ultimoAcesso: usuario.ultimoAcesso,
criadoEm: usuario.criadoEm,
role: roleObj,
funcionario
});
} catch (error) {
console.error(`Erro ao processar usuário ${usuario._id}:`, error);
// Continua processando outros usuários mesmo se houver erro em um
}
}
// Log de usuários sem role para depuração
if (usuariosSemRole.length > 0) {
console.warn(
`⚠️ Encontrados ${usuariosSemRole.length} usuário(s) com perfil ausente:`,
usuariosSemRole.map(
(u) =>
`${u.nome}${u.matricula !== 'N/A' ? ` (${u.matricula})` : ''} - RoleID: ${u.roleId}`
)
);
}
return resultado;
}
});
/**
* Ativar/Desativar usuário
*/
export const alterarStatus = mutation({
args: {
usuarioId: v.id('usuarios'),
ativo: v.boolean()
},
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.patch(args.usuarioId, {
ativo: args.ativo,
atualizadoEm: Date.now()
});
// Se desativar, desativar todas as sessões
if (!args.ativo) {
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 });
}
}
return null;
}
});
/**
* Resetar senha do usuário
*/
// export const resetarSenha = mutation({
// args: {
// usuarioId: v.id("usuarios"),
// novaSenha: v.string(),
// },
// returns: v.null(),
// handler: async (ctx, args) => {
// const senhaHash = await hashPassword(args.novaSenha);
// await ctx.db.patch(args.usuarioId, {
// senhaHash,
// primeiroAcesso: true,
// atualizadoEm: Date.now(),
// });
// // Desativar todas as sessões
// 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 });
// }
// return null;
// },
// });
/**
* Excluir usuário
*/
export const excluir = mutation({
args: {
usuarioId: v.id('usuarios')
},
returns: v.null(),
handler: async (ctx, args) => {
// Excluir sessões
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.delete(sessao._id);
}
// Excluir usuário
await ctx.db.delete(args.usuarioId);
return null;
}
});
/**
* Ativar usuário
*/
export const ativar = mutation({
args: {
id: v.id('usuarios')
},
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.patch(args.id, {
ativo: true,
atualizadoEm: Date.now()
});
return null;
}
});
/**
* Desativar usuário
*/
export const desativar = mutation({
args: {
id: v.id('usuarios')
},
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.patch(args.id, {
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.id))
.collect();
for (const sessao of sessoes) {
await ctx.db.patch(sessao._id, { ativo: false });
}
return null;
}
});
/**
* Alterar role de um usuário
*/
export const alterarRole = mutation({
args: {
usuarioId: v.id('usuarios'),
novaRoleId: v.id('roles')
},
returns: v.null(),
handler: async (ctx, args) => {
// Verificar se a role existe
const role = await ctx.db.get(args.novaRoleId);
if (!role) {
throw new Error('Role não encontrada');
}
// Atualizar usuário
await ctx.db.patch(args.usuarioId, {
roleId: args.novaRoleId,
atualizadoEm: Date.now()
});
return null;
}
});
/**
* Atualizar perfil do usuário (foto, setor, status, preferências)
*/
export const atualizarPerfil = mutation({
args: {
fotoPerfil: v.optional(v.id('_storage')),
avatar: v.optional(v.string()), // URL do avatar gerado (ex: DiceBear)
setor: v.optional(v.string()),
statusMensagem: v.optional(v.string()),
statusPresenca: v.optional(
v.union(
v.literal('online'),
v.literal('offline'),
v.literal('ausente'),
v.literal('externo'),
v.literal('em_reuniao')
)
),
notificacoesAtivadas: v.optional(v.boolean()),
somNotificacao: v.optional(v.boolean()),
temaPreferido: v.optional(v.string())
},
returns: v.null(),
handler: async (ctx, args) => {
const usuarioAtual = await getCurrentUserFunction(ctx);
if (!usuarioAtual) throw new Error('Usuário não encontrado');
// Validar statusMensagem (max 100 chars)
if (args.statusMensagem && args.statusMensagem.length > 100) {
throw new Error('Mensagem de status deve ter no máximo 100 caracteres');
}
// Atualizar apenas os campos fornecidos
const updates: Partial<Doc<'usuarios'>> & { atualizadoEm: number } = {
atualizadoEm: Date.now()
};
if (args.fotoPerfil !== undefined) updates.fotoPerfil = args.fotoPerfil;
if (args.avatar !== undefined) updates.avatar = args.avatar;
if (args.setor !== undefined) updates.setor = args.setor;
if (args.statusMensagem !== undefined) updates.statusMensagem = args.statusMensagem;
if (args.statusPresenca !== undefined) {
updates.statusPresenca = args.statusPresenca;
updates.ultimaAtividade = Date.now();
}
if (args.notificacoesAtivadas !== undefined)
updates.notificacoesAtivadas = args.notificacoesAtivadas;
if (args.somNotificacao !== undefined) updates.somNotificacao = args.somNotificacao;
if (args.temaPreferido !== undefined) updates.temaPreferido = args.temaPreferido;
await ctx.db.patch(usuarioAtual._id, updates);
return null;
}
});
/**
* Atualizar tema preferido do usuário
*/
export const atualizarTema = mutation({
args: {
temaPreferido: v.string()
},
returns: v.object({ sucesso: v.boolean() }),
handler: async (ctx, args) => {
const usuarioAtual = await getCurrentUserFunction(ctx);
if (!usuarioAtual) {
throw new Error('Usuário não encontrado');
}
await ctx.db.patch(usuarioAtual._id, {
temaPreferido: args.temaPreferido,
atualizadoEm: Date.now()
});
return { sucesso: true };
}
});
/**
* Obter perfil do usuário atual
*/
export const obterPerfil = query({
args: {},
returns: v.union(
v.object({
_id: v.id('usuarios'),
nome: v.string(),
email: v.string(),
matricula: v.optional(v.string()),
funcionarioId: v.optional(v.id('funcionarios')),
fotoPerfil: v.optional(v.id('_storage')),
fotoPerfilUrl: v.union(v.string(), v.null()),
avatar: v.optional(v.string()), // URL do avatar gerado (ex: DiceBear)
setor: v.optional(v.string()),
statusMensagem: v.optional(v.string()),
statusPresenca: v.optional(
v.union(
v.literal('online'),
v.literal('offline'),
v.literal('ausente'),
v.literal('externo'),
v.literal('em_reuniao')
)
),
notificacoesAtivadas: v.boolean(),
somNotificacao: v.boolean()
}),
v.null()
),
handler: async (ctx) => {
const usuarioAutenticado = await getCurrentUserFunction(ctx);
if (!usuarioAutenticado) {
return null;
}
const usuarioAtual = usuarioAutenticado;
// Buscar fotoPerfil URL se existir
let fotoPerfilUrl = null;
if (usuarioAtual.fotoPerfil) {
fotoPerfilUrl = await ctx.storage.getUrl(usuarioAtual.fotoPerfil);
}
const matricula = await obterMatriculaUsuario(ctx, usuarioAtual);
return {
_id: usuarioAtual._id,
nome: usuarioAtual.nome,
email: usuarioAtual.email,
matricula: matricula || undefined,
funcionarioId: usuarioAtual.funcionarioId,
fotoPerfil: usuarioAtual.fotoPerfil,
fotoPerfilUrl,
avatar: usuarioAtual.avatar,
setor: usuarioAtual.setor,
statusMensagem: usuarioAtual.statusMensagem,
statusPresenca: usuarioAtual.statusPresenca,
notificacoesAtivadas: usuarioAtual.notificacoesAtivadas ?? true,
somNotificacao: usuarioAtual.somNotificacao ?? true
};
}
});
/**
* Listar todos usuários para o chat (com foto e status)
*/
export const listarParaChat = query({
args: {},
returns: v.array(
v.object({
_id: v.id('usuarios'),
nome: v.string(),
email: v.string(),
matricula: v.optional(v.string()),
fotoPerfil: v.optional(v.id('_storage')),
fotoPerfilUrl: v.union(v.string(), v.null()),
avatar: v.optional(v.string()), // URL do avatar gerado (ex: DiceBear)
statusPresenca: v.optional(
v.union(
v.literal('online'),
v.literal('offline'),
v.literal('ausente'),
v.literal('externo'),
v.literal('em_reuniao')
)
),
statusMensagem: v.optional(v.string()),
ultimaAtividade: v.optional(v.number())
})
),
handler: async (ctx) => {
// Obter usuário autenticado usando função helper compartilhada
const usuarioAtual = await getCurrentUserFunction(ctx);
if (!usuarioAtual) {
return [];
}
// Buscar todos os usuários ativos
const usuarios = await ctx.db
.query('usuarios')
.filter((q) => q.eq(q.field('ativo'), true))
.collect();
// Filtrar o usuário atual da lista apenas se conseguimos identificá-lo com certeza
// Se não conseguimos identificar (usuarioAtual é null), retornar todos
// O frontend fará um filtro adicional usando obterPerfil como camada de segurança
const usuariosFiltrados = usuarioAtual
? usuarios.filter((u) => u._id !== usuarioAtual._id)
: usuarios;
// Buscar foto de perfil URL para cada usuário
const usuariosComFoto = await Promise.all(
usuariosFiltrados.map(async (usuario) => {
let fotoPerfilUrl = null;
if (usuario.fotoPerfil) {
fotoPerfilUrl = await ctx.storage.getUrl(usuario.fotoPerfil);
}
const matricula = await obterMatriculaUsuario(ctx, usuario);
return {
_id: usuario._id,
nome: usuario.nome,
email: usuario.email,
matricula: matricula || undefined,
fotoPerfil: usuario.fotoPerfil,
fotoPerfilUrl,
statusPresenca: usuario.statusPresenca || 'offline',
statusMensagem: usuario.statusMensagem,
ultimaAtividade: usuario.ultimaAtividade
};
})
);
return usuariosComFoto;
}
});
/**
* Gera URL para upload de foto de perfil
*/
export const uploadFotoPerfil = mutation({
args: {},
returns: v.string(),
handler: async (ctx) => {
const usuarioAtual = await getCurrentUserFunction(ctx);
if (!usuarioAtual) throw new Error('Usuário não autenticado');
return await ctx.storage.generateUploadUrl();
}
});
// ==================== 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 usuarioAtual = await getCurrentUserFunction(ctx);
if (!usuarioAtual) {
return { sucesso: false as const, erro: 'Usuário não autenticado' };
}
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 usuarioAtual = await getCurrentUserFunction(ctx);
if (!usuarioAtual) {
return { sucesso: false as const, erro: 'Usuário não autenticado' };
}
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' };
}
// 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' };
}
// Buscar a role do usuário
if (!resetadoPor.roleId) {
return { sucesso: false as const, erro: 'Usuário não possui role definida' };
}
const role = await ctx.db.get(resetadoPor.roleId);
if (!role) {
return { sucesso: false as const, erro: 'Role do usuário não encontrada' };
}
// 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 {
// Nota: Better Auth gerencia senhas através do sistema de autenticação.
// A senha não é armazenada diretamente na tabela usuarios.
// Para resetar a senha, seria necessário usar a API do Better Auth,
// mas isso requer uma implementação adicional.
// Por enquanto, atualizamos apenas os campos do usuário que podemos modificar.
// Atualizar usuário (sem senhaHash, pois não existe no schema)
await ctx.db.patch(args.usuarioId, {
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 {
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 usuarioAtual = await getCurrentUserFunction(ctx);
if (!usuarioAtual) {
return { sucesso: false as const, erro: 'Usuário não autenticado' };
}
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: Partial<Doc<'usuarios'>> & { atualizadoEm: number } = {
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 };
}
});
/**
* Criar/Promover usuário Admin Master (TI_MASTER - nível 0)
*/
export const criarAdminMaster = mutation({
args: {
nome: v.string(),
email: v.string(),
senha: v.optional(v.string())
},
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) => {
// Garantir que a role TI_MASTER exista (nível 0)
let roleTIMaster = await ctx.db
.query('roles')
.withIndex('by_nome', (q) => q.eq('nome', 'ti_master'))
.first();
if (!roleTIMaster) {
const roleId = await ctx.db.insert('roles', {
nome: 'ti_master',
descricao: 'TI Master',
nivel: 0,
setor: 'ti',
customizado: false,
editavel: false
});
roleTIMaster = await ctx.db.get(roleId);
}
if (!roleTIMaster) {
return {
sucesso: false as const,
erro: 'Falha ao garantir role TI Master'
};
}
const senhaTemporaria = args.senha || gerarSenhaTemporaria();
const authUserId = await createAuthUser(ctx, {
nome: args.nome,
email: args.email,
password: senhaTemporaria
});
// Verificar se email já existe
const existentePorEmail = await ctx.db
.query('usuarios')
.withIndex('by_email', (q) => q.eq('email', args.email))
.first();
if (existentePorEmail) {
// Promove usuário existente por email
await ctx.db.patch(existentePorEmail._id, {
nome: args.nome,
roleId: roleTIMaster._id,
ativo: true,
primeiroAcesso: true,
atualizadoEm: Date.now(),
authId: authUserId
});
return {
sucesso: true as const,
usuarioId: existentePorEmail._id,
senhaTemporaria
};
}
// Criar novo usuário TI Master
const usuarioId = await ctx.db.insert('usuarios', {
authId: authUserId,
nome: args.nome,
email: args.email,
roleId: roleTIMaster._id,
ativo: true,
primeiroAcesso: true,
criadoEm: Date.now(),
atualizadoEm: Date.now()
});
return { sucesso: true as const, usuarioId, senhaTemporaria };
}
});