From 93a2071e77063af275a04a47f78b387822943772 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Wed, 13 Nov 2024 05:04:54 +0200 Subject: [PATCH] added admin action qortal request --- src/App.tsx | 2 +- src/atoms/global.ts | 8 ++ src/components/Apps/AppViewer.tsx | 2 +- src/components/Apps/AppsCategory.tsx | 3 + src/components/Apps/AppsCategoryDesktop.tsx | 4 +- src/components/Apps/AppsLibrary.tsx | 4 +- src/components/Apps/AppsLibraryDesktop.tsx | 4 +- .../Apps/useQortalMessageListener.tsx | 10 +- src/messaging/messagesToBackground.tsx | 4 +- src/qortalRequests.ts | 26 +++- src/qortalRequests/get.ts | 115 ++++++++++++++++-- 11 files changed, 156 insertions(+), 26 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 96e5e31..b084c62 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -651,7 +651,7 @@ function App() { if (message.action === "QORTAL_REQUEST_PERMISSION") { try { if(message?.payload?.checkbox1){ - qortalRequestCheckbox1Ref.current = message?.payload?.checkbox1 + qortalRequestCheckbox1Ref.current = message?.payload?.checkbox1?.value || false } await showQortalRequestExtension(message?.payload); diff --git a/src/atoms/global.ts b/src/atoms/global.ts index 5e9c2bc..d2fbda7 100644 --- a/src/atoms/global.ts +++ b/src/atoms/global.ts @@ -28,6 +28,14 @@ export const sortablePinnedAppsAtom = atom({ { name: 'Q-Trade', service: 'APP' + }, + { + name: 'Q-Support', + service: 'APP' + }, + { + name: 'NodeInfo', + service: 'APP' } ], }); diff --git a/src/components/Apps/AppViewer.tsx b/src/components/Apps/AppViewer.tsx index 77ec0e4..e27810e 100644 --- a/src/components/Apps/AppViewer.tsx +++ b/src/components/Apps/AppViewer.tsx @@ -15,7 +15,7 @@ export const AppViewer = React.forwardRef(({ app , hide, isDevMode}, iframeRef) const { rootHeight } = useContext(MyContext); // const iframeRef = useRef(null); const { document, window: frameWindow } = useFrame(); - const {path, history, changeCurrentIndex} = useQortalMessageListener(frameWindow, iframeRef, app?.tabId, isDevMode) + const {path, history, changeCurrentIndex} = useQortalMessageListener(frameWindow, iframeRef, app?.tabId, isDevMode, app?.name, app?.service) const [url, setUrl] = useState('') useEffect(()=> { diff --git a/src/components/Apps/AppsCategory.tsx b/src/components/Apps/AppsCategory.tsx index a999c95..206ed69 100644 --- a/src/components/Apps/AppsCategory.tsx +++ b/src/components/Apps/AppsCategory.tsx @@ -39,6 +39,9 @@ const officialAppList = [ "qombo", "q-fund", "q-shop", + "q-trade", + "q-support", + "NodeInfo" ]; const ScrollerStyled = styled('div')({ diff --git a/src/components/Apps/AppsCategoryDesktop.tsx b/src/components/Apps/AppsCategoryDesktop.tsx index 456c597..d265add 100644 --- a/src/components/Apps/AppsCategoryDesktop.tsx +++ b/src/components/Apps/AppsCategoryDesktop.tsx @@ -47,7 +47,9 @@ const officialAppList = [ "qombo", "q-fund", "q-shop", - "q-trade" + "q-trade", + "q-support", + "NodeInfo" ]; const ScrollerStyled = styled("div")({ diff --git a/src/components/Apps/AppsLibrary.tsx b/src/components/Apps/AppsLibrary.tsx index 87ddbbc..cc1ded8 100644 --- a/src/components/Apps/AppsLibrary.tsx +++ b/src/components/Apps/AppsLibrary.tsx @@ -41,7 +41,9 @@ const officialAppList = [ "qombo", "q-fund", "q-shop", - "q-trade" + "q-trade", + "q-support", + "NodeInfo" ]; const ScrollerStyled = styled('div')({ diff --git a/src/components/Apps/AppsLibraryDesktop.tsx b/src/components/Apps/AppsLibraryDesktop.tsx index 5a0012a..b7ea49f 100644 --- a/src/components/Apps/AppsLibraryDesktop.tsx +++ b/src/components/Apps/AppsLibraryDesktop.tsx @@ -56,7 +56,9 @@ const officialAppList = [ "qombo", "q-fund", "q-shop", - "q-trade" + "q-trade", + "q-support", + "NodeInfo" ]; const ScrollerStyled = styled("div")({ diff --git a/src/components/Apps/useQortalMessageListener.tsx b/src/components/Apps/useQortalMessageListener.tsx index 0cebaec..840554b 100644 --- a/src/components/Apps/useQortalMessageListener.tsx +++ b/src/components/Apps/useQortalMessageListener.tsx @@ -182,7 +182,7 @@ const UIQortalRequests = [ 'GET_WALLET_BALANCE', 'GET_USER_WALLET_INFO', 'GET_CROSSCHAIN_SERVER_INFO', 'GET_TX_ACTIVITY_SUMMARY', 'GET_FOREIGN_FEE', 'UPDATE_FOREIGN_FEE', 'GET_SERVER_CONNECTION_HISTORY', 'SET_CURRENT_FOREIGN_SERVER', - 'ADD_FOREIGN_SERVER', 'REMOVE_FOREIGN_SERVER', 'GET_DAY_SUMMARY', 'CREATE_TRADE_BUY_ORDER', 'CREATE_TRADE_SELL_ORDER', 'CANCEL_TRADE_SELL_ORDER', 'IS_USING_GATEWAY' + 'ADD_FOREIGN_SERVER', 'REMOVE_FOREIGN_SERVER', 'GET_DAY_SUMMARY', 'CREATE_TRADE_BUY_ORDER', 'CREATE_TRADE_SELL_ORDER', 'CANCEL_TRADE_SELL_ORDER', 'IS_USING_GATEWAY', 'ADMIN_ACTION' ]; @@ -382,7 +382,7 @@ const UIQortalRequests = [ return obj; // Updated object with references to stored files } -export const useQortalMessageListener = (frameWindow, iframeRef, tabId, isDevMode) => { +export const useQortalMessageListener = (frameWindow, iframeRef, tabId, isDevMode, appName, appService) => { const [path, setPath] = useState('') const [history, setHistory] = useState({ customQDNHistoryPaths: [], @@ -436,7 +436,9 @@ isDOMContentLoaded: false if (event?.data?.requestedHandler !== 'UI') return; const sendMessageToRuntime = (message, eventPort) => { - window.sendMessage(message.action, message.payload, 300000, message.isExtension) + window.sendMessage(message.action, message.payload, 300000, message.isExtension, { + name: appName, service: appService + }) .then((response) => { if (response.error) { eventPort.postMessage({ @@ -553,7 +555,7 @@ isDOMContentLoaded: false }; - }, [isDevMode]); // Empty dependency array to run once when the component mounts + }, [isDevMode, appName, appService]); // Empty dependency array to run once when the component mounts diff --git a/src/messaging/messagesToBackground.tsx b/src/messaging/messagesToBackground.tsx index f252aa5..f64665f 100644 --- a/src/messaging/messagesToBackground.tsx +++ b/src/messaging/messagesToBackground.tsx @@ -24,13 +24,13 @@ window.addEventListener("message", (event) => { } }); -export const sendMessageBackground = (action, data = {}, timeout = 60000, isExtension) => { +export const sendMessageBackground = (action, data = {}, timeout = 60000, isExtension, appInfo) => { return new Promise((resolve, reject) => { const requestId = generateRequestId(); // Unique ID for each request callbackMap.set(requestId, { resolve, reject }); // Store both resolve and reject callbacks const targetOrigin = window.location.origin // Send the message with `backgroundMessage` type - window.postMessage({ type: "backgroundMessage", action, requestId, payload: data, isExtension }, targetOrigin); + window.postMessage({ type: "backgroundMessage", action, requestId, payload: data, isExtension, appInfo }, targetOrigin); // Set up a timeout to automatically reject if no response is received const timeoutId = setTimeout(() => { diff --git a/src/qortalRequests.ts b/src/qortalRequests.ts index 72cea92..6d78abd 100644 --- a/src/qortalRequests.ts +++ b/src/qortalRequests.ts @@ -1,5 +1,5 @@ import { gateways, getApiKeyFromStorage } from "./background"; -import { addForeignServer, addListItems, cancelSellOrder, createBuyOrder, createPoll, createSellOrder, decryptData, deleteListItems, deployAt, encryptData, getCrossChainServerInfo, getDaySummary, getForeignFee, getListItems, getServerConnectionHistory, getTxActivitySummary, getUserAccount, getUserWallet, getUserWalletInfo, getWalletBalance, joinGroup, publishMultipleQDNResources, publishQDNResource, removeForeignServer, saveFile, sendChatMessage, sendCoin, setCurrentForeignServer, updateForeignFee, voteOnPoll } from "./qortalRequests/get"; +import { addForeignServer, addListItems, adminAction, cancelSellOrder, createBuyOrder, createPoll, createSellOrder, decryptData, deleteListItems, deployAt, encryptData, getCrossChainServerInfo, getDaySummary, getForeignFee, getListItems, getServerConnectionHistory, getTxActivitySummary, getUserAccount, getUserWallet, getUserWalletInfo, getWalletBalance, joinGroup, publishMultipleQDNResources, publishQDNResource, removeForeignServer, saveFile, sendChatMessage, sendCoin, setCurrentForeignServer, updateForeignFee, voteOnPoll } from "./qortalRequests/get"; import { getData, storeData } from "./utils/chromeStorage"; @@ -68,6 +68,7 @@ export const isRunningGateway = async ()=> { // Ensure the message is from a trusted source const isFromExtension = request?.isExtension; + const appInfo = request?.appInfo; if (request?.type !== "backgroundMessage") return; // Only process messages of type 'backgroundMessage' @@ -75,7 +76,7 @@ export const isRunningGateway = async ()=> { switch (request.action) { case "GET_USER_ACCOUNT": { try { - const res = await getUserAccount(); + const res = await getUserAccount({isFromExtension, appInfo}); event.source.postMessage({ requestId: request.requestId, action: request.action, @@ -357,7 +358,7 @@ export const isRunningGateway = async ()=> { case "GET_WALLET_BALANCE": { try { - const res = await getWalletBalance(request.payload, false, isFromExtension); + const res = await getWalletBalance(request.payload, false, isFromExtension, appInfo); event.source.postMessage({ requestId: request.requestId, action: request.action, @@ -671,6 +672,25 @@ export const isRunningGateway = async ()=> { } break; } + case "ADMIN_ACTION": { + try { + const res = await adminAction(request.payload, isFromExtension) + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + payload: res, + type: "backgroundMessageResponse", + }, event.origin); + } catch (error) { + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + error: error?.message, + type: "backgroundMessageResponse", + }, event.origin); + } + break; + } default: break; } diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index 677b102..73e5607 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -287,8 +287,30 @@ async function getUserPermission(payload, isFromExtension) { } -export const getUserAccount = async () => { +export const getUserAccount = async ({isFromExtension, appInfo}) => { try { + const value = (await getPermission(`qAPPAutoAuth-${appInfo?.name}`)) || false; + let skip = false; + if (value) { + skip = true; + } + let resPermission + if(!skip){ + resPermission = await getUserPermission({ + text1: "Do you give this application permission to authenticate?", + checkbox1: { + value: false, + label: "Always authenticate automatically", + }, + }, isFromExtension); + } + + const { accepted = false, checkbox1 = false } = resPermission || {}; + if(resPermission){ + setPermission(`qAPPAutoAuth-${appInfo?.name}`, checkbox1); + } + if (accepted || skip) { + const wallet = await getSaveWallet(); const address = wallet.address0; const publicKey = wallet.publicKey; @@ -296,7 +318,12 @@ export const getUserAccount = async () => { address, publicKey, }; + } else { + throw new Error("User declined request"); + + } } catch (error) { + console.log('per error', error) throw new Error("Unable to fetch user account"); } }; @@ -369,8 +396,10 @@ export const decryptData = async (data) => { }; export const getListItems = async (data, isFromExtension) => { - const localNodeAvailable = await isUsingLocal() - if(!localNodeAvailable) throw new Error('Please use your local node.') + const isGateway = await isRunningGateway() + if(isGateway){ + throw new Error('This action cannot be done through a gateway') + } const requiredFields = ["list_name"]; const missingFields: string[] = []; requiredFields.forEach((field) => { @@ -421,8 +450,10 @@ export const getListItems = async (data, isFromExtension) => { }; export const addListItems = async (data, isFromExtension) => { - const localNodeAvailable = await isUsingLocal() - if(!localNodeAvailable) throw new Error('Please use your local node.') + const isGateway = await isRunningGateway() + if(isGateway){ + throw new Error('This action cannot be done through a gateway') + } const requiredFields = ["list_name", "items"]; const missingFields: string[] = []; requiredFields.forEach((field) => { @@ -474,8 +505,10 @@ export const addListItems = async (data, isFromExtension) => { }; export const deleteListItems = async (data, isFromExtension) => { - const localNodeAvailable = await isUsingLocal() - if(!localNodeAvailable) throw new Error('Please use your local node.') + const isGateway = await isRunningGateway() + if(isGateway){ + throw new Error('This action cannot be done through a gateway') + } const requiredFields = ["list_name", "item"]; const missingFields: string[] = []; requiredFields.forEach((field) => { @@ -1359,7 +1392,7 @@ export const getUserWallet = async (data, isFromExtension) => { } }; -export const getWalletBalance = async (data, bypassPermission?: boolean, isFromExtension) => { +export const getWalletBalance = async (data, bypassPermission?: boolean, isFromExtension?: boolean, appInfo?: any) => { const requiredFields = ["coin"]; const missingFields: string[] = []; requiredFields.forEach((field) => { @@ -1373,7 +1406,7 @@ export const getWalletBalance = async (data, bypassPermission?: boolean, isFromE throw new Error(errorMsg); } - const value = (await getPermission(`qAPPAutoWalletBalance-${data.coin}`)) || false; + const value = (await getPermission(`qAPPAutoWalletBalance-${appInfo?.name}-${data.coin}`)) || false; let skip = false; if (value) { skip = true; @@ -1392,7 +1425,7 @@ export const getWalletBalance = async (data, bypassPermission?: boolean, isFromE } const { accepted = false, checkbox1 = false } = resPermission || {}; if(resPermission){ - setPermission(`qAPPAutoWalletBalance-${data.coin}`, checkbox1); + setPermission(`qAPPAutoWalletBalance-${appInfo?.name}-${data.coin}`, checkbox1); } if (accepted || bypassPermission || skip) { let coin = data.coin; @@ -2020,8 +2053,9 @@ export const sendCoin = async (data, isFromExtension) => { const address = wallet.address0; const resKeyPair = await getKeyPair(); const parsedData = resKeyPair; - const localNodeAvailable = await isUsingLocal() - if(checkCoin !== 'QORT' && !localNodeAvailable) throw new Error('Cannot send a non-QORT coin through the gateway. Please use your local node.') + const isGateway = await isRunningGateway() + + if(checkCoin !== 'QORT' && isGateway) throw new Error('Cannot send a non-QORT coin through the gateway. Please use your local node.') if (checkCoin === "QORT") { // Params: data.coin, data.destinationAddress, data.amount, data.fee // TODO: prompt user to send. If they confirm, call `POST /crosschain/:coin/send`, or for QORT, broadcast a PAYMENT transaction @@ -2716,4 +2750,61 @@ export const cancelSellOrder = async (data, isFromExtension) => { } catch (error) { throw new Error(error?.message || "Failed to submit sell order."); } +}; + +export const adminAction = async (data, isFromExtension) => { + + const requiredFields = [ + "type", + ]; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(", "); + const errorMsg = `Missing fields: ${missingFieldsString}`; + throw new Error(errorMsg); + } + const isGateway = await isRunningGateway() + if(isGateway){ + throw new Error('This action cannot be done through a gateway') + } + + let apiEndpoint = ''; + switch (data.type.toLowerCase()) { + case 'stop': + apiEndpoint = await createEndpoint('/admin/stop'); + break; + case 'restart': + apiEndpoint = await createEndpoint('/admin/restart'); + break; + case 'bootstrap': + apiEndpoint = await createEndpoint('/admin/bootstrap'); + break; + default: + throw new Error(`Unknown admin action type: ${data.type}`); + } + + const resPermission = await getUserPermission({ + text1: `Do you give this application permission to perform a node ${data.type}?`, + }, isFromExtension); + const { accepted } = resPermission; + if (accepted) { + const response = await fetch(apiEndpoint); + if (!response.ok) throw new Error("Failed to perform request"); + + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + return res; + } else { + throw new Error("User declined request"); + } + }; \ No newline at end of file