portal: surface Vaultwarden status

This commit is contained in:
Brad Stein 2026-01-03 16:54:23 -03:00
parent 7902d7658f
commit a6db3762a3
3 changed files with 139 additions and 2 deletions

View File

@ -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,

View File

@ -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,

View File

@ -123,6 +123,47 @@
</div>
</div>
<div class="card module">
<div class="module-head">
<h2>Vaultwarden</h2>
<span
class="pill mono"
:class="
vaultwarden.status === 'ready' || vaultwarden.status === 'already_present'
? 'pill-ok'
: vaultwarden.status === 'unavailable' || vaultwarden.status === 'error'
? 'pill-bad'
: ''
"
>
{{ vaultwarden.status }}
</span>
</div>
<p class="muted">
Password manager for Atlas accounts. Store your Element recovery key here. Signups are admin-provisioned.
</p>
<div class="kv">
<div class="row">
<span class="k mono">URL</span>
<a class="v mono link" href="https://vault.bstein.dev" target="_blank" rel="noreferrer">vault.bstein.dev</a>
</div>
<div class="row">
<span class="k mono">Username</span>
<span class="v mono">{{ vaultwarden.username }}</span>
</div>
<div class="row">
<span class="k mono">Synced</span>
<span class="v mono">{{ vaultwarden.syncedAt || "never" }}</span>
</div>
</div>
<div v-if="vaultwarden.status === 'invited'" class="hint mono">
Invitation created. Open Vaultwarden and complete setup by choosing a master password.
</div>
<div v-if="vaultwarden.error" class="error-box">
<div class="mono">{{ vaultwarden.error }}</div>
</div>
</div>
<div class="card module">
<div class="module-head">
<h2>Jellyfin</h2>
@ -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;
}
}