onboarding: keep temp password notice and paginate guides
@ -858,12 +858,14 @@ def register(app) -> None:
|
|||||||
response["tasks"] = tasks
|
response["tasks"] = tasks
|
||||||
response["automation_complete"] = provision_tasks_complete(conn, code)
|
response["automation_complete"] = provision_tasks_complete(conn, code)
|
||||||
response["blocked"] = blocked
|
response["blocked"] = blocked
|
||||||
if status in {"awaiting_onboarding", "ready"} and reveal_initial_password:
|
if status in {"awaiting_onboarding", "ready"}:
|
||||||
password = row.get("initial_password")
|
|
||||||
revealed_at = row.get("initial_password_revealed_at")
|
revealed_at = row.get("initial_password_revealed_at")
|
||||||
if isinstance(password, str) and password:
|
if isinstance(revealed_at, datetime):
|
||||||
response["initial_password"] = password
|
response["initial_password_revealed_at"] = revealed_at.astimezone(timezone.utc).isoformat()
|
||||||
if revealed_at is None:
|
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(
|
conn.execute(
|
||||||
"UPDATE access_requests SET initial_password_revealed_at = NOW() WHERE request_code = %s AND initial_password_revealed_at IS NULL",
|
"UPDATE access_requests SET initial_password_revealed_at = NOW() WHERE request_code = %s AND initial_password_revealed_at IS NULL",
|
||||||
(code,),
|
(code,),
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { promises as fs } from "fs";
|
import { promises as fs } from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
|
const SOURCE = path.resolve("..", "media", "onboarding");
|
||||||
const ROOT = path.resolve("public", "media", "onboarding");
|
const ROOT = path.resolve("public", "media", "onboarding");
|
||||||
const MANIFEST = path.join(ROOT, "manifest.json");
|
const MANIFEST = path.join(ROOT, "manifest.json");
|
||||||
const EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".webp"]);
|
const EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".webp"]);
|
||||||
@ -27,7 +28,13 @@ async function ensureDir(dir) {
|
|||||||
async function main() {
|
async function main() {
|
||||||
try {
|
try {
|
||||||
await ensureDir(ROOT);
|
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 = {
|
const payload = {
|
||||||
generated_at: new Date().toISOString(),
|
generated_at: new Date().toISOString(),
|
||||||
files: files.sort(),
|
files: files.sort(),
|
||||||
|
|||||||
@ -125,22 +125,25 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="credential-field" v-if="initialPassword">
|
<div class="credential-field" v-if="showPasswordCard">
|
||||||
<span class="label mono">Temporary password</span>
|
<span class="label mono">Temporary password</span>
|
||||||
<div class="password-row">
|
<div class="password-row">
|
||||||
<input
|
<input
|
||||||
class="input mono"
|
class="input mono"
|
||||||
:type="revealPassword ? 'text' : 'password'"
|
:type="revealPassword ? 'text' : 'password'"
|
||||||
:value="initialPassword"
|
:value="initialPassword || '********'"
|
||||||
readonly
|
readonly
|
||||||
/>
|
/>
|
||||||
<button class="secondary" type="button" @click="togglePassword">
|
<button class="secondary" type="button" @click="togglePassword" :disabled="!initialPassword">
|
||||||
{{ revealPassword ? "Hide" : "Reveal" }}
|
{{ revealPassword ? "Hide" : "Reveal" }}
|
||||||
</button>
|
</button>
|
||||||
<button class="secondary" type="button" @click="copyInitialPassword">
|
<button class="secondary" type="button" @click="copyInitialPassword" :disabled="!initialPassword">
|
||||||
{{ passwordCopied ? "Copied" : "Copy" }}
|
{{ passwordCopied ? "Copied" : "Copy" }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
<p class="muted">
|
<p class="muted">
|
||||||
@ -204,6 +207,12 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</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">
|
<div v-if="step.action === 'keycloak_rotate'" class="step-actions">
|
||||||
<button
|
<button
|
||||||
class="secondary"
|
class="secondary"
|
||||||
@ -251,11 +260,41 @@
|
|||||||
<div v-if="guideGroups(step).length" class="guide-groups">
|
<div v-if="guideGroups(step).length" class="guide-groups">
|
||||||
<div v-for="group in guideGroups(step)" :key="group.id" class="guide-group">
|
<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>
|
<h4 v-if="group.title" class="mono guide-title">{{ group.title }}</h4>
|
||||||
<div class="guide-images">
|
<div v-if="group.shots.length" class="guide-images">
|
||||||
<figure v-for="shot in group.shots" :key="shot.url" class="guide-shot">
|
<figure class="guide-shot">
|
||||||
<img :src="shot.url" :alt="shot.label || step.title" loading="lazy" />
|
<img :src="guideShot(step, group).url" :alt="guideShot(step, group).label || step.title" loading="lazy" />
|
||||||
<figcaption v-if="shot.label" class="mono">{{ shot.label }}</figcaption>
|
<figcaption v-if="guideShot(step, group).label" class="mono">{{ guideShot(step, group).label }}</figcaption>
|
||||||
</figure>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -300,6 +339,7 @@ const loading = ref(false);
|
|||||||
const error = ref("");
|
const error = ref("");
|
||||||
const onboarding = ref({ required_steps: [], optional_steps: [], completed_steps: [] });
|
const onboarding = ref({ required_steps: [], optional_steps: [], completed_steps: [] });
|
||||||
const initialPassword = ref("");
|
const initialPassword = ref("");
|
||||||
|
const initialPasswordRevealedAt = ref("");
|
||||||
const revealPassword = ref(false);
|
const revealPassword = ref(false);
|
||||||
const passwordCopied = ref(false);
|
const passwordCopied = ref(false);
|
||||||
const usernameCopied = ref(false);
|
const usernameCopied = ref(false);
|
||||||
@ -309,6 +349,10 @@ const elementRecoveryKey = ref("");
|
|||||||
const keycloakPasswordRotationRequested = ref(false);
|
const keycloakPasswordRotationRequested = ref(false);
|
||||||
const activeSectionId = ref("vaultwarden");
|
const activeSectionId = ref("vaultwarden");
|
||||||
const guideShots = ref({});
|
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 = {
|
const STEP_PREREQS = {
|
||||||
vaultwarden_master_password: [],
|
vaultwarden_master_password: [],
|
||||||
@ -738,6 +782,37 @@ function guideGroups(step) {
|
|||||||
return Object.values(stepShots);
|
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) {
|
function taskPillClass(value) {
|
||||||
const key = (value || "").trim();
|
const key = (value || "").trim();
|
||||||
if (key === "ok") return "pill-ok";
|
if (key === "ok") return "pill-ok";
|
||||||
@ -773,6 +848,7 @@ async function check() {
|
|||||||
tasks.value = Array.isArray(data.tasks) ? data.tasks : [];
|
tasks.value = Array.isArray(data.tasks) ? data.tasks : [];
|
||||||
blocked.value = Boolean(data.blocked);
|
blocked.value = Boolean(data.blocked);
|
||||||
initialPassword.value = data.initial_password || "";
|
initialPassword.value = data.initial_password || "";
|
||||||
|
initialPasswordRevealedAt.value = data.initial_password_revealed_at || "";
|
||||||
if (showOnboarding.value) {
|
if (showOnboarding.value) {
|
||||||
selectDefaultSection();
|
selectDefaultSection();
|
||||||
}
|
}
|
||||||
@ -1370,7 +1446,6 @@ button.secondary {
|
|||||||
|
|
||||||
.guide-images {
|
.guide-images {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1387,6 +1462,35 @@ button.secondary {
|
|||||||
border-radius: 8px;
|
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 {
|
.ready-box {
|
||||||
margin-top: 18px;
|
margin-top: 18px;
|
||||||
padding: 14px;
|
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 |