';
+ // password / OIDC revalidate
+ h += '
';
+ h += '
';
h += ``;
h += `${place_password_entry.html}`;
h += '
';
+ h += '
';
+ h += '
';
+ h += '
';
+ h += '
';
+ h += '
';
+ h += '
';
// Change Email
h += `
`;
@@ -74,6 +82,18 @@ async function UIWindowChangeEmail (options) {
show_in_taskbar: false,
onAppend: function (this_window) {
$(this_window).find('.new-email').get(0)?.focus({ preventScroll: true });
+ const oidc_only = !!(window.user && window.user.oidc_only);
+ const authRow = $(this_window).find('.change-email-auth-row');
+ if ( oidc_only ) {
+ authRow.find('.change-email-password-wrap').hide();
+ const oidcWrap = authRow.find('.change-email-oidc-wrap').show();
+ oidcWrap.find('.change-email-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('.change-email-oidc-wrap').hide();
+ }
},
window_class: 'window-publishWebsite',
body_css: {
@@ -87,12 +107,34 @@ async function UIWindowChangeEmail (options) {
password_entry.attach(place_password_entry);
- $(el_window).find('.change-email-btn').on('click', function (e) {
- // hide previous error/success msg
- $(el_window).find('.form-success-msg, .form-success-msg').hide();
+ const origin = window.gui_origin || window.api_origin || '';
+ 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-btn').on('click', async function (e) {
+ $(el_window).find('.form-success-msg, .form-error-msg').hide();
const new_email = $(el_window).find('.new-email').val();
- const password = $(el_window).find('.password').val();
+ const password = password_entry.get('value');
+ const oidc_only = !!(window.user && window.user.oidc_only);
if ( ! new_email ) {
$(el_window).find('.form-error-msg').html(i18n('all_fields_required'));
@@ -100,46 +142,64 @@ async function UIWindowChangeEmail (options) {
return;
}
- $(el_window).find('.form-error-msg').hide();
+ if ( oidc_only && !revalidated && !password ) {
+ await myOpenRevalidatePopup();
- // disable button
+ 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');
- // disable input
$(el_window).find('.new-email').attr('disabled', true);
- $.ajax({
- url: `${window.api_origin }/user-protected/change-email`,
- type: 'POST',
- async: true,
- headers: {
- 'Authorization': `Bearer ${window.auth_token}`,
- },
- contentType: 'application/json',
- data: JSON.stringify({
- new_email: new_email,
- password: password_entry.get('value'),
- }),
- success: function (data) {
- $(el_window).find('.form-success-msg').html(i18n('email_change_confirmation_sent'));
- $(el_window).find('.form-success-msg').fadeIn();
- $(el_window).find('input').val('');
- // update email
- window.user.email = new_email;
- // enable button
- $(el_window).find('.change-email-btn').removeClass('disabled');
- // enable input
- $(el_window).find('.new-email').attr('disabled', false);
- },
- error: function (err) {
- $(el_window).find('.form-error-msg').html(html_encode(err.responseJSON?.message));
- $(el_window).find('.form-error-msg').fadeIn();
- // enable button
- $(el_window).find('.change-email-btn').removeClass('disabled');
- // enable input
- $(el_window).find('.new-email').attr('disabled', false);
- },
- });
+ 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({ new_email });
+ 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' },
+ body: JSON.stringify({
+ new_email,
+ 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('.change-email-btn').removeClass('disabled');
+ $(el_window).find('.new-email').attr('disabled', false);
+ }
+
+ function onSuccess () {
+ const new_email = $(el_window).find('.new-email').val();
+ $(el_window).find('.form-success-msg').html(i18n('email_change_confirmation_sent'));
+ $(el_window).find('.form-success-msg').fadeIn();
+ $(el_window).find('input').val('');
+ window.user.email = new_email;
+ $(el_window).find('.change-email-btn').removeClass('disabled');
+ $(el_window).find('.new-email').attr('disabled', false);
+ }
}
export default UIWindowChangeEmail;
\ No newline at end of file
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 += '
';
+ 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;
diff --git a/src/gui/src/UI/UIComponentWindow.js b/src/gui/src/UI/UIComponentWindow.js
index a10baf909..3edffd6e6 100644
--- a/src/gui/src/UI/UIComponentWindow.js
+++ b/src/gui/src/UI/UIComponentWindow.js
@@ -18,17 +18,20 @@
*/
import UIWindow from './UIWindow.js';
import Placeholder from '../util/Placeholder.js';
+import JustHTML from './Components/JustHTML.js';
/**
* @typedef {Object} UIComponentWindowOptions
- * @property {Component} A component to render in the window
+ * @property {Component} [component] A component to render in the window
+ * @property {string} [html] HTML string to render in the window (uses JustHTML component)
*/
/**
- * Render a UIWindow that contains an instance of Component
+ * Render a UIWindow that contains an instance of Component or HTML string
* @param {UIComponentWindowOptions} options
*/
export default async function UIComponentWindow (options) {
+ const component = options.component ?? new JustHTML({ html: options.html ?? '' });
const placeholder = Placeholder();
const win = await UIWindow({
@@ -37,8 +40,8 @@ export default async function UIComponentWindow (options) {
body_content: placeholder.html,
});
- options.component.attach(placeholder);
- options.component.focus();
+ component.attach(placeholder);
+ component.focus();
return win;
}
diff --git a/src/gui/src/UI/UIWindowChangePassword.js b/src/gui/src/UI/UIWindowChangePassword.js
index ae0722d78..d7da2fd1d 100644
--- a/src/gui/src/UI/UIWindowChangePassword.js
+++ b/src/gui/src/UI/UIWindowChangePassword.js
@@ -17,8 +17,9 @@
* along with this program. If not, see
.
*/
-import UIWindow from './UIWindow.js';
import check_password_strength from '../helpers/check_password_strength.js';
+import { openRevalidatePopup } from '../util/openid.js';
+import UIWindow from './UIWindow.js';
async function UIWindowChangePassword (options) {
options = options ?? {};
@@ -30,11 +31,17 @@ async function UIWindowChangePassword (options) {
h += '
';
// success msg
h += '
';
- // current password
- h += '
';
+ // current password / OIDC revalidate
+ h += '
';
+ h += '
';
h += ``;
h += ``;
h += '
';
+ h += '
';
+ h += '
';
+ h += '
';
+ h += '
';
+ h += '
';
// new password
h += '
';
h += ``;
@@ -45,6 +52,7 @@ async function UIWindowChangePassword (options) {
h += ``;
h += ``;
h += '
';
+ h += '
';
// Change Password
h += `
`;
@@ -72,7 +80,19 @@ async function UIWindowChangePassword (options) {
dominant: true,
show_in_taskbar: false,
onAppend: function (this_window) {
- $(this_window).find('.current-password').get(0).focus({ preventScroll: true });
+ $(this_window).find('.current-password').get(0)?.focus({ preventScroll: true });
+ const oidc_only = !!(window.user && window.user.oidc_only);
+ const authRow = $(this_window).find('.change-password-auth-row');
+ if ( oidc_only ) {
+ authRow.find('.change-password-current-wrap').hide();
+ const oidcWrap = authRow.find('.change-password-oidc-wrap').show();
+ oidcWrap.find('.change-password-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('.change-password-oidc-wrap').hide();
+ }
},
window_class: 'window-publishWebsite',
body_css: {
@@ -84,27 +104,52 @@ async function UIWindowChangePassword (options) {
...options.window_options,
});
- $(el_window).find('.change-password-btn').on('click', function (e) {
+ const origin = window.gui_origin || window.api_origin || '';
+ const apiUrl = `${origin}/user-protected/change-password`;
+ let revalidated = false;
+
+ const hint = $(el_window).find('.change-password-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-password-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-password-revalidated-msg').text(i18n('revalidated') || 'Re-validated.').show();
+ };
+
+ $(el_window).find('.change-password-btn').on('click', async function (e) {
const current_password = $(el_window).find('.current-password').val();
const new_password = $(el_window).find('.new-password').val();
const confirm_new_password = $(el_window).find('.confirm-new-password').val();
+ const oidc_only = !!(window.user && window.user.oidc_only);
- // hide success message
- $(el_window).find('.form-success-msg').hide();
+ $(el_window).find('.form-success-msg, .form-error-msg').hide();
- // check if all fields are filled
- if ( !current_password || !new_password || !confirm_new_password ) {
+ if ( !new_password || !confirm_new_password ) {
$(el_window).find('.form-error-msg').html('All fields are required.');
$(el_window).find('.form-error-msg').fadeIn();
return;
}
- // check if new password and confirm new password are the same
- else if ( new_password !== confirm_new_password ) {
+ // For password users, current password is required; for OIDC, we need revalidated or will open popup
+ if ( !oidc_only && !current_password ) {
+ $(el_window).find('.form-error-msg').html('All fields are required.');
+ $(el_window).find('.form-error-msg').fadeIn();
+ return;
+ }
+ if ( new_password !== confirm_new_password ) {
$(el_window).find('.form-error-msg').html(i18n('passwords_do_not_match'));
$(el_window).find('.form-error-msg').fadeIn();
return;
}
- // check password strength
const pass_strength = check_password_strength(new_password);
if ( ! pass_strength.overallPass ) {
$(el_window).find('.form-error-msg').html(i18n('password_strength_error'));
@@ -112,31 +157,63 @@ async function UIWindowChangePassword (options) {
return;
}
- $(el_window).find('.form-error-msg').hide();
+ if ( oidc_only && !revalidated && !current_password ) {
+ await myOpenRevalidatePopup();
- $.ajax({
- url: `${window.api_origin }/user-protected/change-password`,
- type: 'POST',
- async: true,
- headers: {
- 'Authorization': `Bearer ${window.auth_token}`,
- },
- contentType: 'application/json',
- data: JSON.stringify({
+ const res = await doSubmit({ new_password });
+ 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-password-btn').addClass('disabled');
+ $(el_window).find('.current-password, .new-password, .confirm-new-password').attr('disabled', true);
+
+ let res = await doSubmit({ current_password, new_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 || res.statusText || 'Request failed');
+ });
+
+ function doSubmit ({ new_password, current_password }) {
+ return fetch(apiUrl, {
+ method: 'POST',
+ credentials: 'include',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
password: current_password,
new_pass: new_password,
}),
- success: function (data) {
- $(el_window).find('.form-success-msg').html(i18n('password_changed'));
- $(el_window).find('.form-success-msg').fadeIn();
- $(el_window).find('input').val('');
- },
- error: function (err) {
- $(el_window).find('.form-error-msg').html(html_encode(err.responseText));
- $(el_window).find('.form-error-msg').fadeIn();
- },
});
- });
+ }
+
+ function onError (message) {
+ $(el_window).find('.form-error-msg').html(html_encode(message));
+ $(el_window).find('.form-error-msg').fadeIn();
+ $(el_window).find('.change-password-btn').removeClass('disabled');
+ $(el_window).find('.current-password, .new-password, .confirm-new-password').attr('disabled', false);
+ }
+
+ function onSuccess () {
+ $(el_window).find('.form-success-msg').html(i18n('password_changed'));
+ $(el_window).find('.form-success-msg').fadeIn();
+ $(el_window).find('input').val('');
+ $(el_window).find('.change-password-btn').removeClass('disabled');
+ $(el_window).find('.current-password, .new-password, .confirm-new-password').attr('disabled', false);
+ }
}
export default UIWindowChangePassword;
\ No newline at end of file
diff --git a/src/gui/src/UI/UIWindowChangeUsername.js b/src/gui/src/UI/UIWindowChangeUsername.js
index d826cf1ef..c94f10f46 100644
--- a/src/gui/src/UI/UIWindowChangeUsername.js
+++ b/src/gui/src/UI/UIWindowChangeUsername.js
@@ -17,8 +17,9 @@
* along with this program. If not, see
.
*/
-import UIWindow from './UIWindow.js';
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) {
options = options ?? {};
@@ -26,17 +27,23 @@ async function UIWindowChangeUsername (options) {
const internal_id = window.uuidv4();
let h = '';
h += '
';
- // error msg
h += '
';
- // success msg
h += '
';
- // new username
h += '
';
h += ``;
h += ``;
h += '
';
-
- // Change Username
+ h += '
';
+ h += `
`;
+ h += '
';
+ h += ``;
+ h += '
';
+ h += '
';
+ h += '
';
+ h += '
';
+ h += '
';
+ h += '
';
+ h += '
';
h += `
`;
h += '
';
@@ -63,6 +70,18 @@ async function UIWindowChangeUsername (options) {
show_in_taskbar: false,
onAppend: function (this_window) {
$(this_window).find('.new-username').get(0)?.focus({ preventScroll: true });
+ const oidc_only = !!(window.user && window.user.oidc_only);
+ const authRow = $(this_window).find('.change-username-auth-row');
+ if ( oidc_only ) {
+ authRow.find('.change-username-password-wrap').hide();
+ const oidcWrap = authRow.find('.change-username-oidc-wrap').show();
+ oidcWrap.find('.change-username-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('.change-username-oidc-wrap').hide();
+ }
},
window_class: 'window-publishWebsite',
body_css: {
@@ -74,59 +93,102 @@ async function UIWindowChangeUsername (options) {
...options.window_options,
});
- $(el_window).find('.change-username-btn').on('click', function (e) {
- // hide previous error/success msg
- $(el_window).find('.form-success-msg, .form-success-msg').hide();
+ const origin = window.gui_origin || window.api_origin || '';
+ 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-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();
+ const password = $(el_window).find('.change-username-password').val();
+ const oidc_only = !!(window.user && window.user.oidc_only);
if ( ! new_username ) {
$(el_window).find('.form-error-msg').html(i18n('all_fields_required'));
$(el_window).find('.form-error-msg').fadeIn();
return;
}
+ if ( oidc_only && !revalidated && !password ) {
+ $(el_window).find('.change-username-btn').addClass('disabled');
+ await 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();
-
- // disable button
$(el_window).find('.change-username-btn').addClass('disabled');
- // disable input
- $(el_window).find('.new-username').attr('disabled', true);
+ $(el_window).find('.new-username, .change-username-password').attr('disabled', true);
- $.ajax({
- url: `${window.api_origin }/change_username`,
- type: 'POST',
- async: true,
- headers: {
- 'Authorization': `Bearer ${window.auth_token}`,
- },
- contentType: 'application/json',
- data: JSON.stringify({
- new_username: new_username,
- }),
- success: function (data) {
- $(el_window).find('.form-success-msg').html(i18n('username_changed'));
- $(el_window).find('.form-success-msg').fadeIn();
- $(el_window).find('input').val('');
- // update auth data
- update_username_in_gui(new_username);
- // update username
- window.user.username = new_username;
- // enable button
- $(el_window).find('.change-username-btn').removeClass('disabled');
- // enable input
- $(el_window).find('.new-username').attr('disabled', false);
- },
- error: function (err) {
- $(el_window).find('.form-error-msg').html(html_encode(err.responseJSON?.message));
- $(el_window).find('.form-error-msg').fadeIn();
- // enable button
- $(el_window).find('.change-username-btn').removeClass('disabled');
- // enable input
- $(el_window).find('.new-username').attr('disabled', false);
- },
- });
+ 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();
+ 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) {
+ const new_username = $(el_window).find('.new-username').val();
+ const body = { new_username };
+ if ( password !== undefined && password !== '' ) body.password = password;
+ // Do not send Authorization: user-protected endpoints use session cookie (hasHttpOnlyCookie)
+ return fetch(apiUrl, {
+ method: 'POST',
+ credentials: 'include',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(body),
+ });
+ }
+
+ function onSuccess () {
+ const new_username = $(el_window).find('.new-username').val();
+ $(el_window).find('.form-success-msg').html(i18n('username_changed'));
+ $(el_window).find('.form-success-msg').fadeIn();
+ $(el_window).find('input').val('');
+ update_username_in_gui(new_username);
+ window.user.username = new_username;
+ $(el_window).find('.change-username-btn').removeClass('disabled');
+ $(el_window).find('.new-username, .change-username-password').attr('disabled', false);
+ }
+
+ function onError (message) {
+ $(el_window).find('.form-error-msg').html(html_encode(message));
+ $(el_window).find('.form-error-msg').fadeIn();
+ $(el_window).find('.change-username-btn').removeClass('disabled');
+ $(el_window).find('.new-username, .change-username-password').attr('disabled', false);
+ }
}
export default UIWindowChangeUsername;
\ No newline at end of file
diff --git a/src/gui/src/UI/UIWindowLogin.js b/src/gui/src/UI/UIWindowLogin.js
index ff62d7d87..c944c4b8c 100644
--- a/src/gui/src/UI/UIWindowLogin.js
+++ b/src/gui/src/UI/UIWindowLogin.js
@@ -17,17 +17,17 @@
* along with this program. If not, see
.
*/
-import UIWindow from './UIWindow.js';
-import UIWindowSignup from './UIWindowSignup.js';
-import UIWindowRecoverPassword from './UIWindowRecoverPassword.js';
import TeePromise from '../util/TeePromise.js';
-import UIComponentWindow from './UIComponentWindow.js';
-import Flexer from './Components/Flexer.js';
-import CodeEntryView from './Components/CodeEntryView.js';
-import JustHTML from './Components/JustHTML.js';
-import StepView from './Components/StepView.js';
import Button from './Components/Button.js';
+import CodeEntryView from './Components/CodeEntryView.js';
+import Flexer from './Components/Flexer.js';
+import JustHTML from './Components/JustHTML.js';
import RecoveryCodeEntryView from './Components/RecoveryCodeEntryView.js';
+import StepView from './Components/StepView.js';
+import UIComponentWindow from './UIComponentWindow.js';
+import UIWindow from './UIWindow.js';
+import UIWindowRecoverPassword from './UIWindowRecoverPassword.js';
+import UIWindowSignup from './UIWindowSignup.js';
async function UIWindowLogin (options) {
options = options ?? {};
@@ -83,6 +83,10 @@ async function UIWindowLogin (options) {
// password recovery
h += `
${i18n('forgot_pass_c2a')}
`;
h += '';
+ h += '
';
+ h += `
${ i18n('or') }
`;
+ h += `
`;
+ h += '
';
h += '
';
// create account link
@@ -148,6 +152,21 @@ async function UIWindowLogin (options) {
});
});
+ (async () => {
+ try {
+ const res = await fetch(`${window.api_origin}/auth/oidc/providers`);
+ if ( ! res.ok ) return;
+ const data = await res.json();
+ if ( data.providers && data.providers.includes('google') ) {
+ $(el_window).find('.oidc-providers-wrapper').show();
+ $(el_window).find('.oidc-google-btn').on('click', function () {
+ window.location.href = `${window.gui_origin}/auth/oidc/google/start?flow=login`;
+ });
+ }
+ } catch (_) {
+ }
+ })();
+
$(el_window).find('.login-btn').on('click', function (e) {
// Prevent default button behavior (important for async requests)
e.preventDefault();
diff --git a/src/gui/src/UI/UIWindowSignup.js b/src/gui/src/UI/UIWindowSignup.js
index 8b8d6beb3..61be05e5e 100644
--- a/src/gui/src/UI/UIWindowSignup.js
+++ b/src/gui/src/UI/UIWindowSignup.js
@@ -17,10 +17,10 @@
* along with this program. If not, see
.
*/
-import UIWindow from './UIWindow.js';
-import UIWindowLogin from './UIWindowLogin.js';
-import UIWindowEmailConfirmationRequired from './UIWindowEmailConfirmationRequired.js';
import check_password_strength from '../helpers/check_password_strength.js';
+import UIWindow from './UIWindow.js';
+import UIWindowEmailConfirmationRequired from './UIWindowEmailConfirmationRequired.js';
+import UIWindowLogin from './UIWindowLogin.js';
function UIWindowSignup (options) {
options = options ?? {};
@@ -96,6 +96,10 @@ function UIWindowSignup (options) {
// Create Account
h += `
`;
h += '';
+ h += '
';
+ h += `
${ i18n('or') }
`;
+ h += `
`;
+ h += '
';
h += '
';
// login link
// create account link
@@ -155,6 +159,21 @@ function UIWindowSignup (options) {
};
initTurnstile();
+
+ (async () => {
+ try {
+ const res = await fetch(`${window.api_origin}/auth/oidc/providers`);
+ if ( ! res.ok ) return;
+ const data = await res.json();
+ if ( data.providers && data.providers.includes('google') ) {
+ $(el_window).find('.oidc-providers-wrapper').show();
+ $(el_window).find('.oidc-google-btn').on('click', function () {
+ window.location.href = `${window.gui_origin}/auth/oidc/google/start?flow=signup`;
+ });
+ }
+ } catch (_) {
+ }
+ })();
},
window_class: 'window-signup',
window_css: {
diff --git a/src/gui/src/helpers.js b/src/gui/src/helpers.js
index 62fcdeac6..539582e4e 100644
--- a/src/gui/src/helpers.js
+++ b/src/gui/src/helpers.js
@@ -461,6 +461,27 @@ window.update_auth_data = async (auth_token, user, api_origin) => {
window.auth_token = auth_token;
localStorage.setItem('auth_token', auth_token);
+ // Set http-only session cookie when user is changing.
+ // This ensures user-protected endpoints, which only refer to the http-only cookie,
+ // act on the intended user.
+ // Only the server can set this cookie, so we call the `/session/sync-cookie` endpoint.
+ const userChanging = !window.user || window.user.uuid !== user.uuid;
+ if ( userChanging && auth_token && (window.gui_origin || window.location?.origin) ) {
+ try {
+ const origin = window.gui_origin || window.location.origin;
+ await fetch(`${origin}/session/sync-cookie`, {
+ method: 'GET',
+ credentials: 'include',
+ headers: { Authorization: `Bearer ${auth_token}` },
+ });
+ } catch (e) {
+ console.error('Failed to sync session cookie:', e);
+ await UIAlert({
+ message: `Failed to sync session cookie: ${ e.message}`,
+ });
+ }
+ }
+
if ( api_origin ) {
window.api_origin = api_origin;
localStorage.setItem('api_origin', api_origin);
diff --git a/src/gui/src/i18n/translations/en.js b/src/gui/src/i18n/translations/en.js
index ba9752e0c..6e2c1b8d9 100644
--- a/src/gui/src/i18n/translations/en.js
+++ b/src/gui/src/i18n/translations/en.js
@@ -49,6 +49,10 @@ const en = {
change_password: 'Change Password',
change_ui_colors: 'Change UI Colors',
change_username: 'Change Username',
+ revalidate_with_google: 'Re-validate with Google',
+ revalidated: 'Re-validated.',
+ revalidate_sign_in_popup: 'Sign in with your linked account in the popup.',
+ revalidate_flow_notice: 'You will be asked to sign in with your linked account when you continue.',
color_depth: 'Color Depth',
clock_visibility: 'Clock Visibility',
close: 'Close',
diff --git a/src/gui/src/initgui.js b/src/gui/src/initgui.js
index aba48a669..b634e37a4 100644
--- a/src/gui/src/initgui.js
+++ b/src/gui/src/initgui.js
@@ -17,11 +17,14 @@
* along with this program. If not, see