Files

1279 lines
35 KiB
TypeScript

import { v } from 'convex/values';
import { mutation, query } from './_generated/server';
import { getCurrentUserFunction } from './auth';
import { Id, Doc } from './_generated/dataModel';
import type { QueryCtx, MutationCtx } from './_generated/server';
import { registrarAtividade } from './logsAtividades';
import { api } from './_generated/api';
/**
* Verificar se usuário aceitou o termo de consentimento
*/
export const verificarConsentimento = query({
args: {
tipo: v.optional(
v.union(
v.literal('termo_uso'),
v.literal('politica_privacidade'),
v.literal('comunicacoes'),
v.literal('compartilhamento_dados')
)
)
},
returns: v.union(
v.object({
aceito: v.boolean(),
versao: v.string(),
aceitoEm: v.number(),
termoObrigatorio: v.boolean(),
versaoTermoAtual: v.string()
}),
v.null()
),
handler: async (ctx, args) => {
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) {
return null;
}
const tipo = args.tipo || 'termo_uso';
// Buscar configuração para verificar se termo é obrigatório
const config = await ctx.db
.query('configuracaoLGPD')
.withIndex('by_ativo', (q) => q.eq('ativo', true))
.first();
const termoObrigatorio = config?.termoObrigatorio ?? false;
const versaoTermoAtual = config?.versaoTermoAtual ?? '1.0';
const consentimento = await ctx.db
.query('consentimentos')
.withIndex('by_usuario_tipo', (q) => q.eq('usuarioId', usuario._id).eq('tipo', tipo))
.order('desc')
.first();
if (!consentimento || !consentimento.aceito || consentimento.revogadoEm) {
return {
aceito: false,
versao: '',
aceitoEm: 0,
termoObrigatorio,
versaoTermoAtual
};
}
return {
aceito: consentimento.aceito,
versao: consentimento.versao,
aceitoEm: consentimento.aceitoEm,
termoObrigatorio,
versaoTermoAtual
};
}
});
/**
* Registrar consentimento do usuário
*/
export const registrarConsentimento = mutation({
args: {
tipo: v.union(
v.literal('termo_uso'),
v.literal('politica_privacidade'),
v.literal('comunicacoes'),
v.literal('compartilhamento_dados')
),
aceito: v.boolean(),
versao: v.string(),
ipAddress: v.optional(v.string()),
userAgent: v.optional(v.string())
},
returns: v.object({ sucesso: v.boolean(), consentimentoId: v.id('consentimentos') }),
handler: async (ctx, args) => {
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) {
throw new Error('Usuário não autenticado');
}
// Verificar se já existe consentimento ativo
const existente = await ctx.db
.query('consentimentos')
.withIndex('by_usuario_tipo', (q) => q.eq('usuarioId', usuario._id).eq('tipo', args.tipo))
.order('desc')
.first();
if (existente && existente.aceito && !existente.revogadoEm) {
// Atualizar consentimento existente
await ctx.db.patch(existente._id, {
aceito: args.aceito,
versao: args.versao,
aceitoEm: Date.now(),
ipAddress: args.ipAddress,
userAgent: args.userAgent,
revogadoEm: undefined,
revogadoPor: undefined
});
return { sucesso: true, consentimentoId: existente._id };
}
// Criar novo consentimento
const consentimentoId = await ctx.db.insert('consentimentos', {
usuarioId: usuario._id,
tipo: args.tipo,
aceito: args.aceito,
versao: args.versao,
ipAddress: args.ipAddress,
userAgent: args.userAgent,
aceitoEm: Date.now()
});
// Log de atividade
await registrarAtividade(
ctx,
usuario._id,
'aceitar_consentimento',
'consentimentos',
JSON.stringify({ tipo: args.tipo, versao: args.versao }),
consentimentoId.toString()
);
return { sucesso: true, consentimentoId };
}
});
/**
* Revogar consentimento
*/
export const revogarConsentimento = mutation({
args: {
tipo: v.union(
v.literal('termo_uso'),
v.literal('politica_privacidade'),
v.literal('comunicacoes'),
v.literal('compartilhamento_dados')
)
},
returns: v.object({ sucesso: v.boolean() }),
handler: async (ctx, args) => {
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) {
throw new Error('Usuário não autenticado');
}
const consentimento = await ctx.db
.query('consentimentos')
.withIndex('by_usuario_tipo', (q) => q.eq('usuarioId', usuario._id).eq('tipo', args.tipo))
.order('desc')
.first();
if (!consentimento) {
throw new Error('Consentimento não encontrado');
}
await ctx.db.patch(consentimento._id, {
revogadoEm: Date.now(),
revogadoPor: usuario._id
});
// Log de atividade
await registrarAtividade(
ctx,
usuario._id,
'revogar_consentimento',
'consentimentos',
JSON.stringify({ tipo: args.tipo }),
consentimento._id.toString()
);
return { sucesso: true };
}
});
/**
* Listar consentimentos do usuário
*/
export const listarConsentimentos = query({
args: {},
returns: v.array(
v.object({
_id: v.id('consentimentos'),
tipo: v.string(),
aceito: v.boolean(),
versao: v.string(),
aceitoEm: v.number(),
revogadoEm: v.union(v.number(), v.null())
})
),
handler: async (ctx) => {
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) {
return [];
}
const consentimentos = await ctx.db
.query('consentimentos')
.withIndex('by_usuario', (q) => q.eq('usuarioId', usuario._id))
.order('desc')
.collect();
return consentimentos.map((c) => ({
_id: c._id,
tipo: c.tipo,
aceito: c.aceito,
versao: c.versao,
aceitoEm: c.aceitoEm,
revogadoEm: c.revogadoEm ?? null
}));
}
});
/**
* Criar solicitação de direito LGPD
*/
export const criarSolicitacao = mutation({
args: {
tipo: v.union(
v.literal('acesso'),
v.literal('correcao'),
v.literal('exclusao'),
v.literal('portabilidade'),
v.literal('revogacao_consentimento'),
v.literal('informacao_compartilhamento')
),
dadosSolicitados: v.optional(v.string()),
observacoes: v.optional(v.string())
},
returns: v.object({ sucesso: v.boolean(), solicitacaoId: v.id('solicitacoesLGPD') }),
handler: async (ctx, args) => {
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) {
throw new Error('Usuário não autenticado');
}
// Prazo de resposta: 15 dias (conforme LGPD)
const prazoResposta = Date.now() + 15 * 24 * 60 * 60 * 1000;
const solicitacaoId = await ctx.db.insert('solicitacoesLGPD', {
tipo: args.tipo,
usuarioId: usuario._id,
funcionarioId: usuario.funcionarioId,
status: 'pendente',
dadosSolicitados: args.dadosSolicitados,
observacoes: args.observacoes,
criadoEm: Date.now(),
prazoResposta
});
// Log de atividade
await registrarAtividade(
ctx,
usuario._id,
'criar_solicitacao_lgpd',
'solicitacoesLGPD',
JSON.stringify({ tipo: args.tipo }),
solicitacaoId.toString()
);
// Notificações (email + opcional chat) para o titular
if (usuario.email) {
let urlSistema = process.env.SITE_URL || 'http://localhost:5173';
if (!urlSistema.match(/^https?:\/\//i)) {
urlSistema = `http://${urlSistema}`;
}
const tipoSolicitacaoLabelMap: Record<string, string> = {
acesso: 'Acesso aos Dados',
correcao: 'Correção de Dados',
exclusao: 'Exclusão de Dados',
portabilidade: 'Portabilidade dos Dados',
revogacao_consentimento: 'Revogação de Consentimento',
informacao_compartilhamento: 'Informação sobre Compartilhamento'
};
const tipoSolicitacaoLabel = tipoSolicitacaoLabelMap[args.tipo] ?? args.tipo;
// Email usando template LGPD
try {
await ctx.scheduler.runAfter(0, api.email.enviarEmailComTemplate, {
destinatario: usuario.email,
destinatarioId: usuario._id,
templateCodigo: 'lgpd_solicitacao_criada',
variaveis: {
nomeTitular: usuario.nome,
tipoSolicitacaoLabel,
prazoResposta: new Date(prazoResposta).toLocaleDateString('pt-BR'),
urlPortalLGPD: `${urlSistema}/privacidade/meus-dados`
},
enviadoPor: usuario._id
});
} catch (error) {
console.error('Erro ao agendar email lgpd_solicitacao_criada:', error);
}
}
return { sucesso: true, solicitacaoId };
}
});
/**
* Listar solicitações do usuário
*/
export const listarMinhasSolicitacoes = query({
args: {
status: v.optional(
v.union(
v.literal('pendente'),
v.literal('em_analise'),
v.literal('concluida'),
v.literal('rejeitada'),
v.literal('cancelada')
)
),
_refresh: v.optional(v.number()) // Parâmetro para forçar atualização no frontend
},
returns: v.array(
v.object({
_id: v.id('solicitacoesLGPD'),
tipo: v.string(),
status: v.string(),
criadoEm: v.number(),
prazoResposta: v.number(),
respondidoEm: v.union(v.number(), v.null()),
resposta: v.union(v.string(), v.null()),
arquivoResposta: v.union(v.string(), v.null())
})
),
handler: async (ctx, args) => {
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) {
return [];
}
try {
let solicitacoes = await ctx.db
.query('solicitacoesLGPD')
.withIndex('by_usuario', (q) => q.eq('usuarioId', usuario._id))
.collect();
// Filtrar por status se especificado
if (args.status) {
solicitacoes = solicitacoes.filter((s) => s.status === args.status);
}
// Ordenar por data de criação (mais recentes primeiro)
solicitacoes.sort((a, b) => b.criadoEm - a.criadoEm);
const resultado = solicitacoes.map((s) => ({
_id: s._id,
tipo: s.tipo,
status: s.status,
criadoEm: s.criadoEm,
prazoResposta: s.prazoResposta,
respondidoEm: s.respondidoEm ?? null,
resposta: s.resposta ?? null,
arquivoResposta: s.arquivoResposta ? s.arquivoResposta.toString() : null
}));
console.log(
`[listarMinhasSolicitacoes] Usuário: ${usuario._id}, Solicitações encontradas: ${resultado.length}`
);
return resultado;
} catch (error) {
console.error('[listarMinhasSolicitacoes] Erro ao listar minhas solicitações:', error);
return [];
}
}
});
/**
* Listar todas as solicitações (apenas TI)
*/
export const listarSolicitacoes = query({
args: {
status: v.optional(
v.union(
v.literal('pendente'),
v.literal('em_analise'),
v.literal('concluida'),
v.literal('rejeitada'),
v.literal('cancelada')
)
),
tipo: v.optional(
v.union(
v.literal('acesso'),
v.literal('correcao'),
v.literal('exclusao'),
v.literal('portabilidade'),
v.literal('revogacao_consentimento'),
v.literal('informacao_compartilhamento')
)
),
limite: v.optional(v.number())
},
returns: v.array(
v.object({
_id: v.id('solicitacoesLGPD'),
tipo: v.string(),
status: v.string(),
usuarioNome: v.string(),
usuarioEmail: v.string(),
usuarioMatricula: v.union(v.string(), v.null()),
dadosSolicitados: v.union(v.string(), v.null()),
observacoes: v.union(v.string(), v.null()),
resposta: v.union(v.string(), v.null()),
arquivoResposta: v.union(v.string(), v.null()),
criadoEm: v.number(),
prazoResposta: v.number(),
respondidoEm: v.union(v.number(), v.null()),
respondidoPorNome: v.union(v.string(), v.null()),
consentimentoTermo: v.union(
v.object({
aceito: v.boolean(),
versao: v.string(),
aceitoEm: v.number(),
revogadoEm: v.union(v.number(), v.null())
}),
v.null()
)
})
),
handler: async (ctx, args) => {
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) {
return [];
}
// Verificar se é TI (simplificado - pode melhorar com verificação de role)
// Por enquanto, qualquer usuário autenticado pode ver (será melhorado)
// Buscar TODAS as solicitações sem filtros iniciais
let solicitacoes = await ctx.db.query('solicitacoesLGPD').collect();
// Filtrar por status
if (args.status) {
solicitacoes = solicitacoes.filter((s) => s.status === args.status);
}
// Filtrar por tipo
if (args.tipo) {
solicitacoes = solicitacoes.filter((s) => s.tipo === args.tipo);
}
// Ordenar por data de criação (mais recentes primeiro)
solicitacoes.sort((a, b) => b.criadoEm - a.criadoEm);
// Aplicar limite se especificado
if (args.limite) {
solicitacoes = solicitacoes.slice(0, args.limite);
}
// Tipo do resultado enriquecido
type SolicitacaoEnriquecida = {
_id: Id<'solicitacoesLGPD'>;
tipo: string;
status: string;
usuarioNome: string;
usuarioEmail: string;
usuarioMatricula: string | null;
dadosSolicitados: string | null;
observacoes: string | null;
resposta: string | null;
arquivoResposta: string | null;
criadoEm: number;
prazoResposta: number;
respondidoEm: number | null;
respondidoPorNome: string | null;
consentimentoTermo: {
aceito: boolean;
versao: string;
aceitoEm: number;
revogadoEm: number | null;
} | null;
};
// Enriquecer com dados do usuário
// Usar Promise.allSettled para garantir que todas as solicitações sejam processadas,
// mesmo se houver erro ao buscar dados de algum usuário
const resultados = await Promise.allSettled(
solicitacoes.map(async (s): Promise<SolicitacaoEnriquecida> => {
try {
const usuarioSolicitante = await ctx.db.get(s.usuarioId);
let matricula: string | null = null;
if (usuarioSolicitante?.funcionarioId) {
const funcionario = await ctx.db.get(usuarioSolicitante.funcionarioId);
matricula = funcionario?.matricula ?? null;
}
let respondidoPorNome: string | null = null;
if (s.respondidoPor) {
const respondente = await ctx.db.get(s.respondidoPor);
respondidoPorNome = respondente?.nome ?? null;
}
// Buscar consentimento do termo de uso
let consentimentoTermo: {
aceito: boolean;
versao: string;
aceitoEm: number;
revogadoEm: number | null;
} | null = null;
if (usuarioSolicitante) {
try {
const consentimento = await ctx.db
.query('consentimentos')
.withIndex('by_usuario_tipo', (q) =>
q.eq('usuarioId', usuarioSolicitante._id).eq('tipo', 'termo_uso')
)
.order('desc')
.first();
if (consentimento && consentimento.aceito && !consentimento.revogadoEm) {
consentimentoTermo = {
aceito: consentimento.aceito,
versao: consentimento.versao,
aceitoEm: consentimento.aceitoEm,
revogadoEm: consentimento.revogadoEm ?? null
};
}
} catch (error) {
// Se houver erro ao buscar consentimento, continua sem ele
console.error('Erro ao buscar consentimento:', error);
}
}
// Buscar URL do arquivo de resposta se existir
let arquivoRespostaUrl: string | null = null;
if (s.arquivoResposta) {
arquivoRespostaUrl = await ctx.storage.getUrl(s.arquivoResposta);
}
return {
_id: s._id,
tipo: s.tipo,
status: s.status,
usuarioNome: usuarioSolicitante?.nome ?? 'Usuário Desconhecido',
usuarioEmail: usuarioSolicitante?.email ?? '',
usuarioMatricula: matricula,
dadosSolicitados: s.dadosSolicitados ?? null,
observacoes: s.observacoes ?? null,
resposta: s.resposta ?? null,
arquivoResposta: arquivoRespostaUrl,
criadoEm: s.criadoEm,
prazoResposta: s.prazoResposta,
respondidoEm: s.respondidoEm ?? null,
respondidoPorNome,
consentimentoTermo
};
} catch (error) {
// Se houver erro ao processar uma solicitação, retorna com dados mínimos
console.error('Erro ao processar solicitação:', s._id, error);
return {
_id: s._id,
tipo: s.tipo,
status: s.status,
usuarioNome: 'Erro ao carregar',
usuarioEmail: '',
usuarioMatricula: null,
dadosSolicitados: s.dadosSolicitados ?? null,
observacoes: s.observacoes ?? null,
resposta: s.resposta ?? null,
arquivoResposta: null,
criadoEm: s.criadoEm,
prazoResposta: s.prazoResposta,
respondidoEm: s.respondidoEm ?? null,
respondidoPorNome: null,
consentimentoTermo: null
};
}
})
);
// Filtrar apenas resultados bem-sucedidos e converter para o tipo correto
const resultado = resultados
.filter((r): r is PromiseFulfilledResult<SolicitacaoEnriquecida> => r.status === 'fulfilled')
.map((r) => r.value);
return resultado;
}
});
/**
* Responder solicitação (apenas TI)
*/
export const responderSolicitacao = mutation({
args: {
solicitacaoId: v.id('solicitacoesLGPD'),
resposta: v.string(),
status: v.union(v.literal('concluida'), v.literal('rejeitada'), v.literal('em_analise')),
arquivoResposta: v.optional(v.id('_storage'))
},
returns: v.object({ sucesso: v.boolean() }),
handler: async (ctx, args) => {
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) {
throw new Error('Usuário não autenticado');
}
const solicitacao = await ctx.db.get(args.solicitacaoId);
if (!solicitacao) {
throw new Error('Solicitação não encontrada');
}
// Atualizar resposta da solicitação
await ctx.db.patch(args.solicitacaoId, {
status: args.status,
resposta: args.resposta,
arquivoResposta: args.arquivoResposta,
respondidoPor: usuario._id,
respondidoEm: Date.now()
});
// Se for uma solicitação de "Revogar Consentimento" concluída,
// revogar todos os consentimentos ativos do titular que fez a solicitação.
if (solicitacao.tipo === 'revogacao_consentimento' && args.status === 'concluida') {
// Garantir que temos o titular associado
if (!solicitacao.usuarioId) {
throw new Error(
'Solicitação de revogação de consentimento sem usuário associado. Verifique os dados.'
);
}
// Buscar consentimentos ativos do usuário
const consentimentosAtivos = await ctx.db
.query('consentimentos')
.withIndex('by_usuario', (q) => q.eq('usuarioId', solicitacao.usuarioId))
.filter((q) => q.eq(q.field('aceito'), true))
.collect();
for (const consentimento of consentimentosAtivos) {
// Pular consentimentos já revogados por segurança
if (consentimento.revogadoEm) continue;
await ctx.db.patch(consentimento._id, {
revogadoEm: Date.now(),
revogadoPor: usuario._id
});
// Registrar atividade individual por consentimento revogado
await registrarAtividade(
ctx,
usuario._id,
'revogar_consentimento_por_solicitacao',
'consentimentos',
JSON.stringify({
tipo: consentimento.tipo,
origem: 'solicitacao_lgpd',
solicitacaoId: args.solicitacaoId
}),
consentimento._id.toString()
);
}
}
// Log de atividade
await registrarAtividade(
ctx,
usuario._id,
'responder_solicitacao_lgpd',
'solicitacoesLGPD',
JSON.stringify({ solicitacaoId: args.solicitacaoId, status: args.status }),
args.solicitacaoId.toString()
);
// Notificações para o titular (email + chat)
const usuarioTitular = await ctx.db.get(solicitacao.usuarioId);
if (usuarioTitular) {
let urlSistema = process.env.SITE_URL || 'http://localhost:5173';
if (!urlSistema.match(/^https?:\/\//i)) {
urlSistema = `http://${urlSistema}`;
}
const tipoSolicitacaoLabelMap: Record<string, string> = {
acesso: 'Acesso aos Dados',
correcao: 'Correção de Dados',
exclusao: 'Exclusão de Dados',
portabilidade: 'Portabilidade dos Dados',
revogacao_consentimento: 'Revogação de Consentimento',
informacao_compartilhamento: 'Informação sobre Compartilhamento'
};
const statusLabelMap: Record<string, string> = {
concluida: 'Concluída',
rejeitada: 'Rejeitada',
em_analise: 'Em Análise'
};
const tipoSolicitacaoLabel = tipoSolicitacaoLabelMap[solicitacao.tipo] ?? solicitacao.tipo;
const statusLabel = statusLabelMap[args.status] ?? args.status;
const resumoResposta =
args.resposta.length > 500 ? `${args.resposta.slice(0, 500)}...` : args.resposta;
// Escolher template conforme o tipo
const tipoToTemplate: Record<string, string> = {
acesso: 'lgpd_resposta_acesso',
correcao: 'lgpd_resposta_correcao',
exclusao: 'lgpd_resposta_exclusao',
portabilidade: 'lgpd_resposta_portabilidade',
revogacao_consentimento: 'lgpd_resposta_revogacao_consentimento',
informacao_compartilhamento: 'lgpd_resposta_informacao_compartilhamento'
};
const templateCodigo = tipoToTemplate[solicitacao.tipo] ?? 'lgpd_resposta_acesso';
// Email para o titular
if (usuarioTitular.email) {
try {
await ctx.scheduler.runAfter(0, api.email.enviarEmailComTemplate, {
destinatario: usuarioTitular.email,
destinatarioId: usuarioTitular._id,
templateCodigo,
variaveis: {
nomeTitular: usuarioTitular.nome,
tipoSolicitacaoLabel,
statusLabel,
resumoResposta,
urlPortalLGPD: `${urlSistema}/privacidade/meus-dados`
},
enviadoPor: usuario._id
});
} catch (error) {
console.error(`Erro ao agendar email ${templateCodigo}:`, error);
}
}
// Mensagem simples no chat entre TI (respondente) e o titular
try {
// Buscar conversa individual existente
const conversas = await ctx.db
.query('conversas')
.filter((q) => q.eq(q.field('tipo'), 'individual'))
.collect();
let conversaId: Id<'conversas'> | null = null;
for (const conversa of conversas) {
if (
conversa.participantes.length === 2 &&
conversa.participantes.includes(usuario._id) &&
conversa.participantes.includes(usuarioTitular._id)
) {
conversaId = conversa._id;
break;
}
}
if (!conversaId) {
conversaId = await ctx.db.insert('conversas', {
tipo: 'individual',
participantes: [usuario._id, usuarioTitular._id],
criadoPor: usuario._id,
criadoEm: Date.now()
});
}
await ctx.db.insert('mensagens', {
conversaId,
remetenteId: usuario._id,
tipo: 'texto',
conteudo: `Respondi sua solicitação LGPD (${tipoSolicitacaoLabel}) com status ${statusLabel}. Resumo: ${resumoResposta}`,
enviadaEm: Date.now()
});
} catch (error) {
console.error('Erro ao criar mensagem de chat para resposta LGPD:', error);
}
}
return { sucesso: true };
}
});
/**
* Cancelar solicitação LGPD (apenas pelo próprio usuário)
*/
export const cancelarSolicitacao = mutation({
args: {
solicitacaoId: v.id('solicitacoesLGPD')
},
returns: v.object({ sucesso: v.boolean() }),
handler: async (ctx, args) => {
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) {
throw new Error('Usuário não autenticado');
}
const solicitacao = await ctx.db.get(args.solicitacaoId);
if (!solicitacao) {
throw new Error('Solicitação não encontrada');
}
// Verificar se a solicitação pertence ao usuário
if (solicitacao.usuarioId !== usuario._id) {
throw new Error('Você não tem permissão para cancelar esta solicitação');
}
// Só pode cancelar se estiver pendente ou em análise
if (solicitacao.status !== 'pendente' && solicitacao.status !== 'em_analise') {
throw new Error('Só é possível cancelar solicitações pendentes ou em análise');
}
// Atualizar status para cancelada
await ctx.db.patch(args.solicitacaoId, {
status: 'cancelada'
});
// Log de atividade
await registrarAtividade(
ctx,
usuario._id,
'cancelar_solicitacao_lgpd',
'solicitacoesLGPD',
JSON.stringify({ solicitacaoId: args.solicitacaoId }),
args.solicitacaoId.toString()
);
return { sucesso: true };
}
});
/**
* Excluir solicitação LGPD (apenas pelo próprio usuário e apenas se cancelada ou pendente)
*/
export const excluirSolicitacao = mutation({
args: {
solicitacaoId: v.id('solicitacoesLGPD')
},
returns: v.object({ sucesso: v.boolean() }),
handler: async (ctx, args) => {
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) {
throw new Error('Usuário não autenticado');
}
const solicitacao = await ctx.db.get(args.solicitacaoId);
if (!solicitacao) {
throw new Error('Solicitação não encontrada');
}
// Verificar se a solicitação pertence ao usuário
if (solicitacao.usuarioId !== usuario._id) {
throw new Error('Você não tem permissão para excluir esta solicitação');
}
// Só pode excluir se estiver pendente ou cancelada
if (solicitacao.status !== 'pendente' && solicitacao.status !== 'cancelada') {
throw new Error('Só é possível excluir solicitações pendentes ou canceladas');
}
// Excluir arquivo de resposta se existir
if (solicitacao.arquivoResposta) {
await ctx.storage.delete(solicitacao.arquivoResposta);
}
// Excluir a solicitação
await ctx.db.delete(args.solicitacaoId);
// Log de atividade
await registrarAtividade(
ctx,
usuario._id,
'excluir_solicitacao_lgpd',
'solicitacoesLGPD',
JSON.stringify({ solicitacaoId: args.solicitacaoId }),
args.solicitacaoId.toString()
);
return { sucesso: true };
}
});
/**
* Exportar dados do usuário (portabilidade)
*/
export const exportarDadosUsuario = query({
args: {},
returns: v.object({
dados: v.string() // JSON string com todos os dados do usuário
}),
handler: async (ctx) => {
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) {
throw new Error('Usuário não autenticado');
}
// Buscar todos os dados do usuário
type DadosUsuario = {
usuario: {
nome: string;
email: string;
setor?: string;
};
consentimentos: Array<{
tipo: string;
aceito: boolean;
versao: string;
aceitoEm: number;
revogadoEm?: number;
}>;
solicitacoes: Array<{
tipo: string;
status: string;
criadoEm: number;
respondidoEm?: number;
}>;
funcionario?: {
nome: string;
matricula?: string;
cpf: string;
email: string;
telefone: string;
descricaoCargo?: string;
};
};
const dadosUsuario: DadosUsuario = {
usuario: {
nome: usuario.nome,
email: usuario.email
},
consentimentos: [],
solicitacoes: []
};
// Consentimentos
const consentimentos = await ctx.db
.query('consentimentos')
.withIndex('by_usuario', (q) => q.eq('usuarioId', usuario._id))
.collect();
dadosUsuario.consentimentos = consentimentos.map((c) => ({
tipo: c.tipo,
aceito: c.aceito,
versao: c.versao,
aceitoEm: c.aceitoEm,
revogadoEm: c.revogadoEm
}));
// Solicitações LGPD
const solicitacoes = await ctx.db
.query('solicitacoesLGPD')
.withIndex('by_usuario', (q) => q.eq('usuarioId', usuario._id))
.collect();
dadosUsuario.solicitacoes = solicitacoes.map((s) => ({
tipo: s.tipo,
status: s.status,
criadoEm: s.criadoEm,
respondidoEm: s.respondidoEm
}));
// Dados do funcionário (se houver)
if (usuario.funcionarioId) {
const funcionario = await ctx.db.get(usuario.funcionarioId);
if (funcionario) {
dadosUsuario.funcionario = {
nome: funcionario.nome,
matricula: funcionario.matricula,
cpf: funcionario.cpf,
email: funcionario.email,
telefone: funcionario.telefone,
descricaoCargo: funcionario.descricaoCargo
};
}
}
return {
dados: JSON.stringify(dadosUsuario, null, 2)
};
}
});
/**
* Criar Registro de Operação de Tratamento (ROT)
*/
export const criarRegistroTratamento = mutation({
args: {
finalidade: v.string(),
baseLegal: v.string(),
categoriasDados: v.array(v.string()),
categoriasTitulares: v.array(v.string()),
medidasSeguranca: v.array(v.string()),
prazoRetencao: v.number(),
compartilhamentoTerceiros: v.boolean(),
terceiros: v.optional(v.array(v.string())),
descricao: v.optional(v.string())
},
returns: v.object({ sucesso: v.boolean(), registroId: v.id('registrosTratamento') }),
handler: async (ctx, args) => {
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) {
throw new Error('Usuário não autenticado');
}
const agora = Date.now();
const registroId = await ctx.db.insert('registrosTratamento', {
finalidade: args.finalidade,
baseLegal: args.baseLegal,
categoriasDados: args.categoriasDados,
categoriasTitulares: args.categoriasTitulares,
medidasSeguranca: args.medidasSeguranca,
prazoRetencao: args.prazoRetencao,
compartilhamentoTerceiros: args.compartilhamentoTerceiros,
terceiros: args.terceiros,
responsavel: usuario._id,
descricao: args.descricao,
criadoEm: agora,
atualizadoEm: agora,
ativo: true
});
// Log de atividade
await registrarAtividade(
ctx,
usuario._id,
'criar_rot',
'registrosTratamento',
JSON.stringify({ finalidade: args.finalidade }),
registroId.toString()
);
return { sucesso: true, registroId };
}
});
/**
* Listar Registros de Tratamento
*/
export const listarRegistrosTratamento = query({
args: {
ativo: v.optional(v.boolean())
},
returns: v.array(
v.object({
_id: v.id('registrosTratamento'),
finalidade: v.string(),
baseLegal: v.string(),
categoriasDados: v.array(v.string()),
categoriasTitulares: v.array(v.string()),
medidasSeguranca: v.array(v.string()),
prazoRetencao: v.number(),
compartilhamentoTerceiros: v.boolean(),
terceiros: v.union(v.array(v.string()), v.null()),
responsavelNome: v.string(),
criadoEm: v.number(),
atualizadoEm: v.number(),
ativo: v.boolean()
})
),
handler: async (ctx, args) => {
let registros = await ctx.db.query('registrosTratamento').collect();
if (args.ativo !== undefined) {
registros = registros.filter((r) => r.ativo === args.ativo);
}
// Enriquecer com nome do responsável
const resultado = await Promise.all(
registros.map(async (r) => {
const responsavel = await ctx.db.get(r.responsavel);
return {
_id: r._id,
finalidade: r.finalidade,
baseLegal: r.baseLegal,
categoriasDados: r.categoriasDados,
categoriasTitulares: r.categoriasTitulares,
medidasSeguranca: r.medidasSeguranca,
prazoRetencao: r.prazoRetencao,
compartilhamentoTerceiros: r.compartilhamentoTerceiros,
terceiros: r.terceiros ?? null,
responsavelNome: responsavel?.nome ?? 'Desconhecido',
criadoEm: r.criadoEm,
atualizadoEm: r.atualizadoEm,
ativo: r.ativo
};
})
);
return resultado;
}
});
/**
* Obter configurações LGPD
*/
export const obterConfiguracaoLGPD = query({
args: {},
returns: v.union(
v.object({
encarregadoNome: v.union(v.string(), v.null()),
encarregadoEmail: v.union(v.string(), v.null()),
encarregadoTelefone: v.union(v.string(), v.null()),
encarregadoHorarioAtendimento: v.union(v.string(), v.null()),
prazoRespostaPadrao: v.number(),
diasAlertaVencimento: v.number(),
termoObrigatorio: v.boolean(),
versaoTermoAtual: v.string()
}),
v.null()
),
handler: async (ctx) => {
const config = await ctx.db
.query('configuracaoLGPD')
.withIndex('by_ativo', (q) => q.eq('ativo', true))
.first();
if (!config) {
// Retornar valores padrão
return {
encarregadoNome: null,
encarregadoEmail: null,
encarregadoTelefone: null,
encarregadoHorarioAtendimento: null,
prazoRespostaPadrao: 15,
diasAlertaVencimento: 3,
termoObrigatorio: false,
versaoTermoAtual: '1.0'
};
}
return {
encarregadoNome: config.encarregadoNome ?? null,
encarregadoEmail: config.encarregadoEmail ?? null,
encarregadoTelefone: config.encarregadoTelefone ?? null,
encarregadoHorarioAtendimento: config.encarregadoHorarioAtendimento ?? null,
prazoRespostaPadrao: config.prazoRespostaPadrao,
diasAlertaVencimento: config.diasAlertaVencimento,
termoObrigatorio: config.termoObrigatorio,
versaoTermoAtual: config.versaoTermoAtual
};
}
});
/**
* Atualizar configurações LGPD (apenas TI)
*/
export const atualizarConfiguracaoLGPD = mutation({
args: {
encarregadoNome: v.optional(v.string()),
encarregadoEmail: v.optional(v.string()),
encarregadoTelefone: v.optional(v.string()),
encarregadoHorarioAtendimento: v.optional(v.string()),
prazoRespostaPadrao: v.optional(v.number()),
diasAlertaVencimento: v.optional(v.number()),
termoObrigatorio: v.optional(v.boolean()),
versaoTermoAtual: v.optional(v.string())
},
returns: v.object({ sucesso: v.boolean() }),
handler: async (ctx, args) => {
const usuario = await getCurrentUserFunction(ctx);
if (!usuario) {
throw new Error('Usuário não autenticado');
}
// Buscar configuração ativa ou criar nova
let config = await ctx.db
.query('configuracaoLGPD')
.withIndex('by_ativo', (q) => q.eq('ativo', true))
.first();
if (config) {
// Desativar configuração antiga
await ctx.db.patch(config._id, { ativo: false });
}
// Buscar valores atuais para manter os que não foram atualizados
const valoresAtuais = config || {
encarregadoNome: undefined,
encarregadoEmail: undefined,
encarregadoTelefone: undefined,
encarregadoHorarioAtendimento: undefined,
prazoRespostaPadrao: 15,
diasAlertaVencimento: 3,
termoObrigatorio: false,
versaoTermoAtual: '1.0'
};
// Criar nova configuração
await ctx.db.insert('configuracaoLGPD', {
encarregadoNome: args.encarregadoNome ?? valoresAtuais.encarregadoNome ?? undefined,
encarregadoEmail: args.encarregadoEmail ?? valoresAtuais.encarregadoEmail ?? undefined,
encarregadoTelefone:
args.encarregadoTelefone ?? valoresAtuais.encarregadoTelefone ?? undefined,
encarregadoHorarioAtendimento:
args.encarregadoHorarioAtendimento ??
valoresAtuais.encarregadoHorarioAtendimento ??
undefined,
prazoRespostaPadrao: args.prazoRespostaPadrao ?? valoresAtuais.prazoRespostaPadrao,
diasAlertaVencimento: args.diasAlertaVencimento ?? valoresAtuais.diasAlertaVencimento,
termoObrigatorio: args.termoObrigatorio ?? valoresAtuais.termoObrigatorio ?? false,
versaoTermoAtual: args.versaoTermoAtual ?? valoresAtuais.versaoTermoAtual ?? '1.0',
ativo: true,
atualizadoPor: usuario._id,
atualizadoEm: Date.now()
});
// Log de atividade
await registrarAtividade(
ctx,
usuario._id,
'atualizar_config_lgpd',
'configuracaoLGPD',
JSON.stringify(args),
''
);
return { sucesso: true };
}
});
/**
* Obter estatísticas LGPD (apenas TI)
*/
export const obterEstatisticasLGPD = query({
args: {},
returns: v.object({
totalSolicitacoes: v.number(),
solicitacoesPendentes: v.number(),
solicitacoesVencendo: v.number(),
solicitacoesPorTipo: v.record(v.string(), v.number()),
totalConsentimentos: v.number(),
consentimentosAtivos: v.number(),
totalROTs: v.number(),
rotsAtivos: v.number()
}),
handler: async (ctx) => {
const solicitacoes = await ctx.db.query('solicitacoesLGPD').collect();
const consentimentos = await ctx.db.query('consentimentos').collect();
const rots = await ctx.db.query('registrosTratamento').collect();
const agora = Date.now();
const tresDias = 3 * 24 * 60 * 60 * 1000;
const solicitacoesVencendo = solicitacoes.filter((s) =>
s.status === 'pendente' || s.status === 'em_analise'
? s.prazoResposta - agora <= tresDias && s.prazoResposta > agora
: false
).length;
const solicitacoesPorTipo: Record<string, number> = {};
solicitacoes.forEach((s) => {
solicitacoesPorTipo[s.tipo] = (solicitacoesPorTipo[s.tipo] || 0) + 1;
});
const consentimentosAtivos = consentimentos.filter((c) => c.aceito && !c.revogadoEm).length;
return {
totalSolicitacoes: solicitacoes.length,
solicitacoesPendentes: solicitacoes.filter((s) => s.status === 'pendente').length,
solicitacoesVencendo,
solicitacoesPorTipo,
totalConsentimentos: consentimentos.length,
consentimentosAtivos,
totalROTs: rots.length,
rotsAtivos: rots.filter((r) => r.ativo).length
};
}
});