Files

698 lines
22 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 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 = '<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 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 });
}
}
})();