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
This commit is contained in:
Tanveer Brar
2025-03-14 19:37:28 -04:00
committed by GitHub
parent a278a6140b
commit 3cdbcd83b3
4 changed files with 195 additions and 124 deletions

View File

@@ -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'))

View File

@@ -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",

View File

@@ -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;
}
]
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 += `<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>`;
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)',
}
})
$(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
/**
* 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 += `<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>`;
h += `</div>`;
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;

View File

@@ -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});
}