diff --git a/src/backend/src/services/FilesystemAPIService.js b/src/backend/src/services/FilesystemAPIService.js index 2b054599..c18ec04e 100644 --- a/src/backend/src/services/FilesystemAPIService.js +++ b/src/backend/src/services/FilesystemAPIService.js @@ -61,7 +61,7 @@ class FilesystemAPIService extends BaseService { app.use(require('../routers/filesystem_api/rename')) app.use(require('../routers/filesystem_api/search')) - + // v1 app.use(require('../routers/writeFile')) app.use(require('../routers/file')) diff --git a/src/gui/src/IPC.js b/src/gui/src/IPC.js index ce1e360e..3b83927d 100644 --- a/src/gui/src/IPC.js +++ b/src/gui/src/IPC.js @@ -1078,12 +1078,15 @@ const ipc_listener = async (event, handled) => { // disable parent window event.data.options.window_options.disable_parent_window = true; - let granted = await UIWindowRequestPermission({ - origin: event.origin, - permission: event.data.options.permission, - window_options: event.data.options.window_options, - }); - + let granted = await UIWindowRequestPermission( + { + permission: event.data.options.permission, + window_options: event.data.options.window_options, + app_uid: app_uuid, + app_name: app_name, + } + ); + // send selected font to requester window target_iframe.contentWindow.postMessage({ msg: "permissionGranted", diff --git a/src/gui/src/UI/UIWindowRequestPermission.js b/src/gui/src/UI/UIWindowRequestPermission.js index 70e1f076..f34a37ee 100644 --- a/src/gui/src/UI/UIWindowRequestPermission.js +++ b/src/gui/src/UI/UIWindowRequestPermission.js @@ -17,127 +17,182 @@ * along with this program. If not, see . */ -import UIWindow from './UIWindow.js' +import UIWindow from './UIWindow.js'; -async function UIWindowRequestPermission(options){ +async function UIWindowRequestPermission(options) { options = options ?? {}; options.reload_on_success = options.reload_on_success ?? false; - return new Promise(async (resolve) => { - let drivers = [ - { - name: 'puter-chat-completion', - human_name: 'AI Chat Completion', - description: 'This app wants to generate text using AI. This may incur costs on your behalf.', - }, - { - name: 'puter-image-generation', - human_name: 'AI Image Generation', - description: 'This app wants to generate images using AI. This may incur costs on your behalf.', - }, - { - name: 'puter-kvstore', - human_name: 'Puter Storage', - description: 'This app wants to securely store data in your Puter account. This app will not be able to access your personal data or data stored by other apps.', + + return new Promise((resolve) => { + get_permission_description(options.permission).then((permission_description) => { + if (!permission_description) { + resolve(false); + return; } - ] - - let parts = options.permission.split(":"); - let driver_name = parts[1]; - let action_name = parts[2]; - - function findDriverByName(driverName) { - return drivers.find(driver => driver.name === driverName); - } - - let driver = findDriverByName(driver_name); - - if(driver === undefined){ - resolve(false); - return; - } - - let h = ``; - h += `
`; - h += `
`; - // title - h += `

"${html_encode(options.app_uid ?? options.origin)}" would Like to use ${html_encode(driver.human_name)}

`; - // todo show the real description of action - h += `

${html_encode(driver.description)}

`; - // Allow/Don't Allow - h += ``; - h += ``; - h += `
`; - h += `
`; - - const el_window = await UIWindow({ - title: null, - app: 'request-authorization', - single_instance: true, - icon: null, - uid: null, - is_dir: false, - body_content: h, - has_head: true, - selectable_body: false, - draggable_body: true, - allow_context_menu: false, - is_draggable: true, - is_droppable: false, - is_resizable: false, - stay_on_top: false, - allow_native_ctxmenu: true, - allow_user_select: true, - ...options.window_options, - width: 350, - dominant: true, - on_close: ()=>{ - resolve(false) - }, - onAppend: function(this_window){ - }, - window_class: 'window-login', - window_css:{ - height: 'initial', - }, - body_css: { - width: 'initial', - padding: '0', - 'background-color': 'rgba(231, 238, 245, .95)', - 'backdrop-filter': 'blur(3px)', - } - }) - - $(el_window).find('.app-auth-allow').on('click', async function(e){ - $(this).addClass('disabled'); - - try{ - const res = await fetch( window.api_origin + "/auth/grant-user-app", { - "headers": { - "Content-Type": "application/json", - "Authorization": "Bearer " + window.auth_token, - }, - "body": JSON.stringify({ - app_uid: options.app_uid, - origin: options.origin, - permission: options.permission - }), - "method": "POST", - }); - }catch(err){ - console.error(err); - resolve(err); - } - - resolve(true); - }) - - $(el_window).find('.app-auth-dont-allow').on('click', function(e){ - $(this).addClass('disabled'); - $(el_window).close(); - resolve(false); - }) - }) + create_permission_window(options, permission_description, resolve).then((el_window) => { + setup_window_events(el_window, options, resolve); + }); + }); + }); } -export default UIWindowRequestPermission \ No newline at end of file +/** + * Creates the permission dialog + */ +async function create_permission_window(options, permission_description, resolve) { + const requestingEntity = options.app_name ?? options.origin; + const h = create_window_content(requestingEntity, permission_description); + + return await UIWindow({ + title: null, + app: 'request-authorization', + single_instance: true, + icon: null, + uid: null, + is_dir: false, + body_content: h, + has_head: true, + selectable_body: false, + draggable_body: true, + allow_context_menu: false, + is_draggable: true, + is_droppable: false, + is_resizable: false, + stay_on_top: false, + allow_native_ctxmenu: true, + allow_user_select: true, + ...options.window_options, + width: 350, + dominant: true, + on_close: () => resolve(false), + onAppend: function(this_window) {}, + window_class: 'window-login', + window_css: { + height: 'initial', + }, + body_css: { + width: 'initial', + padding: '0', + 'background-color': 'rgba(231, 238, 245, .95)', + 'backdrop-filter': 'blur(3px)', + } + }); +} + +/** + * Creates HTML content for permission dialog + */ +function create_window_content(requestingEntity, permission_description) { + let h = ``; + h += `
`; + h += `
`; + // title + h += `

${html_encode(requestingEntity)}

`; + + // show the real description of action + h += `

${html_encode(requestingEntity)} is requesting for permission to ${html_encode(permission_description)}

`; + + // Allow/Don't Allow + h += ``; + h += ``; + h += `
`; + h += `
`; + return h; +} + +/** + * Sets up event handlers for permission dialog + */ +async function setup_window_events(el_window, options, resolve) { + $(el_window).find('.app-auth-allow').on('click', async function(e) { + $(this).addClass('disabled'); + + try { + // register granted permission to app or website + const res = await fetch(window.api_origin + "/auth/grant-user-app", { + headers: { + "Content-Type": "application/json", + "Authorization": "Bearer " + window.auth_token, + }, + body: JSON.stringify({ + app_uid: options.app_uid, + origin: options.origin, + permission: options.permission + }), + method: "POST", + }); + + if (!res.ok) { + throw new Error(`HTTP error! Status: ${res.status}`); + } + + $(el_window).close(); + resolve(true); + } catch (err) { + console.error(err); + resolve(err); + } + }); + + $(el_window).find('.app-auth-dont-allow').on('click', function(e) { + $(this).addClass('disabled'); + $(el_window).close(); + resolve(false); + }); +} + +/** + * Generates user-friendly description of permission string. Currently handles: + * fs:UUID-OF-FILE:read, thread:UUID-OF-THREAD:post, service:name-of-service:ii:name-of-interface, driver:driver-name:action-name + */ +async function get_permission_description(permission) { + const parts = split_permission(permission); + const [resource_type, resource_id, action, interface_name = null] = parts; + let fsentry; + + if (resource_type === "fs") { + fsentry = await puter.fs.stat({ uid: resource_id }); + } + + const permission_mappings = { + "fs": fsentry ? `use ${fsentry.name} located at ${fsentry.dirpath} with ${action} access.` : null, + "thread": action === "post" ? `post to thread ${resource_id}.` : null, + "service": action === "ii" ? `use ${resource_id} to invoke ${interface_name}.` : null, + "driver": `use ${resource_id} to ${action}.`, + }; + + return permission_mappings[resource_type]; +} + +function split_permission(permission) { + return permission + .split(':') + .map(unescape_permission_component); +} + +function unescape_permission_component(component) { + let unescaped_str = ''; + // Constant for unescaped permission component string + const STATE_NORMAL = {}; + // Constant for escaping special characters in permission strings + const STATE_ESCAPE = {}; + let state = STATE_NORMAL; + const const_escapes = { C: ':' }; + for (let i = 0; i < component.length; i++) { + const c = component[i]; + if (state === STATE_NORMAL) { + if (c === '\\') { + state = STATE_ESCAPE; + } else { + unescaped_str += c; + } + } else if (state === STATE_ESCAPE) { + unescaped_str += const_escapes.hasOwnProperty(c) ? const_escapes[c] : c; + state = STATE_NORMAL; + } + } + return unescaped_str; +} + +export default UIWindowRequestPermission; diff --git a/src/puter-js/src/modules/UI.js b/src/puter-js/src/modules/UI.js index bcbc2222..5e25f40d 100644 --- a/src/puter-js/src/modules/UI.js +++ b/src/puter-js/src/modules/UI.js @@ -860,6 +860,19 @@ class UI extends EventListener { this.#postMessageWithObject('setMenubar', spec); } + requestPermission = function(options) { + return new Promise((resolve) => { + if (this.env === 'app') { + return new Promise((resolve) => { + this.#postMessageWithCallback('requestPermission', resolve, { options }); + }) + } else { + // TODO: Implement for web + resolve(false); + } + }) + } + disableMenuItem = function(item_id) { this.#postMessageWithObject('disableMenuItem', {id: item_id}); }