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:
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
+324
-37
@@ -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 },
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user