mirror of
https://github.com/HeyPuter/puter.git
synced 2025-12-30 17:50:00 -06:00
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:
@@ -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'))
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user