portal: improve onboarding guidance and rotation
This commit is contained in:
parent
27fbad1f05
commit
647c954739
@ -1147,6 +1147,13 @@ def register(app) -> None:
|
||||
missing = sorted(prerequisites - current_completed)
|
||||
if missing:
|
||||
return jsonify({"error": "step is blocked", "blocked_by": missing}), 409
|
||||
if step in {"vaultwarden_master_password", "vaultwarden_store_temp_password"}:
|
||||
if not _password_rotation_requested(conn, code):
|
||||
try:
|
||||
_request_keycloak_password_rotation(conn, code, request_username)
|
||||
except Exception:
|
||||
return jsonify({"error": "failed to request keycloak password rotation"}), 502
|
||||
|
||||
if step == "vaultwarden_master_password":
|
||||
if vaultwarden_claim and not username:
|
||||
return jsonify({"error": "login required"}), 401
|
||||
@ -1159,10 +1166,6 @@ def register(app) -> None:
|
||||
return jsonify({"error": "vaultwarden claim not allowed"}), 403
|
||||
if vaultwarden_claim and not admin_client().ready():
|
||||
return jsonify({"error": "keycloak admin unavailable"}), 503
|
||||
try:
|
||||
_request_keycloak_password_rotation(conn, code, request_username)
|
||||
except Exception:
|
||||
return jsonify({"error": "failed to request keycloak password rotation"}), 502
|
||||
if request_username and admin_client().ready():
|
||||
try:
|
||||
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
@ -52,3 +52,9 @@ p {
|
||||
.page > section + section {
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.page {
|
||||
padding: 24px 16px 56px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -498,6 +498,7 @@ const vaultwardenOrder = computed(() => (vaultwardenReady.value ? 3 : 0));
|
||||
const doLogin = () => login("/account");
|
||||
|
||||
const copied = reactive({});
|
||||
const normalizeEmail = (value) => (typeof value === "string" ? value.toLowerCase() : "");
|
||||
onMounted(() => {
|
||||
if (auth.ready && auth.authenticated) {
|
||||
refreshOverview();
|
||||
@ -555,22 +556,22 @@ async function refreshOverview() {
|
||||
}
|
||||
const data = await resp.json();
|
||||
mailu.status = data.mailu?.status || "ready";
|
||||
mailu.username = data.mailu?.username || auth.email || auth.username;
|
||||
mailu.username = normalizeEmail(data.mailu?.username) || normalizeEmail(auth.email) || auth.username;
|
||||
mailu.currentPassword = data.mailu?.app_password || "";
|
||||
nextcloudMail.status = data.nextcloud_mail?.status || "unknown";
|
||||
nextcloudMail.primaryEmail = data.nextcloud_mail?.primary_email || "";
|
||||
nextcloudMail.primaryEmail = normalizeEmail(data.nextcloud_mail?.primary_email) || "";
|
||||
nextcloudMail.accountCount = data.nextcloud_mail?.account_count || "";
|
||||
nextcloudMail.syncedAt = data.nextcloud_mail?.synced_at || "";
|
||||
wger.status = data.wger?.status || "unknown";
|
||||
wger.username = data.wger?.username || mailu.username || auth.username;
|
||||
wger.username = normalizeEmail(data.wger?.username) || mailu.username || auth.username;
|
||||
wger.password = data.wger?.password || "";
|
||||
wger.passwordUpdatedAt = data.wger?.password_updated_at || "";
|
||||
firefly.status = data.firefly?.status || "unknown";
|
||||
firefly.username = data.firefly?.username || mailu.username || auth.username;
|
||||
firefly.username = normalizeEmail(data.firefly?.username) || mailu.username || auth.username;
|
||||
firefly.password = data.firefly?.password || "";
|
||||
firefly.passwordUpdatedAt = data.firefly?.password_updated_at || "";
|
||||
vaultwarden.status = data.vaultwarden?.status || "unknown";
|
||||
vaultwarden.username = data.vaultwarden?.username || mailu.username || auth.username;
|
||||
vaultwarden.username = normalizeEmail(data.vaultwarden?.username) || mailu.username || auth.username;
|
||||
vaultwarden.syncedAt = data.vaultwarden?.synced_at || "";
|
||||
jellyfin.status = data.jellyfin?.status || "ready";
|
||||
jellyfin.username = data.jellyfin?.username || auth.username;
|
||||
@ -778,7 +779,12 @@ async function syncNextcloudMail() {
|
||||
if (!resp.ok) throw new Error(data.error || `status ${resp.status}`);
|
||||
await refreshOverview();
|
||||
} catch (err) {
|
||||
nextcloudMail.error = formatActionError(err, "Sync failed");
|
||||
const message = formatActionError(err, "Sync failed");
|
||||
if (message.toLowerCase().includes("ariadne is busy")) {
|
||||
nextcloudMail.error = "Ariadne is busy. Refresh in a moment; the sync may have completed.";
|
||||
} else {
|
||||
nextcloudMail.error = message;
|
||||
}
|
||||
} finally {
|
||||
nextcloudMail.syncing = false;
|
||||
}
|
||||
@ -1090,6 +1096,42 @@ button.primary {
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.page {
|
||||
padding: 24px 16px 56px;
|
||||
}
|
||||
|
||||
.hero {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.secret-head {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.req-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.admin {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
@ -116,6 +116,10 @@
|
||||
</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>
|
||||
@ -207,7 +211,7 @@
|
||||
</div>
|
||||
|
||||
<details v-if="step.guide" class="guide-details" :open="shouldOpenGuide(step, activeSection)">
|
||||
<summary class="mono">Photo guide</summary>
|
||||
<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>
|
||||
@ -358,7 +362,9 @@ const vaultwardenLoginEmail = computed(() => {
|
||||
}
|
||||
return "your @bstein.dev address";
|
||||
});
|
||||
const vaultwardenLoginEmailLower = computed(() => (vaultwardenLoginEmail.value || "").toLowerCase());
|
||||
const mailAddress = computed(() => (requestUsername.value ? `${requestUsername.value}@bstein.dev` : "your @bstein.dev address"));
|
||||
const mailAddressLower = computed(() => (mailAddress.value || "").toLowerCase());
|
||||
|
||||
const STEP_PREREQS = {
|
||||
vaultwarden_master_password: [],
|
||||
@ -750,16 +756,16 @@ function isStepBlocked(stepId) {
|
||||
|
||||
function stepNote(step) {
|
||||
if (step.id === "vaultwarden_master_password") {
|
||||
return `Vaultwarden uses an email login. Use ${vaultwardenLoginEmail.value} to sign in.`;
|
||||
return `Vaultwarden uses an email login. Use ${vaultwardenLoginEmailLower.value} to sign in.`;
|
||||
}
|
||||
if (step.id === "vaultwarden_store_temp_password") {
|
||||
return "Store the temporary Keycloak password in Vaultwarden so you can rotate it safely later.";
|
||||
}
|
||||
if (step.id === "firefly_password_rotated") {
|
||||
return `Firefly uses an email login. Use ${mailAddress.value} to sign in.`;
|
||||
return `Firefly uses an email login. Use ${mailAddressLower.value} to sign in.`;
|
||||
}
|
||||
if (step.id === "mail_client_setup") {
|
||||
return `Your mailbox address is ${mailAddress.value}.`;
|
||||
return `Your mailbox address is ${mailAddressLower.value}.`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
@ -1479,6 +1485,15 @@ button.copy:disabled {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.credential-head {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.credential-head h4 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.credential-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
@ -1647,6 +1662,33 @@ button.copy:disabled {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.guide-summary {
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(92, 214, 167, 0.5);
|
||||
background: rgba(92, 214, 167, 0.15);
|
||||
color: var(--text-strong);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.guide-summary::after {
|
||||
content: "Tap to open";
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.guide-details[open] .guide-summary::after {
|
||||
content: "Tap to close";
|
||||
}
|
||||
|
||||
.guide-summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.guide-groups {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
@ -1806,10 +1848,41 @@ button.copy:disabled {
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.page {
|
||||
padding: 24px 16px 56px;
|
||||
}
|
||||
|
||||
.status-form {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.onboarding-head {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.credential-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.step-head {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.auto-pill {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.section-actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.step-actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.section-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
|
||||
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
Loading…
x
Reference in New Issue
Block a user