Settings: add dedicated DAV/Pushover tabs, fix CalDAV/CardDAV bugs

- Add admin DAV tab (rename from CalDAV/CardDAV) and Pushover tab
  - Add per-user Pushover tab (User Key only; App Token stays admin-managed)
  - Remove system-wide CalDAV/CardDAV fallback — per-user config only
  - Rewrite contacts_tool.py using httpx directly (caldav 2.x dropped AddressBook)
  - Fix CardDAV REPORT/PROPFIND using SOGo URL pattern
  - Fix CalDAV/CardDAV test endpoints (POST method, URL scheme normalization)
  - Fix Show Password button — API now returns actual credential values
  - Convert Credentials tab to generic key-value store; dedicated keys
    (CalDAV, Pushover, trusted_proxy) excluded via _DEDICATED_CRED_KEYS
This commit is contained in:
2026-04-10 12:06:23 +02:00
parent a9ca08f13d
commit 7b0a9ccc2b
25 changed files with 4011 additions and 235 deletions

View File

@@ -37,32 +37,21 @@ logger = logging.getLogger(__name__)
async def _get_caldav_config(user_id: str | None = None) -> dict:
"""
Two-layer CalDAV config lookup: user_settings → credential_store (global fallback).
Per-user CalDAV config lookup — no system-wide fallback.
Keys in user_settings: caldav_url, caldav_username, caldav_password, caldav_calendar_name
Keys in credential_store: mailcow_host, mailcow_username, mailcow_password, caldav_calendar_name
Returns a dict with url, username, password, calendar_name (any may be None).
Each user (including admin) configures their own CalDAV credentials in Settings → CalDAV.
Returns a dict with url, username, password, calendar_name (any may be None/empty).
"""
if user_id:
from ..database import user_settings_store
url = await user_settings_store.get(user_id, "caldav_url")
if url:
return {
"url": url,
"username": await user_settings_store.get(user_id, "caldav_username"),
"password": await user_settings_store.get(user_id, "caldav_password"),
"calendar_name": await user_settings_store.get(user_id, "caldav_calendar_name"),
}
return {
"url": await user_settings_store.get(user_id, "caldav_url"),
"username": await user_settings_store.get(user_id, "caldav_username"),
"password": await user_settings_store.get(user_id, "caldav_password"),
"calendar_name": await user_settings_store.get(user_id, "caldav_calendar_name"),
}
# Fall back to global credential_store
host = await credential_store.get("mailcow_host")
return {
"url": f"https://{host}/SOGo/dav/" if host else None,
"username": await credential_store.get("mailcow_username"),
"password": await credential_store.get("mailcow_password"),
"calendar_name": await credential_store.get("caldav_calendar_name"),
}
return {"url": None, "username": None, "password": None, "calendar_name": None}
MAX_EVENTS = 100
@@ -135,11 +124,15 @@ class CalDAVTool(BaseTool):
if not url or not username or not password:
raise RuntimeError(
"CalDAV credentials not configured. "
"Set them in Settings → My Settings → CalDAV, or ask the admin to configure global CalDAV."
"Set them in Settings → CalDAV / CardDAV."
)
# Normalise scheme — users often enter just the hostname
if not url.startswith(("http://", "https://")):
url = "https://" + url
# Build principal URL: if the stored URL is already the full principal URL use it directly;
# otherwise append the SOGo-style path (backward compat with old mailcow_host keys).
# otherwise append the SOGo-style path.
if "/SOGo/dav/" in url or url.rstrip("/").endswith(username):
principal_url = url.rstrip("/") + "/"
else: