Browse Source

feat: GUI Permission Dialog (#1177)

* Added requestPermission endpoint in SDK, updated IPC handler HeyPuter/puter#1150

* - Updated UIWindowRequestPermission.js to accept multiple permission types
- Updated dialog message for permission window in UIWindowRequestPermission.js
- Updated parameters for call to UIWindowRequestPermission in IPC.js
- Added search_uid.js endpoint to allow GUI searches for fsentry by file UUID HeyPuter#1150

* Updated body and header for Permission Request Dialog HeyPuter#1150
- Replace app uid with app name for header in UIWindowRequestPermission.js
- Added path for file permission request body in UIWindowRequestPermission.js
- Removed previously added search_uid.js api implementation previously as it is replaced by simpler call in gui

* Updated permission description generation in UIWindowRequestPermission.js HeyPuter#1150

* Removed incorrect web handling in IPC.js HeyPuter#1150

* Formatting fixes
Tanveer Brar 2 months ago
parent
commit
3cdbcd83b3

+ 1 - 1
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'))

+ 9 - 6
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", 

+ 159 - 104
src/gui/src/UI/UIWindowRequestPermission.js

@@ -17,127 +17,182 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-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;
             }
 
-        ]
+            create_permission_window(options, permission_description, resolve).then((el_window) => {
+                setup_window_events(el_window, options, resolve);
+            });
+        });
+    });
+}
 
-        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);
+/**
+ * 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);
 
-        if(driver === undefined){
-            resolve(false);
-            return;
+    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)',
         }
+    });
+}
 
-        let h = ``;
-        h += `<div>`;
-            h += `<div style="padding: 20px; width: 100%; box-sizing: border-box;">`;
-                // title
-                h += `<h1 class="perm-title">"<span style="word-break: break-word;">${html_encode(options.app_uid ?? options.origin)}</span>" would Like to use ${html_encode(driver.human_name)}</h1>`;
-                // todo show the real description of action
-                h += `<p class="perm-description">${html_encode(driver.description)}</p>`;
-                // Allow/Don't Allow
-                h += `<button type="button" class="app-auth-allow button button-primary button-block" style="margin-top: 10px;">${i18n('allow')}</button>`;
-                h += `<button type="button" class="app-auth-dont-allow button button-default button-block" style="margin-top: 10px;">${i18n('dont_allow')}</button>`;
-            h += `</div>`;
+/**
+ * Creates HTML content for permission dialog
+ */
+function create_window_content(requestingEntity, permission_description) {
+    let h = ``;
+    h += `<div>`;
+        h += `<div style="padding: 20px; width: 100%; box-sizing: border-box;">`;
+            // title
+            h += `<h1 class="perm-title">${html_encode(requestingEntity)}</h1>`;
+            
+            // show the real description of action
+            h += `<p class="perm-description">${html_encode(requestingEntity)} is requesting for permission to ${html_encode(permission_description)}</p>`;
+            
+            // Allow/Don't Allow
+            h += `<button type="button" class="app-auth-allow button button-primary button-block" style="margin-top: 10px;">${i18n('allow')}</button>`;
+            h += `<button type="button" class="app-auth-dont-allow button button-default button-block" style="margin-top: 10px;">${i18n('dont_allow')}</button>`;
         h += `</div>`;
-        
-        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)',
-            }
-        })
+    h += `</div>`;
+    return h;
+}
 
-        $(el_window).find('.app-auth-allow').on('click', async function(e){
-            $(this).addClass('disabled');
+/**
+ * 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{
-                const res = await fetch( window.api_origin + "/auth/grant-user-app", {
-                    "headers": {
+        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",
-                });
-            }catch(err){
-                console.error(err);
-                resolve(err);
+                },
+                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);
-        })
-    }) 
+    $(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
+export default UIWindowRequestPermission;

+ 13 - 0
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});
     }