onboarding: keep temp password notice and paginate guides

This commit is contained in:
Brad Stein 2026-01-22 22:03:09 -03:00
parent 994146a99a
commit 3d6a373f3f
40 changed files with 128 additions and 15 deletions

View File

@ -858,12 +858,14 @@ def register(app) -> None:
response["tasks"] = tasks
response["automation_complete"] = provision_tasks_complete(conn, code)
response["blocked"] = blocked
if status in {"awaiting_onboarding", "ready"} and reveal_initial_password:
password = row.get("initial_password")
if status in {"awaiting_onboarding", "ready"}:
revealed_at = row.get("initial_password_revealed_at")
if isinstance(password, str) and password:
response["initial_password"] = password
if revealed_at is None:
if isinstance(revealed_at, datetime):
response["initial_password_revealed_at"] = revealed_at.astimezone(timezone.utc).isoformat()
if reveal_initial_password:
password = row.get("initial_password")
if isinstance(password, str) and password and revealed_at is None:
response["initial_password"] = password
conn.execute(
"UPDATE access_requests SET initial_password_revealed_at = NOW() WHERE request_code = %s AND initial_password_revealed_at IS NULL",
(code,),

View File

@ -1,6 +1,7 @@
import { promises as fs } from "fs";
import path from "path";
const SOURCE = path.resolve("..", "media", "onboarding");
const ROOT = path.resolve("public", "media", "onboarding");
const MANIFEST = path.join(ROOT, "manifest.json");
const EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".webp"]);
@ -27,7 +28,13 @@ async function ensureDir(dir) {
async function main() {
try {
await ensureDir(ROOT);
const files = await walk(ROOT).catch(() => []);
const files = await walk(SOURCE).catch(() => []);
for (const file of files) {
const src = path.join(SOURCE, file);
const dest = path.join(ROOT, file);
await ensureDir(path.dirname(dest));
await fs.copyFile(src, dest);
}
const payload = {
generated_at: new Date().toISOString(),
files: files.sort(),

View File

@ -125,22 +125,25 @@
</button>
</div>
<div class="credential-field" v-if="initialPassword">
<div class="credential-field" v-if="showPasswordCard">
<span class="label mono">Temporary password</span>
<div class="password-row">
<input
class="input mono"
:type="revealPassword ? 'text' : 'password'"
:value="initialPassword"
:value="initialPassword || '********'"
readonly
/>
<button class="secondary" type="button" @click="togglePassword">
<button class="secondary" type="button" @click="togglePassword" :disabled="!initialPassword">
{{ revealPassword ? "Hide" : "Reveal" }}
</button>
<button class="secondary" type="button" @click="copyInitialPassword">
<button class="secondary" type="button" @click="copyInitialPassword" :disabled="!initialPassword">
{{ passwordCopied ? "Copied" : "Copy" }}
</button>
</div>
<p v-if="passwordRevealLocked" class="muted">
This password was already revealed and cannot be shown again. Ask an admin to reset it if you missed it.
</p>
</div>
</div>
<p class="muted">
@ -204,6 +207,12 @@
</a>
</div>
<div v-if="step.action === 'auto'" class="step-actions">
<button class="secondary" type="button" @click="check" :disabled="loading">
Recheck status
</button>
</div>
<div v-if="step.action === 'keycloak_rotate'" class="step-actions">
<button
class="secondary"
@ -251,11 +260,41 @@
<div v-if="guideGroups(step).length" class="guide-groups">
<div v-for="group in guideGroups(step)" :key="group.id" class="guide-group">
<h4 v-if="group.title" class="mono guide-title">{{ group.title }}</h4>
<div class="guide-images">
<figure v-for="shot in group.shots" :key="shot.url" class="guide-shot">
<img :src="shot.url" :alt="shot.label || step.title" loading="lazy" />
<figcaption v-if="shot.label" class="mono">{{ shot.label }}</figcaption>
<div v-if="group.shots.length" class="guide-images">
<figure class="guide-shot">
<img :src="guideShot(step, group).url" :alt="guideShot(step, group).label || step.title" loading="lazy" />
<figcaption v-if="guideShot(step, group).label" class="mono">{{ guideShot(step, group).label }}</figcaption>
</figure>
<div v-if="group.shots.length > 1" class="guide-pagination">
<button
class="secondary"
type="button"
@click="guidePrev(step, group)"
:disabled="guideIndex(step, group) === 0"
>
Prev
</button>
<div class="guide-dots">
<button
v-for="(shot, index) in group.shots"
:key="shot.url"
class="guide-dot mono"
type="button"
:class="{ active: guideIndex(step, group) === index }"
@click="guideSet(step, group, index)"
>
{{ index + 1 }}
</button>
</div>
<button
class="secondary"
type="button"
@click="guideNext(step, group)"
:disabled="guideIndex(step, group) >= group.shots.length - 1"
>
Next
</button>
</div>
</div>
</div>
</div>
@ -300,6 +339,7 @@ const loading = ref(false);
const error = ref("");
const onboarding = ref({ required_steps: [], optional_steps: [], completed_steps: [] });
const initialPassword = ref("");
const initialPasswordRevealedAt = ref("");
const revealPassword = ref(false);
const passwordCopied = ref(false);
const usernameCopied = ref(false);
@ -309,6 +349,10 @@ const elementRecoveryKey = ref("");
const keycloakPasswordRotationRequested = ref(false);
const activeSectionId = ref("vaultwarden");
const guideShots = ref({});
const guidePage = ref({});
const showPasswordCard = computed(() => Boolean(initialPassword.value || initialPasswordRevealedAt.value));
const passwordRevealLocked = computed(() => Boolean(!initialPassword.value && initialPasswordRevealedAt.value));
const STEP_PREREQS = {
vaultwarden_master_password: [],
@ -738,6 +782,37 @@ function guideGroups(step) {
return Object.values(stepShots);
}
function guideKey(step, group) {
const service = step.guide?.service || "unknown";
const stepKey = step.guide?.step || "unknown";
return `${service}:${stepKey}:${group.id}`;
}
function guideIndex(step, group) {
const key = guideKey(step, group);
const index = guidePage.value[key] ?? 0;
const maxIndex = Math.max(group.shots.length - 1, 0);
return Math.min(Math.max(index, 0), maxIndex);
}
function guideSet(step, group, index) {
const key = guideKey(step, group);
const next = Math.min(Math.max(index, 0), group.shots.length - 1);
guidePage.value = { ...guidePage.value, [key]: next };
}
function guidePrev(step, group) {
guideSet(step, group, guideIndex(step, group) - 1);
}
function guideNext(step, group) {
guideSet(step, group, guideIndex(step, group) + 1);
}
function guideShot(step, group) {
return group.shots[guideIndex(step, group)] || {};
}
function taskPillClass(value) {
const key = (value || "").trim();
if (key === "ok") return "pill-ok";
@ -773,6 +848,7 @@ async function check() {
tasks.value = Array.isArray(data.tasks) ? data.tasks : [];
blocked.value = Boolean(data.blocked);
initialPassword.value = data.initial_password || "";
initialPasswordRevealedAt.value = data.initial_password_revealed_at || "";
if (showOnboarding.value) {
selectDefaultSection();
}
@ -1370,7 +1446,6 @@ button.secondary {
.guide-images {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 10px;
}
@ -1387,6 +1462,35 @@ button.secondary {
border-radius: 8px;
}
.guide-pagination {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
flex-wrap: wrap;
}
.guide-dots {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.guide-dot {
border: 1px solid rgba(255, 255, 255, 0.18);
background: rgba(0, 0, 0, 0.3);
color: var(--text-muted);
border-radius: 999px;
padding: 4px 8px;
cursor: pointer;
font-size: 12px;
}
.guide-dot.active {
border-color: rgba(120, 180, 255, 0.5);
color: var(--text-strong);
}
.ready-box {
margin-top: 18px;
padding: 14px;

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB