feat: intégration TikTak #197-200 + scoped tokens, webhooks filtrés, listes mtime/size ; fix e2e-server BUG-112
CI / lint (push) Successful in 3m17s
CI / security (push) Failing after 1m51s
CI / test (push) Successful in 5m14s
CI / build (push) Successful in 1m50s
CI / e2e (push) Successful in 18m18s

This commit is contained in:
2026-10-14 11:42:53 -04:00
parent 2be0b15bcf
commit f783394f09
59 changed files with 3233 additions and 77 deletions
+33 -5
View File
@@ -1486,14 +1486,20 @@ function renderWebhooksUI(webhooks) {
const list = document.getElementById("webhooks-list");
if (!list) return;
if (!webhooks.length) { list.innerHTML = '<div class="config-description">Aucun webhook configuré.</div>'; return; }
list.innerHTML = webhooks.map(w => `
list.innerHTML = webhooks.map(w => {
const filters = [];
if (w.vault) filters.push("vault:" + w.vault);
if (w.path_prefix) filters.push("⇢ " + w.path_prefix);
return `
<div class="webhook-item">
<span class="webhook-name">${escapeHtml(w.name)}</span>
<span class="webhook-url">${escapeHtml(w.url)}</span>
<span class="webhook-events">${(w.events||[]).join(", ")}</span>
${filters.length ? `<span class="webhook-filter">${escapeHtml(filters.join(" · "))}</span>` : ""}
<button class="webhook-delete" data-id="${w.id}">✕</button>
</div>
`).join("");
`;
}).join("");
list.querySelectorAll(".webhook-delete").forEach(b => b.addEventListener("click", async () => {
await api(`/api/webhooks/${b.dataset.id}`, { method: "DELETE" });
loadWebhooksUI();
@@ -1503,8 +1509,13 @@ document.addEventListener("click", function(e) {
if (e.target.id === "webhook-add-btn") {
const name = document.getElementById("webhook-name-input").value.trim();
const url = document.getElementById("webhook-url-input").value.trim();
const vaultEl = document.getElementById("webhook-vault-input");
const prefixEl = document.getElementById("webhook-prefix-input");
if (!url) { showToast(t("config.url_required"), "error"); return; }
api("/api/webhooks", { method: "POST", body: JSON.stringify({ name: name || "Webhook", url, events: ["file_created","file_deleted","file_modified","file_renamed"] }) }).then(() => { loadWebhooksUI(); document.getElementById("webhook-name-input").value = ""; document.getElementById("webhook-url-input").value = ""; }).catch(err => showToast(err.message, "error"));
const body = { name: name || "Webhook", url, events: ["file_created","file_deleted","file_modified","file_renamed"] };
if (vaultEl && vaultEl.value.trim()) body.vault = vaultEl.value.trim();
if (prefixEl && prefixEl.value.trim()) body.path_prefix = prefixEl.value.trim();
api("/api/webhooks", { method: "POST", body: JSON.stringify(body) }).then(() => { loadWebhooksUI(); document.getElementById("webhook-name-input").value = ""; document.getElementById("webhook-url-input").value = ""; if (vaultEl) vaultEl.value = ""; if (prefixEl) prefixEl.value = ""; }).catch(err => showToast(err.message, "error"));
}
});
@@ -1546,6 +1557,12 @@ function renderTokensUI(tokens) {
const status = expired
? '<span class="token-badge token-badge-expired">' + escapeHtml(t("config.token_status_expired")) + "</span>"
: '<span class="token-badge token-badge-active">' + escapeHtml(t("config.token_status_active")) + "</span>";
// #199 : portée affichée (pleine vs fichiers seuls + vaults).
const scopeLabel = (tok.scope || "full") === "files" ? t("config.token_scope_files") : t("config.token_scope_full");
const scopeBadge = '<span class="token-badge token-badge-scope">' + escapeHtml(scopeLabel) + "</span>";
const vaultsLabel = (tok.vaults && tok.vaults.length)
? escapeHtml(tok.vaults.join(", "))
: escapeHtml(t("config.token_vaults_all"));
const meta = [
t("config.token_created") + " " + _formatTokenDate(tok.created_at),
t("config.token_expires") + " " + _tokenExpiryLabel(tok),
@@ -1553,8 +1570,9 @@ function renderTokensUI(tokens) {
].join(" · ");
return '<div class="token-item" data-jti="' + escapeHtml(tok.jti) + '">' +
'<span class="token-name">' + escapeHtml(tok.name) + "</span>" +
status +
status + scopeBadge +
'<span class="token-meta">' + escapeHtml(meta) + "</span>" +
'<span class="token-meta token-vaults">🔒 ' + vaultsLabel + "</span>" +
'<button class="token-delete" data-jti="' + escapeHtml(tok.jti) + '" data-name="' + escapeHtml(tok.name) + '" title="' + escapeHtml(t("config.token_revoke")) + '">✕</button>' +
"</div>";
}).join("");
@@ -1582,13 +1600,23 @@ function bindTokenEvents() {
if (!name) { showToast(t("config.token_name_required"), "error"); return; }
createBtn.disabled = true;
try {
const res = await api("/api/auth/tokens", { method: "POST", body: JSON.stringify({ name, expiry }) });
// #199 : portée limitée optionnelle (vaults + périmètre fichiers).
const scopeEl = document.getElementById("token-scope-select");
const vaultsEl = document.getElementById("token-vaults-input");
const body = { name, expiry };
if (scopeEl && scopeEl.value === "files") body.scope = "files";
if (vaultsEl && vaultsEl.value.trim()) {
body.vaults = vaultsEl.value.split(",").map(s => s.trim()).filter(Boolean);
}
const res = await api("/api/auth/tokens", { method: "POST", body: JSON.stringify(body) });
const area = document.getElementById("token-secret-area");
const ta = document.getElementById("token-secret-value");
ta.value = res.token;
area.classList.remove("hidden");
ta.select();
document.getElementById("token-name-input").value = "";
const _vaultsEl = document.getElementById("token-vaults-input");
if (_vaultsEl) _vaultsEl.value = "";
showToast(t("config.token_created_toast"), "success");
loadTokensUI();
} catch (err) {