5979 lines
187 KiB
JavaScript
5979 lines
187 KiB
JavaScript
(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/<provider-callback>"],
|
|
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/<provider-slug>/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 =
|
|
'<tr><td colspan="3">No accounts visible to broker app credentials.</td></tr>';
|
|
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 =
|
|
'<tr><td colspan="6">No mappings found.</td></tr>';
|
|
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 =
|
|
'<tr><td colspan="4">No allowlisted addresses yet.</td></tr>';
|
|
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 =
|
|
'<tr><td colspan="5">No Q-Apps configured yet.</td></tr>';
|
|
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 =
|
|
'<tr><td colspan="6">No invites generated yet.</td></tr>';
|
|
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 =
|
|
'<tr><td colspan="6">No OIDC clients registered yet.</td></tr>';
|
|
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 =
|
|
'<tr><td colspan="4">No audit events yet.</td></tr>';
|
|
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 =
|
|
'<tr><td colspan="4">Enter a search term.</td></tr>';
|
|
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 =
|
|
'<tr><td colspan="4">No users found.</td></tr>';
|
|
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 =
|
|
'<tr><td colspan="3">Enter a search term.</td></tr>';
|
|
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 =
|
|
'<tr><td colspan="3">No groups found.</td></tr>';
|
|
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);
|
|
});
|
|
})();
|