portal: refine onboarding guides and account access
@ -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)
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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");
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 358 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 236 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 91 KiB |
BIN
media/onboarding/firefly/step2_mobile_app/7_Authorize Access.jpg
Normal file
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 391 KiB |
|
After Width: | Height: | Size: 228 KiB |
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 319 KiB |
|
After Width: | Height: | Size: 500 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 257 KiB |
|
After Width: | Height: | Size: 238 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 35 KiB |
BIN
media/onboarding/nextcloud/step4_mobile_app/5_Grant Access.jpg
Normal file
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |