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