portal: tighten onboarding confirmation flow

This commit is contained in:
Brad Stein 2026-01-22 23:59:02 -03:00
parent 87c3cb35ab
commit 8677efaa94

View File

@ -187,75 +187,6 @@
</a>
</div>
<div v-if="step.action === 'auto' || step.action === 'confirm'" class="step-actions">
<button
class="secondary"
type="button"
@click="confirmStep(step)"
:disabled="loading || isStepDone(step.id) || isStepBlocked(step.id)"
>
Confirm
</button>
</div>
<div v-if="step.action === 'keycloak_rotate'" class="step-actions">
<button
class="secondary"
type="button"
@click="requestKeycloakPasswordRotation"
:disabled="
!auth.authenticated ||
loading ||
isStepDone('keycloak_password_rotated') ||
isStepBlocked('keycloak_password_rotated') ||
keycloakPasswordRotationRequested
"
>
Start Keycloak update
</button>
<a class="mono" href="https://live.bstein.dev" target="_blank" rel="noreferrer">Open Element</a>
<button
class="secondary"
type="button"
@click="confirmStep(step)"
:disabled="loading || isStepDone(step.id) || isStepBlocked(step.id)"
>
Confirm
</button>
</div>
<div v-if="step.action === 'element_recovery'" class="recovery-verify">
<input
v-model="elementRecoveryKey"
class="input mono"
type="text"
placeholder="Paste recovery key (hashed locally)"
:disabled="!auth.authenticated || loading || isStepDone(step.id) || isStepBlocked(step.id)"
/>
<button
class="primary verify"
type="button"
@click="verifyElementRecoveryKey"
:disabled="
!auth.authenticated ||
loading ||
isStepDone(step.id) ||
isStepBlocked(step.id) ||
!elementRecoveryKey.trim()
"
>
Verify
</button>
<button
class="secondary"
type="button"
@click="confirmStep(step)"
:disabled="loading || isStepDone(step.id) || isStepBlocked(step.id) || !elementRecoveryKey.trim()"
>
Confirm
</button>
</div>
<details v-if="step.guide" class="guide-details">
<summary class="mono">Photo guide</summary>
<div v-if="guideGroups(step).length" class="guide-groups">
@ -302,14 +233,63 @@
<p v-else class="muted">Guide coming soon.</p>
</details>
<div v-if="step.action === 'checkbox'" class="step-actions">
<div v-if="step.action === 'element_recovery'" class="recovery-verify">
<input
v-model="elementRecoveryKey"
class="input mono"
type="text"
placeholder="Paste recovery key (hashed locally)"
:disabled="!auth.authenticated || loading || isStepDone(step.id) || isStepBlocked(step.id)"
/>
<button
class="primary verify"
type="button"
@click="verifyElementRecoveryKey"
:disabled="
!auth.authenticated ||
loading ||
isStepDone(step.id) ||
isStepBlocked(step.id) ||
!elementRecoveryKey.trim()
"
>
Verify
</button>
<button
class="secondary"
type="button"
@click="confirmStep(step)"
:disabled="loading || isStepDone(step.id) || isStepBlocked(step.id)"
:disabled="loading || isStepDone(step.id) || isStepBlocked(step.id) || !elementRecoveryKey.trim()"
>
Confirm
{{ confirmLabel(step) }}
</button>
</div>
<div v-else class="step-actions">
<template v-if="step.action === 'keycloak_rotate'">
<button
class="secondary"
type="button"
@click="requestKeycloakPasswordRotation"
:disabled="
!auth.authenticated ||
loading ||
isStepDone('keycloak_password_rotated') ||
isStepBlocked('keycloak_password_rotated') ||
keycloakPasswordRotationRequested
"
>
Start Keycloak update
</button>
<a class="mono" href="https://live.bstein.dev" target="_blank" rel="noreferrer">Open Element</a>
</template>
<button
class="secondary"
type="button"
@click="confirmStep(step)"
:disabled="loading || isConfirming(step) || isStepDone(step.id) || isStepBlocked(step.id)"
>
{{ confirmLabel(step) }}
</button>
</div>
</article>
@ -387,6 +367,7 @@ const activeSectionId = ref("vaultwarden");
const guideShots = ref({});
const guidePage = ref({});
const lightboxShot = ref(null);
const confirmingStepId = ref("");
const showPasswordCard = computed(() => Boolean(initialPassword.value || initialPasswordRevealedAt.value));
const passwordRevealLocked = computed(() => Boolean(!initialPassword.value && initialPasswordRevealedAt.value));
@ -759,6 +740,14 @@ function stepPillClass(step) {
return "pill-warn";
}
function isConfirming(step) {
return confirmingStepId.value === step.id;
}
function confirmLabel(step) {
return isConfirming(step) ? "Confirming..." : "Confirm";
}
function stepCardClass(step) {
return {
done: isStepDone(step.id),
@ -986,19 +975,31 @@ async function setStepCompletion(stepId, completed) {
async function confirmStep(step) {
if (!step || isStepBlocked(step.id) || isStepDone(step.id)) return;
confirmingStepId.value = step.id;
try {
if (step.action === "auto") {
await check();
return;
}
if (step.action === "keycloak_rotate") {
await requestKeycloakPasswordRotation();
await check();
return;
}
if (step.action === "element_recovery") {
await verifyElementRecoveryKey();
return;
}
if (step.action === "confirm") {
await check();
if (!isStepDone(step.id)) {
await setStepCompletion(step.id, true);
}
return;
}
await setStepCompletion(step.id, true);
} finally {
confirmingStepId.value = "";
}
}
async function verifyElementRecoveryKey() {
@ -1454,6 +1455,7 @@ button.copy:disabled {
.step-card.blocked {
opacity: 0.55;
pointer-events: none;
}
.step-card.done {