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

Keycloak temporary credentials

+

Use these to sign in to Nextcloud, Element, and any Keycloak-protected services.

+
Username @@ -207,7 +211,7 @@
- Photo guide + Photo guide

{{ group.title }}

@@ -358,7 +362,9 @@ const vaultwardenLoginEmail = computed(() => { } return "your @bstein.dev address"; }); +const vaultwardenLoginEmailLower = computed(() => (vaultwardenLoginEmail.value || "").toLowerCase()); const mailAddress = computed(() => (requestUsername.value ? `${requestUsername.value}@bstein.dev` : "your @bstein.dev address")); +const mailAddressLower = computed(() => (mailAddress.value || "").toLowerCase()); const STEP_PREREQS = { vaultwarden_master_password: [], @@ -750,16 +756,16 @@ function isStepBlocked(stepId) { function stepNote(step) { if (step.id === "vaultwarden_master_password") { - return `Vaultwarden uses an email login. Use ${vaultwardenLoginEmail.value} to sign in.`; + return `Vaultwarden uses an email login. Use ${vaultwardenLoginEmailLower.value} to sign in.`; } if (step.id === "vaultwarden_store_temp_password") { return "Store the temporary Keycloak password in Vaultwarden so you can rotate it safely later."; } if (step.id === "firefly_password_rotated") { - return `Firefly uses an email login. Use ${mailAddress.value} to sign in.`; + return `Firefly uses an email login. Use ${mailAddressLower.value} to sign in.`; } if (step.id === "mail_client_setup") { - return `Your mailbox address is ${mailAddress.value}.`; + return `Your mailbox address is ${mailAddressLower.value}.`; } return ""; } @@ -1479,6 +1485,15 @@ button.copy:disabled { background: rgba(0, 0, 0, 0.2); } +.credential-head { + margin-bottom: 10px; +} + +.credential-head h4 { + margin: 0 0 4px; + font-size: 18px; +} + .credential-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); @@ -1647,6 +1662,33 @@ button.copy:disabled { margin-top: 10px; } +.guide-summary { + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 10px; + padding: 6px 12px; + border-radius: 999px; + border: 1px solid rgba(92, 214, 167, 0.5); + background: rgba(92, 214, 167, 0.15); + color: var(--text-strong); + font-weight: 600; +} + +.guide-summary::after { + content: "Tap to open"; + font-size: 12px; + color: var(--text-muted); +} + +.guide-details[open] .guide-summary::after { + content: "Tap to close"; +} + +.guide-summary::-webkit-details-marker { + display: none; +} + .guide-groups { display: grid; gap: 12px; @@ -1806,10 +1848,41 @@ button.copy:disabled { } @media (max-width: 720px) { + .page { + padding: 24px 16px 56px; + } + .status-form { flex-direction: column; } + .onboarding-head { + flex-direction: column; + align-items: flex-start; + } + + .credential-grid { + grid-template-columns: 1fr; + } + + .step-head { + flex-direction: column; + align-items: flex-start; + } + + .auto-pill { + margin-left: 0; + } + + .section-actions { + flex-direction: column; + align-items: stretch; + } + + .step-actions { + justify-content: flex-start; + } + .section-actions { width: 100%; justify-content: flex-start; diff --git a/media/onboarding/firefly/step2_mobile_app/9_Toggle on biometric access.jpg b/media/onboarding/firefly/step2_mobile_app/10_Toggle on biometric access.jpg similarity index 100% rename from media/onboarding/firefly/step2_mobile_app/9_Toggle on biometric access.jpg rename to media/onboarding/firefly/step2_mobile_app/10_Toggle on biometric access.jpg diff --git a/media/onboarding/firefly/step2_mobile_app/7_Authorize Access.jpg b/media/onboarding/firefly/step2_mobile_app/8_Authorize Access.jpg similarity index 100% rename from media/onboarding/firefly/step2_mobile_app/7_Authorize Access.jpg rename to media/onboarding/firefly/step2_mobile_app/8_Authorize Access.jpg diff --git a/media/onboarding/firefly/step2_mobile_app/8_You are in but lets give it thumbprint access.jpg b/media/onboarding/firefly/step2_mobile_app/9_You are in but lets give it thumbprint access.jpg similarity index 100% rename from media/onboarding/firefly/step2_mobile_app/8_You are in but lets give it thumbprint access.jpg rename to media/onboarding/firefly/step2_mobile_app/9_You are in but lets give it thumbprint access.jpg