diff --git a/src/gui/src/UI/Settings/UIWindowChangeEmail.js b/src/gui/src/UI/Settings/UIWindowChangeEmail.js index 3ba7398bc..abbbe6aa0 100644 --- a/src/gui/src/UI/Settings/UIWindowChangeEmail.js +++ b/src/gui/src/UI/Settings/UIWindowChangeEmail.js @@ -17,6 +17,7 @@ * along with this program. If not, see . */ +import { openRevalidatePopup } from '../../util/openid.js'; import Placeholder from '../../util/Placeholder.js'; import PasswordEntry from '../Components/PasswordEntry.js'; import UIWindow from '../UIWindow.js'; @@ -107,6 +108,25 @@ async function UIWindowChangeEmail (options) { const apiUrl = `${origin}/user-protected/change-email`; let revalidated = false; + const hint = $(el_window).find('.change-email-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('.change-email-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('.change-email-revalidated-msg').text(i18n('revalidated') || 'Re-validated.').show(); + $(el_window).find('.change-email-revalidate-btn').hide(); + }; + $(el_window).find('.change-email-btn').on('click', async function (e) { $(el_window).find('.form-success-msg, .form-error-msg').hide(); @@ -120,11 +140,38 @@ async function UIWindowChangeEmail (options) { return; } + if ( oidc_only && !revalidated && !password ) { + await myOpenRevalidatePopup(); + + const res = await doSubmit({ new_email }); + 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('.change-email-btn').addClass('disabled'); $(el_window).find('.new-email').attr('disabled', true); - const doSubmit = () => fetch(apiUrl, { + let res = await doSubmit({ new_email, 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(); + 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 ({ new_email, password }) { + return fetch(apiUrl, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, @@ -133,88 +180,10 @@ async function UIWindowChangeEmail (options) { password: password !== undefined && password !== '' ? password : undefined, }), }); - - if ( oidc_only && !revalidated && !password ) { - openRevalidatePopup(null, async (err) => { - if ( err ) { - onError(err.message || 'Re-validation required.'); - return; - } - const res = await doSubmit(); - const data = res.ok ? await res.json().catch(() => ({})) : await res.json().catch(() => ({})); - if ( res.ok ) onSuccess(); - else onError(data.message || 'Request failed'); - }); - return; - } - - let res = await doSubmit(); - 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 ) { - openRevalidatePopup(data.revalidate_url, async (err) => { - if ( err ) { - onError(err.message || 'Re-validation required.'); - return; - } - const r2 = await doSubmit(); - const d2 = r2.ok ? await r2.json().catch(() => ({})) : await r2.json().catch(() => ({})); - if ( r2.ok ) onSuccess(); - else onError(d2.message || 'Request failed'); - }); - return; - } - onError(data.message || 'Request failed'); - }); - - 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 = $(el_window).find('.change-email-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); - revalidated = true; - hint.hide(); - $(el_window).find('.change-email-revalidated-msg').text(i18n('revalidated') || 'Re-validated.').show(); - $(el_window).find('.change-email-revalidate-btn').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; } $(el_window).find('.change-email-revalidate-btn').on('click', function () { - openRevalidatePopup(null, (err) => { - if ( err ) { - $(el_window).find('.form-error-msg').html(html_encode(err.message || 'Re-validation required.')); - $(el_window).find('.form-error-msg').fadeIn(); - } - }); + myOpenRevalidatePopup(); }); function onError (message) { diff --git a/src/gui/src/UI/UIWindowChangeUsername.js b/src/gui/src/UI/UIWindowChangeUsername.js index 4f77ac0a8..370f5c2b3 100644 --- a/src/gui/src/UI/UIWindowChangeUsername.js +++ b/src/gui/src/UI/UIWindowChangeUsername.js @@ -18,6 +18,7 @@ */ import update_username_in_gui from '../helpers/update_username_in_gui.js'; +import { openRevalidatePopup } from '../util/openid.js'; import UIWindow from './UIWindow.js'; async function UIWindowChangeUsername (options) { @@ -93,6 +94,25 @@ async function UIWindowChangeUsername (options) { const apiUrl = `${origin}/user-protected/change-username`; let revalidated = false; + const hint = $(el_window).find('.change-username-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('.change-username-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('.change-username-revalidated-msg').text(i18n('revalidated') || 'Re-validated.').show(); + $(el_window).find('.change-username-revalidate-btn').hide(); + }; + $(el_window).find('.change-username-btn').on('click', async function (e) { $(el_window).find('.form-success-msg, .form-error-msg').hide(); const new_username = $(el_window).find('.new-username').val(); @@ -106,16 +126,12 @@ async function UIWindowChangeUsername (options) { } if ( oidc_only && !revalidated && !password ) { $(el_window).find('.change-username-btn').addClass('disabled'); - openRevalidatePopup(null, async (err) => { - if ( err ) { - onError(err.message || 'Re-validation required.'); - return; - } - const res = await doSubmit(); - const data = res.ok ? await res.json().catch(() => ({})) : await res.json().catch(() => ({})); - if ( res.ok ) onSuccess(); - else onError(data.message || 'Request failed'); - }); + myOpenRevalidatePopup(); + + const res = await doSubmit(); + 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(); @@ -130,55 +146,17 @@ async function UIWindowChangeUsername (options) { return; } if ( data.code === 'oidc_revalidation_required' && data.revalidate_url ) { - openRevalidatePopup(data.revalidate_url, async () => { - const r = await doSubmit(); - if ( r.ok ) onSuccess(); - else r.json().then((d) => onError(d.message || 'Request failed')).catch(() => onError('Request failed')); - }); + myOpenRevalidatePopup(data.revalidate_url); + const r = await doSubmit(); + 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 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 = $(el_window).find('.change-username-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); - revalidated = true; - hint.hide(); - $(el_window).find('.change-username-revalidated-msg').text(i18n('revalidated') || 'Re-validated.').show(); - $(el_window).find('.change-username-revalidate-btn').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; - } - $(el_window).find('.change-username-revalidate-btn').on('click', function () { - openRevalidatePopup(); + myOpenRevalidatePopup(); }); function doSubmit (password) { diff --git a/src/gui/src/util/openid.js b/src/gui/src/util/openid.js new file mode 100644 index 000000000..ab73ef6ec --- /dev/null +++ b/src/gui/src/util/openid.js @@ -0,0 +1,61 @@ +/* + * 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 TeePromise from './TeePromise.js'; + +/** + * This file contains common functions that are used to re-authenticate an + * OIDC-authenticated user when performing actions on protected endpoints. + * + * No design patterns, no abstractions; only simple functions. + * (this is not merely a description; it is a guideline for future changes) + */ + +const POPUP_FEATURES = 'width=500,height=600'; + +export const openRevalidatePopup = async (revalidateUrl) => { + const donePromise = new TeePromise(); + + const url = revalidateUrl; + if ( ! url ) { + throw new Error('No revalidate URL'); + } + let doneCalled = false; + const popup = window.open(url, 'puter-revalidate', POPUP_FEATURES); + 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); + donePromise.resolve(); + }; + window.addEventListener('message', onMessage); + const checkClosed = setInterval(() => { + if ( popup && popup.closed ) { + clearInterval(checkClosed); + window.removeEventListener('message', onMessage); + if ( ! doneCalled ) { + doneCalled = true; + donePromise.reject(new Error('Popup closed')); + } + } + }, 300); + await donePromise; +};