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 = `
|
||||
.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;
|
||||
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; });
|
||||
.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); }
|
||||
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 = '<span>Copyright © ' + new Date().getFullYear() + ' — CoreGuard</span><span>contato@neuralsys.com.br</span>';
|
||||
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 =
|
||||
'<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,
|
||||
customJsStr,
|
||||
customSiteTitle: 'WhatsApp Oficial API — Documentação',
|
||||
customfavIcon: '/api-docs/custom-logo.png',
|
||||
swaggerOptions: { docExpansion: 'none', persistAuthorization: true },
|
||||
}))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user