diff --git a/src/gui/src/UI/Dashboard/TabSecurity.js b/src/gui/src/UI/Dashboard/TabSecurity.js
index 055853755..45244370e 100644
--- a/src/gui/src/UI/Dashboard/TabSecurity.js
+++ b/src/gui/src/UI/Dashboard/TabSecurity.js
@@ -17,8 +17,7 @@
* along with this program. If not, see .
*/
-import TeePromise from '../../util/TeePromise.js';
-import UIComponentWindow from '../UIComponentWindow.js';
+import UIWindowDisable2FA from '../Settings/UIWindowDisable2FA.js';
import UIWindow2FASetup from '../UIWindow2FASetup.js';
import UIWindowChangePassword from '../UIWindowChangePassword.js';
import UIWindowManageSessions from '../UIWindowManageSessions.js';
@@ -148,142 +147,25 @@ const TabSecurity = {
});
$el_window.find('.dashboard-section-security .disable-2fa').on('click', async function (e) {
- let win;
- const password_confirm_promise = new TeePromise();
-
- function openRevalidatePopup (revalidateUrl, onDone) {
- const url = revalidateUrl || (window.user && window.user.oidc_revalidate_url);
- if ( ! url ) {
- onDone && onDone(new Error('No revalidate URL'));
- return null;
- }
- let doneCalled = false;
- const hint = $win.find('.disable-2fa-oidc-hint');
- hint.text(i18n('revalidate_sign_in_popup') || 'Sign in with your linked account in the popup.').show();
- const popup = window.open(url, 'puter-revalidate', 'width=500,height=600');
- const onMessage = (ev) => {
- if ( (ev.origin !== window.gui_origin) && (ev.origin !== window.location.origin) ) return;
- if ( !ev.data || ev.data.type !== 'puter-revalidate-done' ) return;
- if ( doneCalled ) return;
- doneCalled = true;
- window.removeEventListener('message', onMessage);
- hint.hide();
- onDone && onDone();
- };
- window.addEventListener('message', onMessage);
- const checkClosed = setInterval(() => {
- if ( popup && popup.closed ) {
- clearInterval(checkClosed);
- window.removeEventListener('message', onMessage);
- hint.hide();
- if ( ! doneCalled ) {
- doneCalled = true;
- onDone && onDone(new Error('Popup closed'));
- }
- }
- }, 300);
- return popup;
- }
-
- const doRequest = () => fetch(`${window.api_origin}/user-protected/disable-2fa`, {
- method: 'POST',
- credentials: 'include',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ password: $win.find('.password-entry').val() }),
- });
-
- const try_password = async () => {
- const resp = await doRequest();
- if ( resp.status === 200 ) {
- password_confirm_promise.resolve(true);
- $(win).close();
- return;
- }
- const data = await resp.json().catch(() => ({}));
- if ( data.code === 'oidc_revalidation_required' && data.revalidate_url ) {
- openRevalidatePopup(data.revalidate_url, async (err) => {
- if ( err ) {
- $win.find('.error-message').text(err.message || 'Re-validation required.').show();
- return;
- }
- const r2 = await doRequest();
- if ( r2.status === 200 ) {
- password_confirm_promise.resolve(true);
- $(win).close();
- } else {
- let message; try {
- message = (await r2.json()).message;
- } catch (e) {
- }
- $win.find('.error-message').text(message || i18n('error_unknown_cause')).show();
- }
- });
- return;
- }
- const message = data.message || i18n('error_unknown_cause');
- $win.find('.password-entry').addClass('error');
- $win.find('.error-message').text(message).show();
- };
-
- let h = '';
- h += '
';
- h += '
';
- h += `
${i18n('disable_2fa_confirm')} `;
- h += `
${i18n('disable_2fa_instructions')}
`;
- h += '
';
- h += '
';
- h += '
';
- h += '
';
- h += '
';
- h += '
';
- h += '
';
- h += `${i18n('disable_2fa')} `;
- h += `${i18n('cancel')} `;
- h += '
';
- h += '
';
-
- win = await UIComponentWindow({
- html: h,
- width: 500,
- backdrop: true,
- is_resizable: false,
- body_css: {
- width: 'initial',
- 'background-color': 'var(--dashboard-input-background)',
- 'backdrop-filter': 'blur(3px)',
- padding: '20px',
+ const { promise } = await UIWindowDisable2FA({
+ window_options: {
+ parent_uuid: $el_window.attr('data-element_uuid'),
+ backdrop: true,
+ close_on_backdrop_click: true,
+ parent_center: true,
+ stay_on_top: true,
+ has_head: false,
},
});
+ const tfa_was_disabled = await promise;
- // Set up event listeners
- const $win = $(win);
- const $password_entry = $win.find('.password-entry');
-
- $password_entry.on('keypress', (e) => {
- if ( e.which === 13 ) { // Enter key
- try_password();
- }
- });
-
- $win.find('.confirm-disable-2fa').on('click', () => {
- try_password();
- });
-
- $win.find('.cancel-disable-2fa').on('click', () => {
- password_confirm_promise.resolve(false);
- $win.close();
- });
-
- $password_entry.focus();
-
- const ok = await password_confirm_promise;
- if ( ! ok ) return;
-
- $el_window.find('.dashboard-section-security .enable-2fa').show();
- $el_window.find('.dashboard-section-security .disable-2fa').hide();
- $el_window.find('.dashboard-section-security .user-otp-state').text(i18n('two_factor_disabled'));
- $el_window.find('.dashboard-section-security .dashboard-settings-card-2fa').removeClass('dashboard-settings-card-success');
- $el_window.find('.dashboard-section-security .dashboard-settings-card-2fa').addClass('dashboard-settings-card-warning');
+ if ( tfa_was_disabled ) {
+ $el_window.find('.dashboard-section-security .enable-2fa').show();
+ $el_window.find('.dashboard-section-security .disable-2fa').hide();
+ $el_window.find('.dashboard-section-security .user-otp-state').text(i18n('two_factor_disabled'));
+ $el_window.find('.dashboard-section-security .dashboard-settings-card-2fa').removeClass('dashboard-settings-card-success');
+ $el_window.find('.dashboard-section-security .dashboard-settings-card-2fa').addClass('dashboard-settings-card-warning');
+ }
});
},
};
diff --git a/src/gui/src/UI/Settings/UITabSecurity.js b/src/gui/src/UI/Settings/UITabSecurity.js
index 4e5711bbd..342a3e960 100644
--- a/src/gui/src/UI/Settings/UITabSecurity.js
+++ b/src/gui/src/UI/Settings/UITabSecurity.js
@@ -16,9 +16,8 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
-import TeePromise from '../../util/TeePromise.js';
-import UIComponentWindow from '../UIComponentWindow.js';
import UIWindow2FASetup from '../UIWindow2FASetup.js';
+import UIWindowDisable2FA from './UIWindowDisable2FA.js';
export default {
id: 'security',
@@ -82,146 +81,16 @@ export default {
});
$el_window.find('.disable-2fa').on('click', async function (e) {
- let win;
- const password_confirm_promise = new TeePromise();
+ const { promise } = await UIWindowDisable2FA();
+ const tfa_was_disabled = await promise;
- function openRevalidatePopup ($win, revalidateUrl, onDone) {
- const url = revalidateUrl || (window.user && window.user.oidc_revalidate_url);
- if ( ! url ) {
- onDone && onDone(new Error('No revalidate URL'));
- return null;
- }
- let doneCalled = false;
- const hint = $win.find('.disable-2fa-oidc-hint');
- hint.text(i18n('revalidate_sign_in_popup') || 'Sign in with your linked account in the popup.').show();
- const popup = window.open(url, 'puter-revalidate', 'width=500,height=600');
- const onMessage = (ev) => {
- if ( (ev.origin !== window.gui_origin) && (ev.origin !== window.location.origin) ) return;
- if ( !ev.data || ev.data.type !== 'puter-revalidate-done' ) return;
- if ( doneCalled ) return;
- doneCalled = true;
- window.removeEventListener('message', onMessage);
- hint.hide();
- onDone && onDone();
- };
- window.addEventListener('message', onMessage);
- const checkClosed = setInterval(() => {
- if ( popup && popup.closed ) {
- clearInterval(checkClosed);
- window.removeEventListener('message', onMessage);
- hint.hide();
- if ( ! doneCalled ) {
- doneCalled = true;
- onDone && onDone(new Error('Popup closed'));
- }
- }
- }, 300);
- return popup;
+ if ( tfa_was_disabled ) {
+ $el_window.find('.enable-2fa').show();
+ $el_window.find('.disable-2fa').hide();
+ $el_window.find('.user-otp-state').text(i18n('two_factor_disabled'));
+ $el_window.find('.settings-card-security').removeClass('settings-card-success');
+ $el_window.find('.settings-card-security').addClass('settings-card-warning');
}
-
- const doRequest = () => fetch(`${window.api_origin}/user-protected/disable-2fa`, {
- method: 'POST',
- credentials: 'include',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- password: win ? $(win).find('.password-entry').val() : '',
- }),
- });
-
- const try_password = async () => {
- const resp = await doRequest();
- if ( resp.status === 200 ) {
- password_confirm_promise.resolve(true);
- $(win).close();
- return;
- }
- const data = await resp.json().catch(() => ({}));
- const $win = $(win);
- if ( data.code === 'oidc_revalidation_required' && data.revalidate_url ) {
- openRevalidatePopup($win, data.revalidate_url, async (err) => {
- if ( err ) {
- $win.find('.error-message').text(err.message || 'Re-validation required.').show();
- return;
- }
- const r2 = await doRequest();
- if ( r2.status === 200 ) {
- password_confirm_promise.resolve(true);
- $(win).close();
- } else {
- let message; try {
- message = (await r2.json()).message;
- } catch (e) {
- }
- $win.find('.error-message').text(message || i18n('error_unknown_cause')).show();
- }
- });
- return;
- }
- $win.find('.password-entry').addClass('error');
- $win.find('.error-message').text(data.message || i18n('error_unknown_cause')).show();
- };
-
- const oidc_only = !!(window.user && window.user.oidc_only);
- let h = '';
- h += '';
- h += '
';
- h += `
${i18n('disable_2fa_confirm')} `;
- h += `
${i18n('disable_2fa_instructions')}
`;
- if ( oidc_only ) {
- h += `
${i18n('revalidate_flow_notice')}
`;
- }
- h += '
';
- h += '
';
- h += '
';
- h += '
';
- h += '
';
- h += `
${i18n('disable_2fa')} `;
- h += `
${i18n('cancel')} `;
- h += '
';
- h += '
';
-
- win = await UIComponentWindow({
- html: h,
- width: 500,
- backdrop: true,
- is_resizable: false,
- body_css: {
- width: 'initial',
- 'background-color': 'rgb(245 247 249)',
- 'backdrop-filter': 'blur(3px)',
- padding: '20px',
- },
- });
-
- // Set up event listeners
- const $win = $(win);
- const $password_entry = $win.find('.password-entry');
-
- $password_entry.on('keypress', (e) => {
- if ( e.which === 13 ) { // Enter key
- try_password();
- }
- });
-
- $win.find('.confirm-disable-2fa').on('click', () => {
- try_password();
- });
-
- $win.find('.cancel-disable-2fa').on('click', () => {
- password_confirm_promise.resolve(false);
- $win.close();
- });
-
- $password_entry.focus();
-
- const ok = await password_confirm_promise;
- if ( ! ok ) return;
-
- $el_window.find('.enable-2fa').show();
- $el_window.find('.disable-2fa').hide();
- $el_window.find('.user-otp-state').text(i18n('two_factor_disabled'));
- $el_window.find('.settings-card-security').removeClass('settings-card-success');
- $el_window.find('.settings-card-security').addClass('settings-card-warning');
});
},
};
diff --git a/src/gui/src/UI/Settings/UIWindowDisable2FA.js b/src/gui/src/UI/Settings/UIWindowDisable2FA.js
new file mode 100644
index 000000000..57b52fae2
--- /dev/null
+++ b/src/gui/src/UI/Settings/UIWindowDisable2FA.js
@@ -0,0 +1,207 @@
+/**
+ * Copyright (C) 2026-present Puter Technologies Inc.
+ *
+ * This file is part of Puter.
+ *
+ * Puter 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 .
+ */
+
+import { openRevalidatePopup } from '../../util/openid.js';
+import Placeholder from '../../util/Placeholder.js';
+import TeePromise from '../../util/TeePromise.js';
+import PasswordEntry from '../Components/PasswordEntry.js';
+import UIWindow from '../UIWindow.js';
+
+async function UIWindowDisable2FA (options) {
+ options = options ?? {};
+
+ const promise = new TeePromise();
+ let disabled_successfully = false;
+
+ const password_entry = new PasswordEntry({});
+ const place_password_entry = Placeholder();
+
+ const internal_id = window.uuidv4();
+ let h = '';
+ h += '';
+ h += '
';
+ h += '
';
+ h += '
';
+ h += `
${i18n('disable_2fa_instructions')}
`;
+ h += '
';
+ h += '
';
+ h += '
';
+ h += `${i18n('account_password')} `;
+ h += `${place_password_entry.html}`;
+ h += '
';
+ h += '
';
+ h += '
';
+ h += '
';
+ h += '
';
+ h += '
';
+ h += '
';
+ h += `
${i18n('disable_2fa')} `;
+ h += '
';
+
+ const el_window = await UIWindow({
+ title: i18n('disable_2fa'),
+ app: 'disable-2fa',
+ single_instance: true,
+ icon: null,
+ uid: null,
+ is_dir: false,
+ body_content: h,
+ has_head: true,
+ selectable_body: false,
+ draggable_body: false,
+ allow_context_menu: false,
+ is_resizable: false,
+ is_droppable: false,
+ init_center: true,
+ allow_native_ctxmenu: false,
+ allow_user_select: false,
+ width: 350,
+ height: 'auto',
+ dominant: true,
+ show_in_taskbar: false,
+ on_before_exit: async () => {
+ if ( ! disabled_successfully ) {
+ promise.resolve(false);
+ }
+ return true;
+ },
+ onAppend: function (this_window) {
+ $(this_window).find('.disable-2fa-password-wrap input').get(0)?.focus({ preventScroll: true });
+ const oidc_only = !!(window.user && window.user.oidc_only);
+ const authRow = $(this_window).find('.disable-2fa-auth-row');
+ if ( oidc_only ) {
+ authRow.find('.disable-2fa-password-wrap').hide();
+ const oidcWrap = authRow.find('.disable-2fa-oidc-wrap').show();
+ oidcWrap.find('.disable-2fa-oidc-flow-notice').text(
+ i18n('revalidate_flow_notice') ||
+ 'You will be asked to sign in with your linked account when you continue.',
+ );
+ } else {
+ authRow.find('.disable-2fa-oidc-wrap').hide();
+ }
+ },
+ window_class: 'window-publishWebsite',
+ body_css: {
+ width: 'initial',
+ height: '100%',
+ 'background-color': 'rgb(245 247 249)',
+ 'backdrop-filter': 'blur(3px)',
+ },
+ ...options.window_options,
+ });
+
+ password_entry.attach(place_password_entry);
+
+ const origin = window.gui_origin || window.api_origin || '';
+ const apiUrl = `${origin}/user-protected/disable-2fa`;
+ let revalidated = false;
+
+ const hint = $(el_window).find('.disable-2fa-oidc-hint');
+ const REVALIDATE_POPUP_TEXT = i18n('revalidate_sign_in_popup') || 'Sign in with your linked account in the popup.';
+
+ const myOpenRevalidatePopup = async (revalidateUrl) => {
+ revalidateUrl = revalidateUrl || (window.user && window.user.oidc_revalidate_url);
+ $(el_window).find('.disable-2fa-btn').addClass('disabled');
+ hint.text(REVALIDATE_POPUP_TEXT).show();
+ try {
+ await openRevalidatePopup(revalidateUrl);
+ } catch (e) {
+ onError(e.message || 'Authentication failed');
+ return;
+ } finally {
+ hint.hide();
+ }
+ $(el_window).find('.disable-2fa-revalidated-msg').text(i18n('revalidated') || 'Re-validated.').show();
+ };
+
+ $(el_window).find('.disable-2fa-btn').on('click', async function (e) {
+ $(el_window).find('.form-success-msg, .form-error-msg').hide();
+
+ const password = password_entry.get('value');
+ const oidc_only = !!(window.user && window.user.oidc_only);
+
+ if ( !oidc_only && !password ) {
+ $(el_window).find('.form-error-msg').html(i18n('all_fields_required'));
+ $(el_window).find('.form-error-msg').fadeIn();
+ return;
+ }
+
+ if ( oidc_only && !revalidated && !password ) {
+ await myOpenRevalidatePopup();
+
+ const res = await doSubmit({ password: undefined });
+ const data = res.ok ? await res.json().catch(() => ({})) : await res.json().catch(() => ({}));
+ if ( res.ok ) onSuccess();
+ else onError(data.message || 'Request failed');
+ return;
+ }
+ $(el_window).find('.form-error-msg').hide();
+ $(el_window).find('.disable-2fa-btn').addClass('disabled');
+ $(el_window).find('.disable-2fa-password-wrap input').attr('disabled', true);
+
+ let res = await doSubmit({ password });
+ const data = res.ok ? await res.json().catch(() => ({})) : await res.json().catch(() => ({}));
+
+ if ( res.ok ) {
+ onSuccess();
+ return;
+ }
+ if ( data.code === 'oidc_revalidation_required' && data.revalidate_url ) {
+ await myOpenRevalidatePopup(data.revalidate_url);
+ const r = await doSubmit({ password: undefined });
+ if ( r.ok ) onSuccess();
+ else r.json().then((d) => onError(d.message || 'Request failed')).catch(() => onError('Request failed'));
+ return;
+ }
+ onError(data.message || 'Request failed');
+ });
+
+ function doSubmit ({ password }) {
+ return fetch(apiUrl, {
+ method: 'POST',
+ credentials: 'include',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ password: password !== undefined && password !== '' ? password : undefined,
+ }),
+ });
+ }
+
+ function onError (message) {
+ $(el_window).find('.form-error-msg').html(html_encode(message));
+ $(el_window).find('.form-error-msg').fadeIn();
+ $(el_window).find('.disable-2fa-btn').removeClass('disabled');
+ $(el_window).find('.disable-2fa-password-wrap input').attr('disabled', false);
+ }
+
+ function onSuccess () {
+ disabled_successfully = true;
+ $(el_window).find('.form-success-msg').html(i18n('two_factor_disabled'));
+ $(el_window).find('.form-success-msg').fadeIn();
+ if ( window.user ) window.user.otp = false;
+ $(el_window).find('.disable-2fa-btn').removeClass('disabled');
+ $(el_window).find('.disable-2fa-password-wrap input').attr('disabled', false);
+ promise.resolve(true);
+ $(el_window).close();
+ }
+
+ return { promise };
+}
+
+export default UIWindowDisable2FA;