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
@@ -806,6 +806,14 @@
|
||||
</div>
|
||||
<div id="app-pw-list"></div>
|
||||
|
||||
<!-- Single sign-on: hidden unless the server has a provider or the
|
||||
account already holds a linked identity. -->
|
||||
<div id="sso-section" class="hidden">
|
||||
<h4 class="panel-title" style="margin-top:24px" data-i18n="sso_link_title">Single Sign-On</h4>
|
||||
<p class="panel-desc" data-i18n="sso_link_desc">Verknüpfte Konten, mit denen du dich ohne Passwort anmelden kannst.</p>
|
||||
<div id="sso-identity-list"></div>
|
||||
</div>
|
||||
|
||||
<h4 class="panel-title" style="margin-top:24px" data-i18n="settings_privacy">Privatsphäre</h4>
|
||||
<p class="panel-desc" data-i18n="settings_private_visibility_desc">Wie private Termine für andere Gruppenmitglieder erscheinen</p>
|
||||
<div class="form-group">
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Increment APP_VERSION with every code change
|
||||
export const APP_VERSION = 'v92';
|
||||
export const APP_VERSION = 'v93';
|
||||
|
||||
+1
-1
@@ -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 => {
|
||||
|
||||
Reference in New Issue
Block a user