feat: intégration TikTak #197-200 + scoped tokens, webhooks filtrés, listes mtime/size ; fix e2e-server BUG-112
This commit is contained in:
+39
-7
@@ -2576,6 +2576,18 @@
|
||||
<option value="365d" data-i18n="config.token_expiry_365d">1 an</option>
|
||||
<option value="never" data-i18n="config.token_expiry_never">Sans fin</option>
|
||||
</select>
|
||||
<select id="token-scope-select" class="config-input" style="width: 150px" title="Portée">
|
||||
<option value="full" data-i18n="config.token_scope_full">Accès complet</option>
|
||||
<option value="files" data-i18n="config.token_scope_files">Fichiers seuls</option>
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
id="token-vaults-input"
|
||||
data-i18n-placeholder="config.token_vaults_placeholder"
|
||||
placeholder="Vaults (vide = tous, ex : TikTak)"
|
||||
class="config-input"
|
||||
style="width: 200px"
|
||||
/>
|
||||
<button class="config-btn-add" id="token-create-btn" data-i18n="config.token_create">Créer une clé</button>
|
||||
</div>
|
||||
<div id="token-secret-area" class="token-secret-area hidden">
|
||||
@@ -2633,6 +2645,22 @@
|
||||
class="config-input"
|
||||
style="flex: 1"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
id="webhook-vault-input"
|
||||
data-i18n-placeholder="config.webhook_vault_placeholder"
|
||||
placeholder="Vault (vide = tous)"
|
||||
class="config-input"
|
||||
style="width: 130px"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
id="webhook-prefix-input"
|
||||
data-i18n-placeholder="config.webhook_prefix_placeholder"
|
||||
placeholder="Préfixe (vide = tout)"
|
||||
class="config-input"
|
||||
style="width: 140px"
|
||||
/>
|
||||
<button id="webhook-add-btn"
|
||||
class="config-btn config-btn-add" data-i18n="config.add_webhook">
|
||||
Ajouter
|
||||
@@ -5114,9 +5142,11 @@
|
||||
<p>
|
||||
<em
|
||||
>Note : l'événement
|
||||
<code>file_modified</code> est réservé pour
|
||||
usage futur. Les modifications de contenu
|
||||
via l'éditeur ne déclenchent pas encore de
|
||||
<code>file_modified</code> est émis par
|
||||
la sauvegarde texte
|
||||
(<code>PUT /api/file/{vault}/save</code>).
|
||||
Les autres écritures (tableur, IA) ne
|
||||
déclenchent pas encore de
|
||||
webhook.</em
|
||||
>
|
||||
</p>
|
||||
@@ -5289,13 +5319,15 @@ curl -X POST https://votre-serveur.com/webhook \
|
||||
REST sont prises en compte
|
||||
</li>
|
||||
<li>
|
||||
<code>file_modified</code> n'est pas encore
|
||||
dispatché (réservé pour version future)
|
||||
<code>file_modified</code> est émis par la
|
||||
sauvegarde texte
|
||||
(<code>PUT /api/file/{vault}/save</code>)
|
||||
</li>
|
||||
<li data-i18n="auto.1d3330cc">Pas de retry automatique en cas d'échec</li>
|
||||
<li data-i18n="auto.4dfa6a25">
|
||||
Pas de filtrage par vault — un webhook
|
||||
écoute toutes les vaults
|
||||
Filtrage optionnel par vault et préfixe de
|
||||
chemin — sans filtre, un webhook écoute
|
||||
toutes les vaults
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
+33
-5
@@ -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) {
|
||||
|
||||
@@ -248,7 +248,7 @@
|
||||
"auto.4bc1f00a": "Use operators to refine your",
|
||||
"auto.4c908ade": "Click a vault to open it and see its",
|
||||
"auto.4d281c6c": "AI: text processed ✓",
|
||||
"auto.4dfa6a25": "No vault filtering — a webhook listens to",
|
||||
"auto.4dfa6a25": "Optional vault and path-prefix filtering — without a filter, a webhook listens to all vaults",
|
||||
"auto.4fa5d7c2": "Quick editing",
|
||||
"auto.559ef1da": "Recommended workflow",
|
||||
"auto.598a0ca7": "Tracked events",
|
||||
@@ -651,6 +651,10 @@
|
||||
"config.token_revoke_confirm": "Revoke key",
|
||||
"config.token_revoked_toast": "Key revoked (immediate effect on API + MCP)",
|
||||
"config.token_created_toast": "Key created",
|
||||
"config.token_scope_full": "Full access",
|
||||
"config.token_scope_files": "Files only",
|
||||
"config.token_vaults_placeholder": "Vaults (empty = all, e.g. TikTak)",
|
||||
"config.token_vaults_all": "all account vaults",
|
||||
"config.url_required": "URL required",
|
||||
"config.watcher_debounce_label": "Debounce (s)",
|
||||
"config.watcher_enabled_label": "Enable watcher",
|
||||
@@ -658,6 +662,8 @@
|
||||
"config.watcher_polling_label": "Polling mode (fallback)",
|
||||
"config.webhook_name_placeholder": "Name",
|
||||
"config.webhook_url_placeholder": "URL",
|
||||
"config.webhook_vault_placeholder": "Vault (empty = all)",
|
||||
"config.webhook_prefix_placeholder": "Prefix (empty = all)",
|
||||
"config.webhooks": "Webhooks",
|
||||
"config.webhooks_desc": "HTTP notifications to external services on file changes.",
|
||||
"config.plugins": "Plugins",
|
||||
@@ -969,7 +975,7 @@
|
||||
"help.desc_31e7253b": "Each backup",
|
||||
"help.desc_32e3d66d": "En cas d'erreur de clé API : AI: clé API invalide",
|
||||
"help.desc_39979e71": "Vault",
|
||||
"help.desc_39e97b2f": "Note: the <code>file_modified</code> event is reserved for future use and is not currently emitted.",
|
||||
"help.desc_39e97b2f": "Note: the <code>file_modified</code> event is emitted by text saves (PUT /api/file/{vault}/save). Other writers do not trigger webhooks yet.",
|
||||
"help.desc_3ace259e": "— Inline completion",
|
||||
"help.desc_3c4711b8": "Edit config.yaml to customize behavior",
|
||||
"help.desc_3d4e9189": "Simplifies the language for easier reading — replaces complex terms and shortens long sentences.",
|
||||
@@ -1099,7 +1105,7 @@
|
||||
"help.desc_ctrl_tab": ": Next tab",
|
||||
"help.desc_ctrl_w": ": Close the active tab",
|
||||
"help.desc_d186a19e": ": The Recent tab in the sidebar lists your latest files",
|
||||
"help.desc_d3a71d97": "file_modified n'est pas encore dispatché (réservé pour version future)",
|
||||
"help.desc_d3a71d97": "file_modified is emitted by text saves (PUT /api/file/{vault}/save)",
|
||||
"help.desc_d5c977ac": "Code blocks",
|
||||
"help.desc_d646b709": "Compte admin : Créé automatiquement au premier démarrage",
|
||||
"help.desc_d66e911c": "In the",
|
||||
|
||||
@@ -248,7 +248,7 @@
|
||||
"auto.4bc1f00a": "Utilisez des opérateurs pour affiner votre\n recherche :",
|
||||
"auto.4c908ade": "Cliquez sur un vault pour l'ouvrir et voir son\n contenu. Les dossiers affichent un chevron et un\n badge avec le nombre d'éléments.",
|
||||
"auto.4d281c6c": "AI: texte traité ✓",
|
||||
"auto.4dfa6a25": "Pas de filtrage par vault — un webhook\n écoute toutes les vaults",
|
||||
"auto.4dfa6a25": "Filtrage optionnel par vault et préfixe de chemin — un webhook\n écoute toutes les vaults",
|
||||
"auto.4fa5d7c2": "Édition rapide",
|
||||
"auto.559ef1da": "Workflow recommandé",
|
||||
"auto.598a0ca7": "Événements tracés",
|
||||
@@ -651,6 +651,10 @@
|
||||
"config.token_revoke_confirm": "Révoquer la clé",
|
||||
"config.token_revoked_toast": "Clé révoquée (effet immédiat API + MCP)",
|
||||
"config.token_created_toast": "Clé créée",
|
||||
"config.token_scope_full": "Accès complet",
|
||||
"config.token_scope_files": "Fichiers seuls",
|
||||
"config.token_vaults_placeholder": "Vaults (vide = tous, ex : TikTak)",
|
||||
"config.token_vaults_all": "tous les vaults du compte",
|
||||
"config.url_required": "URL requise",
|
||||
"config.watcher_debounce_label": "Debounce (s)",
|
||||
"config.watcher_enabled_label": "Activer la surveillance",
|
||||
@@ -658,6 +662,8 @@
|
||||
"config.watcher_polling_label": "Mode polling (fallback)",
|
||||
"config.webhook_name_placeholder": "Nom",
|
||||
"config.webhook_url_placeholder": "URL",
|
||||
"config.webhook_vault_placeholder": "Vault (vide = tous)",
|
||||
"config.webhook_prefix_placeholder": "Préfixe (vide = tout)",
|
||||
"config.webhooks": "Webhooks",
|
||||
"config.webhooks_desc": "Notifications HTTP vers des services externes lors des changements de fichiers.",
|
||||
"config.plugins": "Plugins",
|
||||
@@ -969,7 +975,7 @@
|
||||
"help.desc_31e7253b": ": Chaque backup\n est horodaté",
|
||||
"help.desc_32e3d66d": "En cas d'erreur de clé API :\n AI: clé API invalide",
|
||||
"help.desc_39979e71": ":\n Choisissez un vault pour réduire le bruit",
|
||||
"help.desc_39e97b2f": "Note : l'événement\n file_modified est réservé pour\n usage futur. Les modifications de contenu\n via l'éditeur ne déclenchent pas encore de\n webhook.",
|
||||
"help.desc_39e97b2f": "Note : l'événement\n file_modified est émis par la sauvegarde texte\n (PUT /api/file/{vault}/save). Les autres écritures\n ne déclenchent pas encore de webhook.",
|
||||
"help.desc_3ace259e": "— Complétion inline\n (",
|
||||
"help.desc_3c4711b8": "Événements tracés :\n Écritures, suppressions, changements de\n configuration",
|
||||
"help.desc_3d4e9189": "📋\n Simplify language :\n Simplifie le langage, évite le\n jargon",
|
||||
@@ -1099,7 +1105,7 @@
|
||||
"help.desc_ctrl_tab": ": Onglet suivant",
|
||||
"help.desc_ctrl_w": ": Fermer l'onglet actif",
|
||||
"help.desc_d186a19e": ":\n L'onglet \"Récent\" dans la sidebar liste vos\n derniers fichiers",
|
||||
"help.desc_d3a71d97": "file_modified n'est pas encore\n dispatché (réservé pour version future)",
|
||||
"help.desc_d3a71d97": "file_modified est émis par\n la sauvegarde texte (PUT /api/file/{vault}/save)",
|
||||
"help.desc_d5c977ac": ": Les blocs de\n code s'adaptent au thème",
|
||||
"help.desc_d646b709": "Compte admin : Créé\n automatiquement au premier démarrage",
|
||||
"help.desc_d66e911c": ": Dans les\n actions du fichier, génère un lien unique",
|
||||
@@ -1138,7 +1144,7 @@
|
||||
"help.desc_fa126609": "🔷\n Convert to canvas :\n Convertit en diagramme Mermaid.js",
|
||||
"help.desc_facet_vaults": ": Compteurs par vault",
|
||||
"help.desc_facet_extensions": ": Compteurs par extension de fichier dans les résultats",
|
||||
"help.desc_fc9391f3": "# Exemple avec curl + jq dans un script bash\nWEBHOOK_URL=\"https://discord.com/api/webhooks/...\"\nOBSIGATE_SECRET=\"mon-secret\"\n\n# Lire le payload depuis stdin\nread -r payload\nevent=$(echo \"$payload\" | jq -r '.event')\npath=$(echo \"$payload\" | jq -r '.data.path')\n\ncurl -H \"Content-Type: application/json\" -d \"{\n \\\"content\\\": \\\"📝 **$event** — $path\\\"\n}\" \"$WEBHOOK_URL\"\n\n Tester avec curl\n # Simuler ce qu'ObsiGate envoie (pour tester votre endpoint)\ncurl -X POST https://votre-serveur.com/webhook \\\n -H \"Content-Type: application/json\" \\\n -H \"X-ObsiGate-Event: file_created\" \\\n -H \"X-ObsiGate-Signature: sha256=$(echo -n '{\"event\":\"file_created\",...}' | openssl dgst -sha256 -hmac 'mon-secret' | cut -d' ' -f2)\" \\\n -d '{\"event\":\"file_created\",\"timestamp\":\"2026-06-03T14:30:00Z\",\"data\":{\"vault\":\"IT\",\"path\":\"notes/test.md\"}}'\n\n Limitations actuelles\n \n \n Timeout de 5 secondes par\n requête — les webhooks sont \"fire and\n forget\" (pas de retry)\n \n \n Les modifications détectées par le watcher\n filesystem (watchdog)\n ne déclenchent pas de\n webhooks — seules les opérations via l'API\n REST sont prises en compte\n \n \n file_modified n'est pas encore\n dispatché (réservé pour version future)\n \n Pas de retry automatique en cas d'échec\n \n Pas de filtrage par vault — un webhook\n écoute toutes les vaults\n \n \n \n \n 💾 Sauvegardes et Audits\n\n Backup automatique\n \n ObsiGate crée automatiquement des sauvegardes\n avant chaque modification destructive.",
|
||||
"help.desc_fc9391f3": "# Exemple avec curl + jq dans un script bash\nWEBHOOK_URL=\"https://discord.com/api/webhooks/...\"\nOBSIGATE_SECRET=\"mon-secret\"\n\n# Lire le payload depuis stdin\nread -r payload\nevent=$(echo \"$payload\" | jq -r '.event')\npath=$(echo \"$payload\" | jq -r '.data.path')\n\ncurl -H \"Content-Type: application/json\" -d \"{\n \\\"content\\\": \\\"📝 **$event** — $path\\\"\n}\" \"$WEBHOOK_URL\"\n\n Tester avec curl\n # Simuler ce qu'ObsiGate envoie (pour tester votre endpoint)\ncurl -X POST https://votre-serveur.com/webhook \\\n -H \"Content-Type: application/json\" \\\n -H \"X-ObsiGate-Event: file_created\" \\\n -H \"X-ObsiGate-Signature: sha256=$(echo -n '{\"event\":\"file_created\",...}' | openssl dgst -sha256 -hmac 'mon-secret' | cut -d' ' -f2)\" \\\n -d '{\"event\":\"file_created\",\"timestamp\":\"2026-06-03T14:30:00Z\",\"data\":{\"vault\":\"IT\",\"path\":\"notes/test.md\"}}'\n\n Limitations actuelles\n \n \n Timeout de 5 secondes par\n requête — les webhooks sont \"fire and\n forget\" (pas de retry)\n \n \n Les modifications détectées par le watcher\n filesystem (watchdog)\n ne déclenchent pas de\n webhooks — seules les opérations via l'API\n REST sont prises en compte\n \n \n file_modified est émis par\n la sauvegarde texte (PUT /api/file/{vault}/save)\n \n Pas de retry automatique en cas d'échec\n \n Filtrage optionnel par vault et préfixe\n de chemin — sans filtre, un webhook écoute\n toutes les vaults\n \n \n \n \n 💾 Sauvegardes et Audits\n\n Backup automatique\n \n ObsiGate crée automatiquement des sauvegardes\n avant chaque modification destructive.",
|
||||
"help.desc_fdeaf803": ": Chaque type de\n fichier a son icône (markdown, code, etc.)",
|
||||
"help.desc_file_suggest": ": Suggestions de titres de fichiers",
|
||||
"help.desc_filter_type": ":\n Inclut/exclut fichiers, dossiers, markdown,\n autres",
|
||||
|
||||
Reference in New Issue
Block a user