feat(oauth): ARIA kann Provider selbst registrieren + Custom-Provider in Diagnostic & App
ARIA hat jetzt das META-Tool oauth_register_provider. Wenn Stefan einen Service nutzen will, der nicht in den (auf Spotify reduzierten) Defaults ist, kann sie auth_url/token_url/scopes/client_auth selbst eintragen — ARIA kennt typische OAuth-Endpunkte (Dropbox, Discord, Notion, Slack, Zoom, Trello, LinkedIn, Reddit, Twitch) aus ihrem Training. Sie traegt NUR die URLs ein, client_id/secret bleiben Stefans Job (Diagnostic / App-UI) — bewusste Trennung damit Credentials nicht im Chat-Verlauf landen. DEFAULT_PROVIDERS auf Spotify reduziert — Rest war aktuell ungenutzt und macht den Code unnoetig "groß". ARIA registriert on-demand. Diagnostic-UI: - Custom-Provider zeigen auth_url/token_url/scopes als sichtbare Felder - Defaults verstecken die Felder hinter "Default-URLs ueberschreiben (advanced)" damit man die Spotify-URLs nicht versehentlich loescht - "+ Custom OAuth-Provider hinzufuegen" Button mit Prompts fuer Name/URLs/Scopes - 🗑-Icon bei Custom-Services (Service komplett entfernen) App-UI (neu fuer unterwegs): - Settings → Sektion 🔑 "OAuth-Apps" zwischen Skills und Protokoll - OAuthBrowser-Komponente analog zu Trigger/Skill-Browser: Liste mit Status, Tap → Edit-Modal mit client_id/secret + Advanced-Toggle fuer URLs. "Autorisieren ↗" oeffnet System-Browser via Linking.openURL, redirected zur RVS-Callback-Page, Status-Refresh nach 8s. - "+ Custom"-Button → Full-Screen-Modal fuer Service-Anlage. - brainApi um listOAuthServices/getOAuthApps/saveOAuthApp/ deleteOAuthApp/authorizeOAuth/revokeOAuth erweitert. Workflow ist jetzt: "verbinde mich mit Dropbox" → ARIA registriert Provider → "trag client_id/secret in Settings ein" → Stefan macht das in App oder Diagnostic → "Autorisieren ↗" → fertig. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+74
-2
@@ -3900,11 +3900,27 @@
|
||||
const isCustom = !knownDefaults.includes(svcName);
|
||||
const customMark = isCustom ? ' <span style="color:#8888AA;font-size:10px;">(custom)</span>' : '';
|
||||
card.style.cssText = 'background:#0D0D1A;border:1px solid #2A2A3E;border-radius:6px;padding:10px 12px;';
|
||||
// Custom-Provider zeigen URL/Scope-Felder zum Editieren — Defaults
|
||||
// verstecken die Felder hinter einem "<details>" damit sie nicht
|
||||
// ausversehen ueberschrieben werden.
|
||||
const scopesValue = Array.isArray(app.scopes) ? app.scopes.join(' ') : '';
|
||||
const urlFieldsHtml = `
|
||||
<label style="color:#8888AA;font-size:11px;margin-top:6px;">auth_url:</label>
|
||||
<input type="text" id="oauth-auth-${_ofmt(svcName)}" value="${_ofmt(app.auth_url || '')}" placeholder="https://provider.com/oauth/authorize"
|
||||
style="background:#1E1E2E;color:#fff;border:1px solid #2A2A3E;border-radius:4px;padding:4px 8px;font-size:11px;font-family:monospace;">
|
||||
<label style="color:#8888AA;font-size:11px;">token_url:</label>
|
||||
<input type="text" id="oauth-tok-${_ofmt(svcName)}" value="${_ofmt(app.token_url || '')}" placeholder="https://provider.com/oauth/token"
|
||||
style="background:#1E1E2E;color:#fff;border:1px solid #2A2A3E;border-radius:4px;padding:4px 8px;font-size:11px;font-family:monospace;">
|
||||
<label style="color:#8888AA;font-size:11px;">scopes (space-separated):</label>
|
||||
<input type="text" id="oauth-scopes-${_ofmt(svcName)}" value="${_ofmt(scopesValue)}" placeholder="read write user.email"
|
||||
style="background:#1E1E2E;color:#fff;border:1px solid #2A2A3E;border-radius:4px;padding:4px 8px;font-size:11px;font-family:monospace;">
|
||||
`;
|
||||
card.innerHTML = `
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
|
||||
<strong style="color:#FFF;text-transform:capitalize;">${_ofmt(svcName)}</strong>${customMark}
|
||||
<span style="color:${statusColor};font-size:12px;flex:1;">${statusText}</span>
|
||||
${s.authenticated ? `<button class="btn secondary" onclick="revokeOAuth('${_ofmt(svcName)}')" style="padding:2px 8px;font-size:10px;" title="Token loeschen">Abmelden</button>` : ''}
|
||||
${isCustom ? `<button class="btn secondary" onclick="deleteOAuthApp('${_ofmt(svcName)}')" style="padding:2px 8px;font-size:10px;background:#3A1F1F;color:#FF6B6B;border-color:#FF6B6B;" title="Service komplett entfernen">🗑</button>` : ''}
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;gap:6px;">
|
||||
<label style="color:#8888AA;font-size:11px;">client_id:</label>
|
||||
@@ -3916,6 +3932,12 @@
|
||||
style="flex:1;background:#1E1E2E;color:#fff;border:1px solid #2A2A3E;border-radius:4px;padding:4px 8px;font-size:12px;font-family:monospace;">
|
||||
<button type="button" class="btn secondary" onclick="toggleSecret('oauth-sec-${_ofmt(svcName)}', this)" style="padding:2px 8px;font-size:10px;">👁</button>
|
||||
</div>
|
||||
${isCustom ? urlFieldsHtml : `
|
||||
<details style="margin-top:4px;">
|
||||
<summary style="color:#666680;font-size:10px;cursor:pointer;">Default-URLs überschreiben (advanced)</summary>
|
||||
<div style="display:flex;flex-direction:column;gap:6px;margin-top:6px;">${urlFieldsHtml}</div>
|
||||
</details>
|
||||
`}
|
||||
<div style="display:flex;gap:6px;margin-top:4px;">
|
||||
<button class="btn primary" onclick="saveOAuthApp('${_ofmt(svcName)}')" style="padding:4px 12px;font-size:11px;">Speichern</button>
|
||||
<button class="btn secondary" onclick="authorizeOAuth('${_ofmt(svcName)}')" style="padding:4px 12px;font-size:11px;" ${!s.configured ? 'disabled title="Erst client_id+secret eintragen"' : ''}>
|
||||
@@ -3926,25 +3948,75 @@
|
||||
`;
|
||||
listEl.appendChild(card);
|
||||
}
|
||||
// "+ Custom Service hinzufuegen"-Button am Ende
|
||||
const addCard = document.createElement('div');
|
||||
addCard.style.cssText = 'background:#0D0D1A;border:1px dashed #2A2A3E;border-radius:6px;padding:10px 12px;';
|
||||
addCard.innerHTML = `
|
||||
<button class="btn secondary" onclick="openOAuthCustomDialog()" style="width:100%;padding:8px;font-size:12px;color:#8888AA;">
|
||||
➕ Custom OAuth-Provider hinzufuegen (Dropbox, Discord, Notion, ...)
|
||||
</button>
|
||||
`;
|
||||
listEl.appendChild(addCard);
|
||||
if (allServices.length === 0) {
|
||||
listEl.innerHTML = '<div style="color:#555570;">Keine Services bekannt.</div>';
|
||||
// (addCard ist trotzdem schon dran)
|
||||
}
|
||||
} catch (e) {
|
||||
listEl.innerHTML = `<div style="color:#FF6B6B;">Fehler beim Laden: ${_ofmt(e.message)}</div>`;
|
||||
}
|
||||
}
|
||||
function openOAuthCustomDialog() {
|
||||
const name = (prompt('Service-Name (z.B. dropbox, discord) — a-z 0-9 _ -:') || '').trim().toLowerCase();
|
||||
if (!name || !/^[a-z0-9_-]+$/.test(name)) {
|
||||
if (name) alert('Ungueltiger Name. Erlaubt: a-z 0-9 _ -');
|
||||
return;
|
||||
}
|
||||
const authUrl = (prompt(`auth_url fuer ${name}:`, 'https://') || '').trim();
|
||||
if (!authUrl) return;
|
||||
const tokenUrl = (prompt(`token_url fuer ${name}:`, 'https://') || '').trim();
|
||||
if (!tokenUrl) return;
|
||||
const scopesRaw = (prompt(`scopes (space-separated, optional):`, '') || '').trim();
|
||||
const scopes = scopesRaw ? scopesRaw.split(/\s+/).filter(Boolean) : undefined;
|
||||
fetch('/api/brain/oauth/apps', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ service: name, auth_url: authUrl, token_url: tokenUrl, scopes }),
|
||||
})
|
||||
.then(r => r.ok ? r.json() : r.text().then(t => Promise.reject(new Error(t))))
|
||||
.then(() => loadOAuthServices())
|
||||
.catch(e => alert('Custom-Service anlegen fehlgeschlagen: ' + e.message));
|
||||
}
|
||||
async function deleteOAuthApp(service) {
|
||||
if (!confirm(`Service "${service}" komplett entfernen? client_id/secret + Token werden geloescht.`)) return;
|
||||
try {
|
||||
const r = await fetch('/api/brain/oauth/apps/' + encodeURIComponent(service), { method: 'DELETE' });
|
||||
if (!r.ok) {
|
||||
alert('Loeschen fehlgeschlagen: ' + (await r.text()));
|
||||
return;
|
||||
}
|
||||
loadOAuthServices();
|
||||
} catch (e) {
|
||||
alert('Loeschen fehlgeschlagen: ' + e.message);
|
||||
}
|
||||
}
|
||||
async function saveOAuthApp(service) {
|
||||
const cid = document.getElementById('oauth-cid-' + service)?.value?.trim() || '';
|
||||
const sec = document.getElementById('oauth-sec-' + service)?.value || '';
|
||||
const authUrl = (document.getElementById('oauth-auth-' + service)?.value || '').trim();
|
||||
const tokenUrl = (document.getElementById('oauth-tok-' + service)?.value || '').trim();
|
||||
const scopesRaw = (document.getElementById('oauth-scopes-' + service)?.value || '').trim();
|
||||
if (!cid) {
|
||||
alert('client_id darf nicht leer sein.');
|
||||
return;
|
||||
}
|
||||
const body = { service, client_id: cid, client_secret: sec };
|
||||
if (authUrl) body.auth_url = authUrl;
|
||||
if (tokenUrl) body.token_url = tokenUrl;
|
||||
if (scopesRaw) body.scopes = scopesRaw.split(/\s+/).filter(Boolean);
|
||||
try {
|
||||
const r = await fetch('/api/brain/oauth/apps', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ service, client_id: cid, client_secret: sec }),
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!r.ok) {
|
||||
const t = await r.text();
|
||||
|
||||
Reference in New Issue
Block a user