portal: refine onboarding guides and account access
@ -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)
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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");
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
|
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 |