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 @@ +
+ Password manager for Atlas accounts. Store your Element recovery key here. Signups are admin-provisioned. +
+