onboarding: keep temp password notice and paginate guides
@ -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,),
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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;
|
||||
|
||||
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 294 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 143 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 161 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 50 KiB |