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;
+};