diff --git a/backend/atlas_portal/routes/access_requests.py b/backend/atlas_portal/routes/access_requests.py index 076b28e..a77563d 100644 --- a/backend/atlas_portal/routes/access_requests.py +++ b/backend/atlas_portal/routes/access_requests.py @@ -65,6 +65,16 @@ ONBOARDING_STEPS: tuple[str, ...] = ( KEYCLOAK_MANAGED_STEPS: set[str] = {"keycloak_password_changed"} +ONBOARDING_STEP_PREREQUISITES: dict[str, set[str]] = { + "vaultwarden_master_password": {"keycloak_password_changed"}, + "element_recovery_key": {"keycloak_password_changed", "vaultwarden_master_password"}, + "element_recovery_key_stored": { + "keycloak_password_changed", + "vaultwarden_master_password", + "element_recovery_key", + }, +} + def _normalize_status(status: str) -> str: cleaned = (status or "").strip().lower() @@ -562,6 +572,12 @@ def register(app) -> None: mark_done = completed if mark_done: + prerequisites = ONBOARDING_STEP_PREREQUISITES.get(step, set()) + if prerequisites: + current_completed = _completed_onboarding_steps(conn, code, username) + missing = sorted(prerequisites - current_completed) + if missing: + return jsonify({"error": "step is blocked", "blocked_by": missing}), 409 conn.execute( """ INSERT INTO access_request_onboarding_steps (request_code, step) diff --git a/frontend/src/views/OnboardingView.vue b/frontend/src/views/OnboardingView.vue index 6ee5107..9954e9b 100644 --- a/frontend/src/views/OnboardingView.vue +++ b/frontend/src/views/OnboardingView.vue @@ -140,10 +140,13 @@ Set a Vaultwarden master password + + {{ stepPillLabel("vaultwarden_master_password") }} +
Open Passwords and set a strong master @@ -156,10 +159,13 @@ Create an Element recovery key + + {{ stepPillLabel("element_recovery_key") }} +
In Element, create a recovery key so you can restore encrypted history if you lose a device. @@ -171,10 +177,13 @@ Store the recovery key in Vaultwarden + + {{ stepPillLabel("element_recovery_key_stored") }} +
Save the recovery key in Vaultwarden so it doesn't get lost.
@@ -247,6 +256,33 @@ function isStepDone(step) { return Array.isArray(steps) ? steps.includes(step) : false; } +function isStepBlocked(step) { + const order = [ + "keycloak_password_changed", + "vaultwarden_master_password", + "element_recovery_key", + "element_recovery_key_stored", + ]; + const idx = order.indexOf(step); + if (idx <= 0) return false; + for (let i = 0; i < idx; i += 1) { + if (!isStepDone(order[i])) return true; + } + return false; +} + +function stepPillLabel(step) { + if (isStepDone(step)) return "done"; + if (isStepBlocked(step)) return "blocked"; + return "pending"; +} + +function stepPillClass(step) { + if (isStepDone(step)) return "pill-ok"; + if (isStepBlocked(step)) return "pill-wait"; + return "pill-warn"; +} + function taskPillClass(status) { const key = (status || "").trim(); if (key === "ok") return "pill-ok";