From 64576e323fbec707cc155f68808426abdbfa45a3 Mon Sep 17 00:00:00 2001 From: abose Date: Mon, 16 Jun 2025 19:09:30 +0530 Subject: [PATCH 01/13] chore: jsdocs for trust_ring --- src/phoenix/trust_ring.js | 99 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/src/phoenix/trust_ring.js b/src/phoenix/trust_ring.js index 757533c3ee..9e1ca6e928 100644 --- a/src/phoenix/trust_ring.js +++ b/src/phoenix/trust_ring.js @@ -1,3 +1,102 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along + * with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + * + */ + +/** + * KernalModeTrust is a security mechanism in Phoenix that provides a trust base for core components before any + * extensions are loaded. It establishes a secure communication channel between core modules and the Tauri shell, + * preventing unauthorized access to sensitive information by extensions or other potentially malicious code. + * + * ## Purpose + * + * The primary purposes of KernalModeTrust are: + * + * 1. **Secure Boot Process**: Ensures that the application can only boot with a properly initialized trust ring. + * 2. **Secure Communication**: Enables core modules to communicate securely without worrying about interception by + * extensions. + * 3. **API Key Management**: Provides secure storage and retrieval of Phoenix API keys. + * 4. **Security Boundary**: Creates a clear security boundary between trusted core components and potentially untrusted + * extensions. + * + * ## Implementation Details + * + * ### Trust Ring Initialization + * + * The trust ring is initialized at boot time before any extensions are loaded: + * + * 1. Random AES-256 keys and initialization vectors (IV) are generated using the Web Crypto API. + * 2. These cryptographic materials are stored in the `window.KernalModeTrust` object. + * 3. The trust relationship is established with the Tauri backend via the `initTrustRing()` function. + * + * The trust ring has several important security characteristics: + * + * 1. **Memory-Only Storage**: The random AES key-based trust ring is only kept in memory and never persisted to disk. + * 2. **One-Time Use**: The trust ring is designed for one-time use and is discarded after serving its purpose. + * 3. **Session Lifetime**: It is maintained in memory only until the end of the Phoenix session. + * 4. **Tauri Communication**: The trust keys are communicated to the Tauri shell at boot time. + * 5. **API Response Encryption**: Once an AES key is trusted by the Tauri shell, all sensitive API responses will be + * encrypted with this key. This means extensions can still call sensitive APIs but will receive only encrypted + * garbage responses without access to the trust key. + * + * ### Security Model + * + * KernalModeTrust implements a strict security model: + * + * 1. **Boot-time Only Access**: The trust ring is only available to code that loads before any extensions. + * 2. **One-time Trust**: For a given Tauri window, the trust ring can only be set once. + * 3. **Deliberate Removal**: Before extensions are loaded, `window.KernalModeTrust` is deleted to prevent extensions + * from accessing it. + * 4. **Dismantling Before Restart**: The trust ring must be dismantled before restarting the application. This is a + * critical security requirement. If not dismantled, the old trust keys will still be in place when the page reloads, + * but the application will lose access to them (as they were only stored in memory). As a result, the Tauri shell + * will not trust any sensitive API calls from the reloaded page, as these calls will rely on the old keys that the + * new page instance cannot access. This security measure intentionally prevents any page reload from maintaining + * trust without explicitly dismantling the old trust ring first, ensuring that malicious code cannot bypass + * security by simply reloading the window. + * + * ### Cryptographic Implementation + * + * KernalModeTrust uses strong cryptography: + * + * 1. **AES-256 Encryption**: Uses AES-256 in GCM mode for secure encryption/decryption. + * 2. **Random Key Generation**: Cryptographically secure random number generation for keys and IVs. + * 3. **Secure Key Storage**: Keys are stored securely in the Tauri backend(which is stored in OS keychain). + * + * ## Security Considerations + * + * 1. **Extension Isolation**: Extensions should never have access to KernalModeTrust to prevent potential security + * breaches. + * + * 2. **One-time Trust**: The trust ring can only be set once per Tauri window, preventing malicious code from replacing + * it. + * + * 3. **Complete Dismantling**: When dismantling the keyring, it's recommended to reload the page immediately to prevent + * any potential exploitation of the system. + * + * 4. **Test Environment Handling**: Special handling exists for test environments to ensure tests can run properly + * without compromising security. + * + * ## Conclusion + * + * KernalModeTrust is a critical security component in Phoenix that establishes a trust boundary between core components + * and extensions. By providing secure communication channels and API key management, it helps maintain the overall + * security posture of the application. + * */ + // Generate random AES-256 key and GCM nonce/IV function generateRandomKeyAndIV() { // Generate 32 random bytes for AES-256 key From 443877c96b034a29f56b4eefe19ad74d93990e2d Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 17 Jun 2025 13:42:22 +0530 Subject: [PATCH 02/13] chore: login to phoenix code wiring in initial --- docs/API-Reference/command/Commands.md | 12 -- src/brackets.js | 1 + src/config.json | 3 +- src/extensionsIntegrated/Phoenix/main.js | 20 +-- src/nls/root/strings.js | 8 +- src/phoenix/trust_ring.js | 30 ++-- .../html/login-popup.html} | 0 .../html/profile-popup.html} | 0 src/services/login.js | 149 ++++++++++++++++++ .../Phoenix => services}/profile-menu.js | 80 ++++++++-- test/spec/Tauri-platform-test.js | 19 +-- 11 files changed, 258 insertions(+), 64 deletions(-) rename src/{extensionsIntegrated/Phoenix/html/login-dialog.html => services/html/login-popup.html} (100%) rename src/{extensionsIntegrated/Phoenix/html/profile-panel.html => services/html/profile-popup.html} (100%) create mode 100644 src/services/login.js rename src/{extensionsIntegrated/Phoenix => services}/profile-menu.js (76%) diff --git a/docs/API-Reference/command/Commands.md b/docs/API-Reference/command/Commands.md index ef97165764..783376af39 100644 --- a/docs/API-Reference/command/Commands.md +++ b/docs/API-Reference/command/Commands.md @@ -824,18 +824,6 @@ Sorts working set by file type ## CMD\_WORKING\_SORT\_TOGGLE\_AUTO Toggles automatic working set sorting -**Kind**: global variable - - -## CMD\_TOGGLE\_SHOW\_WORKING\_SET -Toggles working set visibility - -**Kind**: global variable - - -## CMD\_TOGGLE\_SHOW\_FILE\_TABS -Toggles file tabs visibility - **Kind**: global variable diff --git a/src/brackets.js b/src/brackets.js index 75aabed6d2..a21b33aae5 100644 --- a/src/brackets.js +++ b/src/brackets.js @@ -139,6 +139,7 @@ define(function (require, exports, module) { require("widgets/InlineMenu"); require("thirdparty/tinycolor"); require("utils/LocalizationUtils"); + require("services/login"); // DEPRECATED: In future we want to remove the global CodeMirror, but for now we // expose our required CodeMirror globally so as to avoid breaking extensions in the diff --git a/src/config.json b/src/config.json index a3c6c88410..632c398cc9 100644 --- a/src/config.json +++ b/src/config.json @@ -3,6 +3,7 @@ "app_title": "Phoenix Code", "app_name_about": "Phoenix Code", "about_icon": "styles/images/phoenix-icon.svg", + "account_url": "https://account.phcode.io/", "how_to_use_url": "https://github.com/adobe/brackets/wiki/How-to-Use-Brackets", "docs_url": "https://docs.phcode.dev/", "support_url": "https://github.com/phcode-dev/phoenix/discussions", @@ -59,4 +60,4 @@ "url": "https://github.com/phcode-dev/phoenix/blob/master/LICENSE" } ] -} \ No newline at end of file +} diff --git a/src/extensionsIntegrated/Phoenix/main.js b/src/extensionsIntegrated/Phoenix/main.js index 5bb8dcacbc..3bf88e91e0 100644 --- a/src/extensionsIntegrated/Phoenix/main.js +++ b/src/extensionsIntegrated/Phoenix/main.js @@ -18,7 +18,6 @@ * */ -/*global Phoenix*/ /*eslint no-console: 0*/ /*eslint strict: ["error", "global"]*/ /* jshint ignore:start */ @@ -32,26 +31,10 @@ define(function (require, exports, module) { Strings = require("strings"), Dialogs = require("widgets/Dialogs"), NotificationUI = require("widgets/NotificationUI"), - DefaultDialogs = require("widgets/DefaultDialogs"), - ProfileMenu = require("./profile-menu"); + DefaultDialogs = require("widgets/DefaultDialogs"); const PERSIST_STORAGE_DIALOG_DELAY_SECS = 60000; - let $icon; - function _addToolbarIcon() { - const helpButtonID = "user-profile-button"; - $icon = $("") - .attr({ - id: helpButtonID, - href: "#", - class: "user", - title: Strings.CMD_USER_PROFILE - }) - .appendTo($("#main-toolbar .bottom-buttons")); - $icon.on('click', ()=>{ - ProfileMenu.init(); - }); - } function _showUnSupportedBrowserDialogue() { if(Phoenix.browser.isMobile || Phoenix.browser.isTablet){ Dialogs.showModalDialog( @@ -109,7 +92,6 @@ define(function (require, exports, module) { if(Phoenix.isSpecRunnerWindow){ return; } - _addToolbarIcon(); serverSync.init(); defaultProjects.init(); newProject.init(); diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 681ec04f45..5afada8b77 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -629,7 +629,7 @@ define({ "CMD_AUTO_UPDATE": "Auto Update", "CMD_HOW_TO_USE_BRACKETS": "How to Use {APP_NAME}", "CMD_SUPPORT": "{APP_NAME} Support", - "CMD_USER_PROFILE": "User Profile", + "CMD_USER_PROFILE": "{APP_NAME} Account", "CMD_DOCS": "Help, Getting Started", "CMD_SUGGEST": "Suggest a Feature", "CMD_REPORT_ISSUE": "Report Issue", @@ -1564,5 +1564,9 @@ define({ "GIT_TOAST_MESSAGE": "Click the Git panel icon to manage your repository. Easily commit, push, pull, and view your project history—all in one place.
Learn more about the Git panel →", // surveys - "SURVEY_TITLE_VOTE_FOR_FEATURES_YOU_WANT": "Vote for the features you want to see next!" + "SURVEY_TITLE_VOTE_FOR_FEATURES_YOU_WANT": "Vote for the features you want to see next!", + + // login + "SIGNED_OUT": "You have been signed out.", + "SIGNED_OUT_MESSAGE": "You have been signed out of your {APP_NAME} account. Please sign in again to continue." }); diff --git a/src/phoenix/trust_ring.js b/src/phoenix/trust_ring.js index 9e1ca6e928..6bda953ef9 100644 --- a/src/phoenix/trust_ring.js +++ b/src/phoenix/trust_ring.js @@ -178,17 +178,18 @@ function _selectKeys() { return generateRandomKeyAndIV(); } -const PHCODE_API_KEY = "PHCODE_API_KEY"; +const CRED_KEY_API = "API_KEY"; const { key, iv } = _selectKeys(); // this key is set at boot time as a truct base for all the core components before any extensions are loaded. // just before extensions are loaded, this key is blanked. This can be used by core modules to talk with other // core modules securely without worrying about interception by extensions. // KernalModeTrust should only be available within all code that loads before the first default/any extension. window.KernalModeTrust = { + CRED_KEY_API, aesKeys: { key, iv }, - setPhoenixAPIKey, - getPhoenixAPIKey, - removePhoenixAPIKey, + setCredential, + getCredential, + removeCredential, AESDecryptString, generateRandomKeyAndIV, dismantleKeyring @@ -198,29 +199,38 @@ if(Phoenix.isSpecRunnerWindow){ } // key is 64 hex characters, iv is 24 hex characters -async function setPhoenixAPIKey(apiKey) { +async function setCredential(credKey, secret) { if(!window.__TAURI__){ throw new Error("Phoenix API key can only be set in tauri shell!"); } - return window.__TAURI__.tauri.invoke("store_credential", {scopeName: PHCODE_API_KEY, secretVal: apiKey}); + if(!credKey){ + throw new Error("credKey is required to set credential!"); + } + return window.__TAURI__.tauri.invoke("store_credential", {scopeName: credKey, secretVal: secret}); } -async function getPhoenixAPIKey() { +async function getCredential(credKey) { if(!window.__TAURI__){ throw new Error("Phoenix API key can only be get in tauri shell!"); } - const encryptedKey = await window.__TAURI__.tauri.invoke("get_credential", {scopeName: PHCODE_API_KEY}); + if(!credKey){ + throw new Error("credKey is required to get credential!"); + } + const encryptedKey = await window.__TAURI__.tauri.invoke("get_credential", {scopeName: credKey}); if(!encryptedKey){ return null; } return AESDecryptString(encryptedKey, key, iv); } -async function removePhoenixAPIKey() { +async function removeCredential(credKey) { if(!window.__TAURI__){ throw new Error("Phoenix API key can only be set in tauri shell!"); } - return window.__TAURI__.tauri.invoke("delete_credential", {scopeName: PHCODE_API_KEY}); + if(!credKey){ + throw new Error("credKey is required to remove credential!"); + } + return window.__TAURI__.tauri.invoke("delete_credential", {scopeName: credKey}); } let _dismatled = false; diff --git a/src/extensionsIntegrated/Phoenix/html/login-dialog.html b/src/services/html/login-popup.html similarity index 100% rename from src/extensionsIntegrated/Phoenix/html/login-dialog.html rename to src/services/html/login-popup.html diff --git a/src/extensionsIntegrated/Phoenix/html/profile-panel.html b/src/services/html/profile-popup.html similarity index 100% rename from src/extensionsIntegrated/Phoenix/html/profile-panel.html rename to src/services/html/profile-popup.html diff --git a/src/services/login.js b/src/services/login.js new file mode 100644 index 0000000000..e57241f075 --- /dev/null +++ b/src/services/login.js @@ -0,0 +1,149 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License along + * with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + * + */ + + +define(function (require, exports, module) { + const EventDispatcher = require("utils/EventDispatcher"), + PreferencesManager = require("preferences/PreferencesManager"), + Dialogs = require("widgets/Dialogs"), + DefaultDialogs = require("widgets/DefaultDialogs"), + Strings = require("strings"), + ProfileMenu = require("./profile-menu"); + + const KernalModeTrust = window.KernalModeTrust; + if(!KernalModeTrust){ + // integrated extensions will have access to kernal mode, but not external extensions + throw new Error("Login service should have access to KernalModeTrust. Cannot boot without trust ring"); + } + const secureExports = {}; + KernalModeTrust.loginService = secureExports; + // user profile is something like "apiKey": "uuid...", validationCode: "dfdf", "firstName":"Aa","lastName":"bb", + // "email":"aaaa@sss.com", "customerID":"uuid...","loginTime":1750074393853, + // "profileIcon":{"color":"#14b8a6","initials":"AB"} + let userProfile = null; + let isLoggedInUser = false; + + // just used as trigger to notify different windows about user profile changes + const PREF_USER_PROFILE_VERSION = "userProfileVersion"; + + + function isLoggedIn() { + return isLoggedInUser; + } + + const ERR_RETRY_LATER = "retry_later"; + const ERR_INVALID = "invalid"; + + /** + * Resolves the provided API key and verification code to user profile data + * + * @param {string} apiKey - The API key to be validated. + * @param {string} validationCode - The verification code associated with the API key. + * @return {Promise} A promise resolving to an object containing the user details if successful, + * or an error object with the relevant error code (`ERR_RETRY_LATER` or `ERR_INVALID`) if the operation fails. + * never rejects. + */ + async function _resolveAPIKey(apiKey, validationCode) { + const resolveURL = `${Phoenix.config.account_url}resolveAppSessionID?appSessionID=${apiKey}&validationCode=${validationCode}`; + if (!navigator.onLine) { + return {err: ERR_RETRY_LATER}; + } + try { + const response = await fetch(resolveURL); + if (response.status === 400 || response.status === 404) { + // 404 api key not found and 400 Bad Request, eg: verification code mismatch + return {err: ERR_INVALID}; + } else if (response.ok) { + const userDetails = await response.json(); + userDetails.apiKey = apiKey; + userDetails.validationCode = validationCode; + return {userDetails}; + } + // Other errors like 500 are retriable + console.log('Other error:', response.status); + return {err: ERR_RETRY_LATER}; + } catch (e) { + console.error(e, "Failed to call resolve API endpoint", resolveURL); + return {err: ERR_RETRY_LATER}; + } + } + + async function _verifyLogin() { + const savedUserProfile = await KernalModeTrust.getCredential(KernalModeTrust.CRED_KEY_API); + if(!savedUserProfile){ + console.log("No savedUserProfile found. Not logged in"); + ProfileMenu.setNotLoggedIn(); + return; + } + try { + userProfile = JSON.parse(savedUserProfile); + } catch (e) { + console.error(e, "Failed to parse saved user profile credentials");// this should never happen + ProfileMenu.setNotLoggedIn(); + return; // not logged in if parse fails + } + isLoggedInUser = true; + // api key is present, verify if the key is valid. but just show user that we are logged in with + // stored credentials. + ProfileMenu.setLoggedIn(userProfile.profileIcon.initials, userProfile.profileIcon.color); + const resolveResponse = await _resolveAPIKey(userProfile.apiKey, userProfile.validationCode); + if(resolveResponse.userDetails) { + // a valid user account is in place. update the stored credentials + userProfile = resolveResponse.userDetails; + ProfileMenu.setLoggedIn(userProfile.profileIcon.initials, userProfile.profileIcon.color); + await KernalModeTrust.setCredential(KernalModeTrust.CRED_KEY_API, JSON.stringify(userProfile)); + return; + } + // some error happened. + if(resolveResponse.err === ERR_INVALID) { // the api key is invalid, we need to logout and tell user + ProfileMenu.setNotLoggedIn(); + Dialogs.showModalDialog( + DefaultDialogs.DIALOG_ID_ERROR, + Strings.SIGNED_OUT, + Strings.SIGNED_OUT_MESSAGE + ); + await KernalModeTrust.removeCredential(KernalModeTrust.CRED_KEY_API); + } + // maybe some intermittent network error, ERR_RETRY_LATER is here. do nothing + } + + function init() { + ProfileMenu.init(); + if(!Phoenix.isNativeApp){ + console.warn("Login service is not supported in browser"); + return; + } + _verifyLogin().catch(console.error);// todo raise metrics + const pref = PreferencesManager.stateManager.definePreference(PREF_USER_PROFILE_VERSION, 'string', '0') + .watchExternalChanges(); + + } + + init(); + + // no sensitive apis or events should be triggered from the public exports of this module as extensions + // can read them. Always use KernalModeTrust.loginService for sensitive apis. + EventDispatcher.makeEventDispatcher(exports); + EventDispatcher.makeEventDispatcher(secureExports); + + // kernal exports + secureExports.isLoggedIn = isLoggedIn; + // public exports + exports.isLoggedIn = isLoggedIn; + +}); diff --git a/src/extensionsIntegrated/Phoenix/profile-menu.js b/src/services/profile-menu.js similarity index 76% rename from src/extensionsIntegrated/Phoenix/profile-menu.js rename to src/services/profile-menu.js index 66319d375d..9117aa28fb 100644 --- a/src/extensionsIntegrated/Phoenix/profile-menu.js +++ b/src/services/profile-menu.js @@ -1,10 +1,36 @@ define(function (require, exports, module) { - const Mustache = require("thirdparty/mustache/mustache"); - const PopUpManager = require("widgets/PopUpManager"); + const Mustache = require("thirdparty/mustache/mustache"), + PopUpManager = require("widgets/PopUpManager"), + Strings = require("strings"); + + const KernalModeTrust = window.KernalModeTrust; + if(!KernalModeTrust){ + // integrated extensions will have access to kernal mode, but not external extensions + throw new Error("profile menu should have access to KernalModeTrust. Cannot boot without trust ring"); + } + + let $icon; + + function _createSVGIcon(initials, bgColor) { + return ` + + + ${initials} + `; + } + + function _updateProfileIcon(initials, bgColor) { + $icon.empty() + .append(_createSVGIcon(initials, bgColor)); + } + + function _removeProfileIcon() { + $icon.empty(); + } // HTML templates - const loginTemplate = require("text!./html/login-dialog.html"); - const profileTemplate = require("text!./html/profile-panel.html"); + const loginTemplate = require("text!./html/login-popup.html"); + const profileTemplate = require("text!./html/profile-popup.html"); // for the popup DOM element let $popup = null; @@ -12,9 +38,6 @@ define(function (require, exports, module) { // this is to track whether the popup is visible or not let isPopupVisible = false; - // if user is logged in we show the profile menu, otherwise we show the login menu - let isLoggedIn = false; - // this is to handle document click events to close popup let documentClickHandler = null; @@ -41,14 +64,12 @@ define(function (require, exports, module) { function _handleSignInBtnClick() { console.log("User clicked sign in button"); closePopup(); // need to close the current popup to show the new one - isLoggedIn = true; showProfilePopup(); } function _handleSignOutBtnClick() { console.log("User clicked sign out"); closePopup(); - isLoggedIn = false; showLoginPopup(); } @@ -243,19 +264,56 @@ define(function (require, exports, module) { * this function is called inside the src/extensionsIntegrated/Phoenix/main.js when user clicks on the profile icon * @param {Object} data - Data to populate the templates (optional) */ - function init(data) { + function togglePopup(data) { // check if the popup is already visible or not. if visible close it if (isPopupVisible) { closePopup(); return; } - if (isLoggedIn) { + if (KernalModeTrust.loginService.isLoggedIn()) { showProfilePopup(data); } else { showLoginPopup(data); } } + function init() { + const helpButtonID = "user-profile-button"; + $icon = $("") + .attr({ + id: helpButtonID, + href: "#", + class: "user", + title: Strings.CMD_USER_PROFILE + }) + .appendTo($("#main-toolbar .bottom-buttons")); + // _updateProfileIcon("CA", "blue"); + $icon.on('click', ()=>{ + if(!Phoenix.isNativeApp){ + // in browser app, we don't currently support login + Phoenix.app.openURLInDefaultBrowser("https://account.phcode.io"); + return; + } + togglePopup(); + }); + } + + function setNotLoggedIn() { + if (isPopupVisible) { + closePopup(); + } + _removeProfileIcon(); + } + + function setLoggedIn(initial, color) { + if (isPopupVisible) { + closePopup(); + } + _updateProfileIcon(initial, color); + } + exports.init = init; + exports.setNotLoggedIn = setNotLoggedIn; + exports.setLoggedIn = setLoggedIn; }); diff --git a/test/spec/Tauri-platform-test.js b/test/spec/Tauri-platform-test.js index 1697f5d473..f7cd733506 100644 --- a/test/spec/Tauri-platform-test.js +++ b/test/spec/Tauri-platform-test.js @@ -139,6 +139,7 @@ define(function (require, exports, module) { describe("Credentials OTP API Tests", function () { const scopeName = "testScope"; const trustRing = window.specRunnerTestKernalModeTrust; + const TEST_TRUST_KEY_NAME = "testTrustKey"; function decryptCreds(creds) { return trustRing.AESDecryptString(creds, trustRing.aesKeys.key, trustRing.aesKeys.iv); @@ -253,11 +254,11 @@ define(function (require, exports, module) { expect(response).toEqual(newUUID); }); - // trustRing.getPhoenixAPIKey and set tests + // trustRing.getCredential and set tests async function setSomeKey() { const randomCred = crypto.randomUUID(); - await trustRing.setPhoenixAPIKey(randomCred); - const savedCred = await trustRing.getPhoenixAPIKey(); + await trustRing.setCredential(TEST_TRUST_KEY_NAME, randomCred); + const savedCred = await trustRing.getCredential(TEST_TRUST_KEY_NAME); expect(savedCred).toEqual(randomCred); return savedCred; } @@ -268,15 +269,15 @@ define(function (require, exports, module) { it("Should get and set empty string API key in kernal mode trust ring", async function () { const randomCred = ""; - await trustRing.setPhoenixAPIKey(randomCred); - const savedCred = await trustRing.getPhoenixAPIKey(); + await trustRing.setCredential(TEST_TRUST_KEY_NAME, randomCred); + const savedCred = await trustRing.getCredential(TEST_TRUST_KEY_NAME); expect(savedCred).toEqual(randomCred); }); it("Should remove API key in kernal mode trust ring work as expected", async function () { await setSomeKey(); - await trustRing.removePhoenixAPIKey(); - const cred = await trustRing.getPhoenixAPIKey(); + await trustRing.removeCredential(TEST_TRUST_KEY_NAME); + const cred = await trustRing.getCredential(TEST_TRUST_KEY_NAME); expect(cred).toBeNull(); }); @@ -305,12 +306,12 @@ define(function (require, exports, module) { await window.__TAURI__.tauri.invoke("trust_window_aes_key", trustRing.aesKeys); }); - it("Should getPhoenixAPIKey not work without trust", async function () { + it("Should getCredential not work without trust", async function () { await setSomeKey(); await window.__TAURI__.tauri.invoke("remove_trust_window_aes_key", trustRing.aesKeys); let error; try { - await trustRing.getPhoenixAPIKey(); + await trustRing.getCredential(TEST_TRUST_KEY_NAME); } catch (err) { error = err; } From c17863a9cd84706b7fe9d682a8ee445696dbe4d5 Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 17 Jun 2025 14:16:06 +0530 Subject: [PATCH 03/13] chore: login to phoenix code getAppAuth working --- src/nls/root/strings.js | 6 ++++- src/services/login.js | 51 ++++++++++++++++++++++++++++++++++++ src/services/profile-menu.js | 2 +- 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 5afada8b77..d58307af00 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -1568,5 +1568,9 @@ define({ // login "SIGNED_OUT": "You have been signed out.", - "SIGNED_OUT_MESSAGE": "You have been signed out of your {APP_NAME} account. Please sign in again to continue." + "SIGNED_OUT_MESSAGE": "You have been signed out of your {APP_NAME} account. Please sign in again to continue.", + "SIGNED_IN_OFFLINE_TITLE": "Offline - Cannot Sign In", + "SIGNED_IN_OFFLINE_MESSAGE": "Please connect to the internet to sign in to {APP_NAME}.", + "SIGNED_IN_FAILED_TITLE": "Cannot Sign In", + "SIGNED_IN_FAILED_MESSAGE": "Something went wrong while trying to sign in. Please try again." }); diff --git a/src/services/login.js b/src/services/login.js index e57241f075..1c8f8b54bc 100644 --- a/src/services/login.js +++ b/src/services/login.js @@ -23,6 +23,7 @@ define(function (require, exports, module) { Dialogs = require("widgets/Dialogs"), DefaultDialogs = require("widgets/DefaultDialogs"), Strings = require("strings"), + NativeApp = require("utils/NativeApp"), ProfileMenu = require("./profile-menu"); const KernalModeTrust = window.KernalModeTrust; @@ -122,6 +123,54 @@ define(function (require, exports, module) { // maybe some intermittent network error, ERR_RETRY_LATER is here. do nothing } + // never rejects. + async function _getAppAuthSession() { + const authPortURL = "9797/abc"; // todo autho auth later + const appName = encodeURIComponent(`${Strings.APP_NAME} Desktop on ${Phoenix.platform}`); + const resolveURL = `${Phoenix.config.account_url}getAppAuthSession?autoAuthPort=${authPortURL}&appName=${appName}`; + // {"isSuccess":true,"appSessionID":"a uuid...","validationCode":"SWXP07"} + try { + const response = await fetch(resolveURL); + if (response.ok) { + const {appSessionID, validationCode} = await response.json(); + if(!appSessionID || !validationCode) { + throw new Error("Invalid response from getAppAuthSession API endpoint" + resolveURL); + } + return {appSessionID, validationCode}; + } + return null; + } catch (e) { + console.error(e, "Failed to call getAppAuthSession API endpoint", resolveURL); + // todo raise metrics/log + return null; + } + } + + async function signInToAccount() { + if (!navigator.onLine) { + Dialogs.showModalDialog( + DefaultDialogs.DIALOG_ID_ERROR, + Strings.SIGNED_IN_OFFLINE_TITLE, + Strings.SIGNED_IN_OFFLINE_MESSAGE + ); + return; + } + const appAuthSession = await _getAppAuthSession(); + if(!appAuthSession) { + Dialogs.showModalDialog( + DefaultDialogs.DIALOG_ID_ERROR, + Strings.SIGNED_IN_FAILED_TITLE, + Strings.SIGNED_IN_FAILED_MESSAGE + ); + return; + } + const {appSessionID, validationCode} = appAuthSession; + const appSignInURL = `${Phoenix.config.account_url}authorizeApp?appSessionID=${appSessionID}`; + // show a dialog here with the 6 letter validation code and a button to copy the validation code and another + // button to open the sign in code + NativeApp.openURLInDefaultBrowser(appSignInURL); + } + function init() { ProfileMenu.init(); if(!Phoenix.isNativeApp){ @@ -143,6 +192,8 @@ define(function (require, exports, module) { // kernal exports secureExports.isLoggedIn = isLoggedIn; + secureExports.signInToAccount = signInToAccount; + // public exports exports.isLoggedIn = isLoggedIn; diff --git a/src/services/profile-menu.js b/src/services/profile-menu.js index 9117aa28fb..aa9e447da0 100644 --- a/src/services/profile-menu.js +++ b/src/services/profile-menu.js @@ -64,7 +64,7 @@ define(function (require, exports, module) { function _handleSignInBtnClick() { console.log("User clicked sign in button"); closePopup(); // need to close the current popup to show the new one - showProfilePopup(); + KernalModeTrust.loginService.signInToAccount(); } function _handleSignOutBtnClick() { From a6a6bba518cffd0687e4f797abb0d940ad16cc86 Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 17 Jun 2025 17:02:23 +0530 Subject: [PATCH 04/13] chore: login working initial --- src/config.json | 1 + src/nls/root/strings.js | 7 ++- src/phoenix/trust_ring.js | 2 +- src/services/html/otp-dialog.html | 18 +++++++ src/services/login.js | 90 +++++++++++++++++++++++++++++-- src/services/profile-menu.js | 6 +-- 6 files changed, 114 insertions(+), 10 deletions(-) create mode 100644 src/services/html/otp-dialog.html diff --git a/src/config.json b/src/config.json index 632c398cc9..00e2932caf 100644 --- a/src/config.json +++ b/src/config.json @@ -7,6 +7,7 @@ "how_to_use_url": "https://github.com/adobe/brackets/wiki/How-to-Use-Brackets", "docs_url": "https://docs.phcode.dev/", "support_url": "https://github.com/phcode-dev/phoenix/discussions", + "support_url_account": "https://account.phcode.io/?returnUrl=https%3A%2F%2Faccount.phcode.io%2F%23support", "suggest_feature_url": "https://github.com/phcode-dev/phoenix/discussions/categories/ideas", "report_issue_url": "https://github.com/phcode-dev/phoenix/issues/new/choose", "get_involved_url": "https://github.com/phcode-dev/phoenix/discussions/77", diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index d58307af00..78cd969af0 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -1572,5 +1572,10 @@ define({ "SIGNED_IN_OFFLINE_TITLE": "Offline - Cannot Sign In", "SIGNED_IN_OFFLINE_MESSAGE": "Please connect to the internet to sign in to {APP_NAME}.", "SIGNED_IN_FAILED_TITLE": "Cannot Sign In", - "SIGNED_IN_FAILED_MESSAGE": "Something went wrong while trying to sign in. Please try again." + "SIGNED_IN_FAILED_MESSAGE": "Something went wrong while trying to sign in. Please try again.", + "VALIDATION_CODE_TITLE": "Sign In Verification Code", + "VALIDATION_CODE_MESSAGE": "Please use this Verification code to sign in to your {APP_NAME} account:", + "COPY_VALIDATION_CODE": "Copy Code", + "VALIDATION_CODE_COPIED": "Copied", + "OPEN_SIGN_IN_URL": "Open Sign In Page" }); diff --git a/src/phoenix/trust_ring.js b/src/phoenix/trust_ring.js index 6bda953ef9..0cd11fa29f 100644 --- a/src/phoenix/trust_ring.js +++ b/src/phoenix/trust_ring.js @@ -235,7 +235,7 @@ async function removeCredential(credKey) { let _dismatled = false; async function dismantleKeyring() { - if(!_dismatled){ + if(_dismatled){ throw new Error("Keyring can only be dismantled once!"); // and once dismantled, the next line should be reload page. this is a strict security posture requirement to // prevent extensions from stealing sensitive info from system key ring as once the trust in invalidated, diff --git a/src/services/html/otp-dialog.html b/src/services/html/otp-dialog.html new file mode 100644 index 0000000000..8d207912bd --- /dev/null +++ b/src/services/html/otp-dialog.html @@ -0,0 +1,18 @@ + diff --git a/src/services/login.js b/src/services/login.js index 1c8f8b54bc..08a7376e81 100644 --- a/src/services/login.js +++ b/src/services/login.js @@ -24,7 +24,9 @@ define(function (require, exports, module) { DefaultDialogs = require("widgets/DefaultDialogs"), Strings = require("strings"), NativeApp = require("utils/NativeApp"), - ProfileMenu = require("./profile-menu"); + ProfileMenu = require("./profile-menu"), + Mustache = require("thirdparty/mustache/mustache"), + otpDialogTemplate = require("text!./html/otp-dialog.html"); const KernalModeTrust = window.KernalModeTrust; if(!KernalModeTrust){ @@ -42,6 +44,14 @@ define(function (require, exports, module) { // just used as trigger to notify different windows about user profile changes const PREF_USER_PROFILE_VERSION = "userProfileVersion"; + EventDispatcher.makeEventDispatcher(exports); + EventDispatcher.makeEventDispatcher(secureExports); + + const _EVT_PAGE_FOCUSED = "page_focused"; + $(window).focus(function () { + exports.trigger(_EVT_PAGE_FOCUSED); + }); + function isLoggedIn() { return isLoggedInUser; @@ -112,6 +122,7 @@ define(function (require, exports, module) { } // some error happened. if(resolveResponse.err === ERR_INVALID) { // the api key is invalid, we need to logout and tell user + isLoggedInUser = false; ProfileMenu.setNotLoggedIn(); Dialogs.showModalDialog( DefaultDialogs.DIALOG_ID_ERROR, @@ -166,8 +177,79 @@ define(function (require, exports, module) { } const {appSessionID, validationCode} = appAuthSession; const appSignInURL = `${Phoenix.config.account_url}authorizeApp?appSessionID=${appSessionID}`; - // show a dialog here with the 6 letter validation code and a button to copy the validation code and another - // button to open the sign in code + + // Show dialog with validation code + const dialogData = { + validationCode: validationCode, + Strings: Strings + }; + + const $template = $(Mustache.render(otpDialogTemplate, dialogData)); + const dialog = Dialogs.showModalDialogUsingTemplate($template); + + // Set timeout to close dialog after 5 minutes, as validity is only 5 mins + const closeTimeout = setTimeout(() => { + dialog.close(); + }, 5 * 60 * 1000); + + // Handle button clicks + $template.on('click', '[data-button-id="copy"]', function() { + Phoenix.app.copyToClipboard(validationCode); + + // Show "Copied" feedback + const $validationCodeSpan = $template.find('.validation-code span'); + const originalText = $validationCodeSpan.text(); + + // Replace validation code with "Copied" text + $validationCodeSpan.text(Strings.VALIDATION_CODE_COPIED); + + // Restore original validation code after 1.5 seconds + setTimeout(() => { + $validationCodeSpan.text(originalText); + }, 1500); + }); + + $template.on('click', '[data-button-id="open"]', function() { + NativeApp.openURLInDefaultBrowser(appSignInURL); + }); + $template.on('click', '[data-button-id="cancel"]', function() { + dialog.close(); + }); + + let checking = false, checkAgain = false; + async function checkLoginStatus() { + if(checking) { + checkAgain = true; + return; + } + checking = true; + try { + const resolveResponse = await _resolveAPIKey(appSessionID, validationCode); + if(resolveResponse.userDetails) { + // the user has validated the creds + userProfile = resolveResponse.userDetails; + ProfileMenu.setLoggedIn(userProfile.profileIcon.initials, userProfile.profileIcon.color); + await KernalModeTrust.setCredential(KernalModeTrust.CRED_KEY_API, JSON.stringify(userProfile)); + checkAgain = false; + isLoggedInUser = true; + dialog.close(); + } + } catch (e) { + console.error("Failed to check login status.", e); + } + checking = false; + if(checkAgain) { + checkAgain = false; + setTimeout(checkLoginStatus, 100); + } + } + exports.on(_EVT_PAGE_FOCUSED, checkLoginStatus); + + // Clean up when dialog is closed + dialog.done(function() { + exports.off(_EVT_PAGE_FOCUSED, checkLoginStatus); + clearTimeout(closeTimeout); + }); NativeApp.openURLInDefaultBrowser(appSignInURL); } @@ -187,8 +269,6 @@ define(function (require, exports, module) { // no sensitive apis or events should be triggered from the public exports of this module as extensions // can read them. Always use KernalModeTrust.loginService for sensitive apis. - EventDispatcher.makeEventDispatcher(exports); - EventDispatcher.makeEventDispatcher(secureExports); // kernal exports secureExports.isLoggedIn = isLoggedIn; diff --git a/src/services/profile-menu.js b/src/services/profile-menu.js index aa9e447da0..d7310cb6f0 100644 --- a/src/services/profile-menu.js +++ b/src/services/profile-menu.js @@ -13,7 +13,7 @@ define(function (require, exports, module) { function _createSVGIcon(initials, bgColor) { return ` - + ${initials} `; @@ -74,11 +74,11 @@ define(function (require, exports, module) { } function _handleContactSupportBtnClick() { - Phoenix.app.openURLInDefaultBrowser(brackets.config.support_url); + Phoenix.app.openURLInDefaultBrowser(brackets.config.support_url_account); } function _handleAccountDetailsBtnClick() { - console.log("User clicked account details"); + Phoenix.app.openURLInDefaultBrowser(brackets.config.account_url); } /** From 1741a67c680056ed64ea0cdd5b4b803f6ad7ccb4 Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 17 Jun 2025 17:43:44 +0530 Subject: [PATCH 05/13] chore: signout working --- src/nls/root/strings.js | 3 ++ src/services/login.js | 56 ++++++++++++++++++++++++++++++++++-- src/services/profile-menu.js | 4 +-- 3 files changed, 57 insertions(+), 6 deletions(-) diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 78cd969af0..6a28b56b08 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -1569,10 +1569,13 @@ define({ // login "SIGNED_OUT": "You have been signed out.", "SIGNED_OUT_MESSAGE": "You have been signed out of your {APP_NAME} account. Please sign in again to continue.", + "SIGNED_OUT_MESSAGE_FRIENDLY": "Thank you for using {APP_NAME}. See you soon!", "SIGNED_IN_OFFLINE_TITLE": "Offline - Cannot Sign In", "SIGNED_IN_OFFLINE_MESSAGE": "Please connect to the internet to sign in to {APP_NAME}.", "SIGNED_IN_FAILED_TITLE": "Cannot Sign In", "SIGNED_IN_FAILED_MESSAGE": "Something went wrong while trying to sign in. Please try again.", + "SIGNED_OUT_FAILED_TITLE": "Failed to Sign Out", + "SIGNED_OUT_FAILED_MESSAGE": "Something went wrong while trying to sign out. Please try again.", "VALIDATION_CODE_TITLE": "Sign In Verification Code", "VALIDATION_CODE_MESSAGE": "Please use this Verification code to sign in to your {APP_NAME} account:", "COPY_VALIDATION_CODE": "Copy Code", diff --git a/src/services/login.js b/src/services/login.js index 08a7376e81..a4b1051a08 100644 --- a/src/services/login.js +++ b/src/services/login.js @@ -94,6 +94,12 @@ define(function (require, exports, module) { } } + async function _resetAccountLogin() { + isLoggedInUser = false; + ProfileMenu.setNotLoggedIn(); + await KernalModeTrust.removeCredential(KernalModeTrust.CRED_KEY_API); + } + async function _verifyLogin() { const savedUserProfile = await KernalModeTrust.getCredential(KernalModeTrust.CRED_KEY_API); if(!savedUserProfile){ @@ -122,14 +128,13 @@ define(function (require, exports, module) { } // some error happened. if(resolveResponse.err === ERR_INVALID) { // the api key is invalid, we need to logout and tell user - isLoggedInUser = false; - ProfileMenu.setNotLoggedIn(); + _resetAccountLogin() + .catch(console.error); Dialogs.showModalDialog( DefaultDialogs.DIALOG_ID_ERROR, Strings.SIGNED_OUT, Strings.SIGNED_OUT_MESSAGE ); - await KernalModeTrust.removeCredential(KernalModeTrust.CRED_KEY_API); } // maybe some intermittent network error, ERR_RETRY_LATER is here. do nothing } @@ -253,6 +258,50 @@ define(function (require, exports, module) { NativeApp.openURLInDefaultBrowser(appSignInURL); } + async function signOutAccount() { + try { + const resolveURL = `${Phoenix.config.account_url}logoutSession`; + let input = { + appSessionID: userProfile.apiKey + }; + + const response = await fetch(resolveURL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(input) + }); + + const result = await response.json(); + + if (!result.isSuccess) { + console.error('Error logging out', result); + Dialogs.showModalDialog( + DefaultDialogs.DIALOG_ID_ERROR, + Strings.SIGNED_OUT_FAILED_TITLE, + Strings.SIGNED_OUT_FAILED_MESSAGE + ); + return; + // todo raise metrics + } + await _resetAccountLogin(); + Dialogs.showModalDialog( + DefaultDialogs.DIALOG_ID_INFO, + Strings.SIGNED_OUT, + Strings.SIGNED_OUT_MESSAGE_FRIENDLY + ); + } catch (error) { + console.error("Network error. Could not log out session.", error); + Dialogs.showModalDialog( + DefaultDialogs.DIALOG_ID_ERROR, + Strings.SIGNED_OUT_FAILED_TITLE, + Strings.SIGNED_OUT_FAILED_MESSAGE + ); + // todo raise metrics + } + } + function init() { ProfileMenu.init(); if(!Phoenix.isNativeApp){ @@ -273,6 +322,7 @@ define(function (require, exports, module) { // kernal exports secureExports.isLoggedIn = isLoggedIn; secureExports.signInToAccount = signInToAccount; + secureExports.signOutAccount = signOutAccount; // public exports exports.isLoggedIn = isLoggedIn; diff --git a/src/services/profile-menu.js b/src/services/profile-menu.js index d7310cb6f0..be231e4f68 100644 --- a/src/services/profile-menu.js +++ b/src/services/profile-menu.js @@ -62,15 +62,13 @@ define(function (require, exports, module) { }; function _handleSignInBtnClick() { - console.log("User clicked sign in button"); closePopup(); // need to close the current popup to show the new one KernalModeTrust.loginService.signInToAccount(); } function _handleSignOutBtnClick() { - console.log("User clicked sign out"); closePopup(); - showLoginPopup(); + KernalModeTrust.loginService.signOutAccount(); } function _handleContactSupportBtnClick() { From 949c4bc1fb1d4e6c9531d9836de8b5f00ecc3fb3 Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 17 Jun 2025 18:17:37 +0530 Subject: [PATCH 06/13] feat: multi window signin signout working --- src/services/login.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/services/login.js b/src/services/login.js index a4b1051a08..0fb08324ce 100644 --- a/src/services/login.js +++ b/src/services/login.js @@ -98,6 +98,8 @@ define(function (require, exports, module) { isLoggedInUser = false; ProfileMenu.setNotLoggedIn(); await KernalModeTrust.removeCredential(KernalModeTrust.CRED_KEY_API); + // bump the version so that in multi windows, the other window gets notified of the change + PreferencesManager.stateManager.set(PREF_USER_PROFILE_VERSION, crypto.randomUUID()); } async function _verifyLogin() { @@ -105,6 +107,7 @@ define(function (require, exports, module) { if(!savedUserProfile){ console.log("No savedUserProfile found. Not logged in"); ProfileMenu.setNotLoggedIn(); + isLoggedInUser = false; return; } try { @@ -124,6 +127,8 @@ define(function (require, exports, module) { userProfile = resolveResponse.userDetails; ProfileMenu.setLoggedIn(userProfile.profileIcon.initials, userProfile.profileIcon.color); await KernalModeTrust.setCredential(KernalModeTrust.CRED_KEY_API, JSON.stringify(userProfile)); + // we dont need to bump the PREF_USER_PROFILE_VERSION here as its just a cred update + // (maybe name) and may lead to infi loops. return; } // some error happened. @@ -235,6 +240,8 @@ define(function (require, exports, module) { userProfile = resolveResponse.userDetails; ProfileMenu.setLoggedIn(userProfile.profileIcon.initials, userProfile.profileIcon.color); await KernalModeTrust.setCredential(KernalModeTrust.CRED_KEY_API, JSON.stringify(userProfile)); + // bump the version so that in multi windows, the other window gets notified of the change + PreferencesManager.stateManager.set(PREF_USER_PROFILE_VERSION, crypto.randomUUID()); checkAgain = false; isLoggedInUser = true; dialog.close(); @@ -309,9 +316,9 @@ define(function (require, exports, module) { return; } _verifyLogin().catch(console.error);// todo raise metrics - const pref = PreferencesManager.stateManager.definePreference(PREF_USER_PROFILE_VERSION, 'string', '0') - .watchExternalChanges(); - + const pref = PreferencesManager.stateManager.definePreference(PREF_USER_PROFILE_VERSION, 'string', '0'); + pref.watchExternalChanges(); + pref.on('change', _verifyLogin); } init(); From 59ed7c10f8c7e1d677dfb85a002ae4beabf4f804 Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 17 Jun 2025 19:26:07 +0530 Subject: [PATCH 07/13] chore: support url is now single --- src/config.json | 3 +-- src/services/profile-menu.js | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/config.json b/src/config.json index 00e2932caf..96b5475764 100644 --- a/src/config.json +++ b/src/config.json @@ -6,8 +6,7 @@ "account_url": "https://account.phcode.io/", "how_to_use_url": "https://github.com/adobe/brackets/wiki/How-to-Use-Brackets", "docs_url": "https://docs.phcode.dev/", - "support_url": "https://github.com/phcode-dev/phoenix/discussions", - "support_url_account": "https://account.phcode.io/?returnUrl=https%3A%2F%2Faccount.phcode.io%2F%23support", + "support_url": "https://account.phcode.io/?returnUrl=https%3A%2F%2Faccount.phcode.io%2F%23support", "suggest_feature_url": "https://github.com/phcode-dev/phoenix/discussions/categories/ideas", "report_issue_url": "https://github.com/phcode-dev/phoenix/issues/new/choose", "get_involved_url": "https://github.com/phcode-dev/phoenix/discussions/77", diff --git a/src/services/profile-menu.js b/src/services/profile-menu.js index be231e4f68..4890ad7be8 100644 --- a/src/services/profile-menu.js +++ b/src/services/profile-menu.js @@ -72,7 +72,7 @@ define(function (require, exports, module) { } function _handleContactSupportBtnClick() { - Phoenix.app.openURLInDefaultBrowser(brackets.config.support_url_account); + Phoenix.app.openURLInDefaultBrowser(brackets.config.support_url); } function _handleAccountDetailsBtnClick() { From 88fb5803b876d3479fa1bdd0df8e1450c9d2b392 Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 17 Jun 2025 19:45:54 +0530 Subject: [PATCH 08/13] refactor: login popup strings --- src/nls/root/strings.js | 5 ++++- src/services/html/login-popup.html | 6 +++--- src/services/profile-menu.js | 10 +++------- src/styles/{Extn-UserProfile.less => UserProfile.less} | 0 src/styles/brackets.less | 2 +- 5 files changed, 11 insertions(+), 12 deletions(-) rename src/styles/{Extn-UserProfile.less => UserProfile.less} (100%) diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 6a28b56b08..29fdc1bb52 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -1580,5 +1580,8 @@ define({ "VALIDATION_CODE_MESSAGE": "Please use this Verification code to sign in to your {APP_NAME} account:", "COPY_VALIDATION_CODE": "Copy Code", "VALIDATION_CODE_COPIED": "Copied", - "OPEN_SIGN_IN_URL": "Open Sign In Page" + "OPEN_SIGN_IN_URL": "Open Sign In Page", + "PROFILE_POP_TITLE": "{APP_NAME} Account", + "PROFILE_SIGN_IN": "Sign in to your account", + "CONTACT_SUPPORT": "Contact support" }); diff --git a/src/services/html/login-popup.html b/src/services/html/login-popup.html index 9726584e22..0e0de2569b 100644 --- a/src/services/html/login-popup.html +++ b/src/services/html/login-popup.html @@ -1,16 +1,16 @@
diff --git a/src/services/profile-menu.js b/src/services/profile-menu.js index 4890ad7be8..cf40e06d3d 100644 --- a/src/services/profile-menu.js +++ b/src/services/profile-menu.js @@ -153,23 +153,19 @@ define(function (require, exports, module) { /** * Shows the sign-in popup when the user is not logged in - * @param {Object} loginData - Data to populate the template (optional) */ - function showLoginPopup(loginData) { + function showLoginPopup() { // If popup is already visible, just close it if (isPopupVisible) { closePopup(); return; } - // Merge provided data with defaults - const templateData = $.extend({}, defaultLoginData, loginData || {}); - // create the popup element closePopup(); // close any existing popup first // Render template with data - const renderedTemplate = Mustache.render(loginTemplate, templateData); + const renderedTemplate = Mustache.render(loginTemplate, {Strings}); $popup = $(renderedTemplate); $("body").append($popup); @@ -272,7 +268,7 @@ define(function (require, exports, module) { if (KernalModeTrust.loginService.isLoggedIn()) { showProfilePopup(data); } else { - showLoginPopup(data); + showLoginPopup(); } } diff --git a/src/styles/Extn-UserProfile.less b/src/styles/UserProfile.less similarity index 100% rename from src/styles/Extn-UserProfile.less rename to src/styles/UserProfile.less diff --git a/src/styles/brackets.less b/src/styles/brackets.less index 499a512e51..502ece88fb 100644 --- a/src/styles/brackets.less +++ b/src/styles/brackets.less @@ -44,7 +44,7 @@ @import "Extn-TabBar.less"; @import "Extn-DisplayShortcuts.less"; @import "Extn-CSSColorPreview.less"; -@import "Extn-UserProfile.less"; +@import "UserProfile.less"; /* Overall layout */ From 589d55458516735a9d7393b43cda17851f208105 Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 17 Jun 2025 21:13:14 +0530 Subject: [PATCH 09/13] chore: fill in correct profile details --- src/nls/root/strings.js | 5 ++- src/services/html/profile-popup.html | 13 ++++---- src/services/login.js | 5 +++ src/services/profile-menu.js | 48 ++++++++++------------------ src/styles/UserProfile.less | 28 +++++++++++++--- 5 files changed, 57 insertions(+), 42 deletions(-) diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 29fdc1bb52..49fc226c2c 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -1583,5 +1583,8 @@ define({ "OPEN_SIGN_IN_URL": "Open Sign In Page", "PROFILE_POP_TITLE": "{APP_NAME} Account", "PROFILE_SIGN_IN": "Sign in to your account", - "CONTACT_SUPPORT": "Contact support" + "CONTACT_SUPPORT": "Contact support", + "SIGN_OUT": "Sign out", + "ACCOUNT_DETAILS": "Account Details", + "AI_QUOTA_USED": "AI quota used" }); diff --git a/src/services/html/profile-popup.html b/src/services/html/profile-popup.html index 1677c3d59b..f7b46e85ea 100644 --- a/src/services/html/profile-popup.html +++ b/src/services/html/profile-popup.html @@ -1,19 +1,20 @@