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)
|
missing = sorted(prerequisites - current_completed)
|
||||||
if missing:
|
if missing:
|
||||||
return jsonify({"error": "step is blocked", "blocked_by": missing}), 409
|
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 step == "vaultwarden_master_password":
|
||||||
if vaultwarden_claim and not username:
|
if vaultwarden_claim and not username:
|
||||||
return jsonify({"error": "login required"}), 401
|
return jsonify({"error": "login required"}), 401
|
||||||
@ -1159,10 +1166,6 @@ def register(app) -> None:
|
|||||||
return jsonify({"error": "vaultwarden claim not allowed"}), 403
|
return jsonify({"error": "vaultwarden claim not allowed"}), 403
|
||||||
if vaultwarden_claim and not admin_client().ready():
|
if vaultwarden_claim and not admin_client().ready():
|
||||||
return jsonify({"error": "keycloak admin unavailable"}), 503
|
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():
|
if request_username and admin_client().ready():
|
||||||
try:
|
try:
|
||||||
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
|||||||
@ -52,3 +52,9 @@ p {
|
|||||||
.page > section + section {
|
.page > section + section {
|
||||||
margin-top: 32px;
|
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 doLogin = () => login("/account");
|
||||||
|
|
||||||
const copied = reactive({});
|
const copied = reactive({});
|
||||||
|
const normalizeEmail = (value) => (typeof value === "string" ? value.toLowerCase() : "");
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (auth.ready && auth.authenticated) {
|
if (auth.ready && auth.authenticated) {
|
||||||
refreshOverview();
|
refreshOverview();
|
||||||
@ -555,22 +556,22 @@ async function refreshOverview() {
|
|||||||
}
|
}
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
mailu.status = data.mailu?.status || "ready";
|
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 || "";
|
mailu.currentPassword = data.mailu?.app_password || "";
|
||||||
nextcloudMail.status = data.nextcloud_mail?.status || "unknown";
|
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.accountCount = data.nextcloud_mail?.account_count || "";
|
||||||
nextcloudMail.syncedAt = data.nextcloud_mail?.synced_at || "";
|
nextcloudMail.syncedAt = data.nextcloud_mail?.synced_at || "";
|
||||||
wger.status = data.wger?.status || "unknown";
|
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.password = data.wger?.password || "";
|
||||||
wger.passwordUpdatedAt = data.wger?.password_updated_at || "";
|
wger.passwordUpdatedAt = data.wger?.password_updated_at || "";
|
||||||
firefly.status = data.firefly?.status || "unknown";
|
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.password = data.firefly?.password || "";
|
||||||
firefly.passwordUpdatedAt = data.firefly?.password_updated_at || "";
|
firefly.passwordUpdatedAt = data.firefly?.password_updated_at || "";
|
||||||
vaultwarden.status = data.vaultwarden?.status || "unknown";
|
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 || "";
|
vaultwarden.syncedAt = data.vaultwarden?.synced_at || "";
|
||||||
jellyfin.status = data.jellyfin?.status || "ready";
|
jellyfin.status = data.jellyfin?.status || "ready";
|
||||||
jellyfin.username = data.jellyfin?.username || auth.username;
|
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}`);
|
if (!resp.ok) throw new Error(data.error || `status ${resp.status}`);
|
||||||
await refreshOverview();
|
await refreshOverview();
|
||||||
} catch (err) {
|
} 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 {
|
} finally {
|
||||||
nextcloudMail.syncing = false;
|
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 {
|
.admin {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -116,6 +116,10 @@
|
|||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
<div v-if="requestUsername || initialPassword" class="credential-card">
|
<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-grid">
|
||||||
<div class="credential-field">
|
<div class="credential-field">
|
||||||
<span class="label mono">Username</span>
|
<span class="label mono">Username</span>
|
||||||
@ -207,7 +211,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<details v-if="step.guide" class="guide-details" :open="shouldOpenGuide(step, activeSection)">
|
<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-if="guideGroups(step).length" class="guide-groups">
|
||||||
<div v-for="group in guideGroups(step)" :key="group.id" class="guide-group">
|
<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>
|
<h4 v-if="group.title" class="mono guide-title">{{ group.title }}</h4>
|
||||||
@ -358,7 +362,9 @@ const vaultwardenLoginEmail = computed(() => {
|
|||||||
}
|
}
|
||||||
return "your @bstein.dev address";
|
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 mailAddress = computed(() => (requestUsername.value ? `${requestUsername.value}@bstein.dev` : "your @bstein.dev address"));
|
||||||
|
const mailAddressLower = computed(() => (mailAddress.value || "").toLowerCase());
|
||||||
|
|
||||||
const STEP_PREREQS = {
|
const STEP_PREREQS = {
|
||||||
vaultwarden_master_password: [],
|
vaultwarden_master_password: [],
|
||||||
@ -750,16 +756,16 @@ function isStepBlocked(stepId) {
|
|||||||
|
|
||||||
function stepNote(step) {
|
function stepNote(step) {
|
||||||
if (step.id === "vaultwarden_master_password") {
|
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") {
|
if (step.id === "vaultwarden_store_temp_password") {
|
||||||
return "Store the temporary Keycloak password in Vaultwarden so you can rotate it safely later.";
|
return "Store the temporary Keycloak password in Vaultwarden so you can rotate it safely later.";
|
||||||
}
|
}
|
||||||
if (step.id === "firefly_password_rotated") {
|
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") {
|
if (step.id === "mail_client_setup") {
|
||||||
return `Your mailbox address is ${mailAddress.value}.`;
|
return `Your mailbox address is ${mailAddressLower.value}.`;
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@ -1479,6 +1485,15 @@ button.copy:disabled {
|
|||||||
background: rgba(0, 0, 0, 0.2);
|
background: rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.credential-head {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credential-head h4 {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
.credential-grid {
|
.credential-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
@ -1647,6 +1662,33 @@ button.copy:disabled {
|
|||||||
margin-top: 10px;
|
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 {
|
.guide-groups {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@ -1806,10 +1848,41 @@ button.copy:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
|
.page {
|
||||||
|
padding: 24px 16px 56px;
|
||||||
|
}
|
||||||
|
|
||||||
.status-form {
|
.status-form {
|
||||||
flex-direction: column;
|
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 {
|
.section-actions {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: flex-start;
|
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