Files

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 });
}
}
})();