Fix Agentic Ops 401 — reuse Desk global api() and session check.

Forward Authorization in nginx; accept Spec 027 roles in JWT decode.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ligbox Spec Hub 2026-06-20 06:28:42 +00:00
parent 40bb16bbc9
commit 6daa692af8
5 changed files with 37 additions and 16 deletions

View file

@ -14,8 +14,8 @@ from typing import Any
from fastapi import Depends, Header, HTTPException, Request
from jose import JWTError, jwt
import bcrypt
import time
from app.permissions import HUMAN_ROLES
from app.totp_util import verify_code as verify_totp_code
DB_PATH = Path(os.getenv("SQLITE_PATH", "/data/ops.db"))
@ -134,7 +134,7 @@ def decode_token(token: str) -> DeskUser:
raise HTTPException(401, "invalid or expired token") from exc
username = payload.get("sub")
role = payload.get("role")
if not username or role not in {"super_admin", "ops_lead", "technician", "noc"}:
if not username or role not in HUMAN_ROLES:
raise HTTPException(401, "invalid token claims")
with db() as conn:
row = conn.execute(

View file

@ -2,10 +2,13 @@
const esc = (s) => String(s ?? '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
let state = { threadId: null, selectedAgent: 'A6' };
async function api(path, opts = {}) {
/** Usa o helper global do Desk (app.js) — garante JWT igual aos outros módulos. */
async function agentsApi(path, opts = {}) {
const deskApi = typeof globalThis.api === 'function' ? globalThis.api : null;
if (deskApi) return deskApi(`/v1/agents${path}`, opts);
const h = authHeaders({ ...(opts.headers || {}) });
if (!(opts.body instanceof FormData) && !h['Content-Type']) h['Content-Type'] = 'application/json';
const r = await fetchWithTimeout(`/api/v1/agents${path}`, { ...opts, headers: h });
const r = await fetchWithTimeout(`/api/v1/agents${path}`, { ...opts, headers: h }, 60000);
if (r.status === 401) {
logout();
throw new Error('sessão expirada — faça login novamente');
@ -53,7 +56,7 @@
const box = el.querySelector('#agentic-thread-messages');
if (!box) return;
box.innerHTML = '<p class="loading">Carregando thread…</p>';
const data = await api(`/threads/${threadId}/messages`);
const data = await agentsApi(`/threads/${threadId}/messages`);
box.innerHTML = data.messages.map(threadBubble).join('') || '<p class="empty">Sem mensagens.</p>';
box.scrollTop = box.scrollHeight;
}
@ -62,13 +65,24 @@
const el = document.getElementById('agentic-ops-content');
if (!el) return;
el.innerHTML = '<p class="loading">Carregando Agentic Ops…</p>';
if (!getToken()) {
el.innerHTML = '<p class="error">Sessão não encontrada neste endereço. <a href="/login.html">Fazer login</a> (use sempre o mesmo URL — ex. desk.ligbox.com.br).</p>';
return;
}
if (typeof ensureValidSession === 'function') {
const ok = await ensureValidSession();
if (!ok) {
el.innerHTML = '<p class="error">Sessão expirada. <a href="/login.html">Fazer login</a></p>';
return;
}
}
try {
const [health, roster, inbox, threads, findings] = await Promise.all([
api('/health'),
api('/roster'),
api('/inbox?limit=20'),
api('/threads?limit=15'),
api('/findings?limit=15'),
agentsApi('/health'),
agentsApi('/roster'),
agentsApi('/inbox?limit=20'),
agentsApi('/threads?limit=15'),
agentsApi('/findings?limit=15'),
]);
const tier = health.tier === 't1' ? 'T1 LLM' : 'T0';
const ollama = health.ollama
@ -149,7 +163,7 @@
});
el.querySelectorAll('[data-ack-msg]').forEach(btn => {
btn.addEventListener('click', async () => {
await api(`/messages/${btn.dataset.ackMsg}/ack`, { method: 'POST' });
await agentsApi(`/messages/${btn.dataset.ackMsg}/ack`, { method: 'POST' });
await renderAgenticOps();
});
});
@ -162,7 +176,7 @@
const tid = state.threadId || parseInt(el.querySelector('#agentic-thread-select')?.value, 10);
const body = (input?.value || '').trim();
if (!tid || !body) return;
await api(`/threads/${tid}/reply`, {
await agentsApi(`/threads/${tid}/reply`, {
method: 'POST',
body: JSON.stringify({ body, target_agent: state.selectedAgent }),
});
@ -177,7 +191,7 @@
out.hidden = false;
out.innerHTML = '<p class="loading">A pensar…</p>';
try {
const res = await api('/chat', {
const res = await agentsApi('/chat', {
method: 'POST',
body: JSON.stringify({ question: q, include_findings: true, target_agent: state.selectedAgent }),
});

View file

@ -443,7 +443,7 @@
<script src="/assets/tickets-workspace.js?v=20260619tickets2"></script>
<script src="/assets/tickets-detail-panel.js?v=20260619tickets2"></script>
<script src="/assets/servicos.js?v=20260620agentic"></script>
<script src="/assets/agentic-ops.js?v=20260620agentic2"></script>
<script src="/assets/app.js?v=20260620agentic"></script>
<script src="/assets/agentic-ops.js?v=20260620agentic3"></script>
<script src="/assets/app.js?v=20260620agentic3"></script>
</body>
</html>

View file

@ -4,7 +4,7 @@ server {
resolver 127.0.0.11 valid=10s ipv6=off;
location ~* \.(html)$ {
location ~* \.(html|js)$ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
try_files $uri =404;
}
@ -26,6 +26,7 @@ server {
proxy_pass http://$upstream:8080$request_uri;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Authorization $http_authorization;
proxy_connect_timeout 30s;
proxy_send_timeout 600s;
proxy_read_timeout 600s;

View file

@ -11,9 +11,15 @@ server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Authorization $http_authorization;
proxy_read_timeout 180s;
}
location ~* \.(html|js)$ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
try_files $uri =404;
}
location / {
try_files $uri $uri/ /index.html;
}