386 lines
14 KiB
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>
|