2026-01-02 01:34:18 -03:00
< 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 >
2026-01-02 09:42:06 -03:00
< span class = "pill mono" :class = "statusPillClass(status)" >
{ { statusLabel ( status ) } }
2026-01-02 01:34:18 -03:00
< / 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 >
2026-01-02 10:27:02 -03:00
< div v-if = "requestUsername" class="status-meta" >
< div class = "meta-row" >
< span class = "label mono" > Username < / span >
2026-01-04 08:44:25 -03:00
< button class = "copy mono" type = "button" @click ="copyUsername" >
{ { requestUsername } }
< span v-if = "usernameCopied" class="copied" > copied < / span >
< / button >
2026-01-02 10:27:02 -03:00
< / div >
< / div >
2026-01-03 02:36:29 -03:00
< 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 >
2026-01-02 09:42:06 -03:00
< 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 >
2026-01-03 04:55:03 -03:00
< 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 >
2026-01-02 09:42:06 -03:00
< / div >
< div v-if = "status === 'awaiting_onboarding' || status === 'ready'" class="steps" >
< div class = "onboarding-head" >
< h3 > Onboarding checklist < / h3 >
< span class = "pill mono" : class = "status === 'ready' ? 'pill-info' : 'pill-ok'" >
{ { status === "ready" ? "ready" : "in progress" } }
< / span >
< / div >
< p class = "muted" >
2026-01-04 08:21:28 -03:00
Some steps are verified automatically from Keycloak ( password ) . Others can 't be verified yet — mark them complete once you' re done .
2026-01-02 09:42:06 -03:00
< / p >
2026-01-02 11:12:43 -03:00
< div v-if = "initialPassword" class="initial-password" >
< h3 > Temporary password < / h3 >
< p class = "muted" >
2026-01-04 22:49:34 -03:00
Use this password to log in for the first time . You won 't be forced to change it immediately — you' ll rotate
2026-01-04 23:34:21 -03:00
it later after Vaultwarden is set up . This password is shown once — copy it now . If you refresh this page ,
it may disappear .
2026-01-02 11:12:43 -03:00
< / p >
< div class = "request-code-row" >
< span class = "label mono" > Password < / span >
< button class = "copy mono" type = "button" @click ="copyInitialPassword" >
{ { initialPassword } }
< span v-if = "copied" class="copied" > copied < / span >
< / button >
< / div >
< p class = "muted" >
Log in at
< a href = "https://sso.bstein.dev" target = "_blank" rel = "noreferrer" > sso . bstein . dev < / a >
or go directly to
< a href = "https://cloud.bstein.dev" target = "_blank" rel = "noreferrer" > cloud . bstein . dev < / a > .
< / p >
< / div >
2026-01-02 09:42:06 -03:00
< div v-if = "!auth.authenticated" class="login-callout" >
< p class = "muted" > Log in to check off onboarding steps . < / p >
< button class = "primary" type = "button" @click ="loginToContinue" :disabled = "loading" > Log in < / button >
< / div >
< ul class = "checklist" >
2026-01-04 23:34:21 -03:00
< li class = "check-item" :class = "checkItemClass('vaultwarden_master_password')" >
2026-01-02 10:27:02 -03:00
< label >
< input
type = "checkbox"
2026-01-04 22:49:34 -03:00
: checked = "isStepDone('vaultwarden_master_password')"
: disabled = "!auth.authenticated || loading || isStepBlocked('vaultwarden_master_password')"
@ change = "toggleStep('vaultwarden_master_password', $event)"
2026-01-02 10:27:02 -03:00
/ >
2026-01-04 22:49:34 -03:00
< span > Set a Vaultwarden master password < / span >
< span class = "pill mono auto-pill" :class = "stepPillClass('vaultwarden_master_password')" >
{ { stepPillLabel ( "vaultwarden_master_password" ) } }
< / span >
< / label >
< p class = "muted" >
Open < a href = "https://vault.bstein.dev" target = "_blank" rel = "noreferrer" > Passwords < / a > and set a strong master
2026-01-05 00:06:41 -03:00
password you won ' t forget .
Your master password is the one password to rule all passwords : use a long passphrase ( 64 + characters is a good target ) , and never
write it down or share it with anyone .
If you lose it , Atlas can ' t recover your vault .
If you can ' t sign in yet , check your Atlas mailbox in
2026-01-04 22:49:34 -03:00
< a href = "https://cloud.bstein.dev" target = "_blank" rel = "noreferrer" > Nextcloud Mail < / a > for the invite link .
< / p >
2026-01-05 00:06:41 -03:00
< details class = "howto" >
< summary class = "mono" > Master password guidance < / summary >
< ul class = "muted howto-list" >
< li > Prefer a multi - word passphrase over a single word . < / li >
< li > Never store it in plaintext or share it with anyone . < / li >
< li > If you forget it , Vaultwarden can ’ t decrypt your data . < / li >
< / ul >
< / details >
2026-01-04 22:49:34 -03:00
< / li >
2026-01-04 23:34:21 -03:00
< li class = "check-item" :class = "checkItemClass('vaultwarden_browser_extension')" >
2026-01-04 22:49:34 -03:00
< label >
< input
type = "checkbox"
: checked = "isStepDone('vaultwarden_browser_extension')"
: disabled = "!auth.authenticated || loading || isStepBlocked('vaultwarden_browser_extension')"
@ change = "toggleStep('vaultwarden_browser_extension', $event)"
/ >
< span > Install the Vaultwarden browser extension < / span >
< span class = "pill mono auto-pill" :class = "stepPillClass('vaultwarden_browser_extension')" >
{ { stepPillLabel ( "vaultwarden_browser_extension" ) } }
< / span >
< / label >
< p class = "muted" >
Install Bitwarden in your browser and point it at vault . bstein . dev ( Settings → Account → Environment → Self - hosted ) .
< a href = "https://bitwarden.com/download" target = "_blank" rel = "noreferrer" > Bitwarden downloads < / a > .
< / p >
< / li >
2026-01-06 13:55:24 -03:00
< li class = "check-item" :class = "checkItemClass('vaultwarden_desktop_app')" >
< label >
< input
type = "checkbox"
: checked = "isStepDone('vaultwarden_desktop_app')"
: disabled = "!auth.authenticated || loading || isStepBlocked('vaultwarden_desktop_app')"
@ change = "toggleStep('vaultwarden_desktop_app', $event)"
/ >
< span > Install the Vaultwarden desktop app < / span >
< span class = "pill mono auto-pill" :class = "stepPillClass('vaultwarden_desktop_app')" >
{ { stepPillLabel ( "vaultwarden_desktop_app" ) } }
< / span >
< / label >
< p class = "muted" >
Install the Bitwarden desktop app and set the server to vault . bstein . dev ( Settings → Account → Environment → Self - hosted ) .
< a href = "https://bitwarden.com/download" target = "_blank" rel = "noreferrer" > Bitwarden downloads < / a > .
< / p >
< / li >
2026-01-04 23:34:21 -03:00
< li class = "check-item" :class = "checkItemClass('vaultwarden_mobile_app')" >
2026-01-04 22:49:34 -03:00
< label >
< input
type = "checkbox"
: checked = "isStepDone('vaultwarden_mobile_app')"
: disabled = "!auth.authenticated || loading || isStepBlocked('vaultwarden_mobile_app')"
@ change = "toggleStep('vaultwarden_mobile_app', $event)"
/ >
< span > Install Bitwarden on your phone < / span >
< span class = "pill mono auto-pill" :class = "stepPillClass('vaultwarden_mobile_app')" >
{ { stepPillLabel ( "vaultwarden_mobile_app" ) } }
2026-01-03 00:57:14 -03:00
< / span >
< / label >
< p class = "muted" >
2026-01-04 22:49:34 -03:00
Install the mobile app , set the server to vault . bstein . dev , and enable biometrics for fast unlock .
< a href = "https://bitwarden.com/download" target = "_blank" rel = "noreferrer" > Bitwarden downloads < / a > .
< / p >
< / li >
2026-01-14 17:32:20 -03:00
< li class = "check-item" :class = "checkItemClass('health_data_notice')" >
< label >
< input
type = "checkbox"
: checked = "isStepDone('health_data_notice')"
: disabled = "!auth.authenticated || loading || isStepBlocked('health_data_notice')"
@ change = "toggleStep('health_data_notice', $event)"
/ >
2026-01-15 00:40:43 -03:00
< span > Review the Wger health data notice < / span >
2026-01-14 17:32:20 -03:00
< span class = "pill mono auto-pill" :class = "stepPillClass('health_data_notice')" >
{ { stepPillLabel ( "health_data_notice" ) } }
< / span >
< / label >
< p class = "muted" >
2026-01-15 00:40:43 -03:00
Wger is a personal wellness tool , not medical advice . Use it at your own risk . Your health data belongs to
you and will never be sold or used beyond providing the service . We apply best practices to protect it ,
but no system is risk - free .
2026-01-14 17:32:20 -03:00
< / p >
< / li >
< li class = "check-item" :class = "checkItemClass('wger_login')" >
< label >
< input
type = "checkbox"
: checked = "isStepDone('wger_login')"
: disabled = "!auth.authenticated || loading || isStepBlocked('wger_login')"
@ change = "toggleStep('wger_login', $event)"
/ >
2026-01-15 00:40:43 -03:00
< span > Sign in to Wger < / span >
2026-01-14 17:32:20 -03:00
< span class = "pill mono auto-pill" :class = "stepPillClass('wger_login')" >
{ { stepPillLabel ( "wger_login" ) } }
< / span >
< / label >
< p class = "muted" >
Open < a href = "https://health.bstein.dev" target = "_blank" rel = "noreferrer" > health . bstein . dev < / a > and sign in
with the credentials shown on your < a href = "/account" > Account < / a > page . In the mobile app , set the server
URL to health . bstein . dev and log in once .
< / p >
< / li >
2026-01-16 23:50:07 -03:00
< li class = "check-item" :class = "checkItemClass('actual_login')" >
< label >
< input
type = "checkbox"
: checked = "isStepDone('actual_login')"
: disabled = "!auth.authenticated || loading || isStepBlocked('actual_login')"
@ change = "toggleStep('actual_login', $event)"
/ >
< span > Sign in to Actual Budget < / span >
< span class = "pill mono auto-pill" :class = "stepPillClass('actual_login')" >
{ { stepPillLabel ( "actual_login" ) } }
< / span >
< / label >
< p class = "muted" >
Open < a href = "https://budget.bstein.dev" target = "_blank" rel = "noreferrer" > budget . bstein . dev < / a > and sign in
with your Keycloak account .
< / p >
< / li >
< li class = "check-item" :class = "checkItemClass('firefly_login')" >
< label >
< input
type = "checkbox"
: checked = "isStepDone('firefly_login')"
: disabled = "!auth.authenticated || loading || isStepBlocked('firefly_login')"
@ change = "toggleStep('firefly_login', $event)"
/ >
< span > Sign in to Firefly III < / span >
< span class = "pill mono auto-pill" :class = "stepPillClass('firefly_login')" >
{ { stepPillLabel ( "firefly_login" ) } }
< / span >
< / label >
< p class = "muted" >
Open < a href = "https://money.bstein.dev" target = "_blank" rel = "noreferrer" > money . bstein . dev < / a > and sign in
with the credentials from your < a href = "/account" > Account < / a > page . In the Abacus app , set the server URL
to money . bstein . dev and log in once .
< / p >
< / li >
2026-01-04 23:34:21 -03:00
< li class = "check-item" :class = "checkItemClass('keycloak_password_rotated')" >
2026-01-04 22:49:34 -03:00
< label >
< input type = "checkbox" :checked = "isStepDone('keycloak_password_rotated')" disabled / >
< span > Rotate your Keycloak password < / span >
< span class = "pill mono auto-pill" :class = "keycloakRotationPillClass()" >
{ { keycloakRotationPillLabel ( ) } }
< / span >
< / label >
< div class = "mfa-actions" >
< button
class = "secondary"
type = "button"
@ click = "requestKeycloakPasswordRotation"
: disabled = "
! auth . authenticated ||
loading ||
isStepDone ( 'keycloak_password_rotated' ) ||
isStepBlocked ( 'keycloak_password_rotated' ) ||
keycloakPasswordRotationRequested
"
>
Enable rotation
< / button >
2026-01-06 13:55:24 -03:00
< a
class = "mono"
href = "https://sso.bstein.dev/realms/atlas/account/#/security/signing-in"
target = "_blank"
rel = "noreferrer"
>
Open Keycloak
< / a >
2026-01-04 22:49:34 -03:00
< / div >
< p class = "muted" >
After Vaultwarden is set up , rotate your Keycloak password to a strong one and store it in Vaultwarden .
Atlas verifies this once Keycloak no longer requires you to update your password .
2026-01-03 00:57:14 -03:00
< / p >
< / li >
2026-01-04 23:34:21 -03:00
< li class = "check-item mfa-optional" :class = "mfaItemClass()" >
2026-01-04 21:57:31 -03:00
< div class = "mfa-row" >
< div class = "mfa-text" >
< span class = "mfa-title" > Optional : enable MFA ( TOTP ) for Keycloak < / span >
< p class = "muted mfa-description" >
Add a second factor with a mobile authenticator app . This is optional and won ' t block the rest of onboarding .
< / p >
< / div >
< span class = "pill mono auto-pill" :class = "mfaPillClass()" > { { mfaPillLabel ( ) } } < / span >
< / div >
< div class = "mfa-actions" >
< button
class = "secondary"
type = "button"
@ click = "setMfaOptional('skipped')"
: disabled = "!auth.authenticated || loading || isMfaBlocked() || isMfaDecided()"
>
Skip
< / button >
< button
class = "primary"
type = "button"
@ click = "setMfaOptional('done')"
: disabled = "!auth.authenticated || loading || isMfaBlocked() || isMfaDecided()"
>
Mark complete
< / button >
< / div >
< details class = "mfa-qr" @toggle ="maybeGenerateMfaQrs" >
< summary class = "mono" > Show app install QR codes < / summary >
< p v-if = "mfaQrError" class="muted mfa-error" > {{ mfaQrError }} < / p >
< div v -else class = "mfa-qr-grid" >
< div class = "mfa-qr-card" >
< span class = "mono mfa-qr-label" > Aegis ( Android ) < / span >
< img v-if = "aegisQr" class="mfa-qr-img" :src="aegisQr" alt="Aegis Android app QR code" / >
< a class = "mono mfa-qr-link" :href = "AEGIS_URL" target = "_blank" rel = "noreferrer" > Open store < / a >
< / div >
< div class = "mfa-qr-card" >
< span class = "mono mfa-qr-label" > FreeOTP ( iPhone ) < / span >
< img v-if = "freeOtpQr" class="mfa-qr-img" :src="freeOtpQr" alt="FreeOTP iPhone app QR code" / >
< a class = "mono mfa-qr-link" :href = "FREEOTP_URL" target = "_blank" rel = "noreferrer" > Open store < / a >
< / div >
< / div >
< / details >
< / li >
2026-01-04 23:34:21 -03:00
< li class = "check-item" :class = "checkItemClass('element_recovery_key')" >
2026-01-02 09:42:06 -03:00
< label >
2026-01-04 13:00:42 -03:00
< input type = "checkbox" :checked = "isStepDone('element_recovery_key')" disabled / >
2026-01-02 09:42:06 -03:00
< span > Create an Element recovery key < / span >
2026-01-04 12:30:30 -03:00
< span class = "pill mono auto-pill" :class = "stepPillClass('element_recovery_key')" >
{ { stepPillLabel ( "element_recovery_key" ) } }
< / span >
2026-01-02 09:42:06 -03:00
< / label >
2026-01-04 13:00:42 -03:00
< div class = "recovery-verify" >
< input
v - model = "elementRecoveryKey"
class = "input mono"
type = "text"
placeholder = "Paste recovery key (hashed locally)"
: disabled = "
! auth . authenticated || loading || isStepDone ( 'element_recovery_key' ) || isStepBlocked ( 'element_recovery_key' )
"
/ >
< button
class = "primary verify"
type = "button"
@ click = "verifyElementRecoveryKey"
: disabled = "
! auth . authenticated ||
loading ||
isStepDone ( 'element_recovery_key' ) ||
isStepBlocked ( 'element_recovery_key' ) ||
! elementRecoveryKey . trim ( )
"
>
Verify
< / button >
< / div >
2026-01-02 09:42:06 -03:00
< p class = "muted" >
2026-01-04 13:00:42 -03:00
In Element , create a recovery key so you can restore encrypted history if you lose a device . Atlas stores only a SHA - 256 hash so the
recovery key itself is never saved server - side .
2026-01-04 22:49:34 -03:00
Open < a href = "https://live.bstein.dev/#/settings" target = "_blank" rel = "noreferrer" > Element settings < / a > → Encryption .
2026-01-02 09:42:06 -03:00
< / p >
< / li >
2026-01-04 23:34:21 -03:00
< li class = "check-item" :class = "checkItemClass('element_recovery_key_stored')" >
2026-01-02 09:42:06 -03:00
< label >
< input
type = "checkbox"
: checked = "isStepDone('element_recovery_key_stored')"
2026-01-04 12:30:30 -03:00
: disabled = "!auth.authenticated || loading || isStepBlocked('element_recovery_key_stored')"
2026-01-02 09:42:06 -03:00
@ change = "toggleStep('element_recovery_key_stored', $event)"
/ >
< span > Store the recovery key in Vaultwarden < / span >
2026-01-04 12:30:30 -03:00
< span class = "pill mono auto-pill" :class = "stepPillClass('element_recovery_key_stored')" >
{ { stepPillLabel ( "element_recovery_key_stored" ) } }
< / span >
2026-01-02 09:42:06 -03:00
< / label >
< p class = "muted" > Save the recovery key in Vaultwarden so it doesn ' t get lost . < / p >
< / li >
2026-01-04 13:00:42 -03:00
2026-01-04 23:34:21 -03:00
< li v-for = "step in extraSteps" :key="step.id" class="check-item" :class="checkItemClass(step.id)" >
2026-01-04 13:00:42 -03:00
< label >
< input
type = "checkbox"
: checked = "isStepDone(step.id)"
: disabled = "!auth.authenticated || loading || isStepBlocked(step.id)"
@ change = "toggleStep(step.id, $event)"
/ >
< span > { { step . title } } < / span >
< span class = "pill mono auto-pill" :class = "stepPillClass(step.id)" > { { stepPillLabel ( step . id ) } } < / span >
< / label >
< p class = "muted" >
{ { step . description } }
< template v-if = "step.primaryLink" >
< a :href = "step.primaryLink.href" target = "_blank" rel = "noreferrer" > { { step . primaryLink . text } } < / a
> .
< / template >
< template v-if = "step.secondaryLink" >
< span > < / span >
< a :href = "step.secondaryLink.href" target = "_blank" rel = "noreferrer" > { { step . secondaryLink . text } } < / a
> .
< / template >
< / p >
< / li >
2026-01-02 09:42:06 -03:00
< / ul >
< 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 . You can log in at
2026-01-12 23:29:32 -03:00
< a href = "https://cloud.bstein.dev" target = "_blank" rel = "noreferrer" > cloud . bstein . dev < / a > ,
< a href = "https://notes.bstein.dev" target = "_blank" rel = "noreferrer" > notes . bstein . dev < / a > , and
< a href = "https://tasks.bstein.dev" target = "_blank" rel = "noreferrer" > tasks . bstein . dev < / a > .
2026-01-02 09:42:06 -03:00
< / p >
< / div >
2026-01-02 01:34:18 -03:00
< / 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 >
< / template >
< script setup >
import { onMounted , ref } from "vue" ;
2026-01-04 21:57:31 -03:00
import QRCode from "qrcode" ;
2026-01-02 01:34:18 -03:00
import { useRoute } from "vue-router" ;
2026-01-02 09:42:06 -03:00
import { auth , authFetch , login } from "../auth" ;
2026-01-02 01:34:18 -03:00
const route = useRoute ( ) ;
2026-01-04 21:57:31 -03:00
const AEGIS _URL = "https://play.google.com/store/apps/details?id=com.beemdevelopment.aegis" ;
const FREEOTP _URL = "https://apps.apple.com/app/freeotp-authenticator/id872559395" ;
2026-01-02 01:34:18 -03:00
const requestCode = ref ( "" ) ;
2026-01-02 10:27:02 -03:00
const requestUsername = ref ( "" ) ;
2026-01-02 01:34:18 -03:00
const status = ref ( "" ) ;
const loading = ref ( false ) ;
const error = ref ( "" ) ;
2026-01-04 21:57:31 -03:00
const onboarding = ref ( { required _steps : [ ] , optional _steps : [ ] , completed _steps : [ ] , optional : { } } ) ;
2026-01-02 11:12:43 -03:00
const initialPassword = ref ( "" ) ;
const copied = ref ( false ) ;
2026-01-04 08:44:25 -03:00
const usernameCopied = ref ( false ) ;
2026-01-03 04:55:03 -03:00
const tasks = ref ( [ ] ) ;
const blocked = ref ( false ) ;
2026-01-04 13:00:42 -03:00
const elementRecoveryKey = ref ( "" ) ;
2026-01-04 21:57:31 -03:00
const aegisQr = ref ( "" ) ;
const freeOtpQr = ref ( "" ) ;
const mfaQrError = ref ( "" ) ;
const mfaQrReady = ref ( false ) ;
2026-01-04 22:49:34 -03:00
const keycloakPasswordRotationRequested = ref ( false ) ;
2026-01-04 13:00:42 -03:00
const extraSteps = [
{
id : "elementx_setup" ,
title : "Install Element X and sign in" ,
2026-01-06 13:55:24 -03:00
description :
"Install Element X on mobile and sign in with your Atlas username/password. Use Element Web → Settings → Sessions to connect your phone via QR." ,
2026-01-04 13:00:42 -03:00
primaryLink : { href : "https://live.bstein.dev" , text : "Element" } ,
} ,
{
id : "mail_client_setup" ,
title : "Set up mail on a device" ,
2026-01-04 13:09:45 -03:00
description :
2026-01-06 13:55:24 -03:00
"Use the IMAP/SMTP details on your Account page to add mail to your phone or desktop client (FairEmail on Android, Apple Mail on iOS, Thunderbird on desktop)." ,
2026-01-04 13:00:42 -03:00
primaryLink : { href : "/account" , text : "Account" } ,
} ,
2026-01-12 23:29:32 -03:00
{
id : "outline_login" ,
title : "Open Outline and create your first doc" ,
description : "Create a space for your docs and invite collaborators when you're ready." ,
primaryLink : { href : "https://notes.bstein.dev" , text : "Outline" } ,
} ,
{
id : "planka_login" ,
title : "Open Planka and create a board" ,
description : "Spin up a project board and invite teammates to collaborate." ,
primaryLink : { href : "https://tasks.bstein.dev" , text : "Planka" } ,
} ,
{
id : "jellyfin_login" ,
title : "Sign in to Jellyfin" ,
description : "Sign in with your Atlas username/password (LDAP-backed)." ,
primaryLink : { href : "https://stream.bstein.dev" , text : "Jellyfin" } ,
} ,
2026-01-04 13:00:42 -03:00
] ;
2026-01-02 09:42:06 -03:00
function statusLabel ( value ) {
const key = ( value || "" ) . trim ( ) ;
2026-01-03 02:36:29 -03:00
if ( key === "pending_email_verification" ) return "confirm email" ;
2026-01-02 09:42:06 -03:00
if ( key === "pending" ) return "awaiting approval" ;
if ( key === "accounts_building" ) return "accounts building" ;
if ( key === "awaiting_onboarding" ) return "awaiting onboarding" ;
if ( key === "ready" ) return "ready" ;
if ( key === "denied" ) return "rejected" ;
return key || "unknown" ;
}
function statusPillClass ( value ) {
const key = ( value || "" ) . trim ( ) ;
2026-01-03 02:36:29 -03:00
if ( key === "pending_email_verification" ) return "pill-warn" ;
2026-01-02 09:42:06 -03:00
if ( key === "pending" ) return "pill-wait" ;
if ( key === "accounts_building" ) return "pill-warn" ;
if ( key === "awaiting_onboarding" ) return "pill-ok" ;
if ( key === "ready" ) return "pill-info" ;
if ( key === "denied" ) return "pill-bad" ;
return "pill-warn" ;
}
function isStepDone ( step ) {
const steps = onboarding . value ? . completed _steps || [ ] ;
return Array . isArray ( steps ) ? steps . includes ( step ) : false ;
}
2026-01-02 01:34:18 -03:00
2026-01-04 23:34:21 -03:00
function requiredStepOrder ( ) {
if ( Array . isArray ( onboarding . value ? . required _steps ) && onboarding . value . required _steps . length ) {
return onboarding . value . required _steps ;
}
return [
"vaultwarden_master_password" ,
"vaultwarden_browser_extension" ,
2026-01-06 13:55:24 -03:00
"vaultwarden_desktop_app" ,
2026-01-04 23:34:21 -03:00
"vaultwarden_mobile_app" ,
2026-01-14 17:32:20 -03:00
"health_data_notice" ,
"wger_login" ,
2026-01-16 23:50:07 -03:00
"actual_login" ,
"firefly_login" ,
2026-01-04 23:34:21 -03:00
"keycloak_password_rotated" ,
"element_recovery_key" ,
"element_recovery_key_stored" ,
"elementx_setup" ,
"mail_client_setup" ,
2026-01-12 23:29:32 -03:00
"outline_login" ,
"planka_login" ,
"jellyfin_login" ,
2026-01-04 23:34:21 -03:00
] ;
}
function activeRequiredStep ( ) {
const order = requiredStepOrder ( ) ;
for ( const step of order ) {
if ( ! isStepDone ( step ) ) return step ;
}
return "" ;
}
2026-01-04 21:57:31 -03:00
function mfaOptionalState ( ) {
const state = onboarding . value ? . optional ? . keycloak _mfa _optional ? . state ;
if ( state === "done" || state === "skipped" ) return state ;
return "pending" ;
}
function isMfaDecided ( ) {
const state = mfaOptionalState ( ) ;
return state === "done" || state === "skipped" ;
}
function isMfaBlocked ( ) {
2026-01-04 22:49:34 -03:00
return ! isStepDone ( "keycloak_password_rotated" ) ;
2026-01-04 21:57:31 -03:00
}
2026-01-04 23:34:21 -03:00
function mfaItemClass ( ) {
const state = mfaOptionalState ( ) ;
return {
blocked : isMfaBlocked ( ) ,
done : state === "done" ,
skipped : state === "skipped" ,
optional : true ,
} ;
}
2026-01-04 21:57:31 -03:00
function mfaPillLabel ( ) {
if ( isMfaBlocked ( ) ) return "blocked" ;
const state = mfaOptionalState ( ) ;
if ( state === "done" ) return "done" ;
if ( state === "skipped" ) return "skipped" ;
return "optional" ;
}
function mfaPillClass ( ) {
if ( isMfaBlocked ( ) ) return "pill-wait" ;
const state = mfaOptionalState ( ) ;
if ( state === "done" ) return "pill-ok" ;
if ( state === "skipped" ) return "pill-info" ;
return "pill-warn" ;
}
2026-01-04 22:49:34 -03:00
function keycloakRotationPillLabel ( ) {
if ( isStepDone ( "keycloak_password_rotated" ) ) return "done" ;
if ( isStepBlocked ( "keycloak_password_rotated" ) ) return "blocked" ;
if ( keycloakPasswordRotationRequested . value ) return "rotate now" ;
return "ready" ;
}
function keycloakRotationPillClass ( ) {
if ( isStepDone ( "keycloak_password_rotated" ) ) return "pill-ok" ;
if ( isStepBlocked ( "keycloak_password_rotated" ) ) return "pill-wait" ;
if ( keycloakPasswordRotationRequested . value ) return "pill-warn" ;
return "pill-info" ;
}
2026-01-04 21:57:31 -03:00
async function maybeGenerateMfaQrs ( event ) {
if ( mfaQrReady . value ) return ;
const details = event ? . target ;
if ( details && details . tagName === "DETAILS" && ! details . open ) return ;
mfaQrError . value = "" ;
try {
aegisQr . value = await QRCode . toDataURL ( AEGIS _URL , { width : 220 , margin : 2 } ) ;
freeOtpQr . value = await QRCode . toDataURL ( FREEOTP _URL , { width : 220 , margin : 2 } ) ;
mfaQrReady . value = true ;
} catch ( err ) {
mfaQrError . value = err ? . message || "Failed to generate QR codes" ;
}
}
2026-01-04 12:30:30 -03:00
function isStepBlocked ( step ) {
2026-01-04 23:34:21 -03:00
const order = requiredStepOrder ( ) ;
2026-01-04 12:30:30 -03:00
const idx = order . indexOf ( step ) ;
if ( idx <= 0 ) return false ;
for ( let i = 0 ; i < idx ; i += 1 ) {
if ( ! isStepDone ( order [ i ] ) ) return true ;
}
return false ;
}
2026-01-04 23:34:21 -03:00
function checkItemClass ( step ) {
const activeStep = activeRequiredStep ( ) ;
const done = isStepDone ( step ) ;
const blockedStep = isStepBlocked ( step ) ;
const active = ! done && ! blockedStep && activeStep === step ;
return { done , blocked : blockedStep , active } ;
}
2026-01-04 12:30:30 -03:00
function stepPillLabel ( step ) {
if ( isStepDone ( step ) ) return "done" ;
if ( isStepBlocked ( step ) ) return "blocked" ;
return "pending" ;
}
function stepPillClass ( step ) {
if ( isStepDone ( step ) ) return "pill-ok" ;
if ( isStepBlocked ( step ) ) return "pill-wait" ;
return "pill-warn" ;
}
2026-01-03 04:55:03 -03:00
function taskPillClass ( status ) {
const key = ( status || "" ) . trim ( ) ;
if ( key === "ok" ) return "pill-ok" ;
if ( key === "error" ) return "pill-bad" ;
if ( key === "pending" ) return "pill-warn" ;
return "pill-warn" ;
}
2026-01-02 01:34:18 -03:00
async function check ( ) {
if ( loading . value ) return ;
error . value = "" ;
loading . value = true ;
try {
const resp = await fetch ( "/api/access/request/status" , {
method : "POST" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON . stringify ( { request _code : requestCode . value . trim ( ) } ) ,
} ) ;
const data = await resp . json ( ) . catch ( ( ) => ( { } ) ) ;
if ( ! resp . ok ) throw new Error ( data . error || resp . statusText || ` status ${ resp . status } ` ) ;
status . value = data . status || "unknown" ;
2026-01-02 10:27:02 -03:00
requestUsername . value = data . username || "" ;
2026-01-04 21:57:31 -03:00
onboarding . value = data . onboarding || { required _steps : [ ] , optional _steps : [ ] , completed _steps : [ ] , optional : { } } ;
2026-01-04 22:49:34 -03:00
keycloakPasswordRotationRequested . value = Boolean ( data . onboarding ? . keycloak ? . password _rotation _requested ) ;
2026-01-03 04:55:03 -03:00
tasks . value = Array . isArray ( data . tasks ) ? data . tasks : [ ] ;
blocked . value = Boolean ( data . blocked ) ;
2026-01-04 22:49:34 -03:00
initialPassword . value = data . initial _password || "" ;
2026-01-02 01:34:18 -03:00
} catch ( err ) {
error . value = err . message || "Failed to check status" ;
2026-01-03 04:55:03 -03:00
tasks . value = [ ] ;
blocked . value = false ;
2026-01-04 22:49:34 -03:00
keycloakPasswordRotationRequested . value = false ;
2026-01-02 01:34:18 -03:00
} finally {
loading . value = false ;
}
}
2026-01-02 11:12:43 -03:00
async function copyInitialPassword ( ) {
if ( ! initialPassword . value ) return ;
try {
if ( navigator ? . clipboard ? . writeText ) {
await navigator . clipboard . writeText ( initialPassword . value ) ;
} else {
const textarea = document . createElement ( "textarea" ) ;
textarea . value = initialPassword . value ;
textarea . setAttribute ( "readonly" , "" ) ;
textarea . style . position = "fixed" ;
textarea . style . top = "-9999px" ;
textarea . style . left = "-9999px" ;
document . body . appendChild ( textarea ) ;
textarea . select ( ) ;
textarea . setSelectionRange ( 0 , textarea . value . length ) ;
document . execCommand ( "copy" ) ;
document . body . removeChild ( textarea ) ;
}
copied . value = true ;
setTimeout ( ( ) => ( copied . value = false ) , 1500 ) ;
} catch ( err ) {
error . value = err ? . message || "Failed to copy password" ;
}
}
2026-01-04 08:44:25 -03:00
async function copyUsername ( ) {
if ( ! requestUsername . value ) return ;
try {
if ( navigator ? . clipboard ? . writeText ) {
await navigator . clipboard . writeText ( requestUsername . value ) ;
} else {
const textarea = document . createElement ( "textarea" ) ;
textarea . value = requestUsername . value ;
textarea . setAttribute ( "readonly" , "" ) ;
textarea . style . position = "fixed" ;
textarea . style . top = "-9999px" ;
textarea . style . left = "-9999px" ;
document . body . appendChild ( textarea ) ;
textarea . select ( ) ;
textarea . setSelectionRange ( 0 , textarea . value . length ) ;
document . execCommand ( "copy" ) ;
document . body . removeChild ( textarea ) ;
}
usernameCopied . value = true ;
setTimeout ( ( ) => ( usernameCopied . value = false ) , 1500 ) ;
} catch ( err ) {
error . value = err ? . message || "Failed to copy username" ;
}
}
2026-01-02 09:42:06 -03:00
async function loginToContinue ( ) {
2026-01-04 08:44:25 -03:00
const trimmedCode = requestCode . value . trim ( ) ;
const hint = requestUsername . value . trim ( ) || trimmedCode . split ( "~" , 1 ) [ 0 ] || "" ;
await login ( ` /onboarding?code= ${ encodeURIComponent ( trimmedCode ) } ` , hint ) ;
2026-01-02 09:42:06 -03:00
}
2026-01-04 21:57:31 -03:00
async function setMfaOptional ( state ) {
if ( ! auth . authenticated ) {
error . value = "Log in to update onboarding steps." ;
return ;
}
if ( isMfaBlocked ( ) ) {
2026-01-04 22:49:34 -03:00
error . value = "Rotate your Keycloak password first." ;
2026-01-04 21:57:31 -03:00
return ;
}
if ( state !== "done" && state !== "skipped" ) return ;
error . value = "" ;
loading . value = true ;
try {
const resp = await authFetch ( "/api/access/request/onboarding/mfa" , {
method : "POST" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON . stringify ( { request _code : requestCode . value . trim ( ) , state } ) ,
} ) ;
const data = await resp . json ( ) . catch ( ( ) => ( { } ) ) ;
if ( ! resp . ok ) throw new Error ( data . error || resp . statusText || ` status ${ resp . status } ` ) ;
status . value = data . status || status . value ;
onboarding . value = data . onboarding || onboarding . value ;
} catch ( err ) {
error . value = err . message || "Failed to update MFA step" ;
} finally {
loading . value = false ;
}
}
2026-01-04 22:49:34 -03:00
async function requestKeycloakPasswordRotation ( ) {
if ( ! auth . authenticated ) {
error . value = "Log in to request password rotation." ;
return ;
}
if ( isStepBlocked ( "keycloak_password_rotated" ) ) {
error . value = "Complete earlier onboarding steps first." ;
return ;
}
if ( keycloakPasswordRotationRequested . value ) return ;
error . value = "" ;
loading . value = true ;
try {
const resp = await authFetch ( "/api/access/request/onboarding/keycloak-password-rotate" , {
method : "POST" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON . stringify ( { request _code : requestCode . value . trim ( ) } ) ,
} ) ;
const data = await resp . json ( ) . catch ( ( ) => ( { } ) ) ;
if ( ! resp . ok ) throw new Error ( data . error || resp . statusText || ` status ${ resp . status } ` ) ;
status . value = data . status || status . value ;
onboarding . value = data . onboarding || onboarding . value ;
keycloakPasswordRotationRequested . value = Boolean ( data . onboarding ? . keycloak ? . password _rotation _requested ) ;
} catch ( err ) {
error . value = err . message || "Failed to request password rotation" ;
} finally {
loading . value = false ;
}
}
2026-01-02 09:42:06 -03:00
async function toggleStep ( step , event ) {
const checked = Boolean ( event ? . target ? . checked ) ;
if ( ! auth . authenticated ) {
event ? . preventDefault ? . ( ) ;
return ;
}
2026-01-04 22:49:34 -03:00
if ( step === "keycloak_password_rotated" ) {
2026-01-03 00:57:14 -03:00
event ? . preventDefault ? . ( ) ;
return ;
}
2026-01-04 13:00:42 -03:00
if ( step === "element_recovery_key" ) {
event ? . preventDefault ? . ( ) ;
return ;
}
2026-01-02 09:42:06 -03:00
error . value = "" ;
loading . value = true ;
try {
const resp = await authFetch ( "/api/access/request/onboarding/attest" , {
method : "POST" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON . stringify ( { request _code : requestCode . value . trim ( ) , step , completed : checked } ) ,
} ) ;
const data = await resp . json ( ) . catch ( ( ) => ( { } ) ) ;
if ( ! resp . ok ) throw new Error ( data . error || resp . statusText || ` status ${ resp . status } ` ) ;
status . value = data . status || status . value ;
onboarding . value = data . onboarding || onboarding . value ;
} catch ( err ) {
error . value = err . message || "Failed to update onboarding" ;
} finally {
loading . value = false ;
}
}
2026-01-04 13:00:42 -03:00
async function sha256Hex ( text ) {
const encoder = new TextEncoder ( ) ;
const data = encoder . encode ( text ) ;
const digest = await crypto . subtle . digest ( "SHA-256" , data ) ;
return Array . from ( new Uint8Array ( digest ) )
. map ( ( byte ) => byte . toString ( 16 ) . padStart ( 2 , "0" ) )
. join ( "" ) ;
}
async function verifyElementRecoveryKey ( ) {
if ( ! auth . authenticated ) {
error . value = "Log in to verify your recovery key." ;
return ;
}
if ( isStepBlocked ( "element_recovery_key" ) ) {
error . value = "Complete earlier onboarding steps first." ;
return ;
}
const raw = elementRecoveryKey . value . trim ( ) ;
if ( ! raw ) return ;
error . value = "" ;
loading . value = true ;
try {
const hash = await sha256Hex ( raw ) ;
const resp = await authFetch ( "/api/access/request/onboarding/element-recovery" , {
method : "POST" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON . stringify ( { request _code : requestCode . value . trim ( ) , sha256 : hash } ) ,
} ) ;
const data = await resp . json ( ) . catch ( ( ) => ( { } ) ) ;
if ( ! resp . ok ) throw new Error ( data . error || resp . statusText || ` status ${ resp . status } ` ) ;
status . value = data . status || status . value ;
onboarding . value = data . onboarding || onboarding . value ;
elementRecoveryKey . value = "" ;
} catch ( err ) {
error . value = err . message || "Failed to verify recovery key" ;
} finally {
loading . value = false ;
}
}
2026-01-02 01:34:18 -03:00
onMounted ( async ( ) => {
const code = route . query . code || route . query . request _code || "" ;
if ( typeof code === "string" && code . trim ( ) ) {
requestCode . value = code . trim ( ) ;
await check ( ) ;
}
} ) ;
< / script >
< style scoped >
. page {
max - width : 960 px ;
margin : 0 auto ;
padding : 32 px 22 px 72 px ;
}
. hero {
margin - bottom : 12 px ;
padding : 18 px ;
}
. eyebrow {
text - transform : uppercase ;
letter - spacing : 0.08 em ;
color : var ( -- text - muted ) ;
margin : 0 0 6 px ;
font - size : 13 px ;
}
h1 {
margin : 0 0 6 px ;
font - size : 32 px ;
}
. lede {
margin : 0 ;
color : var ( -- text - muted ) ;
max - width : 640 px ;
}
. module {
padding : 18 px ;
}
. module - head {
display : flex ;
align - items : center ;
justify - content : space - between ;
gap : 18 px ;
}
. status - form {
display : flex ;
gap : 10 px ;
margin - top : 12 px ;
}
2026-01-02 10:27:02 -03:00
. status - meta {
margin - top : 12 px ;
padding : 12 px ;
border - radius : 14 px ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.1 ) ;
background : rgba ( 0 , 0 , 0 , 0.18 ) ;
}
. meta - row {
display : flex ;
align - items : center ;
justify - content : space - between ;
gap : 14 px ;
}
. meta - row . label {
color : var ( -- text - muted ) ;
}
. meta - row . value {
color : var ( -- text - strong ) ;
}
2026-01-02 01:34:18 -03:00
. input {
flex : 1 ;
padding : 10 px 12 px ;
border - radius : 10 px ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.14 ) ;
background : rgba ( 0 , 0 , 0 , 0.25 ) ;
color : var ( -- text - primary ) ;
}
button . primary {
background : linear - gradient ( 90 deg , # 4 f8bff , # 7 dd0ff ) ;
color : # 0b1 222 ;
padding : 10 px 14 px ;
border : none ;
border - radius : 10 px ;
cursor : pointer ;
font - weight : 700 ;
}
. steps {
margin - top : 16 px ;
}
. steps h3 {
margin : 0 0 8 px ;
}
2026-01-02 09:42:06 -03:00
. onboarding - head {
display : flex ;
align - items : center ;
justify - content : space - between ;
gap : 12 px ;
margin - bottom : 8 px ;
}
. login - callout {
margin - top : 10 px ;
display : flex ;
align - items : center ;
justify - content : space - between ;
gap : 14 px ;
padding : 12 px ;
border - radius : 14 px ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.1 ) ;
background : rgba ( 0 , 0 , 0 , 0.18 ) ;
}
. checklist {
margin : 14 px 0 0 ;
padding : 0 ;
list - style : none ;
display : grid ;
gap : 12 px ;
}
. check - item {
border : 1 px solid rgba ( 255 , 255 , 255 , 0.08 ) ;
border - radius : 14 px ;
padding : 12 px 12 px 10 px ;
background : rgba ( 255 , 255 , 255 , 0.02 ) ;
}
2026-01-04 23:34:21 -03:00
. check - item . blocked {
opacity : 0.55 ;
}
. check - item . active {
border - color : rgba ( 125 , 208 , 255 , 0.45 ) ;
background : rgba ( 79 , 139 , 255 , 0.08 ) ;
box - shadow : 0 0 0 1 px rgba ( 79 , 139 , 255 , 0.2 ) ;
}
. check - item . done {
border - color : rgba ( 92 , 214 , 167 , 0.35 ) ;
background : rgba ( 92 , 214 , 167 , 0.05 ) ;
}
. check - item . skipped {
border - color : rgba ( 146 , 158 , 182 , 0.25 ) ;
}
. check - item . done label ,
. check - item . active label {
color : var ( -- text - primary ) ;
}
2026-01-02 09:42:06 -03:00
. check - item label {
display : flex ;
align - items : center ;
gap : 10 px ;
font - weight : 650 ;
color : var ( -- text - strong ) ;
}
2026-01-03 00:57:14 -03:00
. auto - pill {
margin - left : auto ;
font - size : 12 px ;
padding : 3 px 10 px ;
border - radius : 999 px ;
}
2026-01-02 09:42:06 -03:00
. check - item input [ type = "checkbox" ] {
width : 18 px ;
height : 18 px ;
}
2026-01-04 13:00:42 -03:00
. recovery - verify {
display : flex ;
gap : 10 px ;
margin - top : 10 px ;
align - items : stretch ;
}
. recovery - verify . input {
flex : 1 ;
}
. recovery - verify . verify {
min - width : 96 px ;
}
2026-01-04 21:57:31 -03:00
. mfa - row {
display : flex ;
align - items : flex - start ;
justify - content : space - between ;
gap : 12 px ;
}
. mfa - title {
font - weight : 650 ;
color : var ( -- text - strong ) ;
}
. mfa - description {
margin : 6 px 0 0 ;
}
. mfa - actions {
display : flex ;
justify - content : flex - end ;
gap : 10 px ;
margin - top : 10 px ;
}
button . secondary {
padding : 10 px 14 px ;
border - radius : 10 px ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.14 ) ;
background : rgba ( 0 , 0 , 0 , 0.22 ) ;
color : var ( -- text - primary ) ;
cursor : pointer ;
font - weight : 650 ;
}
. mfa - qr {
margin - top : 12 px ;
border - radius : 12 px ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.08 ) ;
background : rgba ( 0 , 0 , 0 , 0.16 ) ;
padding : 10 px 12 px ;
}
. mfa - qr summary {
cursor : pointer ;
color : var ( -- text - muted ) ;
}
. mfa - qr - grid {
margin - top : 12 px ;
display : grid ;
grid - template - columns : repeat ( auto - fit , minmax ( 210 px , 1 fr ) ) ;
gap : 12 px ;
}
. mfa - qr - card {
display : grid ;
justify - items : center ;
gap : 8 px ;
padding : 12 px ;
border - radius : 12 px ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.08 ) ;
background : rgba ( 255 , 255 , 255 , 0.02 ) ;
}
. mfa - qr - label {
color : var ( -- text - muted ) ;
font - size : 12 px ;
}
. mfa - qr - img {
width : 180 px ;
height : 180 px ;
border - radius : 10 px ;
background : # ffffff ;
padding : 6 px ;
}
. mfa - qr - link {
color : rgba ( 125 , 208 , 255 , 0.9 ) ;
font - size : 12 px ;
text - decoration : none ;
}
. mfa - error {
margin - top : 10 px ;
}
2026-01-05 00:06:41 -03:00
. howto {
margin - top : 10 px ;
border - radius : 12 px ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.08 ) ;
background : rgba ( 0 , 0 , 0 , 0.16 ) ;
padding : 10 px 12 px ;
}
. howto summary {
cursor : pointer ;
color : var ( -- text - muted ) ;
}
. howto - list {
margin : 10 px 0 0 ;
padding - left : 18 px ;
display : grid ;
gap : 6 px ;
}
2026-01-04 13:00:42 -03:00
@ media ( max - width : 560 px ) {
. recovery - verify {
flex - direction : column ;
}
. recovery - verify . verify {
width : 100 % ;
}
}
2026-01-02 09:42:06 -03:00
. ready - box {
margin - top : 14 px ;
padding : 14 px ;
border - radius : 14 px ;
border : 1 px solid rgba ( 120 , 180 , 255 , 0.25 ) ;
background : rgba ( 120 , 180 , 255 , 0.06 ) ;
}
2026-01-02 11:12:43 -03:00
. initial - password {
margin - top : 14 px ;
padding : 14 px ;
border - radius : 14 px ;
border : 1 px solid rgba ( 255 , 220 , 120 , 0.25 ) ;
background : rgba ( 255 , 220 , 120 , 0.06 ) ;
}
2026-01-03 04:55:03 -03:00
. task - box {
margin - top : 14 px ;
padding : 14 px ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.08 ) ;
border - radius : 14 px ;
background : rgba ( 0 , 0 , 0 , 0.25 ) ;
}
. task - list {
list - style : none ;
padding : 0 ;
margin : 0 ;
display : grid ;
gap : 10 px ;
}
. task - row {
display : grid ;
gap : 6 px ;
grid - template - columns : 1 fr auto ;
align - items : center ;
}
. task - name {
color : var ( -- text ) ;
}
. task - detail {
grid - column : 1 / - 1 ;
color : var ( -- text - muted ) ;
font - size : 12 px ;
}
2026-01-02 11:12:43 -03:00
. request - code - row {
margin - top : 12 px ;
display : flex ;
flex - direction : column ;
gap : 6 px ;
}
. copy {
display : inline - flex ;
align - items : center ;
gap : 10 px ;
border - radius : 12 px ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.14 ) ;
background : rgba ( 0 , 0 , 0 , 0.22 ) ;
color : var ( -- text - primary ) ;
padding : 10 px 12 px ;
cursor : pointer ;
}
. copied {
font - size : 12 px ;
color : rgba ( 120 , 255 , 160 , 0.9 ) ;
}
2026-01-02 01:34:18 -03:00
. steps ol {
margin : 0 ;
padding - left : 18 px ;
color : var ( -- text - muted ) ;
}
. muted {
color : var ( -- text - muted ) ;
}
. error - box {
margin - top : 12 px ;
border - radius : 12 px ;
border : 1 px solid rgba ( 255 , 87 , 87 , 0.5 ) ;
background : rgba ( 255 , 87 , 87 , 0.06 ) ;
padding : 10 px 12 px ;
}
< / style >