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

871 lines
22 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';
/**
* 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()
);
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')
)
)
},
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 [];
}
let solicitacoes = await ctx.db
.query('solicitacoesLGPD')
.withIndex('by_usuario', (q) => q.eq('usuarioId', usuario._id))
.order('desc')
.collect();
if (args.status) {
solicitacoes = solicitacoes.filter((s) => s.status === args.status);
}
return 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
}));
}
});
/**
* 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')
)
),
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()),
criadoEm: v.number(),
prazoResposta: v.number(),
respondidoEm: v.union(v.number(), v.null()),
respondidoPorNome: v.union(v.string(), 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)
let solicitacoes = await ctx.db.query('solicitacoesLGPD').order('desc').collect();
if (args.status) {
solicitacoes = solicitacoes.filter((s) => s.status === args.status);
}
if (args.tipo) {
solicitacoes = solicitacoes.filter((s) => s.tipo === args.tipo);
}
if (args.limite) {
solicitacoes = solicitacoes.slice(0, args.limite);
}
// Enriquecer com dados do usuário
const resultado = await Promise.all(
solicitacoes.map(async (s) => {
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;
}
return {
_id: s._id,
tipo: s.tipo,
status: s.status,
usuarioNome: usuarioSolicitante?.nome ?? 'Usuário Desconhecido',
usuarioEmail: usuarioSolicitante?.email ?? '',
usuarioMatricula: matricula,
criadoEm: s.criadoEm,
prazoResposta: s.prazoResposta,
respondidoEm: s.respondidoEm ?? null,
respondidoPorNome
};
})
);
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');
}
await ctx.db.patch(args.solicitacaoId, {
status: args.status,
resposta: args.resposta,
arquivoResposta: args.arquivoResposta,
respondidoPor: usuario._id,
respondidoEm: Date.now()
});
// Log de atividade
await registrarAtividade(
ctx,
usuario._id,
'responder_solicitacao_lgpd',
'solicitacoesLGPD',
JSON.stringify({ solicitacaoId: args.solicitacaoId, status: args.status }),
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,
setor: usuario.setor
},
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
};
}
});