onboarding: reveal temp password on demand
This commit is contained in:
parent
3ff868a3ed
commit
7893c787b8
@ -694,6 +694,9 @@ def register(app) -> None:
|
|||||||
code = (payload.get("request_code") or payload.get("code") or "").strip()
|
code = (payload.get("request_code") or payload.get("code") or "").strip()
|
||||||
if not code:
|
if not code:
|
||||||
return jsonify({"error": "request_code is required"}), 400
|
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(
|
if not rate_limit_allow(
|
||||||
f"{ip}:{code}",
|
f"{ip}:{code}",
|
||||||
@ -852,15 +855,16 @@ 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"}:
|
if status in {"awaiting_onboarding", "ready"} and reveal_initial_password:
|
||||||
password = row.get("initial_password")
|
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 and revealed_at is None:
|
if isinstance(password, str) and password:
|
||||||
response["initial_password"] = password
|
response["initial_password"] = password
|
||||||
conn.execute(
|
if revealed_at is None:
|
||||||
"UPDATE access_requests SET initial_password_revealed_at = NOW() WHERE request_code = %s AND initial_password_revealed_at IS NULL",
|
conn.execute(
|
||||||
(code,),
|
"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"}:
|
if status in {"awaiting_onboarding", "ready"}:
|
||||||
response["onboarding_url"] = f"/onboarding?code={code}"
|
response["onboarding_url"] = f"/onboarding?code={code}"
|
||||||
if status in {"awaiting_onboarding", "ready"}:
|
if status in {"awaiting_onboarding", "ready"}:
|
||||||
|
|||||||
@ -226,3 +226,49 @@ class AccessRequestTests(TestCase):
|
|||||||
data = resp.get_json()
|
data = resp.get_json()
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
self.assertTrue(data.get("email_verified"))
|
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")
|
||||||
|
|||||||
@ -108,7 +108,7 @@
|
|||||||
<div class="stepper-title">{{ index + 1 }}. {{ section.title }}</div>
|
<div class="stepper-title">{{ index + 1 }}. {{ section.title }}</div>
|
||||||
<div class="stepper-meta">
|
<div class="stepper-meta">
|
||||||
<span class="pill mono" :class="sectionPillClass(section)">{{ sectionStatusLabel(section) }}</span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@ -149,11 +149,6 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</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-shell" v-if="activeSection">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<div>
|
<div>
|
||||||
@ -294,7 +289,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, ref } from "vue";
|
import { computed, onMounted, ref } from "vue";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import { auth, authFetch, login } from "../auth";
|
import { auth, authFetch } from "../auth";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
@ -764,7 +759,10 @@ async function check() {
|
|||||||
const resp = await fetch("/api/access/request/status", {
|
const resp = await fetch("/api/access/request/status", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
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(() => ({}));
|
const data = await resp.json().catch(() => ({}));
|
||||||
if (!resp.ok) throw new Error(data.error || resp.statusText || `status ${resp.status}`);
|
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));
|
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) {
|
async function toggleStep(stepId, event) {
|
||||||
const checked = Boolean(event?.target?.checked);
|
const checked = Boolean(event?.target?.checked);
|
||||||
if (!auth.authenticated) {
|
if (!auth.authenticated) {
|
||||||
@ -1118,15 +1111,14 @@ button.secondary {
|
|||||||
.section-stepper {
|
.section-stepper {
|
||||||
margin: 16px 0 18px;
|
margin: 16px 0 18px;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-wrap: wrap;
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stepper-item {
|
.stepper-item {
|
||||||
flex: 1 1 180px;
|
min-width: 0;
|
||||||
min-width: 170px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.stepper-card {
|
.stepper-card {
|
||||||
@ -1143,17 +1135,6 @@ button.secondary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.stepper-card::after {
|
.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;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1183,10 +1164,20 @@ button.secondary {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 12px;
|
gap: 6px 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
color: var(--text-muted);
|
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 {
|
.stepper-card.active {
|
||||||
border-color: rgba(125, 208, 255, 0.5);
|
border-color: rgba(125, 208, 255, 0.5);
|
||||||
box-shadow: 0 0 0 1px rgba(79, 139, 255, 0.3);
|
box-shadow: 0 0 0 1px rgba(79, 139, 255, 0.3);
|
||||||
@ -1211,9 +1202,21 @@ button.secondary {
|
|||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.section-stepper {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 860px) {
|
@media (max-width: 860px) {
|
||||||
.stepper-card::after {
|
.section-stepper {
|
||||||
display: none;
|
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;
|
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 {
|
.section-shell {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
padding-top: 12px;
|
padding-top: 12px;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user