Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ee27bb1ab | ||
|
|
e77fcb326d |
No files matched your search
+8
-6
@@ -401,7 +401,7 @@ L'accès passe par un **jeton d'API**, créé dans *Paramètres > API* par un ad
|
|||||||
envoyé dans `Authorization: Bearer lst_…`. Un jeton est en lecture seule et n'ouvre que
|
envoyé dans `Authorization: Bearer lst_…`. Un jeton est en lecture seule et n'ouvre que
|
||||||
`/api/v1/` ; il fonctionne quel que soit le mode d'[authentification](#authentification)
|
`/api/v1/` ; il fonctionne quel que soit le mode d'[authentification](#authentification)
|
||||||
(`local`, `oidc` ou aucune). Il n'est affiché qu'une fois, à sa création : LogStream n'en garde
|
(`local`, `oidc` ou aucune). Il n'est affiché qu'une fois, à sa création : LogStream n'en garde
|
||||||
que l'empreinte SHA-256, dans `/data/api_tokens.json`. Le révoquer coupe l'accès immédiatement ;
|
que l'empreinte SHA-256, dans `/data/api_tokens.json`. Le révoquer (puis Valider) coupe l'accès immédiatement ;
|
||||||
la liste indique quand et d'où chaque jeton a servi pour la dernière fois. Une session ouverte
|
la liste indique quand et d'où chaque jeton a servi pour la dernière fois. Une session ouverte
|
||||||
dans le navigateur peut aussi appeler la route.
|
dans le navigateur peut aussi appeler la route.
|
||||||
|
|
||||||
@@ -470,8 +470,8 @@ s'affichent tout de suite mais ne sont conservées qu'avec le bouton **Valider**
|
|||||||
des onglets : il prend la couleur d'accent tant que des modifications sont en attente. **Annuler**
|
des onglets : il prend la couleur d'accent tant que des modifications sont en attente. **Annuler**
|
||||||
revient aux paramètres enregistrés, et fermer la fenêtre avec des modifications non validées
|
revient aux paramètres enregistrés, et fermer la fenêtre avec des modifications non validées
|
||||||
demande confirmation avant de les abandonner. Localisation et Affichage sont mémorisés par
|
demande confirmation avant de les abandonner. Localisation et Affichage sont mémorisés par
|
||||||
navigateur ; les tags de couleur et les sources de logs sont enregistrés sur le serveur. La purge,
|
navigateur ; les tags de couleur et les sources de logs sont enregistrés sur le serveur. La purge
|
||||||
les jetons d'API et la réinitialisation des colonnes agissent immédiatement.
|
et la réinitialisation des colonnes agissent immédiatement.
|
||||||
|
|
||||||
- **Localisation et langue**
|
- **Localisation et langue**
|
||||||
- *Langue* : anglais ou français.
|
- *Langue* : anglais ou français.
|
||||||
@@ -525,9 +525,11 @@ les jetons d'API et la réinitialisation des colonnes agissent immédiatement.
|
|||||||
désactivée par défaut : mettez `ALLOW_PURGE=true` pour l'autoriser. Tout administrateur peut
|
désactivée par défaut : mettez `ALLOW_PURGE=true` pour l'autoriser. Tout administrateur peut
|
||||||
alors purger : activez l'[authentification](#authentification) si l'interface est accessible
|
alors purger : activez l'[authentification](#authentification) si l'interface est accessible
|
||||||
à d'autres.
|
à d'autres.
|
||||||
- **API** : jetons de l'[API de lecture](#api-de-lecture-n8n-scripts) : en créer un avec un nom
|
- **API** : jetons de l'[API de lecture](#api-de-lecture-n8n-scripts) : en ajouter un avec un
|
||||||
(affiché une seule fois, à copier tout de suite), voir quand et d'où chacun a servi pour la
|
nom, en révoquer un, voir quand et d'où chacun a servi pour la dernière fois. Ajouts et
|
||||||
dernière fois, le révoquer. Réservé aux administrateurs.
|
révocations attendent **Valider** comme les autres réglages (**Annuler** les abandonne). Après
|
||||||
|
Valider, chaque nouveau jeton est affiché une seule fois avec un bouton Copier, et l'exemple
|
||||||
|
curl en dessous l'utilise, surligné, avec son propre bouton Copier. Réservé aux administrateurs.
|
||||||
|
|
||||||
## Authentification
|
## Authentification
|
||||||
|
|
||||||
|
|||||||
@@ -364,7 +364,7 @@ Access goes through an **API token**, created in *Settings > API* by an administ
|
|||||||
as `Authorization: Bearer lst_…`. A token is read-only and only opens `/api/v1/`; it works
|
as `Authorization: Bearer lst_…`. A token is read-only and only opens `/api/v1/`; it works
|
||||||
whatever the [authentication](#authentication) mode (`local`, `oidc`, or none). It is shown once,
|
whatever the [authentication](#authentication) mode (`local`, `oidc`, or none). It is shown once,
|
||||||
when it is created: LogStream only keeps its SHA-256 hash, in `/data/api_tokens.json`.
|
when it is created: LogStream only keeps its SHA-256 hash, in `/data/api_tokens.json`.
|
||||||
Revoking it cuts off access at once; the list shows when and from where each token was last
|
Revoking it (then Save) cuts off access at once; the list shows when and from where each token was last
|
||||||
used. A logged-in browser session can call the route too.
|
used. A logged-in browser session can call the route too.
|
||||||
|
|
||||||
| Parameter | Meaning |
|
| Parameter | Meaning |
|
||||||
@@ -429,7 +429,7 @@ The gear icon opens the settings, organized in tabs. Changes show at once but ar
|
|||||||
with the **Save** button, under the tab labels: it takes the accent color while changes are
|
with the **Save** button, under the tab labels: it takes the accent color while changes are
|
||||||
waiting. **Cancel** goes back to the saved settings, and closing the window with unsaved changes
|
waiting. **Cancel** goes back to the saved settings, and closing the window with unsaved changes
|
||||||
asks before dropping them. Localization and Appearance are remembered per browser; color tags
|
asks before dropping them. Localization and Appearance are remembered per browser; color tags
|
||||||
and log sources are saved on the server. Purging, API tokens and resetting column widths act at once.
|
and log sources are saved on the server. Purging and resetting column widths act at once.
|
||||||
|
|
||||||
- **Localization**
|
- **Localization**
|
||||||
- *Language*: English or French.
|
- *Language*: English or French.
|
||||||
@@ -477,9 +477,11 @@ and log sources are saved on the server. Purging, API tokens and resetting colum
|
|||||||
(already set in `docker-compose.yml`). The feature is off by default: set `ALLOW_PURGE=true`
|
(already set in `docker-compose.yml`). The feature is off by default: set `ALLOW_PURGE=true`
|
||||||
to allow it. Any admin can then purge: turn on [authentication](#authentication) if the UI
|
to allow it. Any admin can then purge: turn on [authentication](#authentication) if the UI
|
||||||
is reachable by others.
|
is reachable by others.
|
||||||
- **API**: API tokens for the [reading API](#reading-api-n8n-scripts): create one with a name
|
- **API**: API tokens for the [reading API](#reading-api-n8n-scripts): add one with a name,
|
||||||
(shown once, copy it then), see when and from where each was last used, revoke it. Reserved
|
revoke one, see when and from where each was last used. Additions and revocations wait for
|
||||||
to administrators.
|
**Save** like the other settings (**Cancel** drops them). After Save, each new token is shown
|
||||||
|
once with a copy button, and the curl example below uses it, highlighted, with its own copy
|
||||||
|
button. Reserved to administrators.
|
||||||
|
|
||||||
## Authentication
|
## Authentication
|
||||||
|
|
||||||
|
|||||||
+105
-43
@@ -176,21 +176,23 @@ const I18N = {
|
|||||||
err_export_pipes: 'Export does not support LogsQL pipes (| …): keep only the filter part',
|
err_export_pipes: 'Export does not support LogsQL pipes (| …): keep only the filter part',
|
||||||
tabApi: 'API',
|
tabApi: 'API',
|
||||||
tokensTitle: 'API tokens',
|
tokensTitle: 'API tokens',
|
||||||
tokensHelp: 'Tokens give scripts and automation tools (n8n…) read-only access to /api/v1/logs, whatever the authentication mode. Only a hash is kept: a token is shown once, when it is created. Revoking it cuts off access at once.',
|
tokensHelp: 'Tokens give scripts and automation tools (n8n…) read-only access to /api/v1/logs, whatever the authentication mode. Additions and revocations apply with Save. Only a hash is kept: a new token is shown once, right after Save.',
|
||||||
tokenCount: (n) => `${n} token${n > 1 ? 's' : ''}`,
|
tokenCount: (n) => `${n} token${n > 1 ? 's' : ''}`,
|
||||||
tokenNone: 'No API token yet.',
|
tokenNone: 'No API token yet.',
|
||||||
tokenNamePh: 'Token name (n8n, backup script…)',
|
tokenNamePh: 'Token name (n8n, backup script…)',
|
||||||
tokenCreate: 'Create a token',
|
tokenCreate: 'Add a token',
|
||||||
tokenOnce: 'Copy this token now: it will not be shown again.',
|
tokenOnce: 'Copy this token now: it will not be shown again.',
|
||||||
tokenCopy: 'Copy',
|
tokenCopy: 'Copy',
|
||||||
tokenRevoke: 'Revoke',
|
tokenRevoke: 'Revoke',
|
||||||
tokenRevokeAsk: (n) => `Revoke the token "${n}"? Tools using it lose access at once.`,
|
tokenUndo: 'Undo',
|
||||||
tokenRevoked: 'Token revoked',
|
tokenCreatePending: 'created on Save',
|
||||||
|
tokenRevokePending: 'revoked on Save',
|
||||||
|
tokenExampleCopy: 'Copy the command',
|
||||||
tokenCreated: (d) => `created ${d}`,
|
tokenCreated: (d) => `created ${d}`,
|
||||||
tokenUsed: (p) => `last used ${p.d}${p.ip ? ' from ' + p.ip : ''}`,
|
tokenUsed: (p) => `last used ${p.d}${p.ip ? ' from ' + p.ip : ''}`,
|
||||||
tokenNever: 'never used',
|
tokenNever: 'never used',
|
||||||
tokensUse: 'Usage',
|
tokensUse: 'Usage',
|
||||||
tokensUseHelp: 'Send the token in an Authorization header. Each answer has next_cursor: pass it as since on the next call to get the following logs, without gaps or duplicates (see the README for an n8n workflow).',
|
tokensUseHelp: 'Send the token in an Authorization header (highlighted below). Each answer has next_cursor: pass it as since on the next call to get the following logs, without gaps or duplicates (see the README for an n8n workflow).',
|
||||||
err_token_name: 'Give the token a name of 1 to 80 characters',
|
err_token_name: 'Give the token a name of 1 to 80 characters',
|
||||||
err_token_max: 'Too many API tokens',
|
err_token_max: 'Too many API tokens',
|
||||||
tabLocale: 'Localization', tabFilters: 'Patterns', tabInterface: 'Appearance', tabData: 'Database',
|
tabLocale: 'Localization', tabFilters: 'Patterns', tabInterface: 'Appearance', tabData: 'Database',
|
||||||
@@ -427,21 +429,23 @@ const I18N = {
|
|||||||
err_export_pipes: 'L\'export ne gère pas les pipes LogsQL (| …) : gardez seulement la partie filtre',
|
err_export_pipes: 'L\'export ne gère pas les pipes LogsQL (| …) : gardez seulement la partie filtre',
|
||||||
tabApi: 'API',
|
tabApi: 'API',
|
||||||
tokensTitle: "Jetons d'API",
|
tokensTitle: "Jetons d'API",
|
||||||
tokensHelp: "Les jetons donnent aux scripts et outils d'automatisation (n8n…) un accès en lecture seule à /api/v1/logs, quel que soit le mode d'authentification. Seule une empreinte est conservée : un jeton n'est affiché qu'une fois, à sa création. Le révoquer coupe l'accès immédiatement.",
|
tokensHelp: "Les jetons donnent aux scripts et outils d'automatisation (n8n…) un accès en lecture seule à /api/v1/logs, quel que soit le mode d'authentification. Ajouts et révocations s'appliquent avec Valider. Seule une empreinte est conservée : un nouveau jeton n'est affiché qu'une fois, juste après Valider.",
|
||||||
tokenCount: (n) => `${n} jeton${n > 1 ? 's' : ''}`,
|
tokenCount: (n) => `${n} jeton${n > 1 ? 's' : ''}`,
|
||||||
tokenNone: "Aucun jeton d'API pour l'instant.",
|
tokenNone: "Aucun jeton d'API pour l'instant.",
|
||||||
tokenNamePh: 'Nom du jeton (n8n, script de sauvegarde…)',
|
tokenNamePh: 'Nom du jeton (n8n, script de sauvegarde…)',
|
||||||
tokenCreate: 'Créer un jeton',
|
tokenCreate: 'Ajouter un jeton',
|
||||||
tokenOnce: "Copiez ce jeton maintenant : il ne sera plus affiché.",
|
tokenOnce: "Copiez ce jeton maintenant : il ne sera plus affiché.",
|
||||||
tokenCopy: 'Copier',
|
tokenCopy: 'Copier',
|
||||||
tokenRevoke: 'Révoquer',
|
tokenRevoke: 'Révoquer',
|
||||||
tokenRevokeAsk: (n) => `Révoquer le jeton « ${n} » ? Les outils qui l'utilisent perdent l'accès immédiatement.`,
|
tokenUndo: 'Annuler',
|
||||||
tokenRevoked: 'Jeton révoqué',
|
tokenCreatePending: 'créé en validant',
|
||||||
|
tokenRevokePending: 'révoqué en validant',
|
||||||
|
tokenExampleCopy: 'Copier la commande',
|
||||||
tokenCreated: (d) => `créé le ${d}`,
|
tokenCreated: (d) => `créé le ${d}`,
|
||||||
tokenUsed: (p) => `dernière utilisation ${p.d}${p.ip ? ' depuis ' + p.ip : ''}`,
|
tokenUsed: (p) => `dernière utilisation ${p.d}${p.ip ? ' depuis ' + p.ip : ''}`,
|
||||||
tokenNever: 'jamais utilisé',
|
tokenNever: 'jamais utilisé',
|
||||||
tokensUse: 'Utilisation',
|
tokensUse: 'Utilisation',
|
||||||
tokensUseHelp: "Envoyez le jeton dans un en-tête Authorization. Chaque réponse contient next_cursor : passez-le dans since à l'appel suivant pour obtenir les logs qui suivent, sans trou ni doublon (voir le README pour un workflow n8n).",
|
tokensUseHelp: "Envoyez le jeton dans un en-tête Authorization (surligné ci-dessous). Chaque réponse contient next_cursor : passez-le dans since à l'appel suivant pour obtenir les logs qui suivent, sans trou ni doublon (voir le README pour un workflow n8n).",
|
||||||
err_token_name: 'Donnez au jeton un nom de 1 à 80 caractères',
|
err_token_name: 'Donnez au jeton un nom de 1 à 80 caractères',
|
||||||
err_token_max: "Trop de jetons d'API",
|
err_token_max: "Trop de jetons d'API",
|
||||||
tabLocale: 'Localisation et langue', tabFilters: 'Motifs de recherche', tabInterface: 'Affichage', tabData: 'Base de données',
|
tabLocale: 'Localisation et langue', tabFilters: 'Motifs de recherche', tabInterface: 'Affichage', tabData: 'Base de données',
|
||||||
@@ -2704,31 +2708,62 @@ $('#purgeBtn').addEventListener('click', async () => {
|
|||||||
|
|
||||||
/* ================= API tokens ================= */
|
/* ================= API tokens ================= */
|
||||||
|
|
||||||
const tokens = { list: null, error: null };
|
// Creations and revocations wait for Save like the other settings
|
||||||
|
// (settings.pending.tokens = { create: [names], revoke: [ids] }); a new token
|
||||||
|
// is shown once, after Save, with a curl example using it.
|
||||||
|
const tokens = { list: null, error: null, fresh: [] };
|
||||||
|
|
||||||
|
const pendingTokens = () => settings.pending.tokens || { create: [], revoke: [] };
|
||||||
|
function pendTokens(p) {
|
||||||
|
if (p.create.length || p.revoke.length) settings.pending.tokens = p;
|
||||||
|
else delete settings.pending.tokens;
|
||||||
|
renderTokens();
|
||||||
|
renderSaveState();
|
||||||
|
}
|
||||||
|
|
||||||
|
function curlExample(token) {
|
||||||
|
const url = `${location.origin}/api/v1/logs?query=severity:err&since=15m&limit=1000`;
|
||||||
|
return {
|
||||||
|
text: `curl -H "Authorization: Bearer ${token}" \\\n "${url}"`,
|
||||||
|
html: `curl -H "Authorization: Bearer <mark>${esc(token)}</mark>" \\\n "${esc(url)}"`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function renderTokens() {
|
function renderTokens() {
|
||||||
const el = $('#tokenList');
|
const el = $('#tokenList');
|
||||||
const day = (x) => fmtFull(x).replace(/[.,]\d{3}(?=\D*$)/, '');
|
const day = (x) => fmtFull(x).replace(/[.,]\d{3}(?=\D*$)/, '');
|
||||||
|
const p = pendingTokens();
|
||||||
$('#tokenCount').textContent = tokens.list ? t('tokenCount', tokens.list.length) : '';
|
$('#tokenCount').textContent = tokens.list ? t('tokenCount', tokens.list.length) : '';
|
||||||
$('#tokenExample').textContent = `curl -H "Authorization: Bearer lst_…" \\\n "${location.origin}/api/v1/logs?query=severity:err&since=15m&limit=1000"`;
|
|
||||||
|
// Tokens shown once after Save, and the curl example with the last one.
|
||||||
|
const fresh = tokens.fresh;
|
||||||
|
$('#tokenNew').hidden = !fresh.length;
|
||||||
|
$('#tokenNew').innerHTML = fresh.length ? `<p class="small">${esc(t('tokenOnce'))}</p>` + fresh.map((f, i) =>
|
||||||
|
`<div class="token-secret"><span class="token-label">${esc(f.name)}</span><code>${esc(f.token)}</code>`
|
||||||
|
+ `<button class="btn" type="button" data-copy-token="${i}">${esc(t('tokenCopy'))}</button></div>`).join('') : '';
|
||||||
|
$('#tokenExample').innerHTML = curlExample(fresh.length ? fresh[fresh.length - 1].token : 'lst_…').html;
|
||||||
|
|
||||||
if (!tokens.list) {
|
if (!tokens.list) {
|
||||||
el.innerHTML = tokens.error ? `<div class="token-empty bad">${esc(tokens.error)}</div>` : '';
|
el.innerHTML = tokens.error ? `<div class="token-empty bad">${esc(tokens.error)}</div>` : '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!tokens.list.length) {
|
let html = tokens.list.map((k) => {
|
||||||
el.innerHTML = `<div class="token-empty">${esc(t('tokenNone'))}</div>`;
|
const gone = p.revoke.includes(k.id);
|
||||||
return;
|
|
||||||
}
|
|
||||||
el.innerHTML = tokens.list.map((k) => {
|
|
||||||
const used = k.last_used ? t('tokenUsed', { d: day(k.last_used), ip: k.last_ip }) : t('tokenNever');
|
const used = k.last_used ? t('tokenUsed', { d: day(k.last_used), ip: k.last_ip }) : t('tokenNever');
|
||||||
return `<div class="token-row"><div><b>${esc(k.name)}</b> <code>${esc(k.hint)}…</code></div>`
|
const info = gone ? t('tokenRevokePending') : `${t('tokenCreated', day(k.created))} · ${used}`;
|
||||||
+ `<button class="btn danger" type="button" data-revoke="${esc(k.id)}">${esc(t('tokenRevoke'))}</button>`
|
return `<div class="token-row${gone ? ' revoked' : ''}"><div><b>${esc(k.name)}</b> <code>${esc(k.hint)}…</code></div>`
|
||||||
+ `<div class="muted">${esc(t('tokenCreated', day(k.created)))} · ${esc(used)}</div></div>`;
|
+ (gone
|
||||||
|
? `<button class="btn" type="button" data-unrevoke="${esc(k.id)}">${esc(t('tokenUndo'))}</button>`
|
||||||
|
: `<button class="btn danger" type="button" data-revoke="${esc(k.id)}">${esc(t('tokenRevoke'))}</button>`)
|
||||||
|
+ `<div class="muted">${esc(info)}</div></div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
html += p.create.map((name, i) => `<div class="token-row pending"><div><b>${esc(name)}</b></div>`
|
||||||
|
+ `<button class="btn" type="button" data-uncreate="${i}">${esc(t('tokenUndo'))}</button>`
|
||||||
|
+ `<div class="muted">${esc(t('tokenCreatePending'))}</div></div>`).join('');
|
||||||
|
el.innerHTML = html || `<div class="token-empty">${esc(t('tokenNone'))}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadTokens() {
|
async function loadTokens() {
|
||||||
$('#tokenNew').hidden = true;
|
|
||||||
try {
|
try {
|
||||||
tokens.list = await api('/api/tokens');
|
tokens.list = await api('/api/tokens');
|
||||||
tokens.error = null;
|
tokens.error = null;
|
||||||
@@ -2739,35 +2774,59 @@ async function loadTokens() {
|
|||||||
renderTokens();
|
renderTokens();
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#tokenForm').addEventListener('submit', async (ev) => {
|
// Save: revocations first, then creations; the new tokens are shown once.
|
||||||
|
// What failed stays pending, so Save can try it again.
|
||||||
|
async function saveTokens() {
|
||||||
|
const p = pendingTokens();
|
||||||
|
const errors = [];
|
||||||
|
const left = { create: [], revoke: [] };
|
||||||
|
for (const id of p.revoke) {
|
||||||
|
try {
|
||||||
|
await api('/api/tokens/' + encodeURIComponent(id), { method: 'DELETE' });
|
||||||
|
} catch (e) {
|
||||||
|
if (!/not found/.test(e.message)) { errors.push(e.message); left.revoke.push(id); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const fresh = [];
|
||||||
|
for (const name of p.create) {
|
||||||
|
try {
|
||||||
|
const res = await api('/api/tokens', { method: 'POST', body: { name } });
|
||||||
|
fresh.push({ name: res.name, token: res.token });
|
||||||
|
} catch (e) { errors.push(e.message); left.create.push(name); }
|
||||||
|
}
|
||||||
|
tokens.fresh = fresh;
|
||||||
|
if (errors.length) settings.pending.tokens = left;
|
||||||
|
else delete settings.pending.tokens;
|
||||||
|
await loadTokens();
|
||||||
|
if (errors.length) throw new Error(errors.join(' · '));
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#tokenForm').addEventListener('submit', (ev) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
const name = $('#tokenName').value.trim();
|
const name = $('#tokenName').value.trim();
|
||||||
if (!name) { $('#tokenName').focus(); return; }
|
if (!name) { $('#tokenName').focus(); return; }
|
||||||
try {
|
const p = pendingTokens();
|
||||||
const res = await api('/api/tokens', { method: 'POST', body: { name } });
|
pendTokens({ create: [...p.create, name], revoke: p.revoke });
|
||||||
$('#tokenName').value = '';
|
$('#tokenName').value = '';
|
||||||
await loadTokens();
|
|
||||||
$('#tokenSecret').textContent = res.token;
|
|
||||||
$('#tokenNew').hidden = false;
|
|
||||||
} catch (e) {
|
|
||||||
toast(e.message);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#tokenCopy').addEventListener('click', () => copy($('#tokenSecret').textContent));
|
$('#tokenNew').addEventListener('click', (ev) => {
|
||||||
|
const b = ev.target.closest('[data-copy-token]');
|
||||||
|
if (b) copy(tokens.fresh[b.dataset.copyToken].token);
|
||||||
|
});
|
||||||
|
|
||||||
$('#tokenList').addEventListener('click', async (ev) => {
|
$('#tokenExampleCopy').addEventListener('click', () => {
|
||||||
const b = ev.target.closest('[data-revoke]');
|
const fresh = tokens.fresh;
|
||||||
|
copy(curlExample(fresh.length ? fresh[fresh.length - 1].token : 'lst_…').text);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#tokenList').addEventListener('click', (ev) => {
|
||||||
|
const p = pendingTokens();
|
||||||
|
const b = ev.target.closest('[data-revoke], [data-unrevoke], [data-uncreate]');
|
||||||
if (!b) return;
|
if (!b) return;
|
||||||
const k = tokens.list?.find((x) => x.id === b.dataset.revoke);
|
if (b.dataset.revoke) pendTokens({ create: p.create, revoke: [...p.revoke, b.dataset.revoke] });
|
||||||
if (!k || !confirm(t('tokenRevokeAsk', k.name))) return;
|
else if (b.dataset.unrevoke) pendTokens({ create: p.create, revoke: p.revoke.filter((id) => id !== b.dataset.unrevoke) });
|
||||||
try {
|
else pendTokens({ create: p.create.filter((_, i) => i !== Number(b.dataset.uncreate)), revoke: p.revoke });
|
||||||
await api('/api/tokens/' + encodeURIComponent(k.id), { method: 'DELETE' });
|
|
||||||
toast(t('tokenRevoked'));
|
|
||||||
} catch (e) {
|
|
||||||
toast(e.message);
|
|
||||||
}
|
|
||||||
loadTokens();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ================= Tag management ================= */
|
/* ================= Tag management ================= */
|
||||||
@@ -2934,6 +2993,7 @@ $('#settingsBtn').addEventListener('click', () => {
|
|||||||
loadDocker();
|
loadDocker();
|
||||||
renderDbStats();
|
renderDbStats();
|
||||||
loadDbStats();
|
loadDbStats();
|
||||||
|
tokens.fresh = [];
|
||||||
loadTokens();
|
loadTokens();
|
||||||
showSettingsTab(store.get('settingsTab', 'locale'));
|
showSettingsTab(store.get('settingsTab', 'locale'));
|
||||||
$('#settingsDlg').showModal();
|
$('#settingsDlg').showModal();
|
||||||
@@ -2984,6 +3044,7 @@ const SOURCE_SAVE = {
|
|||||||
selflog: async () => { selfState = await api('/api/selflog', { method: 'PUT', body: { enabled: selfState.enabled, min_level: selfState.min_level || 'warning' } }); },
|
selflog: async () => { selfState = await api('/api/selflog', { method: 'PUT', body: { enabled: selfState.enabled, min_level: selfState.min_level || 'warning' } }); },
|
||||||
docker: async () => { dockerState = await api('/api/docker', { method: 'PUT', body: settings.pending.docker }); },
|
docker: async () => { dockerState = await api('/api/docker', { method: 'PUT', body: settings.pending.docker }); },
|
||||||
files: saveFiles,
|
files: saveFiles,
|
||||||
|
tokens: saveTokens,
|
||||||
};
|
};
|
||||||
|
|
||||||
async function saveSettings() {
|
async function saveSettings() {
|
||||||
@@ -3062,6 +3123,7 @@ function cancelSettings() {
|
|||||||
settings.orig = {};
|
settings.orig = {};
|
||||||
fileDrafts = [];
|
fileDrafts = [];
|
||||||
reloadSources();
|
reloadSources();
|
||||||
|
renderTokens();
|
||||||
renderSaveState();
|
renderSaveState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-9
@@ -462,20 +462,17 @@
|
|||||||
<div id="tokenList" class="token-list"></div>
|
<div id="tokenList" class="token-list"></div>
|
||||||
<form id="tokenForm" class="token-form">
|
<form id="tokenForm" class="token-form">
|
||||||
<input id="tokenName" type="text" class="dk-filter" maxlength="80" autocomplete="off" spellcheck="false" data-i18n-ph="tokenNamePh" data-i18n-aria="tokenNamePh">
|
<input id="tokenName" type="text" class="dk-filter" maxlength="80" autocomplete="off" spellcheck="false" data-i18n-ph="tokenNamePh" data-i18n-aria="tokenNamePh">
|
||||||
<button id="tokenCreate" class="btn" type="submit" data-i18n="tokenCreate">Create a token</button>
|
<button id="tokenCreate" class="btn" type="submit" data-i18n="tokenCreate">Add a token</button>
|
||||||
</form>
|
</form>
|
||||||
<div id="tokenNew" class="token-new" hidden>
|
<div id="tokenNew" class="token-new" hidden></div>
|
||||||
<p class="small" data-i18n="tokenOnce"></p>
|
|
||||||
<div class="token-secret">
|
|
||||||
<code id="tokenSecret"></code>
|
|
||||||
<button id="tokenCopy" class="btn" type="button" data-i18n="tokenCopy">Copy</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
<section class="set-section">
|
<section class="set-section">
|
||||||
<h3 data-i18n="tokensUse">Usage</h3>
|
<h3 data-i18n="tokensUse">Usage</h3>
|
||||||
<p class="muted small" data-i18n="tokensUseHelp"></p>
|
<p class="muted small" data-i18n="tokensUseHelp"></p>
|
||||||
<pre id="tokenExample" class="token-example"></pre>
|
<div class="token-example-box">
|
||||||
|
<pre id="tokenExample" class="token-example"></pre>
|
||||||
|
<button id="tokenExampleCopy" class="btn" type="button" data-i18n="tokenExampleCopy">Copy the command</button>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -928,6 +928,13 @@ body.login-page {
|
|||||||
.token-form .btn { height: 28px; font-size: 12px; }
|
.token-form .btn { height: 28px; font-size: 12px; }
|
||||||
.token-new { margin-top: 12px; padding: 10px 12px; border-radius: 8px; border: 1px solid var(--accent); background: var(--accent-soft); }
|
.token-new { margin-top: 12px; padding: 10px 12px; border-radius: 8px; border: 1px solid var(--accent); background: var(--accent-soft); }
|
||||||
.token-secret { display: flex; gap: 10px; align-items: center; margin-top: 8px; }
|
.token-secret { display: flex; gap: 10px; align-items: center; margin-top: 8px; }
|
||||||
|
.token-label { font-size: 12.5px; font-weight: 600; white-space: nowrap; }
|
||||||
|
.token-row.revoked b, .token-row.revoked code { text-decoration: line-through; opacity: .6; }
|
||||||
|
.token-row.pending { border: 1px dashed var(--accent); }
|
||||||
|
.token-example-box { display: flex; gap: 10px; align-items: flex-start; margin-top: 8px; }
|
||||||
|
.token-example-box .token-example { flex: 1; min-width: 0; margin: 0; }
|
||||||
|
.token-example-box .btn { height: 28px; font-size: 12px; }
|
||||||
|
.token-example mark { background: var(--accent-soft); color: var(--accent); border-radius: 3px; padding: 0 2px; font-weight: 600; }
|
||||||
.token-secret code { flex: 1; min-width: 0; font: 12px var(--mono); overflow-wrap: anywhere; user-select: all; }
|
.token-secret code { flex: 1; min-width: 0; font: 12px var(--mono); overflow-wrap: anywhere; user-select: all; }
|
||||||
.token-secret .btn { height: 28px; font-size: 12px; }
|
.token-secret .btn { height: 28px; font-size: 12px; }
|
||||||
.token-example {
|
.token-example {
|
||||||
|
|||||||
Reference in new issue
Block a user