Skip to content

Commit f8c7d23

Browse files
fix(dashboard): prevent XSS and handle missing API key clearly
Replaces innerHTML interpolation of server-supplied fields (name, clientName, jid) with DOM createElement/textContent to eliminate potential XSS. Also detects a missing or empty API key before making any requests and renders an actionable message explaining how to set it, instead of showing a generic fetch error.
1 parent 59fb736 commit f8c7d23

1 file changed

Lines changed: 76 additions & 34 deletions

File tree

manager/dashboard/index.html

Lines changed: 76 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,14 @@ <h2 class="text-base font-semibold text-gray-900">Instâncias</h2>
147147
<script>
148148
const apiKey = localStorage.getItem('apikey') || ''
149149

150+
function esc(str) {
151+
const d = document.createElement('div')
152+
d.textContent = str == null ? '' : String(str)
153+
return d.innerHTML
154+
}
155+
150156
async function apiFetch(path) {
157+
if (!apiKey) throw new Error('missing-apikey')
151158
const res = await fetch(path, { headers: { apikey: apiKey } })
152159
if (!res.ok) throw new Error(`HTTP ${res.status}`)
153160
return res.json()
@@ -158,52 +165,87 @@ <h2 class="text-base font-semibold text-gray-900">Instâncias</h2>
158165
if (el) el.textContent = val
159166
}
160167

