import {
getNotificationsEnabled,
setNotificationsEnabled,
getDesktopNotificationsEnabled,
setDesktopNotificationsEnabled,
requestDesktopNotificationPermission,
} from "./notify-client.js";
import { escapeHtml, formatBytes as formatSharedBytes, formatDate as formatSharedDate } from "./format.js";
import { getCertificateTrustGuide } from "./cert-banner.js";
import {
isEasterEggEnabled,
setEasterEggEnabled,
showEnabledEasterEgg,
} from "./easter-egg.js";
import {
animateElement,
animateTextChange,
concealElement,
emphasizeElement,
revealElement,
} from "./motion.js";
const PREF_REFRESH_KEY = "overlord_refresh_interval_seconds";
const NAV_MODE_KEY = "sb_mode";
const NAV_HIDDEN_KEY = "nav_hidden";
const usernameEl = document.getElementById("settings-username");
const roleEl = document.getElementById("settings-role");
const messageEl = document.getElementById("settings-message");
const passwordForm = document.getElementById("password-form");
const currentPasswordInput = document.getElementById("current-password");
const newPasswordInput = document.getElementById("new-password");
const confirmPasswordInput = document.getElementById("confirm-password");
const passwordPolicyHint = document.getElementById("password-policy-hint");
const mfaStatusText = document.getElementById("mfa-status-text");
const mfaStartBtn = document.getElementById("mfa-start-btn");
const mfaSetupPanel = document.getElementById("mfa-setup-panel");
const mfaQrCode = document.getElementById("mfa-qr-code");
const mfaSecretText = document.getElementById("mfa-secret-text");
const mfaEnableCodeInput = document.getElementById("mfa-enable-code");
const mfaEnableBtn = document.getElementById("mfa-enable-btn");
const mfaOtpauthLink = document.getElementById("mfa-otpauth-link");
const mfaDisablePanel = document.getElementById("mfa-disable-panel");
const mfaDisablePasswordInput = document.getElementById("mfa-disable-password");
const mfaDisableCodeInput = document.getElementById("mfa-disable-code");
const mfaDisableBtn = document.getElementById("mfa-disable-btn");
const mfaMessage = document.getElementById("mfa-message");
const prefsForm = document.getElementById("prefs-form");
const prefNotificationsInput = document.getElementById("pref-notifications");
const prefDesktopNotificationsInput = document.getElementById("pref-desktop-notifications");
const prefDesktopNotificationsHint = document.getElementById("pref-desktop-notifications-hint");
const prefRefreshSecondsInput = document.getElementById("pref-refresh-seconds");
const easterEggEnabledInput = document.getElementById("easter-egg-enabled");
const easterEggStatus = document.getElementById("easter-egg-status");
const inputArchiveUserForm = document.getElementById("input-archive-user-form");
const inputArchiveMyEnabledInput = document.getElementById("input-archive-my-enabled");
const inputArchiveAdminForm = document.getElementById("input-archive-admin-form");
const inputArchiveGlobalEnabledInput = document.getElementById("input-archive-global-enabled");
const inputArchiveRetentionDaysInput = document.getElementById("input-archive-retention-days");
const inputArchiveMaxFileMbInput = document.getElementById("input-archive-max-file-mb");
const inputArchivePollSecondsInput = document.getElementById("input-archive-poll-seconds");
const myTelegramChatIdInput = document.getElementById("my-telegram-chat-id");
const saveMyTelegramBtn = document.getElementById("save-my-telegram");
const bansTableBody = document.getElementById("bans-table-body");
const bansPermissionNote = document.getElementById("bans-permission-note");
const refreshBansBtn = document.getElementById("refresh-bans-btn");
const securityForm = document.getElementById("security-form");
const securityPermissionNote = document.getElementById("security-permission-note");
const securitySaveBtn = document.getElementById("security-save-btn");
const securitySessionTtlInput = document.getElementById("security-session-ttl");
const securityLoginMaxAttemptsInput = document.getElementById("security-login-max-attempts");
const securityLoginWindowInput = document.getElementById("security-login-window");
const securityLockoutInput = document.getElementById("security-lockout");
const securityPasswordMinInput = document.getElementById("security-password-min");
const securityRequireUppercaseInput = document.getElementById("security-require-uppercase");
const securityRequireLowercaseInput = document.getElementById("security-require-lowercase");
const securityRequireNumberInput = document.getElementById("security-require-number");
const securityRequireSymbolInput = document.getElementById("security-require-symbol");
const securityMfaAdminsInput = document.getElementById("security-mfa-admins");
const securityMfaNonAdminsInput = document.getElementById("security-mfa-non-admins");
const tlsForm = document.getElementById("tls-form");
const tlsPermissionNote = document.getElementById("tls-permission-note");
const tlsSaveBtn = document.getElementById("tls-save-btn");
const tlsCertbotAutoBtn = document.getElementById("tls-certbot-auto-btn");
const tlsCertbotEmailInput = document.getElementById("tls-certbot-email");
const tlsCertbotEnabledInput = document.getElementById("tls-certbot-enabled");
const tlsCertbotLivePathInput = document.getElementById("tls-certbot-live-path");
const tlsCertbotDomainInput = document.getElementById("tls-certbot-domain");
const tlsCertbotCertFileInput = document.getElementById("tls-certbot-cert-file");
const tlsCertbotKeyFileInput = document.getElementById("tls-certbot-key-file");
const tlsCertbotCaFileInput = document.getElementById("tls-certbot-ca-file");
const tlsActiveCertSource = document.getElementById("tls-active-cert-source");
const tlsDownloadCertBtn = document.getElementById("tls-download-cert-btn");
const tlsTrustHelpBtn = document.getElementById("tls-trust-help-btn");
const tlsTrustInstructions = document.getElementById("tls-trust-instructions");
const oidcForm = document.getElementById("oidc-form");
const oidcPermissionNote = document.getElementById("oidc-permission-note");
const oidcSaveBtn = document.getElementById("oidc-save-btn");
const oidcEnabledInput = document.getElementById("oidc-enabled");
const oidcLabelInput = document.getElementById("oidc-label");
const oidcIssuerInput = document.getElementById("oidc-issuer");
const oidcClientIdInput = document.getElementById("oidc-client-id");
const oidcClientSecretInput = document.getElementById("oidc-client-secret");
const oidcSecretHint = document.getElementById("oidc-secret-hint");
const oidcRedirectUriInput = document.getElementById("oidc-redirect-uri");
const oidcScopesInput = document.getElementById("oidc-scopes");
const oidcClientAuthMethodInput = document.getElementById("oidc-client-auth-method");
const oidcAutoProvisionInput = document.getElementById("oidc-auto-provision");
const oidcAllowEmailLinkInput = document.getElementById("oidc-allow-email-link");
const oidcDefaultRoleInput = document.getElementById("oidc-default-role");
const oidcAllowedDomainsInput = document.getElementById("oidc-allowed-domains");
const oidcAllowedEmailsInput = document.getElementById("oidc-allowed-emails");
const oidcGroupClaimInput = document.getElementById("oidc-group-claim");
const oidcAdminGroupsInput = document.getElementById("oidc-admin-groups");
const oidcOperatorGroupsInput = document.getElementById("oidc-operator-groups");
const oidcViewerGroupsInput = document.getElementById("oidc-viewer-groups");
const appearanceForm = document.getElementById("appearance-form");
const appearancePermissionNote = document.getElementById("appearance-permission-note");
const appearanceSaveBtn = document.getElementById("appearance-save-btn");
const appearanceCustomCssInput = document.getElementById("appearance-custom-css");
const simpleThemeEnabledInput = document.getElementById("simple-theme-enabled");
const simpleThemeControls = document.getElementById("simple-theme-controls");
const simpleThemeAccentInput = document.getElementById("simple-theme-accent");
const simpleThemeBackgroundInput = document.getElementById("simple-theme-background");
const simpleThemeSurfaceInput = document.getElementById("simple-theme-surface");
const simpleThemeTextInput = document.getElementById("simple-theme-text");
const simpleThemeCornersInput = document.getElementById("simple-theme-corners");
const simpleThemeResetBtn = document.getElementById("simple-theme-reset");
const simpleThemePreview = document.getElementById("simple-theme-preview");
const simpleThemePreviewBar = document.getElementById("simple-theme-preview-bar");
const simpleThemePreviewBody = document.getElementById("simple-theme-preview-body");
const simpleThemePreviewDot = document.getElementById("simple-theme-preview-dot");
const simpleThemePreviewMuted = document.getElementById("simple-theme-preview-muted");
const simpleThemePreviewButton = document.getElementById("simple-theme-preview-button");
const brandProductNameInput = document.getElementById("brand-product-name");
const brandTabNameInput = document.getElementById("brand-tab-name");
const brandFaviconUrlInput = document.getElementById("brand-favicon-url");
const brandFaviconFileInput = document.getElementById("brand-favicon-file");
const brandDashboardBackgroundUrlInput = document.getElementById("brand-dashboard-background-url");
const brandDashboardBackgroundFileInput = document.getElementById("brand-dashboard-background-file");
const brandNavNameInput = document.getElementById("brand-nav-name");
const brandAccentColorInput = document.getElementById("brand-accent-color");
const brandIconClassInput = document.getElementById("brand-icon-class");
const brandNavLogoUrlInput = document.getElementById("brand-nav-logo-url");
const brandNavLogoFileInput = document.getElementById("brand-nav-logo-file");
const brandNavLogoAltInput = document.getElementById("brand-nav-logo-alt");
const brandLoginLogoUrlInput = document.getElementById("brand-login-logo-url");
const brandLoginLogoFileInput = document.getElementById("brand-login-logo-file");
const brandLoginLogoAltInput = document.getElementById("brand-login-logo-alt");
const brandLoginTitleInput = document.getElementById("brand-login-title");
const brandLoginSubtitleInput = document.getElementById("brand-login-subtitle");
const brandHeroImageUrlInput = document.getElementById("brand-hero-image-url");
const brandHeroImageFileInput = document.getElementById("brand-hero-image-file");
const brandHeroImageAltInput = document.getElementById("brand-hero-image-alt");
const brandFooterTextInput = document.getElementById("brand-footer-text");
const brandSupportTextInput = document.getElementById("brand-support-text");
const brandSupportUrlInput = document.getElementById("brand-support-url");
const exportImportSection = document.getElementById("export-import-section");
const exportSettingsBtn = document.getElementById("export-settings-btn");
const importSettingsFile = document.getElementById("import-settings-file");
const exportImportMessage = document.getElementById("export-import-message");
const wipeOfflineSection = document.getElementById("wipe-offline-section");
const wipeOfflineBtn = document.getElementById("wipe-offline-btn");
const wipeOfflineMessage = document.getElementById("wipe-offline-message");
let currentUser = null;
let securityConfig = null;
let tlsConfig = null;
let oidcConfig = null;
function showMessage(text, type = "ok") {
if (!messageEl) return;
messageEl.textContent = text;
messageEl.classList.remove(
"hidden",
"text-emerald-200",
"border-emerald-700",
"bg-emerald-900/30",
"text-rose-200",
"border-rose-700",
"bg-rose-900/30",
);
if (type === "error") {
messageEl.classList.add("text-rose-200", "border-rose-700", "bg-rose-900/30");
} else {
messageEl.classList.add("text-emerald-200", "border-emerald-700", "bg-emerald-900/30");
}
animateTextChange(messageEl);
}
function formatDate(timestamp) {
return formatSharedDate(timestamp, "-");
}
function showSettingsSuccess(text, showInline = showMessage) {
showInline(text);
window.showToast?.(text, "success");
}
function canManageClientBans(role) {
return userHas("network:manage-bans");
}
function isAdmin(role) {
return role === "admin";
}
function userHas(perm) {
return !!currentUser?.permissions?.includes(perm);
}
function requireUiPermission(perm, message = "You do not have permission for this setting.") {
if (userHas(perm)) return true;
showMessage(message, "error");
return false;
}
function applyPermissionVisibility() {
document.querySelectorAll("[data-permission]").forEach((el) => {
const perm = el.dataset.permission;
if (perm && userHas(perm)) {
el.classList.remove("hidden");
} else {
el.classList.add("hidden");
}
});
const sidebar = document.getElementById("settings-sidebar");
if (!sidebar) return;
const children = Array.from(sidebar.children);
for (let i = 0; i < children.length; i++) {
const label = children[i];
if (!label.classList.contains("settings-nav-group-label")) continue;
let hasVisibleLink = false;
for (let j = i + 1; j < children.length; j++) {
const next = children[j];
if (next.classList.contains("settings-nav-group-label")) break;
if (next.classList.contains("settings-nav-link") && !next.classList.contains("hidden")) {
hasVisibleLink = true;
break;
}
}
label.classList.toggle("hidden", !hasVisibleLink);
}
}
async function renderPermissionsOverview() {
const container = document.getElementById("settings-permissions-summary");
if (!container || !currentUser) return;
const granted = new Set(currentUser.permissions || []);
let catalog = [];
try {
const res = await fetch("/api/permissions", { credentials: "include" });
if (res.ok) {
const data = await res.json().catch(() => ({}));
catalog = Array.isArray(data.permissions) ? data.permissions : [];
}
} catch {}
const byId = new Map(catalog.map((perm) => [perm.id, perm.description]));
const groups = [
{
title: "User Administration",
icon: "fa-users-gear",
perms: ["users:manage", "audit:view"],
},
{
title: "Client Operations",
icon: "fa-desktop",
perms: [
"clients:control",
"clients:build",
"clients:metadata",
"clients:disconnect",
"clients:reconnect",
"clients:uninstall",
"clients:elevate",
"clients:winre",
],
},
{
title: "System Settings",
icon: "fa-sliders",
perms: [
"system:security",
"system:tls",
"system:oidc",
"system:registration",
"system:notifications",
"system:chat",
"system:appearance",
"system:thumbnails",
"system:backstage-dll",
"system:input-archive",
"system:build-limits",
"system:export-import",
"system:health",
"system:health:manage",
"system:profiler",
],
},
];
container.innerHTML = groups
.map((group) => {
const rows = group.perms
.map((perm) => {
const has = granted.has(perm);
return `
${escapeHtml(perm)}
${escapeHtml(byId.get(perm) || "")}
`;
})
.join("");
return `
${escapeHtml(group.title)}
${rows}
`;
})
.join("");
}
function getPasswordRequirementsText() {
const minLength = Number(securityConfig?.passwordMinLength) || 6;
const requirements = [`min ${minLength} chars`];
if (securityConfig?.passwordRequireUppercase) requirements.push("uppercase");
if (securityConfig?.passwordRequireLowercase) requirements.push("lowercase");
if (securityConfig?.passwordRequireNumber) requirements.push("number");
if (securityConfig?.passwordRequireSymbol) requirements.push("symbol");
return requirements.join(", ");
}
function updatePasswordPolicyUi() {
const minLength = Number(securityConfig?.passwordMinLength) || 6;
if (newPasswordInput) {
newPasswordInput.minLength = minLength;
newPasswordInput.placeholder = `New password (${getPasswordRequirementsText()})`;
}
if (confirmPasswordInput) {
confirmPasswordInput.minLength = minLength;
}
if (passwordPolicyHint) {
passwordPolicyHint.textContent = `Policy: ${getPasswordRequirementsText()}`;
}
}
function setSecurityFormDisabled(disabled) {
const controls = [
securitySessionTtlInput,
securityLoginMaxAttemptsInput,
securityLoginWindowInput,
securityLockoutInput,
securityPasswordMinInput,
securityRequireUppercaseInput,
securityRequireLowercaseInput,
securityRequireNumberInput,
securityRequireSymbolInput,
securityMfaAdminsInput,
securityMfaNonAdminsInput,
securitySaveBtn,
];
for (const control of controls) {
if (!control) continue;
control.disabled = disabled;
}
}
function setTlsFormDisabled(disabled) {
const controls = [
tlsCertbotEmailInput,
tlsCertbotEnabledInput,
tlsCertbotLivePathInput,
tlsCertbotDomainInput,
tlsCertbotCertFileInput,
tlsCertbotKeyFileInput,
tlsCertbotCaFileInput,
tlsSaveBtn,
tlsCertbotAutoBtn,
];
for (const control of controls) {
if (!control) continue;
control.disabled = disabled;
}
}
function setTlsAutoSetupRunning(running) {
if (!tlsCertbotAutoBtn) return;
tlsCertbotAutoBtn.disabled = running;
tlsCertbotAutoBtn.innerHTML = running
? 'Running Certbot Setup...'
: 'Auto Setup Free SSL (Let\'s Encrypt)';
}
function applySecurityForm() {
if (!securityConfig) return;
securitySessionTtlInput.value = String(securityConfig.sessionTtlHours || 168);
securityLoginMaxAttemptsInput.value = String(securityConfig.loginMaxAttempts || 5);
securityLoginWindowInput.value = String(securityConfig.loginWindowMinutes || 15);
securityLockoutInput.value = String(securityConfig.loginLockoutMinutes || 30);
securityPasswordMinInput.value = String(securityConfig.passwordMinLength || 6);
securityRequireUppercaseInput.checked = Boolean(securityConfig.passwordRequireUppercase);
securityRequireLowercaseInput.checked = Boolean(securityConfig.passwordRequireLowercase);
securityRequireNumberInput.checked = Boolean(securityConfig.passwordRequireNumber);
securityRequireSymbolInput.checked = Boolean(securityConfig.passwordRequireSymbol);
if (securityMfaAdminsInput) securityMfaAdminsInput.checked = Boolean(securityConfig.mfaRequiredForAdmins);
if (securityMfaNonAdminsInput) securityMfaNonAdminsInput.checked = Boolean(securityConfig.mfaRequiredForNonAdmins);
updatePasswordPolicyUi();
}
async function loadSecurityPolicy() {
if (!currentUser) return;
if (!userHas("system:security")) {
securityPermissionNote.classList.remove("hidden");
setSecurityFormDisabled(true);
securityConfig = {
passwordMinLength: 6,
passwordRequireUppercase: false,
passwordRequireLowercase: false,
passwordRequireNumber: false,
passwordRequireSymbol: false,
mfaRequiredForAdmins: false,
mfaRequiredForNonAdmins: false,
};
updatePasswordPolicyUi();
return;
}
securityPermissionNote.classList.add("hidden");
const res = await fetch("/api/settings/security", { credentials: "include" });
if (!res.ok) {
showMessage("Failed to load security settings.", "error");
setSecurityFormDisabled(true);
return;
}
const data = await res.json().catch(() => ({}));
securityConfig = data.security || null;
applySecurityForm();
setSecurityFormDisabled(false);
}
function applyTlsForm() {
const certbot = tlsConfig?.certbot || {};
tlsCertbotEnabledInput.checked = Boolean(certbot.enabled);
tlsCertbotEmailInput.value = "";
tlsCertbotLivePathInput.value = certbot.livePath || "/etc/letsencrypt/live";
tlsCertbotDomainInput.value = certbot.domain || "";
tlsCertbotCertFileInput.value = certbot.certFileName || "fullchain.pem";
tlsCertbotKeyFileInput.value = certbot.keyFileName || "privkey.pem";
tlsCertbotCaFileInput.value = certbot.caFileName || "chain.pem";
}
tlsTrustHelpBtn?.addEventListener("click", () => {
if (tlsTrustInstructions?.classList.contains("hidden")) {
revealElement(tlsTrustInstructions, { duration: 200, offset: 5 });
} else {
concealElement(tlsTrustInstructions, { duration: 120, offset: -3 });
}
});
async function loadActiveTlsCertificate() {
const guide = getCertificateTrustGuide();
if (tlsTrustHelpBtn) {
tlsTrustHelpBtn.innerHTML = `Trust on ${guide.label}`;
}
if (tlsTrustInstructions) {
tlsTrustInstructions.innerHTML = guide.instructions;
}
try {
const res = await fetch("/api/cert/info", { credentials: "include" });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
const source = String(data.source || "unknown");
if (tlsActiveCertSource) {
tlsActiveCertSource.textContent = source === "self-signed"
? "Self-signed"
: source === "certbot"
? "Certbot / Let's Encrypt"
: source === "configured"
? "Configured certificate"
: "Unknown source";
tlsActiveCertSource.classList.toggle("border-amber-600", source === "self-signed");
tlsActiveCertSource.classList.toggle("bg-amber-950", source === "self-signed");
tlsActiveCertSource.classList.toggle("text-amber-200", source === "self-signed");
}
} catch {
if (tlsActiveCertSource) tlsActiveCertSource.textContent = "Unavailable";
if (tlsDownloadCertBtn) {
tlsDownloadCertBtn.setAttribute("aria-disabled", "true");
tlsDownloadCertBtn.classList.add("pointer-events-none", "opacity-50");
}
}
}
async function loadTlsSettings() {
if (!currentUser) return;
if (!userHas("system:tls")) {
tlsPermissionNote.classList.remove("hidden");
setTlsFormDisabled(true);
tlsConfig = {
certbot: {
enabled: false,
livePath: "/etc/letsencrypt/live",
domain: "",
certFileName: "fullchain.pem",
keyFileName: "privkey.pem",
caFileName: "chain.pem",
},
};
applyTlsForm();
return;
}
tlsPermissionNote.classList.add("hidden");
const res = await fetch("/api/settings/tls", { credentials: "include" });
if (!res.ok) {
showMessage("Failed to load TLS settings.", "error");
setTlsFormDisabled(true);
return;
}
const data = await res.json().catch(() => ({}));
tlsConfig = data.tls || null;
applyTlsForm();
setTlsFormDisabled(false);
await loadActiveTlsCertificate();
}
function listToCsv(value) {
return Array.isArray(value) ? value.join(", ") : "";
}
function csvToList(value) {
return String(value || "")
.split(",")
.map((item) => item.trim())
.filter(Boolean);
}
function setOidcFormDisabled(disabled) {
const controls = [
oidcEnabledInput,
oidcLabelInput,
oidcIssuerInput,
oidcClientIdInput,
oidcClientSecretInput,
oidcRedirectUriInput,
oidcScopesInput,
oidcClientAuthMethodInput,
oidcAutoProvisionInput,
oidcAllowEmailLinkInput,
oidcDefaultRoleInput,
oidcAllowedDomainsInput,
oidcAllowedEmailsInput,
oidcGroupClaimInput,
oidcAdminGroupsInput,
oidcOperatorGroupsInput,
oidcViewerGroupsInput,
oidcSaveBtn,
];
for (const control of controls) {
if (!control) continue;
control.disabled = disabled;
}
}
function applyOidcForm() {
const cfg = oidcConfig || {};
if (oidcEnabledInput) oidcEnabledInput.checked = Boolean(cfg.enabled);
if (oidcLabelInput) oidcLabelInput.value = cfg.label || "Single sign-on";
if (oidcIssuerInput) oidcIssuerInput.value = cfg.issuer || "";
if (oidcClientIdInput) oidcClientIdInput.value = cfg.clientId || "";
if (oidcClientSecretInput) oidcClientSecretInput.value = "";
if (oidcSecretHint) {
oidcSecretHint.textContent = cfg.clientSecretSet
? "A client secret is saved. Leave this blank to keep it."
: "No client secret is currently saved.";
}
if (oidcRedirectUriInput) oidcRedirectUriInput.value = cfg.redirectUri || "";
if (oidcScopesInput) oidcScopesInput.value = listToCsv(cfg.scopes || ["openid", "profile", "email"]);
if (oidcClientAuthMethodInput) oidcClientAuthMethodInput.value = cfg.clientAuthMethod || "client_secret_post";
if (oidcAutoProvisionInput) oidcAutoProvisionInput.checked = cfg.autoProvision !== false;
if (oidcAllowEmailLinkInput) oidcAllowEmailLinkInput.checked = Boolean(cfg.allowEmailLink);
if (oidcDefaultRoleInput) oidcDefaultRoleInput.value = cfg.defaultRole || "viewer";
if (oidcAllowedDomainsInput) oidcAllowedDomainsInput.value = listToCsv(cfg.allowedDomains);
if (oidcAllowedEmailsInput) oidcAllowedEmailsInput.value = listToCsv(cfg.allowedEmails);
if (oidcGroupClaimInput) oidcGroupClaimInput.value = cfg.groupClaim || "groups";
if (oidcAdminGroupsInput) oidcAdminGroupsInput.value = listToCsv(cfg.adminGroups);
if (oidcOperatorGroupsInput) oidcOperatorGroupsInput.value = listToCsv(cfg.operatorGroups);
if (oidcViewerGroupsInput) oidcViewerGroupsInput.value = listToCsv(cfg.viewerGroups);
}
async function loadOidcSettings() {
if (!currentUser) return;
if (!userHas("system:oidc")) {
if (oidcPermissionNote) oidcPermissionNote.classList.remove("hidden");
setOidcFormDisabled(true);
return;
}
if (oidcPermissionNote) oidcPermissionNote.classList.add("hidden");
const res = await fetch("/api/settings/oidc", { credentials: "include" });
if (!res.ok) {
showMessage("Failed to load OIDC settings.", "error");
setOidcFormDisabled(true);
return;
}
const data = await res.json().catch(() => ({}));
oidcConfig = data.oidc || null;
applyOidcForm();
setOidcFormDisabled(false);
}
function updateNavLayoutButtons(mode, sidebarBtn, topbarBtn) {
const active = ["bg-indigo-600/80", "border-indigo-500", "text-white"];
const inactive = ["bg-slate-800", "border-slate-700", "text-slate-400", "hover:bg-slate-700", "hover:text-slate-200"];
const base = ["nav-layout-btn", "flex-1", "flex", "items-center", "justify-center", "gap-2", "px-3", "py-2", "rounded-lg", "border", "text-sm", "font-medium", "transition-colors"];
sidebarBtn.className = [...base, ...(mode === "sidebar" ? active : inactive)].join(" ");
topbarBtn.className = [...base, ...(mode === "topbar" ? active : inactive)].join(" ");
sidebarBtn.dataset.selected = mode === "sidebar" ? "true" : "false";
topbarBtn.dataset.selected = mode === "topbar" ? "true" : "false";
}
function loadPrefs() {
prefNotificationsInput.checked = getNotificationsEnabled();
if (prefDesktopNotificationsInput) {
prefDesktopNotificationsInput.checked = getDesktopNotificationsEnabled();
}
const refreshSeconds = Number(localStorage.getItem(PREF_REFRESH_KEY) || 8);
prefRefreshSecondsInput.value = String(Math.min(120, Math.max(3, refreshSeconds)));
const navMode = localStorage.getItem(NAV_MODE_KEY) || "topbar";
const sidebarBtn = document.getElementById("pref-nav-sidebar");
const topbarBtn = document.getElementById("pref-nav-topbar");
if (sidebarBtn && topbarBtn) {
updateNavLayoutButtons(navMode === "topbar" ? "topbar" : "sidebar", sidebarBtn, topbarBtn);
}
const navHiddenInput = document.getElementById("pref-nav-hidden");
if (navHiddenInput) {
navHiddenInput.checked = localStorage.getItem(NAV_HIDDEN_KEY) === "true";
}
}
function initEasterEggPreference() {
if (!easterEggEnabledInput) return;
easterEggEnabledInput.checked = isEasterEggEnabled();
easterEggEnabledInput.addEventListener("change", async () => {
const enabled = easterEggEnabledInput.checked;
setEasterEggEnabled(enabled);
if (!easterEggStatus) return;
if (!enabled) {
easterEggStatus.textContent = "Disabled. The image will not be requested.";
easterEggStatus.className = "text-xs text-slate-500";
return;
}
easterEggEnabledInput.disabled = true;
easterEggStatus.textContent = "Loading the easter egg…";
easterEggStatus.className = "text-xs text-violet-300";
try {
await showEnabledEasterEgg({ force: true });
easterEggStatus.textContent = "Enabled and shown in the browser console.";
easterEggStatus.className = "text-xs text-emerald-400";
} catch (error) {
setEasterEggEnabled(false);
easterEggEnabledInput.checked = false;
easterEggStatus.textContent = `Could not load the easter egg: ${error.message}`;
easterEggStatus.className = "text-xs text-rose-400";
} finally {
easterEggEnabledInput.disabled = false;
}
});
}
async function loadCurrentUser() {
const res = await fetch("/api/auth/me", { credentials: "include" });
if (!res.ok) {
window.location.href = "/";
return;
}
currentUser = await res.json();
usernameEl.textContent = currentUser.username || "unknown";
roleEl.textContent = currentUser.role || "unknown";
}
async function updatePassword(event) {
event.preventDefault();
if (!currentUser?.userId) return;
const currentPassword = currentPasswordInput.value;
const newPassword = newPasswordInput.value;
const confirmPassword = confirmPasswordInput.value;
const minLength = Number(securityConfig?.passwordMinLength) || 6;
if (newPassword.length < minLength) {
showMessage(`New password must be at least ${minLength} characters.`, "error");
return;
}
if (newPassword !== confirmPassword) {
showMessage("Password confirmation does not match.", "error");
return;
}
const res = await fetch(`/api/users/${currentUser.userId}/password`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
currentPassword,
newPassword,
}),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
showMessage(data.error || "Failed to update password.", "error");
return;
}
passwordForm.reset();
showSettingsSuccess("Password updated successfully.");
}
function showMfaMessage(text, type = "ok") {
if (!mfaMessage) return;
mfaMessage.textContent = text;
mfaMessage.className = `text-sm rounded-lg px-3 py-2 border ${
type === "error"
? "border-rose-800 bg-rose-900/20 text-rose-200"
: "border-emerald-800 bg-emerald-900/20 text-emerald-200"
}`;
mfaMessage.classList.remove("hidden");
setTimeout(() => mfaMessage.classList.add("hidden"), 5000);
}
async function loadMfaStatus() {
if (!mfaStatusText) return;
try {
const res = await fetch("/api/mfa/status", { credentials: "include" });
if (!res.ok) {
mfaStatusText.textContent = "Failed to load MFA status.";
return;
}
const data = await res.json();
const requiredText = data.required ? " Required by policy." : "";
mfaStatusText.textContent = data.enabled
? `MFA is enabled.${requiredText}`
: `MFA is not enabled.${requiredText}`;
if (mfaStartBtn) mfaStartBtn.classList.toggle("hidden", !!data.enabled);
if (mfaDisablePanel) mfaDisablePanel.classList.toggle("hidden", !data.enabled);
if (mfaDisableBtn) mfaDisableBtn.disabled = !!data.required;
if (mfaDisablePanel) {
mfaDisablePanel.classList.toggle("opacity-60", !!data.required);
}
} catch {
mfaStatusText.textContent = "Failed to load MFA status.";
}
}
function renderMfaQrCode(svgMarkup) {
if (!mfaQrCode) return;
mfaQrCode.replaceChildren();
const svg = typeof svgMarkup === "string" ? svgMarkup.trim() : "";
if (!svg || svg.length > 512 * 1024 || !/^