(function () { const root = document.getElementById("qortal-admin-root"); if (!root) { return; } const statusUrl = root.dataset.statusUrl; const setupUrl = root.dataset.setupUrl; const setupPlanUrl = root.dataset.setupPlanUrl; const setupOccUrl = root.dataset.setupOccUrl; const notifyUrl = root.dataset.notifyUrl; const searchUsersUrl = root.dataset.searchUsersUrl; const searchGroupsUrl = root.dataset.searchGroupsUrl; const settingsUrl = root.dataset.settingsUrl; const walletsUrl = root.dataset.walletsUrl; const createWalletUrl = root.dataset.createWalletUrl; const registerExternalAuthUrl = root.dataset.registerExternalAuthUrl; const mappingsUrl = root.dataset.mappingsUrl; const linkMappingUrl = root.dataset.linkMappingUrl; const unlinkMappingUrl = root.dataset.unlinkMappingUrl; const allowlistUrl = root.dataset.allowlistUrl; const addAllowlistUrl = root.dataset.addAllowlistUrl; const removeAllowlistUrl = root.dataset.removeAllowlistUrl; const invitesUrl = root.dataset.invitesUrl; const createInviteUrl = root.dataset.createInviteUrl; const revokeInviteUrl = root.dataset.revokeInviteUrl; const qappsJson = root.dataset.qappsJson || "[]"; const qappsEnabledValue = root.dataset.qappsEnabled === "1"; const qappsBrowserEnabledValue = root.dataset.qappsBrowserEnabled === "1"; const qappsBrowserAddressValue = root.dataset.qappsBrowserAddress || ""; const qappsDebugEnabledValue = root.dataset.qappsDebugEnabled === "1"; const nextcloudPublicUrlValue = root.dataset.nextcloudPublicUrl || ""; const feedbackEl = document.getElementById("qortal-admin-feedback"); const statusEl = document.getElementById("qortal-admin-status"); const saveButton = document.getElementById("qortal-save-settings"); const refreshSetupButton = document.getElementById("qortal-refresh-setup"); const testButton = document.getElementById("qortal-test-connection"); const setupPlanButton = document.getElementById("qortal-setup-plan"); const setupOccButton = document.getElementById("qortal-setup-occ"); const sendNotificationsButton = document.getElementById( "qortal-send-notifications" ); const searchUsersButton = document.getElementById( "qortal-user-search-button" ); const searchGroupsButton = document.getElementById( "qortal-group-search-button" ); const setupOverviewList = document.getElementById( "qortal-setup-overview-list" ); const setupOverviewNote = document.getElementById( "qortal-setup-overview-note" ); const setupComponentsCard = document.getElementById( "qortal-setup-components" ); const allowlistCard = document.getElementById("qortal-allowlist-card"); const invitesCard = document.getElementById("qortal-invites-card"); const inviteQortalCard = document.getElementById("qortal-invite-qortal-card"); const oidcPolicyMode = document.getElementById("qortal-oidc-policy-mode"); const oidcGuard = document.getElementById("qortal-oidc-guard"); const oidcInviteTtl = document.getElementById("qortal-oidc-invite-ttl"); const oidcRequireEmail = document.getElementById("qortal-oidc-require-email"); const oidcRedirects = document.getElementById("qortal-oidc-redirects"); const oidcPolicyNote = document.getElementById("qortal-oidc-policy-note"); const oidcPolicySelect = document.getElementById("qortal-oidc-policy-select"); const oidcGuardToggle = document.getElementById("qortal-oidc-guard-toggle"); const oidcInviteInput = document.getElementById( "qortal-oidc-invite-ttl-input" ); const oidcRequireEmailToggle = document.getElementById( "qortal-oidc-require-email-toggle" ); const oidcRedirectInput = document.getElementById( "qortal-oidc-redirect-allowlist-input" ); const oidcEnvButton = document.getElementById("qortal-oidc-env-generate"); const oidcEnvOutput = document.getElementById("qortal-oidc-env-output"); const externalAuthBaseUrlInput = document.getElementById( "qortal-external-auth-base-url" ); const brokerInternalApiTokenInput = document.getElementById( "qortal-broker-internal-api-token" ); const externalAuthAppIdInput = document.getElementById( "qortal-external-auth-app-id" ); const externalAuthAppSecretInput = document.getElementById( "qortal-external-auth-app-secret" ); const externalAuthNodeUrlInput = document.getElementById( "qortal-external-auth-node-url" ); const externalAuthNodeApiKeyInput = document.getElementById( "qortal-external-auth-node-api-key" ); const externalAuthNodeApiKeyModeInput = document.getElementById( "qortal-external-auth-node-api-key-mode" ); const externalAuthNodeApiKeyPathsInput = document.getElementById( "qortal-external-auth-node-api-key-paths" ); const externalAuthEnvButton = document.getElementById( "qortal-external-auth-env-generate" ); const externalAuthEnvOutput = document.getElementById( "qortal-external-auth-env-output" ); const externalAuthAppNameInput = document.getElementById( "qortal-external-auth-app-name" ); const externalAuthRegisterButton = document.getElementById( "qortal-external-auth-register" ); const qortalNodeUrlInput = document.getElementById("qortal-node-url"); const qortalNodeApiKeyInput = document.getElementById("qortal-node-api-key"); const qortalGatewayUrlInput = document.getElementById("qortal-gateway-url"); const qortalGatewayInsecureInput = document.getElementById( "qortal-gateway-insecure" ); const qortalNodeEnvButton = document.getElementById( "qortal-node-env-generate" ); const qortalNodeEnvOutput = document.getElementById("qortal-node-env-output"); const createWalletButton = document.getElementById("qortal-create-wallet"); const createWalletLinkButton = document.getElementById( "qortal-create-wallet-link" ); const refreshWalletsButton = document.getElementById( "qortal-refresh-wallets" ); const walletUserIdInput = document.getElementById("qortal-wallet-user-id"); const linkMappingButton = document.getElementById("qortal-link-mapping"); const refreshMappingsButton = document.getElementById( "qortal-refresh-mappings" ); const addAllowlistButton = document.getElementById("qortal-add-allowlist"); const refreshAllowlistButton = document.getElementById( "qortal-refresh-allowlist" ); const createInviteButton = document.getElementById("qortal-create-invite"); const refreshInvitesButton = document.getElementById( "qortal-refresh-invites" ); const walletCreateResult = document.getElementById( "qortal-wallet-create-result" ); const walletsBody = document.getElementById("qortal-wallets-body"); const mappingsBody = document.getElementById("qortal-mappings-body"); const allowlistBody = document.getElementById("qortal-allowlist-body"); const inviteCreateResult = document.getElementById( "qortal-invite-create-result" ); const invitesBody = document.getElementById("qortal-invites-body"); const qappsEnabledInput = document.getElementById("qortal-qapps-enabled"); const qappsBrowserEnabledInput = document.getElementById( "qortal-qapps-browser-enabled" ); const qappsBrowserAddressInput = document.getElementById( "qortal-qapps-browser-address" ); const qappsDebugEnabledInput = document.getElementById( "qortal-qapps-debug-enabled" ); const qappsNameInput = document.getElementById("qortal-qapps-name"); const qappsAddressInput = document.getElementById("qortal-qapps-address"); const qappsIconModeInput = document.getElementById("qortal-qapps-icon-mode"); const qappsIconUrlInput = document.getElementById("qortal-qapps-icon-url"); const qappsDescriptionInput = document.getElementById( "qortal-qapps-description" ); const addQappButton = document.getElementById("qortal-add-qapp"); const clearQappsButton = document.getElementById("qortal-clear-qapps"); const qappsBody = document.getElementById("qortal-qapps-body"); const oidcIssuerInput = document.getElementById("qortal-oidc-issuer-url"); const oidcClientIdInput = document.getElementById("qortal-oidc-client-id"); const oidcClientSecretInput = document.getElementById( "qortal-oidc-client-secret" ); const nextcloudPublicUrlInput = document.getElementById( "qortal-nextcloud-public-url" ); const setupResult = document.getElementById("qortal-setup-result"); const inviteMessageButton = document.getElementById( "qortal-generate-invite-message" ); const copyInviteMessageButton = document.getElementById( "qortal-copy-invite-message" ); const inviteMessageBox = document.getElementById("qortal-invite-message"); const notifyUserIds = document.getElementById("qortal-notify-user-ids"); const notifyGroupIds = document.getElementById("qortal-notify-group-ids"); const notifyEmail = document.getElementById("qortal-notify-email"); const notifyInApp = document.getElementById("qortal-notify-inapp"); const notifyQueue = document.getElementById("qortal-notify-queue"); const notifyIncludeInvite = document.getElementById( "qortal-notify-include-invite" ); const notifyResult = document.getElementById("qortal-notify-result"); const notifyEmailSubject = document.getElementById( "qortal-notify-email-subject" ); const notifyEmailBody = document.getElementById("qortal-notify-email-body"); const userSearchQuery = document.getElementById("qortal-user-search-query"); const userSearchResults = document.getElementById( "qortal-user-search-results" ); const groupSearchQuery = document.getElementById("qortal-group-search-query"); const groupSearchResults = document.getElementById( "qortal-group-search-results" ); const previewEmailButton = document.getElementById("qortal-preview-email"); const previewSubject = document.getElementById( "qortal-email-preview-subject" ); const previewBody = document.getElementById("qortal-email-preview-body"); if ( !statusUrl || !setupUrl || !setupPlanUrl || !setupOccUrl || !notifyUrl || !searchUsersUrl || !searchGroupsUrl || !settingsUrl || !walletsUrl || !createWalletUrl || !registerExternalAuthUrl || !mappingsUrl || !linkMappingUrl || !unlinkMappingUrl || !allowlistUrl || !addAllowlistUrl || !removeAllowlistUrl || !invitesUrl || !createInviteUrl || !revokeInviteUrl ) { return; } if ( !feedbackEl || !statusEl || !saveButton || !refreshSetupButton || !testButton || !setupPlanButton || !setupOccButton || !sendNotificationsButton || !searchUsersButton || !searchGroupsButton || !setupOverviewList || !setupOverviewNote || !setupComponentsCard || !allowlistCard || !invitesCard || !inviteQortalCard || !oidcPolicyMode || !oidcGuard || !oidcInviteTtl || !oidcRequireEmail || !oidcRedirects || !oidcPolicyNote || !oidcPolicySelect || !oidcGuardToggle || !oidcInviteInput || !oidcRequireEmailToggle || !oidcRedirectInput || !oidcEnvButton || !oidcEnvOutput || !externalAuthBaseUrlInput || !brokerInternalApiTokenInput || !externalAuthAppIdInput || !externalAuthAppSecretInput || !externalAuthNodeUrlInput || !externalAuthNodeApiKeyInput || !externalAuthNodeApiKeyModeInput || !externalAuthNodeApiKeyPathsInput || !externalAuthEnvButton || !externalAuthEnvOutput || !externalAuthAppNameInput || !externalAuthRegisterButton || !qortalNodeUrlInput || !qortalNodeApiKeyInput || !qortalGatewayUrlInput || !qortalGatewayInsecureInput || !qortalNodeEnvButton || !qortalNodeEnvOutput || !createWalletButton || !createWalletLinkButton || !refreshWalletsButton || !walletUserIdInput || !linkMappingButton || !refreshMappingsButton || !walletCreateResult || !walletsBody || !mappingsBody || !addAllowlistButton || !refreshAllowlistButton || !createInviteButton || !refreshInvitesButton || !allowlistBody || !inviteCreateResult || !invitesBody || !qappsEnabledInput || !qappsBrowserEnabledInput || !qappsBrowserAddressInput || !qappsDebugEnabledInput || !qappsNameInput || !qappsAddressInput || !qappsIconModeInput || !qappsIconUrlInput || !qappsDescriptionInput || !addQappButton || !clearQappsButton || !qappsBody || !oidcIssuerInput || !oidcClientIdInput || !oidcClientSecretInput || !nextcloudPublicUrlInput || !setupResult || !inviteMessageButton || !copyInviteMessageButton || !inviteMessageBox || !notifyUserIds || !notifyGroupIds || !notifyEmail || !notifyInApp || !notifyQueue || !notifyIncludeInvite || !notifyResult || !notifyEmailSubject || !notifyEmailBody || !userSearchQuery || !userSearchResults || !groupSearchQuery || !groupSearchResults || !previewEmailButton || !previewSubject || !previewBody ) { return; } const headers = { requesttoken: OC.requestToken, }; let lastUserResults = []; let lastSetupPlan = null; let autoProvisionEnabled = false; let autoProvisionGuardEnabled = false; let hasUnsavedChanges = false; let baselineSnapshot = null; let qapps = []; try { const parsed = JSON.parse(qappsJson); if (Array.isArray(parsed)) { qapps = parsed .filter(function (entry) { return ( entry && typeof entry.address === "string" && entry.address.length > 0 ); }) .map(function (entry) { return { name: typeof entry.name === "string" ? entry.name : "", address: entry.address, description: typeof entry.description === "string" ? entry.description : "", iconMode: typeof entry.iconMode === "string" ? entry.iconMode : "auto", iconUrl: typeof entry.iconUrl === "string" ? entry.iconUrl : "", }; }); } } catch (error) { qapps = []; } qappsEnabledInput.checked = qappsEnabledValue; qappsBrowserEnabledInput.checked = qappsBrowserEnabledValue; qappsBrowserAddressInput.value = qappsBrowserAddressValue; qappsDebugEnabledInput.checked = qappsDebugEnabledValue; qappsIconModeInput.value = "auto"; qappsIconUrlInput.value = ""; updateIconUrlState(); oidcEnvOutput.textContent = buildEnvSnippet(); externalAuthEnvOutput.textContent = buildExternalAuthEnvSnippet(); qortalNodeEnvOutput.textContent = buildQortalNodeEnvSnippet(); baselineSnapshot = JSON.stringify(collectSettings()); function markDirty() { hasUnsavedChanges = true; } function updateIconUrlState() { const isCustom = qappsIconModeInput.value === "custom"; qappsIconUrlInput.disabled = !isCustom; if (!isCustom) { qappsIconUrlInput.value = ""; } } function resetDirty() { hasUnsavedChanges = false; baselineSnapshot = JSON.stringify(collectSettings()); } function setFeedback(message, isError) { feedbackEl.textContent = message; feedbackEl.classList.toggle("error", Boolean(isError)); feedbackEl.classList.toggle("success", !isError && message.length > 0); } function collectSettings() { return { brokerBaseUrl: document.getElementById("qortal-broker-base-url").value, brokerInternalApiToken: brokerInternalApiTokenInput.value, externalAuthDocsUrl: document.getElementById( "qortal-external-auth-docs-url" ).value, externalAuthBaseUrl: externalAuthBaseUrlInput.value, externalAuthAppId: externalAuthAppIdInput.value, externalAuthAppSecret: externalAuthAppSecretInput.value, externalAuthNodeUrl: externalAuthNodeUrlInput.value, externalAuthNodeApiKey: externalAuthNodeApiKeyInput.value, externalAuthNodeApiKeyMode: externalAuthNodeApiKeyModeInput.value, externalAuthNodeApiKeyPaths: externalAuthNodeApiKeyPathsInput.value, featureQdnBackups: document.getElementById("qortal-feature-qdn-backups") .checked ? "1" : "", featureQmail: document.getElementById("qortal-feature-qmail").checked ? "1" : "", oidcIssuerUrl: oidcIssuerInput.value, oidcClientId: oidcClientIdInput.value, oidcClientSecret: oidcClientSecretInput.value, oidcPolicyMode: oidcPolicySelect.value, oidcAutoProvisionGuard: oidcGuardToggle.value, oidcInviteTtlSeconds: oidcInviteInput.value.trim(), oidcRequireEmailForNewAccount: oidcRequireEmailToggle.value, oidcRedirectAllowlist: oidcRedirectInput.value, nextcloudPublicUrl: nextcloudPublicUrlInput.value, qortalNodeUrl: qortalNodeUrlInput.value, qortalNodeApiKey: qortalNodeApiKeyInput.value, qortalGatewayUrl: qortalGatewayUrlInput.value, qortalGatewayAllowInsecure: qortalGatewayInsecureInput.checked ? "1" : "", notifyEmailSubject: notifyEmailSubject.value, notifyEmailBody: notifyEmailBody.value, qappsEnabled: qappsEnabledInput.checked ? "1" : "", qappsFullBrowserEnabled: qappsBrowserEnabledInput.checked ? "1" : "", qappsFullBrowserAddress: qappsBrowserAddressInput.value, qappsDebugEnabled: qappsDebugEnabledInput.checked ? "1" : "", qappsList: JSON.stringify(qapps), }; } async function requestJson(url, options) { const response = await fetch(url, options); const payload = await response.json(); if (!response.ok) { throw new Error(payload.error || "Request failed"); } return payload; } function assertBrokerOk(payload, fallbackError) { if (payload && payload.ok === false) { throw new Error(payload.error || fallbackError); } } function formatTimestamp(value) { if (!value) { return ""; } try { return new Date(value).toLocaleString(); } catch (error) { return String(value); } } function renderStatusItem(label, status, detail) { const li = document.createElement("li"); const dot = document.createElement("span"); dot.className = "qortal-status-dot " + status; const text = document.createElement("span"); text.textContent = detail ? `${label}: ${detail}` : label; li.appendChild(dot); li.appendChild(text); return li; } function normalizeGuardMode(value) { if (value === true) { return "invite_or_allowlist"; } if (value === false) { return "off"; } if (typeof value !== "string") { return ""; } const raw = value.trim().toLowerCase(); if (["invite_or_allowlist", "enabled", "on", "true", "1"].includes(raw)) { return "invite_or_allowlist"; } if (["off", "disabled", "none", "false", "0"].includes(raw)) { return "off"; } return ""; } function isGuardEnabled(value) { return normalizeGuardMode(value) === "invite_or_allowlist"; } function extractBrokerError(config) { if (!config || typeof config !== "object") { return ""; } if ( config.ok === false && typeof config.error === "string" && config.error.trim() !== "" ) { return config.error; } if ( config.data && typeof config.data === "object" && typeof config.data.error === "string" && config.data.error.trim() !== "" ) { return config.data.error; } return ""; } function renderSetupOverview(payload) { setupOverviewList.innerHTML = ""; setupOverviewNote.textContent = ""; const brokerHealth = payload && payload.status ? payload.status.brokerHealth : null; const qortalHealth = payload && payload.status ? payload.status.qortalHealth : null; const rawConfig = payload ? payload.oidcConfig : null; const oidcConfig = rawConfig && rawConfig.ok !== false ? rawConfig.data || rawConfig : null; if (brokerHealth && brokerHealth.ok !== false) { setupOverviewList.appendChild(renderStatusItem("Broker reachable", "ok")); } else { setupOverviewList.appendChild( renderStatusItem( "Broker reachable", "error", brokerHealth && brokerHealth.error ? brokerHealth.error : "unreachable" ) ); } if (qortalHealth && qortalHealth.ok !== false) { setupOverviewList.appendChild( renderStatusItem("External Auth reachable", "ok") ); } else { setupOverviewList.appendChild( renderStatusItem( "External Auth reachable", "error", qortalHealth && qortalHealth.error ? qortalHealth.error : "unreachable" ) ); } const externalAuthBaseUrl = externalAuthBaseUrlInput.value.trim(); const externalAuthAppId = externalAuthAppIdInput.value.trim(); const externalAuthAppSecret = externalAuthAppSecretInput.value.trim(); const externalAuthNodeUrl = externalAuthNodeUrlInput.value.trim(); const externalAuthNodeApiKey = externalAuthNodeApiKeyInput.value.trim(); const externalAuthNodeApiKeyMode = externalAuthNodeApiKeyModeInput.value.trim(); const externalAuthNodeApiKeyPaths = externalAuthNodeApiKeyPathsInput.value.trim(); const qortalNodeApiKey = qortalNodeApiKeyInput.value.trim(); if (externalAuthBaseUrl) { setupOverviewList.appendChild( renderStatusItem("External Auth Base URL", "ok", externalAuthBaseUrl) ); } else { setupOverviewList.appendChild( renderStatusItem("External Auth Base URL", "warn", "missing") ); } if (externalAuthAppId && externalAuthAppSecret) { setupOverviewList.appendChild( renderStatusItem("External Auth app credentials", "ok") ); } else { setupOverviewList.appendChild( renderStatusItem("External Auth app credentials", "warn", "missing") ); } if (externalAuthNodeUrl) { setupOverviewList.appendChild( renderStatusItem("External Auth Qortal node", "ok", externalAuthNodeUrl) ); } else { setupOverviewList.appendChild( renderStatusItem("External Auth Qortal node", "warn", "missing") ); } if (externalAuthNodeApiKey) { setupOverviewList.appendChild( renderStatusItem("External Auth node API key", "ok", "set") ); } else if (qortalNodeApiKey) { setupOverviewList.appendChild( renderStatusItem( "External Auth node API key", "warn", "using Qortal node key fallback" ) ); } else if (externalAuthNodeUrl) { setupOverviewList.appendChild( renderStatusItem("External Auth node API key", "warn", "missing") ); } if (externalAuthNodeApiKeyMode) { setupOverviewList.appendChild( renderStatusItem( "External Auth key mode", "ok", externalAuthNodeApiKeyMode ) ); } if (externalAuthNodeApiKeyPaths) { setupOverviewList.appendChild( renderStatusItem( "External Auth key paths", "ok", externalAuthNodeApiKeyPaths ) ); } const qortalNodeUrl = qortalNodeUrlInput.value.trim(); const qortalGatewayUrl = qortalGatewayUrlInput.value.trim(); const qortalGatewayAllowInsecure = qortalGatewayInsecureInput.checked; if (qortalNodeUrl) { setupOverviewList.appendChild( renderStatusItem("Qortal node URL", "ok", qortalNodeUrl) ); } else { setupOverviewList.appendChild( renderStatusItem("Qortal node URL", "warn", "missing") ); } if (qortalNodeApiKey) { setupOverviewList.appendChild( renderStatusItem("Qortal node API key", "ok", "set") ); } else { setupOverviewList.appendChild( renderStatusItem("Qortal node API key", "warn", "missing") ); } if (qortalGatewayUrl) { setupOverviewList.appendChild( renderStatusItem("Gateway node URL", "ok", qortalGatewayUrl) ); } else { setupOverviewList.appendChild( renderStatusItem("Gateway node URL", "warn", "missing") ); } if (qortalGatewayAllowInsecure) { setupOverviewList.appendChild( renderStatusItem("Gateway TLS verification", "warn", "disabled") ); } const publicUrl = nextcloudPublicUrlInput.value.trim(); if (publicUrl) { setupOverviewList.appendChild( renderStatusItem("Nextcloud public URL set", "ok", publicUrl) ); } else { setupOverviewList.appendChild( renderStatusItem("Nextcloud public URL set", "warn", "missing") ); } const issuer = oidcIssuerInput.value.trim() || brokerBaseFromSettings(); if (issuer) { setupOverviewList.appendChild( renderStatusItem("OIDC issuer URL", "ok", issuer) ); } else { setupOverviewList.appendChild( renderStatusItem("OIDC issuer URL", "warn", "missing") ); } if (oidcConfig && oidcConfig.policyMode) { setupOverviewList.appendChild( renderStatusItem("OIDC policy mode", "ok", oidcConfig.policyMode) ); } else { const policyError = extractBrokerError(rawConfig); setupOverviewList.appendChild( renderStatusItem( "OIDC policy mode", "warn", policyError ? `unknown (${policyError})` : "unknown (broker config not available)" ) ); } if (oidcConfig) { const guardMode = normalizeGuardMode(oidcConfig.autoProvisionGuard); if (guardMode) { setupOverviewList.appendChild( renderStatusItem( "Auto-provision guard", guardMode === "invite_or_allowlist" ? "ok" : "warn", guardMode ) ); } else { setupOverviewList.appendChild( renderStatusItem("Auto-provision guard", "warn", "unknown") ); } } const hasClientSecret = Boolean(oidcClientSecretInput.value.trim()); setupOverviewList.appendChild( renderStatusItem( "OIDC client secret", hasClientSecret ? "ok" : "error", hasClientSecret ? "set" : "missing" ) ); } function brokerBaseFromSettings() { const broker = document.getElementById("qortal-broker-base-url"); if (!broker) { return ""; } return broker.value.trim(); } function renderOidcPolicy(config) { const data = config && config.ok !== false ? config.data || config : null; if (!data) { const brokerError = extractBrokerError(config); oidcPolicyMode.textContent = "unavailable"; oidcGuard.textContent = "unavailable"; oidcInviteTtl.textContent = "-"; oidcRequireEmail.textContent = "unavailable"; oidcRedirects.textContent = "-"; oidcPolicyNote.textContent = brokerError ? `Broker policy config is unavailable: ${brokerError}` : "Broker policy config is unavailable. Check broker connectivity."; autoProvisionEnabled = oidcPolicySelect.value === "auto_provision"; autoProvisionGuardEnabled = isGuardEnabled(oidcGuardToggle.value); updateAutoProvisionVisibility({ policyMode: autoProvisionEnabled ? "auto_provision" : "link_only", autoProvisionGuard: autoProvisionGuardEnabled, }); oidcEnvOutput.textContent = buildEnvSnippet(); return; } const source = data.sources || {}; const policySource = source.policyMode === "admin" ? "admin override" : "env"; const guardSource = source.autoProvisionGuard === "admin" ? "admin override" : "env"; const ttlSource = source.inviteTtlSeconds === "admin" ? "admin override" : "env"; const requireEmailSource = source.requireEmailForNewAccount === "admin" ? "admin override" : "env"; const redirectsSource = source.redirectUriAllowlist === "admin" ? "admin override" : "env"; const guardMode = normalizeGuardMode(data.autoProvisionGuard); oidcPolicyMode.textContent = `${ data.policyMode || "unknown" } (${policySource})`; oidcGuard.textContent = `${guardMode || "unknown"} (${guardSource})`; oidcInviteTtl.textContent = `${ typeof data.inviteTtlSeconds === "number" ? String(data.inviteTtlSeconds) : "-" } (${ttlSource})`; oidcRequireEmail.textContent = `${ data.requireEmailForNewAccount ? "required" : "off" } (${requireEmailSource})`; const redirectsText = Array.isArray(data.redirectUriAllowlist) && data.redirectUriAllowlist.length > 0 ? data.redirectUriAllowlist.join(", ") : "(none)"; oidcRedirects.textContent = `${redirectsText} (${redirectsSource})`; const overrides = data.overrides || {}; oidcPolicySelect.value = typeof overrides.policyMode === "string" ? overrides.policyMode : ""; oidcGuardToggle.value = normalizeGuardMode(overrides.autoProvisionGuard); oidcInviteInput.value = typeof overrides.inviteTtlSeconds === "number" ? String(overrides.inviteTtlSeconds) : ""; if (typeof overrides.requireEmailForNewAccount === "boolean") { oidcRequireEmailToggle.value = overrides.requireEmailForNewAccount ? "required" : "off"; } else { oidcRequireEmailToggle.value = ""; } oidcRedirectInput.value = Array.isArray(overrides.redirectUriAllowlist) ? overrides.redirectUriAllowlist.join(",") : ""; oidcEnvOutput.textContent = buildEnvSnippet(); autoProvisionEnabled = data.policyMode === "auto_provision"; autoProvisionGuardEnabled = guardMode === "invite_or_allowlist"; if (autoProvisionEnabled) { oidcPolicyNote.textContent = `Auto-provision is enabled. Guard mode is ${ guardMode || "unknown" }. Source labels show env vs admin overrides.`; } else { oidcPolicyNote.textContent = "Auto-provision is disabled. Invite tokens are hidden; allowlist entries are not enforced. Source labels show env vs admin overrides."; } updateAutoProvisionVisibility(data); } function updateAutoProvisionVisibility(config) { const policy = config && config.policyMode ? config.policyMode : "link_only"; const guardEnabled = isGuardEnabled(config && config.autoProvisionGuard); const autoProvisionEnabled = policy === "auto_provision"; allowlistCard.classList.toggle("qortal-hidden", false); invitesCard.classList.toggle( "qortal-hidden", !autoProvisionEnabled || !guardEnabled ); notifyIncludeInvite.disabled = !autoProvisionEnabled; if (!autoProvisionEnabled) { notifyIncludeInvite.checked = false; } } function updateSetupComponentsVisibility(plan) { if (!plan || !Array.isArray(plan.errors)) { setupComponentsCard.classList.remove("qortal-hidden"); setupOverviewNote.textContent = "Run setup if this is a fresh instance."; return; } if (plan.errors.length > 0) { setupComponentsCard.classList.remove("qortal-hidden"); setupOverviewNote.textContent = "Setup commands are required to complete OIDC provider configuration."; } else { setupComponentsCard.classList.add("qortal-hidden"); setupOverviewNote.textContent = "Setup commands are hidden because configuration looks complete."; } } function buildEnvSnippet() { const policyMode = oidcPolicySelect.value.trim(); const guard = oidcGuardToggle.value.trim(); const inviteTtl = oidcInviteInput.value.trim(); const requireEmail = oidcRequireEmailToggle.value.trim(); const redirectAllowlist = oidcRedirectInput.value.trim(); const lines = []; if (policyMode) { lines.push(`OIDC_POLICY_MODE=${policyMode}`); } if (guard) { lines.push(`OIDC_AUTO_PROVISION_GUARD=${guard}`); } if (inviteTtl) { lines.push(`OIDC_INVITE_TTL_SECONDS=${inviteTtl}`); } if (requireEmail) { lines.push( `OIDC_REQUIRE_EMAIL_FOR_NEW_ACCOUNT=${ requireEmail === "required" ? "true" : "false" }` ); } if (redirectAllowlist) { lines.push(`OIDC_REDIRECT_URI_ALLOWLIST=${redirectAllowlist}`); } if (lines.length === 0) { lines.push("# Using env defaults for OIDC policy settings"); } return lines.join("\n"); } function buildExternalAuthEnvSnippet() { const baseUrl = externalAuthBaseUrlInput.value.trim() || "http://external_auth:3191"; const brokerInternalApiToken = brokerInternalApiTokenInput.value.trim(); const appId = externalAuthAppIdInput.value.trim(); const appSecret = externalAuthAppSecretInput.value.trim(); const nodeUrl = externalAuthNodeUrlInput.value.trim(); const nodeApiKey = externalAuthNodeApiKeyInput.value.trim(); const nodeApiKeyMode = externalAuthNodeApiKeyModeInput.value.trim() || "paths"; const defaultNodeApiKeyPaths = "/"; const nodeApiKeyPathsRaw = externalAuthNodeApiKeyPathsInput.value.trim(); const nodeApiKeyPaths = nodeApiKeyMode === "paths" ? nodeApiKeyPathsRaw || defaultNodeApiKeyPaths : nodeApiKeyPathsRaw; const lines = [`QORTAL_EXTERNAL_AUTH_BASE_URL=${baseUrl}`]; if (brokerInternalApiToken) { lines.push(`BROKER_INTERNAL_API_TOKEN=${brokerInternalApiToken}`); } if (appId) { lines.push(`QORTAL_EXTERNAL_AUTH_APP_ID=${appId}`); } if (appSecret) { lines.push(`QORTAL_EXTERNAL_AUTH_APP_SECRET=${appSecret}`); } if (nodeUrl) { lines.push(`QORTAL_AUTH_NODE_URL=${nodeUrl}`); } if (nodeApiKey) { lines.push(`QORTAL_AUTH_NODE_API_KEY=${nodeApiKey}`); } if (nodeApiKeyMode) { lines.push(`QORTAL_AUTH_NODE_API_KEY_MODE=${nodeApiKeyMode}`); } if (nodeApiKeyPaths) { lines.push(`QORTAL_AUTH_NODE_API_KEY_PATHS=${nodeApiKeyPaths}`); } return lines.join("\n"); } async function registerExternalAuthApp() { const name = externalAuthAppNameInput.value.trim() || "qortal-nextcloud-integration"; const warning = "Registering a new External Auth app will replace existing credentials.\\n\\n" + "If External Auth is already configured via .env, this will generate a new App ID/Secret and you may lose access to existing wallets.\\n\\n" + "Backup your .env or .env.devprod before continuing.\\n\\n" + "Continue?"; if (!window.confirm(warning)) { return; } setFeedback("Registering External Auth app...", false); const body = new URLSearchParams({ name }); const payload = await requestJson(registerExternalAuthUrl, { method: "POST", headers, body, }); externalAuthAppIdInput.value = payload.appId || ""; externalAuthAppSecretInput.value = payload.appSecret || ""; externalAuthEnvOutput.textContent = buildExternalAuthEnvSnippet(); await saveSettings(); setFeedback("External Auth app registered and saved.", false); } function buildQortalNodeEnvSnippet() { const nodeUrl = qortalNodeUrlInput.value.trim(); const nodeApiKey = qortalNodeApiKeyInput.value.trim(); const gatewayUrl = qortalGatewayUrlInput.value.trim(); const lines = []; if (nodeUrl) { lines.push(`QORTAL_NODE_URL=${nodeUrl}`); } if (nodeApiKey) { lines.push(`QORTAL_NODE_API_KEY=${nodeApiKey}`); } if (gatewayUrl) { lines.push(`QORTAL_GATEWAY_URL=${gatewayUrl}`); } if (lines.length === 0) { lines.push( "# Set Qortal node/gateway values above to generate a snippet." ); } return lines.join("\n"); } function renderWallets(payload) { const wallets = payload && payload.ok && payload.data && Array.isArray(payload.data.wallets) ? payload.data.wallets : []; walletsBody.innerHTML = ""; if (wallets.length === 0) { walletsBody.innerHTML = 'No wallets visible to broker app credentials.'; return; } wallets.forEach(function (wallet) { const row = document.createElement("tr"); const walletIdCell = document.createElement("td"); const walletIdCode = document.createElement("code"); walletIdCode.textContent = wallet.walletId || ""; walletIdCell.appendChild(walletIdCode); const addressCell = document.createElement("td"); const addressCode = document.createElement("code"); addressCode.textContent = wallet.address0 || ""; addressCell.appendChild(addressCode); const createdCell = document.createElement("td"); createdCell.textContent = formatTimestamp( wallet.createdAt ? Number(wallet.createdAt) : "" ); row.appendChild(walletIdCell); row.appendChild(addressCell); row.appendChild(createdCell); walletsBody.appendChild(row); }); } function renderMappings(payload) { const mappings = payload && payload.ok && payload.data && Array.isArray(payload.data.mappings) ? payload.data.mappings : []; mappingsBody.innerHTML = ""; if (mappings.length === 0) { mappingsBody.innerHTML = 'No mappings found.'; return; } mappings.forEach(function (mapping) { const row = document.createElement("tr"); const addressCell = document.createElement("td"); const addressCode = document.createElement("code"); addressCode.textContent = mapping.qortalAddress || ""; addressCell.appendChild(addressCode); const userCell = document.createElement("td"); const userCode = document.createElement("code"); userCode.textContent = mapping.nextcloudUserId || ""; userCell.appendChild(userCode); const walletCell = document.createElement("td"); const walletCode = document.createElement("code"); walletCode.textContent = mapping.walletId || ""; walletCell.appendChild(walletCode); const statusCell = document.createElement("td"); statusCell.textContent = mapping.status || ""; const updatedCell = document.createElement("td"); updatedCell.textContent = formatTimestamp(mapping.updatedAt); const actionCell = document.createElement("td"); const unlinkButton = document.createElement("button"); unlinkButton.className = "button"; unlinkButton.textContent = "Unlink"; unlinkButton.addEventListener("click", async function () { try { await unlinkMapping(mapping.qortalAddress); setFeedback("Mapping unlinked.", false); } catch (error) { setFeedback(error.message || "Failed to unlink mapping", true); } }); actionCell.appendChild(unlinkButton); row.appendChild(addressCell); row.appendChild(userCell); row.appendChild(walletCell); row.appendChild(statusCell); row.appendChild(updatedCell); row.appendChild(actionCell); mappingsBody.appendChild(row); }); } function renderAllowlist(payload) { const entries = payload && payload.ok && payload.data && Array.isArray(payload.data.allowlist) ? payload.data.allowlist : []; allowlistBody.innerHTML = ""; if (entries.length === 0) { allowlistBody.innerHTML = 'No allowlisted addresses yet.'; return; } entries.forEach(function (entry) { const row = document.createElement("tr"); const addressCell = document.createElement("td"); const addressCode = document.createElement("code"); addressCode.textContent = entry.qortalAddress || ""; addressCell.appendChild(addressCode); const addedByCell = document.createElement("td"); addedByCell.textContent = entry.addedBy || ""; const addedAtCell = document.createElement("td"); addedAtCell.textContent = formatTimestamp(entry.addedAt); const actionCell = document.createElement("td"); const removeButton = document.createElement("button"); removeButton.className = "button"; removeButton.textContent = "Remove"; removeButton.addEventListener("click", async function () { try { await removeAllowlist(entry.qortalAddress); setFeedback("Allowlist entry removed.", false); } catch (error) { setFeedback( error.message || "Failed to remove allowlist entry", true ); } }); actionCell.appendChild(removeButton); row.appendChild(addressCell); row.appendChild(addedByCell); row.appendChild(addedAtCell); row.appendChild(actionCell); allowlistBody.appendChild(row); }); } function renderQapps() { qappsBody.innerHTML = ""; if (qapps.length === 0) { qappsBody.innerHTML = 'No Q-Apps configured yet.'; return; } qapps.forEach(function (entry) { const row = document.createElement("tr"); const nameCell = document.createElement("td"); nameCell.textContent = entry.name || ""; const addressCell = document.createElement("td"); const addressCode = document.createElement("code"); addressCode.textContent = entry.address || ""; addressCell.appendChild(addressCode); const descriptionCell = document.createElement("td"); descriptionCell.textContent = entry.description || ""; const iconCell = document.createElement("td"); if (entry.iconMode === "custom" && entry.iconUrl) { const img = document.createElement("img"); img.src = entry.iconUrl; img.alt = entry.name || "Q-App icon"; img.className = "qortal-qapp-icon"; iconCell.appendChild(img); } else { iconCell.textContent = "auto"; } const actionCell = document.createElement("td"); const removeButton = document.createElement("button"); removeButton.className = "button"; removeButton.textContent = "Remove"; removeButton.addEventListener("click", async function () { qapps = qapps.filter(function (item) { return item.address !== entry.address; }); renderQapps(); markDirty(); try { await saveSettings(); setFeedback("Q-App removed and saved.", false); } catch (error) { setFeedback(error.message || "Failed to save Q-App removal", true); } }); actionCell.appendChild(removeButton); row.appendChild(nameCell); row.appendChild(addressCell); row.appendChild(iconCell); row.appendChild(descriptionCell); row.appendChild(actionCell); qappsBody.appendChild(row); }); } function addQapp() { const name = qappsNameInput.value.trim(); const address = qappsAddressInput.value.trim(); const iconMode = qappsIconModeInput.value === "custom" ? "custom" : "auto"; const iconUrl = qappsIconUrlInput.value.trim(); const description = qappsDescriptionInput.value.trim(); if (!address) { throw new Error("Q-App address is required"); } if (!/^qortal:\/\//i.test(address)) { throw new Error("Q-App address must start with qortal://"); } if (iconMode === "custom" && !iconUrl) { throw new Error( "Custom icon URL is required when icon mode is set to custom" ); } const entry = { name: name || address, address: address, description: description, iconMode: iconMode, iconUrl: iconUrl, }; const exists = qapps.some(function (item) { return item.address === entry.address; }); if (exists) { throw new Error("Q-App address already exists in list"); } qapps.push(entry); qappsNameInput.value = ""; qappsAddressInput.value = ""; qappsIconModeInput.value = "auto"; qappsIconUrlInput.value = ""; qappsDescriptionInput.value = ""; renderQapps(); markDirty(); } function clearQapps() { qapps = []; renderQapps(); markDirty(); } function inviteStatus(invite) { if (!invite) { return ""; } if (invite.revokedAt) { return "revoked"; } if (invite.usedAt) { return "used"; } if (invite.expiresAt && new Date(invite.expiresAt).getTime() < Date.now()) { return "expired"; } return "active"; } function renderInvites(payload) { const invites = payload && payload.ok && payload.data && Array.isArray(payload.data.invites) ? payload.data.invites : []; invitesBody.innerHTML = ""; if (invites.length === 0) { invitesBody.innerHTML = 'No invites generated yet.'; return; } invites.forEach(function (invite) { const row = document.createElement("tr"); const tokenCell = document.createElement("td"); const tokenCode = document.createElement("code"); tokenCode.textContent = invite.token || ""; tokenCell.appendChild(tokenCode); const statusCell = document.createElement("td"); statusCell.textContent = inviteStatus(invite); const expiresCell = document.createElement("td"); expiresCell.textContent = formatTimestamp(invite.expiresAt); const usedByCell = document.createElement("td"); const usedText = invite.usedByQortalAddress || invite.usedByNextcloudUserId || ""; if (usedText) { const usedCode = document.createElement("code"); usedCode.textContent = usedText; usedByCell.appendChild(usedCode); } const actionCell = document.createElement("td"); const revokeButton = document.createElement("button"); revokeButton.className = "button"; revokeButton.textContent = "Revoke"; revokeButton.disabled = Boolean(invite.revokedAt || invite.usedAt); revokeButton.addEventListener("click", async function () { try { await revokeInvite(invite.token); setFeedback("Invite revoked.", false); } catch (error) { setFeedback(error.message || "Failed to revoke invite", true); } }); actionCell.appendChild(revokeButton); row.appendChild(tokenCell); row.appendChild(statusCell); row.appendChild(expiresCell); row.appendChild(usedByCell); row.appendChild(actionCell); invitesBody.appendChild(row); }); } async function refreshSetupData() { setFeedback("Refreshing setup data...", false); const payload = await requestJson(setupUrl, { method: "GET", headers, }); assertBrokerOk( payload.status && payload.status.brokerHealth, "Broker health check failed" ); assertBrokerOk( payload.status && payload.status.qortalHealth, "Qortal health check failed" ); assertBrokerOk(payload.wallets, "Failed to fetch wallets"); assertBrokerOk(payload.mappings, "Failed to fetch mappings"); statusEl.textContent = JSON.stringify(payload.status, null, 2); renderWallets(payload.wallets); renderMappings(payload.mappings); renderSetupOverview(payload); renderOidcPolicy(payload.oidcConfig); setFeedback("Setup data refreshed.", false); } async function saveSettings() { setFeedback("Saving...", false); const form = new URLSearchParams(collectSettings()); const payload = await requestJson(settingsUrl, { method: "POST", headers, body: form, }); statusEl.textContent = JSON.stringify( { settings: payload.settings || {}, oidcPolicySync: payload.oidcPolicySync || null, qortalRuntimeSync: payload.qortalRuntimeSync || null, }, null, 2 ); refreshSetupPlan().catch(function () { // ignore }); refreshSetupData().catch(function () { // ignore }); resetDirty(); const syncErrors = []; const syncWarnings = []; if (payload.oidcPolicySync && payload.oidcPolicySync.ok === false) { syncErrors.push( `broker policy sync failed: ${ payload.oidcPolicySync.error || "Unknown error" }` ); } if (payload.qortalRuntimeSync && payload.qortalRuntimeSync.ok === false) { syncErrors.push( `broker runtime sync failed: ${ payload.qortalRuntimeSync.error || "Unknown error" }` ); } if ( payload.qortalRuntimeSync && payload.qortalRuntimeSync.ok !== false && payload.qortalRuntimeSync.nodeSettingsSync && payload.qortalRuntimeSync.nodeSettingsSync.warning ) { syncWarnings.push(payload.qortalRuntimeSync.nodeSettingsSync.warning); } if (syncErrors.length > 0) { setFeedback(`Settings saved, but ${syncErrors.join(" | ")}`, true); return; } if (syncWarnings.length > 0) { setFeedback( `Settings saved with warning: ${syncWarnings.join(" | ")}`, false ); return; } setFeedback("Settings and runtime sync saved.", false); } async function testConnection() { setFeedback("Testing broker connectivity...", false); const payload = await requestJson(statusUrl, { method: "GET", headers, }); statusEl.textContent = JSON.stringify(payload, null, 2); setFeedback("Broker connectivity check completed.", false); } async function runSetupPlan() { setFeedback("Generating setup plan...", false); const payload = await requestJson(setupPlanUrl, { method: "POST", headers, }); setupResult.textContent = JSON.stringify(payload, null, 2); lastSetupPlan = payload; updateSetupComponentsVisibility(payload); setFeedback("Setup plan generated.", false); } async function refreshSetupPlan() { const payload = await requestJson(setupPlanUrl, { method: "POST", headers, }); lastSetupPlan = payload; updateSetupComponentsVisibility(payload); } async function runSetupOcc() { setFeedback("Running setup via occ...", false); const payload = await requestJson(setupOccUrl, { method: "POST", headers, }); setupResult.textContent = JSON.stringify(payload, null, 2); setFeedback("Setup completed.", false); } async function sendNotifications() { const body = new URLSearchParams({ userIds: notifyUserIds.value || "", groupIds: notifyGroupIds.value || "", sendEmail: notifyEmail.checked ? "1" : "", sendInApp: notifyInApp.checked ? "1" : "", includeInvite: notifyIncludeInvite.checked ? "1" : "", queue: notifyQueue.checked ? "1" : "", }); const payload = await requestJson(notifyUrl, { method: "POST", headers, body, }); notifyResult.textContent = JSON.stringify(payload, null, 2); return payload; } async function searchUsers() { const query = userSearchQuery.value.trim(); if (!query) { userSearchResults.innerHTML = 'Enter a search term.'; return; } const url = searchUsersUrl + "?query=" + encodeURIComponent(query); const payload = await requestJson(url, { method: "GET", headers, }); const users = payload.users || []; lastUserResults = users; userSearchResults.innerHTML = ""; if (users.length === 0) { userSearchResults.innerHTML = 'No users found.'; return; } users.forEach(function (user) { const row = document.createElement("tr"); const idCell = document.createElement("td"); idCell.textContent = user.id || ""; const nameCell = document.createElement("td"); nameCell.textContent = user.displayName || ""; const emailCell = document.createElement("td"); emailCell.textContent = user.email || ""; const actionCell = document.createElement("td"); const addButton = document.createElement("button"); addButton.className = "button"; addButton.textContent = "Add"; addButton.addEventListener("click", function () { appendUserId(user.id || ""); }); actionCell.appendChild(addButton); row.appendChild(idCell); row.appendChild(nameCell); row.appendChild(emailCell); row.appendChild(actionCell); userSearchResults.appendChild(row); }); } async function searchGroups() { const query = groupSearchQuery.value.trim(); if (!query) { groupSearchResults.innerHTML = 'Enter a search term.'; return; } const url = searchGroupsUrl + "?query=" + encodeURIComponent(query); const payload = await requestJson(url, { method: "GET", headers, }); const groups = payload.groups || []; groupSearchResults.innerHTML = ""; if (groups.length === 0) { groupSearchResults.innerHTML = 'No groups found.'; return; } groups.forEach(function (group) { const row = document.createElement("tr"); const idCell = document.createElement("td"); idCell.textContent = group.id || ""; const nameCell = document.createElement("td"); nameCell.textContent = group.displayName || ""; const actionCell = document.createElement("td"); const addButton = document.createElement("button"); addButton.className = "button"; addButton.textContent = "Add"; addButton.addEventListener("click", function () { appendGroupId(group.id || ""); }); actionCell.appendChild(addButton); row.appendChild(idCell); row.appendChild(nameCell); row.appendChild(actionCell); groupSearchResults.appendChild(row); }); } function appendUserId(userId) { if (!userId) { return; } const existing = new Set( notifyUserIds.value .split(/\s+/) .map(function (entry) { return entry.trim(); }) .filter(Boolean) ); existing.add(userId); notifyUserIds.value = Array.from(existing).join("\n"); } function appendGroupId(groupId) { if (!groupId) { return; } const existing = new Set( notifyGroupIds.value .split(/\s+/) .map(function (entry) { return entry.trim(); }) .filter(Boolean) ); existing.add(groupId); notifyGroupIds.value = Array.from(existing).join("\n"); } function renderTemplate(template, tokens) { let result = template || ""; Object.keys(tokens).forEach(function (key) { result = result.split("{" + key + "}").join(tokens[key]); }); return result.trim(); } function buildEmailPreview() { const subjectTemplate = notifyEmailSubject.value || ""; const bodyTemplate = notifyEmailBody.value || ""; const inviteToken = notifyIncludeInvite.checked ? "INVITE_TOKEN" : ""; const baseUrl = nextcloudPublicUrlInput.value.trim() || nextcloudPublicUrlValue || window.location.origin; const link = baseUrl.replace(/\/$/, "") + "/settings/user/qortal_integration#qortal-create-wallet"; const firstUserId = (notifyUserIds.value || "") .split(/\s+/) .map(function (entry) { return entry.trim(); }) .filter(Boolean)[0] || "user"; const match = lastUserResults.find(function (user) { return user.id === firstUserId; }) || {}; const displayName = match.displayName || firstUserId; const tokens = { link: link, invite: inviteToken, user: firstUserId, displayName: displayName, }; previewSubject.value = renderTemplate(subjectTemplate, tokens); previewBody.value = renderTemplate(bodyTemplate, tokens); } async function createWallet() { const password = document.getElementById("qortal-wallet-password").value; const kdfThreads = document.getElementById( "qortal-wallet-kdf-threads" ).value; if (!password) { throw new Error("Wallet password is required"); } setFeedback("Creating wallet...", false); const body = new URLSearchParams({ password, kdfThreads: kdfThreads || "", }); const payload = await requestJson(createWalletUrl, { method: "POST", headers, body, }); assertBrokerOk(payload, "Wallet creation failed"); walletCreateResult.textContent = JSON.stringify(payload, null, 2); document.getElementById("qortal-wallet-password").value = ""; await refreshWallets(); setFeedback("Wallet created.", false); } async function createWalletAndLink() { const password = document.getElementById("qortal-wallet-password").value; const kdfThreads = document.getElementById( "qortal-wallet-kdf-threads" ).value; const nextcloudUserId = walletUserIdInput.value.trim(); if (!password) { throw new Error("Wallet password is required"); } if (!nextcloudUserId) { throw new Error("Nextcloud user ID is required to link a wallet"); } setFeedback("Creating wallet and linking user...", false); const body = new URLSearchParams({ password, kdfThreads: kdfThreads || "", }); const payload = await requestJson(createWalletUrl, { method: "POST", headers, body, }); const wallet = payload.wallet; if (!wallet || !wallet.address0) { throw new Error("Wallet creation did not return an address"); } const linkBody = new URLSearchParams({ qortalAddress: wallet.address0, walletId: wallet.walletId || "", nextcloudUserId, }); const linkPayload = await requestJson(linkMappingUrl, { method: "POST", headers, body: linkBody, }); statusEl.textContent = JSON.stringify( { wallet: payload, mapping: linkPayload, }, null, 2 ); await refreshWallets(); await refreshMappings(); setFeedback("Wallet created and linked.", false); } async function refreshWallets() { const payload = await requestJson(walletsUrl, { method: "GET", headers, }); assertBrokerOk(payload, "Failed to fetch wallets"); renderWallets(payload); } async function linkMapping() { const qortalAddress = document .getElementById("qortal-link-address") .value.trim(); const walletId = document .getElementById("qortal-link-wallet-id") .value.trim(); const nextcloudUserId = document .getElementById("qortal-link-user-id") .value.trim(); if (!qortalAddress || !nextcloudUserId) { throw new Error("Qortal address and Nextcloud user ID are required"); } setFeedback("Linking mapping...", false); const body = new URLSearchParams({ qortalAddress: qortalAddress, walletId: walletId, nextcloudUserId: nextcloudUserId, }); const payload = await requestJson(linkMappingUrl, { method: "POST", headers, body, }); assertBrokerOk(payload, "Failed to link mapping"); statusEl.textContent = JSON.stringify(payload, null, 2); await refreshMappings(); setFeedback("Mapping linked.", false); } async function unlinkMapping(qortalAddress) { if (!qortalAddress) { throw new Error("qortalAddress is required"); } const body = new URLSearchParams({ qortalAddress: qortalAddress, }); const payload = await requestJson(unlinkMappingUrl, { method: "POST", headers, body, }); assertBrokerOk(payload, "Failed to unlink mapping"); statusEl.textContent = JSON.stringify(payload, null, 2); await refreshMappings(); } async function refreshMappings() { const payload = await requestJson(mappingsUrl, { method: "GET", headers, }); assertBrokerOk(payload, "Failed to fetch mappings"); renderMappings(payload); } async function refreshAllowlist() { const payload = await requestJson(allowlistUrl, { method: "GET", headers, }); assertBrokerOk(payload, "Failed to fetch allowlist"); renderAllowlist(payload); } async function addAllowlist() { const qortalAddress = document .getElementById("qortal-allowlist-address") .value.trim(); if (!qortalAddress) { throw new Error("Qortal address is required"); } const body = new URLSearchParams({ qortalAddress: qortalAddress, }); const payload = await requestJson(addAllowlistUrl, { method: "POST", headers, body, }); assertBrokerOk(payload, "Failed to add allowlist entry"); await refreshAllowlist(); } async function removeAllowlist(qortalAddress) { if (!qortalAddress) { throw new Error("qortalAddress is required"); } const body = new URLSearchParams({ qortalAddress: qortalAddress, }); const payload = await requestJson(removeAllowlistUrl, { method: "POST", headers, body, }); assertBrokerOk(payload, "Failed to remove allowlist entry"); await refreshAllowlist(); } async function refreshInvites() { const payload = await requestJson(invitesUrl, { method: "GET", headers, }); assertBrokerOk(payload, "Failed to fetch invites"); renderInvites(payload); } async function createInvite() { const expiryValue = document.getElementById( "qortal-invite-expiry-hours" ).value; const expiryHours = expiryValue ? Number(expiryValue) : null; if ( expiryHours !== null && (!Number.isFinite(expiryHours) || expiryHours <= 0) ) { throw new Error("Expiry hours must be a positive number"); } const body = new URLSearchParams(); if (expiryHours !== null) { body.set("expiresInHours", String(Math.floor(expiryHours))); } const payload = await requestJson(createInviteUrl, { method: "POST", headers, body, }); assertBrokerOk(payload, "Failed to create invite"); inviteCreateResult.textContent = JSON.stringify(payload, null, 2); await refreshInvites(); return payload; } async function revokeInvite(token) { if (!token) { throw new Error("token is required"); } const body = new URLSearchParams({ token: token }); const payload = await requestJson(revokeInviteUrl, { method: "POST", headers, body, }); assertBrokerOk(payload, "Failed to revoke invite"); await refreshInvites(); } renderQapps(); addQappButton.addEventListener("click", function () { (async function () { try { addQapp(); await saveSettings(); setFeedback("Q-App added and saved.", false); } catch (error) { setFeedback(error.message || "Failed to add Q-App", true); } })(); }); clearQappsButton.addEventListener("click", function () { (async function () { try { clearQapps(); await saveSettings(); setFeedback("Q-App list cleared and saved.", false); } catch (error) { setFeedback(error.message || "Failed to clear Q-App list", true); } })(); }); saveButton.addEventListener("click", async function () { try { await saveSettings(); } catch (error) { setFeedback(error.message || "Failed to save settings", true); } }); window.addEventListener("beforeunload", function (event) { if (!hasUnsavedChanges) { return; } event.preventDefault(); event.returnValue = "You have unsaved changes. Save settings or they will be lost."; }); refreshSetupButton.addEventListener("click", async function () { try { await refreshSetupData(); } catch (error) { setFeedback(error.message || "Failed to load setup data", true); } }); testButton.addEventListener("click", async function () { try { await testConnection(); } catch (error) { setFeedback(error.message || "Broker connectivity check failed", true); } }); setupPlanButton.addEventListener("click", async function () { try { await runSetupPlan(); } catch (error) { setFeedback(error.message || "Failed to generate setup plan", true); } }); setupOccButton.addEventListener("click", async function () { try { await runSetupOcc(); } catch (error) { setFeedback(error.message || "Setup failed", true); } }); sendNotificationsButton.addEventListener("click", async function () { try { setFeedback("Sending notifications...", false); await sendNotifications(); setFeedback("Notifications sent.", false); } catch (error) { setFeedback(error.message || "Failed to send notifications", true); } }); searchUsersButton.addEventListener("click", async function () { try { await searchUsers(); } catch (error) { setFeedback(error.message || "User search failed", true); } }); searchGroupsButton.addEventListener("click", async function () { try { await searchGroups(); } catch (error) { setFeedback(error.message || "Group search failed", true); } }); previewEmailButton.addEventListener("click", function () { buildEmailPreview(); }); oidcEnvButton.addEventListener("click", function () { oidcEnvOutput.textContent = buildEnvSnippet(); }); [ oidcPolicySelect, oidcGuardToggle, oidcInviteInput, oidcRequireEmailToggle, oidcRedirectInput, ].forEach(function (el) { el.addEventListener("change", function () { oidcEnvOutput.textContent = buildEnvSnippet(); markDirty(); }); }); externalAuthEnvButton.addEventListener("click", function () { externalAuthEnvOutput.textContent = buildExternalAuthEnvSnippet(); }); externalAuthRegisterButton.addEventListener("click", async function () { try { await registerExternalAuthApp(); } catch (error) { setFeedback( error.message || "Failed to register External Auth app", true ); } }); [ brokerInternalApiTokenInput, externalAuthBaseUrlInput, externalAuthAppIdInput, externalAuthAppSecretInput, externalAuthNodeUrlInput, externalAuthNodeApiKeyInput, externalAuthNodeApiKeyModeInput, externalAuthNodeApiKeyPathsInput, ].forEach(function (el) { el.addEventListener("change", function () { externalAuthEnvOutput.textContent = buildExternalAuthEnvSnippet(); markDirty(); }); }); qortalNodeEnvButton.addEventListener("click", function () { qortalNodeEnvOutput.textContent = buildQortalNodeEnvSnippet(); }); [ qortalNodeUrlInput, qortalNodeApiKeyInput, qortalGatewayUrlInput, qortalGatewayInsecureInput, ].forEach(function (el) { el.addEventListener("change", function () { qortalNodeEnvOutput.textContent = buildQortalNodeEnvSnippet(); markDirty(); }); }); [ document.getElementById("qortal-broker-base-url"), brokerInternalApiTokenInput, document.getElementById("qortal-external-auth-docs-url"), document.getElementById("qortal-feature-qdn-backups"), document.getElementById("qortal-feature-qmail"), oidcIssuerInput, oidcClientIdInput, oidcClientSecretInput, nextcloudPublicUrlInput, notifyEmailSubject, notifyEmailBody, qappsEnabledInput, qappsBrowserEnabledInput, qappsBrowserAddressInput, qappsDebugEnabledInput, qappsIconModeInput, qappsIconUrlInput, ].forEach(function (el) { if (!el) { return; } el.addEventListener("change", function () { markDirty(); if (el === qappsIconModeInput) { updateIconUrlState(); } }); if ( el.tagName === "TEXTAREA" || el.type === "text" || el.type === "password" ) { el.addEventListener("input", function () { markDirty(); }); } }); createWalletButton.addEventListener("click", async function () { try { await createWallet(); } catch (error) { setFeedback(error.message || "Failed to create wallet", true); } }); createWalletLinkButton.addEventListener("click", async function () { try { await createWalletAndLink(); } catch (error) { setFeedback( error.message || "Failed to create wallet and link user", true ); } }); refreshWalletsButton.addEventListener("click", async function () { try { await refreshWallets(); setFeedback("Wallet list refreshed.", false); } catch (error) { setFeedback(error.message || "Failed to load wallets", true); } }); linkMappingButton.addEventListener("click", async function () { try { await linkMapping(); } catch (error) { setFeedback(error.message || "Failed to link mapping", true); } }); refreshMappingsButton.addEventListener("click", async function () { try { await refreshMappings(); setFeedback("Mappings refreshed.", false); } catch (error) { setFeedback(error.message || "Failed to load mappings", true); } }); addAllowlistButton.addEventListener("click", async function () { try { await addAllowlist(); setFeedback("Allowlist updated.", false); } catch (error) { setFeedback(error.message || "Failed to add allowlist entry", true); } }); refreshAllowlistButton.addEventListener("click", async function () { try { await refreshAllowlist(); setFeedback("Allowlist refreshed.", false); } catch (error) { setFeedback(error.message || "Failed to load allowlist", true); } }); createInviteButton.addEventListener("click", async function () { try { await createInvite(); setFeedback("Invite created.", false); } catch (error) { setFeedback(error.message || "Failed to create invite", true); } }); refreshInvitesButton.addEventListener("click", async function () { try { await refreshInvites(); setFeedback("Invites refreshed.", false); } catch (error) { setFeedback(error.message || "Failed to load invites", true); } }); inviteMessageButton.addEventListener("click", async function () { try { setFeedback("Generating invite message...", false); let token = ""; if (autoProvisionEnabled && autoProvisionGuardEnabled) { const payload = await createInvite(); const invite = payload && payload.data && payload.data.invite ? payload.data.invite : payload && payload.invite ? payload.invite : payload; token = invite && invite.token ? invite.token : ""; } const baseUrl = nextcloudPublicUrlInput.value.trim() || nextcloudPublicUrlValue || ""; const loginPath = "/login"; const fullLink = baseUrl ? baseUrl.replace(/\/$/, "") + loginPath : loginPath; const message = autoProvisionEnabled && autoProvisionGuardEnabled ? [ "Hello,", "", "You have been invited to create a Nextcloud account using Qortal authentication.", "Open the Qortal Integration onboarding page:", fullLink, "", token ? `Invite token: ${token}` : "Invite token: (admin to provide)", "", "Create new or import existing Qortal Account (seed phrase or backup) to complete setup.", "After linking, you can sign in to Nextcloud using your Qortal account.", ].join("\n") : [ "Hello,", "", "Your admin enabled Qortal login for existing Nextcloud users.", "Open the Qortal Integration settings to create or link your wallet:", baseUrl ? baseUrl.replace(/\/$/, "") + "/settings/user/qortal_integration#qortal-create-wallet" : "/settings/user/qortal_integration#qortal-create-wallet", "", "If you do not have a Nextcloud account yet, contact your admin to create one.", ].join("\n"); inviteMessageBox.value = message; setFeedback("Invite message generated.", false); } catch (error) { setFeedback(error.message || "Failed to generate invite message", true); } }); copyInviteMessageButton.addEventListener("click", async function () { try { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(inviteMessageBox.value || ""); setFeedback("Invite message copied.", false); } else { inviteMessageBox.focus(); inviteMessageBox.select(); setFeedback("Copy manually (clipboard API unavailable).", true); } } catch (error) { setFeedback("Failed to copy invite message", true); } }); refreshSetupData().catch(function (error) { setFeedback(error.message || "Initial setup data load failed", true); }); refreshSetupPlan().catch(function () { // ignore }); refreshAllowlist().catch(function (error) { setFeedback(error.message || "Initial allowlist load failed", true); }); refreshInvites().catch(function (error) { setFeedback(error.message || "Initial invite load failed", true); }); })();