portal: enforce onboarding step prerequisites

This commit is contained in:
Brad Stein 2026-01-04 12:30:30 -03:00
parent 5f8ef42d79
commit a63bb3b048
2 changed files with 55 additions and 3 deletions

View File

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

View File

@ -140,10 +140,13 @@
<input
type="checkbox"
:checked="isStepDone('vaultwarden_master_password')"
:disabled="!auth.authenticated || loading"
:disabled="!auth.authenticated || loading || isStepBlocked('vaultwarden_master_password')"
@change="toggleStep('vaultwarden_master_password', $event)"
/>
<span>Set a Vaultwarden master password</span>
<span class="pill mono auto-pill" :class="stepPillClass('vaultwarden_master_password')">
{{ stepPillLabel("vaultwarden_master_password") }}
</span>
</label>
<p class="muted">
Open <a href="https://vault.bstein.dev" target="_blank" rel="noreferrer">Passwords</a> and set a strong master
@ -156,10 +159,13 @@
<input
type="checkbox"
:checked="isStepDone('element_recovery_key')"
:disabled="!auth.authenticated || loading"
:disabled="!auth.authenticated || loading || isStepBlocked('element_recovery_key')"
@change="toggleStep('element_recovery_key', $event)"
/>
<span>Create an Element recovery key</span>
<span class="pill mono auto-pill" :class="stepPillClass('element_recovery_key')">
{{ stepPillLabel("element_recovery_key") }}
</span>
</label>
<p class="muted">
In Element, create a recovery key so you can restore encrypted history if you lose a device.
@ -171,10 +177,13 @@
<input
type="checkbox"
:checked="isStepDone('element_recovery_key_stored')"
:disabled="!auth.authenticated || loading"
:disabled="!auth.authenticated || loading || isStepBlocked('element_recovery_key_stored')"
@change="toggleStep('element_recovery_key_stored', $event)"
/>
<span>Store the recovery key in Vaultwarden</span>
<span class="pill mono auto-pill" :class="stepPillClass('element_recovery_key_stored')">
{{ stepPillLabel("element_recovery_key_stored") }}
</span>
</label>
<p class="muted">Save the recovery key in Vaultwarden so it doesn't get lost.</p>
</li>
@ -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";