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

@@ -42,6 +42,8 @@ from .context_vars import current_user as _current_user_var
from .database import close_db, credential_store, init_db
from .inbox.listener import inbox_listener
from .mcp import create_mcp_app, _session_manager
from .monitors.page_monitor import page_monitor
from .monitors.rss_monitor import rss_monitor
from .telegram.listener import telegram_listener
from .tools import build_registry
from .users import assign_existing_data_to_admin, create_user, get_user_by_username, user_count
@@ -190,6 +192,11 @@ async def lifespan(app: FastAPI):
print("[aide] Agent ready.")
agent_runner.init(_agent)
await agent_runner.start()
# Wire monitors into the shared scheduler after it starts
page_monitor.init(agent_runner.scheduler)
rss_monitor.init(agent_runner.scheduler)
await page_monitor.start_all()
await rss_monitor.start_all()
await _migrate_email_accounts()
await inbox_listener.start_all()
telegram_listener.start()
@@ -260,7 +267,7 @@ import time as _time
_USER_COOKIE = "aide_user"
_EXEMPT_PATHS = frozenset({"/login", "/login/mfa", "/logout", "/setup", "/health"})
_EXEMPT_PREFIXES = ("/static/", "/brain-mcp/", "/docs", "/redoc", "/openapi.json")
_EXEMPT_PREFIXES = ("/static/", "/brain-mcp/", "/docs", "/redoc", "/openapi.json", "/webhook/")
_EXEMPT_API_PATHS = frozenset({"/api/settings/api-key"})
@@ -380,6 +387,87 @@ app.include_router(api_router, prefix="/api")
app.mount("/brain-mcp", create_mcp_app())
# ── Public webhook trigger endpoints ─────────────────────────────────────────
# These live outside /api and outside the auth middleware (token = auth).
_webhook_logger = logging.getLogger("server.webhook")
async def _handle_webhook_trigger(token: str, message: str, wait: bool = False) -> JSONResponse:
"""Shared logic for GET and POST webhook triggers."""
from .webhooks.endpoints import get_by_token, record_trigger
from .security import sanitize_external_content
ep = await get_by_token(token)
if ep is None:
return JSONResponse({"error": "Not found"}, status_code=404)
agent_id = ep.get("agent_id")
if not agent_id:
return JSONResponse({"error": "No agent configured for this webhook"}, status_code=422)
message = (message or "").strip()
if not message:
message = "Webhook triggered"
message = await sanitize_external_content(message, source="webhook")
from .audit import audit_log
ep_id = str(ep["id"])
session_id = f"webhook:{ep_id}"
await audit_log.record(
tool_name="webhook",
arguments={"endpoint": ep.get("name"), "message": message[:200]},
session_id=session_id,
)
if wait:
result = await agent_runner.run_agent_and_wait(
agent_id=agent_id,
override_message=message,
session_id=session_id,
)
await record_trigger(ep_id)
return JSONResponse({"ok": True, "result": result})
else:
run = await agent_runner.run_agent_now(agent_id=agent_id, override_message=message)
await record_trigger(ep_id)
run_id = run.get("id") or run.get("error", "error")
return JSONResponse({"ok": True, "run_id": run_id})
@app.get("/webhook/{token}")
async def webhook_trigger_get(token: str, q: str = "", wait: bool = False):
"""iOS Shortcuts / simple GET trigger. Message via ?q= query param."""
ep = await _get_webhook_endpoint_for_get(token)
if ep is None:
return JSONResponse({"error": "Not found"}, status_code=404)
return await _handle_webhook_trigger(token, q, wait=wait)
@app.post("/webhook/{token}")
async def webhook_trigger_post(token: str, request: Request):
"""External service POST trigger. Message via JSON body {"message": "..."}."""
try:
body = await request.json()
message = body.get("message", "")
wait = bool(body.get("wait", False))
except Exception:
message = ""
wait = False
return await _handle_webhook_trigger(token, message, wait=wait)
async def _get_webhook_endpoint_for_get(token: str) -> dict | None:
"""Return endpoint only if allow_get is True."""
from .webhooks.endpoints import get_by_token
ep = await get_by_token(token)
if ep and not ep.get("allow_get", True):
return None
return ep
# ── Auth helpers ──────────────────────────────────────────────────────────────
@@ -682,6 +770,11 @@ async def audit_page(request: Request):
return templates.TemplateResponse("audit.html", await _ctx(request))
@app.get("/monitors", response_class=HTMLResponse)
async def monitors_page(request: Request):
return templates.TemplateResponse("monitors.html", await _ctx(request))
@app.get("/help", response_class=HTMLResponse)
async def help_page(request: Request):
return templates.TemplateResponse("help.html", await _ctx(request))