From a977ee3c2bc215107dfbaa51dffc3936860d5f0b Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Thu, 10 Sep 2026 06:10:43 +0200 Subject: [PATCH] feat(oidc): verknuepfte SSO-Konten in den Einstellungen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- frontend/index.html | 8 ++++++ frontend/js/calendar.js | 59 +++++++++++++++++++++++++++++++++++++++++ frontend/js/i18n.js | 12 +++++++++ frontend/js/version.js | 2 +- frontend/sw.js | 2 +- 5 files changed, 81 insertions(+), 2 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index f0e76c5..06432f8 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -806,6 +806,14 @@
+ + +

Privatsphäre

Wie private Termine für andere Gruppenmitglieder erscheinen

diff --git a/frontend/js/calendar.js b/frontend/js/calendar.js index 9ef4e9a..41f62bb 100644 --- a/frontend/js/calendar.js +++ b/frontend/js/calendar.js @@ -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 = `

${t('sso_none_linked')}

`; + section.classList.remove('hidden'); + return; + } + + list.innerHTML = identities.map(i => `
+ ${escHtml(i.name || i.provider)} + ${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')} + +
`).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'); diff --git a/frontend/js/i18n.js b/frontend/js/i18n.js index d500c72..d00b716 100644 --- a/frontend/js/i18n.js +++ b/frontend/js/i18n.js @@ -39,6 +39,12 @@ const translations = { sso_err_discovery_failed: 'Anmeldedienst antwortet nicht korrekt', sso_err_setup_required: 'Bitte zuerst das Administrator-Konto einrichten', sso_link_title: 'Single Sign-On', + sso_link_desc: 'Verknüpfte Konten, mit denen du dich ohne Passwort anmelden kannst.', + sso_none_linked: 'Noch kein Konto verknüpft. Melde dich einmal über den SSO-Knopf auf der Anmeldeseite an.', + sso_last_login: 'Zuletzt', + sso_never_used: 'Noch nicht benutzt', + sso_unlinked: 'Verknüpfung aufgehoben', + sso_last_identity_hint: 'Letzte Verknüpfung — dieses Konto hat kein Passwort und wäre danach nicht mehr erreichbar.', sso_unlink: 'Verknüpfung aufheben', // Topbar @@ -846,6 +852,12 @@ const translations = { sso_err_discovery_failed: 'Sign-in service returned an invalid response', sso_err_setup_required: 'Please set up the administrator account first', sso_link_title: 'Single sign-on', + sso_link_desc: 'Linked accounts you can sign in with, without a password.', + sso_none_linked: 'No account linked yet. Sign in once via the SSO button on the login screen.', + sso_last_login: 'Last used', + sso_never_used: 'Not used yet', + sso_unlinked: 'Link removed', + sso_last_identity_hint: 'Last link — this account has no password and would become unreachable.', sso_unlink: 'Unlink', // Topbar diff --git a/frontend/js/version.js b/frontend/js/version.js index c8e2cb0..d8186c2 100644 --- a/frontend/js/version.js +++ b/frontend/js/version.js @@ -1,2 +1,2 @@ // Increment APP_VERSION with every code change -export const APP_VERSION = 'v92'; +export const APP_VERSION = 'v93'; diff --git a/frontend/sw.js b/frontend/sw.js index 836c5d7..6c3e4c4 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -7,7 +7,7 @@ // the entry HTML / version files). New releases take effect on the next // reload, no manual SW unregister required. -const CACHE_VERSION = 'calendarr-v38'; +const CACHE_VERSION = 'calendarr-v39'; const OFFLINE_SHELL = ['/', '/index.html']; self.addEventListener('install', event => {