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 += ``; - h += ``; - 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 += ``; - h += ``; - 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 += ``; + h += `${place_password_entry.html}`; + h += '
'; + h += ''; + h += ''; + h += '
'; + h += ``; + 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;