diff --git a/backend/atlas_portal/routes/access_requests.py b/backend/atlas_portal/routes/access_requests.py index edd83e5..c55908e 100644 --- a/backend/atlas_portal/routes/access_requests.py +++ b/backend/atlas_portal/routes/access_requests.py @@ -1147,6 +1147,13 @@ def register(app) -> None: missing = sorted(prerequisites - current_completed) if missing: return jsonify({"error": "step is blocked", "blocked_by": missing}), 409 + if step in {"vaultwarden_master_password", "vaultwarden_store_temp_password"}: + if not _password_rotation_requested(conn, code): + try: + _request_keycloak_password_rotation(conn, code, request_username) + except Exception: + return jsonify({"error": "failed to request keycloak password rotation"}), 502 + if step == "vaultwarden_master_password": if vaultwarden_claim and not username: return jsonify({"error": "login required"}), 401 @@ -1159,10 +1166,6 @@ def register(app) -> None: return jsonify({"error": "vaultwarden claim not allowed"}), 403 if vaultwarden_claim and not admin_client().ready(): return jsonify({"error": "keycloak admin unavailable"}), 503 - try: - _request_keycloak_password_rotation(conn, code, request_username) - except Exception: - return jsonify({"error": "failed to request keycloak password rotation"}), 502 if request_username and admin_client().ready(): try: now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") diff --git a/frontend/src/assets/base.css b/frontend/src/assets/base.css index 8e681fe..0246652 100644 --- a/frontend/src/assets/base.css +++ b/frontend/src/assets/base.css @@ -52,3 +52,9 @@ p { .page > section + section { margin-top: 32px; } + +@media (max-width: 720px) { + .page { + padding: 24px 16px 56px; + } +} diff --git a/frontend/src/views/AccountView.vue b/frontend/src/views/AccountView.vue index 17e2ff8..2ab1089 100644 --- a/frontend/src/views/AccountView.vue +++ b/frontend/src/views/AccountView.vue @@ -498,6 +498,7 @@ const vaultwardenOrder = computed(() => (vaultwardenReady.value ? 3 : 0)); const doLogin = () => login("/account"); const copied = reactive({}); +const normalizeEmail = (value) => (typeof value === "string" ? value.toLowerCase() : ""); onMounted(() => { if (auth.ready && auth.authenticated) { refreshOverview(); @@ -555,22 +556,22 @@ async function refreshOverview() { } const data = await resp.json(); mailu.status = data.mailu?.status || "ready"; - mailu.username = data.mailu?.username || auth.email || auth.username; + mailu.username = normalizeEmail(data.mailu?.username) || normalizeEmail(auth.email) || auth.username; mailu.currentPassword = data.mailu?.app_password || ""; nextcloudMail.status = data.nextcloud_mail?.status || "unknown"; - nextcloudMail.primaryEmail = data.nextcloud_mail?.primary_email || ""; + nextcloudMail.primaryEmail = normalizeEmail(data.nextcloud_mail?.primary_email) || ""; nextcloudMail.accountCount = data.nextcloud_mail?.account_count || ""; nextcloudMail.syncedAt = data.nextcloud_mail?.synced_at || ""; wger.status = data.wger?.status || "unknown"; - wger.username = data.wger?.username || mailu.username || auth.username; + wger.username = normalizeEmail(data.wger?.username) || mailu.username || auth.username; wger.password = data.wger?.password || ""; wger.passwordUpdatedAt = data.wger?.password_updated_at || ""; firefly.status = data.firefly?.status || "unknown"; - firefly.username = data.firefly?.username || mailu.username || auth.username; + firefly.username = normalizeEmail(data.firefly?.username) || mailu.username || auth.username; firefly.password = data.firefly?.password || ""; firefly.passwordUpdatedAt = data.firefly?.password_updated_at || ""; vaultwarden.status = data.vaultwarden?.status || "unknown"; - vaultwarden.username = data.vaultwarden?.username || mailu.username || auth.username; + vaultwarden.username = normalizeEmail(data.vaultwarden?.username) || mailu.username || auth.username; vaultwarden.syncedAt = data.vaultwarden?.synced_at || ""; jellyfin.status = data.jellyfin?.status || "ready"; jellyfin.username = data.jellyfin?.username || auth.username; @@ -778,7 +779,12 @@ async function syncNextcloudMail() { if (!resp.ok) throw new Error(data.error || `status ${resp.status}`); await refreshOverview(); } catch (err) { - nextcloudMail.error = formatActionError(err, "Sync failed"); + const message = formatActionError(err, "Sync failed"); + if (message.toLowerCase().includes("ariadne is busy")) { + nextcloudMail.error = "Ariadne is busy. Refresh in a moment; the sync may have completed."; + } else { + nextcloudMail.error = message; + } } finally { nextcloudMail.syncing = false; } @@ -1090,6 +1096,42 @@ button.primary { } } +@media (max-width: 720px) { + .page { + padding: 24px 16px 56px; + } + + .hero { + flex-direction: column; + align-items: flex-start; + } + + .hero-actions { + width: 100%; + justify-content: flex-start; + gap: 12px; + } + + .row { + flex-direction: column; + align-items: flex-start; + } + + .actions { + flex-direction: column; + align-items: stretch; + } + + .secret-head { + flex-direction: column; + align-items: flex-start; + } + + .req-row { + grid-template-columns: 1fr; + } +} + .admin { margin-top: 12px; } diff --git a/frontend/src/views/OnboardingView.vue b/frontend/src/views/OnboardingView.vue index 362d0b6..711852c 100644 --- a/frontend/src/views/OnboardingView.vue +++ b/frontend/src/views/OnboardingView.vue @@ -116,6 +116,10 @@
Use these to sign in to Nextcloud, Element, and any Keycloak-protected services.
+