diff --git a/src/gui/src/UI/UIWindowRequestPermission.js b/src/gui/src/UI/UIWindowRequestPermission.js index d3c0e2986..95a6e9b22 100644 --- a/src/gui/src/UI/UIWindowRequestPermission.js +++ b/src/gui/src/UI/UIWindowRequestPermission.js @@ -145,7 +145,7 @@ async function setup_window_events (el_window, options, resolve) { /** * Generates user-friendly description of permission string in HTML format. - * + * * @param {string} permission - The permission string to describe * @returns {string} The user-friendly description of the permission in HTML format */ @@ -231,7 +231,17 @@ async function get_permission_description (permission) { } } - return null + if ( parts[0] === 'app-root-dir' ) { + // Format: app-root-dir:resource_request_code:access + if ( parts[2] === 'read' ) { + return i18n('perm_app_root_dir_read'); + } + if ( parts[2] === 'write' ) { + return i18n('perm_app_root_dir_write'); + } + } + + return null; } /** diff --git a/src/gui/src/i18n/translations/en.js b/src/gui/src/i18n/translations/en.js index 71c8c384b..ba9752e0c 100644 --- a/src/gui/src/i18n/translations/en.js +++ b/src/gui/src/i18n/translations/en.js @@ -116,7 +116,6 @@ const en = { desktop_background_fit: 'Fit', developers: 'Developers', dir_published_as_website: '%strong% has been published to:', - directory_depth_limit_exceeded: 'Cannot create folder. The maximum directory depth has been reached. Please create the folder in a higher-level directory.', disable_2fa: 'Disable 2FA', disable_2fa_confirm: 'Are you sure you want to disable 2FA?', disable_2fa_instructions: 'Enter your password to disable 2FA.', @@ -169,7 +168,6 @@ const en = { item: 'item', items_in_trash_cannot_be_renamed: 'This item can\'t be renamed because it\'s in the trash. To rename this item, first drag it out of the Trash.', jpeg_image: 'JPEG image', - add_to_desktop: 'Add to Desktop', keep_in_taskbar: 'Keep in Taskbar', language: 'Language', license: 'License', @@ -203,6 +201,7 @@ const en = { no_dir_associated_with_site: 'No directory associated with this address.', no_websites_published: 'You have not published any websites yet. Right click on a folder to get started.', ok: 'OK', + or: 'or', open: 'Open', new_window: 'New Window', open_in_ai: 'Open in AI', @@ -483,6 +482,8 @@ const en = { // Signup Window 'signup_confirm_password': 'Confirm Password', + sign_in_with_google: 'Sign in with Google', + sign_up_with_google: 'Sign up with Google', // Login Window 'login_email_username_required': 'Email or username is required', @@ -550,6 +551,8 @@ const en = { 'perm_apps_write': 'manage your apps', 'perm_subdomains_read': 'see your subdomains', 'perm_subdomains_write': 'manage your subdomains', + 'perm_app_root_dir_read': 'read the root directory of one of your apps', + 'perm_app_root_dir_write': 'read and write to the root directory of one of your apps', 'error_user_or_path_not_found': 'User or path not found.', 'error_invalid_username': 'Invalid username.', diff --git a/src/puter-js/src/modules/Perms.js b/src/puter-js/src/modules/Perms.js index c819a9c6b..f2f4eb0df 100644 --- a/src/puter-js/src/modules/Perms.js +++ b/src/puter-js/src/modules/Perms.js @@ -21,11 +21,15 @@ export default class Perms { ...(body ? { body: JSON.stringify(body) } : {}), }); if ( resp.headers.get('content-type')?.includes('application/json') ) { - return await resp.json(); + const jsonResult = await resp.json(); + if ( resp.status !== 200 ) { + jsonResult.error = true; + } + return jsonResult; } - return { message: await resp.text(), code: 'unknown_error' }; + return { error: true, message: await resp.text(), code: 'unknown_error' }; } catch (e) { - return { message: e.message, code: 'internal_error' }; + return { error: true, message: e.message, code: 'internal_error' }; } } @@ -317,6 +321,76 @@ export default class Perms { return granted; } + /** + * Request read access to the root directory of one of the user's apps, + * identified by its resource request code (e.g. from app.resource_request_code). + * If the user grants permission, returns the filesystem item for that directory. + * If the user denies or an error occurs, returns undefined. + * + * @param {string} resourceRequestCode - The resource request code (e.g. `${app.uid}:root_dir`) + * @return {Promise} The directory fs item (stat) or undefined + */ + async requestReadAppRootDir (app_uid) { + return await this.#requestAppRootDir('read', app_uid); + } + + /** + * Request write access to the root directory of one of the user's apps, + * identified by its resource request code (e.g. from app.resource_request_code). + * If the user grants permission, returns the filesystem item for that directory. + * If the user denies or an error occurs, returns undefined. + * + * @param {string} resourceRequestCode - The resource request code (e.g. `${app.uid}:root_dir`) + * @return {Promise} The directory fs item (stat) or undefined + */ + async requestWriteAppRootDir (app_uid) { + return await this.#requestAppRootDir('write', app_uid); + } + + async #requestAppRootDir (access, app_uid) { + if ( typeof app_uid === 'object' && app_uid !== null ) { + app_uid = app_uid.uid; + } + if ( typeof app_uid !== 'string' ) { + throw new Error('parameter app_uid must be a strinkg'); + } + + let result; + const fetchIt = async () => result = await this.req_('/auth/request-app-root-dir', { + app_uid, + access: 'read', + }); + await fetchIt(); + if ( ! result.error ) return result; + + // Request permission + app_uid = (typeof app_or_uuid === 'object') && app_uid !== null + ? app_uid.uid : app_uid; + const readAppRootDirPermission = `app-root-dir:${app_uid}:read`; + const granted = await this.puter.ui.requestPermission({ + permission: readAppRootDirPermission, + }); + + if ( granted ) { + await fetchIt(); + + // If the server has cache invalidation issues, we still want this + // to work anyway. This is a hack, but also a reasonable safeguard. + let delay = 100; + const maxTotalWait = 5000; + let totalWaited = 0; + while ( result.error && totalWaited < maxTotalWait ) { + await new Promise(r => setTimeout(r, delay)); + totalWaited += delay; + await fetchIt(); + if ( ! result.error ) break; + delay = Math.min(delay * 2, Math.max(100, maxTotalWait - totalWaited)); + } + } + if ( ! result.error ) return result; + return undefined; + } + /** * Internal helper to request access to a user's special folder. * @private