onboarding: reveal temp password on demand

This commit is contained in:
Brad Stein 2026-01-22 18:48:19 -03:00
parent 3ff868a3ed
commit 7893c787b8
3 changed files with 90 additions and 49 deletions

View File

@ -694,6 +694,9 @@ def register(app) -> None:
code = (payload.get("request_code") or payload.get("code") or "").strip()
if not code:
return jsonify({"error": "request_code is required"}), 400
reveal_initial_password = bool(
payload.get("reveal_initial_password") or payload.get("reveal_password")
)
if not rate_limit_allow(
f"{ip}:{code}",
@ -852,15 +855,16 @@ def register(app) -> None:
response["tasks"] = tasks
response["automation_complete"] = provision_tasks_complete(conn, code)
response["blocked"] = blocked
if status in {"awaiting_onboarding", "ready"}:
if status in {"awaiting_onboarding", "ready"} and reveal_initial_password:
password = row.get("initial_password")
revealed_at = row.get("initial_password_revealed_at")
if isinstance(password, str) and password and revealed_at is None:
if isinstance(password, str) and password:
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,),
)
if revealed_at is None:
conn.execute(
"UPDATE access_requests SET initial_password_revealed_at = NOW() WHERE request_code = %s AND initial_password_revealed_at IS NULL",
(code,),
)
if status in {"awaiting_onboarding", "ready"}:
response["onboarding_url"] = f"/onboarding?code={code}"
if status in {"awaiting_onboarding", "ready"}:

View File

@ -226,3 +226,49 @@ class AccessRequestTests(TestCase):
data = resp.get_json()
self.assertEqual(resp.status_code, 200)
self.assertTrue(data.get("email_verified"))
def test_status_hides_initial_password_without_reveal_flag(self):
rows = {
"SELECT status": {
"status": "awaiting_onboarding",
"username": "alice",
"initial_password": "temp-pass",
"initial_password_revealed_at": None,
"email_verified_at": None,
}
}
with (
mock.patch.object(ar, "connect", lambda: dummy_connect(rows)),
mock.patch.object(ar, "_advance_status", lambda *args, **kwargs: "awaiting_onboarding"),
):
resp = self.client.post(
"/api/access/request/status",
data=json.dumps({"request_code": "alice~CODE123"}),
content_type="application/json",
)
data = resp.get_json()
self.assertEqual(resp.status_code, 200)
self.assertIsNone(data.get("initial_password"))
def test_status_reveals_initial_password_with_flag(self):
rows = {
"SELECT status": {
"status": "awaiting_onboarding",
"username": "alice",
"initial_password": "temp-pass",
"initial_password_revealed_at": None,
"email_verified_at": None,
}
}
with (
mock.patch.object(ar, "connect", lambda: dummy_connect(rows)),
mock.patch.object(ar, "_advance_status", lambda *args, **kwargs: "awaiting_onboarding"),
):
resp = self.client.post(
"/api/access/request/status",
data=json.dumps({"request_code": "alice~CODE123", "reveal_initial_password": True}),
content_type="application/json",
)
data = resp.get_json()
self.assertEqual(resp.status_code, 200)
self.assertEqual(data.get("initial_password"), "temp-pass")

View File

@ -108,7 +108,7 @@
<div class="stepper-title">{{ index + 1 }}. {{ section.title }}</div>
<div class="stepper-meta">
<span class="pill mono" :class="sectionPillClass(section)">{{ sectionStatusLabel(section) }}</span>
<span class="mono">{{ sectionProgress(section) }}</span>
<span class="pill mono pill-compact">{{ sectionProgress(section) }}</span>
</div>
</div>
</button>
@ -149,11 +149,6 @@
</p>
</div>
<div v-if="!auth.authenticated" class="login-callout">
<p class="muted">Log in to check off onboarding steps and verify your Element recovery key.</p>
<button class="primary" type="button" @click="loginToContinue" :disabled="loading">Log in</button>
</div>
<div class="section-shell" v-if="activeSection">
<div class="section-header">
<div>
@ -294,7 +289,7 @@
<script setup>
import { computed, onMounted, ref } from "vue";
import { useRoute } from "vue-router";
import { auth, authFetch, login } from "../auth";
import { auth, authFetch } from "../auth";
const route = useRoute();
@ -764,7 +759,10 @@ async function check() {
const resp = await fetch("/api/access/request/status", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ request_code: requestCode.value.trim() }),
body: JSON.stringify({
request_code: requestCode.value.trim(),
reveal_initial_password: true,
}),
});
const data = await resp.json().catch(() => ({}));
if (!resp.ok) throw new Error(data.error || resp.statusText || `status ${resp.status}`);
@ -825,11 +823,6 @@ function copyUsername() {
copyText(requestUsername.value, (value) => (usernameCopied.value = value));
}
async function loginToContinue() {
const hint = requestUsername.value.trim() || requestCode.value.split("~", 1)[0] || "";
await login(`/onboarding?code=${encodeURIComponent(requestCode.value.trim())}`, hint);
}
async function toggleStep(stepId, event) {
const checked = Boolean(event?.target?.checked);
if (!auth.authenticated) {
@ -1118,15 +1111,14 @@ button.secondary {
.section-stepper {
margin: 16px 0 18px;
list-style: none;
display: flex;
flex-wrap: wrap;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
padding: 0;
}
.stepper-item {
flex: 1 1 180px;
min-width: 170px;
min-width: 0;
}
.stepper-card {
@ -1143,17 +1135,6 @@ button.secondary {
}
.stepper-card::after {
content: "";
position: absolute;
left: 24px;
right: -12px;
top: 18px;
height: 2px;
background: rgba(255, 255, 255, 0.08);
z-index: 0;
}
.stepper-item:last-child .stepper-card::after {
display: none;
}
@ -1183,10 +1164,20 @@ button.secondary {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
gap: 6px 10px;
flex-wrap: wrap;
color: var(--text-muted);
}
.pill-compact {
padding: 6px 10px;
font-size: 12px;
white-space: nowrap;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
.stepper-card.active {
border-color: rgba(125, 208, 255, 0.5);
box-shadow: 0 0 0 1px rgba(79, 139, 255, 0.3);
@ -1211,9 +1202,21 @@ button.secondary {
opacity: 0.6;
}
@media (max-width: 1200px) {
.section-stepper {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 860px) {
.stepper-card::after {
display: none;
.section-stepper {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 520px) {
.section-stepper {
grid-template-columns: 1fr;
}
}
@ -1247,18 +1250,6 @@ button.secondary {
flex: 1;
}
.login-callout {
margin-top: 14px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
padding: 12px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(0, 0, 0, 0.18);
}
.section-shell {
margin-top: 16px;
padding-top: 12px;