feat: apply same Swagger UI template as whatsapp-api

Logo, header (CoreGuard / WhatsAPI Oficial), search filter,
auth gate, PT-BR button labels and footer — identical to whatsapp-api.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 17:24:35 -03:00
parent b96d37ab11
commit 238bbba7bf
2 changed files with 328 additions and 41 deletions
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

+323 -36
View File
@@ -64,66 +64,352 @@ if (enableSwagger) {
} }
} }
routes.get('/api-docs/custom-logo.png', (req, res) => {
res.sendFile(path.resolve(__dirname, '..', 'LogoMenor.png'))
})
const customCss = ` const customCss = `
.swagger-ui .topbar { background: #1a1a2e; } /* Traduzir botões e labels do Swagger UI para PT-BR via CSS content */
.swagger-ui .topbar .topbar-wrapper .link { display: grid; grid-template-columns: 1fr; align-items: center; } .swagger-ui .btn.try-out__btn { font-size: 0; }
.swagger-ui .topbar .topbar-wrapper .link img { display: none; } .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 span { display: none; }
.swagger-ui .topbar .topbar-wrapper .link::before { .swagger-ui .topbar .topbar-wrapper .link::before {
content: "CoreGuard • WhatsAPI Oficial"; content: "CoreGuard";
color: #fff; font-family: sans-serif; font-weight: 700; font-size: 20px; padding: 8px 0; 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; } .swagger-ui .topbar .topbar-wrapper .link::after {
body:not(.neuralsys-authorized) .swagger-ui .opblock-tag { pointer-events: none; opacity: 0.55; } 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 .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 { .neuralsys-footer {
background: #000; color: #fff; padding: 16px 32px; font-size: 13px; background: #000;
display: flex; justify-content: space-between; font-family: sans-serif; margin-top: 40px; 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 = ` const customJsStr = `
(function () { (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() { function currentKey() {
try { try {
var auth = window.ui && window.ui.authSelectors.authorized(); var auth = window.ui.authSelectors.authorized()
if (!auth) return null; if (!auth) return null
var js = auth.toJS ? auth.toJS() : auth; var js = auth.toJS ? auth.toJS() : auth
if (js && js.apiKeyAuth && js.apiKeyAuth.value) return js.apiKeyAuth.value; if (js && js.apiKeyAuth && js.apiKeyAuth.value) return js.apiKeyAuth.value
} catch (e) {} } catch (e) {}
return null; 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) { function setAuthorized(ok) {
authorized = ok; authorized = ok
document.body.classList.toggle('neuralsys-authorized', ok); document.body.classList.toggle('neuralsys-authorized', ok)
if (!ok) { if (!ok) collapseAll()
document.querySelectorAll('.opblock-tag-section.is-open').forEach(function(s) {
var h = s.querySelector('.opblock-tag'); if (h) h.click();
});
}
} }
function validateKey(key) { function validateKey(key) {
if (validating) return; validating = true; if (validating) return
validating = true
fetch('/validateApiKey', { headers: { 'x-api-key': key } }) fetch('/validateApiKey', { headers: { 'x-api-key': key } })
.then(function(r) { setAuthorized(r.status === 200); }) .then(function (r) { setAuthorized(r.status === 200) })
.catch(function() { setAuthorized(false); }) .catch(function () { setAuthorized(false) })
.then(function() { validating = false; }); .then(function () { validating = false })
} }
function pollAuth() { function pollAuth() {
var key = currentKey(); var key = currentKey()
if (key !== lastKey) { lastKey = key; if (key) validateKey(key); else setAuthorized(false); } if (key !== lastKey) {
lastKey = key
if (key) { validateKey(key) } else { setAuthorized(false) }
} }
}
function installFooter() { function installFooter() {
if (document.querySelector('.neuralsys-footer')) return; if (document.querySelector('.neuralsys-footer')) return
var f = document.createElement('footer'); var footer = document.createElement('footer')
f.className = 'neuralsys-footer'; footer.className = 'neuralsys-footer'
f.innerHTML = '<span>Copyright © ' + new Date().getFullYear() + ' — CoreGuard</span><span>contato@neuralsys.com.br</span>'; var year = new Date().getFullYear()
document.body.appendChild(f); footer.innerHTML =
'<span>Copyright \\u00A9 ' + year + ' - CoreGuard</span>' +
'<a href="mailto:contato@neuralsys.com.br">' +
'<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">' +
'<path d="M20 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm0 4-8 5-8-5V6l8 5 8-5z"/>' +
'</svg>' +
'<span>contato@neuralsys.com.br</span>' +
'</a>'
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, customCss,
customJsStr, customJsStr,
customSiteTitle: 'WhatsApp Oficial API — Documentação', customSiteTitle: 'WhatsApp Oficial API — Documentação',
customfavIcon: '/api-docs/custom-logo.png',
swaggerOptions: { docExpansion: 'none', persistAuthorization: true }, swaggerOptions: { docExpansion: 'none', persistAuthorization: true },
})) }))
} }