From e145f5dcc3a9f87bf8de800ef8ce825a96cb6cfb Mon Sep 17 00:00:00 2001
From: KernelDeimos <7225168+KernelDeimos@users.noreply.github.com>
Date: Fri, 13 Feb 2026 19:49:52 -0500
Subject: [PATCH] dev(oidc): rewrite "Disable 2FA" window
In lieu of knowing exactly what happened (probably more than one thing),
the "Disable 2FA" window was very broken. It was blank, but then after
fixing that all the actions were broken. There wasn't much value in
keeping the implementation though, because it was already inconsistent
with other flows - instead of fixing what was there it made more sense
to re-use the pattern of UIWindowChangeUsername and UIWindowChangeEmail,
creating UIWindowDisable2FA. After testing this, it works much better
(it actaully works), but there is a caching issue unrelated to the UI
implementation.
---
src/gui/src/UI/Dashboard/TabSecurity.js | 152 ++-----------
src/gui/src/UI/Settings/UITabSecurity.js | 149 +------------
src/gui/src/UI/Settings/UIWindowDisable2FA.js | 207 ++++++++++++++++++
3 files changed, 233 insertions(+), 275 deletions(-)
create mode 100644 src/gui/src/UI/Settings/UIWindowDisable2FA.js
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;