initial prototype based on nat-as-server
Some checks failed
PHPUnit / test (push) Has been cancelled
Some checks failed
PHPUnit / test (push) Has been cancelled
This commit is contained in:
208
website/js/password-modal.js
Normal file
208
website/js/password-modal.js
Normal file
@ -0,0 +1,208 @@
|
||||
import { apiPatch, apiPost, invalidateSessionCheck } from './api.js';
|
||||
import { getRole, getUser, showToast, formatRoleLabel } from './app.js';
|
||||
|
||||
/** @typedef {'admin_reset' | 'self'} PasswordModalMode */
|
||||
|
||||
/** @type {{ mode: PasswordModalMode, userID?: string, username?: string, role?: string } | null} */
|
||||
let modalContext = null;
|
||||
|
||||
function esc(s) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = s ?? '';
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
function roleLabel(role) {
|
||||
return formatRoleLabel(role);
|
||||
}
|
||||
|
||||
function signInHintForRole(role) {
|
||||
if (role === 'coach') {
|
||||
return 'Counselors sign in through the mobile app and will be prompted to choose a new password there.';
|
||||
}
|
||||
return 'Supervisors sign in through this website and will be prompted to choose a new password at login.';
|
||||
}
|
||||
|
||||
function ensurePasswordModal() {
|
||||
let modal = document.getElementById('passwordModal');
|
||||
if (modal) return modal;
|
||||
|
||||
modal = document.createElement('div');
|
||||
modal.id = 'passwordModal';
|
||||
modal.className = 'modal-overlay';
|
||||
modal.hidden = true;
|
||||
modal.setAttribute('role', 'dialog');
|
||||
modal.setAttribute('aria-modal', 'true');
|
||||
modal.setAttribute('aria-labelledby', 'passwordModalTitle');
|
||||
modal.innerHTML = `
|
||||
<div class="modal-dialog">
|
||||
<h2 id="passwordModalTitle">Password</h2>
|
||||
<p class="modal-subtitle" id="passwordModalSubtitle"></p>
|
||||
<p class="modal-hint" id="passwordModalHint"></p>
|
||||
<p class="modal-hint" id="passwordModalTempHint" style="display:none">
|
||||
They sign in with the password below, then must choose their own password before they can continue.
|
||||
</p>
|
||||
<div class="form-group" id="passwordModalOldGroup" style="display:none">
|
||||
<label for="pw_old_password">Current password</label>
|
||||
<input type="password" id="pw_old_password" autocomplete="current-password" placeholder="Your current password">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="pw_new_password">New password</label>
|
||||
<input type="password" id="pw_new_password" autocomplete="new-password" placeholder="At least 6 characters">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="pw_new_password_confirm">Confirm new password</label>
|
||||
<input type="password" id="pw_new_password_confirm" autocomplete="new-password" placeholder="Repeat password">
|
||||
</div>
|
||||
<p id="passwordModalError" class="error-text" style="display:none"></p>
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn btn-sm" data-password-cancel>Cancel</button>
|
||||
<button type="button" class="btn btn-primary btn-sm" data-password-submit>Save</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) closePasswordModal();
|
||||
});
|
||||
modal.querySelector('[data-password-cancel]').addEventListener('click', closePasswordModal);
|
||||
modal.querySelector('[data-password-submit]').addEventListener('click', submitPasswordModal);
|
||||
modal.querySelector('#pw_new_password_confirm').addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') submitPasswordModal();
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
const el = document.getElementById('passwordModal');
|
||||
if (!el || el.hidden) return;
|
||||
if (e.key === 'Escape') closePasswordModal();
|
||||
});
|
||||
|
||||
return modal;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ mode: PasswordModalMode, userID?: string, username?: string, role?: string }} options
|
||||
*/
|
||||
function openPasswordModal(options) {
|
||||
modalContext = options;
|
||||
const modal = ensurePasswordModal();
|
||||
const isSelf = options.mode === 'self';
|
||||
const username = options.username || getUser();
|
||||
const role = options.role || getRole();
|
||||
|
||||
modal.querySelector('#passwordModalTitle').textContent = isSelf ? 'Change your password' : 'Reset password';
|
||||
modal.querySelector('#passwordModalSubtitle').innerHTML = isSelf
|
||||
? `Choose a new password for <strong>${esc(username)}</strong> (${esc(roleLabel(role))}).`
|
||||
: `Set a new password for <strong>${esc(username)}</strong> (${esc(roleLabel(role))}).`;
|
||||
modal.querySelector('#passwordModalHint').textContent = isSelf
|
||||
? 'You will stay signed in after saving. Use this password on your next login.'
|
||||
: signInHintForRole(role);
|
||||
|
||||
modal.querySelector('#passwordModalTempHint').style.display = isSelf ? 'none' : '';
|
||||
modal.querySelector('#passwordModalOldGroup').style.display = isSelf ? '' : 'none';
|
||||
|
||||
modal.querySelector('#pw_old_password').value = '';
|
||||
modal.querySelector('#pw_new_password').value = '';
|
||||
modal.querySelector('#pw_new_password_confirm').value = '';
|
||||
const errEl = modal.querySelector('#passwordModalError');
|
||||
errEl.style.display = 'none';
|
||||
errEl.textContent = '';
|
||||
|
||||
modal.querySelector('[data-password-submit]').textContent = isSelf ? 'Change password' : 'Set password';
|
||||
|
||||
modal.hidden = false;
|
||||
document.body.classList.add('modal-open');
|
||||
(isSelf ? modal.querySelector('#pw_old_password') : modal.querySelector('#pw_new_password')).focus();
|
||||
}
|
||||
|
||||
export function closePasswordModal() {
|
||||
const modal = document.getElementById('passwordModal');
|
||||
if (modal) modal.hidden = true;
|
||||
document.body.classList.remove('modal-open');
|
||||
modalContext = null;
|
||||
}
|
||||
|
||||
export function openAdminResetPasswordModal({ userID, username, role }) {
|
||||
openPasswordModal({ mode: 'admin_reset', userID, username, role });
|
||||
}
|
||||
|
||||
export function openChangeOwnPasswordModal() {
|
||||
openPasswordModal({ mode: 'self' });
|
||||
}
|
||||
|
||||
function showModalError(el, msg) {
|
||||
el.textContent = msg;
|
||||
el.style.display = '';
|
||||
}
|
||||
|
||||
async function submitPasswordModal() {
|
||||
if (!modalContext) return;
|
||||
const modal = document.getElementById('passwordModal');
|
||||
const errEl = modal.querySelector('#passwordModalError');
|
||||
errEl.style.display = 'none';
|
||||
|
||||
const newPassword = modal.querySelector('#pw_new_password').value;
|
||||
const confirm = modal.querySelector('#pw_new_password_confirm').value;
|
||||
|
||||
if (newPassword.length < 6) {
|
||||
showModalError(errEl, 'Password must be at least 6 characters');
|
||||
return;
|
||||
}
|
||||
if (newPassword !== confirm) {
|
||||
showModalError(errEl, 'Passwords do not match');
|
||||
return;
|
||||
}
|
||||
|
||||
const submitBtn = modal.querySelector('[data-password-submit]');
|
||||
submitBtn.disabled = true;
|
||||
const prevLabel = submitBtn.textContent;
|
||||
submitBtn.textContent = 'Saving…';
|
||||
|
||||
try {
|
||||
if (modalContext.mode === 'self') {
|
||||
const oldPassword = modal.querySelector('#pw_old_password').value;
|
||||
if (!oldPassword) {
|
||||
showModalError(errEl, 'Current password is required');
|
||||
return;
|
||||
}
|
||||
const data = await apiPost('auth/change-password', {
|
||||
username: getUser(),
|
||||
old_password: oldPassword,
|
||||
new_password: newPassword,
|
||||
});
|
||||
if (!data.success) throw new Error(data.error || 'Failed to change password');
|
||||
if (data.token) localStorage.setItem('qdb_token', data.token);
|
||||
if (data.user) localStorage.setItem('qdb_user', data.user);
|
||||
if (data.role) localStorage.setItem('qdb_role', data.role);
|
||||
invalidateSessionCheck();
|
||||
closePasswordModal();
|
||||
showToast('Your password was changed', 'success');
|
||||
return;
|
||||
}
|
||||
|
||||
const { userID, username } = modalContext;
|
||||
|
||||
const data = await apiPatch('users.php', {
|
||||
userID,
|
||||
password: newPassword,
|
||||
mustChangePassword: 1,
|
||||
});
|
||||
if (!data.success) throw new Error(data.error || 'Failed to reset password');
|
||||
|
||||
closePasswordModal();
|
||||
showToast(
|
||||
`Temporary password set for "${username}" — they must change it on next sign-in`,
|
||||
'success'
|
||||
);
|
||||
|
||||
document.dispatchEvent(new CustomEvent('qdb:password-reset', {
|
||||
detail: { userID, mustChangePassword: data.mustChangePassword ?? 1 },
|
||||
}));
|
||||
} catch (e) {
|
||||
showModalError(errEl, e.message);
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = prevLabel;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user