From 4a7066296ef18162c0bc0707f1fd3104846d0cf5 Mon Sep 17 00:00:00 2001 From: Tiago Date: Tue, 6 Oct 2026 15:59:27 -0300 Subject: [PATCH] Exibe api key no master --- .../java/com/seconectai/condocontab/Main.java | 2 +- .../condocontab/master/http/MasterHandlers.java | 16 +++++++++++++--- src/main/webapp/master/clientes.js | 9 ++++++--- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/seconectai/condocontab/Main.java b/src/main/java/com/seconectai/condocontab/Main.java index 208053a..2eb7911 100644 --- a/src/main/java/com/seconectai/condocontab/Main.java +++ b/src/main/java/com/seconectai/condocontab/Main.java @@ -54,7 +54,7 @@ public final class Main { AuthHandlers authHandlers = new AuthHandlers(config, dataSourceProvider, jwtService); AuthMiddleware authMiddleware = new AuthMiddleware(jwtService, apiKeyService); TenantDataSourceProvider tenantDataSourceProvider = new TenantDataSourceProvider(config, dataSourceProvider); - MasterHandlers masterHandlers = new MasterHandlers(dataSourceProvider, new TenantProvisioner(config), evolutionApiClient); + MasterHandlers masterHandlers = new MasterHandlers(dataSourceProvider, new TenantProvisioner(config), evolutionApiClient, config.jwtSecret()); ClienteHandlers clienteHandlers = new ClienteHandlers(tenantDataSourceProvider, dataSourceProvider, Path.of("/app/uploads"), evolutionApiClient, emailClient); FinanceiroHandlers financeiroHandlers = new FinanceiroHandlers(dataSourceProvider, abacatePayClient); AbacatePayWebhookHandler abacatePayWebhookHandler = new AbacatePayWebhookHandler(dataSourceProvider, abacatePayClient); diff --git a/src/main/java/com/seconectai/condocontab/master/http/MasterHandlers.java b/src/main/java/com/seconectai/condocontab/master/http/MasterHandlers.java index df8b86b..bdf774e 100644 --- a/src/main/java/com/seconectai/condocontab/master/http/MasterHandlers.java +++ b/src/main/java/com/seconectai/condocontab/master/http/MasterHandlers.java @@ -1,6 +1,7 @@ package com.seconectai.condocontab.master.http; import com.seconectai.condocontab.auth.PasswordHasher; +import com.seconectai.condocontab.auth.ApiKeyService; import com.seconectai.condocontab.config.DataSourceProvider; import com.seconectai.condocontab.http.HttpResponses; import com.seconectai.condocontab.integracao.EvolutionApiClient; @@ -29,11 +30,13 @@ public final class MasterHandlers implements HttpHandler { private final DataSourceProvider dataSourceProvider; private final TenantProvisioner tenantProvisioner; private final EvolutionApiClient evolutionApiClient; + private final String apiKeySecret; - public MasterHandlers(DataSourceProvider dataSourceProvider, TenantProvisioner tenantProvisioner, EvolutionApiClient evolutionApiClient) { + public MasterHandlers(DataSourceProvider dataSourceProvider, TenantProvisioner tenantProvisioner, EvolutionApiClient evolutionApiClient, String apiKeySecret) { this.dataSourceProvider = dataSourceProvider; this.tenantProvisioner = tenantProvisioner; this.evolutionApiClient = evolutionApiClient; + this.apiKeySecret = apiKeySecret; } @Override @@ -407,6 +410,7 @@ public final class MasterHandlers implements HttpHandler { String status = resultado.sucesso() ? "ativa" : "provisionamento_pendente"; dao.atualizarStatus(criada.id(), status); if (planoId != null) { salvarAssinatura(conn, criada.id(), planoId); copiarFuncionalidadesPlano(conn, criada.id(), planoId); } + garantirApiKeyFixa(conn, criada.id(), criada.slug()); Funcionario principal = new FuncionarioDao(conn).inserir(new Funcionario(null, criada.id(), nome, emailPrincipal, telefonePrincipal, PasswordHasher.hash(senhaPrincipal), "admin_empresa", "ativo", null)); Map resp = new LinkedHashMap<>(); resp.put("empresa", empresaMap(dao.buscarPorId(criada.id()).orElseThrow())); @@ -433,6 +437,7 @@ public final class MasterHandlers implements HttpHandler { sql.append(" ORDER BY e.id"); try (Connection conn = dataSourceProvider.getMasterConnection()) { garantirPeriodosAtuais(conn); + garantirApiKeysFixas(conn); try (PreparedStatement ps = conn.prepareStatement(sql.toString())) { bind(ps, params); List> rows = new ArrayList<>(); @@ -669,9 +674,13 @@ public final class MasterHandlers implements HttpHandler { private void copiarFuncionalidadesPlano(Connection c,long empresaId,long planoId)throws SQLException{try(PreparedStatement ps=c.prepareStatement("INSERT INTO empresa_funcionalidades (empresa_id,funcionalidade_id,ativa) SELECT ?, funcionalidade_id, true FROM plano_funcionalidades WHERE plano_id=? ON CONFLICT (empresa_id,funcionalidade_id) DO UPDATE SET ativa=true")){ps.setLong(1,empresaId);ps.setLong(2,planoId);ps.executeUpdate();}} private void salvarAssinatura(Connection c,long empresaId,long planoId)throws SQLException{Map p=one(c,"SELECT valor_mensal_centavos FROM planos WHERE id=?",List.of(planoId));if(p==null)throw new IllegalArgumentException("Plano nao encontrado");try(PreparedStatement ps=c.prepareStatement("UPDATE assinaturas SET status='inativa' WHERE empresa_id=? AND status='ativa'")){ps.setLong(1,empresaId);ps.executeUpdate();}try(PreparedStatement ps=c.prepareStatement("INSERT INTO assinaturas (empresa_id,plano_id,valor_mensal_centavos,dia_vencimento,status) VALUES (?,?,?,?, 'ativa')")){ps.setLong(1,empresaId);ps.setLong(2,planoId);ps.setLong(3,((Number)p.get("valor_mensal_centavos")).longValue());ps.setInt(4,7);ps.executeUpdate();}} - private Map empresaMap(Empresa e){Map m=new LinkedHashMap<>();m.put("id",e.id());m.put("nome",e.nome());m.put("cnpj",e.cnpj());m.put("slug",e.slug());m.put("database_name",e.databaseName());m.put("status",e.status());m.put("criado_em",e.criadoEm());return m;} + private String apiKeyFixa(String slug){return "sk_seconectai_"+ApiKeyService.hash(apiKeySecret+":"+slug).substring(0,40);} + private void garantirApiKeysFixas(Connection c)throws SQLException{try(PreparedStatement ps=c.prepareStatement("SELECT id,slug FROM empresas WHERE status<>'excluida' ORDER BY id")){try(ResultSet rs=ps.executeQuery()){while(rs.next())garantirApiKeyFixa(c,rs.getLong("id"),rs.getString("slug"));}}} + private void garantirApiKeyFixa(Connection c,long empresaId,String slug)throws SQLException{String chave=apiKeyFixa(slug),hash=ApiKeyService.hash(chave);if(exists(c,"SELECT 1 FROM api_keys WHERE empresa_id=? AND key_hash=? AND ativa=true",empresaId,hash))return;try(PreparedStatement ps=c.prepareStatement("INSERT INTO api_keys (empresa_id,key_hash,descricao,ativa) VALUES (?,?,?,true) ON CONFLICT (key_hash) DO NOTHING")){ps.setLong(1,empresaId);ps.setString(2,hash);ps.setString(3,"Chave fixa de integracao");ps.executeUpdate();}} + + private Map empresaMap(Empresa e){Map m=new LinkedHashMap<>();m.put("id",e.id());m.put("nome",e.nome());m.put("cnpj",e.cnpj());m.put("slug",e.slug());m.put("database_name",e.databaseName());m.put("status",e.status());m.put("criado_em",e.criadoEm());m.put("api_key",apiKeyFixa(e.slug()));return m;} private Map funcionarioMap(Funcionario f){Map m=new LinkedHashMap<>();m.put("id",f.id());m.put("empresa_id",f.empresaId());m.put("nome",f.nome());m.put("email",f.email());m.put("telefone",f.telefone());m.put("cargo",f.cargo()==null?"":f.cargo());m.put("status",f.status());return m;} - private Map empresaResumoMap(ResultSet rs)throws SQLException{Map m=new LinkedHashMap<>();m.put("id",rs.getLong("id"));m.put("nome",rs.getString("nome"));m.put("cnpj",rs.getString("cnpj"));m.put("slug",rs.getString("slug"));m.put("database_name",rs.getString("database_name"));m.put("status",rs.getString("status"));m.put("plano_id",rs.getObject("plano_id"));m.put("plano",rs.getString("plano_nome"));m.put("plano_valor_centavos",rs.getObject("plano_valor_centavos"));m.put("dia_vencimento",rs.getObject("dia_vencimento"));m.put("periodo_status",rs.getString("periodo_status"));m.put("mes_referencia",rs.getObject("mes_referencia"));m.put("data_inicio",rs.getObject("data_inicio"));m.put("data_fim",rs.getObject("data_fim"));m.put("data_vencimento",rs.getObject("data_vencimento"));return m;} + private Map empresaResumoMap(ResultSet rs)throws SQLException{Map m=new LinkedHashMap<>();m.put("id",rs.getLong("id"));m.put("nome",rs.getString("nome"));m.put("cnpj",rs.getString("cnpj"));m.put("slug",rs.getString("slug"));m.put("database_name",rs.getString("database_name"));m.put("status",rs.getString("status"));m.put("plano_id",rs.getObject("plano_id"));m.put("plano",rs.getString("plano_nome"));m.put("plano_valor_centavos",rs.getObject("plano_valor_centavos"));m.put("dia_vencimento",rs.getObject("dia_vencimento"));m.put("periodo_status",rs.getString("periodo_status"));m.put("mes_referencia",rs.getObject("mes_referencia"));m.put("data_inicio",rs.getObject("data_inicio"));m.put("data_fim",rs.getObject("data_fim"));m.put("data_vencimento",rs.getObject("data_vencimento"));m.put("api_key",apiKeyFixa(rs.getString("slug")));return m;} private Map planoMap(Connection c,long id)throws SQLException{try(PreparedStatement ps=c.prepareStatement("SELECT * FROM planos WHERE id=?")){ps.setLong(1,id);try(ResultSet rs=ps.executeQuery()){rs.next();return planoMap(rs);}}} private Map planoMap(ResultSet rs)throws SQLException{Map m=new LinkedHashMap<>();m.put("id",rs.getLong("id"));m.put("nome",rs.getString("nome"));m.put("valor_mensal_centavos",rs.getLong("valor_mensal_centavos"));m.put("descricao",rs.getString("descricao"));m.put("ativo",rs.getBoolean("ativo"));return m;} private Map resultMap(ResultSet rs)throws SQLException{Map m=new LinkedHashMap<>();ResultSetMetaData md=rs.getMetaData();for(int i=1;i<=md.getColumnCount();i++){m.put(md.getColumnLabel(i),rs.getObject(i));}return m;} @@ -686,6 +695,7 @@ public final class MasterHandlers implements HttpHandler { private long parseId(String s){return Long.parseLong(s);} private boolean exists(Connection c,String sql,String value)throws SQLException{try(PreparedStatement ps=c.prepareStatement(sql)){ps.setString(1,value);try(ResultSet rs=ps.executeQuery()){return rs.next();}}} private boolean exists(Connection c,String sql,long value)throws SQLException{try(PreparedStatement ps=c.prepareStatement(sql)){ps.setLong(1,value);try(ResultSet rs=ps.executeQuery()){return rs.next();}}} + private boolean exists(Connection c,String sql,long value,String value2)throws SQLException{try(PreparedStatement ps=c.prepareStatement(sql)){ps.setLong(1,value);ps.setString(2,value2);try(ResultSet rs=ps.executeQuery()){return rs.next();}}} private void bind(PreparedStatement ps,List params)throws SQLException{for(int i=0;i stringArray(String raw,String key){String marker="\""+key+"\"";int p=raw.indexOf(marker);if(p<0)return List.of();int start=raw.indexOf('[',p);int end=raw.indexOf(']',start);if(start<0||end<0)return List.of();String body=raw.substring(start+1,end).trim();if(body.isBlank())return List.of();List out=new ArrayList<>();for(String part:body.split(",")){String v=part.trim();if(v.startsWith("\"")&&v.endsWith("\""))v=v.substring(1,v.length()-1);if(!v.isBlank())out.add(v);}return out;} private String removeArrayField(String raw,String key){String marker="\""+key+"\"";int p=raw.indexOf(marker);if(p<0)return raw;int start=raw.indexOf('[',p);int end=raw.indexOf(']',start);if(start<0||end<0)return raw;return raw.substring(0,start)+"\"\""+raw.substring(end+1);} diff --git a/src/main/webapp/master/clientes.js b/src/main/webapp/master/clientes.js index d150f8d..91fa492 100644 --- a/src/main/webapp/master/clientes.js +++ b/src/main/webapp/master/clientes.js @@ -1,10 +1,11 @@ const empresaForm=document.querySelector('#empresa-form'),rows=document.querySelector('#rows'),busca=document.querySelector('#busca'),planoSelect=document.querySelector('#plano-select');let empresas=[],planos=[],modo=null,selecionado=null; +function esc(v){return String(v??'').replace(/[&<>'"]/g,c=>({'&':'&','<':'<','>':'>',"'":''','"':'"'}[c]))} function planoLabel(p){return `${p.nome} - ${Util.money(p.valor_mensal_centavos)}`} function planoOptions(valor){return ''+planos.map(p=>``).join('')} function renderPlanos(){planoSelect.innerHTML=planoOptions('')} -function match(e,q){if(!q)return true;return [e.slug,e.nome,e.cnpj,e.status,e.plano].join(' ').toLowerCase().includes(q.toLowerCase())} +function match(e,q){if(!q)return true;return [e.slug,e.nome,e.cnpj,e.status,e.plano,e.api_key].join(' ').toLowerCase().includes(q.toLowerCase())} function statusAction(e){const acao=e.status==='ativa'?'Bloquear':'Ativar';return ``} -function detalheRow(e){if(selecionado!==e.id||!modo)return'';if(modo==='ver')return `

${e.nome}

Slug: ${e.slug}

CNPJ: ${e.cnpj||'-'}

Status: ${e.status}

Plano: ${e.plano?`${e.plano} ${e.plano_valor_centavos?'- '+Util.money(e.plano_valor_centavos):''}`:'-'}

Conta principal: abra Editar para alterar dados da empresa; funcionarios ficam no acesso da empresa.

`;return `
`} +function detalheRow(e){if(selecionado!==e.id||!modo)return'';if(modo==='ver')return `

${esc(e.nome)}

Slug: ${esc(e.slug)}

CNPJ: ${esc(e.cnpj||'-')}

Status: ${esc(e.status)}

Plano: ${e.plano?`${esc(e.plano)} ${e.plano_valor_centavos?'- '+Util.money(e.plano_valor_centavos):''}`:'-'}

Conta principal: abra Editar para alterar dados da empresa; funcionarios ficam no acesso da empresa.

`;return `
`} function render(){const q=busca.value.trim();const lista=empresas.filter(e=>match(e,q));rows.innerHTML=lista.length?lista.map(e=>`${e.slug}${e.nome}${statusAction(e)}${e.plano?`${e.plano} ${e.plano_valor_centavos?'- '+Util.money(e.plano_valor_centavos):''}`:'-'} ${detalheRow(e)}`).join(''):'Nenhum cliente encontrado.'} async function load(){try{const [pe,de]=await Promise.all([Api.get('/api/master/planos'),Api.get('/api/master/empresas')]);planos=pe.planos||[];empresas=de.empresas||[];renderPlanos();render()}catch(e){Util.toast(e.message,true)}} empresaForm.onsubmit=async e=>{e.preventDefault();try{await Api.post('/api/master/empresas',Util.formData(empresaForm));empresaForm.reset();Util.toast('Cliente cadastrado');load()}catch(err){Util.toast(err.message,true)}}; @@ -12,7 +13,9 @@ busca.oninput=render; window.ver=id=>{selecionado=selecionado===id&&modo==='ver'?null:id;modo=selecionado?'ver':null;render()}; window.editar=id=>{selecionado=selecionado===id&&modo==='editar'?null:id;modo=selecionado?'editar':null;render()}; window.fecharDetalhe=()=>{selecionado=null;modo=null;render()}; +window.copiarApiKey=async id=>{const e=empresas.find(x=>Number(x.id)===Number(id));if(!e?.api_key)return;try{await navigator.clipboard.writeText(e.api_key);Util.toast('API Key copiada')}catch(_){Util.toast('Selecione e copie a API Key manualmente',true)}}; +window.solicitarAlteracaoApiKey=id=>{const e=empresas.find(x=>Number(x.id)===Number(id));Util.toast('Solicitacao de alteracao registrada para '+(e?.nome||'cliente'))}; window.salvarEdicao=async(ev,id)=>{ev.preventDefault();try{await Api.put(`/api/master/empresas/${id}`,Util.formData(ev.target));Util.toast('Cliente atualizado');selecionado=null;modo=null;load()}catch(err){Util.toast(err.message,true)}}; window.alterarStatus=async(id,status)=>{try{await Api.post(`/api/master/empresas/${id}/status`,{status});Util.toast(status==='ativa'?'Cliente ativado':'Cliente bloqueado');load()}catch(err){Util.toast(err.message,true)}}; window.excluir=async id=>{try{await Api.request(`/api/master/empresas/${id}`,{method:'DELETE'});Util.toast('Cliente excluido');selecionado=null;modo=null;load()}catch(err){Util.toast(err.message,true)}}; -load(); \ No newline at end of file +load();