Exibe api key no master

This commit is contained in:
Tiago
2026-10-06 15:59:27 -03:00
parent 8fd7c57c18
commit 4a7066296e
3 changed files with 20 additions and 7 deletions
@@ -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);
@@ -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<String, Object> 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<Map<String,Object>> 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<String,Object> 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<String,Object> empresaMap(Empresa e){Map<String,Object> 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<String,Object> empresaMap(Empresa e){Map<String,Object> 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<String,Object> funcionarioMap(Funcionario f){Map<String,Object> 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<String,Object> empresaResumoMap(ResultSet rs)throws SQLException{Map<String,Object> 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<String,Object> empresaResumoMap(ResultSet rs)throws SQLException{Map<String,Object> 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<String,Object> 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<String,Object> planoMap(ResultSet rs)throws SQLException{Map<String,Object> 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<String,Object> resultMap(ResultSet rs)throws SQLException{Map<String,Object> 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<Object> params)throws SQLException{for(int i=0;i<params.size();i++)ps.setObject(i+1,params.get(i));}
private List<String> 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<String> 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);}
+6 -3
View File
@@ -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=>({'&':'&amp;','<':'&lt;','>':'&gt;',"'":'&#39;','"':'&quot;'}[c]))}
function planoLabel(p){return `${p.nome} - ${Util.money(p.valor_mensal_centavos)}`}
function planoOptions(valor){return '<option value="">Sem plano</option>'+planos.map(p=>`<option value="${p.id}" ${String(valor||'')===String(p.id)?'selected':''}>${planoLabel(p)}</option>`).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 `<button class="status-action" onclick="alterarStatus(${e.id},'${e.status==='ativa'?'bloqueada':'ativa'}')">${Util.badge(e.status)}<span>${acao}</span></button>`}
function detalheRow(e){if(selecionado!==e.id||!modo)return'';if(modo==='ver')return `<tr><td colspan="5"><div class="inline-panel"><h2>${e.nome}</h2><p><strong>Slug:</strong> ${e.slug}</p><p><strong>CNPJ:</strong> ${e.cnpj||'-'}</p><p><strong>Status:</strong> ${e.status}</p><p><strong>Plano:</strong> ${e.plano?`${e.plano} ${e.plano_valor_centavos?'- '+Util.money(e.plano_valor_centavos):''}`:'-'}</p><p><strong>Conta principal:</strong> abra Editar para alterar dados da empresa; funcionarios ficam no acesso da empresa.</p><button class="btn secondary" onclick="fecharDetalhe()">Fechar</button></div></td></tr>`;return `<tr><td colspan="5"><form class="inline-panel grid cols" onsubmit="salvarEdicao(event,${e.id})"><div class="field"><label>Nome</label><input name="nome" value="${e.nome||''}" required></div><div class="field"><label>CNPJ</label><input name="cnpj" value="${e.cnpj||''}"></div><div class="field"><label>Plano</label><select name="plano_id">${planoOptions(e.plano_id)}</select></div><div class="row"><button class="btn">Salvar</button><button type="button" class="btn secondary" onclick="fecharDetalhe()">Cancelar</button></div></form></td></tr>`}
function detalheRow(e){if(selecionado!==e.id||!modo)return'';if(modo==='ver')return `<tr><td colspan="5"><div class="inline-panel"><h2>${esc(e.nome)}</h2><p><strong>Slug:</strong> ${esc(e.slug)}</p><p><strong>CNPJ:</strong> ${esc(e.cnpj||'-')}</p><p><strong>Status:</strong> ${esc(e.status)}</p><p><strong>Plano:</strong> ${e.plano?`${esc(e.plano)} ${e.plano_valor_centavos?'- '+Util.money(e.plano_valor_centavos):''}`:'-'}</p><div class="field"><label>API Key de integração</label><input readonly value="${esc(e.api_key||'')}" onclick="this.select()"></div><div class="row compact"><button class="btn secondary" type="button" onclick="copiarApiKey(${e.id})">Copiar API Key</button><button class="btn secondary" type="button" onclick="solicitarAlteracaoApiKey(${e.id})">Solicitar alteração</button></div><p><strong>Conta principal:</strong> abra Editar para alterar dados da empresa; funcionarios ficam no acesso da empresa.</p><button class="btn secondary" onclick="fecharDetalhe()">Fechar</button></div></td></tr>`;return `<tr><td colspan="5"><form class="inline-panel grid cols" onsubmit="salvarEdicao(event,${e.id})"><div class="field"><label>Nome</label><input name="nome" value="${esc(e.nome||'')}" required></div><div class="field"><label>CNPJ</label><input name="cnpj" value="${esc(e.cnpj||'')}"></div><div class="field"><label>Plano</label><select name="plano_id">${planoOptions(e.plano_id)}</select></div><div class="row"><button class="btn">Salvar</button><button type="button" class="btn secondary" onclick="fecharDetalhe()">Cancelar</button></div></form></td></tr>`}
function render(){const q=busca.value.trim();const lista=empresas.filter(e=>match(e,q));rows.innerHTML=lista.length?lista.map(e=>`<tr><td>${e.slug}</td><td>${e.nome}</td><td>${statusAction(e)}</td><td>${e.plano?`${e.plano} ${e.plano_valor_centavos?'- '+Util.money(e.plano_valor_centavos):''}`:'-'}</td><td><button class="btn secondary" onclick="editar(${e.id})">Editar</button> <button class="btn secondary" onclick="ver(${e.id})">Ver</button> <button class="btn danger" onclick="excluir(${e.id})">Excluir</button></td></tr>${detalheRow(e)}`).join(''):'<tr><td colspan="5">Nenhum cliente encontrado.</td></tr>'}
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();
load();