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: if not settings.ACCOUNT_ALLOWED_GROUPS:
return True, None return True, None
groups = set(getattr(g, "keycloak_groups", []) or []) groups = set(getattr(g, "keycloak_groups", []) or [])
if not groups:
return True, None
if groups.intersection(settings.ACCOUNT_ALLOWED_GROUPS): if groups.intersection(settings.ACCOUNT_ALLOWED_GROUPS):
return True, None return True, None
return False, (jsonify({"error": "forbidden"}), 403) return False, (jsonify({"error": "forbidden"}), 403)

View File

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

View File

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

View File

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

View File

@ -341,6 +341,7 @@ const STEP_PREREQS = {
nextcloud_mobile_app: ["nextcloud_web_access"], nextcloud_mobile_app: ["nextcloud_web_access"],
budget_encryption_ack: ["nextcloud_mail_integration"], budget_encryption_ack: ["nextcloud_mail_integration"],
firefly_password_rotated: ["element_recovery_key"], firefly_password_rotated: ["element_recovery_key"],
firefly_mobile_app: ["firefly_password_rotated"],
wger_password_rotated: ["firefly_password_rotated"], wger_password_rotated: ["firefly_password_rotated"],
jellyfin_web_access: ["vaultwarden_master_password"], jellyfin_web_access: ["vaultwarden_master_password"],
jellyfin_mobile_app: ["jellyfin_web_access"], jellyfin_mobile_app: ["jellyfin_web_access"],
@ -489,21 +490,21 @@ const SECTION_DEFS = [
{ {
id: "budget", id: "budget",
title: "Budget Encryption", 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: [ steps: [
{ {
id: "budget_encryption_ack", id: "budget_encryption_ack",
title: "Encrypt sensitive data before you upload", title: "Enable encryption inside Actual Budget",
action: "checkbox", action: "checkbox",
description: 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: [ bullets: [
"Use a modern tool (age, GPG) or store secrets directly in Vaultwarden.", "Keep the encryption key only in Vaultwarden.",
"When in doubt, encrypt first and ask questions later.", "If you lose the key, your budget data cannot be recovered.",
], ],
links: [ links: [
{ href: "https://age-encryption.org", text: "age" }, { href: "https://budget.bstein.dev", text: "budget.bstein.dev" },
{ href: "https://www.gnupg.org", text: "GnuPG" }, { href: "https://vault.bstein.dev", text: "Vaultwarden" },
], ],
guide: { service: "budget", step: "step1_encrypt_data" }, guide: { service: "budget", step: "step1_encrypt_data" },
}, },
@ -526,6 +527,18 @@ const SECTION_DEFS = [
], ],
guide: { service: "firefly", step: "step1_web_access" }, 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 { .stepper-meta {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: center;
gap: 4px; gap: 8px;
flex-wrap: nowrap; flex-wrap: nowrap;
color: var(--text-muted); color: var(--text-muted);
} }
@ -1473,21 +1486,15 @@ button.copy:disabled {
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(0, 0, 0, 0.2); background: rgba(0, 0, 0, 0.2);
padding: 0; padding: 0;
position: relative;
cursor: zoom-in; cursor: zoom-in;
} }
.guide-shot figcaption { .guide-shot figcaption {
position: absolute; margin: 0;
top: 8px; padding: 10px 12px 6px;
left: 8px; font-size: 15px;
right: 8px; font-weight: 600;
font-size: 12px;
color: var(--text-strong); color: var(--text-strong);
background: rgba(0, 0, 0, 0.65);
padding: 4px 8px;
border-radius: 8px;
z-index: 2;
} }
.guide-shot img { .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