99 lines
3.5 KiB
JavaScript
99 lines
3.5 KiB
JavaScript
const PID_API_BASE = "/api";
|
|
|
|
function pidGetCookie(name) {
|
|
const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
|
|
return match ? decodeURIComponent(match[1]) : null;
|
|
}
|
|
|
|
async function pidEnsureCsrfCookie() {
|
|
if (pidGetCookie("csrftoken")) return;
|
|
await fetch(`${PID_API_BASE}/auth/csrf/`, { credentials: "include" });
|
|
}
|
|
|
|
/**
|
|
* Escape de texto para montar HTML por template string (`innerHTML`). Vale
|
|
* para conteúdo **e** para valor de atributo entre aspas duplas ou simples:
|
|
* trata `& < > " '`. Todo dado vindo do servidor, do usuário ou de arquivo
|
|
* anexado passa por aqui antes de entrar num `innerHTML` (senão um texto com
|
|
* HTML vira código executado no navegador de quem abre a tela: XSS). Nunca
|
|
* usar em HTML que já é intencional (texto rico sanitizado no servidor com
|
|
* nh3, como a Ajuda e as observações de Acessos Gerais).
|
|
*/
|
|
const PID_ESCAPE_HTML_MAPA = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
|
|
function pidEscapeHtml(texto) {
|
|
if (texto === null || texto === undefined) return "";
|
|
return String(texto).replace(/[&<>"']/g, (ch) => PID_ESCAPE_HTML_MAPA[ch]);
|
|
}
|
|
|
|
// Mensagem genérica já diz o que fazer em seguida, pra quem lê não ficar sem saída.
|
|
const PID_ERRO_GENERICO = "Erro ao processar a solicitação. Tente novamente; se persistir, contate a Inovação.";
|
|
|
|
function pidErrorMessageFrom(data) {
|
|
if (!data) return PID_ERRO_GENERICO;
|
|
if (typeof data === "string") return data;
|
|
if (typeof data.detail === "string") return data.detail;
|
|
const values = Object.values(data).flat().filter((v) => typeof v === "string");
|
|
return values.length ? values.join(" ") : PID_ERRO_GENERICO;
|
|
}
|
|
|
|
/**
|
|
* Fetch com sessão (cookie) do Django. Adiciona X-CSRFToken automaticamente em
|
|
* métodos que não são GET/HEAD/OPTIONS, buscando o cookie csrftoken antes se
|
|
* ainda não existir. Em 401, redireciona para o login por padrão — passe
|
|
* `redirectOn401: false` em telas onde uma resposta 401 é esperada (ex.: o
|
|
* próprio formulário de login, ou a checagem de sessão já ativa em index.html).
|
|
*/
|
|
async function pidApiRequest(path, options = {}) {
|
|
const method = (options.method || "GET").toUpperCase();
|
|
const isSafeMethod = method === "GET" || method === "HEAD" || method === "OPTIONS";
|
|
const redirectOn401 = options.redirectOn401 !== false;
|
|
|
|
if (!isSafeMethod) await pidEnsureCsrfCookie();
|
|
|
|
const headers = { Accept: "application/json", ...(options.headers || {}) };
|
|
let body = options.body;
|
|
const isFormData = typeof FormData !== "undefined" && body instanceof FormData;
|
|
if (body !== undefined && !isFormData) {
|
|
headers["Content-Type"] = "application/json";
|
|
body = JSON.stringify(body);
|
|
}
|
|
// FormData: deixa o browser definir o Content-Type (com o boundary do multipart) sozinho.
|
|
if (!isSafeMethod) headers["X-CSRFToken"] = pidGetCookie("csrftoken") || "";
|
|
|
|
let response;
|
|
try {
|
|
response = await fetch(`${PID_API_BASE}${path}`, {
|
|
method,
|
|
credentials: "include",
|
|
headers,
|
|
body,
|
|
});
|
|
} catch (e) {
|
|
throw new Error("Não foi possível conectar ao servidor.");
|
|
}
|
|
|
|
if (response.status === 401 && redirectOn401) {
|
|
window.location.href = "index.html";
|
|
return null;
|
|
}
|
|
|
|
if (response.status === 204) return null;
|
|
|
|
const text = await response.text();
|
|
let data = null;
|
|
if (text) {
|
|
try {
|
|
data = JSON.parse(text);
|
|
} catch (e) {
|
|
data = null;
|
|
}
|
|
}
|
|
|
|
if (!response.ok) {
|
|
throw new Error(pidErrorMessageFrom(data));
|
|
}
|
|
|
|
return data;
|
|
}
|