refactor(bstein-home): extract account dashboard flow

This commit is contained in:
codex 2026-04-21 06:57:07 -03:00
parent 4ad9803c0c
commit 0273da9e79
2 changed files with 524 additions and 458 deletions

View 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,
};
}

View File

@ -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>