1123 lines
37 KiB
JavaScript
1123 lines
37 KiB
JavaScript
(function () {
|
|
function init() {
|
|
const root = document.getElementById('qortal-personal-root');
|
|
if (!root) {
|
|
return false;
|
|
}
|
|
|
|
if (root.dataset.initialized === 'true') {
|
|
return true;
|
|
}
|
|
root.dataset.initialized = 'true';
|
|
|
|
const userMappingsUrl = root.dataset.userMappingsUrl;
|
|
const userCreateWalletUrl = root.dataset.userCreateWalletUrl;
|
|
const userBackupWalletUrl = root.dataset.userBackupWalletUrl;
|
|
const userImportSeedLinkUrl = root.dataset.userImportSeedLinkUrl;
|
|
const userImportBackupLinkUrl = root.dataset.userImportBackupLinkUrl;
|
|
const userRecoveryStatusUrl = root.dataset.userRecoveryStatusUrl;
|
|
const userRecoveryEnrollUrl = root.dataset.userRecoveryEnrollUrl;
|
|
const userRecoveryDisableUrl = root.dataset.userRecoveryDisableUrl;
|
|
const userRecoveryRevealUrl = root.dataset.userRecoveryRevealUrl;
|
|
const userRecoveryCompleteEmergencyUrl = root.dataset.userRecoveryCompleteEmergencyUrl;
|
|
const userUnlinkMappingUrl = root.dataset.userUnlinkMappingUrl;
|
|
const userQappsPrefsUrl = root.dataset.userQappsPrefsUrl;
|
|
const userQappsRulesUrl = root.dataset.userQappsRulesUrl;
|
|
const userQappsRuleRevokeUrl = root.dataset.userQappsRuleRevokeUrl;
|
|
const sovereignMode = root.dataset.sovereignMode === 'full_qortal' ? 'full_qortal' : 'powered';
|
|
const isPoweredMode = sovereignMode === 'powered';
|
|
const modeCopy = isPoweredMode
|
|
? {
|
|
title: 'NuQloud Account Link',
|
|
help: 'Import an existing account by seed phrase or backup JSON to link it with your profile.',
|
|
}
|
|
: {
|
|
title: 'Qortal Account Link',
|
|
help: 'Import an existing Qortal account by seed phrase or backup JSON to link it with your Nextcloud profile.',
|
|
};
|
|
const createWalletButton = document.getElementById('qortal-create-wallet-button');
|
|
const createPasswordEl = document.getElementById('qortal-create-password');
|
|
const createKdfEl = document.getElementById('qortal-create-kdf');
|
|
const createResultEl = document.getElementById('qortal-create-wallet-result');
|
|
const backupWalletSelectEl = document.getElementById('qortal-backup-wallet-select');
|
|
const backupWalletPasswordEl = document.getElementById('qortal-backup-wallet-password');
|
|
const backupReencryptToggleEl = document.getElementById('qortal-backup-reencrypt-toggle');
|
|
const backupReencryptFieldsEl = document.getElementById('qortal-backup-reencrypt-fields');
|
|
const backupNewPasswordEl = document.getElementById('qortal-backup-new-password');
|
|
const backupNewPasswordConfirmEl = document.getElementById('qortal-backup-new-password-confirm');
|
|
const backupDownloadEl = document.getElementById('qortal-backup-download');
|
|
const backupSaveEl = document.getElementById('qortal-backup-save');
|
|
const backupWalletButton = document.getElementById('qortal-backup-wallet-button');
|
|
const backupResultEl = document.getElementById('qortal-backup-result');
|
|
const recoveryWalletSelectEl = document.getElementById('qortal-recovery-wallet-select');
|
|
const recoveryCurrentPasswordEl = document.getElementById('qortal-recovery-current-password');
|
|
const recoveryPassphraseEl = document.getElementById('qortal-recovery-passphrase');
|
|
const recoveryPassphraseConfirmEl = document.getElementById('qortal-recovery-passphrase-confirm');
|
|
const recoveryEnableManagedEl = document.getElementById('qortal-recovery-enable-managed');
|
|
const recoveryEnableEmergencyLoginEl = document.getElementById('qortal-recovery-enable-emergency-login');
|
|
const recoverySaveButton = document.getElementById('qortal-recovery-save-button');
|
|
const recoveryDisableButton = document.getElementById('qortal-recovery-disable-button');
|
|
const recoveryAdminCodeEl = document.getElementById('qortal-recovery-admin-code');
|
|
const recoveryRevealButton = document.getElementById('qortal-recovery-reveal-button');
|
|
const recoveryCompleteEmergencyButton = document.getElementById('qortal-recovery-complete-emergency-button');
|
|
const recoveryStatusEl = document.getElementById('qortal-recovery-status');
|
|
const recoveryResultEl = document.getElementById('qortal-recovery-result');
|
|
const importButton = document.getElementById('qortal-import-link');
|
|
const importBackupButton = document.getElementById('qortal-import-backup-link');
|
|
const refreshMappingsButton = document.getElementById('qortal-refresh-user-mappings');
|
|
const seedPhraseEl = document.getElementById('qortal-seed-phrase');
|
|
const walletPasswordEl = document.getElementById('qortal-wallet-password');
|
|
const backupFileEl = document.getElementById('qortal-backup-file');
|
|
const backupPasswordEl = document.getElementById('qortal-backup-password');
|
|
const feedbackEl = document.getElementById('qortal-personal-feedback');
|
|
const successEl = document.getElementById('qortal-personal-success');
|
|
const resultEl = document.getElementById('qortal-personal-result');
|
|
const mappingsBody = document.getElementById('qortal-user-mappings-body');
|
|
const defaultApprovalModeEl = document.getElementById('qortal-default-approval-mode');
|
|
const defaultApprovalTempMinutesEl = document.getElementById('qortal-default-approval-temp-minutes');
|
|
const defaultApprovalUnlock10MinEl = document.getElementById('qortal-default-approval-unlock-10-min');
|
|
const defaultUnlockSession20MinEl = document.getElementById('qortal-default-unlock-session-20-min');
|
|
const defaultTalkBridgeAutoApproveSendChatMessageEl = document.getElementById('qortal-default-talk-bridge-auto-approve-send-chat-message');
|
|
const highRiskConfirmationEnabledEl = document.getElementById('qortal-high-risk-confirmation-enabled');
|
|
const saveApprovalDefaultsButton = document.getElementById('qortal-save-approval-defaults');
|
|
const resetApprovalDefaultsButton = document.getElementById('qortal-reset-approval-defaults');
|
|
const approvalDefaultsResultEl = document.getElementById('qortal-approval-defaults-result');
|
|
const refreshApprovalRulesButton = document.getElementById('qortal-refresh-approval-rules');
|
|
const approvalRulesBody = document.getElementById('qortal-user-approval-rules-body');
|
|
const approvalRulesResultEl = document.getElementById('qortal-approval-rules-result');
|
|
const titleEl = document.getElementById('qortal-personal-title');
|
|
const helpEl = document.getElementById('qortal-personal-help');
|
|
const fullOnlySections = root.querySelectorAll('.qortal-full-only');
|
|
|
|
if (
|
|
!userMappingsUrl ||
|
|
!userCreateWalletUrl ||
|
|
!userBackupWalletUrl ||
|
|
!userImportSeedLinkUrl ||
|
|
!userImportBackupLinkUrl ||
|
|
!userRecoveryStatusUrl ||
|
|
!userRecoveryEnrollUrl ||
|
|
!userRecoveryDisableUrl ||
|
|
!userRecoveryRevealUrl ||
|
|
!userRecoveryCompleteEmergencyUrl ||
|
|
!userUnlinkMappingUrl ||
|
|
!userQappsPrefsUrl ||
|
|
!userQappsRulesUrl ||
|
|
!userQappsRuleRevokeUrl ||
|
|
!createWalletButton ||
|
|
!createPasswordEl ||
|
|
!createKdfEl ||
|
|
!createResultEl ||
|
|
!backupWalletSelectEl ||
|
|
!backupWalletPasswordEl ||
|
|
!backupReencryptToggleEl ||
|
|
!backupReencryptFieldsEl ||
|
|
!backupNewPasswordEl ||
|
|
!backupNewPasswordConfirmEl ||
|
|
!backupDownloadEl ||
|
|
!backupSaveEl ||
|
|
!backupWalletButton ||
|
|
!backupResultEl ||
|
|
!recoveryWalletSelectEl ||
|
|
!recoveryCurrentPasswordEl ||
|
|
!recoveryPassphraseEl ||
|
|
!recoveryPassphraseConfirmEl ||
|
|
!recoveryEnableManagedEl ||
|
|
!recoveryEnableEmergencyLoginEl ||
|
|
!recoverySaveButton ||
|
|
!recoveryDisableButton ||
|
|
!recoveryAdminCodeEl ||
|
|
!recoveryRevealButton ||
|
|
!recoveryCompleteEmergencyButton ||
|
|
!recoveryStatusEl ||
|
|
!recoveryResultEl ||
|
|
!importButton ||
|
|
!importBackupButton ||
|
|
!refreshMappingsButton ||
|
|
!seedPhraseEl ||
|
|
!walletPasswordEl ||
|
|
!backupFileEl ||
|
|
!backupPasswordEl ||
|
|
!feedbackEl ||
|
|
!successEl ||
|
|
!resultEl ||
|
|
!mappingsBody ||
|
|
!defaultApprovalModeEl ||
|
|
!defaultApprovalTempMinutesEl ||
|
|
!defaultApprovalUnlock10MinEl ||
|
|
!defaultUnlockSession20MinEl ||
|
|
!defaultTalkBridgeAutoApproveSendChatMessageEl ||
|
|
!highRiskConfirmationEnabledEl ||
|
|
!saveApprovalDefaultsButton ||
|
|
!resetApprovalDefaultsButton ||
|
|
!approvalDefaultsResultEl ||
|
|
!refreshApprovalRulesButton ||
|
|
!approvalRulesBody ||
|
|
!approvalRulesResultEl
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
const headers = {
|
|
requesttoken: OC.requestToken,
|
|
};
|
|
let availableWalletMappings = [];
|
|
let preferredBackupWalletId = '';
|
|
|
|
function applyModeCopy() {
|
|
if (titleEl) {
|
|
titleEl.textContent = modeCopy.title;
|
|
}
|
|
if (helpEl) {
|
|
helpEl.textContent = modeCopy.help;
|
|
}
|
|
if (isPoweredMode) {
|
|
fullOnlySections.forEach(function (section) {
|
|
if (section && section.classList) {
|
|
section.classList.add('qortal-hidden');
|
|
}
|
|
});
|
|
}
|
|
root.setAttribute('data-mode-active', sovereignMode);
|
|
}
|
|
|
|
function setFeedback(message, isError) {
|
|
feedbackEl.textContent = message;
|
|
feedbackEl.classList.toggle('error', Boolean(isError));
|
|
feedbackEl.classList.toggle('success', !isError && message.length > 0);
|
|
}
|
|
|
|
function setSuccess(message) {
|
|
successEl.textContent = message || '';
|
|
}
|
|
|
|
function setApprovalDefaultsResult(message, isError) {
|
|
approvalDefaultsResultEl.textContent = message || '';
|
|
approvalDefaultsResultEl.classList.toggle('error', Boolean(isError));
|
|
}
|
|
|
|
function setApprovalRulesResult(message, isError) {
|
|
approvalRulesResultEl.textContent = message || '';
|
|
approvalRulesResultEl.classList.toggle('error', Boolean(isError));
|
|
}
|
|
|
|
function normalizeApprovalTempMinutes(value) {
|
|
const parsed = Number(String(value || '').trim());
|
|
if (!Number.isFinite(parsed)) {
|
|
return 120;
|
|
}
|
|
const allowed = [60, 120, 180, 360, 720, 1440, 7200];
|
|
const normalizedValue = Math.floor(parsed);
|
|
return allowed.includes(normalizedValue) ? normalizedValue : 120;
|
|
}
|
|
|
|
function applyApprovalDefaultsFromDataset() {
|
|
const unlockedMinutes = normalizeApprovalTempMinutes(root.dataset.userQappsApprovalTempMinutes || '120');
|
|
defaultApprovalModeEl.value = String(unlockedMinutes);
|
|
defaultApprovalTempMinutesEl.value = String(unlockedMinutes);
|
|
defaultApprovalUnlock10MinEl.checked = root.dataset.userQappsApprovalUnlock10Min === '1';
|
|
defaultUnlockSession20MinEl.checked = root.dataset.userQappsUnlockSession20Min === '1';
|
|
defaultTalkBridgeAutoApproveSendChatMessageEl.checked =
|
|
root.dataset.userTalkBridgeAutoApproveSendChatMessage === '1';
|
|
highRiskConfirmationEnabledEl.checked =
|
|
root.dataset.userQappsHighRiskConfirmationEnabled !== '0';
|
|
setApprovalDefaultsResult('Current unlock window loaded. Save changes to apply for future prompts.', false);
|
|
}
|
|
|
|
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 extractBackupObject(payload) {
|
|
let candidate = payload;
|
|
for (let i = 0; i < 4; i++) {
|
|
if (candidate && typeof candidate === 'object' && candidate.data && typeof candidate.data === 'object') {
|
|
candidate = candidate.data;
|
|
continue;
|
|
}
|
|
if (candidate && typeof candidate === 'object' && candidate.backup !== undefined) {
|
|
candidate = candidate.backup;
|
|
if (typeof candidate === 'string') {
|
|
try {
|
|
candidate = JSON.parse(candidate);
|
|
} catch (_error) {
|
|
candidate = null;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
return candidate && typeof candidate === 'object' ? candidate : null;
|
|
}
|
|
|
|
function formatTimestamp(value) {
|
|
if (!value) {
|
|
return '';
|
|
}
|
|
try {
|
|
return new Date(value).toLocaleString();
|
|
} catch (error) {
|
|
return String(value);
|
|
}
|
|
}
|
|
|
|
function formatApprovalMode(value) {
|
|
const mode = String(value || '').trim().toLowerCase();
|
|
if (mode === 'persistent') {
|
|
return 'Always allow';
|
|
}
|
|
if (mode === 'session') {
|
|
return 'Allow for session';
|
|
}
|
|
return mode || '-';
|
|
}
|
|
|
|
function downloadJsonFile(filename, data) {
|
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
const anchor = document.createElement('a');
|
|
anchor.href = url;
|
|
anchor.download = filename;
|
|
document.body.appendChild(anchor);
|
|
anchor.click();
|
|
anchor.remove();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
function truncateText(value, maxLength) {
|
|
const text = String(value || '');
|
|
if (text.length <= maxLength) {
|
|
return text;
|
|
}
|
|
return text.slice(0, Math.max(0, maxLength - 1)) + '…';
|
|
}
|
|
|
|
function resolveMappingDisplayName(mapping) {
|
|
if (!mapping || typeof mapping !== 'object') {
|
|
return '';
|
|
}
|
|
const primaryName = String(
|
|
mapping.primaryName || mapping.registeredName || mapping.name || mapping.qortalName || ''
|
|
).trim();
|
|
if (primaryName) {
|
|
return primaryName;
|
|
}
|
|
const address = String(mapping.qortalAddress || '').trim();
|
|
return address;
|
|
}
|
|
|
|
function getBackupWalletOptions() {
|
|
const options = [];
|
|
const seenWalletIds = new Set();
|
|
|
|
availableWalletMappings.forEach(function (mapping) {
|
|
if (!mapping || typeof mapping !== 'object') {
|
|
return;
|
|
}
|
|
const walletId = String(mapping.walletId || '').trim();
|
|
if (!walletId || seenWalletIds.has(walletId)) {
|
|
return;
|
|
}
|
|
seenWalletIds.add(walletId);
|
|
const address = String(mapping.qortalAddress || '').trim();
|
|
const status = String(mapping.status || '').trim();
|
|
const displayName = resolveMappingDisplayName(mapping);
|
|
const displayAddress = address ? truncateText(address, 18) : '';
|
|
const labelBase = displayName || (displayAddress || 'linked account');
|
|
const labelWithAddress = displayName && displayAddress && displayName !== address
|
|
? (labelBase + ' • ' + displayAddress)
|
|
: labelBase;
|
|
options.push({
|
|
walletId: walletId,
|
|
address: address,
|
|
label: labelWithAddress + (status ? ' • ' + status : ''),
|
|
});
|
|
});
|
|
|
|
return options;
|
|
}
|
|
|
|
function refreshBackupWalletSelect() {
|
|
const options = getBackupWalletOptions();
|
|
backupWalletSelectEl.innerHTML = '';
|
|
|
|
if (options.length === 0) {
|
|
const emptyOption = document.createElement('option');
|
|
emptyOption.value = '';
|
|
emptyOption.textContent = 'No linked accounts available';
|
|
backupWalletSelectEl.appendChild(emptyOption);
|
|
return options;
|
|
}
|
|
|
|
options.forEach(function (entry) {
|
|
const optionEl = document.createElement('option');
|
|
optionEl.value = entry.walletId;
|
|
optionEl.textContent = entry.label;
|
|
if (preferredBackupWalletId && preferredBackupWalletId === entry.walletId) {
|
|
optionEl.selected = true;
|
|
}
|
|
backupWalletSelectEl.appendChild(optionEl);
|
|
});
|
|
|
|
if (!preferredBackupWalletId) {
|
|
backupWalletSelectEl.selectedIndex = 0;
|
|
preferredBackupWalletId = backupWalletSelectEl.value;
|
|
return options;
|
|
}
|
|
if (backupWalletSelectEl.selectedIndex < 0) {
|
|
backupWalletSelectEl.selectedIndex = 0;
|
|
preferredBackupWalletId = backupWalletSelectEl.value;
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
function refreshRecoveryWalletSelect() {
|
|
const options = getBackupWalletOptions();
|
|
const previousSelectedWalletId = String(recoveryWalletSelectEl.value || '').trim();
|
|
recoveryWalletSelectEl.innerHTML = '';
|
|
|
|
if (options.length === 0) {
|
|
const emptyOption = document.createElement('option');
|
|
emptyOption.value = '';
|
|
emptyOption.textContent = 'No linked accounts available';
|
|
recoveryWalletSelectEl.appendChild(emptyOption);
|
|
return;
|
|
}
|
|
|
|
options.forEach(function (entry) {
|
|
const optionEl = document.createElement('option');
|
|
optionEl.value = entry.walletId;
|
|
optionEl.textContent = entry.label;
|
|
if (previousSelectedWalletId && previousSelectedWalletId === entry.walletId) {
|
|
optionEl.selected = true;
|
|
}
|
|
recoveryWalletSelectEl.appendChild(optionEl);
|
|
});
|
|
if (recoveryWalletSelectEl.selectedIndex < 0) {
|
|
recoveryWalletSelectEl.selectedIndex = 0;
|
|
}
|
|
}
|
|
|
|
function setRecoveryResult(message, isError) {
|
|
recoveryResultEl.textContent = message || '';
|
|
recoveryResultEl.classList.toggle('error', Boolean(isError));
|
|
}
|
|
|
|
function renderRecoveryStatus(status) {
|
|
const data = status && typeof status === 'object' ? status : {};
|
|
const managedEnabled = Boolean(data.managedEnabled);
|
|
const emergencyLoginEnabled = Boolean(data.emergencyLoginEnabled);
|
|
const lockActive = Boolean(data.lockActive);
|
|
const emergencyRequired = Boolean(data.emergencyRequired);
|
|
const emergencyActive = Boolean(data.emergencyActive);
|
|
const walletId = String(data.walletId || '').trim();
|
|
const updatedAt = String(data.updatedAt || '').trim();
|
|
const lockedUntil = String(data.lockedUntil || '').trim();
|
|
const emergencyUntil = String(data.emergencyUntil || '').trim();
|
|
|
|
recoveryEnableManagedEl.checked = managedEnabled;
|
|
recoveryEnableEmergencyLoginEl.checked = emergencyLoginEnabled;
|
|
if (walletId) {
|
|
const option = Array.from(recoveryWalletSelectEl.options).find(function (opt) {
|
|
return String(opt.value || '').trim() === walletId;
|
|
});
|
|
if (option) {
|
|
recoveryWalletSelectEl.value = walletId;
|
|
}
|
|
}
|
|
|
|
const lines = [];
|
|
lines.push(managedEnabled ? 'Managed recovery is enabled.' : 'Managed recovery is disabled.');
|
|
if (updatedAt) {
|
|
lines.push('Escrow updated: ' + formatTimestamp(updatedAt));
|
|
}
|
|
if (lockActive && lockedUntil) {
|
|
lines.push('Recovery locked until: ' + formatTimestamp(lockedUntil));
|
|
}
|
|
if (emergencyRequired) {
|
|
lines.push('Emergency reset active. Replace backup password now.');
|
|
} else if (emergencyActive && emergencyUntil) {
|
|
lines.push('Emergency window active until: ' + formatTimestamp(emergencyUntil));
|
|
}
|
|
recoveryStatusEl.textContent = lines.join(' ');
|
|
}
|
|
|
|
function syncBackupReencryptFields() {
|
|
const showFields = backupReencryptToggleEl.checked;
|
|
backupReencryptFieldsEl.classList.toggle('qortal-hidden', !showFields);
|
|
if (!showFields) {
|
|
backupNewPasswordEl.value = '';
|
|
backupNewPasswordConfirmEl.value = '';
|
|
}
|
|
}
|
|
|
|
function renderMappings(payload) {
|
|
const mappings = payload && payload.ok && payload.data && Array.isArray(payload.data.mappings)
|
|
? payload.data.mappings
|
|
: [];
|
|
availableWalletMappings = mappings;
|
|
const previousSelectedWalletId = String(backupWalletSelectEl.value || '').trim();
|
|
if (!preferredBackupWalletId && previousSelectedWalletId) {
|
|
preferredBackupWalletId = previousSelectedWalletId;
|
|
}
|
|
refreshBackupWalletSelect();
|
|
refreshRecoveryWalletSelect();
|
|
|
|
mappingsBody.innerHTML = '';
|
|
|
|
if (mappings.length === 0) {
|
|
mappingsBody.innerHTML = '<tr><td colspan="5">No linked Qortal accounts yet.</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 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 = 'Remove';
|
|
unlinkButton.addEventListener('click', async function () {
|
|
try {
|
|
await unlinkMapping(mapping.qortalAddress);
|
|
setFeedback('Linked account removed.', false);
|
|
} catch (error) {
|
|
setFeedback(error.message || 'Failed to remove linked account', true);
|
|
}
|
|
});
|
|
actionCell.appendChild(unlinkButton);
|
|
|
|
row.appendChild(addressCell);
|
|
row.appendChild(walletCell);
|
|
row.appendChild(statusCell);
|
|
row.appendChild(updatedCell);
|
|
row.appendChild(actionCell);
|
|
mappingsBody.appendChild(row);
|
|
});
|
|
}
|
|
|
|
function renderApprovalRules(payload) {
|
|
const data = payload && payload.data && typeof payload.data === 'object' ? payload.data : payload;
|
|
const rules = data && Array.isArray(data.rules) ? data.rules : [];
|
|
approvalRulesBody.innerHTML = '';
|
|
|
|
if (rules.length === 0) {
|
|
approvalRulesBody.innerHTML = '<tr><td colspan="6">No active approval rules.</td></tr>';
|
|
return;
|
|
}
|
|
|
|
rules.forEach(function (rule) {
|
|
const row = document.createElement('tr');
|
|
const modeCell = document.createElement('td');
|
|
modeCell.textContent = formatApprovalMode(rule.mode);
|
|
|
|
const appCell = document.createElement('td');
|
|
appCell.textContent = rule.app || '-';
|
|
|
|
const scopeCell = document.createElement('td');
|
|
const scopeCode = document.createElement('code');
|
|
scopeCode.textContent = rule.scope || '';
|
|
scopeCell.appendChild(scopeCode);
|
|
|
|
const requestTypeCell = document.createElement('td');
|
|
requestTypeCell.textContent = rule.requestType || '-';
|
|
|
|
const updatedCell = document.createElement('td');
|
|
updatedCell.textContent = formatTimestamp(rule.updatedAt);
|
|
|
|
const actionCell = document.createElement('td');
|
|
const revokeButton = document.createElement('button');
|
|
revokeButton.className = 'button';
|
|
revokeButton.textContent = 'Remove';
|
|
revokeButton.addEventListener('click', async function () {
|
|
try {
|
|
await revokeApprovalRule(rule);
|
|
setApprovalRulesResult('Rule removed.', false);
|
|
} catch (error) {
|
|
setApprovalRulesResult(error.message || 'Failed to remove rule', true);
|
|
}
|
|
});
|
|
actionCell.appendChild(revokeButton);
|
|
|
|
row.appendChild(modeCell);
|
|
row.appendChild(appCell);
|
|
row.appendChild(scopeCell);
|
|
row.appendChild(requestTypeCell);
|
|
row.appendChild(updatedCell);
|
|
row.appendChild(actionCell);
|
|
approvalRulesBody.appendChild(row);
|
|
});
|
|
}
|
|
|
|
async function refreshMappings() {
|
|
const payload = await requestJson(userMappingsUrl, {
|
|
method: 'GET',
|
|
headers,
|
|
});
|
|
assertBrokerOk(payload, 'Failed to fetch linked accounts');
|
|
renderMappings(payload);
|
|
}
|
|
|
|
async function loadRecoveryStatus() {
|
|
const payload = await requestJson(userRecoveryStatusUrl, {
|
|
method: 'GET',
|
|
headers,
|
|
});
|
|
const data = payload && payload.data ? payload.data : payload;
|
|
renderRecoveryStatus(data);
|
|
return data;
|
|
}
|
|
|
|
async function saveRecoverySettings() {
|
|
const walletId = String(recoveryWalletSelectEl.value || '').trim();
|
|
const currentBackupPassword = recoveryCurrentPasswordEl.value.trim();
|
|
const recoveryPassphrase = recoveryPassphraseEl.value.trim();
|
|
const recoveryPassphraseConfirm = recoveryPassphraseConfirmEl.value.trim();
|
|
const managedEnabled = recoveryEnableManagedEl.checked;
|
|
const emergencyLoginEnabled = recoveryEnableEmergencyLoginEl.checked;
|
|
|
|
if (!managedEnabled) {
|
|
throw new Error('Enable managed recovery to save settings');
|
|
}
|
|
if (!walletId) {
|
|
throw new Error('Select a linked account');
|
|
}
|
|
if (!currentBackupPassword) {
|
|
throw new Error('Current backup password is required');
|
|
}
|
|
if (!recoveryPassphrase || !recoveryPassphraseConfirm) {
|
|
throw new Error('Recovery passphrase and confirmation are required');
|
|
}
|
|
if (recoveryPassphrase !== recoveryPassphraseConfirm) {
|
|
throw new Error('Recovery passphrase fields must match');
|
|
}
|
|
if (recoveryPassphrase.length < 12) {
|
|
throw new Error('Recovery passphrase must be at least 12 characters');
|
|
}
|
|
|
|
const body = new URLSearchParams({
|
|
walletId,
|
|
currentBackupPassword,
|
|
recoveryPassphrase,
|
|
recoveryPassphraseConfirm,
|
|
managedEnabled: managedEnabled ? '1' : '',
|
|
emergencyLoginEnabled: emergencyLoginEnabled ? '1' : '',
|
|
});
|
|
|
|
const payload = await requestJson(userRecoveryEnrollUrl, {
|
|
method: 'POST',
|
|
headers,
|
|
body,
|
|
});
|
|
const data = payload && payload.data ? payload.data : payload;
|
|
renderRecoveryStatus(data);
|
|
recoveryCurrentPasswordEl.value = '';
|
|
recoveryPassphraseEl.value = '';
|
|
recoveryPassphraseConfirmEl.value = '';
|
|
setRecoveryResult('Recovery settings saved.', false);
|
|
}
|
|
|
|
async function disableRecoverySettings() {
|
|
const payload = await requestJson(userRecoveryDisableUrl, {
|
|
method: 'POST',
|
|
headers,
|
|
body: new URLSearchParams({ disable: '1' }),
|
|
});
|
|
const data = payload && payload.data ? payload.data : payload;
|
|
renderRecoveryStatus(data);
|
|
setRecoveryResult('Managed recovery disabled.', false);
|
|
}
|
|
|
|
async function revealRecoveryPassword() {
|
|
const recoveryPassphrase = recoveryPassphraseEl.value.trim();
|
|
const adminCode = recoveryAdminCodeEl.value.trim();
|
|
if (!recoveryPassphrase) {
|
|
throw new Error('Recovery passphrase is required');
|
|
}
|
|
if (!adminCode) {
|
|
throw new Error('Admin recovery code is required');
|
|
}
|
|
|
|
const body = new URLSearchParams({
|
|
recoveryPassphrase,
|
|
adminCode,
|
|
});
|
|
const payload = await requestJson(userRecoveryRevealUrl, {
|
|
method: 'POST',
|
|
headers,
|
|
body,
|
|
});
|
|
const data = payload && payload.data ? payload.data : payload;
|
|
const backupPassword = data && data.backupPassword ? String(data.backupPassword) : '';
|
|
if (!backupPassword) {
|
|
throw new Error('Recovery returned no password');
|
|
}
|
|
if (data && data.recoveryStatus) {
|
|
renderRecoveryStatus(data.recoveryStatus);
|
|
}
|
|
setRecoveryResult('Recovered backup password: ' + backupPassword, false);
|
|
recoveryAdminCodeEl.value = '';
|
|
}
|
|
|
|
async function completeEmergencyRecovery() {
|
|
const payload = await requestJson(userRecoveryCompleteEmergencyUrl, {
|
|
method: 'POST',
|
|
headers,
|
|
body: new URLSearchParams({ complete: '1' }),
|
|
});
|
|
const data = payload && payload.data ? payload.data : payload;
|
|
renderRecoveryStatus(data);
|
|
setRecoveryResult('Emergency reset status cleared.', false);
|
|
}
|
|
|
|
async function refreshApprovalRules() {
|
|
const payload = await requestJson(userQappsRulesUrl, {
|
|
method: 'GET',
|
|
headers,
|
|
});
|
|
assertBrokerOk(payload, 'Failed to fetch approval rules');
|
|
renderApprovalRules(payload);
|
|
}
|
|
|
|
async function importAndLink() {
|
|
const seedPhrase = seedPhraseEl.value.trim();
|
|
const password = walletPasswordEl.value.trim();
|
|
|
|
if (!seedPhrase || !password) {
|
|
throw new Error('Seed phrase and account password are required');
|
|
}
|
|
|
|
setFeedback('Importing account and linking profile...', false);
|
|
setSuccess('');
|
|
|
|
const body = new URLSearchParams({
|
|
seedPhrase,
|
|
password,
|
|
});
|
|
|
|
const payload = await requestJson(userImportSeedLinkUrl, {
|
|
method: 'POST',
|
|
headers,
|
|
body,
|
|
});
|
|
assertBrokerOk(payload, 'Import/link failed');
|
|
|
|
resultEl.textContent = JSON.stringify(payload, null, 2);
|
|
seedPhraseEl.value = '';
|
|
walletPasswordEl.value = '';
|
|
await refreshMappings();
|
|
setFeedback('Account imported and linked.', false);
|
|
setSuccess('Account linked. You can now authenticate to Nextcloud using your Qortal account.');
|
|
}
|
|
|
|
async function createWallet() {
|
|
if (createWalletButton.disabled) {
|
|
return;
|
|
}
|
|
const password = createPasswordEl.value.trim();
|
|
const kdfThreads = createKdfEl.value.trim();
|
|
|
|
if (!password) {
|
|
throw new Error('Account password is required');
|
|
}
|
|
|
|
createWalletButton.disabled = true;
|
|
setFeedback('Creating account...', false);
|
|
setSuccess('');
|
|
|
|
const body = new URLSearchParams({
|
|
password,
|
|
kdfThreads: kdfThreads || '',
|
|
});
|
|
|
|
try {
|
|
const payload = await requestJson(userCreateWalletUrl, {
|
|
method: 'POST',
|
|
headers,
|
|
body,
|
|
});
|
|
assertBrokerOk(payload, 'Account creation failed');
|
|
|
|
createResultEl.textContent = JSON.stringify(payload, null, 2);
|
|
const wallet = payload.wallet || payload.data || payload;
|
|
if (wallet && wallet.walletId) {
|
|
preferredBackupWalletId = String(wallet.walletId).trim();
|
|
}
|
|
if (!backupWalletPasswordEl.value) {
|
|
backupWalletPasswordEl.value = password;
|
|
}
|
|
await refreshMappings();
|
|
setFeedback('Account created and linked.', false);
|
|
setSuccess('Account created. You can now authenticate to Nextcloud using your Qortal account.');
|
|
} finally {
|
|
createWalletButton.disabled = false;
|
|
}
|
|
}
|
|
|
|
async function backupWallet() {
|
|
const walletId = backupWalletSelectEl.value.trim();
|
|
const password = backupWalletPasswordEl.value.trim();
|
|
const reEncryptEnabled = backupReencryptToggleEl.checked;
|
|
const reEncryptPassword = reEncryptEnabled ? backupNewPasswordEl.value.trim() : '';
|
|
const newPassword = reEncryptEnabled ? backupNewPasswordConfirmEl.value.trim() : '';
|
|
const download = backupDownloadEl.checked;
|
|
const saveToFiles = backupSaveEl.checked;
|
|
|
|
if (!walletId) {
|
|
throw new Error('Linked account is required');
|
|
}
|
|
if (!password) {
|
|
throw new Error('Current backup password is required');
|
|
}
|
|
if (reEncryptEnabled && !reEncryptPassword) {
|
|
throw new Error('Enter a new backup password or disable re-encrypt');
|
|
}
|
|
if (reEncryptEnabled && !newPassword) {
|
|
throw new Error('Confirm the new backup password');
|
|
}
|
|
if (reEncryptEnabled && reEncryptPassword !== newPassword) {
|
|
throw new Error('New backup password fields must match');
|
|
}
|
|
if (!download && !saveToFiles) {
|
|
throw new Error('Select at least one backup option');
|
|
}
|
|
|
|
setFeedback('Creating account backup...', false);
|
|
setSuccess('');
|
|
|
|
const body = new URLSearchParams({
|
|
walletId,
|
|
password,
|
|
saveToFiles: saveToFiles ? '1' : '',
|
|
});
|
|
if (reEncryptEnabled) {
|
|
body.set('reEncryptPassword', reEncryptPassword);
|
|
body.set('newPassword', newPassword);
|
|
}
|
|
|
|
const payload = await requestJson(userBackupWalletUrl, {
|
|
method: 'POST',
|
|
headers,
|
|
body,
|
|
});
|
|
assertBrokerOk(payload, 'Backup failed');
|
|
|
|
backupResultEl.textContent = JSON.stringify(payload, null, 2);
|
|
const backupData = extractBackupObject(payload);
|
|
if (download && backupData) {
|
|
const filename = `qortal-account-${walletId}.json`;
|
|
downloadJsonFile(filename, backupData);
|
|
}
|
|
if (saveToFiles && payload.savedPath) {
|
|
setSuccess(`Backup saved to Nextcloud Files: ${payload.savedPath}`);
|
|
}
|
|
setFeedback('Backup completed.', false);
|
|
}
|
|
|
|
async function importBackupAndLink() {
|
|
const password = backupPasswordEl.value.trim();
|
|
if (!password) {
|
|
throw new Error('Backup password is required');
|
|
}
|
|
|
|
if (!backupFileEl.files || backupFileEl.files.length === 0) {
|
|
throw new Error('Backup JSON file is required');
|
|
}
|
|
|
|
const file = backupFileEl.files[0];
|
|
const text = await file.text();
|
|
let parsedBackup;
|
|
try {
|
|
parsedBackup = JSON.parse(text);
|
|
} catch (error) {
|
|
throw new Error('Backup file is not valid JSON');
|
|
}
|
|
|
|
if (!parsedBackup || typeof parsedBackup !== 'object') {
|
|
throw new Error('Backup file must contain a JSON object');
|
|
}
|
|
const backupData = extractBackupObject(parsedBackup);
|
|
if (!backupData) {
|
|
throw new Error('Backup file does not contain a valid backup object');
|
|
}
|
|
|
|
setFeedback('Importing backup and linking account...', false);
|
|
setSuccess('');
|
|
|
|
const body = new URLSearchParams({
|
|
backupJson: JSON.stringify(backupData),
|
|
password,
|
|
});
|
|
|
|
const payload = await requestJson(userImportBackupLinkUrl, {
|
|
method: 'POST',
|
|
headers,
|
|
body,
|
|
});
|
|
assertBrokerOk(payload, 'Import/link failed');
|
|
|
|
resultEl.textContent = JSON.stringify(payload, null, 2);
|
|
backupFileEl.value = '';
|
|
backupPasswordEl.value = '';
|
|
await refreshMappings();
|
|
setFeedback('Backup imported and linked.', false);
|
|
setSuccess('Account linked. You can now authenticate to Nextcloud using your Qortal account.');
|
|
}
|
|
|
|
async function unlinkMapping(qortalAddress) {
|
|
if (!qortalAddress) {
|
|
throw new Error('qortalAddress is required');
|
|
}
|
|
|
|
const body = new URLSearchParams({
|
|
qortalAddress: qortalAddress,
|
|
});
|
|
|
|
const payload = await requestJson(userUnlinkMappingUrl, {
|
|
method: 'POST',
|
|
headers,
|
|
body,
|
|
});
|
|
assertBrokerOk(payload, 'Unlink failed');
|
|
resultEl.textContent = JSON.stringify(payload, null, 2);
|
|
await refreshMappings();
|
|
}
|
|
|
|
async function revokeApprovalRule(rule) {
|
|
const walletId = rule && typeof rule.walletId === 'string' ? rule.walletId.trim() : '';
|
|
const scope = rule && typeof rule.scope === 'string' ? rule.scope.trim() : '';
|
|
const app = rule && typeof rule.app === 'string' ? rule.app.trim() : '';
|
|
const requestType = rule && typeof rule.requestType === 'string' ? rule.requestType.trim() : '';
|
|
if (!walletId || !scope) {
|
|
throw new Error('Invalid rule payload');
|
|
}
|
|
|
|
const body = new URLSearchParams({
|
|
walletId,
|
|
scope,
|
|
app,
|
|
requestType,
|
|
});
|
|
|
|
const payload = await requestJson(userQappsRuleRevokeUrl, {
|
|
method: 'POST',
|
|
headers,
|
|
body,
|
|
});
|
|
assertBrokerOk(payload, 'Failed to revoke rule');
|
|
await refreshApprovalRules();
|
|
}
|
|
|
|
async function saveApprovalDefaults() {
|
|
const unlockWindowMinutes = normalizeApprovalTempMinutes(defaultApprovalModeEl.value);
|
|
defaultApprovalTempMinutesEl.value = String(unlockWindowMinutes);
|
|
const body = new URLSearchParams({
|
|
approvalMode: 'once',
|
|
approvalTempMinutes: String(unlockWindowMinutes),
|
|
talkBridgeAutoApproveSendChatMessage: defaultTalkBridgeAutoApproveSendChatMessageEl.checked ? '1' : '',
|
|
highRiskConfirmationEnabled: highRiskConfirmationEnabledEl.checked ? '1' : '0',
|
|
});
|
|
|
|
const payload = await requestJson(userQappsPrefsUrl, {
|
|
method: 'POST',
|
|
headers,
|
|
body,
|
|
});
|
|
assertBrokerOk(payload, 'Failed to save unlock window');
|
|
resultEl.textContent = JSON.stringify(payload, null, 2);
|
|
|
|
const saved = payload && payload.data ? payload.data : payload;
|
|
root.dataset.userQappsApprovalMode = 'once';
|
|
root.dataset.userQappsApprovalTempMinutes = String(normalizeApprovalTempMinutes(saved && saved.approvalTempMinutes ? saved.approvalTempMinutes : '120'));
|
|
defaultApprovalModeEl.value = root.dataset.userQappsApprovalTempMinutes;
|
|
defaultApprovalTempMinutesEl.value = root.dataset.userQappsApprovalTempMinutes;
|
|
root.dataset.userQappsApprovalUnlock10Min = '0';
|
|
root.dataset.userQappsUnlockSession20Min = '0';
|
|
root.dataset.userTalkBridgeAutoApproveSendChatMessage =
|
|
saved && saved.talkBridgeAutoApproveSendChatMessage ? '1' : '0';
|
|
root.dataset.userQappsHighRiskConfirmationEnabled =
|
|
!saved || saved.highRiskConfirmationEnabled !== false ? '1' : '0';
|
|
applyApprovalDefaultsFromDataset();
|
|
setApprovalDefaultsResult('Unlock window saved.', false);
|
|
}
|
|
|
|
importButton.addEventListener('click', async function () {
|
|
try {
|
|
await importAndLink();
|
|
} catch (error) {
|
|
setFeedback(error.message || 'Import/link failed', true);
|
|
}
|
|
});
|
|
|
|
refreshMappingsButton.addEventListener('click', async function () {
|
|
try {
|
|
await refreshMappings();
|
|
setFeedback('Linked accounts refreshed.', false);
|
|
} catch (error) {
|
|
setFeedback(error.message || 'Failed to refresh linked accounts', true);
|
|
}
|
|
});
|
|
|
|
importBackupButton.addEventListener('click', async function () {
|
|
try {
|
|
await importBackupAndLink();
|
|
} catch (error) {
|
|
setFeedback(error.message || 'Backup import/link failed', true);
|
|
}
|
|
});
|
|
|
|
createWalletButton.addEventListener('click', async function () {
|
|
try {
|
|
await createWallet();
|
|
} catch (error) {
|
|
setFeedback(error.message || 'Account creation failed', true);
|
|
}
|
|
});
|
|
|
|
backupWalletButton.addEventListener('click', async function () {
|
|
try {
|
|
await backupWallet();
|
|
} catch (error) {
|
|
setFeedback(error.message || 'Backup failed', true);
|
|
}
|
|
});
|
|
|
|
backupWalletSelectEl.addEventListener('change', function () {
|
|
preferredBackupWalletId = String(backupWalletSelectEl.value || '').trim();
|
|
});
|
|
|
|
backupReencryptToggleEl.addEventListener('change', function () {
|
|
syncBackupReencryptFields();
|
|
});
|
|
|
|
recoverySaveButton.addEventListener('click', async function () {
|
|
try {
|
|
await saveRecoverySettings();
|
|
} catch (error) {
|
|
setRecoveryResult(error.message || 'Failed to save recovery settings', true);
|
|
}
|
|
});
|
|
|
|
recoveryDisableButton.addEventListener('click', async function () {
|
|
try {
|
|
await disableRecoverySettings();
|
|
} catch (error) {
|
|
setRecoveryResult(error.message || 'Failed to disable recovery settings', true);
|
|
}
|
|
});
|
|
|
|
recoveryRevealButton.addEventListener('click', async function () {
|
|
try {
|
|
await revealRecoveryPassword();
|
|
} catch (error) {
|
|
setRecoveryResult(error.message || 'Failed to reveal backup password', true);
|
|
}
|
|
});
|
|
|
|
recoveryCompleteEmergencyButton.addEventListener('click', async function () {
|
|
try {
|
|
await completeEmergencyRecovery();
|
|
} catch (error) {
|
|
setRecoveryResult(error.message || 'Failed to complete emergency recovery', true);
|
|
}
|
|
});
|
|
|
|
saveApprovalDefaultsButton.addEventListener('click', async function () {
|
|
try {
|
|
await saveApprovalDefaults();
|
|
} catch (error) {
|
|
setApprovalDefaultsResult(error.message || 'Failed to save unlock window', true);
|
|
}
|
|
});
|
|
|
|
resetApprovalDefaultsButton.addEventListener('click', function () {
|
|
defaultApprovalModeEl.value = '120';
|
|
defaultApprovalTempMinutesEl.value = '120';
|
|
defaultApprovalUnlock10MinEl.checked = false;
|
|
defaultUnlockSession20MinEl.checked = false;
|
|
defaultTalkBridgeAutoApproveSendChatMessageEl.checked = false;
|
|
setApprovalDefaultsResult('Default unlock window restored in the form. Click Save Unlock Window to apply.', false);
|
|
});
|
|
|
|
defaultApprovalModeEl.addEventListener('change', function () {
|
|
defaultApprovalTempMinutesEl.value = String(normalizeApprovalTempMinutes(defaultApprovalModeEl.value));
|
|
});
|
|
|
|
refreshApprovalRulesButton.addEventListener('click', async function () {
|
|
try {
|
|
await refreshApprovalRules();
|
|
setApprovalRulesResult('Approval rules refreshed.', false);
|
|
} catch (error) {
|
|
setApprovalRulesResult(error.message || 'Failed to refresh approval rules', true);
|
|
}
|
|
});
|
|
|
|
[createPasswordEl, createKdfEl].forEach(function (el) {
|
|
el.addEventListener('keydown', function (event) {
|
|
if (event.key === 'Enter') {
|
|
event.preventDefault();
|
|
}
|
|
});
|
|
});
|
|
|
|
applyApprovalDefaultsFromDataset();
|
|
applyModeCopy();
|
|
syncBackupReencryptFields();
|
|
|
|
refreshMappings().catch(function (error) {
|
|
setFeedback(error.message || 'Initial linked account load failed', true);
|
|
});
|
|
loadRecoveryStatus().catch(function (error) {
|
|
setRecoveryResult(error.message || 'Failed to load recovery status', true);
|
|
});
|
|
if (!isPoweredMode) {
|
|
refreshApprovalRules().catch(function (error) {
|
|
setApprovalRulesResult(error.message || 'Initial approval rules load failed', true);
|
|
});
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
if (!init()) {
|
|
const observer = new MutationObserver(function () {
|
|
if (init()) {
|
|
observer.disconnect();
|
|
}
|
|
});
|
|
|
|
if (document.body) {
|
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
}
|
|
}
|
|
})();
|