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() }), v.null() ), handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { return null; } const tipo = args.tipo || 'termo_uso'; 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 null; } return { aceito: consentimento.aceito, versao: consentimento.versao, aceitoEm: consentimento.aceitoEm }; } }); /** * 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 const dadosUsuario: any = { usuario: { nome: usuario.nome, email: usuario.email, setor: usuario.setor }, consentimentos: [], solicitacoes: [], atividades: [] }; // 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, cargo: funcionario.cargo, setor: funcionario.setor }; } } 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()), prazoRespostaPadrao: v.number(), diasAlertaVencimento: v.number() }), 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, prazoRespostaPadrao: 15, diasAlertaVencimento: 3 }; } return { encarregadoNome: config.encarregadoNome ?? null, encarregadoEmail: config.encarregadoEmail ?? null, encarregadoTelefone: config.encarregadoTelefone ?? null, prazoRespostaPadrao: config.prazoRespostaPadrao, diasAlertaVencimento: config.diasAlertaVencimento }; } }); /** * 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()), prazoRespostaPadrao: v.optional(v.number()), diasAlertaVencimento: v.optional(v.number()) }, 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 }); } // Criar nova configuração await ctx.db.insert('configuracaoLGPD', { encarregadoNome: args.encarregadoNome, encarregadoEmail: args.encarregadoEmail, encarregadoTelefone: args.encarregadoTelefone, prazoRespostaPadrao: args.prazoRespostaPadrao ?? 15, diasAlertaVencimento: args.diasAlertaVencimento ?? 3, 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 = {}; 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 }; } });