From a6db3762a38d65bff207830796f2fbf0aa44f4bd Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Sat, 3 Jan 2026 16:54:23 -0300 Subject: [PATCH] portal: surface Vaultwarden status --- backend/atlas_portal/provisioning.py | 30 +++++++++++++- backend/atlas_portal/routes/account.py | 55 ++++++++++++++++++++++++- frontend/src/views/AccountView.vue | 56 ++++++++++++++++++++++++++ 3 files changed, 139 insertions(+), 2 deletions(-) diff --git a/backend/atlas_portal/provisioning.py b/backend/atlas_portal/provisioning.py index a6fb30f..f42018a 100644 --- a/backend/atlas_portal/provisioning.py +++ b/backend/atlas_portal/provisioning.py @@ -326,11 +326,39 @@ def provision_access_request(request_code: str) -> ProvisionResult: try: if not user_id: raise RuntimeError("missing user id") - result = invite_user(mailu_email or f"{username}@{settings.MAILU_DOMAIN}") + vaultwarden_email = mailu_email or f"{username}@{settings.MAILU_DOMAIN}" + try: + full = admin_client().get_user(user_id) + attrs = full.get("attributes") or {} + override = None + if isinstance(attrs, dict): + raw = attrs.get("vaultwarden_email") + if isinstance(raw, list): + for item in raw: + if isinstance(item, str) and item.strip(): + override = item.strip() + break + elif isinstance(raw, str) and raw.strip(): + override = raw.strip() + if override: + vaultwarden_email = override + except Exception: + pass + + result = invite_user(vaultwarden_email) if result.ok: _upsert_task(conn, request_code, "vaultwarden_invite", "ok", result.status) else: _upsert_task(conn, request_code, "vaultwarden_invite", "error", result.detail or result.status) + + # Persist Vaultwarden association/status on the Keycloak user so the portal can display it quickly. + try: + now_iso = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + admin_client().set_user_attribute(username, "vaultwarden_email", vaultwarden_email) + admin_client().set_user_attribute(username, "vaultwarden_status", result.status) + admin_client().set_user_attribute(username, "vaultwarden_synced_at", now_iso) + except Exception: + pass except Exception as exc: _upsert_task( conn, diff --git a/backend/atlas_portal/routes/account.py b/backend/atlas_portal/routes/account.py index 18fa3b0..516ef1f 100644 --- a/backend/atlas_portal/routes/account.py +++ b/backend/atlas_portal/routes/account.py @@ -40,6 +40,9 @@ def register(app) -> None: nextcloud_mail_primary_email = "" nextcloud_mail_account_count = "" nextcloud_mail_synced_at = "" + vaultwarden_email = "" + vaultwarden_status = "" + vaultwarden_synced_at = "" jellyfin_status = "ready" jellyfin_sync_status = "unknown" jellyfin_sync_detail = "" @@ -85,9 +88,31 @@ def register(app) -> None: nextcloud_mail_synced_at = str(raw_synced[0]) elif isinstance(raw_synced, str) and raw_synced: nextcloud_mail_synced_at = raw_synced + raw_vw_email = attrs.get("vaultwarden_email") + if isinstance(raw_vw_email, list) and raw_vw_email: + vaultwarden_email = str(raw_vw_email[0]) + elif isinstance(raw_vw_email, str) and raw_vw_email: + vaultwarden_email = raw_vw_email + raw_vw_status = attrs.get("vaultwarden_status") + if isinstance(raw_vw_status, list) and raw_vw_status: + vaultwarden_status = str(raw_vw_status[0]) + elif isinstance(raw_vw_status, str) and raw_vw_status: + vaultwarden_status = raw_vw_status + raw_vw_synced = attrs.get("vaultwarden_synced_at") + if isinstance(raw_vw_synced, list) and raw_vw_synced: + vaultwarden_synced_at = str(raw_vw_synced[0]) + elif isinstance(raw_vw_synced, str) and raw_vw_synced: + vaultwarden_synced_at = raw_vw_synced user_id = user.get("id") if isinstance(user, dict) else None - if user_id and (not keycloak_email or not mailu_email or not mailu_app_password): + if user_id and ( + not keycloak_email + or not mailu_email + or not mailu_app_password + or not vaultwarden_email + or not vaultwarden_status + or not vaultwarden_synced_at + ): full = admin_client().get_user(str(user_id)) if not keycloak_email: keycloak_email = str(full.get("email") or "") @@ -124,14 +149,34 @@ def register(app) -> None: nextcloud_mail_synced_at = str(raw_synced[0]) elif isinstance(raw_synced, str) and raw_synced: nextcloud_mail_synced_at = raw_synced + if not vaultwarden_email: + raw_vw_email = attrs.get("vaultwarden_email") + if isinstance(raw_vw_email, list) and raw_vw_email: + vaultwarden_email = str(raw_vw_email[0]) + elif isinstance(raw_vw_email, str) and raw_vw_email: + vaultwarden_email = raw_vw_email + if not vaultwarden_status: + raw_vw_status = attrs.get("vaultwarden_status") + if isinstance(raw_vw_status, list) and raw_vw_status: + vaultwarden_status = str(raw_vw_status[0]) + elif isinstance(raw_vw_status, str) and raw_vw_status: + vaultwarden_status = raw_vw_status + if not vaultwarden_synced_at: + raw_vw_synced = attrs.get("vaultwarden_synced_at") + if isinstance(raw_vw_synced, list) and raw_vw_synced: + vaultwarden_synced_at = str(raw_vw_synced[0]) + elif isinstance(raw_vw_synced, str) and raw_vw_synced: + vaultwarden_synced_at = raw_vw_synced except Exception: mailu_status = "unavailable" nextcloud_mail_status = "unavailable" + vaultwarden_status = "unavailable" jellyfin_status = "unavailable" jellyfin_sync_status = "unknown" jellyfin_sync_detail = "unavailable" mailu_username = mailu_email or (f"{username}@{settings.MAILU_DOMAIN}" if username else "") + vaultwarden_username = vaultwarden_email or mailu_username if not mailu_app_password and mailu_status == "ready": mailu_status = "needs app password" @@ -162,6 +207,9 @@ def register(app) -> None: jellyfin_sync_status = "ok" jellyfin_sync_detail = "LDAP-backed (Keycloak is source of truth)" + if not vaultwarden_status: + vaultwarden_status = "needs provisioning" + return jsonify( { "user": {"username": username, "email": keycloak_email, "groups": g.keycloak_groups}, @@ -172,6 +220,11 @@ def register(app) -> None: "account_count": nextcloud_mail_account_count, "synced_at": nextcloud_mail_synced_at, }, + "vaultwarden": { + "status": vaultwarden_status, + "username": vaultwarden_username, + "synced_at": vaultwarden_synced_at, + }, "jellyfin": { "status": jellyfin_status, "username": username, diff --git a/frontend/src/views/AccountView.vue b/frontend/src/views/AccountView.vue index 6fb5be3..b539e8b 100644 --- a/frontend/src/views/AccountView.vue +++ b/frontend/src/views/AccountView.vue @@ -123,6 +123,47 @@ +
+
+

