From ed45a9a23debed28ebf034dd824bd4539d5b7680 Mon Sep 17 00:00:00 2001 From: ScreenTinker Date: Sun, 14 Jun 2026 17:54:23 -0500 Subject: [PATCH] feat(ui): surface the agency portal handoff at token creation (#73) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When an agency token is created, the once-shown secret box now also shows the Portal URL (window.location.origin + '/agency' — the real public host the admin is on, correct behind Cloudflare, config-free) and a COPYABLE INVITE: "Go to and paste this access key: ". The key lives in the invite TEXT, never in a URL — no magic link, because Cloudflare logs query strings and chat apps unfurl links (the key would leak on paste). Same exposure as the key field itself, just with the destination surfaced. The existing "won't see it again" warning now covers the invite too (it contains the key). i18n x5 (parity test). Skipped the optional per-row portal URL in the token list: it's the same /agency for every agency token, so per-row it's noise; the creation invite + the /docs link cover discovery. Confirmed: invite copy button copies the full "go here + paste key" text; /agency resolves (200); i18n parity + full suite green (149). Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/js/i18n/de.js | 4 ++++ frontend/js/i18n/en.js | 4 ++++ frontend/js/i18n/es.js | 4 ++++ frontend/js/i18n/fr.js | 4 ++++ frontend/js/i18n/pt.js | 4 ++++ frontend/js/views/settings.js | 19 +++++++++++++++++++ 6 files changed, 39 insertions(+) diff --git a/frontend/js/i18n/de.js b/frontend/js/i18n/de.js index 232876d..d41d4ee 100644 --- a/frontend/js/i18n/de.js +++ b/frontend/js/i18n/de.js @@ -354,6 +354,10 @@ export default { 'apitoken.title': 'API-Tokens', 'apitoken.desc': 'Persönliche Zugriffstokens für die öffentliche API, beschränkt auf diesen Arbeitsbereich. Behandeln Sie sie wie Passwörter – wer das Token hat, kann hier in Ihrem Namen handeln.', 'apitoken.docs_link': 'Neu bei der API? Zur vollständigen Dokumentation →', + 'apitoken.portal_url_label': 'Agentur-Portal-URL', + 'apitoken.invite_label': 'Einladung zum Kopieren — an die Agentur senden:', + 'apitoken.invite_text': 'Gehe zu {url} und füge diesen Zugriffsschlüssel ein: {key}', + 'apitoken.copy_invite': 'Einladung kopieren', 'apitoken.name_placeholder': 'z. B. Agentur-Integration', 'apitoken.scope_read': 'Nur Lesen', 'apitoken.scope_write': 'Lesen & Schreiben', diff --git a/frontend/js/i18n/en.js b/frontend/js/i18n/en.js index 654f3cd..871364d 100644 --- a/frontend/js/i18n/en.js +++ b/frontend/js/i18n/en.js @@ -390,6 +390,10 @@ export default { 'apitoken.title': 'API Tokens', 'apitoken.desc': 'Personal access tokens for the public API, scoped to this workspace. Treat them like passwords — anyone with the token can act as you here.', 'apitoken.docs_link': 'New to the API? See the full documentation →', + 'apitoken.portal_url_label': 'Agency portal URL', + 'apitoken.invite_label': 'Copyable invite — send this to the agency:', + 'apitoken.invite_text': 'Go to {url} and paste this access key: {key}', + 'apitoken.copy_invite': 'Copy invite', 'apitoken.name_placeholder': 'e.g. Agency integration', 'apitoken.scope_read': 'Read only', 'apitoken.scope_write': 'Read & write', diff --git a/frontend/js/i18n/es.js b/frontend/js/i18n/es.js index 1b66161..93c68bd 100644 --- a/frontend/js/i18n/es.js +++ b/frontend/js/i18n/es.js @@ -353,6 +353,10 @@ export default { 'apitoken.title': 'Tokens de API', 'apitoken.desc': 'Tokens de acceso personal para la API pública, limitados a este espacio de trabajo. Trátalos como contraseñas: cualquiera que tenga el token puede actuar como tú aquí.', 'apitoken.docs_link': '¿Nuevo en la API? Consulta la documentación completa →', + 'apitoken.portal_url_label': 'URL del portal de agencia', + 'apitoken.invite_label': 'Invitación para copiar — envíala a la agencia:', + 'apitoken.invite_text': 'Ve a {url} y pega esta clave de acceso: {key}', + 'apitoken.copy_invite': 'Copiar invitación', 'apitoken.name_placeholder': 'p. ej. Integración de agencia', 'apitoken.scope_read': 'Solo lectura', 'apitoken.scope_write': 'Lectura y escritura', diff --git a/frontend/js/i18n/fr.js b/frontend/js/i18n/fr.js index 4b738c0..232d511 100644 --- a/frontend/js/i18n/fr.js +++ b/frontend/js/i18n/fr.js @@ -354,6 +354,10 @@ export default { 'apitoken.title': "Jetons d'API", 'apitoken.desc': "Jetons d'accès personnels pour l'API publique, limités à cet espace de travail. Traitez-les comme des mots de passe : toute personne disposant du jeton peut agir en votre nom ici.", 'apitoken.docs_link': "Nouveau sur l'API ? Voir la documentation complète →", + 'apitoken.portal_url_label': 'URL du portail agence', + 'apitoken.invite_label': "Invitation à copier — envoyez-la à l'agence :", + 'apitoken.invite_text': "Allez sur {url} et collez cette clé d'accès : {key}", + 'apitoken.copy_invite': "Copier l'invitation", 'apitoken.name_placeholder': 'p. ex. Intégration agence', 'apitoken.scope_read': 'Lecture seule', 'apitoken.scope_write': 'Lecture et écriture', diff --git a/frontend/js/i18n/pt.js b/frontend/js/i18n/pt.js index 447f1b8..ddbbe2a 100644 --- a/frontend/js/i18n/pt.js +++ b/frontend/js/i18n/pt.js @@ -354,6 +354,10 @@ export default { 'apitoken.title': 'Tokens de API', 'apitoken.desc': 'Tokens de acesso pessoal para a API pública, restritos a este espaço de trabalho. Trate-os como senhas — qualquer pessoa com o token pode agir como você aqui.', 'apitoken.docs_link': 'Novo na API? Veja a documentação completa →', + 'apitoken.portal_url_label': 'URL do portal da agência', + 'apitoken.invite_label': 'Convite para copiar — envie para a agência:', + 'apitoken.invite_text': 'Acesse {url} e cole esta chave de acesso: {key}', + 'apitoken.copy_invite': 'Copiar convite', 'apitoken.name_placeholder': 'ex.: Integração da agência', 'apitoken.scope_read': 'Somente leitura', 'apitoken.scope_write': 'Leitura e escrita', diff --git a/frontend/js/views/settings.js b/frontend/js/views/settings.js index 3749684..3c8027a 100644 --- a/frontend/js/views/settings.js +++ b/frontend/js/views/settings.js @@ -472,6 +472,11 @@ export async function render(container) { const r = await api.createToken(payload); const box = document.getElementById('tokenSecretBox'); box.style.display = 'block'; + // #73: for agency tokens, surface the handoff (portal URL + a copyable invite). The key + // is in the invite TEXT, never in a URL (Cloudflare logs query strings + chat apps unfurl + // links). window.location.origin is the real public host the admin is on (correct behind CF). + const portalUrl = window.location.origin + '/agency'; + const inviteText = t('apitoken.invite_text', { url: portalUrl, key: r.token }); box.innerHTML = `

${t('apitoken.secret_title')}

@@ -480,6 +485,14 @@ export async function render(container) {
+ ${scope === 'agency' ? ` +
+ + + + + +
` : ''} `; document.getElementById('copyTokenBtn')?.addEventListener('click', async () => { @@ -488,6 +501,12 @@ export async function render(container) { showToast(t('apitoken.copied'), 'success'); } catch { /* clipboard may be unavailable; the field is selectable */ } }); + document.getElementById('copyInviteBtn')?.addEventListener('click', async () => { + try { + await navigator.clipboard.writeText(inviteText); // full "go here + paste key" text + showToast(t('apitoken.copied'), 'success'); + } catch { /* field is selectable as a fallback */ } + }); document.getElementById('tokName').value = ''; showToast(t('apitoken.created_toast'), 'success'); loadTokens();