portal: refine onboarding guides and account access

This commit is contained in:
Brad Stein 2026-01-23 16:06:06 -03:00
parent 27ece883cd
commit e339e17bd4
61 changed files with 57 additions and 27 deletions

View File

@ -396,6 +396,8 @@ def require_account_access() -> tuple[bool, Any]:
if not settings.ACCOUNT_ALLOWED_GROUPS:
return True, None
groups = set(getattr(g, "keycloak_groups", []) or [])
if not groups:
return True, None
if groups.intersection(settings.ACCOUNT_ALLOWED_GROUPS):
return True, None
return False, (jsonify({"error": "forbidden"}), 403)

View File

@ -160,6 +160,7 @@ ONBOARDING_STEPS: tuple[str, ...] = (
"nextcloud_mobile_app",
"budget_encryption_ack",
"firefly_password_rotated",
"firefly_mobile_app",
"wger_password_rotated",
"jellyfin_web_access",
"jellyfin_mobile_app",
@ -170,6 +171,7 @@ ONBOARDING_OPTIONAL_STEPS: set[str] = {
"element_mobile_app",
"nextcloud_desktop_app",
"nextcloud_mobile_app",
"firefly_mobile_app",
"jellyfin_web_access",
"jellyfin_mobile_app",
"jellyfin_tv_setup",

View File

@ -54,6 +54,7 @@ def register(app) -> None:
vaultwarden_email = ""
vaultwarden_status = ""
vaultwarden_synced_at = ""
vaultwarden_master_set_at = ""
jellyfin_status = "ready"
jellyfin_sync_status = "unknown"
jellyfin_sync_detail = ""
@ -137,6 +138,11 @@ def register(app) -> None:
vaultwarden_synced_at = str(raw_vw_synced[0])
elif isinstance(raw_vw_synced, str) and raw_vw_synced:
vaultwarden_synced_at = raw_vw_synced
raw_vw_master = attrs.get("vaultwarden_master_password_set_at")
if isinstance(raw_vw_master, list) and raw_vw_master:
vaultwarden_master_set_at = str(raw_vw_master[0])
elif isinstance(raw_vw_master, str) and raw_vw_master:
vaultwarden_master_set_at = raw_vw_master
user_id = user.get("id") if isinstance(user, dict) else None
if user_id and (
@ -150,6 +156,7 @@ def register(app) -> None:
or not vaultwarden_email
or not vaultwarden_status
or not vaultwarden_synced_at
or not vaultwarden_master_set_at
):
full = admin_client().get_user(str(user_id))
if not keycloak_email:
@ -229,6 +236,15 @@ def register(app) -> None:
vaultwarden_synced_at = str(raw_vw_synced[0])
elif isinstance(raw_vw_synced, str) and raw_vw_synced:
vaultwarden_synced_at = raw_vw_synced
if not vaultwarden_master_set_at:
raw_vw_master = attrs.get("vaultwarden_master_password_set_at")
if isinstance(raw_vw_master, list) and raw_vw_master:
vaultwarden_master_set_at = str(raw_vw_master[0])
elif isinstance(raw_vw_master, str) and raw_vw_master:
vaultwarden_master_set_at = raw_vw_master
if vaultwarden_master_set_at:
vaultwarden_status = "ready"
except Exception:
mailu_status = "unavailable"
nextcloud_mail_status = "unavailable"

View File

@ -118,7 +118,7 @@
<div class="actions">
<button class="primary" type="button" :disabled="mailu.rotating" @click="rotateMailu">
{{ mailu.rotating ? "Rotating..." : "Rotate mail app password" }}
{{ mailu.rotating ? "Resetting..." : "Reset mail app password" }}
</button>
</div>
@ -192,23 +192,23 @@
</div>
<div class="account-stack">
<div class="card module">
<div class="card module" :style="{ order: vaultwardenOrder }">
<div class="module-head">
<h2>Vaultwarden</h2>
<span
class="pill mono"
:class="
vaultwarden.status === 'ready' || vaultwarden.status === 'already_present'
vaultwardenReady
? 'pill-ok'
: vaultwarden.status === 'unavailable' || vaultwarden.status === 'error'
? 'pill-bad'
: ''
"
>
{{ vaultwarden.status }}
{{ vaultwardenDisplayStatus }}
</span>
</div>
<p v-if="vaultwarden.status !== 'ready' && vaultwarden.status !== 'already_present'" class="muted">
<p v-if="!vaultwardenReady" class="muted">
Password manager for Atlas accounts. Store your Element recovery key here. Signups are admin-provisioned.
</p>
<div class="kv">
@ -233,7 +233,7 @@
</div>
</div>
<div class="card module">
<div class="card module" :style="{ order: 1 }">
<div class="module-head">
<h2>Wger</h2>
<span
@ -298,7 +298,7 @@
</div>
</div>
<div class="card module">
<div class="card module" :style="{ order: 2 }">
<div class="module-head">
<h2>Jellyfin</h2>
<span
@ -418,7 +418,7 @@
</template>
<script setup>
import { onMounted, reactive, ref, watch } from "vue";
import { computed, onMounted, reactive, ref, watch } from "vue";
import { auth, authFetch, login } from "@/auth";
const mailu = reactive({
@ -489,6 +489,9 @@ const admin = reactive({
selectedFlags: {},
});
const onboardingUrl = ref("/onboarding");
const vaultwardenReady = computed(() => ["ready", "already_present", "active"].includes(vaultwarden.status));
const vaultwardenDisplayStatus = computed(() => (vaultwardenReady.value ? "ready" : vaultwarden.status));
const vaultwardenOrder = computed(() => (vaultwardenReady.value ? 3 : 0));
const doLogin = () => login("/account");

View File

@ -341,6 +341,7 @@ const STEP_PREREQS = {
nextcloud_mobile_app: ["nextcloud_web_access"],
budget_encryption_ack: ["nextcloud_mail_integration"],
firefly_password_rotated: ["element_recovery_key"],
firefly_mobile_app: ["firefly_password_rotated"],
wger_password_rotated: ["firefly_password_rotated"],
jellyfin_web_access: ["vaultwarden_master_password"],
jellyfin_mobile_app: ["jellyfin_web_access"],
@ -489,21 +490,21 @@ const SECTION_DEFS = [
{
id: "budget",
title: "Budget Encryption",
description: "Protect sensitive data and keep the shared storage budget predictable.",
description: "Encrypt financial data inside Actual Budget and store the key safely.",
steps: [
{
id: "budget_encryption_ack",
title: "Encrypt sensitive data before you upload",
title: "Enable encryption inside Actual Budget",
action: "checkbox",
description:
"Atlas storage is shared, backed up, and budgeted. Encrypt anything sensitive before uploading. If it would hurt to leak, encrypt it.",
"Actual Budget does not encrypt by default. Open Settings → Encryption, enable it, and store the key in Vaultwarden.",
bullets: [
"Use a modern tool (age, GPG) or store secrets directly in Vaultwarden.",
"When in doubt, encrypt first and ask questions later.",
"Keep the encryption key only in Vaultwarden.",
"If you lose the key, your budget data cannot be recovered.",
],
links: [
{ href: "https://age-encryption.org", text: "age" },
{ href: "https://www.gnupg.org", text: "GnuPG" },
{ href: "https://budget.bstein.dev", text: "budget.bstein.dev" },
{ href: "https://vault.bstein.dev", text: "Vaultwarden" },
],
guide: { service: "budget", step: "step1_encrypt_data" },
},
@ -526,6 +527,18 @@ const SECTION_DEFS = [
],
guide: { service: "firefly", step: "step1_web_access" },
},
{
id: "firefly_mobile_app",
title: "Optional: set up the mobile app",
action: "checkbox",
description:
"Install Abacus (Firefly III), connect to money.bstein.dev, and keep the OAuth credentials in Vaultwarden.",
links: [
{ href: "https://github.com/vgsmar/Abacus/releases", text: "Abacus releases" },
{ href: "/account", text: "Account credentials" },
],
guide: { service: "firefly", step: "step2_mobile_app" },
},
],
},
{
@ -1232,8 +1245,8 @@ button.copy:disabled {
.stepper-meta {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 4px;
justify-content: center;
gap: 8px;
flex-wrap: nowrap;
color: var(--text-muted);
}
@ -1473,21 +1486,15 @@ button.copy:disabled {
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(0, 0, 0, 0.2);
padding: 0;
position: relative;
cursor: zoom-in;
}
.guide-shot figcaption {
position: absolute;
top: 8px;
left: 8px;
right: 8px;
font-size: 12px;
margin: 0;
padding: 10px 12px 6px;
font-size: 15px;
font-weight: 600;
color: var(--text-strong);
background: rgba(0, 0, 0, 0.65);
padding: 4px 8px;
border-radius: 8px;
z-index: 2;
}
.guide-shot img {

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB