mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-26 16:07:13 +00:00
dev(oidc): re-auth remaining protected endpoints
The OIDC re-authentication flow, which replaces password confirmation for accounts that were created with OIDC and do not have a password, was previously added to "change username" for manual testing of the backend-side implementation. Add the re-authentication flow to the remaining user-protected endpoints, which are: - change password - change email - disable two-factor authentication When using "change password" on a new account created via OIDC, the account changes state to a passworded account which causes these flows to use password confirmation as before instead of re-authentication.
This commit is contained in:
@@ -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 += '</div>';
|
||||
h += '<div style="display: flex; flex-direction: column; gap: 10pt;">';
|
||||
h += '<input type="password" class="password-entry" />';
|
||||
h += '<p class="disable-2fa-oidc-hint" style="margin:0;font-size:12px;color:#666;display:none;"></p>';
|
||||
h += '<span class="error-message" style="color: var(--dashboard-error-text); display: none;"></span>';
|
||||
h += '</div>';
|
||||
h += '<div style="display: flex; gap: 5pt;">';
|
||||
|
||||
@@ -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 += `<h3 style="text-align:center; font-weight: 500; font-size: 20px;">${i18n('disable_2fa_confirm')}</h3>`;
|
||||
h += `<p style="text-align:center; padding: 0 20px;">${i18n('disable_2fa_instructions')}</p>`;
|
||||
h += '</div>';
|
||||
h += '<div style="display: flex; gap: 5pt;">';
|
||||
h += '<div style="display: flex; flex-direction: column; gap: 10pt;">';
|
||||
h += '<input type="password" class="password-entry" />';
|
||||
h += '<p class="disable-2fa-oidc-hint" style="margin:0;font-size:12px;color:#666;display:none;"></p>';
|
||||
h += '<span class="error-message" style="color: red; display: none;"></span>';
|
||||
h += `<button class="button confirm-disable-2fa">${i18n('disable_2fa')}</button>`;
|
||||
h += `<button class="button secondary cancel-disable-2fa">${i18n('cancel')}</button>`;
|
||||
h += '</div>';
|
||||
|
||||
@@ -41,11 +41,18 @@ async function UIWindowChangeEmail (options) {
|
||||
h += `<label for="confirm-new-email-${internal_id}">${i18n('new_email')}</label>`;
|
||||
h += `<input id="confirm-new-email-${internal_id}" type="text" name="new-email" class="new-email" autocomplete="off" />`;
|
||||
h += '</div>';
|
||||
// password confirmation
|
||||
h += '<div style="overflow: hidden; margin-top: 10px; margin-bottom: 30px;">';
|
||||
// password / OIDC revalidate
|
||||
h += '<div class="change-email-auth-row" style="overflow: hidden; margin-top: 10px; margin-bottom: 30px;">';
|
||||
h += '<div class="change-email-password-wrap">';
|
||||
h += `<label>${i18n('account_password')}</label>`;
|
||||
h += `${place_password_entry.html}`;
|
||||
h += '</div>';
|
||||
h += '<div class="change-email-oidc-wrap" style="display:none;">';
|
||||
h += '<button type="button" class="button change-email-revalidate-btn"></button>';
|
||||
h += '<span class="change-email-revalidated-msg" style="display:none;"></span>';
|
||||
h += '</div>';
|
||||
h += '<p class="change-email-oidc-hint" style="margin-top:6px;font-size:12px;color:#666;display:none;"></p>';
|
||||
h += '</div>';
|
||||
|
||||
// Change Email
|
||||
h += `<button class="change-email-btn button button-primary button-block button-normal">${i18n('change_email')}</button>`;
|
||||
@@ -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;
|
||||
@@ -17,8 +17,8 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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 += '<div class="form-error-msg"></div>';
|
||||
// success msg
|
||||
h += '<div class="form-success-msg"></div>';
|
||||
// current password
|
||||
h += '<div style="overflow: hidden; margin-bottom: 20px;">';
|
||||
// current password / OIDC revalidate
|
||||
h += '<div class="change-password-auth-row" style="overflow: hidden; margin-bottom: 20px;">';
|
||||
h += '<div class="change-password-current-wrap">';
|
||||
h += `<label for="current-password-${internal_id}">${i18n('current_password')}</label>`;
|
||||
h += `<input id="current-password-${internal_id}" class="current-password" type="password" name="current-password" autocomplete="current-password" />`;
|
||||
h += '</div>';
|
||||
h += '<div class="change-password-oidc-wrap" style="display:none;">';
|
||||
h += '<button type="button" class="button change-password-revalidate-btn"></button>';
|
||||
h += '<span class="change-password-revalidated-msg" style="display:none;"></span>';
|
||||
h += '</div>';
|
||||
h += '</div>';
|
||||
// new password
|
||||
h += '<div style="overflow: hidden; margin-top: 20px; margin-bottom: 20px;">';
|
||||
h += `<label for="new-password-${internal_id}">${i18n('new_password')}</label>`;
|
||||
@@ -45,6 +51,7 @@ async function UIWindowChangePassword (options) {
|
||||
h += `<label for="confirm-new-password-${internal_id}">${i18n('confirm_new_password')}</label>`;
|
||||
h += `<input id="confirm-new-password-${internal_id}" type="password" name="confirm-new-password" class="confirm-new-password" autocomplete="off" />`;
|
||||
h += '</div>';
|
||||
h += '<p class="change-password-oidc-hint" style="margin-top:6px;font-size:12px;color:#666;display:none;"></p>';
|
||||
|
||||
// Change Password
|
||||
h += `<button class="change-password-btn button button-primary button-block button-normal">${i18n('change_password')}</button>`;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user