diff --git a/src/gui/src/UI/Dashboard/TabSecurity.js b/src/gui/src/UI/Dashboard/TabSecurity.js
index b9990ba96..055853755 100644
--- a/src/gui/src/UI/Dashboard/TabSecurity.js
+++ b/src/gui/src/UI/Dashboard/TabSecurity.js
@@ -150,32 +150,79 @@ const TabSecurity = {
$el_window.find('.dashboard-section-security .disable-2fa').on('click', async function (e) {
let win;
const password_confirm_promise = new TeePromise();
- const try_password = async () => {
- const value = $win.find('.password-entry').val();
- // Do not send Authorization: user-protected endpoints use session cookie (hasHttpPowers)
- const resp = await fetch(`${window.api_origin}/user-protected/disable-2fa`, {
- method: 'POST',
- credentials: 'include',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- password: value,
- }),
- });
- if ( resp.status !== 200 ) {
- /* eslint no-empty: ["error", { "allowEmptyCatch": true }] */
- let message; try {
- message = (await resp.json()).message;
- } catch (e) {
+
+ 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'));
+ }
}
- message = message || i18n('error_unknown_cause');
- $win.find('.password-entry').addClass('error');
- $win.find('.error-message').text(message).show();
+ }, 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;
}
- password_confirm_promise.resolve(true);
- $(win).close();
+ 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 = '';
@@ -186,6 +233,7 @@ const TabSecurity = {
h += '';
h += '
';
diff --git a/src/gui/src/UI/Settings/UITabSecurity.js b/src/gui/src/UI/Settings/UITabSecurity.js
index af2ca184d..d4518d0fe 100644
--- a/src/gui/src/UI/Settings/UITabSecurity.js
+++ b/src/gui/src/UI/Settings/UITabSecurity.js
@@ -82,33 +82,83 @@ export default {
});
$el_window.find('.disable-2fa').on('click', async function (e) {
- let win, password_entry;
+ let win;
const password_confirm_promise = new TeePromise();
- const try_password = async () => {
- const value = password_entry.get('value');
- // No Authorization header: user-protected endpoints use session cookie (hasHttpPowers)
- const resp = await fetch(`${window.api_origin}/user-protected/disable-2fa`, {
- method: 'POST',
- credentials: 'include',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- password: value,
- }),
- });
- if ( resp.status !== 200 ) {
- /* eslint no-empty: ["error", { "allowEmptyCatch": true }] */
- let message; try {
- message = (await resp.json()).message;
- } catch (e) {
+
+ 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'));
+ }
}
- message = message || i18n('error_unknown_cause');
- password_entry.set('error', message);
+ }, 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 ? $(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;
}
- password_confirm_promise.resolve(true);
- $(win).close();
+ 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();
};
let h = '';
@@ -117,8 +167,10 @@ export default {
h += `
${i18n('disable_2fa_confirm')}
`;
h += `
${i18n('disable_2fa_instructions')}
`;
h += '
';
- h += '';
+ // password / OIDC revalidate
+ h += '
';
+ h += '
';
h += ``;
h += `${place_password_entry.html}`;
h += '
';
+ h += '
';
+ h += '';
+ h += '';
+ h += '
';
+ h += '
';
+ h += '
';
// Change Email
h += `
`;
@@ -74,6 +81,15 @@ 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-revalidate-btn').text(i18n('revalidate_with_google') || 'Re-validate with Google');
+ } else {
+ authRow.find('.change-email-oidc-wrap').hide();
+ }
},
window_class: 'window-publishWebsite',
body_css: {
@@ -87,12 +103,16 @@ 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;
+
+ $(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'));
@@ -101,45 +121,118 @@ async function UIWindowChangeEmail (options) {
}
$(el_window).find('.form-error-msg').hide();
-
- // disable button
$(el_window).find('.change-email-btn').addClass('disabled');
- // disable input
$(el_window).find('.new-email').attr('disabled', true);
- $.ajax({
- url: `${window.gui_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'),
+ const doSubmit = () => fetch(apiUrl, {
+ method: 'POST',
+ credentials: 'include',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ new_email,
+ password: password !== undefined && password !== '' ? password : undefined,
}),
- 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));
+ });
+
+ 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();
- // enable button
- $(el_window).find('.change-email-btn').removeClass('disabled');
- // enable input
- $(el_window).find('.new-email').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-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/UIWindowChangePassword.js b/src/gui/src/UI/UIWindowChangePassword.js
index db210ea09..27d99e090 100644
--- a/src/gui/src/UI/UIWindowChangePassword.js
+++ b/src/gui/src/UI/UIWindowChangePassword.js
@@ -17,8 +17,8 @@
* along with this program. If not, see
.
*/
-import UIWindow from './UIWindow.js';
import check_password_strength from '../helpers/check_password_strength.js';
+import UIWindow from './UIWindow.js';
async function UIWindowChangePassword (options) {
options = options ?? {};
@@ -30,11 +30,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 +51,7 @@ async function UIWindowChangePassword (options) {
h += ``;
h += ``;
h += '
';
+ h += '
';
// Change Password
h += `
`;
@@ -72,7 +79,16 @@ 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-revalidate-btn').text(i18n('revalidate_with_google') || 'Re-validate with Google');
+ } else {
+ authRow.find('.change-password-oidc-wrap').hide();
+ }
},
window_class: 'window-publishWebsite',
body_css: {
@@ -84,27 +100,47 @@ 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`;
+
+ $(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 ( oidc_only && !revalidated && !current_password ) {
+ $(el_window).find('.change-password-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');
+ });
+ 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'));
@@ -113,29 +149,102 @@ async function UIWindowChangePassword (options) {
}
$(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);
- // Do not send Authorization: user-protected endpoints use session cookie (hasHttpPowers)
- $.ajax({
- url: `${window.api_origin }/user-protected/change-password`,
- type: 'POST',
- async: true,
- xhrFields: { withCredentials: true },
- contentType: 'application/json',
- data: JSON.stringify({
- password: current_password,
+ const doSubmit = (currentPass) => fetch(apiUrl, {
+ method: 'POST',
+ credentials: 'include',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ password: currentPass !== undefined ? currentPass : 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();
- },
+ });
+
+ let res = await doSubmit(current_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 ) {
+ 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 || res.statusText || 'Request failed');
+ });
+ let revalidated = false;
+
+ 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-password-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-password-revalidated-msg').text(i18n('revalidated') || 'Re-validated.').show();
+ $(el_window).find('.change-password-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-password-revalidate-btn').on('click', function () {
+ openRevalidatePopup(null, (err) => {
+ if ( err ) {
+ onError(err.message || 'Re-validation required.');
+ }
});
});
+
+ 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