diff --git a/LogoMenor.png b/LogoMenor.png new file mode 100644 index 0000000..aa6a647 Binary files /dev/null and b/LogoMenor.png differ diff --git a/src/routes.js b/src/routes.js index 4864198..6fc335d 100644 --- a/src/routes.js +++ b/src/routes.js @@ -64,66 +64,352 @@ if (enableSwagger) { } } + routes.get('/api-docs/custom-logo.png', (req, res) => { + res.sendFile(path.resolve(__dirname, '..', 'LogoMenor.png')) + }) + const customCss = ` - .swagger-ui .topbar { background: #1a1a2e; } - .swagger-ui .topbar .topbar-wrapper .link { display: grid; grid-template-columns: 1fr; align-items: center; } - .swagger-ui .topbar .topbar-wrapper .link img { display: none; } + /* Traduzir botões e labels do Swagger UI para PT-BR via CSS content */ + .swagger-ui .btn.try-out__btn { font-size: 0; } + .swagger-ui .btn.try-out__btn::before { content: "Experimentar"; font-size: 14px; } + .swagger-ui .try-out .btn.cancel { font-size: 0; } + .swagger-ui .try-out .btn.cancel::before { content: "Cancelar"; font-size: 14px; } + .swagger-ui .btn.execute { font-size: 0; } + .swagger-ui .btn.execute::before { content: "Executar"; font-size: 14px; font-weight: bold; } + .swagger-ui .btn.btn-clear { font-size: 0; } + .swagger-ui .btn.btn-clear::before { content: "Limpar"; font-size: 14px; } + .swagger-ui .auth-wrapper .authorize { font-size: 0; } + .swagger-ui .auth-wrapper .authorize span { display: none; } + .swagger-ui .auth-wrapper .authorize::before { content: "Autorizar"; font-size: 14px; font-weight: bold; } + .swagger-ui .auth-btn-wrapper .btn-done { font-size: 0; } + .swagger-ui .auth-btn-wrapper .btn-done::before { content: "Fechar"; font-size: 14px; } + .swagger-ui .auth-container .auth-btn-wrapper .authorize { font-size: 0; } + .swagger-ui .auth-container .auth-btn-wrapper .authorize::before { content: "Autorizar"; font-size: 14px; } + .swagger-ui .btn-group .btn { color: inherit; } + .swagger-ui .info .title small { display: none; } + + /* Logo customizada da Neuralsys no topbar (substitui a logo do Swagger) */ + .swagger-ui .topbar .topbar-wrapper .link { + display: grid; + grid-template-columns: auto 1fr; + grid-template-rows: auto auto; + column-gap: 14px; + align-items: center; + } + .swagger-ui .topbar .topbar-wrapper .link img { + content: url("./custom-logo.png"); + height: 56px; + width: auto; + grid-column: 1; + grid-row: 1 / span 2; + } .swagger-ui .topbar .topbar-wrapper .link span { display: none; } .swagger-ui .topbar .topbar-wrapper .link::before { - content: "CoreGuard • WhatsAPI Oficial"; - color: #fff; font-family: sans-serif; font-weight: 700; font-size: 20px; padding: 8px 0; + content: "CoreGuard"; + grid-column: 2; + grid-row: 1; + align-self: end; + color: #fff; + font-family: sans-serif; + font-weight: 700; + font-size: 22px; + line-height: 1; + letter-spacing: 0.3px; } - .swagger-ui section.models, .swagger-ui .models { display: none !important; } - body:not(.neuralsys-authorized) .swagger-ui .opblock-tag { pointer-events: none; opacity: 0.55; } + .swagger-ui .topbar .topbar-wrapper .link::after { + content: "WhatsAPI Oficial"; + grid-column: 2; + grid-row: 2; + align-self: start; + color: #fff; + font-family: sans-serif; + font-weight: 600; + font-size: 14px; + line-height: 1.1; + letter-spacing: 0.4px; + margin-top: 3px; + } + + /* Classe usada pelo filtro customizado para esconder endpoints/seções */ + .swagger-ui .neuralsys-hidden { display: none !important; } + + /* Container do filtro customizado (substitui o filtro nativo) */ + .swagger-ui .neuralsys-filter-container { + margin: 20px 0; + padding: 0 20px; + max-width: 1460px; + box-sizing: border-box; + } + .swagger-ui .neuralsys-filter-container input { + width: 100%; + padding: 10px 14px; + border: 2px solid #41444e; + border-radius: 4px; + font-size: 14px; + box-sizing: border-box; + outline: none; + transition: border-color .15s; + } + .swagger-ui .neuralsys-filter-container input:focus { + border-color: #4990e2; + } + + /* Labels de "Parameters" / "Responses" / "No parameters" etc. */ + .swagger-ui .opblock-section-header h4 span[data-name="parameters"]::after, + .swagger-ui .opblock-section-header h4.parameters-header::after { content: ""; } + + /* Botão "Copy to clipboard" sempre visível nos endpoints da lista */ + .swagger-ui .opblock .opblock-summary .view-line-link.copy-to-clipboard { + width: 24px !important; + } + + /* Ocultar seção "Schemas" no final da página */ + .swagger-ui section.models, + .swagger-ui .models { display: none !important; } + + /* Gate: bloqueia expansão e filtragem até autorizar com apiKey válida */ + body:not(.neuralsys-authorized) .swagger-ui .opblock-tag { pointer-events: none; opacity: 0.55; cursor: not-allowed; } body:not(.neuralsys-authorized) .swagger-ui .opblock-tag-section .no-margin { display: none !important; } + body:not(.neuralsys-authorized) .swagger-ui .neuralsys-filter-container { display: none !important; } + /* Rodapé minimalista */ + body { margin-bottom: 0; } .neuralsys-footer { - background: #000; color: #fff; padding: 16px 32px; font-size: 13px; - display: flex; justify-content: space-between; font-family: sans-serif; margin-top: 40px; + background: #000; + color: #fff; + padding: 16px 32px; + font-size: 13px; + display: flex; + justify-content: space-between; + align-items: center; + font-family: sans-serif; + margin-top: 40px; } + .neuralsys-footer a { color: #fff; text-decoration: none; display: inline-flex; align-items: center; gap: 8px; } + .neuralsys-footer a:hover { text-decoration: underline; } + .neuralsys-footer svg { width: 16px; height: 16px; fill: currentColor; } ` const customJsStr = ` (function () { - var authorized = false, lastKey = null, validating = false; + var HIDDEN = 'neuralsys-hidden' + + function buildCorpus(op, rawPath) { + var paramNames = (op.parameters || []).map(function (p) { return p.name || '' }).join(' ') + var props = op && op.requestBody && op.requestBody.content && + op.requestBody.content['application/json'] && + op.requestBody.content['application/json'].schema && + op.requestBody.content['application/json'].schema.properties + var reqBodyProps = props ? Object.keys(props).join(' ') : '' + return (rawPath + ' ' + (op.summary || '') + ' ' + (op.description || '') + + ' ' + paramNames + ' ' + reqBodyProps).toLowerCase() + } + + function resetAll() { + document.querySelectorAll('.' + HIDDEN).forEach(function (el) { + el.classList.remove(HIDDEN) + }) + } + + function installFilter() { + if (!window.ui) { return setTimeout(installFilter, 250) } + if (document.querySelector('.neuralsys-filter-container')) return + + var wrapper = document.querySelector('.swagger-ui .wrapper') + var firstSection = document.querySelector('.opblock-tag-section') + if (!wrapper || !firstSection) { return setTimeout(installFilter, 250) } + + var container = document.createElement('div') + container.className = 'neuralsys-filter-container' + var input = document.createElement('input') + input.type = 'text' + input.placeholder = 'Filtrar...' + input.setAttribute('aria-label', 'Filtrar endpoints') + container.appendChild(input) + + firstSection.parentNode.insertBefore(container, firstSection) + + var specJson = window.ui.specSelectors.specJson().toJS() + var paths = specJson.paths || {} + + var tagIndex = null + function buildTagIndex() { + if (tagIndex) return tagIndex + tagIndex = {} + var httpMethods = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head'] + Object.keys(paths).forEach(function (p) { + Object.keys(paths[p] || {}).forEach(function (m) { + var ml = m.toLowerCase() + if (httpMethods.indexOf(ml) === -1) return + var op = paths[p][m] + if (!op || typeof op !== 'object') return + var tags = (op.tags && op.tags.length) ? op.tags : ['default'] + tags.forEach(function (t) { + if (!tagIndex[t]) tagIndex[t] = [] + tagIndex[t].push({ path: p, method: ml, corpus: buildCorpus(op, p) }) + }) + }) + }) + return tagIndex + } + + function getTagName(sec) { + var tagEl = sec.querySelector('.opblock-tag') + if (!tagEl) return '' + var name = tagEl.getAttribute('data-tag') || tagEl.getAttribute('data-name') + if (name) return name + var span = tagEl.querySelector('span') || tagEl + return ((span.textContent || '').replace(/\\s+/g, ' ') || '').trim() + } + + function hideNonMatchingBlocks(q) { + document.querySelectorAll('.opblock').forEach(function (block) { + var pathEl = block.querySelector('.opblock-summary-path') + var methodEl = block.querySelector('.opblock-summary-method') + if (!pathEl || !methodEl) { + block.classList.remove(HIDDEN) + return + } + var rawPath = pathEl.getAttribute('data-path') || + (pathEl.querySelector('a') && pathEl.querySelector('a').textContent) || + pathEl.textContent + rawPath = (rawPath || '').trim() + var method = methodEl.textContent.trim().toLowerCase() + var op = (paths[rawPath] && paths[rawPath][method]) || {} + var corpus = buildCorpus(op, rawPath) + if (corpus.indexOf(q) !== -1) { + block.classList.remove(HIDDEN) + } else { + block.classList.add(HIDDEN) + } + }) + } + + function applyFilter() { + var q = input.value.trim().toLowerCase() + if (!q) { resetAll(); return } + + var idx = buildTagIndex() + var needsExpansion = false + + document.querySelectorAll('.opblock-tag-section').forEach(function (sec) { + var tagName = getTagName(sec) + var ops = idx[tagName] || [] + var hasMatch = ops.some(function (o) { return o.corpus.indexOf(q) !== -1 }) + if (!hasMatch) { + sec.classList.add(HIDDEN) + return + } + sec.classList.remove(HIDDEN) + if (!sec.classList.contains('is-open')) { + var header = sec.querySelector('.opblock-tag') + if (header) { + header.click() + needsExpansion = true + } + } + }) + + if (needsExpansion) { + setTimeout(function () { hideNonMatchingBlocks(q) }, 80) + } else { + hideNonMatchingBlocks(q) + } + } + + input.addEventListener('input', applyFilter) + } + + var observer = new MutationObserver(function () { + if (!document.querySelector('.neuralsys-filter-container') && + document.querySelector('.opblock-tag-section')) { + installFilter() + } + translateCopyButtons() + if (!document.querySelector('.neuralsys-footer')) installFooter() + }) + + function translateCopyButtons() { + var label = 'Copiar para área de transferência' + document.querySelectorAll('.view-line-link.copy-to-clipboard, .copy-to-clipboard').forEach(function (el) { + if (el.getAttribute('title') !== label) el.setAttribute('title', label) + if (el.getAttribute('aria-label') !== label) el.setAttribute('aria-label', label) + }) + } + + var authorized = false + var lastKey = null + var validating = false + function currentKey() { try { - var auth = window.ui && window.ui.authSelectors.authorized(); - if (!auth) return null; - var js = auth.toJS ? auth.toJS() : auth; - if (js && js.apiKeyAuth && js.apiKeyAuth.value) return js.apiKeyAuth.value; - } catch(e) {} - return null; + var auth = window.ui.authSelectors.authorized() + if (!auth) return null + var js = auth.toJS ? auth.toJS() : auth + if (js && js.apiKeyAuth && js.apiKeyAuth.value) return js.apiKeyAuth.value + } catch (e) {} + return null } + + function collapseAll() { + document.querySelectorAll('.opblock-tag-section.is-open').forEach(function (sec) { + var header = sec.querySelector('.opblock-tag') + if (header) header.click() + }) + } + function setAuthorized(ok) { - authorized = ok; - document.body.classList.toggle('neuralsys-authorized', ok); - if (!ok) { - document.querySelectorAll('.opblock-tag-section.is-open').forEach(function(s) { - var h = s.querySelector('.opblock-tag'); if (h) h.click(); - }); + authorized = ok + document.body.classList.toggle('neuralsys-authorized', ok) + if (!ok) collapseAll() + } + + function validateKey(key) { + if (validating) return + validating = true + fetch('/validateApiKey', { headers: { 'x-api-key': key } }) + .then(function (r) { setAuthorized(r.status === 200) }) + .catch(function () { setAuthorized(false) }) + .then(function () { validating = false }) + } + + function pollAuth() { + var key = currentKey() + if (key !== lastKey) { + lastKey = key + if (key) { validateKey(key) } else { setAuthorized(false) } } } - function validateKey(key) { - if (validating) return; validating = true; - fetch('/validateApiKey', { headers: { 'x-api-key': key } }) - .then(function(r) { setAuthorized(r.status === 200); }) - .catch(function() { setAuthorized(false); }) - .then(function() { validating = false; }); - } - function pollAuth() { - var key = currentKey(); - if (key !== lastKey) { lastKey = key; if (key) validateKey(key); else setAuthorized(false); } - } + function installFooter() { - if (document.querySelector('.neuralsys-footer')) return; - var f = document.createElement('footer'); - f.className = 'neuralsys-footer'; - f.innerHTML = 'Copyright © ' + new Date().getFullYear() + ' — CoreGuardcontato@neuralsys.com.br'; - document.body.appendChild(f); + if (document.querySelector('.neuralsys-footer')) return + var footer = document.createElement('footer') + footer.className = 'neuralsys-footer' + var year = new Date().getFullYear() + footer.innerHTML = + 'Copyright \\u00A9 ' + year + ' - CoreGuard' + + '' + + '' + + '' + + '' + + 'contato@neuralsys.com.br' + + '' + document.body.appendChild(footer) + } + + function boot() { + installFilter() + translateCopyButtons() + installFooter() + setAuthorized(false) + setInterval(pollAuth, 500) + observer.observe(document.body, { childList: true, subtree: true }) + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', function () { setTimeout(boot, 400) }) + } else { + setTimeout(boot, 400) } - function boot() { setAuthorized(false); setInterval(pollAuth, 500); installFooter(); } - if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function() { setTimeout(boot, 400); }); - else setTimeout(boot, 400); })(); ` @@ -132,6 +418,7 @@ if (enableSwagger) { customCss, customJsStr, customSiteTitle: 'WhatsApp Oficial API — Documentação', + customfavIcon: '/api-docs/custom-logo.png', swaggerOptions: { docExpansion: 'none', persistAuthorization: true }, })) }