feat(oidc): verknuepfte SSO-Konten in den Einstellungen
Die Endpunkte gab es schon, die Oberflaeche fehlte — es war schlicht nicht sichtbar, ob und womit ein Konto verknuepft ist. Unter Einstellungen > Profil, zwischen App-Passwoertern und Privatsphaere, steht jetzt "Single Sign-On" mit den verknuepften Konten (Anbieter, E-Mail, letzte Nutzung) und je einem Knopf zum Aufheben. Ohne Verknuepfung erscheint ein Hinweis, wie man eine anlegt; ist auf dem Server ueberhaupt kein Provider konfiguriert, bleibt der ganze Abschnitt unsichtbar. Bei einem per SSO angelegten Konto ist die letzte Verknuepfung nicht loesbar — es hat kein brauchbares Passwort und waere danach dauerhaft unerreichbar. Der Knopf ist in dem Fall deaktiviert und erklaert sich per Tooltip; der Server lehnt es zusaetzlich ab. Im Browser durchgespielt: verknuepfen, Eintrag erscheint mit Anbieter und Datum, aufheben, Bestaetigung, leerer Zustand. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
725b61c8cb
commit
a977ee3c2b
@@ -3398,6 +3398,7 @@ function populateSettings() {
|
||||
if (dh) dh.checked = !!p.directory_hidden;
|
||||
}).catch(() => {});
|
||||
initAppPasswords();
|
||||
initSSOIdentities();
|
||||
|
||||
// Show users nav button only for admins
|
||||
const user = JSON.parse(localStorage.getItem('user') || '{}');
|
||||
@@ -4940,6 +4941,64 @@ function bindProfileModal() {
|
||||
};
|
||||
}
|
||||
|
||||
// ── Linked SSO accounts — lives in Settings → Profile ──
|
||||
async function initSSOIdentities() {
|
||||
const section = document.getElementById('sso-section');
|
||||
const list = document.getElementById('sso-identity-list');
|
||||
if (!section || !list) return;
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = await api.oidcIdentities();
|
||||
} catch (e) {
|
||||
section.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
if (!data) return;
|
||||
|
||||
const identities = data.identities || [];
|
||||
// An SSO-provisioned account cannot fall back to a password, so its last
|
||||
// link must stay — otherwise the user locks themselves out for good.
|
||||
const isSSOOnly = data.auth_source === 'sso';
|
||||
|
||||
if (!identities.length) {
|
||||
// Nothing linked: only worth showing if SSO is available at all.
|
||||
let enabled = false;
|
||||
try {
|
||||
const cfg = await api.oidcProviders();
|
||||
enabled = !!(cfg && cfg.enabled);
|
||||
} catch (e) { /* treat as unavailable */ }
|
||||
if (!enabled) { section.classList.add('hidden'); return; }
|
||||
list.innerHTML = `<p class="text-muted">${t('sso_none_linked')}</p>`;
|
||||
section.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = identities.map(i => `<div class="app-pw-item">
|
||||
<span class="app-pw-name">${escHtml(i.name || i.provider)}</span>
|
||||
<span class="app-pw-meta">${i.email ? escHtml(i.email) + ' · ' : ''}${
|
||||
i.last_login_at
|
||||
? t('sso_last_login') + ' ' + new Date(i.last_login_at).toLocaleDateString(getLocale())
|
||||
: t('sso_never_used')}</span>
|
||||
<button class="btn btn-ghost btn-sm sso-unlink" data-id="${i.id}"
|
||||
${isSSOOnly && identities.length === 1 ? 'disabled title="' + escHtml(t('sso_last_identity_hint')) + '"' : ''}
|
||||
>${t('sso_unlink')}</button>
|
||||
</div>`).join('');
|
||||
section.classList.remove('hidden');
|
||||
|
||||
list.querySelectorAll('.sso-unlink').forEach(btn => {
|
||||
btn.onclick = async () => {
|
||||
try {
|
||||
await api.oidcUnlink(btn.dataset.id);
|
||||
showToast(t('sso_unlinked'));
|
||||
initSSOIdentities();
|
||||
} catch (e) {
|
||||
showToast(e.message === 'oidc_last_identity' ? t('sso_last_identity_hint') : e.message, true);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// ── App passwords (CalDAV) — lives in Settings → Profile ──
|
||||
function initAppPasswords() {
|
||||
const appPwList = document.getElementById('app-pw-list');
|
||||
|
||||
Reference in New Issue
Block a user