bstein-dev-home/frontend/src/views/OnboardingView.vue

386 lines
14 KiB
Vue

<template>
<div class="page">
<section class="card hero glass">
<div>
<p class="eyebrow">Atlas</p>
<h1>Onboarding</h1>
<p class="lede">Use your request code to view status and next steps.</p>
</div>
</section>
<section class="card module">
<div class="module-head">
<h2>Request Code</h2>
<span class="pill mono" :class="statusPillClass(status)">
{{ statusLabel(status) }}
</span>
</div>
<div class="status-form">
<input
v-model="requestCode"
class="input mono"
type="text"
placeholder="username~XXXXXXXXXX"
:disabled="loading"
/>
<button class="primary" type="button" @click="check" :disabled="loading || !requestCode.trim()">
{{ loading ? "Checking..." : "Check" }}
</button>
</div>
<div v-if="status === 'pending_email_verification'" class="steps">
<h3>Confirm your email</h3>
<p class="muted">
Open the verification email from Atlas and click the link to confirm your address. After verification, an admin can
approve your request.
</p>
<p class="muted">
If you did not receive an email, return to
<a href="/request-access">Request Access</a>
and submit again using a reachable external address.
</p>
</div>
<div v-if="status === 'pending'" class="steps">
<h3>Awaiting approval</h3>
<p class="muted">An Atlas admin has to approve this request before an account can be provisioned.</p>
</div>
<div v-if="status === 'accounts_building'" class="steps">
<h3>Accounts building</h3>
<p class="muted">Your request is approved. Atlas is now preparing accounts and credentials. Check back in a minute.</p>
<div v-if="tasks.length" class="task-box">
<div class="module-head" style="margin-bottom: 10px;">
<h3>Automation</h3>
<span class="pill mono" :class="blocked ? 'pill-bad' : 'pill-ok'">
{{ blocked ? "blocked" : "running" }}
</span>
</div>
<ul class="task-list">
<li v-for="item in tasks" :key="item.task" class="task-row">
<span class="mono task-name">{{ item.task }}</span>
<span class="pill mono" :class="taskPillClass(item.status)">{{ item.status }}</span>
<span v-if="item.detail" class="mono task-detail">{{ item.detail }}</span>
</li>
</ul>
<p v-if="blocked" class="muted" style="margin-top: 10px;">
One or more automation steps failed. Fix the error above, then check again.
</p>
<div v-if="blocked" class="actions" style="margin-top: 10px;">
<button class="pill mono" type="button" :disabled="retrying" @click="retryProvisioning">
{{ retrying ? "Retrying..." : "Retry failed steps" }}
</button>
<span v-if="retryMessage" class="hint mono">{{ retryMessage }}</span>
</div>
<p v-if="blocked" class="muted" style="margin-top: 8px;">
If the error mentions rate limiting or a temporary outage, wait a few minutes and retry. If it keeps failing,
contact an admin.
</p>
</div>
</div>
<div v-if="showOnboarding" class="steps">
<div class="onboarding-head">
<div>
<h3>Onboarding</h3>
<p class="muted">
Onboarding is fully self-service. There are 8 steps for 8 services below. Each step has smaller tasks; press
Confirm when you finish a task. You can pause and return later.
</p>
</div>
<span v-if="status !== 'ready'" class="pill mono pill-info">active</span>
</div>
<ol class="section-stepper">
<li v-for="(section, index) in sections" :key="section.id" class="stepper-item">
<button
class="stepper-card"
:class="sectionCardClass(section)"
:disabled="isSectionLocked(section)"
type="button"
@click="selectSection(section.id)"
>
<span class="stepper-dot" aria-hidden="true"></span>
<div class="stepper-body">
<div class="stepper-title">{{ index + 1 }}. {{ section.title }}</div>
<div class="stepper-meta">
<span v-if="sectionStatusLabel(section)" class="pill mono" :class="sectionPillClass(section)">
{{ sectionStatusLabel(section) }}
</span>
<span class="pill mono pill-compact">{{ sectionProgress(section) }}</span>
</div>
</div>
</button>
</li>
</ol>
<div v-if="requestUsername || initialPassword" class="credential-card">
<div class="credential-head">
<h4>Keycloak temporary credentials</h4>
<p class="muted">Use these to sign in to Nextcloud, Element, and any Keycloak-protected services.</p>
</div>
<div class="credential-grid">
<div class="credential-field">
<span class="label mono">Username</span>
<div class="password-row">
<input class="input mono" type="text" :value="requestUsername || ''" readonly />
<button class="secondary" type="button" @click="copyUsername" :disabled="!requestUsername">
{{ usernameCopied ? "Copied" : "Copy" }}
</button>
</div>
</div>
<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 || '********'"
readonly
/>
<span class="tooltip-wrap" :title="passwordRevealHint">
<button class="secondary" type="button" @click="togglePassword" :disabled="!initialPassword">
{{ revealPassword ? "Hide" : "Reveal" }}
</button>
</span>
<span class="tooltip-wrap" :title="passwordRevealHint">
<button class="secondary" type="button" @click="copyInitialPassword" :disabled="!initialPassword">
{{ passwordCopied ? "Copied" : "Copy" }}
</button>
</span>
</div>
</div>
</div>
<p class="muted">
Use the temporary password to sign in the first time (Nextcloud, Element). You will rotate it after Vaultwarden is
ready. Store the new password in Vaultwarden.
</p>
</div>
<div class="section-shell" v-if="activeSection">
<div class="section-header">
<div>
<h3>{{ activeSection.title }}</h3>
<p v-if="activeSection.summary" class="muted">{{ activeSection.summary }}</p>
<p v-if="activeSection.benefit" class="muted">{{ activeSection.benefit }}</p>
</div>
</div>
<div class="step-grid">
<article v-for="step in activeSection.steps" :key="step.id" class="step-card" :class="stepCardClass(step)">
<div class="step-head">
<div class="step-title">
<label v-if="step.action === 'checkbox'" class="step-label">
<input
type="checkbox"
:checked="isStepDone(step.id)"
:disabled="loading || isStepBlocked(step.id)"
@change="toggleStep(step.id, $event)"
/>
<span>{{ step.title }}</span>
</label>
<div v-else class="step-label">
<span>{{ step.title }}</span>
</div>
</div>
<span class="pill mono auto-pill" :class="stepPillClass(step)">
{{ stepPillLabel(step) }}
</span>
</div>
<p class="muted" v-if="step.description">{{ step.description }}</p>
<p class="step-note" v-if="stepNote(step)">{{ stepNote(step) }}</p>
<ul v-if="step.bullets && step.bullets.length" class="step-bullets">
<li v-for="bullet in step.bullets" :key="bullet">{{ bullet }}</li>
</ul>
<div v-if="step.links && step.links.length" class="step-links">
<a
v-for="link in step.links"
:key="link.href"
:href="link.href"
:title="link.href"
target="_blank"
rel="noreferrer"
>
{{ link.text }}
</a>
</div>
<details v-if="step.guide" class="guide-details" :open="shouldOpenGuide(step, activeSection)">
<summary class="mono guide-summary">Photo guide</summary>
<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 v-if="group.shots.length" class="guide-images">
<figure class="guide-shot" @click="openLightbox(guideShot(step, group))">
<figcaption v-if="guideShot(step, group).label" class="mono">{{ guideShot(step, group).label }}</figcaption>
<img :src="guideShot(step, group).url" :alt="guideShot(step, group).label || step.title" loading="lazy" />
</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>
<p v-else class="muted">Guide coming soon.</p>
</details>
<div class="step-actions">
<button
class="secondary"
type="button"
@click="confirmStep(step)"
:disabled="loading || isConfirming(step) || isStepDone(step.id) || isStepBlocked(step.id)"
>
{{ confirmLabel(step) }}
</button>
</div>
</article>
</div>
<div class="section-actions">
<button class="secondary" type="button" @click="prevSection" :disabled="!hasPrevSection">
Previous
</button>
<button
class="secondary"
type="button"
@click="nextSection"
:disabled="!hasNextSection || isSectionLocked(nextSectionItem) || !sectionGateComplete(activeSection)"
>
Next
</button>
</div>
</div>
<div v-if="status === 'ready'" class="ready-box">
<h3>You're ready</h3>
<p class="muted">
Your Atlas account is provisioned and onboarding is complete. Visit the Apps page to access the full suite.
<a href="/apps">Atlas Apps</a>.
</p>
</div>
</div>
<div v-if="status === 'denied'" class="steps">
<h3>Denied</h3>
<p class="muted">This request was denied. Contact the Atlas admin if you think this is a mistake.</p>
</div>
<div v-if="error" class="error-box">
<div class="mono">{{ error }}</div>
</div>
</section>
<div v-if="lightboxShot" class="lightbox" @click.self="closeLightbox">
<div class="lightbox-card">
<div class="lightbox-head">
<span class="mono lightbox-label">{{ lightboxShot.label || "Guide image" }}</span>
<button class="secondary" type="button" @click="closeLightbox">Close</button>
</div>
<img :src="lightboxShot.url" :alt="lightboxShot.label || 'Guide image'" />
</div>
</div>
</div>
</template>
<script setup>
import { useRoute } from "vue-router";
import { useOnboardingFlow } from "../onboarding/useOnboardingFlow";
const {
requestCode,
requestUsername,
status,
loading,
error,
onboarding,
initialPassword,
revealPassword,
passwordCopied,
usernameCopied,
tasks,
blocked,
retrying,
retryMessage,
lightboxShot,
showPasswordCard,
passwordRevealHint,
sections,
activeSection,
nextSectionItem,
hasPrevSection,
hasNextSection,
showOnboarding,
selectSection,
prevSection,
nextSection,
statusLabel,
statusPillClass,
isStepDone,
isStepBlocked,
stepNote,
stepPillLabel,
stepPillClass,
isConfirming,
confirmLabel,
stepCardClass,
sectionProgress,
sectionStatusLabel,
sectionPillClass,
isSectionLocked,
sectionCardClass,
guideGroups,
guideIndex,
guideSet,
guidePrev,
guideNext,
guideShot,
shouldOpenGuide,
openLightbox,
closeLightbox,
taskPillClass,
check,
retryProvisioning,
togglePassword,
copyInitialPassword,
copyUsername,
toggleStep,
confirmStep,
} = useOnboardingFlow(useRoute());
</script>
<style scoped src="../styles/onboarding.css"></style>
<style scoped src="../styles/onboarding-guides.css"></style>