Vaultwarden

+ + {{ vaultwarden.status }} + +
+

+ Password manager for Atlas accounts. Store your Element recovery key here. Signups are admin-provisioned. +

+
+
+ URL + vault.bstein.dev +
+
+ Username + {{ vaultwarden.username }} +
+
+ Synced + {{ vaultwarden.syncedAt || "never" }} +
+
+
+ Invitation created. Open Vaultwarden and complete setup by choosing a master password. +
+
+
{{ vaultwarden.error }}
+
+
+

Jellyfin

@@ -231,6 +272,13 @@ const jellyfin = reactive({ error: "", }); +const vaultwarden = reactive({ + status: "loading", + username: "", + syncedAt: "", + error: "", +}); + const nextcloudMail = reactive({ status: "loading", primaryEmail: "", @@ -260,6 +308,7 @@ onMounted(() => { mailu.status = "login required"; nextcloudMail.status = "login required"; jellyfin.status = "login required"; + vaultwarden.status = "login required"; } }); @@ -271,6 +320,7 @@ watch( mailu.status = "login required"; nextcloudMail.status = "login required"; jellyfin.status = "login required"; + vaultwarden.status = "login required"; admin.enabled = false; admin.requests = []; return; @@ -284,6 +334,7 @@ watch( async function refreshOverview() { mailu.error = ""; jellyfin.error = ""; + vaultwarden.error = ""; nextcloudMail.error = ""; try { const resp = await authFetch("/api/account/overview", { @@ -302,6 +353,9 @@ async function refreshOverview() { nextcloudMail.primaryEmail = data.nextcloud_mail?.primary_email || ""; nextcloudMail.accountCount = data.nextcloud_mail?.account_count || ""; nextcloudMail.syncedAt = data.nextcloud_mail?.synced_at || ""; + vaultwarden.status = data.vaultwarden?.status || "unknown"; + vaultwarden.username = data.vaultwarden?.username || auth.email || auth.username; + vaultwarden.syncedAt = data.vaultwarden?.synced_at || ""; jellyfin.status = data.jellyfin?.status || "ready"; jellyfin.username = data.jellyfin?.username || auth.username; jellyfin.syncStatus = data.jellyfin?.sync_status || ""; @@ -309,12 +363,14 @@ async function refreshOverview() { } catch (err) { mailu.status = "unavailable"; nextcloudMail.status = "unavailable"; + vaultwarden.status = "unavailable"; jellyfin.status = "unavailable"; jellyfin.syncStatus = ""; jellyfin.syncDetail = ""; const message = err?.message ? `Failed to load account status (${err.message})` : "Failed to load account status."; mailu.error = message; nextcloudMail.error = message; + vaultwarden.error = message; jellyfin.error = message; } }