(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 userUnlinkMappingUrl = root.dataset.userUnlinkMappingUrl; const userQappsPrefsUrl = root.dataset.userQappsPrefsUrl; const userQappsRulesUrl = root.dataset.userQappsRulesUrl; const userQappsRuleRevokeUrl = root.dataset.userQappsRuleRevokeUrl; 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 backupWalletIdEl = document.getElementById('qortal-backup-wallet-id'); const backupWalletPasswordEl = document.getElementById('qortal-backup-wallet-password'); 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 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 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'); if ( !userMappingsUrl || !userCreateWalletUrl || !userBackupWalletUrl || !userImportSeedLinkUrl || !userImportBackupLinkUrl || !userUnlinkMappingUrl || !userQappsPrefsUrl || !userQappsRulesUrl || !userQappsRuleRevokeUrl || !createWalletButton || !createPasswordEl || !createKdfEl || !createResultEl || !backupWalletIdEl || !backupWalletPasswordEl || !backupDownloadEl || !backupSaveEl || !backupWalletButton || !backupResultEl || !importButton || !importBackupButton || !refreshMappingsButton || !seedPhraseEl || !walletPasswordEl || !backupFileEl || !backupPasswordEl || !feedbackEl || !successEl || !resultEl || !mappingsBody || !defaultApprovalModeEl || !defaultApprovalTempMinutesEl || !defaultApprovalUnlock10MinEl || !defaultUnlockSession20MinEl || !saveApprovalDefaultsButton || !resetApprovalDefaultsButton || !approvalDefaultsResultEl || !refreshApprovalRulesButton || !approvalRulesBody || !approvalRulesResultEl ) { return false; } const headers = { requesttoken: OC.requestToken, }; 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 normalizeApprovalMode(mode) { const normalized = String(mode || '').trim(); if (normalized === 'session') { return 'type_minutes'; } if (normalized === 'scope') { return 'type_always'; } if (normalized === 'app') { return 'app_always'; } if (normalized === 'type_minutes' || normalized === 'type_always' || normalized === 'app_always') { return normalized; } return 'once'; } function normalizeApprovalTempMinutes(value) { const parsed = Number(String(value || '').trim()); if (!Number.isFinite(parsed)) { return 10; } return Math.max(1, Math.min(Math.floor(parsed), 1440)); } function applyApprovalDefaultsFromDataset() { defaultApprovalModeEl.value = normalizeApprovalMode(root.dataset.userQappsApprovalMode || 'once'); defaultApprovalTempMinutesEl.value = String(normalizeApprovalTempMinutes(root.dataset.userQappsApprovalTempMinutes || '10')); defaultApprovalTempMinutesEl.disabled = normalizeApprovalMode(defaultApprovalModeEl.value) !== 'type_minutes'; defaultApprovalUnlock10MinEl.checked = root.dataset.userQappsApprovalUnlock10Min === '1'; defaultUnlockSession20MinEl.checked = root.dataset.userQappsUnlockSession20Min === '1'; setApprovalDefaultsResult( 'Current defaults loaded. Save changes to apply for future Q-Apps auth popups.', 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 renderMappings(payload) { const mappings = payload && payload.ok && payload.data && Array.isArray(payload.data.mappings) ? payload.data.mappings : []; mappingsBody.innerHTML = ''; if (mappings.length === 0) { mappingsBody.innerHTML = 'No linked Qortal accounts yet.'; 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 = 'No active approval rules.'; 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 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 wallet password are required'); } setFeedback('Importing wallet and linking account...', 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('Wallet imported and linked.', false); setSuccess('Wallet 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('Wallet password is required'); } createWalletButton.disabled = true; setFeedback('Creating wallet...', false); setSuccess(''); const body = new URLSearchParams({ password, kdfThreads: kdfThreads || '', }); try { const payload = await requestJson(userCreateWalletUrl, { method: 'POST', headers, body, }); assertBrokerOk(payload, 'Wallet creation failed'); createResultEl.textContent = JSON.stringify(payload, null, 2); const wallet = payload.wallet || payload.data || payload; if (wallet && wallet.walletId) { backupWalletIdEl.value = wallet.walletId; } if (!backupWalletPasswordEl.value) { backupWalletPasswordEl.value = password; } await refreshMappings(); setFeedback('Wallet created and linked.', false); setSuccess('Wallet created. You can now authenticate to Nextcloud using your Qortal account.'); } finally { createWalletButton.disabled = false; } } async function backupWallet() { const walletId = backupWalletIdEl.value.trim(); const password = backupWalletPasswordEl.value.trim(); const download = backupDownloadEl.checked; const saveToFiles = backupSaveEl.checked; if (!walletId) { throw new Error('Wallet ID is required'); } if (!password) { throw new Error('Backup password is required'); } if (!download && !saveToFiles) { throw new Error('Select at least one backup option'); } setFeedback('Creating wallet backup...', false); setSuccess(''); const body = new URLSearchParams({ walletId, password, saveToFiles: saveToFiles ? '1' : '', }); 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-wallet-${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('Wallet 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 body = new URLSearchParams({ approvalMode: normalizeApprovalMode(defaultApprovalModeEl.value), approvalTempMinutes: String(normalizeApprovalTempMinutes(defaultApprovalTempMinutesEl.value)), approvalUnlock10Min: defaultApprovalUnlock10MinEl.checked ? '1' : '', unlockSession20Min: defaultUnlockSession20MinEl.checked ? '1' : '', }); const payload = await requestJson(userQappsPrefsUrl, { method: 'POST', headers, body, }); assertBrokerOk(payload, 'Failed to save approval defaults'); resultEl.textContent = JSON.stringify(payload, null, 2); const saved = payload && payload.data ? payload.data : payload; root.dataset.userQappsApprovalMode = normalizeApprovalMode(saved && saved.approvalMode ? saved.approvalMode : 'once'); root.dataset.userQappsApprovalTempMinutes = String(normalizeApprovalTempMinutes(saved && saved.approvalTempMinutes ? saved.approvalTempMinutes : '10')); root.dataset.userQappsApprovalUnlock10Min = saved && saved.approvalUnlock10Min ? '1' : '0'; root.dataset.userQappsUnlockSession20Min = saved && saved.unlockSession20Min ? '1' : '0'; applyApprovalDefaultsFromDataset(); setApprovalDefaultsResult('Approval defaults 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 || 'Wallet creation failed', true); } }); backupWalletButton.addEventListener('click', async function () { try { await backupWallet(); } catch (error) { setFeedback(error.message || 'Backup failed', true); } }); saveApprovalDefaultsButton.addEventListener('click', async function () { try { await saveApprovalDefaults(); } catch (error) { setApprovalDefaultsResult(error.message || 'Failed to save approval defaults', true); } }); resetApprovalDefaultsButton.addEventListener('click', function () { defaultApprovalModeEl.value = 'once'; defaultApprovalTempMinutesEl.value = '10'; defaultApprovalTempMinutesEl.disabled = true; defaultApprovalUnlock10MinEl.checked = false; defaultUnlockSession20MinEl.checked = false; setApprovalDefaultsResult('Safe defaults restored in the form. Click Save Approval Defaults to apply.', false); }); defaultApprovalModeEl.addEventListener('change', function () { defaultApprovalTempMinutesEl.disabled = normalizeApprovalMode(defaultApprovalModeEl.value) !== 'type_minutes'; }); 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(); refreshMappings().catch(function (error) { setFeedback(error.message || 'Initial linked account load failed', true); }); 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 }); } } })();