diff --git a/backend/atlas_portal/routes/account.py b/backend/atlas_portal/routes/account.py index 99c5c4a..481e30f 100644 --- a/backend/atlas_portal/routes/account.py +++ b/backend/atlas_portal/routes/account.py @@ -255,6 +255,53 @@ def register(app) -> None: jellyfin_sync_status = "unknown" jellyfin_sync_detail = "unavailable" + if ( + username + and not vaultwarden_master_set_at + and vaultwarden_status in {"", "invited", "needs provisioning"} + and settings.PORTAL_DATABASE_URL + ): + try: + with connect() as conn: + row = conn.execute( + """ + SELECT request_code + FROM access_requests + WHERE username = %s AND status IN ('awaiting_onboarding', 'ready') + ORDER BY created_at DESC + LIMIT 1 + """, + (username,), + ).fetchone() + if not row: + row = conn.execute( + """ + SELECT request_code + FROM access_requests + WHERE username = %s + ORDER BY created_at DESC + LIMIT 1 + """, + (username,), + ).fetchone() + if row and isinstance(row, dict): + request_code = str(row.get("request_code") or "").strip() + if request_code: + step = conn.execute( + """ + SELECT 1 + FROM access_request_onboarding_steps + WHERE request_code = %s AND step = %s + LIMIT 1 + """, + (request_code, "vaultwarden_master_password"), + ).fetchone() + if step: + vaultwarden_master_set_at = "confirmed" + vaultwarden_status = "ready" + except Exception: + pass + mailu_username = mailu_email or (f"{username}@{settings.MAILU_DOMAIN}" if username else "") firefly_username = mailu_username vaultwarden_username = vaultwarden_email or mailu_username @@ -460,6 +507,16 @@ def register(app) -> None: return jsonify({"status": "ok", "password": password}) + @app.route("/api/account/wger/rotation/check", methods=["POST"]) + @require_auth + def account_wger_rotation_check() -> Any: + ok, resp = require_account_access() + if not ok: + return resp + if ariadne_client.enabled(): + return ariadne_client.proxy("POST", "/api/account/wger/rotation/check") + return jsonify({"error": "server not configured"}), 503 + @app.route("/api/account/firefly/reset", methods=["POST"]) @require_auth def account_firefly_reset() -> Any: @@ -513,6 +570,16 @@ def register(app) -> None: return jsonify({"status": "ok", "password": password}) + @app.route("/api/account/firefly/rotation/check", methods=["POST"]) + @require_auth + def account_firefly_rotation_check() -> Any: + ok, resp = require_account_access() + if not ok: + return resp + if ariadne_client.enabled(): + return ariadne_client.proxy("POST", "/api/account/firefly/rotation/check") + return jsonify({"error": "server not configured"}), 503 + @app.route("/api/account/nextcloud/mail/sync", methods=["POST"]) @require_auth def account_nextcloud_mail_sync() -> Any: diff --git a/frontend/src/views/AccountView.vue b/frontend/src/views/AccountView.vue index 9c10d58..2454f87 100644 --- a/frontend/src/views/AccountView.vue +++ b/frontend/src/views/AccountView.vue @@ -675,6 +675,16 @@ function formatName(req) { return parts.length ? parts.join(" ") : "unknown"; } +function formatActionError(err, fallback) { + const message = err?.message || ""; + if (!message) return fallback; + const normalized = message.toLowerCase(); + if (normalized.includes("ariadne unavailable") || normalized.includes("status 502") || normalized.includes("status 503")) { + return "Ariadne is busy. Please try again in a moment."; + } + return message; +} + function toggleFlag(username, flag, event) { const checked = Boolean(event?.target?.checked); const selected = Array.isArray(admin.selectedFlags[username]) ? [...admin.selectedFlags[username]] : []; @@ -709,7 +719,7 @@ async function rotateMailu() { } await refreshOverview(); } catch (err) { - mailu.error = err.message || "Rotation failed"; + mailu.error = formatActionError(err, "Rotation failed"); } finally { mailu.rotating = false; } @@ -728,7 +738,7 @@ async function resetWger() { } await refreshOverview(); } catch (err) { - wger.error = err.message || "Reset failed"; + wger.error = formatActionError(err, "Reset failed"); } finally { wger.resetting = false; } @@ -747,7 +757,7 @@ async function resetFirefly() { } await refreshOverview(); } catch (err) { - firefly.error = err.message || "Reset failed"; + firefly.error = formatActionError(err, "Reset failed"); } finally { firefly.resetting = false; } @@ -766,7 +776,7 @@ async function syncNextcloudMail() { if (!resp.ok) throw new Error(data.error || `status ${resp.status}`); await refreshOverview(); } catch (err) { - nextcloudMail.error = err.message || "Sync failed"; + nextcloudMail.error = formatActionError(err, "Sync failed"); } finally { nextcloudMail.syncing = false; } diff --git a/frontend/src/views/OnboardingView.vue b/frontend/src/views/OnboardingView.vue index 8bc83a8..646c29b 100644 --- a/frontend/src/views/OnboardingView.vue +++ b/frontend/src/views/OnboardingView.vue @@ -945,6 +945,12 @@ async function confirmStep(step) { return; } if (step.action === "auto") { + if (step.id === "firefly_password_rotated") { + await runRotationCheck("firefly"); + } + if (step.id === "wger_password_rotated") { + await runRotationCheck("wger"); + } await check(); return; } @@ -956,11 +962,29 @@ async function confirmStep(step) { return; } await setStepCompletion(step.id, true); + } catch (err) { + error.value = err?.message || "Failed to confirm step"; } finally { confirmingStepId.value = ""; } } +async function runRotationCheck(service) { + if (!auth.authenticated) { + throw new Error("Log in to update onboarding steps."); + } + const endpoint = + service === "firefly" + ? "/api/account/firefly/rotation/check" + : "/api/account/wger/rotation/check"; + const resp = await authFetch(endpoint, { method: "POST" }); + const data = await resp.json().catch(() => ({})); + if (!resp.ok) { + throw new Error(data.error || resp.statusText || `status ${resp.status}`); + } + return data; +} + async function requestKeycloakPasswordRotation() { if (!requestCode.value.trim()) { error.value = "Request code is missing.";