diff --git a/frontend/js/i18n/de.js b/frontend/js/i18n/de.js index d4f680a..704482b 100644 --- a/frontend/js/i18n/de.js +++ b/frontend/js/i18n/de.js @@ -357,6 +357,12 @@ export default { 'apitoken.scope_read': 'Nur Lesen', 'apitoken.scope_write': 'Lesen & Schreiben', 'apitoken.scope_full': 'Voll (inkl. Gerätebefehle)', + 'apitoken.scope_agency': 'Agentur (nur in gewählte Playlists hochladen)', + 'apitoken.agency_playlists_label': 'Playlists, in die dieser Agentur-Token posten darf', + 'apitoken.agency_playlists_hint': 'Der Token kann nur in diese Playlists hochladen und zeitlich begrenzte Elemente hinzufügen. Hinzufügungen landen als Entwurf zur Veröffentlichung durch dich.', + 'apitoken.agency_needs_playlists': 'Wähle mindestens eine Playlist für einen Agentur-Token.', + 'apitoken.agency_no_playlists': 'Erstelle zuerst eine Playlist – ein Agentur-Token muss auf eine zielen.', + 'apitoken.targets_label': 'Zugewiesen:', 'apitoken.create': 'Token erstellen', 'apitoken.none': 'Noch keine Tokens.', 'apitoken.col_token': 'Token', diff --git a/frontend/js/i18n/en.js b/frontend/js/i18n/en.js index 7efdaa0..f07750f 100644 --- a/frontend/js/i18n/en.js +++ b/frontend/js/i18n/en.js @@ -393,6 +393,12 @@ export default { 'apitoken.scope_read': 'Read only', 'apitoken.scope_write': 'Read & write', 'apitoken.scope_full': 'Full (incl. device commands)', + 'apitoken.scope_agency': 'Agency (upload to chosen playlists only)', + 'apitoken.agency_playlists_label': 'Playlists this agency token may post to', + 'apitoken.agency_playlists_hint': 'The token can upload and add date-bounded items to these playlists only. Additions land as drafts for you to publish.', + 'apitoken.agency_needs_playlists': 'Select at least one playlist for an agency token.', + 'apitoken.agency_no_playlists': 'Create a playlist first — an agency token must target one.', + 'apitoken.targets_label': 'Designated:', 'apitoken.create': 'Create token', 'apitoken.none': 'No tokens yet.', 'apitoken.col_token': 'Token', diff --git a/frontend/js/i18n/es.js b/frontend/js/i18n/es.js index f722311..ce06355 100644 --- a/frontend/js/i18n/es.js +++ b/frontend/js/i18n/es.js @@ -356,6 +356,12 @@ export default { 'apitoken.scope_read': 'Solo lectura', 'apitoken.scope_write': 'Lectura y escritura', 'apitoken.scope_full': 'Completo (incl. comandos de dispositivo)', + 'apitoken.scope_agency': 'Agencia (subir solo a listas elegidas)', + 'apitoken.agency_playlists_label': 'Listas a las que este token de agencia puede publicar', + 'apitoken.agency_playlists_hint': 'El token solo puede subir y añadir elementos con fechas a estas listas. Las adiciones quedan como borrador para que las publiques.', + 'apitoken.agency_needs_playlists': 'Selecciona al menos una lista para un token de agencia.', + 'apitoken.agency_no_playlists': 'Crea una lista primero: un token de agencia debe apuntar a una.', + 'apitoken.targets_label': 'Designadas:', 'apitoken.create': 'Crear token', 'apitoken.none': 'Aún no hay tokens.', 'apitoken.col_token': 'Token', diff --git a/frontend/js/i18n/fr.js b/frontend/js/i18n/fr.js index e7e1c48..d361b67 100644 --- a/frontend/js/i18n/fr.js +++ b/frontend/js/i18n/fr.js @@ -357,6 +357,12 @@ export default { 'apitoken.scope_read': 'Lecture seule', 'apitoken.scope_write': 'Lecture et écriture', 'apitoken.scope_full': 'Complet (cmd. appareils incluses)', + 'apitoken.scope_agency': 'Agence (téléverser uniquement vers les listes choisies)', + 'apitoken.agency_playlists_label': 'Listes vers lesquelles ce jeton d\'agence peut publier', + 'apitoken.agency_playlists_hint': 'Le jeton peut uniquement téléverser et ajouter des éléments datés à ces listes. Les ajouts restent en brouillon pour que vous les publiiez.', + 'apitoken.agency_needs_playlists': 'Sélectionnez au moins une liste pour un jeton d\'agence.', + 'apitoken.agency_no_playlists': 'Créez d\'abord une liste : un jeton d\'agence doit en cibler une.', + 'apitoken.targets_label': 'Assignées :', 'apitoken.create': 'Créer un jeton', 'apitoken.none': 'Aucun jeton pour le moment.', 'apitoken.col_token': 'Jeton', diff --git a/frontend/js/i18n/pt.js b/frontend/js/i18n/pt.js index da40f03..641f53d 100644 --- a/frontend/js/i18n/pt.js +++ b/frontend/js/i18n/pt.js @@ -357,6 +357,12 @@ export default { 'apitoken.scope_read': 'Somente leitura', 'apitoken.scope_write': 'Leitura e escrita', 'apitoken.scope_full': 'Completo (incl. comandos de dispositivo)', + 'apitoken.scope_agency': 'Agência (enviar apenas para listas escolhidas)', + 'apitoken.agency_playlists_label': 'Listas às quais este token de agência pode publicar', + 'apitoken.agency_playlists_hint': 'O token só pode enviar e adicionar itens com datas a estas listas. As adições ficam como rascunho para você publicar.', + 'apitoken.agency_needs_playlists': 'Selecione pelo menos uma lista para um token de agência.', + 'apitoken.agency_no_playlists': 'Crie uma lista primeiro: um token de agência deve apontar para uma.', + 'apitoken.targets_label': 'Designadas:', 'apitoken.create': 'Criar token', 'apitoken.none': 'Ainda não há tokens.', 'apitoken.col_token': 'Token', diff --git a/frontend/js/views/settings.js b/frontend/js/views/settings.js index 6e0fe02..caa5e87 100644 --- a/frontend/js/views/settings.js +++ b/frontend/js/views/settings.js @@ -74,10 +74,16 @@ export async function render(container) { + +
${t('settings.loading_users')}
${t('apitoken.agency_no_playlists')}
`; + } + }); + document.getElementById('createTokenBtn')?.addEventListener('click', async () => { const name = document.getElementById('tokName').value.trim(); const scope = document.getElementById('tokScope').value; + const payload = { name, scope }; + if (scope === 'agency') { + const ids = [...document.querySelectorAll('#agencyPlaylistList .agency-pl:checked')].map(c => c.value); + if (!ids.length) return showToast(t('apitoken.agency_needs_playlists'), 'error'); + payload.target_playlist_ids = ids; + } const btn = document.getElementById('createTokenBtn'); btn.disabled = true; try { - const r = await api.createToken({ name, scope }); + const r = await api.createToken(payload); const box = document.getElementById('tokenSecretBox'); box.style.display = 'block'; box.innerHTML = ` diff --git a/server/routes/tokens.js b/server/routes/tokens.js index 20436c3..673ea36 100644 --- a/server/routes/tokens.js +++ b/server/routes/tokens.js @@ -19,6 +19,11 @@ router.get('/', (req, res) => { SELECT id, prefix, name, scope, workspace_id, created_at, last_used_at, revoked_at FROM api_tokens WHERE user_id = ? AND workspace_id = ? ORDER BY created_at DESC `).all(req.user.id, req.workspaceId); + // #73: attach designated playlists for agency tokens so the admin sees the binding persist. + const targetsStmt = db.prepare('SELECT p.id, p.name FROM api_token_targets t JOIN playlists p ON p.id = t.playlist_id WHERE t.token_id = ? ORDER BY p.name'); + for (const r of rows) { + if (r.scope === 'agency') r.targets = targetsStmt.all(r.id); + } res.json(rows); });