Die Platzhalter-URI calendarr://oauth/callback durch die tatsaechlichen
Reverse-DNS-Schemata der beiden Apps ersetzt und vermerkt, dass sie als
Public Clients ohne Secret laufen und ohne MOBILE_CLIENT_ID keinen
SSO-Button zeigen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Zusaetzlicher Anmeldeweg per OIDC neben dem bestehenden Passwort-Login. Ohne
gesetztes OIDC_PROVIDERS aendert sich nichts: /api/auth/oidc/providers meldet
enabled:false und der Login-Screen bleibt unveraendert.
Backend
- oidc_config.py: Provider-Registry rein aus ENV. Ein zweiter Provider ist
reine Konfiguration (OIDC_PROVIDERS + OIDC_<KEY>_* Block), keine Codeaenderung.
- oidc_client.py: Discovery, JWKS, Token-Tausch, ID-Token-Pruefung ueber
authlib.jose mit fest gepinnter Signatur-Allowlist (nur asymmetrisch, damit
alg:none und Algorithmus-Confusion ausgeschlossen sind).
- oidc_identity.py: Zuordnung ueber (provider_key, sub). Ein E-Mail-Treffer auf
ein bestehendes Konto wird standardmaessig abgelehnt statt verknuepft — sonst
erbt jeder, der im IdP eine beliebige E-Mail setzen kann, das passende Konto.
Verknuepfen ist eine explizite, authentifizierte Aktion. Auto-Provisioning
nur per ENV-Flag, standardmaessig aus, optional mit Domain-Allowlist.
- routers/oidc_router.py: /providers, /start, /callback, /complete, /exchange,
/link, /identities. PKCE S256; Flow-State als kurzlebiges signiertes JWT in
einem HttpOnly-Cookie mit jti==state statt eines prozesslokalen Dicts, damit
der Flow Neustarts und mehrere Worker uebersteht. Das Calendarr-JWT wird ueber
ein Einmal-Cookie uebergeben, nie ueber URL oder Fragment.
- /exchange als Kontrakt fuer die AppAuth-Clients: die App fuehrt den Flow als
Public Client selbst aus und tauscht das ID-Token; kein Secret im Request.
- models.py: neue Tabelle oidc_identities, users.auth_source; Migration in
main.py im dortigen try/except-Stil.
Nebenbefund, mitbehoben: auth.verify_password liess ValueError aus
bcrypt.checkpw durch. Ein nicht wohlgeformter password_hash wurde damit zu
HTTP 500 auf /api/auth/login und bei jedem CalDAV-Basic-Auth. SSO-Nutzer
bekommen einen echten bcrypt-Hash ueber ein zufaelliges, nirgends gespeichertes
Geheimnis; fuer CalDAV nutzen sie App-Passwoerter wie 2FA-Nutzer.
Web
- SSO-Block auf dem Login-Screen, aus /providers befuellt; Buttons sind Links,
weil /start eine Top-Level-Navigation braucht.
- consumeSsoHandoff() laeuft vor den Setup-/Token-Pruefungen in boot() und
raeumt die Query per replaceState ab, bevor es awaited.
- 401-Auto-Logout um die SSO-Pfade erweitert, sonst dreht ein fehlgeschlagener
Handoff eine Reload-Schleife.
- i18n-Keys in de + en (Fallback-Kette deckt die uebrigen acht Sprachen).
- Service Worker laesst /api/auth/oidc/ unangetastet durch.
37 neue Tests (Gesamtsuite 60 gruen): Signaturpruefung mit echten RSA-Tokens,
alg:none, falscher iss/aud/nonce, unbekannte client_id, Provisioning-Policies,
Username-Kollision, Einmaligkeit des Handoff-Cookies, unerreichbarer Provider.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Statt eines einzelnen Punkts in --primary zeigt der Mini-Kalender jetzt fuer
jeden Kalender, der an dem Tag mindestens einen Termin hat, einen Punkt in
dessen Farbe (max. 4, stabil nach erstem Auftreten sortiert). Die Farbquelle
ist dieselbe wie in Monats-/Wochenansicht: ev.color || ev.calendarColor.
Ausserdem laeuft der Mini-Kalender jetzt ueber filterEvents(), damit
ausgeblendete Gruppenmitglieder keinen Punkt mehr erzeugen. Auf dem
"heute"-Kreis bekommen die Punkte einen duennen weissen Ring, damit sie auf
der farbigen Flaeche lesbar bleiben.
Cherry-pick von 7a2085b (master) auf beta; die selected-Day-Regel aus beta
(color-mix) bleibt erhalten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Equal rights for all calendar types: the sidebar remove button is now a uniform
"hide" (eye) for every calendar instead of delete-for-some / nothing-for-shared.
- Owned local (incl. birthday) and iCal now hide via the server sidebar_hidden
flag (delete stays available in Settings' calendar table).
- Calendars shared with me get a per-device sidebar hide (they can't carry a
server flag); their events are filtered out while hidden, and the Settings
calendar table's "shared with me" rows gain an un-hide eye toggle.
- External caldav/google/ha unchanged.
Group calendars intentionally left as-is for now.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Admin default-theme editor: "Theme importieren" button loads a .theme.json
into the editor (colours only, live preview) so an imported theme can be set
as the instance default (still confirmed via "Standard speichern").
- Branding: drop the "Standard" placeholder; show pixel-dimension hints instead
(logo shown at max 150×40, favicon 128×128). Logo preview box now mirrors its
real topbar footprint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the blue defaults with the provided green theme (primary #58B900,
accent #45A148, today #6FB669, greens for month/selected/today-bg, etc.). These
apply when no admin instance theme is set. THEME.md defaults table updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Admin settings tab (admin-only) now holds user management plus a server-wide
default theme editor (live preview + discard) and logo/favicon branding.
- New singleton InstanceSettings model + /api/instance router (public GET for
the login screen; admin-gated theme PUT and logo/favicon upload/delete,
reusing the avatar PIL/validation pattern).
- Instance default theme is the base users inherit and the target a per-colour
"Reset" returns to (baseColor: instance default -> built-in).
- Custom favicon overrides the primary-colour tinting; custom logo replaces the
top-left glyph+text, hard-capped so it only scales down and never breaks layout.
Branding + defaults load before login (public endpoint).
- Theme export now uses the recognised .theme.json extension (old .theme still
imports); import stays partial/unknown-key aware.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Theme:
- Export/import themes via UI as <date>_<time>.theme (JSON, readable keys,
link to THEME.md). Import is partial-aware: only present params are written
(server accepts partial); prompts before importing files with unknown params.
- Dynamic favicon + theme-color tinted to the primary colour on load/save.
- New themable colours: general hover-highlight, day hover/selected/bg,
today background, plus two unified sidebar action-icon colours
(inactive/active) covering bell, hide, delete and read-only icons.
- All new colours are per-setting syncable; documented in THEME.md.
UX:
- Styled confirm dialog (#modal-confirm) replaces window.confirm() for
calendar delete and account disconnect.
- Birthday/local calendars and iCal subscriptions can now be hidden from the
sidebar via Settings (new sidebar_hidden column + hide toggle).
Backend: additive nullable columns + idempotent migrations for user_settings
colours and local_calendars/ical_subscriptions.sidebar_hidden.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The per-device hide branch was gated on owned!==false (my own calendars) instead
of owned===false (shared with me), so unchecking a shared calendar hit the
owner-only enabled PUT and 404'd. Swap to owned===false.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bug 1 — a calendar shared with me stayed visible after unchecking it: the hide
was a one-shot cache filter the server undid on refetch. Add a per-device
hidden set (localStorage 'hiddenLocalCalendars'), honoured in filterEvents
(normal view) and used to drive the checkbox state, so it survives refetch/reload.
Bug 2 — in the group combined view, other members' events showed edit/delete and
403'd on save. The combined endpoint now emits read_only (editable = the group
calendar OR my own events), via a read_only param threaded through
build_local_event_dict/expand_recurring_local. The event popup and edit modal now
treat read_only events as read-only (copy still allowed). Test added.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Shared calendars belong to someone else, so a server colour PUT would 403.
Instead store the recipient's chosen colour locally (localStorage 'sharedCalColors',
keyed by calendar id) and re-apply it to events on every fetch. The colour dot
on a shared calendar now opens the picker instead of showing an error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Sidebar: don't start an inline rename on a calendar I don't own (owned=false)
— the save would only 403. Colour picker was already gated.
- Sidebar: show a struck-through pencil icon on read-only shared calendars.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Backend: co-member group_visible calendars now surface in /local/calendars
(owned=false, shared_by=owner, read-only, group_shared) and in the normal
/caldav/events merge (via readable_local_calendar_ids), deduped against
direct shares / group calendars so nothing appears twice.
- Backend: new User.directory_hidden — a user can hide from sharing/group
pickers (/users/directory), while admin user management (/users/) still lists
them. Migration + profile GET/PUT.
- Backend: /groups/{id} members carry shares_calendar so clients can drop
phantom rows for members who share nothing.
- Frontend: reachable "Teilen" button on owned local calendars; share modal is
now a checkbox multi-select of users (checked = shared). Hidden-profile toggle
in Settings → Profile. Group member filter only lists members who actually
share (phantom fix). Calendars shared with me moved to a dedicated read-only
"shared with me" section in the manage table.
- Tests: group_visible propagation, no-share absence, dedup, directory_hidden.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A calendar shared with another person now appears to the recipient under the
OWNER's name (Guido's "Persönlich" shows as "Guido"), matching the iOS filter
sheet. The original calendar name moves to the sub-label / settings source
column so it stays identifiable. Sidebar list + settings table.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- dav_router: PUT now looks up the event within the authenticated calendar only
(local_events.uid is globally unique), so a CalDAV client can no longer
overwrite another user's/calendar's event; a cross-calendar UID clash returns
409 instead of a 500 from the UNIQUE constraint.
- ical_router: _fetch_ics validates the URL (http/https only), resolves the host
and rejects private/loopback/link-local/reserved targets, follows redirects
manually re-validating each hop, and caps the response size — closing an
authenticated SSRF into internal services / cloud metadata.
- profile_router: GET /profile/avatar/{user_id} now requires authentication.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Login names are stored lowercase and the web login already compares with
func.lower(); CalDAV Basic Auth used an exact match, so "Scarriffle" failed to
authenticate while "scarriffle" worked. Compare case-insensitively too.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The app-password UI was in the user-menu profile modal, but users manage CalDAV
in Settings, so it went unnoticed. Move the section into the Settings → Profile
panel (next to account/privacy) and drive it from openSettingsModal via a
top-level initAppPasswords().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CalDAV clients send only user+password over Basic Auth and can't provide a TOTP
code, so account passwords would bypass 2FA. Add revocable app passwords:
- models: AppPassword table (bcrypt hash, label, last_used); auto-created via
create_all
- profile_router: GET/POST/DELETE /profile/app-passwords (plaintext shown once)
- dav_router: Basic Auth accepts any app password; the account password is
accepted only when 2FA is disabled
- frontend: "App-Passwörter (CalDAV)" section in the profile modal (create/show-
once/copy/revoke) + i18n (de/en); login hint now says app password
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- dav_router: add Basic-Auth principal-discovery tree at /caldav/ (and
/.well-known/caldav) so clients can add a CalDAV account with server URL +
username + password; lists all published calendars. Token URL /dav/{token}/
still works without login. Handlers generalised over a base href.
- dav_util: derive the public origin from X-Forwarded-Proto/-Host (or
PUBLIC_BASE_URL) so published URLs are https, not internal http:8080.
- local_router: expose caldav_login_url alongside caldav_url.
- frontend/i18n: show both the no-login token URL and the login URL + hint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- app.js: reload once on SW controllerchange (guarded against loops / first
install) and poll reg.update() hourly for long-open tabs
- sw.js: bump cache to v24 so the new (network-first) worker replaces any stale
cache-first worker and cleans old caches on activate
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fresh commit so a git pull visibly advances the deployed version and the PWA
service worker refreshes its cached assets. No functional change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Backend:
- LocalCalendar gains caldav_published + dav_token + dav_ctag; LocalEvent gains
etag (migrations in main.py). bump_dav() refreshes ctag/etag on every local
event write (create/update/delete/import).
- local_router: PUT /calendars/{id} accepts caldav_published (mints/revokes
token), new POST /calendars/{id}/dav-token/rotate, _cal_dict exposes
caldav_published + caldav_url.
- New dav_router mounted at root (/dav/{token}/...): a minimal two-way CalDAV
server (OPTIONS/PROPFIND/REPORT/GET/PUT/DELETE) reusing ical_io build/parse,
ctag-based change detection. Secret token = auth, no login.
Frontend:
- Settings calendar table: per-local-calendar publish toggle + subscribe URL
with copy and token-rotate; i18n (de/en) and styling.
Note: reverse proxy must allow WebDAV methods (PROPFIND/REPORT/PUT/DELETE).
VALARM/reminders are not round-tripped via CalDAV.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The multi-day mini-calendar and short-event week-view fixes were already
committed but not visible because the PWA served cached assets. Bump the
version so clients pick up the new JS/CSS.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Mini calendar: mark every day an event spans (not just the start day) so
multi-day events (Urlaub/Ferien) show a dot across the whole range
- Week view: render short events on one line (time next to title) so the title
stays visible; drop the location line when there's no room
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The agenda lists the whole cached range, so clicking Today only reset scroll
to the top (earliest past event). Scroll the agenda to the current date (or the
next day with events) after rendering, so Today/mini-cal clicks land correctly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Don't start a header drag when the pointer goes down on a button/link, so
edit/copy/delete/close work again
- Make the popup body text selectable (user-select: text) for copying
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Render event descriptions as sanitized HTML (links/line breaks) instead of
raw escaped text; no script execution
- Reminder picker: presets + custom number+unit (minutes/hours/days/weeks)
- Grey out + hint the reminder editor when a calendar's notifications are off
(reminders are kept, just not fired)
- New synced setting default_event_duration_minutes (default 60) for new events
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>