(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 userMappingsUrl = root.dataset.userMappingsUrl; const accountDashboardUrl = root.dataset.accountDashboardUrl || "/apps/qortal_integration/"; const adminDisplayName = root.dataset.adminDisplayName || ""; const adminEmail = root.dataset.adminEmail || ""; const settingsUrl = root.dataset.settingsUrl; const billingTestUrl = root.dataset.billingTestUrl; const billingSyncUrl = root.dataset.billingSyncUrl; const billingBootstrapUrl = root.dataset.billingBootstrapUrl; const billingCheckoutUrl = root.dataset.billingCheckoutUrl; const billingCatalogUrl = root.dataset.billingCatalogUrl || ""; 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 oidcClientsUrl = root.dataset.oidcClientsUrl; const createOidcClientUrl = root.dataset.createOidcClientUrl; const updateOidcClientUrlTemplate = root.dataset.updateOidcClientUrlTemplate; const setOidcClientEnabledUrlTemplate = root.dataset.setOidcClientEnabledUrlTemplate; const rotateOidcClientSecretUrlTemplate = root.dataset.rotateOidcClientSecretUrlTemplate; const oidcAuditEventsUrl = root.dataset.oidcAuditEventsUrl; 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 adminRecoveryStatusUrl = root.dataset.adminRecoveryStatusUrl; const adminRecoveryIssueCodeUrl = root.dataset.adminRecoveryIssueCodeUrl; const adminRecoveryStartEmergencyUrl = root.dataset.adminRecoveryStartEmergencyUrl; const qappsJson = root.dataset.qappsJson || "[]"; const qappsEnabledValue = root.dataset.qappsEnabled === "1"; const qappsBrowserEnabledValue = root.dataset.qappsBrowserEnabled === "1"; let qappsBrowserAddressValue = root.dataset.qappsBrowserAddress || ""; const qappsDebugEnabledValue = root.dataset.qappsDebugEnabled === "1"; const qappsAdvancedUnlockSettingsEnabledValue = root.dataset.qappsAdvancedUnlockSettingsEnabled === "1"; const qappsNonAdminRestrictionsEnabledValue = root.dataset.qappsNonAdminRestrictionsEnabled !== "0"; const qappsNonAdminRestrictionRulesJson = root.dataset.qappsNonAdminRestrictionRulesJson || "[]"; const userPublishingEnabledValue = root.dataset.userPublishingEnabled === "1"; const externalAuthEnvBaseUrl = root.dataset.externalAuthEnvBaseUrl === "1"; const externalAuthEnvAppCreds = root.dataset.externalAuthEnvAppCreds === "1"; const externalAuthEnvNodeUrl = root.dataset.externalAuthEnvNodeUrl === "1"; const nextcloudPublicUrlValue = root.dataset.nextcloudPublicUrl || ""; const sovereignModeValue = root.dataset.sovereignMode === "full_qortal" ? "full_qortal" : "powered"; const feedbackEl = document.getElementById("qortal-admin-feedback"); const statusEl = document.getElementById("qortal-admin-status"); const saveButton = document.getElementById("qortal-save-settings"); const saveAccessButton = document.getElementById("qortal-save-access"); const saveRuntimeButton = document.getElementById("qortal-save-runtime"); const saveIdentityButton = document.getElementById("qortal-save-identity"); const saveMspButton = document.getElementById("qortal-save-msp"); const saveChdDashboardLinkButton = document.getElementById( "qortal-save-chd-admin-dashboard" ); const savePackagingButton = document.getElementById("qortal-save-packaging"); const bootstrapPackagingButton = document.getElementById( "qortal-bootstrap-packaging" ); const testPackagingButton = document.getElementById("qortal-test-packaging"); const syncPackagingButton = document.getElementById("qortal-sync-packaging"); 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 inviteCommentInput = document.getElementById("qortal-invite-comment"); const refreshInvitesButton = document.getElementById( "qortal-refresh-invites" ); const idpTemplateInput = document.getElementById("qortal-idp-template"); const idpClientIdInput = document.getElementById("qortal-idp-client-id"); const idpDisplayNameInput = document.getElementById( "qortal-idp-display-name" ); const idpRedirectUrisInput = document.getElementById( "qortal-idp-redirect-uris" ); const idpPostLogoutRedirectUrisInput = document.getElementById( "qortal-idp-post-logout-redirect-uris" ); const idpAllowedScopesInput = document.getElementById( "qortal-idp-allowed-scopes" ); const idpAllowedGroupsInput = document.getElementById( "qortal-idp-allowed-groups" ); const idpRequirePkceInput = document.getElementById( "qortal-idp-require-pkce" ); const idpApplyTemplateButton = document.getElementById( "qortal-idp-apply-template" ); const idpSaveClientButton = document.getElementById( "qortal-idp-save-client" ); const idpCancelEditButton = document.getElementById( "qortal-idp-cancel-edit" ); const idpRefreshClientsButton = document.getElementById( "qortal-idp-refresh-clients" ); const idpRefreshAuditButton = document.getElementById( "qortal-idp-refresh-audit" ); const idpCopySecretButton = document.getElementById( "qortal-idp-copy-secret" ); const recoveryStatusCheckButton = document.getElementById( "qortal-recovery-status-check" ); const recoveryIssueCodeButton = document.getElementById( "qortal-recovery-issue-code" ); const recoveryStartEmergencyButton = document.getElementById( "qortal-recovery-start-emergency" ); const recoveryTargetUserIdInput = document.getElementById( "qortal-recovery-target-user-id" ); const recoveryCodeExpiryMinutesInput = document.getElementById( "qortal-recovery-code-expiry-minutes" ); const recoveryEmergencyHoursInput = document.getElementById( "qortal-recovery-emergency-hours" ); const recoveryAdminResult = document.getElementById( "qortal-recovery-admin-result" ); 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 idpSecretOutput = document.getElementById("qortal-idp-secret-output"); const idpConfigOutput = document.getElementById("qortal-idp-config-output"); const idpClientsBody = document.getElementById("qortal-idp-clients-body"); const idpAuditBody = document.getElementById("qortal-idp-audit-body"); const qappsEnabledInput = document.getElementById("qortal-qapps-enabled"); const qappsBrowserEnabledInput = document.getElementById( "qortal-qapps-browser-enabled" ); const qappsDebugEnabledInput = document.getElementById( "qortal-qapps-debug-enabled" ); const qappsAdvancedUnlockSettingsEnabledInput = document.getElementById( "qortal-qapps-advanced-unlock-settings-enabled" ); const qappsNonAdminRestrictionsEnabledInput = document.getElementById( "qortal-qapps-non-admin-restrictions-enabled" ); const qappsNonAdminRulesContainer = document.getElementById( "qortal-qapps-non-admin-rules" ); const qappsNonAdminRulesAddButton = document.getElementById( "qortal-qapps-non-admin-rules-add" ); const userPublishingEnabledInput = document.getElementById( "qortal-user-publishing-enabled" ); const filesPublishMaxSizeOverrideInput = document.getElementById( "qortal-files-publish-max-size-override" ); const filesPublishMaxCountOverrideInput = document.getElementById( "qortal-files-publish-max-count-override" ); const scBillingEnabledInput = document.getElementById( "qortal-sc-billing-enabled" ); const scBillingManagedByChdInput = document.getElementById( "qortal-sc-billing-managed-by-chd" ); const scAdvancedAdminEnabledInput = document.getElementById( "qortal-sc-advanced-admin-enabled" ); const mspModeEnabledInput = document.getElementById( "qortal-msp-mode-enabled" ); const mspAdminGroupInput = document.getElementById("qortal-msp-admin-group"); const chdAdminDashboardEnabledInput = document.getElementById( "qortal-chd-admin-dashboard-enabled" ); const scBillingProviderInput = document.getElementById( "qortal-sc-billing-provider" ); const scBillingSyncModeInput = document.getElementById( "qortal-sc-billing-sync-mode" ); const scChdBaseUrlInput = document.getElementById("qortal-sc-chd-base-url"); const scChdSyncPathInput = document.getElementById("qortal-sc-chd-sync-path"); const scChdRegisterPathInput = document.getElementById( "qortal-sc-chd-register-path" ); const scChdCheckoutPathInput = document.getElementById( "qortal-sc-chd-checkout-path" ); const scChdCatalogPathInput = document.getElementById( "qortal-sc-chd-catalog-path" ); const scChdCatalogCategoryIdInput = document.getElementById( "qortal-sc-chd-catalog-category-id" ); const scChdCatalogAudienceInput = document.getElementById( "qortal-sc-chd-catalog-audience" ); const scChdRegistrationTokenInput = document.getElementById( "qortal-sc-chd-registration-token" ); const scChdInstanceIdInput = document.getElementById( "qortal-sc-chd-instance-id" ); const scChdInstanceTokenInput = document.getElementById( "qortal-sc-chd-instance-token" ); const scChdInstanceSecretInput = document.getElementById( "qortal-sc-chd-instance-secret" ); const scPackageTierInput = document.getElementById("qortal-sc-package-tier"); const scBillingContactNameInput = document.getElementById( "qortal-sc-billing-contact-name" ); const scBillingContactEmailInput = document.getElementById( "qortal-sc-billing-contact-email" ); const scBillingCompanyNameInput = document.getElementById( "qortal-sc-billing-company-name" ); const scBillingPhoneInput = document.getElementById( "qortal-sc-billing-phone" ); const scBillingAddress1Input = document.getElementById( "qortal-sc-billing-address1" ); const scBillingAddress2Input = document.getElementById( "qortal-sc-billing-address2" ); const scBillingCityInput = document.getElementById("qortal-sc-billing-city"); const scBillingStateInput = document.getElementById( "qortal-sc-billing-state" ); const scBillingPostalCodeInput = document.getElementById( "qortal-sc-billing-postal-code" ); const scBillingCountryInput = document.getElementById( "qortal-sc-billing-country" ); const scAdminQortalAddressInput = document.getElementById( "qortal-sc-admin-q-address" ); const scAccountStatusEl = document.getElementById("qortal-sc-account-status"); const scCreditsBalanceInput = document.getElementById( "qortal-sc-credits-balance" ); const scMonthlyCreditsInput = document.getElementById( "qortal-sc-monthly-credits" ); const scCapacityGbInput = document.getElementById("qortal-sc-capacity-gb"); const scReplicationTargetInput = document.getElementById( "qortal-sc-replication-target" ); const sovereignModeInput = document.getElementById("qortal-sovereign-mode"); 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 qappsIconFromNameInput = document.getElementById( "qortal-qapps-icon-from-name" ); const qappsDescriptionInput = document.getElementById( "qortal-qapps-description" ); const addQappButton = document.getElementById("qortal-add-qapp"); const cancelQappEditButton = document.getElementById( "qortal-cancel-qapp-edit" ); 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 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"); const onboardCloudCard = document.getElementById("qortal-onboard-cloud-card"); const onboardCloudBody = document.getElementById("qortal-onboard-cloud-body"); const onboardToggleButton = document.getElementById("qortal-onboard-toggle"); const purchasesCard = document.getElementById("qortal-purchases-card"); const purchasesBody = document.getElementById("qortal-purchases-body"); const purchasesToggleButton = document.getElementById( "qortal-purchases-toggle" ); const purchasesNoteEl = document.getElementById("qortal-purchases-note"); const subscriptionCards = document.getElementById( "qortal-subscription-cards" ); const oneTimeCards = document.getElementById("qortal-onetime-cards"); if ( !statusUrl || !setupUrl || !setupPlanUrl || !setupOccUrl || !notifyUrl || !searchUsersUrl || !searchGroupsUrl || !userMappingsUrl || !settingsUrl || !billingTestUrl || !billingSyncUrl || !billingBootstrapUrl || !billingCheckoutUrl || !walletsUrl || !createWalletUrl || !registerExternalAuthUrl || !mappingsUrl || !linkMappingUrl || !unlinkMappingUrl || !allowlistUrl || !oidcClientsUrl || !createOidcClientUrl || !updateOidcClientUrlTemplate || !setOidcClientEnabledUrlTemplate || !rotateOidcClientSecretUrlTemplate || !oidcAuditEventsUrl || !addAllowlistUrl || !removeAllowlistUrl || !invitesUrl || !createInviteUrl || !revokeInviteUrl || !adminRecoveryStatusUrl || !adminRecoveryIssueCodeUrl || !adminRecoveryStartEmergencyUrl ) { return; } if ( !feedbackEl || !statusEl || !saveButton || !testPackagingButton || !syncPackagingButton || !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 || !idpTemplateInput || !idpClientIdInput || !idpDisplayNameInput || !idpRedirectUrisInput || !idpPostLogoutRedirectUrisInput || !idpAllowedScopesInput || !idpAllowedGroupsInput || !idpRequirePkceInput || !idpApplyTemplateButton || !idpSaveClientButton || !idpCancelEditButton || !idpRefreshClientsButton || !idpRefreshAuditButton || !idpCopySecretButton || !recoveryStatusCheckButton || !recoveryIssueCodeButton || !recoveryStartEmergencyButton || !recoveryTargetUserIdInput || !recoveryCodeExpiryMinutesInput || !recoveryEmergencyHoursInput || !recoveryAdminResult || !allowlistBody || !inviteCreateResult || !invitesBody || !idpSecretOutput || !idpConfigOutput || !idpClientsBody || !idpAuditBody || !qappsEnabledInput || !qappsBrowserEnabledInput || !qappsDebugEnabledInput || !qappsAdvancedUnlockSettingsEnabledInput || !userPublishingEnabledInput || !scBillingEnabledInput || !scBillingManagedByChdInput || !scAdvancedAdminEnabledInput || !mspModeEnabledInput || !mspAdminGroupInput || !chdAdminDashboardEnabledInput || !scBillingProviderInput || !scBillingSyncModeInput || !scChdBaseUrlInput || !scChdSyncPathInput || !scChdRegisterPathInput || !scChdCheckoutPathInput || !scChdCatalogPathInput || !scChdCatalogCategoryIdInput || !scChdCatalogAudienceInput || !scChdRegistrationTokenInput || !scChdInstanceIdInput || !scChdInstanceTokenInput || !scChdInstanceSecretInput || !scPackageTierInput || !scBillingContactNameInput || !scBillingContactEmailInput || !scBillingCompanyNameInput || !scBillingPhoneInput || !scBillingAddress1Input || !scBillingAddress2Input || !scBillingCityInput || !scBillingStateInput || !scBillingPostalCodeInput || !scBillingCountryInput || !scAdminQortalAddressInput || !scAccountStatusEl || !scCreditsBalanceInput || !scMonthlyCreditsInput || !scCapacityGbInput || !scReplicationTargetInput || !sovereignModeInput || !qappsNameInput || !qappsAddressInput || !qappsIconModeInput || !qappsIconUrlInput || !qappsDescriptionInput || !addQappButton || !clearQappsButton || !qappsBody || !oidcIssuerInput || !oidcClientIdInput || !oidcClientSecretInput || !nextcloudPublicUrlInput || !setupResult || !inviteMessageButton || !copyInviteMessageButton || !inviteMessageBox || !notifyUserIds || !notifyGroupIds || !notifyEmail || !notifyInApp || !notifyResult || !notifyEmailSubject || !notifyEmailBody || !userSearchQuery || !userSearchResults || !groupSearchQuery || !groupSearchResults || !previewEmailButton || !previewSubject || !previewBody ) { return; } const headers = { requesttoken: OC.requestToken, }; root.setAttribute("data-mode-active", sovereignModeValue); root.setAttribute( "data-billing-advanced", root.dataset.billingAdvanced === "1" ? "1" : "0" ); let lastUserResults = []; let lastSetupPlan = null; let autoProvisionEnabled = false; let autoProvisionGuardEnabled = false; let hasUnsavedChanges = false; let editingQappAddressKey = ""; let editingOidcClientId = ""; let latestOidcClientSecret = ""; let baselineSnapshot = null; let purchasesManualToggle = false; let purchasesCatalogSource = "defaults"; const defaultSubscriptionPackages = [ { tier: "free_core", code: "free_core", name: "Free Core", price: "$0 / month", summary: "Base NuQloud integration connectivity.", features: [ "0 monthly credits", "0 GB encrypted capacity", "0 replication nodes", ], }, { tier: "nuqloud_starter", code: "nuqloud_starter", name: "Starter", price: "", summary: "Single-user or lighter private-cloud plan.", features: [ "500 initial publishing credits", "10 GB on-instance storage", "10 GB decentralized publish storage", ], }, { tier: "nuqloud_advanced", code: "nuqloud_advanced", name: "Advanced", price: "", summary: "Expanded private-cloud plan for heavier single-user or hosted-account usage.", features: [ "2,000 monthly credits", "25 GB on-instance storage", "25 GB decentralized publish storage", ], }, { tier: "nuqloud_pro", code: "nuqloud_pro", name: "Pro", price: "", summary: "Professional-use private-cloud plan.", features: [ "5,000 monthly credits", "100 GB on-instance storage", "100 GB decentralized publish storage", ], }, { tier: "nuqloud_team_starter", code: "nuqloud_team_starter", name: "Team Starter", price: "", summary: "Hosted multi-user team package at the Starter level.", features: ["Everything included in Starter", "For up to 5 users"], }, { tier: "nuqloud_team_advanced", code: "nuqloud_team_advanced", name: "Team Advanced", price: "", summary: "Hosted multi-user team package at the Advanced level.", features: ["Everything included in Advanced", "For up to 5 users"], }, { tier: "nuqloud_team_pro", code: "nuqloud_team_pro", name: "Team Pro", price: "", summary: "Hosted multi-user team package at the Pro level.", features: ["Everything included in Pro", "For up to 5 users"], }, { tier: "nuqloud_branded", code: "nuqloud_branded", name: "NuQloud Branded", price: "", summary: "Managed branded instance for an organization or community.", features: [ "10,000 monthly credits", "150 GB on-instance storage", "150 GB decentralized publish storage", ], }, { tier: "nuqloud_branded_pro", code: "nuqloud_branded_pro", name: "Branded Pro", price: "", summary: "Higher-capacity branded instance for a growing organization or community.", features: [ "25,000 monthly credits", "500 GB on-instance storage", "500 GB decentralized publish storage", ], }, { tier: "nuqloud_branded_enterprise", code: "nuqloud_branded_enterprise", name: "Branded Enterprise", price: "", summary: "Custom branded deployment for larger or more complex environments.", features: [ "100,000 monthly credits", "1 TB on-instance storage per instance", "1 TB decentralized publish storage per instance", ], }, ]; const oidcClientTemplates = { gitea: { clientId: "gitea", displayName: "Gitea", redirectUris: ["https://git.example.com/user/oauth2/NuQloud/callback"], scopes: ["openid", "profile", "email"], requirePkce: false, guide: [ "Gitea setup", "1. Add an OAuth2/OpenID Connect authentication source in Gitea.", "2. Use Authentication Name: NuQloud. This value is part of the callback URL.", "3. Callback URL format: https://git.example.com/user/oauth2/NuQloud/callback.", "4. Use the issuer/discovery URL, client ID, and one-time client secret shown here.", ], }, grafana: { clientId: "grafana", displayName: "Grafana", redirectUris: ["https://grafana.example.com/login/generic_oauth"], scopes: ["openid", "profile", "email", "groups"], requirePkce: false, guide: [ "Grafana setup", "1. Configure [auth.generic_oauth] in grafana.ini or equivalent env vars.", "2. Callback URL format: https://grafana.example.com/login/generic_oauth.", "3. Set auth_url, token_url, and api_url from the issuer endpoints.", "4. Use scopes: openid profile email groups if group claims are needed.", ], }, wikijs: { clientId: "wikijs", displayName: "Wiki.js", redirectUris: ["https://wiki.example.com/login/"], scopes: ["openid", "profile", "email", "groups"], requirePkce: false, guide: [ "Wiki.js setup", "1. Add Generic OpenID Connect / OAuth2 in Wiki.js Authentication settings.", "2. Copy Wiki.js's displayed Callback URL / Redirect URI into this client.", "3. Use claim mappings: email=email, display name=name, groups=groups.", "4. Enable self-registration only if your Wiki.js group policy is ready.", ], }, openproject: { clientId: "openproject", displayName: "OpenProject", redirectUris: ["https://projects.example.com/auth//callback"], scopes: ["openid", "profile", "email"], requirePkce: false, guide: [ "OpenProject setup", "1. Add an OpenID Connect provider in OpenProject authentication settings.", "2. Copy the exact Redirect URI shown by OpenProject into this client.", "3. Provider callback paths vary by provider slug, so do not keep the placeholder.", "4. Use issuer/discovery, client ID, and client secret from this panel.", ], }, outline: { clientId: "outline", displayName: "Outline", redirectUris: ["https://docs.example.com/auth/oidc.callback"], scopes: ["openid", "profile", "email"], requirePkce: false, guide: [ "Outline setup", "1. Configure Outline OIDC using issuer/discovery, client ID, and client secret.", "2. Common callback URL format: https://docs.example.com/auth/oidc.callback.", "3. Use email and profile scopes unless your deployment needs group data.", "4. Restart Outline after changing OIDC environment variables.", ], }, generic: { clientId: "private-app", displayName: "Private App", redirectUris: ["https://app.example.com/oidc/callback"], scopes: ["openid", "profile", "email"], requirePkce: true, guide: [ "Generic OIDC app setup", "1. Register the app's exact HTTPS redirect URI here.", "2. Use discovery whenever the app supports it.", "3. Enable PKCE for public clients, desktop apps, and browser-only apps.", "4. Add allowed groups only when access should be limited server-side.", ], }, }; const defaultOneTimePurchases = [ { code: "credits-1000", name: "1000 Decentralized Publishing Credits", price: "", credits: 1000, category: "addon", purchaseInterval: "one_time", actionLabel: "Buy add-on", summary: "Publish credits for decentralized encrypted storage and communications powered by QDN.", }, { code: "credits-5000", name: "5000 Decentralized Publishing Credits", price: "", credits: 5000, category: "addon", purchaseInterval: "one_time", actionLabel: "Buy add-on", summary: "Publish credits for decentralized encrypted storage and communications.", }, { code: "capacity-25", name: "25GB Additional Storage", price: "", category: "addon", purchaseInterval: "monthly", actionLabel: "Add monthly add-on", summary: "Adds 25 GB on-instance storage and 25 GB decentralized publish storage.", }, { code: "capacity-100", name: "100GB Additional Storage", price: "", category: "addon", purchaseInterval: "monthly", actionLabel: "Add monthly add-on", summary: "Adds 100 GB on-instance storage and 100 GB decentralized publish storage.", }, ]; const clonePurchaseCatalogEntries = function (entries) { return entries.map(function (entry) { return Object.assign({}, entry); }); }; const defaultSubscriptionsByCode = {}; const defaultSubscriptionsByTier = {}; defaultSubscriptionPackages.forEach(function (entry) { const codeKey = String(entry.code || "") .trim() .toLowerCase(); const tierKey = String(entry.tier || "") .trim() .toLowerCase(); if (codeKey) { defaultSubscriptionsByCode[codeKey] = entry; } if (tierKey) { defaultSubscriptionsByTier[tierKey] = entry; } }); const defaultAddonsByCode = {}; defaultOneTimePurchases.forEach(function (entry) { const codeKey = String(entry.code || "") .trim() .toLowerCase(); if (codeKey) { defaultAddonsByCode[codeKey] = entry; } if (entry.codeByTier && typeof entry.codeByTier === "object") { Object.keys(entry.codeByTier).forEach(function (tierKey) { const tierCode = String(entry.codeByTier[tierKey] || "") .trim() .toLowerCase(); if (tierCode) { defaultAddonsByCode[tierCode] = entry; } }); } }); const defaultPurchasesNoteText = purchasesNoteEl ? String(purchasesNoteEl.textContent || "").trim() : "Choose subscription packages and add-ons. Your billing profile above is reused automatically."; let subscriptionPackages = clonePurchaseCatalogEntries( defaultSubscriptionPackages ); let oneTimePurchases = clonePurchaseCatalogEntries(defaultOneTimePurchases); let qappsNonAdminRestrictionRules = []; function sanitizeNonAdminRestrictionRule(entry) { if (!entry || typeof entry !== "object") { return null; } const methodRaw = String(entry.method || "*") .trim() .toUpperCase(); const method = [ "*", "GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD", ].includes(methodRaw) ? methodRaw : "*"; let path = String(entry.path || "").trim(); if (!path) { return null; } if (!path.startsWith("/")) { path = `/${path.replace(/^\/+/, "")}`; } const matchRaw = String(entry.match || "prefix") .trim() .toLowerCase(); const match = ["prefix", "exact", "regex"].includes(matchRaw) ? matchRaw : "prefix"; const actionRaw = String(entry.action || "deny") .trim() .toLowerCase(); const action = actionRaw === "not_found" ? "not_found" : "deny"; const enabled = !Object.prototype.hasOwnProperty.call(entry, "enabled") || Boolean(entry.enabled); return { enabled, method, path, match, action, }; } function renderQappsNonAdminRestrictionRules() { if (!qappsNonAdminRulesContainer) { return; } qappsNonAdminRulesContainer.innerHTML = ""; if ( !Array.isArray(qappsNonAdminRestrictionRules) || !qappsNonAdminRestrictionRules.length ) { const empty = document.createElement("p"); empty.className = "qortal-note"; empty.textContent = "No custom rules configured."; qappsNonAdminRulesContainer.appendChild(empty); return; } qappsNonAdminRestrictionRules.forEach(function (rule, index) { const row = document.createElement("div"); row.className = "qortal-chip"; row.style.display = "grid"; row.style.gridTemplateColumns = "auto 90px minmax(220px,1fr) 90px 110px auto"; row.style.gap = "8px"; row.style.alignItems = "center"; row.style.marginBottom = "8px"; const enabled = document.createElement("input"); enabled.type = "checkbox"; enabled.checked = rule.enabled !== false; enabled.title = "Enable rule"; enabled.addEventListener("change", function () { qappsNonAdminRestrictionRules[index].enabled = enabled.checked; markDirty(); }); row.appendChild(enabled); const method = document.createElement("select"); ["*", "GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"].forEach( function (value) { const option = document.createElement("option"); option.value = value; option.textContent = value; if (value === String(rule.method || "*").toUpperCase()) { option.selected = true; } method.appendChild(option); } ); method.addEventListener("change", function () { qappsNonAdminRestrictionRules[index].method = method.value; markDirty(); }); row.appendChild(method); const path = document.createElement("input"); path.type = "text"; path.value = String(rule.path || ""); path.placeholder = "/admin/apikey/generate"; path.addEventListener("change", function () { qappsNonAdminRestrictionRules[index].path = path.value.trim(); markDirty(); }); row.appendChild(path); const match = document.createElement("select"); ["prefix", "exact", "regex"].forEach(function (value) { const option = document.createElement("option"); option.value = value; option.textContent = value; if (value === String(rule.match || "prefix").toLowerCase()) { option.selected = true; } match.appendChild(option); }); match.addEventListener("change", function () { qappsNonAdminRestrictionRules[index].match = match.value; markDirty(); }); row.appendChild(match); const action = document.createElement("select"); ["deny", "not_found"].forEach(function (value) { const option = document.createElement("option"); option.value = value; option.textContent = value === "deny" ? "deny(403)" : "not_found(404)"; if (value === String(rule.action || "deny").toLowerCase()) { option.selected = true; } action.appendChild(option); }); action.addEventListener("change", function () { qappsNonAdminRestrictionRules[index].action = action.value; markDirty(); }); row.appendChild(action); const remove = document.createElement("button"); remove.type = "button"; remove.className = "button"; remove.textContent = "Remove"; remove.addEventListener("click", function () { qappsNonAdminRestrictionRules.splice(index, 1); renderQappsNonAdminRestrictionRules(); markDirty(); }); row.appendChild(remove); qappsNonAdminRulesContainer.appendChild(row); }); } function normalizeQappIconMode(value) { const normalized = String(value || "") .trim() .toLowerCase(); if (normalized === "custom" || normalized === "name") { return normalized; } return "auto"; } 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.trim() : "", address: String(entry.address || "").trim(), description: typeof entry.description === "string" ? entry.description.trim() : "", iconMode: normalizeQappIconMode(entry.iconMode), iconUrl: typeof entry.iconUrl === "string" ? entry.iconUrl.trim() : "", iconFromName: typeof entry.iconFromName === "string" ? entry.iconFromName.trim() : typeof entry.iconSourceName === "string" ? entry.iconSourceName.trim() : "", }; }); } } catch (error) { qapps = []; } try { const parsed = JSON.parse(qappsNonAdminRestrictionRulesJson); if (Array.isArray(parsed)) { qappsNonAdminRestrictionRules = parsed .map(sanitizeNonAdminRestrictionRule) .filter(Boolean); } } catch (_error) { qappsNonAdminRestrictionRules = []; } qappsEnabledInput.checked = qappsEnabledValue; qappsBrowserEnabledInput.checked = qappsBrowserEnabledValue; qappsDebugEnabledInput.checked = qappsDebugEnabledValue; qappsAdvancedUnlockSettingsEnabledInput.checked = qappsAdvancedUnlockSettingsEnabledValue; if (qappsNonAdminRestrictionsEnabledInput) { qappsNonAdminRestrictionsEnabledInput.checked = qappsNonAdminRestrictionsEnabledValue; } renderQappsNonAdminRestrictionRules(); userPublishingEnabledInput.checked = userPublishingEnabledValue; sovereignModeInput.value = sovereignModeValue; scAdvancedAdminEnabledInput.checked = root.dataset.billingAdvanced === "1"; qappsIconModeInput.value = "auto"; qappsIconUrlInput.value = ""; if (qappsIconFromNameInput) { qappsIconFromNameInput.value = ""; } updateIconInputState(); setQappFormMode(); updateBillingAdvancedState(); try { initializeCollapsibleCards(); } catch (_error) { // Do not block the rest of admin UI initialization if collapsible setup fails. } setOnboardingVisibility(userPublishingEnabledInput.checked); setBillingAccountStatus(scAdminQortalAddressInput.value); updatePurchasesNote(); renderPurchases(); refreshBillingCatalog("init"); setPurchasesVisibility(!isExistingClientInstance()); oidcEnvOutput.textContent = buildEnvSnippet(); externalAuthEnvOutput.textContent = buildExternalAuthEnvSnippet(); qortalNodeEnvOutput.textContent = buildQortalNodeEnvSnippet(); baselineSnapshot = JSON.stringify(collectSettings()); function markDirty() { hasUnsavedChanges = true; } function normalizeQappAddressKey(address) { return String(address || "") .trim() .toLowerCase(); } function setQappFormMode() { const isEditing = editingQappAddressKey !== ""; if (addQappButton) { addQappButton.textContent = isEditing ? "Update App" : "Add App"; } if (cancelQappEditButton) { cancelQappEditButton.classList.toggle("qortal-hidden", !isEditing); } } function clearQappForm() { qappsNameInput.value = ""; qappsAddressInput.value = ""; qappsIconModeInput.value = "auto"; qappsIconUrlInput.value = ""; if (qappsIconFromNameInput) { qappsIconFromNameInput.value = ""; } qappsDescriptionInput.value = ""; updateIconInputState(); } function enterQappEditMode(entry) { if (!entry || !entry.address) { return; } editingQappAddressKey = normalizeQappAddressKey(entry.address); qappsNameInput.value = entry.name || ""; qappsAddressInput.value = entry.address || ""; qappsIconModeInput.value = normalizeQappIconMode(entry.iconMode); qappsIconUrlInput.value = entry.iconUrl || ""; if (qappsIconFromNameInput) { qappsIconFromNameInput.value = entry.iconFromName || ""; } qappsDescriptionInput.value = entry.description || ""; updateIconInputState(); setQappFormMode(); } function exitQappEditMode() { editingQappAddressKey = ""; clearQappForm(); setQappFormMode(); } function updateIconInputState() { const mode = normalizeQappIconMode(qappsIconModeInput.value); const isCustom = mode === "custom"; const isName = mode === "name"; qappsIconModeInput.value = mode; qappsIconUrlInput.disabled = !isCustom; if (!isCustom) { qappsIconUrlInput.value = ""; } if (qappsIconFromNameInput) { qappsIconFromNameInput.disabled = !isName; if (!isName) { qappsIconFromNameInput.value = ""; } } } function updateBillingAdvancedState() { root.setAttribute( "data-billing-advanced", scAdvancedAdminEnabledInput.checked ? "1" : "0" ); } function setCollapsibleCardExpanded(card, body, toggleButton, expanded) { const isExpanded = Boolean(expanded); card.setAttribute("data-expanded", isExpanded ? "1" : "0"); body.hidden = !isExpanded; toggleButton.setAttribute("aria-expanded", isExpanded ? "true" : "false"); toggleButton.textContent = isExpanded ? "Hide" : "Show"; } function initializeCollapsibleCards() { function findDirectChildByClass(parent, className) { for (let i = 0; i < parent.children.length; i += 1) { const child = parent.children[i]; if (child.classList && child.classList.contains(className)) { return child; } } return null; } function findDirectHeading(parent) { for (let i = 0; i < parent.children.length; i += 1) { const child = parent.children[i]; if (child.tagName && child.tagName.toLowerCase() === "h3") { return child; } } return null; } const cards = root.querySelectorAll(".qortal-card[data-collapsible='1']"); cards.forEach(function (card) { if (card.getAttribute("data-collapsible-bound") === "1") { return; } card.setAttribute("data-collapsible-bound", "1"); let header = findDirectChildByClass(card, "qortal-card-header"); if (!header) { const heading = findDirectHeading(card); if (!heading) { return; } header = document.createElement("div"); header.className = "qortal-card-header"; card.insertBefore(header, heading); header.appendChild(heading); } let body = findDirectChildByClass(card, "qortal-card-body"); if (!body) { body = document.createElement("div"); body.className = "qortal-card-body"; Array.from(card.children).forEach(function (child) { if (child === header) { return; } body.appendChild(child); }); card.appendChild(body); } let toggleButton = null; for (let i = 0; i < header.children.length; i += 1) { const child = header.children[i]; if (child.classList && child.classList.contains("qortal-card-toggle")) { toggleButton = child; break; } } if (!toggleButton) { toggleButton = document.createElement("button"); toggleButton.type = "button"; toggleButton.className = "button qortal-card-toggle"; header.appendChild(toggleButton); } const defaultMode = String( card.getAttribute("data-collapsible-default") || "closed" ) .trim() .toLowerCase(); setCollapsibleCardExpanded( card, body, toggleButton, defaultMode === "open" ); toggleButton.addEventListener("click", function () { setCollapsibleCardExpanded(card, body, toggleButton, body.hidden); }); }); } function setOnboardingVisibility(visible) { if (!onboardCloudBody || !onboardToggleButton || !onboardCloudCard) { return; } const isVisible = Boolean(visible); onboardCloudBody.hidden = !isVisible; onboardCloudCard.setAttribute("data-expanded", isVisible ? "1" : "0"); onboardToggleButton.setAttribute( "aria-expanded", isVisible ? "true" : "false" ); onboardToggleButton.textContent = isVisible ? "Hide" : "Show"; } function normalizePositiveInt(value, fallbackValue) { const parsed = Number.parseInt(String(value || "").trim(), 10); if (!Number.isFinite(parsed) || parsed < 0) { return fallbackValue; } return parsed; } function updatePurchasesNote() { if (!purchasesNoteEl) { return; } let noteText = defaultPurchasesNoteText; const debugEnabled = qappsDebugEnabledInput ? qappsDebugEnabledInput.checked : qappsDebugEnabledValue; if (debugEnabled) { noteText += ` [Catalog source: ${purchasesCatalogSource}]`; } purchasesNoteEl.textContent = noteText; } function setPurchasesCatalogSource(source) { const normalized = String(source || "") .trim() .toLowerCase(); purchasesCatalogSource = normalized === "live" ? "live" : "defaults"; updatePurchasesNote(); } function getTierRank(tier) { switch (String(tier || "").trim()) { case "replication_basic": return 1; case "replication_standard": return 2; case "replication_enterprise": return 3; case "nuqloud_starter": return 1; case "nuqloud_advanced": return 2; case "nuqloud_pro": return 3; case "nuqloud_team_starter": return 4; case "nuqloud_team_advanced": return 5; case "nuqloud_team_pro": return 6; case "nuqloud_branded": return 7; case "nuqloud_branded_pro": return 8; case "nuqloud_branded_enterprise": return 9; case "free_core": default: return 0; } } function normalizeTierKey(tier) { const normalized = String(tier || "") .trim() .toLowerCase(); switch (normalized) { case "replication_basic": return "nuqloud_starter"; case "replication_standard": return "nuqloud_advanced"; case "replication_enterprise": return "nuqloud_pro"; case "nuqloud_team": return "nuqloud_team_starter"; case "free_core": case "nuqloud_starter": case "nuqloud_advanced": case "nuqloud_pro": case "nuqloud_team_starter": case "nuqloud_team_advanced": case "nuqloud_team_pro": case "nuqloud_branded": case "nuqloud_branded_pro": case "nuqloud_branded_enterprise": return normalized; default: return ""; } } function deriveTierFromEntry(code, tier, name) { const normalizedTier = normalizeTierKey(tier); if (normalizedTier !== "") { return normalizedTier; } const codeKey = String(code || "") .trim() .toLowerCase(); if (normalizeTierKey(codeKey) !== "") { return codeKey; } const nameKey = String(name || "") .trim() .toLowerCase(); if (nameKey.indexOf("free core") !== -1) { return "free_core"; } if (nameKey.indexOf("replication basic") !== -1) { return "nuqloud_starter"; } if (nameKey.indexOf("replication standard") !== -1) { return "nuqloud_advanced"; } if (nameKey.indexOf("replication enterprise") !== -1) { return "nuqloud_pro"; } if (nameKey.indexOf("nuqloud starter") !== -1) { return "nuqloud_starter"; } if (nameKey.indexOf("nuqloud advanced") !== -1) { return "nuqloud_advanced"; } if (nameKey.indexOf("nuqloud pro") !== -1) { return "nuqloud_pro"; } if (nameKey.indexOf("team starter") !== -1) { return "nuqloud_team_starter"; } if (nameKey.indexOf("team advanced") !== -1) { return "nuqloud_team_advanced"; } if (nameKey.indexOf("team pro") !== -1) { return "nuqloud_team_pro"; } if (nameKey.indexOf("branded enterprise") !== -1) { return "nuqloud_branded_enterprise"; } if (nameKey.indexOf("branded pro") !== -1) { return "nuqloud_branded_pro"; } if (nameKey.indexOf("branded") !== -1) { return "nuqloud_branded"; } return ""; } function getDefaultSubscriptionEntry(entry) { const codeKey = String(entry && entry.code ? entry.code : "") .trim() .toLowerCase(); if (codeKey && defaultSubscriptionsByCode[codeKey]) { return defaultSubscriptionsByCode[codeKey]; } const tierKey = normalizeTierKey(entry && entry.tier ? entry.tier : ""); if (tierKey && defaultSubscriptionsByTier[tierKey]) { return defaultSubscriptionsByTier[tierKey]; } return null; } function getDefaultAddonEntry(entry) { const codeKey = String(entry && entry.code ? entry.code : "") .trim() .toLowerCase(); if (codeKey && defaultAddonsByCode[codeKey]) { return defaultAddonsByCode[codeKey]; } return null; } function enrichSubscriptionCatalogEntry(entry) { const normalized = Object.assign({}, entry); normalized.tier = deriveTierFromEntry( normalized.code, normalized.tier, normalized.name ); const defaults = getDefaultSubscriptionEntry(normalized); if (defaults) { if (!normalized.name) { normalized.name = defaults.name; } if (!normalized.price) { normalized.price = defaults.price; } if (!normalized.summary) { normalized.summary = defaults.summary; } if ( !Array.isArray(normalized.features) || normalized.features.length === 0 ) { normalized.features = Array.isArray(defaults.features) ? defaults.features.slice() : []; } } return normalized; } function enrichAddonCatalogEntry(entry) { const normalized = Object.assign({}, entry); const defaults = getDefaultAddonEntry(normalized); if (defaults) { if (!normalized.name) { normalized.name = defaults.name; } if (!normalized.summary) { normalized.summary = defaults.summary; } if (!normalized.actionLabel) { normalized.actionLabel = defaults.actionLabel || "Buy add-on"; } if (!normalized.price) { normalized.price = defaults.price; } if ( (!normalized.codeByTier || typeof normalized.codeByTier !== "object") && defaults.codeByTier ) { normalized.codeByTier = Object.assign({}, defaults.codeByTier); } if ( (!normalized.priceByTier || typeof normalized.priceByTier !== "object") && defaults.priceByTier ) { normalized.priceByTier = Object.assign({}, defaults.priceByTier); } if ( !Number.isFinite(normalized.credits) && Number.isFinite(defaults.credits) ) { normalized.credits = defaults.credits; } if (!normalized.purchaseInterval && defaults.purchaseInterval) { normalized.purchaseInterval = defaults.purchaseInterval; } } return normalized; } function compareSubscriptionEntries(a, b) { const rankDelta = getTierRank(a.tier) - getTierRank(b.tier); if (rankDelta !== 0) { return rankDelta; } return String(a.name || "").localeCompare(String(b.name || ""), undefined, { sensitivity: "base", }); } function compareAddonEntries(a, b) { const monthlyA = isMonthlyAddonItem(a) ? 1 : 0; const monthlyB = isMonthlyAddonItem(b) ? 1 : 0; if (monthlyA !== monthlyB) { return monthlyA - monthlyB; } return String(a.name || "").localeCompare(String(b.name || ""), undefined, { sensitivity: "base", }); } function getAddonTierKeyFromCode(code) { const normalized = String(code || "") .trim() .toLowerCase(); if (!normalized) { return ""; } const isCapacityAddon = normalized.includes("capacity_") || normalized.includes("capacity-"); const hasMonthlyMarker = normalized.includes("_monthly") || normalized.includes("-monthly"); if ( !isCapacityAddon || (!hasMonthlyMarker && !normalized.startsWith("capacity-")) ) { return ""; } if (/_enterprise$/.test(normalized)) { return "nuqloud_pro"; } if (/_standard$/.test(normalized)) { return "nuqloud_advanced"; } if (/_basic$/.test(normalized)) { return "nuqloud_starter"; } return ""; } function isMonthlyAddonItem(item) { if (!item || typeof item !== "object") { return false; } const interval = String(item.purchaseInterval || "") .trim() .toLowerCase(); const type = String(item.purchaseType || "") .trim() .toLowerCase(); const code = String(item.code || "") .trim() .toLowerCase(); return ( interval === "monthly" || type === "monthly_addon" || code.indexOf("_monthly") !== -1 || code.indexOf("-monthly") !== -1 || code.indexOf("capacity_") === 0 || code.indexOf("capacity-") === 0 || code.indexOf("storage-") === 0 || code.indexOf("storage") === 0 || code.indexOf("capacity") !== -1 ); } function monthlyAddonMatchesActiveTier(item, activeTier) { if (!isMonthlyAddonItem(item)) { return true; } if (item.codeByTier && typeof item.codeByTier === "object") { return true; } const targetTier = getAddonTierKeyFromCode(item.code); if (!targetTier) { return true; } const normalizedActiveTier = normalizeTierKey(activeTier || "free_core") || "free_core"; if (targetTier === "nuqloud_starter") { return ( normalizedActiveTier === "free_core" || normalizedActiveTier === "nuqloud_starter" ); } return normalizedActiveTier === targetTier; } function currentTier() { const value = String(scPackageTierInput.value || "").trim(); return value || "free_core"; } function isExistingClientInstance() { if (String(scChdInstanceIdInput.value || "").trim() !== "") { return true; } if (currentTier() !== "free_core") { return true; } return normalizePositiveInt(scMonthlyCreditsInput.value, 0) > 0; } function setPurchasesVisibility(show) { if (!purchasesBody || !purchasesToggleButton || !purchasesCard) { return; } const visible = Boolean(show); purchasesBody.hidden = !visible; purchasesCard.setAttribute("data-expanded", visible ? "1" : "0"); purchasesToggleButton.textContent = visible ? "Hide" : "Show"; purchasesToggleButton.setAttribute( "aria-expanded", visible ? "true" : "false" ); } function refreshPurchasesVisibilityDefault() { if (!purchasesBody || purchasesManualToggle) { return; } setPurchasesVisibility(!isExistingClientInstance()); } function renderSubscriptionCards() { if (!subscriptionCards) { return; } const activeTier = currentTier(); const activeRank = getTierRank(activeTier); subscriptionCards.innerHTML = ""; subscriptionPackages.forEach(function (pkg) { const card = document.createElement("article"); card.className = "qortal-purchase-card qortal-purchase-card--subscription"; if (pkg.tier === activeTier) { card.classList.add("qortal-purchase-current"); } const tierPill = document.createElement("span"); tierPill.className = "qortal-purchase-pill"; tierPill.textContent = pkg.tier === activeTier ? "Current Plan" : "Subscription"; card.appendChild(tierPill); const heading = document.createElement("h5"); heading.textContent = pkg.name; card.appendChild(heading); const price = document.createElement("p"); price.className = "qortal-purchase-price"; price.textContent = pkg.price; card.appendChild(price); const summary = document.createElement("p"); summary.className = "qortal-purchase-meta"; summary.textContent = pkg.summary; card.appendChild(summary); const featureList = document.createElement("ul"); featureList.className = "qortal-purchase-features"; pkg.features.forEach(function (feature) { const item = document.createElement("li"); item.textContent = feature; featureList.appendChild(item); }); card.appendChild(featureList); const actions = document.createElement("div"); actions.className = "qortal-purchase-actions"; const checkoutButton = document.createElement("button"); checkoutButton.className = "button"; if (pkg.tier === activeTier) { checkoutButton.textContent = "Current plan"; checkoutButton.disabled = true; } else if (getTierRank(pkg.tier) > activeRank) { checkoutButton.textContent = "Upgrade"; } else { checkoutButton.textContent = "Switch plan"; } checkoutButton.addEventListener("click", async function () { try { scPackageTierInput.value = pkg.tier; renderPurchases(); markDirty(); await openBillingCheckout({ packageTier: pkg.tier, purchaseMeta: { checkoutType: "package", purchaseType: "subscription", purchaseCategory: "package", purchaseCode: pkg.code, productId: pkg.productId, purchaseProductId: pkg.productId, purchaseLabel: pkg.name, purchaseInterval: "monthly", }, }); } catch (error) { setFeedback(error.message || "Failed to open package checkout", true); } }); actions.appendChild(checkoutButton); card.appendChild(actions); subscriptionCards.appendChild(card); }); } function renderOneTimeCards() { if (!oneTimeCards) { return; } oneTimeCards.innerHTML = ""; const activeTier = currentTier(); function resolveTierValue(baseValue, byTierMap) { if (!byTierMap || typeof byTierMap !== "object") { return baseValue; } const tierValue = byTierMap[activeTier]; if (typeof tierValue === "string" && tierValue.trim() !== "") { return tierValue.trim(); } if ( typeof byTierMap.free_core === "string" && byTierMap.free_core.trim() !== "" ) { return byTierMap.free_core.trim(); } return baseValue; } const seenOneTimeKeys = new Set(); oneTimePurchases .filter(function (item) { return monthlyAddonMatchesActiveTier(item, activeTier); }) .forEach(function (item) { const resolvedCode = resolveTierValue( String(item.code || "").trim(), item.codeByTier ); const resolvedPrice = resolveTierValue( String(item.price || "").trim(), item.priceByTier ); const dedupeKey = [ String(resolvedCode || "") .trim() .toLowerCase(), String(item.purchaseInterval || "") .trim() .toLowerCase(), String(item.category || "") .trim() .toLowerCase(), String(item.name || "") .trim() .toLowerCase(), ].join("|"); if (seenOneTimeKeys.has(dedupeKey)) { return; } seenOneTimeKeys.add(dedupeKey); const card = document.createElement("article"); card.className = "qortal-purchase-card qortal-purchase-card--onetime"; const tierPill = document.createElement("span"); tierPill.className = "qortal-purchase-pill"; tierPill.textContent = isMonthlyAddonItem(item) ? "Monthly add-on" : "One-time purchase"; card.appendChild(tierPill); const heading = document.createElement("h5"); heading.textContent = item.name; card.appendChild(heading); const price = document.createElement("p"); price.className = "qortal-purchase-price"; price.textContent = resolvedPrice; card.appendChild(price); const summary = document.createElement("p"); summary.className = "qortal-purchase-meta"; summary.textContent = item.summary; card.appendChild(summary); const actions = document.createElement("div"); actions.className = "qortal-purchase-actions"; const checkoutButton = document.createElement("button"); checkoutButton.className = "button"; checkoutButton.textContent = item.actionLabel || "Buy add-on"; checkoutButton.addEventListener("click", async function () { try { const purchaseCategory = String(item.category || "one_time").trim(); const quantity = String(item.quantity || "1"); const purchaseType = isMonthlyAddonItem(item) ? "monthly_addon" : "one_time"; const purchaseMeta = { checkoutType: "one_time", purchaseType: purchaseType, purchaseCategory: purchaseCategory, purchaseCode: resolvedCode, productId: item.productId, purchaseProductId: item.productId, oneTimeCode: resolvedCode, oneTimeQuantity: quantity, purchaseLabel: item.name, quantity: quantity, }; if (item.purchaseInterval) { purchaseMeta.purchaseInterval = String(item.purchaseInterval); } if (Number.isFinite(item.credits)) { purchaseMeta.purchaseCredits = String(item.credits); } await openBillingCheckout({ packageTier: currentTier(), purchaseMeta: purchaseMeta, }); } catch (error) { setFeedback( error.message || "Failed to open add-on checkout", true ); } }); actions.appendChild(checkoutButton); card.appendChild(actions); oneTimeCards.appendChild(card); }); } function renderPurchases() { renderSubscriptionCards(); renderOneTimeCards(); } function normalizeCatalogList(raw) { if (!Array.isArray(raw)) { return []; } return raw .filter(function (entry) { return entry && typeof entry === "object"; }) .map(function (entry) { function firstNonEmptyValue(keys) { for (let i = 0; i < keys.length; i += 1) { const key = keys[i]; const value = entry[key]; if (value === undefined || value === null) { continue; } const normalizedValue = String(value).trim(); if (normalizedValue !== "") { return normalizedValue; } } return ""; } const normalized = Object.assign({}, entry); normalized.code = firstNonEmptyValue([ "code", "purchaseCode", "slug", "sku", "id", ]); normalized.productId = firstNonEmptyValue([ "productId", "product_id", "id", ]); normalized.name = firstNonEmptyValue(["name", "title", "label"]); normalized.price = firstNonEmptyValue([ "price", "priceLabel", "displayPrice", "amount", ]); normalized.summary = firstNonEmptyValue([ "summary", "description", "details", ]); normalized.tier = deriveTierFromEntry( entry.code, entry.tier, normalized.name ); normalized.category = firstNonEmptyValue([ "category", "purchaseCategory", "purchase_category", "group", ]); normalized.purchaseType = firstNonEmptyValue([ "purchaseType", "purchase_type", "type", "kind", ]); normalized.purchaseInterval = firstNonEmptyValue([ "purchaseInterval", "purchase_interval", "interval", "billingCycle", "cycle", ]); normalized.actionLabel = firstNonEmptyValue([ "actionLabel", "buttonLabel", "cta", "action", ]); normalized.quantity = String(entry.quantity || "1").trim(); normalized.features = Array.isArray(entry.features) ? entry.features .map(function (feature) { return String(feature || "").trim(); }) .filter(function (feature) { return feature !== ""; }) : []; normalized.codeByTier = entry.codeByTier && typeof entry.codeByTier === "object" ? entry.codeByTier : undefined; normalized.priceByTier = entry.priceByTier && typeof entry.priceByTier === "object" ? entry.priceByTier : undefined; const creditsCandidate = Number.parseInt( String(entry.credits || entry.purchaseCredits || "").trim(), 10 ); if (Number.isFinite(creditsCandidate) && creditsCandidate > 0) { normalized.credits = creditsCandidate; } return normalized; }) .filter(function (entry) { return entry.code !== ""; }); } function applyBillingCatalogPayload(payload) { const payloadData = payload && payload.data && typeof payload.data === "object" ? payload.data : null; const catalog = payload && payload.catalog && typeof payload.catalog === "object" ? payload.catalog : payloadData && payloadData.catalog && typeof payloadData.catalog === "object" ? payloadData.catalog : payloadData && payloadData.subscriptions && Array.isArray(payloadData.subscriptions) ? payloadData : payload; const subscriptions = normalizeCatalogList( catalog && catalog.subscriptions ); const addons = normalizeCatalogList( catalog && (catalog.addons || catalog.oneTime) ); if (subscriptions.length === 0 && addons.length === 0) { subscriptionPackages = clonePurchaseCatalogEntries( defaultSubscriptionPackages ); oneTimePurchases = clonePurchaseCatalogEntries(defaultOneTimePurchases); setPurchasesCatalogSource("defaults"); return false; } subscriptionPackages = subscriptions.length > 0 ? subscriptions .map(enrichSubscriptionCatalogEntry) .sort(compareSubscriptionEntries) : clonePurchaseCatalogEntries(defaultSubscriptionPackages); oneTimePurchases = addons.length > 0 ? addons.map(enrichAddonCatalogEntry).sort(compareAddonEntries) : clonePurchaseCatalogEntries(defaultOneTimePurchases); setPurchasesCatalogSource("live"); return true; } async function refreshBillingCatalog(context) { if (!billingCatalogUrl) { return; } try { const payload = await requestJson(billingCatalogUrl, { method: "GET", headers, }); if (applyBillingCatalogPayload(payload)) { renderPurchases(); } } catch (error) { if (context === "manual") { setFeedback(error.message || "Failed to load billing catalog", true); } } } function deriveCompanyFromUrl(urlValue) { const normalized = String(urlValue || "").trim(); if (!normalized) { return ""; } try { const parsed = new URL(normalized); return parsed.hostname || ""; } catch (_error) { return normalized.replace(/^https?:\/\//i, "").split("/")[0] || ""; } } function setBillingAccountStatus(address) { if ( sovereignModeInput.value !== "powered" || !scBillingManagedByChdInput.checked ) { scAccountStatusEl.textContent = ""; scAccountStatusEl.classList.remove("qortal-warning-note"); if (bootstrapPackagingButton) { bootstrapPackagingButton.disabled = false; } return; } const normalized = String(address || "").trim(); if (normalized) { scAccountStatusEl.textContent = "Account linked: " + normalized; scAccountStatusEl.classList.remove("qortal-warning-note"); if (bootstrapPackagingButton) { bootstrapPackagingButton.disabled = false; } return; } const dashboardPath = String(accountDashboardUrl || "").trim(); scAccountStatusEl.textContent = "NuQloud account required before connect/sync or checkout."; if (dashboardPath) { const separator = document.createTextNode(" "); const link = document.createElement("a"); link.href = dashboardPath; link.textContent = "Create your account in the dashboard first"; scAccountStatusEl.appendChild(separator); scAccountStatusEl.appendChild(link); scAccountStatusEl.appendChild(document.createTextNode(".")); } scAccountStatusEl.classList.add("qortal-warning-note"); if (bootstrapPackagingButton) { bootstrapPackagingButton.disabled = true; } } function applyBillingDefaultsFromAdminContext() { const changedKeys = []; const maybeSet = function (input, key, value) { const trimmedValue = String(value || "").trim(); if (!trimmedValue) { return; } if (String(input.value || "").trim() !== "") { return; } input.value = trimmedValue; changedKeys.push(key); }; const inferredPublicUrl = String(nextcloudPublicUrlInput.value || "").trim() || String(nextcloudPublicUrlValue || "").trim() || window.location.origin; if ( String(nextcloudPublicUrlInput.value || "").trim() === "" && inferredPublicUrl ) { nextcloudPublicUrlInput.value = inferredPublicUrl; changedKeys.push("nextcloudPublicUrl"); } maybeSet( scBillingContactNameInput, "scBillingContactName", adminDisplayName ); maybeSet( scBillingCompanyNameInput, "scBillingCompanyName", deriveCompanyFromUrl(nextcloudPublicUrlInput.value || inferredPublicUrl) ); return changedKeys; } function extractPreferredAddressFromUserMappings(payload) { if (!payload || typeof payload !== "object") { return ""; } const list = payload.data && payload.data.mappings && Array.isArray(payload.data.mappings) ? payload.data.mappings : Array.isArray(payload.mappings) ? payload.mappings : []; let fallback = ""; list.forEach(function (entry) { if (!entry || typeof entry !== "object") { return; } const address = String(entry.qortalAddress || entry.address || "").trim(); if (!address) { return; } if (!fallback) { fallback = address; } if (String(entry.status || "").trim() === "linked") { fallback = address; } }); return fallback; } async function refreshAdminFundingAddress() { const payload = await requestJson(userMappingsUrl, { method: "GET", headers, }); const resolvedAddress = extractPreferredAddressFromUserMappings(payload); if (resolvedAddress) { scAdminQortalAddressInput.value = resolvedAddress; } setBillingAccountStatus(scAdminQortalAddressInput.value); return resolvedAddress; } function resetDirty() { hasUnsavedChanges = false; baselineSnapshot = JSON.stringify(collectSettings()); } function getChangedKeys(currentSettings) { let baseline = {}; try { baseline = baselineSnapshot ? JSON.parse(baselineSnapshot) : {}; } catch (_error) { baseline = {}; } return Object.keys(currentSettings).filter(function (key) { return String(currentSettings[key] ?? "") !== String(baseline[key] ?? ""); }); } function setFeedback(message, isError) { feedbackEl.textContent = message; feedbackEl.classList.toggle("error", Boolean(isError)); feedbackEl.classList.toggle("success", !isError && message.length > 0); } function collectSettings() { root.setAttribute( "data-mode-active", sovereignModeInput.value === "full_qortal" ? "full_qortal" : "powered" ); return { sovereignMode: sovereignModeInput.value, 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, 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: qappsBrowserAddressValue, qappsDebugEnabled: qappsDebugEnabledInput.checked ? "1" : "", qappsAdvancedUnlockSettingsEnabled: qappsAdvancedUnlockSettingsEnabledInput.checked ? "1" : "", qappsNonAdminRestrictionsEnabled: qappsNonAdminRestrictionsEnabledInput && qappsNonAdminRestrictionsEnabledInput.checked ? "1" : "", qappsNonAdminRestrictionRules: JSON.stringify( qappsNonAdminRestrictionRules .map(sanitizeNonAdminRestrictionRule) .filter(Boolean) ), userPublishingEnabled: userPublishingEnabledInput.checked ? "1" : "", filesPublishMaxSizeOverride: filesPublishMaxSizeOverrideInput ? filesPublishMaxSizeOverrideInput.value.trim() : "", filesPublishMaxCountOverride: filesPublishMaxCountOverrideInput ? filesPublishMaxCountOverrideInput.value.trim() : "", scBillingDashboardEnabled: scBillingEnabledInput.checked ? "1" : "", scBillingManagedByChd: scBillingManagedByChdInput.checked ? "1" : "", scAdvancedAdminEnabled: scAdvancedAdminEnabledInput.checked ? "1" : "", mspModeEnabled: mspModeEnabledInput.checked ? "1" : "", mspAdminGroup: mspAdminGroupInput.value.trim(), chdAdminDashboardEnabled: chdAdminDashboardEnabledInput.checked ? "1" : "", scBillingProvider: scBillingProviderInput.value, scBillingSyncMode: scBillingSyncModeInput.value, scChdBaseUrl: scChdBaseUrlInput.value.trim(), scChdSyncPath: scChdSyncPathInput.value.trim(), scChdRegisterPath: scChdRegisterPathInput.value.trim(), scChdCheckoutPath: scChdCheckoutPathInput.value.trim(), scChdCatalogPath: scChdCatalogPathInput.value.trim(), scChdCatalogCategoryId: scChdCatalogCategoryIdInput.value.trim(), scChdCatalogAudience: scChdCatalogAudienceInput.value.trim(), scChdRegistrationToken: scChdRegistrationTokenInput.value.trim(), scChdInstanceId: scChdInstanceIdInput.value.trim(), scChdInstanceToken: scChdInstanceTokenInput.value.trim(), scChdInstanceSecret: scChdInstanceSecretInput.value.trim(), scPackageTier: scPackageTierInput.value, scBillingContactName: scBillingContactNameInput.value.trim(), scBillingContactEmail: scBillingContactEmailInput.value.trim(), scBillingCompanyName: scBillingCompanyNameInput.value.trim(), scBillingPhone: scBillingPhoneInput.value.trim(), scBillingAddress1: scBillingAddress1Input.value.trim(), scBillingAddress2: scBillingAddress2Input.value.trim(), scBillingCity: scBillingCityInput.value.trim(), scBillingState: scBillingStateInput.value.trim(), scBillingPostalCode: scBillingPostalCodeInput.value.trim(), scBillingCountry: scBillingCountryInput.value.trim(), scAdminQortalAddress: scAdminQortalAddressInput.value.trim(), scCreditsBalance: scCreditsBalanceInput.value.trim(), scMonthlyCredits: scMonthlyCreditsInput.value.trim(), scEncryptedCapacityGb: scCapacityGbInput.value.trim(), scReplicationTarget: scReplicationTargetInput.value.trim(), 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 splitListValue(value, splitWhitespace) { return String(value || "") .split(splitWhitespace ? /[\s,]+/ : /[\n,]/) .map(function (entry) { return entry.trim(); }) .filter(Boolean); } function joinListValue(value) { return Array.isArray(value) ? value.join("\n") : ""; } function urlFromClientTemplate(template, clientId) { return String(template || "").replace( "__CLIENT_ID__", encodeURIComponent(clientId) ); } 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 if (externalAuthEnvBaseUrl) { setupOverviewList.appendChild( renderStatusItem("External Auth Base URL", "ok", "configured via .env") ); } else { setupOverviewList.appendChild( renderStatusItem("External Auth Base URL", "warn", "missing") ); } if (externalAuthAppId && externalAuthAppSecret) { setupOverviewList.appendChild( renderStatusItem("External Auth app credentials", "ok") ); } else if (externalAuthEnvAppCreds) { setupOverviewList.appendChild( renderStatusItem( "External Auth app credentials", "ok", "configured via .env" ) ); } else { setupOverviewList.appendChild( renderStatusItem("External Auth app credentials", "warn", "missing") ); } if (externalAuthNodeUrl) { setupOverviewList.appendChild( renderStatusItem("External Auth Qortal node", "ok", externalAuthNodeUrl) ); } else if (externalAuthEnvNodeUrl) { setupOverviewList.appendChild( renderStatusItem( "External Auth Qortal node", "ok", "configured via .env" ) ); } 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 chdManaged = scBillingManagedByChdInput.checked; const billingProvider = scBillingProviderInput.value; const billingSyncMode = scBillingSyncModeInput.value; const chdBaseUrl = scChdBaseUrlInput.value.trim(); const chdInstanceId = scChdInstanceIdInput.value.trim(); setupOverviewList.appendChild( renderStatusItem( "CHD billing mode", chdManaged ? "ok" : "warn", chdManaged ? "managed" : "disabled" ) ); if (chdManaged && chdBaseUrl) { setupOverviewList.appendChild( renderStatusItem("CHD billing URL", "ok", chdBaseUrl) ); } else if (chdManaged) { setupOverviewList.appendChild( renderStatusItem("CHD billing URL", "warn", "missing") ); } if (chdManaged && chdInstanceId) { setupOverviewList.appendChild( renderStatusItem("CHD instance ID", "ok", chdInstanceId) ); } else if (chdManaged) { setupOverviewList.appendChild( renderStatusItem("CHD instance ID", "warn", "missing") ); } setupOverviewList.appendChild( renderStatusItem("Billing provider", "ok", billingProvider || "paymenter") ); setupOverviewList.appendChild( renderStatusItem( "Connector mode", billingSyncMode === "read_only" || billingSyncMode === "managed" ? "ok" : "warn", billingSyncMode || "read_only" ) ); 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."; const selectedPolicy = oidcPolicySelect.value.trim(); const selectedGuard = oidcGuardToggle.value.trim(); autoProvisionEnabled = selectedPolicy ? selectedPolicy === "auto_provision" : true; autoProvisionGuardEnabled = selectedGuard ? isGuardEnabled(selectedGuard) : true; 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 : "auto_provision"; const guardEnabled = config && Object.prototype.hasOwnProperty.call(config, "autoProvisionGuard") ? isGuardEnabled(config.autoProvisionGuard) : true; const autoProvisionEnabled = policy === "auto_provision"; allowlistCard.classList.toggle("qortal-hidden", false); invitesCard.classList.toggle( "qortal-hidden", !autoProvisionEnabled || !guardEnabled ); } 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 accounts 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); }); } async function moveQappByIndex(index, delta) { const from = Number(index); const shift = Number(delta); if (!Number.isInteger(from) || !Number.isInteger(shift) || shift === 0) { return; } const to = from + shift; if (from < 0 || from >= qapps.length || to < 0 || to >= qapps.length) { return; } const moving = qapps[from]; qapps.splice(from, 1); qapps.splice(to, 0, moving); renderQapps(); markDirty(); try { await saveSettings(["qappsList"]); setFeedback("Q-App order saved.", false); } catch (error) { setFeedback(error.message || "Failed to save Q-App order", true); } } function truncateQappDescription(value, maxLength) { const text = String(value || "").trim(); const limit = Number.isInteger(maxLength) && maxLength > 3 ? maxLength : 96; if (text.length <= limit) { return text; } return text.slice(0, limit - 1).trimEnd() + "…"; } function renderQapps() { qappsBody.innerHTML = ""; if (qapps.length === 0) { qappsBody.innerHTML = 'No Q-Apps configured yet.'; return; } qapps.forEach(function (entry, index) { const row = document.createElement("tr"); const entryAddressKey = normalizeQappAddressKey(entry.address); const nameCell = document.createElement("td"); const displayName = entry.name || entry.address || ""; nameCell.textContent = String(index + 1) + ". " + displayName; const addressCell = document.createElement("td"); const addressCode = document.createElement("code"); addressCode.textContent = entry.address || ""; addressCell.appendChild(addressCode); const descriptionCell = document.createElement("td"); const fullDescription = String(entry.description || ""); descriptionCell.className = "qortal-qapp-description-cell"; descriptionCell.textContent = truncateQappDescription( fullDescription, 28 ); if (fullDescription) { descriptionCell.title = fullDescription; } 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 if (entry.iconMode === "name" && entry.iconFromName) { iconCell.textContent = "name: " + entry.iconFromName; } else { iconCell.textContent = "auto"; } const actionCell = document.createElement("td"); actionCell.className = "qortal-qapp-actions-cell"; const moveUpButton = document.createElement("button"); moveUpButton.className = "button"; moveUpButton.textContent = "Up"; moveUpButton.disabled = index === 0; moveUpButton.addEventListener("click", async function () { await moveQappByIndex(index, -1); }); actionCell.appendChild(moveUpButton); actionCell.appendChild(document.createTextNode(" ")); const moveDownButton = document.createElement("button"); moveDownButton.className = "button"; moveDownButton.textContent = "Down"; moveDownButton.disabled = index === qapps.length - 1; moveDownButton.addEventListener("click", async function () { await moveQappByIndex(index, 1); }); actionCell.appendChild(moveDownButton); actionCell.appendChild(document.createTextNode(" ")); const editButton = document.createElement("button"); editButton.className = "button"; editButton.textContent = "Edit"; editButton.addEventListener("click", function () { enterQappEditMode(entry); setFeedback( "Editing app entry. Click Update App to save changes.", false ); }); actionCell.appendChild(editButton); actionCell.appendChild(document.createTextNode(" ")); const removeButton = document.createElement("button"); removeButton.className = "button"; removeButton.textContent = "Remove"; removeButton.addEventListener("click", async function () { qapps = qapps.filter(function (item) { return normalizeQappAddressKey(item.address) !== entryAddressKey; }); if (editingQappAddressKey === entryAddressKey) { exitQappEditMode(); } 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 = normalizeQappIconMode(qappsIconModeInput.value); const iconUrl = qappsIconUrlInput.value.trim(); const iconFromName = qappsIconFromNameInput ? qappsIconFromNameInput.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" ); } if (iconMode === "name" && !iconFromName) { throw new Error( "Icon source name is required when icon mode is set to Use Icon From Name" ); } const entry = { name: name || address, address: address, description: description, iconMode: iconMode, iconUrl: iconMode === "custom" ? iconUrl : "", iconFromName: iconMode === "name" ? iconFromName : "", }; const entryKey = normalizeQappAddressKey(entry.address); const existingIndex = qapps.findIndex(function (item) { return normalizeQappAddressKey(item.address) === entryKey; }); const editingIndex = editingQappAddressKey ? qapps.findIndex(function (item) { return ( normalizeQappAddressKey(item.address) === editingQappAddressKey ); }) : -1; let action = "added"; if (editingQappAddressKey) { if (existingIndex !== -1 && existingIndex !== editingIndex) { qapps[existingIndex] = entry; if (editingIndex !== -1) { qapps.splice(editingIndex, 1); } } else if (editingIndex !== -1) { qapps[editingIndex] = entry; } else if (existingIndex !== -1) { qapps[existingIndex] = entry; } else { qapps.push(entry); action = "added"; } if (action !== "added") { action = "updated"; } } else if (existingIndex !== -1) { qapps[existingIndex] = entry; action = "updated"; } else { qapps.push(entry); action = "added"; } exitQappEditMode(); renderQapps(); markDirty(); return action; } function clearQapps() { qapps = []; exitQappEditMode(); 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 commentCell = document.createElement("td"); commentCell.textContent = invite.comment || "—"; 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(commentCell); row.appendChild(statusCell); row.appendChild(expiresCell); row.appendChild(usedByCell); row.appendChild(actionCell); invitesBody.appendChild(row); }); } function resetOidcClientForm() { editingOidcClientId = ""; idpClientIdInput.disabled = false; idpClientIdInput.value = ""; idpDisplayNameInput.value = ""; idpRedirectUrisInput.value = ""; idpPostLogoutRedirectUrisInput.value = ""; idpAllowedScopesInput.value = "openid,profile,email"; idpAllowedGroupsInput.value = ""; idpRequirePkceInput.checked = false; idpSaveClientButton.textContent = "Create Client"; idpCancelEditButton.classList.add("qortal-hidden"); renderOidcClientConfigSummary(); } function readOidcClientForm() { const clientId = idpClientIdInput.value.trim(); const displayName = idpDisplayNameInput.value.trim(); const redirectUris = splitListValue(idpRedirectUrisInput.value); if (!clientId) { throw new Error("Client ID is required"); } if (!displayName) { throw new Error("Display name is required"); } if (redirectUris.length === 0) { throw new Error("At least one redirect URI is required"); } return { clientId, displayName, redirectUris, postLogoutRedirectUris: splitListValue( idpPostLogoutRedirectUrisInput.value ), allowedScopes: splitListValue(idpAllowedScopesInput.value, true), allowedGroups: splitListValue(idpAllowedGroupsInput.value), requirePkce: idpRequirePkceInput.checked ? "1" : "", }; } function enterOidcClientEdit(client) { editingOidcClientId = client.clientId || ""; idpClientIdInput.value = editingOidcClientId; idpClientIdInput.disabled = true; idpDisplayNameInput.value = client.displayName || ""; idpRedirectUrisInput.value = joinListValue(client.redirectUris); idpPostLogoutRedirectUrisInput.value = joinListValue( client.postLogoutRedirectUris ); idpAllowedScopesInput.value = Array.isArray(client.allowedScopes) ? client.allowedScopes.join(",") : "openid,profile,email"; idpAllowedGroupsInput.value = Array.isArray(client.allowedGroups) ? client.allowedGroups.join(",") : ""; idpRequirePkceInput.checked = Boolean(client.requirePkce); idpSaveClientButton.textContent = "Update Client"; idpCancelEditButton.classList.remove("qortal-hidden"); renderOidcClientConfigSummary(); } function selectedOidcTemplate() { return ( oidcClientTemplates[idpTemplateInput.value] || oidcClientTemplates.generic ); } function applySelectedOidcTemplate() { const template = selectedOidcTemplate(); idpClientIdInput.value = template.clientId; idpDisplayNameInput.value = template.displayName; idpRedirectUrisInput.value = joinListValue(template.redirectUris); idpPostLogoutRedirectUrisInput.value = ""; idpAllowedScopesInput.value = template.scopes.join(","); idpAllowedGroupsInput.value = ""; idpRequirePkceInput.checked = Boolean(template.requirePkce); renderOidcClientConfigSummary(); } function renderOidcClientConfigSummary() { const issuer = (oidcIssuerInput.value || brokerBaseFromSettings()).trim(); const clientId = idpClientIdInput.value.trim() || "(client id)"; const scopes = splitListValue(idpAllowedScopesInput.value).join(" "); const guide = selectedOidcTemplate().guide || []; idpConfigOutput.textContent = [ "Issuer: " + issuer, "Discovery URL: " + issuer.replace(/\/$/, "") + "/.well-known/openid-configuration", "Client ID: " + clientId, "Scopes: " + (scopes || "openid profile email"), "Auth method: client_secret_basic or client_secret_post", "", ...guide, ].join("\n"); } function renderOidcClients(payload) { const clients = payload && payload.ok && payload.data && Array.isArray(payload.data.clients) ? payload.data.clients : []; idpClientsBody.innerHTML = ""; if (clients.length === 0) { idpClientsBody.innerHTML = 'No OIDC clients registered yet.'; return; } clients.forEach(function (client) { const row = document.createElement("tr"); const clientCell = document.createElement("td"); const name = document.createElement("strong"); name.textContent = client.displayName || client.clientId || ""; const id = document.createElement("code"); id.textContent = client.clientId || ""; clientCell.appendChild(name); clientCell.appendChild(document.createElement("br")); clientCell.appendChild(id); const redirectsCell = document.createElement("td"); redirectsCell.textContent = Array.isArray(client.redirectUris) ? client.redirectUris.join(", ") : ""; const scopesCell = document.createElement("td"); scopesCell.textContent = Array.isArray(client.allowedScopes) ? client.allowedScopes.join(", ") : ""; const groupsCell = document.createElement("td"); groupsCell.textContent = Array.isArray(client.allowedGroups) && client.allowedGroups.length > 0 ? client.allowedGroups.join(", ") : "Any"; const statusCell = document.createElement("td"); statusCell.textContent = (client.enabled ? "enabled" : "disabled") + (client.requirePkce ? " / PKCE" : ""); const actionCell = document.createElement("td"); const editButton = document.createElement("button"); editButton.className = "button"; editButton.textContent = "Edit"; editButton.addEventListener("click", function () { enterOidcClientEdit(client); }); actionCell.appendChild(editButton); actionCell.appendChild(document.createTextNode(" ")); const toggleButton = document.createElement("button"); toggleButton.className = "button"; toggleButton.textContent = client.enabled ? "Disable" : "Enable"; toggleButton.addEventListener("click", async function () { try { await setOidcClientEnabled(client.clientId, !client.enabled); setFeedback("OIDC client updated.", false); } catch (error) { setFeedback(error.message || "Failed to update OIDC client", true); } }); actionCell.appendChild(toggleButton); actionCell.appendChild(document.createTextNode(" ")); const rotateButton = document.createElement("button"); rotateButton.className = "button"; rotateButton.textContent = "Rotate Secret"; rotateButton.addEventListener("click", async function () { try { await rotateOidcClientSecret(client.clientId); setFeedback("OIDC client secret rotated.", false); } catch (error) { setFeedback(error.message || "Failed to rotate client secret", true); } }); actionCell.appendChild(rotateButton); row.appendChild(clientCell); row.appendChild(redirectsCell); row.appendChild(scopesCell); row.appendChild(groupsCell); row.appendChild(statusCell); row.appendChild(actionCell); idpClientsBody.appendChild(row); }); } function renderOidcAuditEvents(payload) { const events = payload && payload.ok && payload.data && Array.isArray(payload.data.events) ? payload.data.events : []; idpAuditBody.innerHTML = ""; if (events.length === 0) { idpAuditBody.innerHTML = 'No audit events yet.'; return; } events.slice(0, 50).forEach(function (event) { const row = document.createElement("tr"); const eventCell = document.createElement("td"); eventCell.textContent = event.eventType || ""; const clientCell = document.createElement("td"); clientCell.textContent = event.clientId || ""; const userCell = document.createElement("td"); userCell.textContent = event.nextcloudUserId || ""; const createdCell = document.createElement("td"); createdCell.textContent = formatTimestamp(event.createdAt); row.appendChild(eventCell); row.appendChild(clientCell); row.appendChild(userCell); row.appendChild(createdCell); idpAuditBody.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(forcedChangedKeys) { setFeedback("Saving...", false); const settings = collectSettings(); const form = new URLSearchParams(settings); const changedByDiff = getChangedKeys(settings); const changedKeys = Array.isArray(forcedChangedKeys) ? changedByDiff.filter(function (key) { return forcedChangedKeys.includes(key); }) : changedByDiff; form.set("changedKeys", JSON.stringify(changedKeys)); 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 = []; const syncSkipped = []; 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 (payload.oidcPolicySync && payload.oidcPolicySync.skipped) { syncSkipped.push( `broker policy sync skipped (${ payload.oidcPolicySync.reason || "not needed" })` ); } if (payload.qortalRuntimeSync && payload.qortalRuntimeSync.skipped) { syncSkipped.push( `broker runtime sync skipped (${ payload.qortalRuntimeSync.reason || "not needed" })` ); } 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; } if (syncSkipped.length > 0) { setFeedback(`Settings saved. ${syncSkipped.join(" | ")}`, false); return; } setFeedback("Settings and runtime sync saved.", false); } function applyBillingSettingsFromPayload(settings) { if (!settings || typeof settings !== "object") { return; } if (Object.prototype.hasOwnProperty.call(settings, "scBillingProvider")) { scBillingProviderInput.value = String( settings.scBillingProvider || "paymenter" ); } if ( Object.prototype.hasOwnProperty.call(settings, "scAdvancedAdminEnabled") ) { scAdvancedAdminEnabledInput.checked = Boolean( settings.scAdvancedAdminEnabled ); updateBillingAdvancedState(); } if (Object.prototype.hasOwnProperty.call(settings, "scBillingSyncMode")) { scBillingSyncModeInput.value = String( settings.scBillingSyncMode || "read_only" ); } if ( Object.prototype.hasOwnProperty.call( settings, "scBillingDashboardEnabled" ) ) { scBillingEnabledInput.checked = Boolean( settings.scBillingDashboardEnabled ); } if ( Object.prototype.hasOwnProperty.call(settings, "chdAdminDashboardEnabled") ) { chdAdminDashboardEnabledInput.checked = Boolean( settings.chdAdminDashboardEnabled ); } if (Object.prototype.hasOwnProperty.call(settings, "scChdBaseUrl")) { scChdBaseUrlInput.value = String(settings.scChdBaseUrl || ""); } if (Object.prototype.hasOwnProperty.call(settings, "scChdSyncPath")) { scChdSyncPathInput.value = String(settings.scChdSyncPath || ""); } if (Object.prototype.hasOwnProperty.call(settings, "scChdRegisterPath")) { scChdRegisterPathInput.value = String(settings.scChdRegisterPath || ""); } if (Object.prototype.hasOwnProperty.call(settings, "scChdCheckoutPath")) { scChdCheckoutPathInput.value = String(settings.scChdCheckoutPath || ""); } if (Object.prototype.hasOwnProperty.call(settings, "scChdCatalogPath")) { scChdCatalogPathInput.value = String(settings.scChdCatalogPath || ""); } if ( Object.prototype.hasOwnProperty.call(settings, "scChdCatalogCategoryId") ) { scChdCatalogCategoryIdInput.value = String( settings.scChdCatalogCategoryId || "" ); } if ( Object.prototype.hasOwnProperty.call(settings, "scChdCatalogAudience") ) { scChdCatalogAudienceInput.value = String( settings.scChdCatalogAudience || "" ); } if ( Object.prototype.hasOwnProperty.call(settings, "scChdRegistrationToken") ) { scChdRegistrationTokenInput.value = String( settings.scChdRegistrationToken || "" ); } if (Object.prototype.hasOwnProperty.call(settings, "scChdInstanceId")) { scChdInstanceIdInput.value = String(settings.scChdInstanceId || ""); } if (Object.prototype.hasOwnProperty.call(settings, "scChdInstanceToken")) { scChdInstanceTokenInput.value = String(settings.scChdInstanceToken || ""); } if (Object.prototype.hasOwnProperty.call(settings, "scChdInstanceSecret")) { scChdInstanceSecretInput.value = String( settings.scChdInstanceSecret || "" ); } if (Object.prototype.hasOwnProperty.call(settings, "scPackageTier")) { scPackageTierInput.value = String(settings.scPackageTier || "free_core"); } if ( Object.prototype.hasOwnProperty.call(settings, "scBillingContactName") ) { scBillingContactNameInput.value = String( settings.scBillingContactName || "" ); } if ( Object.prototype.hasOwnProperty.call(settings, "scBillingContactEmail") ) { scBillingContactEmailInput.value = String( settings.scBillingContactEmail || "" ); } if ( Object.prototype.hasOwnProperty.call(settings, "scBillingCompanyName") ) { scBillingCompanyNameInput.value = String( settings.scBillingCompanyName || "" ); } if (Object.prototype.hasOwnProperty.call(settings, "scBillingPhone")) { scBillingPhoneInput.value = String(settings.scBillingPhone || ""); } if (Object.prototype.hasOwnProperty.call(settings, "scBillingAddress1")) { scBillingAddress1Input.value = String(settings.scBillingAddress1 || ""); } if (Object.prototype.hasOwnProperty.call(settings, "scBillingAddress2")) { scBillingAddress2Input.value = String(settings.scBillingAddress2 || ""); } if (Object.prototype.hasOwnProperty.call(settings, "scBillingCity")) { scBillingCityInput.value = String(settings.scBillingCity || ""); } if (Object.prototype.hasOwnProperty.call(settings, "scBillingState")) { scBillingStateInput.value = String(settings.scBillingState || ""); } if (Object.prototype.hasOwnProperty.call(settings, "scBillingPostalCode")) { scBillingPostalCodeInput.value = String( settings.scBillingPostalCode || "" ); } if (Object.prototype.hasOwnProperty.call(settings, "scBillingCountry")) { scBillingCountryInput.value = String(settings.scBillingCountry || ""); } if ( Object.prototype.hasOwnProperty.call(settings, "scAdminQortalAddress") ) { scAdminQortalAddressInput.value = String( settings.scAdminQortalAddress || "" ); } if (Object.prototype.hasOwnProperty.call(settings, "scCreditsBalance")) { scCreditsBalanceInput.value = String(settings.scCreditsBalance || "0"); } if (Object.prototype.hasOwnProperty.call(settings, "scMonthlyCredits")) { scMonthlyCreditsInput.value = String(settings.scMonthlyCredits || "0"); } if ( Object.prototype.hasOwnProperty.call(settings, "scEncryptedCapacityGb") ) { scCapacityGbInput.value = String(settings.scEncryptedCapacityGb || "0"); } if (Object.prototype.hasOwnProperty.call(settings, "scReplicationTarget")) { scReplicationTargetInput.value = String( settings.scReplicationTarget || "0" ); } if ( Object.prototype.hasOwnProperty.call(settings, "userPublishingEnabled") ) { userPublishingEnabledInput.checked = Boolean( settings.userPublishingEnabled ); } if ( filesPublishMaxSizeOverrideInput && Object.prototype.hasOwnProperty.call( settings, "filesPublishMaxSizeOverride" ) ) { filesPublishMaxSizeOverrideInput.value = String( settings.filesPublishMaxSizeOverride || "" ); } if ( filesPublishMaxCountOverrideInput && Object.prototype.hasOwnProperty.call( settings, "filesPublishMaxCountOverride" ) ) { filesPublishMaxCountOverrideInput.value = String( settings.filesPublishMaxCountOverride || "" ); } if (Object.prototype.hasOwnProperty.call(settings, "qappsEnabled")) { qappsEnabledInput.checked = Boolean(settings.qappsEnabled); } if ( Object.prototype.hasOwnProperty.call( settings, "qappsAdvancedUnlockSettingsEnabled" ) ) { qappsAdvancedUnlockSettingsEnabledInput.checked = Boolean( settings.qappsAdvancedUnlockSettingsEnabled ); } if ( Object.prototype.hasOwnProperty.call( settings, "qappsNonAdminRestrictionsEnabled" ) && qappsNonAdminRestrictionsEnabledInput ) { qappsNonAdminRestrictionsEnabledInput.checked = Boolean( settings.qappsNonAdminRestrictionsEnabled ); } if ( Object.prototype.hasOwnProperty.call( settings, "qappsNonAdminRestrictionRules" ) ) { const incomingRules = Array.isArray( settings.qappsNonAdminRestrictionRules ) ? settings.qappsNonAdminRestrictionRules : []; qappsNonAdminRestrictionRules = incomingRules .map(sanitizeNonAdminRestrictionRule) .filter(Boolean); renderQappsNonAdminRestrictionRules(); } if ( Object.prototype.hasOwnProperty.call(settings, "qappsFullBrowserAddress") ) { qappsBrowserAddressValue = String(settings.qappsFullBrowserAddress || ""); } if (Object.prototype.hasOwnProperty.call(settings, "scChdLastSyncStatus")) { const statusEl = document.getElementById( "qortal-sc-chd-last-sync-status" ); if (statusEl) { statusEl.textContent = String(settings.scChdLastSyncStatus || "").trim() || "not synced"; } } if ( Object.prototype.hasOwnProperty.call(settings, "scChdLastSyncMessage") ) { const messageEl = document.getElementById( "qortal-sc-chd-last-sync-message" ); if (messageEl) { messageEl.textContent = String(settings.scChdLastSyncMessage || ""); } } setBillingAccountStatus(scAdminQortalAddressInput.value); renderPurchases(); refreshPurchasesVisibilityDefault(); refreshBillingCatalog("auto"); } async function syncBillingEntitlements() { setFeedback("Syncing CHD entitlements...", false); const payload = await requestJson(billingSyncUrl, { method: "POST", headers, body: new URLSearchParams(), }); applyBillingSettingsFromPayload(payload.settings || {}); statusEl.textContent = JSON.stringify(payload, null, 2); resetDirty(); setFeedback("CHD entitlements synced and applied.", false); } async function testBillingConnector() { setFeedback("Testing billing connector...", false); const payload = await requestJson(billingTestUrl, { method: "POST", headers, body: new URLSearchParams(), }); statusEl.textContent = JSON.stringify(payload, null, 2); setFeedback( "Billing connector test succeeded (no changes applied).", false ); } async function bootstrapBillingConnector() { setFeedback("Connecting installation to CHD...", false); const body = new URLSearchParams({ packageTier: scPackageTierInput.value || "", }); const payload = await requestJson(billingBootstrapUrl, { method: "POST", headers, body, }); applyBillingSettingsFromPayload(payload.settings || {}); statusEl.textContent = JSON.stringify(payload, null, 2); resetDirty(); setFeedback("Installation connected and entitlements synced.", false); } async function openBillingCheckout(options) { const checkoutOptions = options && typeof options === "object" ? options : {}; const billingContactName = scBillingContactNameInput.value.trim(); const billingContactEmail = scBillingContactEmailInput.value.trim(); if (!billingContactName || !billingContactEmail) { setFeedback( "Shared instance billing contact name/email are missing. Continuing to checkout; you may be asked to complete billing details.", false ); } const purchaseMeta = checkoutOptions.purchaseMeta && typeof checkoutOptions.purchaseMeta === "object" ? checkoutOptions.purchaseMeta : null; const purchaseType = String( purchaseMeta && purchaseMeta.purchaseType ? purchaseMeta.purchaseType : "" ) .trim() .toLowerCase(); const purchaseCategory = String( purchaseMeta && purchaseMeta.purchaseCategory ? purchaseMeta.purchaseCategory : "" ) .trim() .toLowerCase(); const purchaseCode = String( purchaseMeta && purchaseMeta.purchaseCode ? purchaseMeta.purchaseCode : "" ).trim(); let selectedTier = String(checkoutOptions.packageTier || "").trim() || scPackageTierInput.value || "free_core"; if ( purchaseCode && (purchaseType === "subscription" || purchaseCategory === "package") && (!String(checkoutOptions.packageTier || "").trim() || selectedTier === "free_core") ) { selectedTier = purchaseCode; } setFeedback("Requesting checkout link...", false); const body = new URLSearchParams({ packageTier: selectedTier, billingContactName, billingContactEmail, billingCompanyName: scBillingCompanyNameInput.value.trim(), billingPhone: scBillingPhoneInput.value.trim(), billingAddress1: scBillingAddress1Input.value.trim(), billingAddress2: scBillingAddress2Input.value.trim(), billingCity: scBillingCityInput.value.trim(), billingState: scBillingStateInput.value.trim(), billingPostalCode: scBillingPostalCodeInput.value.trim(), billingCountry: scBillingCountryInput.value.trim(), }); if (purchaseMeta) { Object.keys(purchaseMeta).forEach(function (key) { const value = purchaseMeta[key]; if (value === undefined || value === null) { return; } const normalized = String(value).trim(); if (!normalized) { return; } body.set(key, normalized); }); } const payload = await requestJson(billingCheckoutUrl, { method: "POST", headers, body, }); const checkoutUrl = payload.checkoutUrl || ""; if (!checkoutUrl) { throw new Error("Checkout URL is missing from connector response"); } window.open(checkoutUrl, "_blank", "noopener,noreferrer"); statusEl.textContent = JSON.stringify(payload, null, 2); const purchaseLabel = purchaseMeta && typeof purchaseMeta.purchaseLabel === "string" ? purchaseMeta.purchaseLabel.trim() : ""; if (purchaseLabel) { setFeedback(`Checkout opened for ${purchaseLabel}.`, false); return; } setFeedback("Checkout opened in a new tab.", false); } async function autoInitializeBilling() { if (sovereignModeInput.value !== "powered") { return; } if (!scBillingManagedByChdInput.checked) { return; } const changedKeys = applyBillingDefaultsFromAdminContext(); let resolvedAddress = ""; try { resolvedAddress = await refreshAdminFundingAddress(); if (resolvedAddress && !changedKeys.includes("scAdminQortalAddress")) { changedKeys.push("scAdminQortalAddress"); } } catch (_error) { setBillingAccountStatus(scAdminQortalAddressInput.value); } if (changedKeys.length > 0) { await saveSettings(changedKeys); } const hasAddress = String(scAdminQortalAddressInput.value || "").trim() !== ""; if (!hasAddress) { return; } const hasBaseUrl = String(scChdBaseUrlInput.value || "").trim() !== ""; if (!hasBaseUrl) { return; } const hasInstanceId = String(scChdInstanceIdInput.value || "").trim() !== ""; if (!hasInstanceId) { await bootstrapBillingConnector(); return; } const lastSyncStatus = String( (document.getElementById("qortal-sc-chd-last-sync-status") || {}) .textContent || "" ) .trim() .toLowerCase(); if (lastSyncStatus !== "success") { await syncBillingEntitlements(); } } 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" : "", }); const payload = await requestJson(notifyUrl, { method: "POST", headers, body, }); notifyResult.textContent = JSON.stringify(payload, null, 2); return payload; } function summarizeNotificationResult(payload) { const results = Array.isArray(payload && payload.results) ? payload.results : []; let emailSent = 0; let emailFailed = 0; let inAppSent = 0; let inAppFailed = 0; results.forEach(function (entry) { if (entry && entry.email) { if (entry.email.sent) { emailSent += 1; } else { emailFailed += 1; } } if (entry && entry.inApp) { if (entry.inApp.sent) { inAppSent += 1; } else { inAppFailed += 1; } } }); return { totalTargets: Number( payload && payload.sent ? payload.sent : results.length || 0 ), emailSent, emailFailed, inAppSent, inAppFailed, hasFailures: emailFailed > 0 || inAppFailed > 0, }; } 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 baseUrl = nextcloudPublicUrlInput.value.trim() || nextcloudPublicUrlValue || window.location.origin; const link = baseUrl.replace(/\/$/, "") + "/apps/qortal_integration/account"; 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, user: firstUserId, displayName: displayName, email: match.email || "", cloudName: "NuQloud", instanceName: "NuQloud", invite: "", orderId: "", invoiceId: "", packageTier: "", }; 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("Account password is required"); } setFeedback("Creating account...", false); const body = new URLSearchParams({ password, kdfThreads: kdfThreads || "", }); const payload = await requestJson(createWalletUrl, { method: "POST", headers, body, }); assertBrokerOk(payload, "Account creation failed"); walletCreateResult.textContent = JSON.stringify(payload, null, 2); document.getElementById("qortal-wallet-password").value = ""; await refreshWallets(); setFeedback("Account 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("Account password is required"); } if (!nextcloudUserId) { throw new Error("Nextcloud user ID is required to link an account"); } setFeedback("Creating account 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("Account 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("Account created and linked.", false); } async function refreshWallets() { const payload = await requestJson(walletsUrl, { method: "GET", headers, }); assertBrokerOk(payload, "Failed to fetch accounts"); 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 refreshOidcClients() { const payload = await requestJson(oidcClientsUrl, { method: "GET", headers, }); assertBrokerOk(payload, "Failed to fetch OIDC clients"); renderOidcClients(payload); } async function refreshOidcAuditEvents() { const payload = await requestJson(oidcAuditEventsUrl, { method: "GET", headers, }); assertBrokerOk(payload, "Failed to fetch OIDC audit events"); renderOidcAuditEvents(payload); } async function saveOidcClient() { const client = readOidcClientForm(); const body = new URLSearchParams(); Object.entries(client).forEach(function ([key, value]) { if (Array.isArray(value)) { body.set(key, value.join("\n")); } else { body.set(key, String(value)); } }); const isEdit = editingOidcClientId !== ""; const url = isEdit ? urlFromClientTemplate(updateOidcClientUrlTemplate, editingOidcClientId) : createOidcClientUrl; const payload = await requestJson(url, { method: "POST", headers, body, }); assertBrokerOk(payload, "Failed to save OIDC client"); const secret = payload && payload.data && typeof payload.data.clientSecret === "string" ? payload.data.clientSecret : ""; latestOidcClientSecret = secret; idpSecretOutput.textContent = secret ? "Client secret (shown once):\n" + secret : ""; resetOidcClientForm(); await refreshOidcClients(); } async function setOidcClientEnabled(clientId, enabled) { const body = new URLSearchParams({ enabled: enabled ? "1" : "", }); const payload = await requestJson( urlFromClientTemplate(setOidcClientEnabledUrlTemplate, clientId), { method: "POST", headers, body, } ); assertBrokerOk(payload, "Failed to update OIDC client"); await refreshOidcClients(); } async function rotateOidcClientSecret(clientId) { if ( !window.confirm( "Rotate this client secret? Existing app configuration using the old secret will stop working." ) ) { return; } const payload = await requestJson( urlFromClientTemplate(rotateOidcClientSecretUrlTemplate, clientId), { method: "POST", headers, } ); assertBrokerOk(payload, "Failed to rotate OIDC client secret"); const secret = payload && payload.data && typeof payload.data.clientSecret === "string" ? payload.data.clientSecret : ""; latestOidcClientSecret = secret; idpSecretOutput.textContent = secret ? "Client secret (shown once):\n" + secret : ""; await refreshOidcClients(); } async function copyLatestOidcClientSecret() { if (!latestOidcClientSecret) { throw new Error("No client secret is currently available to copy"); } if (!navigator.clipboard) { idpSecretOutput.focus(); throw new Error("Clipboard API unavailable. Select and copy the secret manually."); } await navigator.clipboard.writeText(latestOidcClientSecret); } async function createInvite() { const expiryValue = document.getElementById( "qortal-invite-expiry-hours" ).value; const expiryHours = expiryValue ? Number(expiryValue) : null; const comment = inviteCommentInput ? inviteCommentInput.value.trim() : ""; 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))); } if (comment) { body.set("comment", comment.slice(0, 500)); } 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(); } function parsePositiveInteger(value, fallbackValue, min, max) { const numeric = Number(value); if (!Number.isFinite(numeric)) { return fallbackValue; } const whole = Math.floor(numeric); if (whole < min) { return min; } if (whole > max) { return max; } return whole; } function getRecoveryTargetUserId() { return recoveryTargetUserIdInput.value.trim(); } function setRecoveryAdminResult(payload) { recoveryAdminResult.textContent = JSON.stringify(payload, null, 2); } async function checkRecoveryStatus() { const targetUserId = getRecoveryTargetUserId(); if (!targetUserId) { throw new Error("Target Nextcloud user ID is required"); } const query = new URLSearchParams({ targetUserId }); const payload = await requestJson( adminRecoveryStatusUrl + "?" + query.toString(), { method: "GET", headers, } ); setRecoveryAdminResult(payload); return payload; } async function issueRecoveryCode() { const targetUserId = getRecoveryTargetUserId(); if (!targetUserId) { throw new Error("Target Nextcloud user ID is required"); } const expiresMinutes = parsePositiveInteger( recoveryCodeExpiryMinutesInput.value, 15, 5, 60 ); recoveryCodeExpiryMinutesInput.value = String(expiresMinutes); const body = new URLSearchParams({ targetUserId, expiresMinutes: String(expiresMinutes), }); const payload = await requestJson(adminRecoveryIssueCodeUrl, { method: "POST", headers, body, }); setRecoveryAdminResult(payload); return payload; } async function startEmergencyRecovery() { const targetUserId = getRecoveryTargetUserId(); if (!targetUserId) { throw new Error("Target Nextcloud user ID is required"); } const durationHours = parsePositiveInteger( recoveryEmergencyHoursInput.value, 24, 1, 72 ); recoveryEmergencyHoursInput.value = String(durationHours); const body = new URLSearchParams({ targetUserId, durationHours: String(durationHours), }); const payload = await requestJson(adminRecoveryStartEmergencyUrl, { method: "POST", headers, body, }); setRecoveryAdminResult(payload); return payload; } renderQapps(); addQappButton.addEventListener("click", function () { (async function () { try { const action = addQapp(); await saveSettings(); setFeedback( action === "updated" ? "Q-App updated and saved." : "Q-App added and saved.", false ); } catch (error) { setFeedback(error.message || "Failed to add Q-App", true); } })(); }); if (cancelQappEditButton) { cancelQappEditButton.addEventListener("click", function () { exitQappEditMode(); setFeedback("Q-App edit canceled.", false); }); } if (qappsNonAdminRulesAddButton) { qappsNonAdminRulesAddButton.addEventListener("click", function () { qappsNonAdminRestrictionRules.push({ enabled: true, method: "POST", path: "/admin/apikey/generate", match: "prefix", action: "deny", }); renderQappsNonAdminRestrictionRules(); markDirty(); }); } 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); } }); if (saveAccessButton) { saveAccessButton.addEventListener("click", async function () { try { await saveSettings([ "sovereignMode", "nextcloudPublicUrl", "qappsEnabled", "qappsFullBrowserEnabled", "qappsDebugEnabled", "qappsAdvancedUnlockSettingsEnabled", "qappsNonAdminRestrictionsEnabled", "qappsNonAdminRestrictionRules", "userPublishingEnabled", "qappsList", "notifyEmailSubject", "notifyEmailBody", ]); } catch (error) { setFeedback(error.message || "Failed to save access settings", true); } }); } if (saveRuntimeButton) { saveRuntimeButton.addEventListener("click", async function () { try { await saveSettings([ "brokerBaseUrl", "brokerInternalApiToken", "externalAuthBaseUrl", "externalAuthAppId", "externalAuthAppSecret", "externalAuthNodeUrl", "externalAuthNodeApiKey", "externalAuthNodeApiKeyMode", "externalAuthNodeApiKeyPaths", "externalAuthDocsUrl", "qortalNodeUrl", "qortalNodeApiKey", "qortalGatewayUrl", "qortalGatewayAllowInsecure", ]); } catch (error) { setFeedback(error.message || "Failed to save runtime settings", true); } }); } if (saveIdentityButton) { saveIdentityButton.addEventListener("click", async function () { try { await saveSettings([ "oidcIssuerUrl", "oidcClientId", "oidcClientSecret", "oidcPolicyMode", "oidcAutoProvisionGuard", "oidcInviteTtlSeconds", "oidcRequireEmailForNewAccount", "oidcRedirectAllowlist", ]); } catch (error) { setFeedback(error.message || "Failed to save identity settings", true); } }); } if (saveMspButton) { saveMspButton.addEventListener("click", async function () { try { await saveSettings(["mspModeEnabled", "mspAdminGroup"]); } catch (error) { setFeedback( error.message || "Failed to save MSP delegation settings", true ); } }); } if (saveChdDashboardLinkButton) { saveChdDashboardLinkButton.addEventListener("click", async function () { try { await saveSettings(["chdAdminDashboardEnabled"]); } catch (error) { setFeedback( error.message || "Failed to save CHD dashboard link setting", true ); } }); } if (savePackagingButton) { savePackagingButton.addEventListener("click", async function () { try { await saveSettings([ "scPackageTier", "scAdvancedAdminEnabled", "scBillingManagedByChd", "scBillingProvider", "scBillingSyncMode", "scBillingContactName", "scBillingContactEmail", "scBillingCompanyName", "scBillingPhone", "scBillingAddress1", "scBillingAddress2", "scBillingCity", "scBillingState", "scBillingPostalCode", "scBillingCountry", "scAdminQortalAddress", "scChdBaseUrl", "scChdSyncPath", "scChdRegisterPath", "scChdCheckoutPath", "scChdCatalogPath", "scChdCatalogCategoryId", "scChdCatalogAudience", "scChdRegistrationToken", "scChdInstanceId", "scChdInstanceToken", "scChdInstanceSecret", ]); } catch (error) { setFeedback( error.message || "Failed to save billing connectivity settings", true ); } }); } if (testPackagingButton) { testPackagingButton.addEventListener("click", async function () { try { await saveSettings([ "scPackageTier", "scAdvancedAdminEnabled", "scBillingManagedByChd", "scBillingProvider", "scBillingSyncMode", "scBillingContactName", "scBillingContactEmail", "scBillingCompanyName", "scBillingPhone", "scBillingAddress1", "scBillingAddress2", "scBillingCity", "scBillingState", "scBillingPostalCode", "scBillingCountry", "scAdminQortalAddress", "scChdBaseUrl", "scChdSyncPath", "scChdRegisterPath", "scChdCheckoutPath", "scChdCatalogPath", "scChdCatalogCategoryId", "scChdCatalogAudience", "scChdRegistrationToken", "scChdInstanceId", "scChdInstanceToken", "scChdInstanceSecret", ]); await testBillingConnector(); } catch (error) { statusEl.textContent = JSON.stringify( { ok: false, action: "billing_test", error: error && error.message ? error.message : String(error), }, null, 2 ); setFeedback(error.message || "Failed to test billing connector", true); } }); } if (syncPackagingButton) { syncPackagingButton.addEventListener("click", async function () { try { await syncBillingEntitlements(); } catch (error) { setFeedback(error.message || "Failed to sync CHD entitlements", true); } }); } if (bootstrapPackagingButton) { bootstrapPackagingButton.addEventListener("click", async function () { try { await saveSettings([ "scPackageTier", "scAdvancedAdminEnabled", "scBillingManagedByChd", "scBillingProvider", "scBillingSyncMode", "scBillingContactName", "scBillingContactEmail", "scBillingCompanyName", "scBillingPhone", "scBillingAddress1", "scBillingAddress2", "scBillingCity", "scBillingState", "scBillingPostalCode", "scBillingCountry", "scAdminQortalAddress", "scChdBaseUrl", "scChdSyncPath", "scChdRegisterPath", "scChdCheckoutPath", "scChdCatalogPath", "scChdCatalogCategoryId", "scChdCatalogAudience", "scChdRegistrationToken", "scChdInstanceId", "scChdInstanceToken", "scChdInstanceSecret", "nextcloudPublicUrl", ]); await bootstrapBillingConnector(); } catch (error) { setFeedback( error.message || "Failed to connect installation to CHD", 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 onboarding messages...", false); const payload = await sendNotifications(); const summary = summarizeNotificationResult(payload); if (summary.hasFailures) { setFeedback( `Completed with issues. In-app: ${summary.inAppSent} sent / ${summary.inAppFailed} failed. Email: ${summary.emailSent} sent / ${summary.emailFailed} failed.`, true ); } else { setFeedback( `Onboarding messages sent. In-app: ${summary.inAppSent}. Email: ${summary.emailSent}.`, 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(); }); if (onboardToggleButton && onboardCloudBody) { onboardToggleButton.addEventListener("click", function () { setOnboardingVisibility(onboardCloudBody.hidden); }); } if (purchasesToggleButton && purchasesBody) { purchasesToggleButton.addEventListener("click", function () { purchasesManualToggle = true; setPurchasesVisibility(purchasesBody.hidden); }); } 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"), oidcIssuerInput, oidcClientIdInput, oidcClientSecretInput, nextcloudPublicUrlInput, notifyEmailSubject, notifyEmailBody, qappsEnabledInput, qappsBrowserEnabledInput, qappsDebugEnabledInput, qappsAdvancedUnlockSettingsEnabledInput, qappsNonAdminRestrictionsEnabledInput, userPublishingEnabledInput, scAdvancedAdminEnabledInput, mspModeEnabledInput, mspAdminGroupInput, chdAdminDashboardEnabledInput, scBillingManagedByChdInput, scBillingProviderInput, scBillingSyncModeInput, scPackageTierInput, scBillingContactNameInput, scBillingContactEmailInput, scBillingCompanyNameInput, scBillingPhoneInput, scBillingAddress1Input, scBillingAddress2Input, scBillingCityInput, scBillingStateInput, scBillingPostalCodeInput, scBillingCountryInput, scChdBaseUrlInput, scChdSyncPathInput, scChdRegisterPathInput, scChdCheckoutPathInput, scChdCatalogPathInput, scChdCatalogCategoryIdInput, scChdCatalogAudienceInput, scChdRegistrationTokenInput, scChdInstanceIdInput, scChdInstanceTokenInput, scChdInstanceSecretInput, sovereignModeInput, qappsIconModeInput, qappsIconUrlInput, qappsIconFromNameInput, ].forEach(function (el) { if (!el) { return; } el.addEventListener("change", function () { if (el === sovereignModeInput) { root.setAttribute( "data-mode-active", sovereignModeInput.value === "full_qortal" ? "full_qortal" : "powered" ); } markDirty(); if (el === qappsIconModeInput) { updateIconInputState(); } if (el === scAdvancedAdminEnabledInput) { updateBillingAdvancedState(); } if (el === scBillingManagedByChdInput) { setBillingAccountStatus(scAdminQortalAddressInput.value); } if (el === qappsDebugEnabledInput) { updatePurchasesNote(); } if (el === scPackageTierInput) { renderPurchases(); refreshPurchasesVisibilityDefault(); } if (el === userPublishingEnabledInput) { setOnboardingVisibility(userPublishingEnabledInput.checked); } }); 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 account", true); } }); createWalletLinkButton.addEventListener("click", async function () { try { await createWalletAndLink(); } catch (error) { setFeedback( error.message || "Failed to create account and link user", true ); } }); refreshWalletsButton.addEventListener("click", async function () { try { await refreshWallets(); setFeedback("Account list refreshed.", false); } catch (error) { setFeedback(error.message || "Failed to load accounts", 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); } }); idpApplyTemplateButton.addEventListener("click", function () { applySelectedOidcTemplate(); setFeedback("OIDC client template loaded.", false); }); idpSaveClientButton.addEventListener("click", async function () { try { await saveOidcClient(); setFeedback("OIDC client saved.", false); } catch (error) { setFeedback(error.message || "Failed to save OIDC client", true); } }); idpCancelEditButton.addEventListener("click", function () { resetOidcClientForm(); setFeedback("OIDC client edit cancelled.", false); }); idpRefreshClientsButton.addEventListener("click", async function () { try { await refreshOidcClients(); setFeedback("OIDC clients refreshed.", false); } catch (error) { setFeedback(error.message || "Failed to load OIDC clients", true); } }); idpRefreshAuditButton.addEventListener("click", async function () { try { await refreshOidcAuditEvents(); setFeedback("OIDC audit refreshed.", false); } catch (error) { setFeedback(error.message || "Failed to load OIDC audit", true); } }); idpCopySecretButton.addEventListener("click", async function () { try { await copyLatestOidcClientSecret(); setFeedback("Client secret copied.", false); } catch (error) { setFeedback(error.message || "Failed to copy client secret", true); } }); [ idpTemplateInput, idpClientIdInput, idpAllowedScopesInput, oidcIssuerInput, ].forEach(function (input) { input.addEventListener("input", renderOidcClientConfigSummary); input.addEventListener("change", renderOidcClientConfigSummary); }); recoveryStatusCheckButton.addEventListener("click", async function () { try { await checkRecoveryStatus(); setFeedback("Recovery status loaded.", false); } catch (error) { setFeedback(error.message || "Failed to load recovery status", true); } }); recoveryIssueCodeButton.addEventListener("click", async function () { try { await issueRecoveryCode(); setFeedback("One-time recovery code issued.", false); } catch (error) { setFeedback(error.message || "Failed to issue recovery code", true); } }); recoveryStartEmergencyButton.addEventListener("click", async function () { try { await startEmergencyRecovery(); setFeedback("Emergency temporary login reset started.", false); } catch (error) { setFeedback( error.message || "Failed to start emergency temporary login", true ); } }); inviteMessageButton.addEventListener("click", async function () { try { setFeedback("Generating invite message...", false); let token = ""; let inviteLink = ""; 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 : ""; inviteLink = (payload && payload.data && typeof payload.data.inviteLink === "string" && payload.data.inviteLink.trim()) || (invite && typeof invite.inviteLink === "string" ? invite.inviteLink.trim() : "") || (payload && typeof payload.inviteLink === "string" ? payload.inviteLink.trim() : ""); } const baseUrl = nextcloudPublicUrlInput.value.trim() || nextcloudPublicUrlValue || ""; const loginPath = "/login"; const inviteQuery = token ? `?invite_token=${encodeURIComponent(token)}` : ""; const fullLink = inviteLink || (baseUrl ? baseUrl.replace(/\/$/, "") + loginPath + inviteQuery : loginPath + inviteQuery); const poweredMode = sovereignModeInput.value !== "full_qortal"; const message = autoProvisionEnabled && autoProvisionGuardEnabled ? [ "Hello,", "", poweredMode ? "You have been invited to create a cloud account using Create Account / Login authentication." : "You have been invited to create a Nextcloud account using Qortal authentication.", "Open the invite link:", fullLink, "", token ? `Invite token (already embedded in the link): ${token}` : "Invite token: (admin to provide)", "", ...(poweredMode ? [ "Use the E-Mail address you used on the payment server so the account stays connected properly to the payment system.", "Create a new NuQloud account or import an existing NuQloud backup to complete setup.", ] : [ "Create new or import existing Qortal Account (seed phrase or backup) to complete setup.", ]), poweredMode ? "Your NuQloud account will auto-link to your new cloud user for providerless decentralized authentication." : "After linking, you can sign in to Nextcloud using your Qortal account.", ].join("\n") : [ "Hello,", "", poweredMode ? "Your admin enabled Create Account / Login for existing Nextcloud users." : "Your admin enabled Qortal login for existing Nextcloud users.", poweredMode ? "Open the NuQloud for Nextcloud settings to create or link your account:" : "Open the Qortal Integration settings to create or link your account:", 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); } }); (async function () { try { await refreshSetupData(); } catch (error) { setFeedback(error.message || "Initial setup data load failed", true); return; } try { await autoInitializeBilling(); } catch (error) { setFeedback( error.message || "Automatic billing initialization 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); }); renderOidcClientConfigSummary(); refreshOidcClients().catch(function (error) { setFeedback(error.message || "Initial OIDC client load failed", true); }); refreshOidcAuditEvents().catch(function (error) { setFeedback(error.message || "Initial OIDC audit load failed", true); }); })();