import { apiPatch, apiPost, invalidateSessionCheck } from './api.js';
import { getRole, getUser, showToast } 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) {
if (!role) return '';
if (role === 'coach') return 'Counselor';
return role.charAt(0).toUpperCase() + role.slice(1);
}
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 = `
`;
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 ${esc(username)} (${esc(roleLabel(role))}).`
: `Set a new password for ${esc(username)} (${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('#passwordModalModeFieldset').style.display = isSelf ? 'none' : '';
modal.querySelector('#passwordModalOldGroup').style.display = isSelf ? '' : 'none';
if (!isSelf) {
modal.querySelector('input[name="pw_mode"][value="temporary"]').checked = true;
}
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 mode = modal.querySelector('input[name="pw_mode"]:checked')?.value || 'temporary';
const temporary = mode === 'temporary';
const { userID, username } = modalContext;
const data = await apiPatch('users.php', {
userID,
password: newPassword,
mustChangePassword: temporary ? 1 : 0,
});
if (!data.success) throw new Error(data.error || 'Failed to reset password');
closePasswordModal();
showToast(
temporary
? `Temporary password set for "${username}" — they must change it on next sign-in`
: `Permanent password set for "${username}"`,
'success'
);
document.dispatchEvent(new CustomEvent('qdb:password-reset', {
detail: { userID, mustChangePassword: data.mustChangePassword ?? (temporary ? 1 : 0) },
}));
} catch (e) {
showModalError(errEl, e.message);
} finally {
submitBtn.disabled = false;
submitBtn.textContent = prevLabel;
}
}