From df1f5c44cc2910a4728ad1cbde6b0d2255d437e5 Mon Sep 17 00:00:00 2001
From: KernelDeimos <7225168+KernelDeimos@users.noreply.github.com>
Date: Thu, 12 Feb 2026 18:27:01 -0500
Subject: [PATCH] refactor(oidc): extract common (email + username)
There is common functionality between all of the GUI code for actions on
protected endpoints. Update UIWindowChangeEmail and
UIWindowChangeUsername to both use a new utility function called
openRevalidatePopup in util/openid.js.
This file is called `openid.js` instead of `oidc.js` so that it's more
easily recognized by contributors who might be more familiar with the
name of the organization than the name of the standard itself.
After these changes, UIWindowChangePassword and the "disable 2FA" button
in UITabSecurity still need to be updated to use `util/openid.js`
instead of duplicating this functionality.
The justification for following DRY here instead of leaving the
implementation as-is is because these flows are particularly error
prone and will be difficult to maintain without this consistency. Some
subtle bugs I previously wasn't aware of got fixed in the process.
---
.../src/UI/Settings/UIWindowChangeEmail.js | 129 +++++++-----------
src/gui/src/UI/UIWindowChangeUsername.js | 84 +++++-------
src/gui/src/util/openid.js | 61 +++++++++
3 files changed, 141 insertions(+), 133 deletions(-)
create mode 100644 src/gui/src/util/openid.js
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;
+};