refactor(bstein-home): extract account dashboard flow
This commit is contained in:
parent
4ad9803c0c
commit
0273da9e79
497
frontend/src/account/useAccountDashboard.js
Normal file
497
frontend/src/account/useAccountDashboard.js
Normal file
@ -0,0 +1,497 @@
|
||||
import { computed, onMounted, reactive, ref, watch } from "vue";
|
||||
import { auth, authFetch, login } from "@/auth";
|
||||
|
||||
/**
|
||||
* Build the Account page dashboard and admin action state.
|
||||
*
|
||||
* WHY: this page coordinates several downstream services plus admin
|
||||
* approval flows, so isolating the orchestration keeps the SFC readable and
|
||||
* gives tests a direct seam for service state behavior.
|
||||
*
|
||||
* @returns {object} reactive service cards, admin state, and event handlers.
|
||||
*/
|
||||
export function useAccountDashboard() {
|
||||
const mailu = reactive({
|
||||
status: "loading",
|
||||
imap: "mail.bstein.dev:993 (TLS)",
|
||||
smtp: "mail.bstein.dev:587 (STARTTLS)",
|
||||
username: "",
|
||||
currentPassword: "",
|
||||
revealPassword: false,
|
||||
rotating: false,
|
||||
newPassword: "",
|
||||
error: "",
|
||||
});
|
||||
|
||||
const jellyfin = reactive({
|
||||
status: "loading",
|
||||
username: "",
|
||||
syncStatus: "",
|
||||
syncDetail: "",
|
||||
error: "",
|
||||
});
|
||||
|
||||
const vaultwarden = reactive({
|
||||
status: "loading",
|
||||
username: "",
|
||||
syncedAt: "",
|
||||
error: "",
|
||||
});
|
||||
|
||||
const nextcloudMail = reactive({
|
||||
status: "loading",
|
||||
primaryEmail: "",
|
||||
accountCount: "",
|
||||
syncedAt: "",
|
||||
syncing: false,
|
||||
error: "",
|
||||
});
|
||||
|
||||
const wger = reactive({
|
||||
status: "loading",
|
||||
username: "",
|
||||
password: "",
|
||||
passwordUpdatedAt: "",
|
||||
revealPassword: false,
|
||||
resetting: false,
|
||||
error: "",
|
||||
});
|
||||
|
||||
const firefly = reactive({
|
||||
status: "loading",
|
||||
username: "",
|
||||
password: "",
|
||||
passwordUpdatedAt: "",
|
||||
revealPassword: false,
|
||||
resetting: false,
|
||||
error: "",
|
||||
});
|
||||
|
||||
const admin = reactive({
|
||||
enabled: false,
|
||||
loading: false,
|
||||
requests: [],
|
||||
error: "",
|
||||
acting: {},
|
||||
flags: [],
|
||||
flagsLoading: false,
|
||||
notes: {},
|
||||
selectedFlags: {},
|
||||
});
|
||||
const onboardingUrl = ref("/onboarding");
|
||||
const vaultwardenReady = computed(() =>
|
||||
["ready", "already_present", "active", "grandfathered"].includes(vaultwarden.status),
|
||||
);
|
||||
const vaultwardenDisplayStatus = computed(() => (vaultwardenReady.value ? "ready" : vaultwarden.status));
|
||||
const vaultwardenOrder = computed(() => (vaultwardenReady.value ? 3 : 0));
|
||||
|
||||
const doLogin = () => login("/account");
|
||||
|
||||
const copied = reactive({});
|
||||
const normalizeEmail = (value) => (typeof value === "string" ? value.toLowerCase() : "");
|
||||
onMounted(() => {
|
||||
if (auth.ready && auth.authenticated) {
|
||||
refreshOverview();
|
||||
refreshAdminRequests();
|
||||
refreshAdminFlags();
|
||||
} else {
|
||||
mailu.status = "login required";
|
||||
nextcloudMail.status = "login required";
|
||||
jellyfin.status = "login required";
|
||||
vaultwarden.status = "login required";
|
||||
wger.status = "login required";
|
||||
firefly.status = "login required";
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [auth.ready, auth.authenticated],
|
||||
([ready, authenticated]) => {
|
||||
if (!ready) return;
|
||||
if (!authenticated) {
|
||||
mailu.status = "login required";
|
||||
nextcloudMail.status = "login required";
|
||||
jellyfin.status = "login required";
|
||||
vaultwarden.status = "login required";
|
||||
wger.status = "login required";
|
||||
firefly.status = "login required";
|
||||
onboardingUrl.value = "/onboarding";
|
||||
admin.enabled = false;
|
||||
admin.requests = [];
|
||||
admin.flags = [];
|
||||
return;
|
||||
}
|
||||
refreshOverview();
|
||||
refreshAdminRequests();
|
||||
refreshAdminFlags();
|
||||
},
|
||||
{ immediate: false },
|
||||
);
|
||||
|
||||
async function refreshOverview() {
|
||||
mailu.error = "";
|
||||
jellyfin.error = "";
|
||||
vaultwarden.error = "";
|
||||
nextcloudMail.error = "";
|
||||
wger.error = "";
|
||||
firefly.error = "";
|
||||
try {
|
||||
const resp = await authFetch("/api/account/overview", {
|
||||
headers: { Accept: "application/json" },
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
throw new Error(data?.error || `status ${resp.status}`);
|
||||
}
|
||||
const data = await resp.json();
|
||||
mailu.status = data.mailu?.status || "ready";
|
||||
mailu.username = normalizeEmail(data.mailu?.username) || normalizeEmail(auth.email) || auth.username;
|
||||
mailu.currentPassword = data.mailu?.app_password || "";
|
||||
nextcloudMail.status = data.nextcloud_mail?.status || "unknown";
|
||||
nextcloudMail.primaryEmail = normalizeEmail(data.nextcloud_mail?.primary_email) || "";
|
||||
nextcloudMail.accountCount = data.nextcloud_mail?.account_count || "";
|
||||
nextcloudMail.syncedAt = data.nextcloud_mail?.synced_at || "";
|
||||
wger.status = data.wger?.status || "unknown";
|
||||
wger.username = normalizeEmail(data.wger?.username) || mailu.username || auth.username;
|
||||
wger.password = data.wger?.password || "";
|
||||
wger.passwordUpdatedAt = data.wger?.password_updated_at || "";
|
||||
firefly.status = data.firefly?.status || "unknown";
|
||||
firefly.username = normalizeEmail(data.firefly?.username) || mailu.username || auth.username;
|
||||
firefly.password = data.firefly?.password || "";
|
||||
firefly.passwordUpdatedAt = data.firefly?.password_updated_at || "";
|
||||
vaultwarden.status = data.vaultwarden?.status || "unknown";
|
||||
vaultwarden.username = normalizeEmail(data.vaultwarden?.username) || mailu.username || auth.username;
|
||||
vaultwarden.syncedAt = data.vaultwarden?.synced_at || "";
|
||||
jellyfin.status = data.jellyfin?.status || "ready";
|
||||
jellyfin.username = data.jellyfin?.username || auth.username;
|
||||
jellyfin.syncStatus = data.jellyfin?.sync_status || "";
|
||||
jellyfin.syncDetail = data.jellyfin?.sync_detail || "";
|
||||
onboardingUrl.value = data.onboarding_url || "/onboarding";
|
||||
} catch (err) {
|
||||
mailu.status = "unavailable";
|
||||
nextcloudMail.status = "unavailable";
|
||||
wger.status = "unavailable";
|
||||
firefly.status = "unavailable";
|
||||
vaultwarden.status = "unavailable";
|
||||
jellyfin.status = "unavailable";
|
||||
jellyfin.syncStatus = "";
|
||||
jellyfin.syncDetail = "";
|
||||
onboardingUrl.value = "/onboarding";
|
||||
const message = err?.message ? `Failed to load account status (${err.message})` : "Failed to load account status.";
|
||||
mailu.error = message;
|
||||
nextcloudMail.error = message;
|
||||
wger.error = message;
|
||||
firefly.error = message;
|
||||
vaultwarden.error = message;
|
||||
jellyfin.error = message;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAdminRequests() {
|
||||
if (!auth.authenticated) {
|
||||
admin.enabled = false;
|
||||
admin.requests = [];
|
||||
return;
|
||||
}
|
||||
admin.error = "";
|
||||
admin.loading = true;
|
||||
try {
|
||||
const resp = await authFetch("/api/admin/access/requests", {
|
||||
headers: { Accept: "application/json" },
|
||||
cache: "no-store",
|
||||
});
|
||||
if (resp.status === 403) {
|
||||
admin.enabled = false;
|
||||
admin.requests = [];
|
||||
return;
|
||||
}
|
||||
if (!resp.ok) throw new Error(`status ${resp.status}`);
|
||||
const data = await resp.json();
|
||||
admin.enabled = true;
|
||||
admin.requests = Array.isArray(data.requests) ? data.requests : [];
|
||||
for (const req of admin.requests) {
|
||||
if (!req?.username) continue;
|
||||
if (!(req.username in admin.notes)) admin.notes[req.username] = "";
|
||||
if (!(req.username in admin.selectedFlags)) admin.selectedFlags[req.username] = [];
|
||||
}
|
||||
} catch (err) {
|
||||
admin.enabled = false;
|
||||
admin.requests = [];
|
||||
admin.error = err.message || "Failed to load access requests.";
|
||||
} finally {
|
||||
admin.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAdminFlags() {
|
||||
if (!auth.authenticated) {
|
||||
admin.flags = [];
|
||||
admin.flagsLoading = false;
|
||||
return;
|
||||
}
|
||||
admin.flagsLoading = true;
|
||||
try {
|
||||
const resp = await authFetch("/api/admin/access/flags", {
|
||||
headers: { Accept: "application/json" },
|
||||
cache: "no-store",
|
||||
});
|
||||
if (resp.status === 403) {
|
||||
admin.flags = [];
|
||||
return;
|
||||
}
|
||||
if (!resp.ok) throw new Error(`status ${resp.status}`);
|
||||
const data = await resp.json();
|
||||
admin.flags = Array.isArray(data.flags) ? data.flags : [];
|
||||
} catch (err) {
|
||||
admin.flags = [];
|
||||
admin.error = admin.error || err.message || "Failed to load access flags.";
|
||||
} finally {
|
||||
admin.flagsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function hasFlag(username, flag) {
|
||||
const selected = admin.selectedFlags[username];
|
||||
return Array.isArray(selected) && selected.includes(flag);
|
||||
}
|
||||
|
||||
function formatName(req) {
|
||||
if (!req) return "unknown";
|
||||
const parts = [];
|
||||
if (req.first_name && String(req.first_name).trim()) {
|
||||
parts.push(String(req.first_name).trim());
|
||||
}
|
||||
if (req.last_name && String(req.last_name).trim()) {
|
||||
parts.push(String(req.last_name).trim());
|
||||
}
|
||||
return parts.length ? parts.join(" ") : "unknown";
|
||||
}
|
||||
|
||||
function formatActionError(err, fallback) {
|
||||
const message = err?.message || "";
|
||||
if (!message) return fallback;
|
||||
const normalized = message.toLowerCase();
|
||||
if (normalized.includes("ariadne unavailable") || normalized.includes("status 502") || normalized.includes("status 503")) {
|
||||
return "Ariadne is busy. Please try again in a moment.";
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
function toggleFlag(username, flag, event) {
|
||||
const checked = Boolean(event?.target?.checked);
|
||||
const selected = Array.isArray(admin.selectedFlags[username]) ? [...admin.selectedFlags[username]] : [];
|
||||
const next = checked ? Array.from(new Set([...selected, flag])) : selected.filter((item) => item !== flag);
|
||||
admin.selectedFlags[username] = next;
|
||||
}
|
||||
|
||||
async function rotateMailu() {
|
||||
mailu.error = "";
|
||||
mailu.newPassword = "";
|
||||
mailu.rotating = true;
|
||||
try {
|
||||
const resp = await authFetch("/api/account/mailu/rotate", { method: "POST" });
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok) throw new Error(data.error || `status ${resp.status}`);
|
||||
mailu.newPassword = data.password || "";
|
||||
if (mailu.newPassword) {
|
||||
mailu.currentPassword = mailu.newPassword;
|
||||
mailu.revealPassword = true;
|
||||
}
|
||||
const syncEnabled = Boolean(data.sync_enabled);
|
||||
const syncOk = Boolean(data.sync_ok);
|
||||
const syncError = data.sync_error || "";
|
||||
if (!syncEnabled) {
|
||||
mailu.status = "updated";
|
||||
mailu.error = "Mail sync is not configured; password may not take effect until an admin sync runs.";
|
||||
} else if (!syncOk) {
|
||||
mailu.status = "sync pending";
|
||||
mailu.error = syncError || "Mail sync did not confirm success yet. Try again in a moment.";
|
||||
} else {
|
||||
mailu.status = "updated";
|
||||
}
|
||||
await refreshOverview();
|
||||
} catch (err) {
|
||||
mailu.error = formatActionError(err, "Rotation failed");
|
||||
} finally {
|
||||
mailu.rotating = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function resetWger() {
|
||||
wger.error = "";
|
||||
wger.resetting = true;
|
||||
try {
|
||||
const resp = await authFetch("/api/account/wger/reset", { method: "POST" });
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok) throw new Error(data.error || `status ${resp.status}`);
|
||||
if (data.password) {
|
||||
wger.password = data.password;
|
||||
wger.revealPassword = true;
|
||||
}
|
||||
await refreshOverview();
|
||||
} catch (err) {
|
||||
wger.error = formatActionError(err, "Reset failed");
|
||||
} finally {
|
||||
wger.resetting = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function resetFirefly() {
|
||||
firefly.error = "";
|
||||
firefly.resetting = true;
|
||||
try {
|
||||
const resp = await authFetch("/api/account/firefly/reset", { method: "POST" });
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok) throw new Error(data.error || `status ${resp.status}`);
|
||||
if (data.password) {
|
||||
firefly.password = data.password;
|
||||
firefly.revealPassword = true;
|
||||
}
|
||||
await refreshOverview();
|
||||
} catch (err) {
|
||||
firefly.error = formatActionError(err, "Reset failed");
|
||||
} finally {
|
||||
firefly.resetting = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function syncNextcloudMail() {
|
||||
nextcloudMail.error = "";
|
||||
nextcloudMail.syncing = true;
|
||||
try {
|
||||
const resp = await authFetch("/api/account/nextcloud/mail/sync", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ wait: true }),
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok) throw new Error(data.error || `status ${resp.status}`);
|
||||
await refreshOverview();
|
||||
} catch (err) {
|
||||
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 {
|
||||
nextcloudMail.syncing = false;
|
||||
}
|
||||
}
|
||||
|
||||
function fallbackCopy(text) {
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = text;
|
||||
textarea.setAttribute("readonly", "");
|
||||
textarea.style.position = "fixed";
|
||||
textarea.style.top = "-9999px";
|
||||
textarea.style.left = "-9999px";
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
textarea.setSelectionRange(0, textarea.value.length);
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
|
||||
async function approve(username) {
|
||||
admin.error = "";
|
||||
admin.acting[username] = true;
|
||||
try {
|
||||
const flags = Array.isArray(admin.selectedFlags[username]) ? admin.selectedFlags[username] : [];
|
||||
const note = (admin.notes[username] || "").trim();
|
||||
const payload = {};
|
||||
if (flags.length) payload.flags = flags;
|
||||
if (note) payload.note = note;
|
||||
const resp = await authFetch(`/api/admin/access/requests/${encodeURIComponent(username)}/approve`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
throw new Error(data.error || `status ${resp.status}`);
|
||||
}
|
||||
await refreshAdminRequests();
|
||||
} catch (err) {
|
||||
admin.error = err.message || "Approve failed";
|
||||
} finally {
|
||||
admin.acting[username] = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function deny(username) {
|
||||
admin.error = "";
|
||||
admin.acting[username] = true;
|
||||
try {
|
||||
const note = (admin.notes[username] || "").trim();
|
||||
const payload = note ? { note } : {};
|
||||
const resp = await authFetch(`/api/admin/access/requests/${encodeURIComponent(username)}/deny`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
throw new Error(data.error || `status ${resp.status}`);
|
||||
}
|
||||
await refreshAdminRequests();
|
||||
} catch (err) {
|
||||
admin.error = err.message || "Deny failed";
|
||||
} finally {
|
||||
admin.acting[username] = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function copy(key, text) {
|
||||
if (!text) return;
|
||||
try {
|
||||
if (navigator?.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} else {
|
||||
fallbackCopy(text);
|
||||
}
|
||||
copied[key] = true;
|
||||
window.setTimeout(() => {
|
||||
copied[key] = false;
|
||||
}, 1500);
|
||||
} catch {
|
||||
try {
|
||||
fallbackCopy(text);
|
||||
copied[key] = true;
|
||||
window.setTimeout(() => {
|
||||
copied[key] = false;
|
||||
}, 1500);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
auth,
|
||||
mailu,
|
||||
jellyfin,
|
||||
vaultwarden,
|
||||
nextcloudMail,
|
||||
wger,
|
||||
firefly,
|
||||
admin,
|
||||
onboardingUrl,
|
||||
vaultwardenReady,
|
||||
vaultwardenDisplayStatus,
|
||||
vaultwardenOrder,
|
||||
doLogin,
|
||||
copied,
|
||||
hasFlag,
|
||||
formatName,
|
||||
toggleFlag,
|
||||
rotateMailu,
|
||||
resetWger,
|
||||
resetFirefly,
|
||||
syncNextcloudMail,
|
||||
approve,
|
||||
deny,
|
||||
copy,
|
||||
};
|
||||
}
|
||||
@ -418,465 +418,34 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref, watch } from "vue";
|
||||
import { auth, authFetch, login } from "@/auth";
|
||||
import { useAccountDashboard } from "../account/useAccountDashboard";
|
||||
|
||||
const mailu = reactive({
|
||||
status: "loading",
|
||||
imap: "mail.bstein.dev:993 (TLS)",
|
||||
smtp: "mail.bstein.dev:587 (STARTTLS)",
|
||||
username: "",
|
||||
currentPassword: "",
|
||||
revealPassword: false,
|
||||
rotating: false,
|
||||
newPassword: "",
|
||||
error: "",
|
||||
});
|
||||
|
||||
const jellyfin = reactive({
|
||||
status: "loading",
|
||||
username: "",
|
||||
syncStatus: "",
|
||||
syncDetail: "",
|
||||
error: "",
|
||||
});
|
||||
|
||||
const vaultwarden = reactive({
|
||||
status: "loading",
|
||||
username: "",
|
||||
syncedAt: "",
|
||||
error: "",
|
||||
});
|
||||
|
||||
const nextcloudMail = reactive({
|
||||
status: "loading",
|
||||
primaryEmail: "",
|
||||
accountCount: "",
|
||||
syncedAt: "",
|
||||
syncing: false,
|
||||
error: "",
|
||||
});
|
||||
|
||||
const wger = reactive({
|
||||
status: "loading",
|
||||
username: "",
|
||||
password: "",
|
||||
passwordUpdatedAt: "",
|
||||
revealPassword: false,
|
||||
resetting: false,
|
||||
error: "",
|
||||
});
|
||||
|
||||
const firefly = reactive({
|
||||
status: "loading",
|
||||
username: "",
|
||||
password: "",
|
||||
passwordUpdatedAt: "",
|
||||
revealPassword: false,
|
||||
resetting: false,
|
||||
error: "",
|
||||
});
|
||||
|
||||
const admin = reactive({
|
||||
enabled: false,
|
||||
loading: false,
|
||||
requests: [],
|
||||
error: "",
|
||||
acting: {},
|
||||
flags: [],
|
||||
flagsLoading: false,
|
||||
notes: {},
|
||||
selectedFlags: {},
|
||||
});
|
||||
const onboardingUrl = ref("/onboarding");
|
||||
const vaultwardenReady = computed(() =>
|
||||
["ready", "already_present", "active", "grandfathered"].includes(vaultwarden.status),
|
||||
);
|
||||
const vaultwardenDisplayStatus = computed(() => (vaultwardenReady.value ? "ready" : vaultwarden.status));
|
||||
const vaultwardenOrder = computed(() => (vaultwardenReady.value ? 3 : 0));
|
||||
|
||||
const doLogin = () => login("/account");
|
||||
|
||||
const copied = reactive({});
|
||||
const normalizeEmail = (value) => (typeof value === "string" ? value.toLowerCase() : "");
|
||||
onMounted(() => {
|
||||
if (auth.ready && auth.authenticated) {
|
||||
refreshOverview();
|
||||
refreshAdminRequests();
|
||||
refreshAdminFlags();
|
||||
} else {
|
||||
mailu.status = "login required";
|
||||
nextcloudMail.status = "login required";
|
||||
jellyfin.status = "login required";
|
||||
vaultwarden.status = "login required";
|
||||
wger.status = "login required";
|
||||
firefly.status = "login required";
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [auth.ready, auth.authenticated],
|
||||
([ready, authenticated]) => {
|
||||
if (!ready) return;
|
||||
if (!authenticated) {
|
||||
mailu.status = "login required";
|
||||
nextcloudMail.status = "login required";
|
||||
jellyfin.status = "login required";
|
||||
vaultwarden.status = "login required";
|
||||
wger.status = "login required";
|
||||
firefly.status = "login required";
|
||||
onboardingUrl.value = "/onboarding";
|
||||
admin.enabled = false;
|
||||
admin.requests = [];
|
||||
admin.flags = [];
|
||||
return;
|
||||
}
|
||||
refreshOverview();
|
||||
refreshAdminRequests();
|
||||
refreshAdminFlags();
|
||||
},
|
||||
{ immediate: false },
|
||||
);
|
||||
|
||||
async function refreshOverview() {
|
||||
mailu.error = "";
|
||||
jellyfin.error = "";
|
||||
vaultwarden.error = "";
|
||||
nextcloudMail.error = "";
|
||||
wger.error = "";
|
||||
firefly.error = "";
|
||||
try {
|
||||
const resp = await authFetch("/api/account/overview", {
|
||||
headers: { Accept: "application/json" },
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
throw new Error(data?.error || `status ${resp.status}`);
|
||||
}
|
||||
const data = await resp.json();
|
||||
mailu.status = data.mailu?.status || "ready";
|
||||
mailu.username = normalizeEmail(data.mailu?.username) || normalizeEmail(auth.email) || auth.username;
|
||||
mailu.currentPassword = data.mailu?.app_password || "";
|
||||
nextcloudMail.status = data.nextcloud_mail?.status || "unknown";
|
||||
nextcloudMail.primaryEmail = normalizeEmail(data.nextcloud_mail?.primary_email) || "";
|
||||
nextcloudMail.accountCount = data.nextcloud_mail?.account_count || "";
|
||||
nextcloudMail.syncedAt = data.nextcloud_mail?.synced_at || "";
|
||||
wger.status = data.wger?.status || "unknown";
|
||||
wger.username = normalizeEmail(data.wger?.username) || mailu.username || auth.username;
|
||||
wger.password = data.wger?.password || "";
|
||||
wger.passwordUpdatedAt = data.wger?.password_updated_at || "";
|
||||
firefly.status = data.firefly?.status || "unknown";
|
||||
firefly.username = normalizeEmail(data.firefly?.username) || mailu.username || auth.username;
|
||||
firefly.password = data.firefly?.password || "";
|
||||
firefly.passwordUpdatedAt = data.firefly?.password_updated_at || "";
|
||||
vaultwarden.status = data.vaultwarden?.status || "unknown";
|
||||
vaultwarden.username = normalizeEmail(data.vaultwarden?.username) || mailu.username || auth.username;
|
||||
vaultwarden.syncedAt = data.vaultwarden?.synced_at || "";
|
||||
jellyfin.status = data.jellyfin?.status || "ready";
|
||||
jellyfin.username = data.jellyfin?.username || auth.username;
|
||||
jellyfin.syncStatus = data.jellyfin?.sync_status || "";
|
||||
jellyfin.syncDetail = data.jellyfin?.sync_detail || "";
|
||||
onboardingUrl.value = data.onboarding_url || "/onboarding";
|
||||
} catch (err) {
|
||||
mailu.status = "unavailable";
|
||||
nextcloudMail.status = "unavailable";
|
||||
wger.status = "unavailable";
|
||||
firefly.status = "unavailable";
|
||||
vaultwarden.status = "unavailable";
|
||||
jellyfin.status = "unavailable";
|
||||
jellyfin.syncStatus = "";
|
||||
jellyfin.syncDetail = "";
|
||||
onboardingUrl.value = "/onboarding";
|
||||
const message = err?.message ? `Failed to load account status (${err.message})` : "Failed to load account status.";
|
||||
mailu.error = message;
|
||||
nextcloudMail.error = message;
|
||||
wger.error = message;
|
||||
firefly.error = message;
|
||||
vaultwarden.error = message;
|
||||
jellyfin.error = message;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAdminRequests() {
|
||||
if (!auth.authenticated) {
|
||||
admin.enabled = false;
|
||||
admin.requests = [];
|
||||
return;
|
||||
}
|
||||
admin.error = "";
|
||||
admin.loading = true;
|
||||
try {
|
||||
const resp = await authFetch("/api/admin/access/requests", {
|
||||
headers: { Accept: "application/json" },
|
||||
cache: "no-store",
|
||||
});
|
||||
if (resp.status === 403) {
|
||||
admin.enabled = false;
|
||||
admin.requests = [];
|
||||
return;
|
||||
}
|
||||
if (!resp.ok) throw new Error(`status ${resp.status}`);
|
||||
const data = await resp.json();
|
||||
admin.enabled = true;
|
||||
admin.requests = Array.isArray(data.requests) ? data.requests : [];
|
||||
for (const req of admin.requests) {
|
||||
if (!req?.username) continue;
|
||||
if (!(req.username in admin.notes)) admin.notes[req.username] = "";
|
||||
if (!(req.username in admin.selectedFlags)) admin.selectedFlags[req.username] = [];
|
||||
}
|
||||
} catch (err) {
|
||||
admin.enabled = false;
|
||||
admin.requests = [];
|
||||
admin.error = err.message || "Failed to load access requests.";
|
||||
} finally {
|
||||
admin.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAdminFlags() {
|
||||
if (!auth.authenticated) {
|
||||
admin.flags = [];
|
||||
admin.flagsLoading = false;
|
||||
return;
|
||||
}
|
||||
admin.flagsLoading = true;
|
||||
try {
|
||||
const resp = await authFetch("/api/admin/access/flags", {
|
||||
headers: { Accept: "application/json" },
|
||||
cache: "no-store",
|
||||
});
|
||||
if (resp.status === 403) {
|
||||
admin.flags = [];
|
||||
return;
|
||||
}
|
||||
if (!resp.ok) throw new Error(`status ${resp.status}`);
|
||||
const data = await resp.json();
|
||||
admin.flags = Array.isArray(data.flags) ? data.flags : [];
|
||||
} catch (err) {
|
||||
admin.flags = [];
|
||||
admin.error = admin.error || err.message || "Failed to load access flags.";
|
||||
} finally {
|
||||
admin.flagsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function hasFlag(username, flag) {
|
||||
const selected = admin.selectedFlags[username];
|
||||
return Array.isArray(selected) && selected.includes(flag);
|
||||
}
|
||||
|
||||
function formatName(req) {
|
||||
if (!req) return "unknown";
|
||||
const parts = [];
|
||||
if (req.first_name && String(req.first_name).trim()) {
|
||||
parts.push(String(req.first_name).trim());
|
||||
}
|
||||
if (req.last_name && String(req.last_name).trim()) {
|
||||
parts.push(String(req.last_name).trim());
|
||||
}
|
||||
return parts.length ? parts.join(" ") : "unknown";
|
||||
}
|
||||
|
||||
function formatActionError(err, fallback) {
|
||||
const message = err?.message || "";
|
||||
if (!message) return fallback;
|
||||
const normalized = message.toLowerCase();
|
||||
if (normalized.includes("ariadne unavailable") || normalized.includes("status 502") || normalized.includes("status 503")) {
|
||||
return "Ariadne is busy. Please try again in a moment.";
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
function toggleFlag(username, flag, event) {
|
||||
const checked = Boolean(event?.target?.checked);
|
||||
const selected = Array.isArray(admin.selectedFlags[username]) ? [...admin.selectedFlags[username]] : [];
|
||||
const next = checked ? Array.from(new Set([...selected, flag])) : selected.filter((item) => item !== flag);
|
||||
admin.selectedFlags[username] = next;
|
||||
}
|
||||
|
||||
async function rotateMailu() {
|
||||
mailu.error = "";
|
||||
mailu.newPassword = "";
|
||||
mailu.rotating = true;
|
||||
try {
|
||||
const resp = await authFetch("/api/account/mailu/rotate", { method: "POST" });
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok) throw new Error(data.error || `status ${resp.status}`);
|
||||
mailu.newPassword = data.password || "";
|
||||
if (mailu.newPassword) {
|
||||
mailu.currentPassword = mailu.newPassword;
|
||||
mailu.revealPassword = true;
|
||||
}
|
||||
const syncEnabled = Boolean(data.sync_enabled);
|
||||
const syncOk = Boolean(data.sync_ok);
|
||||
const syncError = data.sync_error || "";
|
||||
if (!syncEnabled) {
|
||||
mailu.status = "updated";
|
||||
mailu.error = "Mail sync is not configured; password may not take effect until an admin sync runs.";
|
||||
} else if (!syncOk) {
|
||||
mailu.status = "sync pending";
|
||||
mailu.error = syncError || "Mail sync did not confirm success yet. Try again in a moment.";
|
||||
} else {
|
||||
mailu.status = "updated";
|
||||
}
|
||||
await refreshOverview();
|
||||
} catch (err) {
|
||||
mailu.error = formatActionError(err, "Rotation failed");
|
||||
} finally {
|
||||
mailu.rotating = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function resetWger() {
|
||||
wger.error = "";
|
||||
wger.resetting = true;
|
||||
try {
|
||||
const resp = await authFetch("/api/account/wger/reset", { method: "POST" });
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok) throw new Error(data.error || `status ${resp.status}`);
|
||||
if (data.password) {
|
||||
wger.password = data.password;
|
||||
wger.revealPassword = true;
|
||||
}
|
||||
await refreshOverview();
|
||||
} catch (err) {
|
||||
wger.error = formatActionError(err, "Reset failed");
|
||||
} finally {
|
||||
wger.resetting = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function resetFirefly() {
|
||||
firefly.error = "";
|
||||
firefly.resetting = true;
|
||||
try {
|
||||
const resp = await authFetch("/api/account/firefly/reset", { method: "POST" });
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok) throw new Error(data.error || `status ${resp.status}`);
|
||||
if (data.password) {
|
||||
firefly.password = data.password;
|
||||
firefly.revealPassword = true;
|
||||
}
|
||||
await refreshOverview();
|
||||
} catch (err) {
|
||||
firefly.error = formatActionError(err, "Reset failed");
|
||||
} finally {
|
||||
firefly.resetting = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function syncNextcloudMail() {
|
||||
nextcloudMail.error = "";
|
||||
nextcloudMail.syncing = true;
|
||||
try {
|
||||
const resp = await authFetch("/api/account/nextcloud/mail/sync", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ wait: true }),
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok) throw new Error(data.error || `status ${resp.status}`);
|
||||
await refreshOverview();
|
||||
} catch (err) {
|
||||
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 {
|
||||
nextcloudMail.syncing = false;
|
||||
}
|
||||
}
|
||||
|
||||
function fallbackCopy(text) {
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = text;
|
||||
textarea.setAttribute("readonly", "");
|
||||
textarea.style.position = "fixed";
|
||||
textarea.style.top = "-9999px";
|
||||
textarea.style.left = "-9999px";
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
textarea.setSelectionRange(0, textarea.value.length);
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
|
||||
async function approve(username) {
|
||||
admin.error = "";
|
||||
admin.acting[username] = true;
|
||||
try {
|
||||
const flags = Array.isArray(admin.selectedFlags[username]) ? admin.selectedFlags[username] : [];
|
||||
const note = (admin.notes[username] || "").trim();
|
||||
const payload = {};
|
||||
if (flags.length) payload.flags = flags;
|
||||
if (note) payload.note = note;
|
||||
const resp = await authFetch(`/api/admin/access/requests/${encodeURIComponent(username)}/approve`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
throw new Error(data.error || `status ${resp.status}`);
|
||||
}
|
||||
await refreshAdminRequests();
|
||||
} catch (err) {
|
||||
admin.error = err.message || "Approve failed";
|
||||
} finally {
|
||||
admin.acting[username] = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function deny(username) {
|
||||
admin.error = "";
|
||||
admin.acting[username] = true;
|
||||
try {
|
||||
const note = (admin.notes[username] || "").trim();
|
||||
const payload = note ? { note } : {};
|
||||
const resp = await authFetch(`/api/admin/access/requests/${encodeURIComponent(username)}/deny`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
throw new Error(data.error || `status ${resp.status}`);
|
||||
}
|
||||
await refreshAdminRequests();
|
||||
} catch (err) {
|
||||
admin.error = err.message || "Deny failed";
|
||||
} finally {
|
||||
admin.acting[username] = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function copy(key, text) {
|
||||
if (!text) return;
|
||||
try {
|
||||
if (navigator?.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} else {
|
||||
fallbackCopy(text);
|
||||
}
|
||||
copied[key] = true;
|
||||
window.setTimeout(() => {
|
||||
copied[key] = false;
|
||||
}, 1500);
|
||||
} catch (err) {
|
||||
try {
|
||||
fallbackCopy(text);
|
||||
copied[key] = true;
|
||||
window.setTimeout(() => {
|
||||
copied[key] = false;
|
||||
}, 1500);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
const {
|
||||
auth,
|
||||
mailu,
|
||||
jellyfin,
|
||||
vaultwarden,
|
||||
nextcloudMail,
|
||||
wger,
|
||||
firefly,
|
||||
admin,
|
||||
onboardingUrl,
|
||||
vaultwardenReady,
|
||||
vaultwardenDisplayStatus,
|
||||
vaultwardenOrder,
|
||||
doLogin,
|
||||
copied,
|
||||
hasFlag,
|
||||
formatName,
|
||||
toggleFlag,
|
||||
rotateMailu,
|
||||
resetWger,
|
||||
resetFirefly,
|
||||
syncNextcloudMail,
|
||||
approve,
|
||||
deny,
|
||||
copy,
|
||||
} = useAccountDashboard();
|
||||
</script>
|
||||
|
||||
<style scoped src="../styles/account.css"></style>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user