168+
function renderMissingApiKey() {
169+
const container = document.getElementById('table-container')
170+
container.innerHTML = '<div class="px-6 py-10 text-center text-sm text-yellow-600">API Key não configurada. Defina <code>localStorage.setItem(\'apikey\', \'SUA_KEY\')</code> no console e recarregue.</div>'
171+
set('metric-total', '—')
172+
set('metric-connected', '—')
173+
set('metric-disconnected', '—')
174+
}
175+
161176
function renderTable(instances) {
162177
const container = document.getElementById('table-container')
163178
if (!instances.length) {
164179
container.innerHTML = '<div class="px-6 py-10 text-center text-sm text-gray-400">Nenhuma instância encontrada.</div>'
165180
return
166181
}
167-
const rows = instances.map(inst => {
182+
183+
const wrapper = document.createElement('div')
184+
wrapper.className = 'overflow-x-auto'
185+
186+
const table = document.createElement('table')
187+
table.className = 'w-full text-left'
188+
189+
const thead = document.createElement('thead')
190+
thead.innerHTML = `
191+
<tr class="bg-gray-50">
192+
<th class="py-3 px-4 text-xs font-semibold uppercase tracking-wide text-gray-500">Nome</th>
193+
<th class="py-3 px-4 text-xs font-semibold uppercase tracking-wide text-gray-500">Status</th>
194+
<th class="py-3 px-4 text-xs font-semibold uppercase tracking-wide text-gray-500">Telefone</th>
195+
<th class="py-3 px-4 text-xs font-semibold uppercase tracking-wide text-gray-500">Client</th>
196+
<th class="py-3 px-4 text-xs font-semibold uppercase tracking-wide text-gray-500">AlwaysOnline</th>
197+
</tr>`
198+
table.appendChild(thead)
199+
200+
const tbody = document.createElement('tbody')
201+
instances.forEach(inst => {
168202
const phone = (inst.jid || '').replace(/@.+/, '') || '—'
169-
const statusBadge = inst.connected
170-
? `<span class="inline-flex items-center gap-1.5 rounded-full bg-green-50 px-2.5 py-0.5 text-xs font-medium text-green-700">
171-
<span class="h-1.5 w-1.5 rounded-full bg-green-500 pulse"></span>Conectado</span>`
172-
: `<span class="inline-flex items-center gap-1.5 rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-500">
173-
<span class="h-1.5 w-1.5 rounded-full bg-gray-400"></span>Desconectado</span>`
174-
const alwaysOnline = inst.alwaysOnline
203+
204+
const tr = document.createElement('tr')
205+
tr.className = 'border-t border-gray-100 hover:bg-gray-50 transition-colors'
206+
207+
const tdName = document.createElement('td')
208+
tdName.className = 'py-3 px-4'
209+
const nameSpan = document.createElement('span')
210+
nameSpan.className = 'font-medium text-gray-900 text-sm'
211+
nameSpan.textContent = inst.name || '—'
212+
tdName.appendChild(nameSpan)
213+
214+
const tdStatus = document.createElement('td')
215+
tdStatus.className = 'py-3 px-4'
216+
tdStatus.innerHTML = inst.connected
217+
? `<span class="inline-flex items-center gap-1.5 rounded-full bg-green-50 px-2.5 py-0.5 text-xs font-medium text-green-700"><span class="h-1.5 w-1.5 rounded-full bg-green-500 pulse"></span>Conectado</span>`
218+
: `<span class="inline-flex items-center gap-1.5 rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-500"><span class="h-1.5 w-1.5 rounded-full bg-gray-400"></span>Desconectado</span>`
219+
220+
const tdPhone = document.createElement('td')
221+
tdPhone.className = 'py-3 px-4 text-sm text-gray-500'
222+
tdPhone.textContent = phone
223+
224+
const tdClient = document.createElement('td')
225+
tdClient.className = 'py-3 px-4 text-sm text-gray-400'
226+
tdClient.textContent = inst.clientName || '—'
227+
228+
const tdAO = document.createElement('td')
229+
tdAO.className = 'py-3 px-4'
230+
tdAO.innerHTML = inst.alwaysOnline
175231
? `<span class="inline-flex items-center gap-1 rounded-full bg-blue-50 px-2.5 py-0.5 text-xs font-medium text-blue-700">⚡ Ativo</span>`
176232
: `<span class="text-xs text-gray-400">—</span>`
177-
return `
178-
<tr class="border-t border-gray-100 hover:bg-gray-50 transition-colors">
179-
<td class="py-3 px-4">
180-
<span class="font-medium text-gray-900 text-sm">${inst.name}</span>
181-
</td>
182-
<td class="py-3 px-4">${statusBadge}</td>
183-
<td class="py-3 px-4 text-sm text-gray-500">${phone}</td>
184-
<td class="py-3 px-4 text-sm text-gray-400">${inst.clientName || '—'}</td>
185-
<td class="py-3 px-4">${alwaysOnline}</td>
186-
</tr>`
187-
}).join('')
188-
189-
container.innerHTML = `
190-
<div class="overflow-x-auto">
191-
<table class="w-full text-left">
192-
<thead>
193-
<tr class="bg-gray-50">
194-
<th class="py-3 px-4 text-xs font-semibold uppercase tracking-wide text-gray-500">Nome</th>
195-
<th class="py-3 px-4 text-xs font-semibold uppercase tracking-wide text-gray-500">Status</th>
196-
<th class="py-3 px-4 text-xs font-semibold uppercase tracking-wide text-gray-500">Telefone</th>
197-
<th class="py-3 px-4 text-xs font-semibold uppercase tracking-wide text-gray-500">Client</th>
198-
<th class="py-3 px-4 text-xs font-semibold uppercase tracking-wide text-gray-500">AlwaysOnline</th>
199-
</tr>
200-
</thead>
201-
<tbody>${rows}</tbody>
202-
</table>
203-
</div>`
233+
234+
tr.append(tdName, tdStatus, tdPhone, tdClient, tdAO)
235+
tbody.appendChild(tr)
236+
})
237+
238+
table.appendChild(tbody)
239+
wrapper.appendChild(table)
240+
container.replaceChildren(wrapper)
204241
}
205242

206243
async function loadData() {
244+
if (!apiKey) {
245+
renderMissingApiKey()
246+
return
247+
}
248+
207249
const icon = document.getElementById('refresh-icon')
208250
icon.classList.add('spin')
209251
try {

0 commit comments

Comments
 (0)