Added UI for mfa and other profile settings
This commit is contained in:
@@ -39,6 +39,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
if (document.getElementById('items-table-body')) loadItems();
|
||||
if (document.getElementById('locations-table-body')) loadLocations();
|
||||
if (document.getElementById('projects-table-body')) loadProjects();
|
||||
if (document.getElementById('account-settings-content')) loadAccountSettings();
|
||||
|
||||
loadProfile();
|
||||
});
|
||||
@@ -481,4 +482,247 @@ async function loadProfile() {
|
||||
} catch (e) {
|
||||
username.innerHTML = '<tr><td colspan="2" style="color:var(--error); text-align:center; padding:1.5rem;">Failed to load data.</td></tr>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- ACCOUNT SETTINGS ----
|
||||
let latestRecoveryCodes = [];
|
||||
|
||||
function showAccountSettingsMessage(message, type = 'success') {
|
||||
const box = document.getElementById('account-settings-message');
|
||||
if (!box) return;
|
||||
box.textContent = message;
|
||||
box.className = `message ${type}`;
|
||||
box.style.display = 'block';
|
||||
}
|
||||
|
||||
function setTwoFactorPanels(enabled) {
|
||||
const badge = document.getElementById('two-factor-badge');
|
||||
const status = document.getElementById('two-factor-status');
|
||||
const disabledPanel = document.getElementById('two-factor-disabled-panel');
|
||||
const enabledPanel = document.getElementById('two-factor-enabled-panel');
|
||||
|
||||
if (!badge || !status || !disabledPanel || !enabledPanel) return;
|
||||
|
||||
if (enabled) {
|
||||
badge.textContent = 'Enabled';
|
||||
badge.classList.add('success');
|
||||
status.textContent = '2FA is enabled for your account.';
|
||||
disabledPanel.style.display = 'none';
|
||||
enabledPanel.style.display = 'block';
|
||||
} else {
|
||||
badge.textContent = 'Disabled';
|
||||
badge.classList.remove('success');
|
||||
status.textContent = '2FA is disabled. Enable it to protect your account with an authenticator app.';
|
||||
disabledPanel.style.display = 'block';
|
||||
enabledPanel.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function renderRecoveryCodes(codes) {
|
||||
latestRecoveryCodes = codes || [];
|
||||
const panel = document.getElementById('recovery-codes-panel');
|
||||
const list = document.getElementById('recovery-codes-list');
|
||||
if (!panel || !list) return;
|
||||
|
||||
if (latestRecoveryCodes.length === 0) {
|
||||
panel.style.display = 'none';
|
||||
list.textContent = '';
|
||||
return;
|
||||
}
|
||||
|
||||
list.textContent = latestRecoveryCodes.join('\n');
|
||||
panel.style.display = 'block';
|
||||
}
|
||||
|
||||
async function loadAccountSettings() {
|
||||
try {
|
||||
const data = await apiRequest('/api/profile');
|
||||
|
||||
const usernameInput = document.getElementById('settings-username');
|
||||
const avatarPreview = document.getElementById('settings-avatar-preview');
|
||||
const remaining = document.getElementById('recovery-codes-remaining');
|
||||
|
||||
if (usernameInput) usernameInput.value = data.username || '';
|
||||
if (avatarPreview && data.username) avatarPreview.innerText = data.username[0].toLocaleUpperCase();
|
||||
if (remaining) remaining.innerText = data.recovery_codes_remaining || 0;
|
||||
|
||||
setTwoFactorPanels(!!data.two_factor_enabled);
|
||||
renderRecoveryCodes([]);
|
||||
} catch (err) {
|
||||
showAccountSettingsMessage(err.message || 'Failed to load account settings.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function saveAccountUsername(event) {
|
||||
event.preventDefault();
|
||||
|
||||
try {
|
||||
const data = await apiRequest('/api/account/username', 'POST', {
|
||||
username: document.getElementById('settings-username').value.trim(),
|
||||
password: document.getElementById('settings-username-password').value
|
||||
});
|
||||
|
||||
document.getElementById('settings-username-password').value = '';
|
||||
showAccountSettingsMessage('Username updated.');
|
||||
|
||||
const username = document.getElementById('username');
|
||||
const avatar = document.getElementById('avatar');
|
||||
const avatarPreview = document.getElementById('settings-avatar-preview');
|
||||
if (username) username.innerText = data.username;
|
||||
if (avatar && data.username) avatar.innerText = data.username[0].toLocaleUpperCase();
|
||||
if (avatarPreview && data.username) avatarPreview.innerText = data.username[0].toLocaleUpperCase();
|
||||
} catch (err) {
|
||||
showAccountSettingsMessage(err.message || 'Could not update username.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function saveAccountPassword(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const currentPassword = document.getElementById('settings-current-password').value;
|
||||
const newPassword = document.getElementById('settings-new-password').value;
|
||||
const confirmPassword = document.getElementById('settings-confirm-password').value;
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
showAccountSettingsMessage('New passwords do not match.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await apiRequest('/api/account/password', 'POST', {
|
||||
current_password: currentPassword,
|
||||
new_password: newPassword
|
||||
});
|
||||
|
||||
if (data && data.access_token && data.refresh_token) {
|
||||
localStorage.setItem('access_token', data.access_token);
|
||||
localStorage.setItem('refresh_token', data.refresh_token);
|
||||
}
|
||||
|
||||
document.getElementById('password-form').reset();
|
||||
showAccountSettingsMessage('Password updated. Your session was refreshed.');
|
||||
} catch (err) {
|
||||
showAccountSettingsMessage(err.message || 'Could not update password.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function startTwoFactorSetup() {
|
||||
try {
|
||||
const data = await apiRequest('/api/2fa/setup', 'POST');
|
||||
const panel = document.getElementById('two-factor-setup-panel');
|
||||
const qr = document.getElementById('two-factor-qr');
|
||||
const secret = document.getElementById('two-factor-secret');
|
||||
const otpauth = document.getElementById('two-factor-otpauth');
|
||||
|
||||
if (panel) panel.style.display = 'block';
|
||||
if (qr) {
|
||||
qr.src = data.qr_code;
|
||||
qr.style.display = data.qr_code ? 'block' : 'none';
|
||||
}
|
||||
if (secret) secret.textContent = data.secret || '';
|
||||
if (otpauth) {
|
||||
otpauth.href = data.otpauth_url || '#';
|
||||
otpauth.textContent = data.otpauth_url || 'No otpauth URL available';
|
||||
}
|
||||
|
||||
showAccountSettingsMessage('Scan the QR code or enter the setup key manually, then confirm the 6-digit code.');
|
||||
} catch (err) {
|
||||
showAccountSettingsMessage(err.message || 'Could not start 2FA setup.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function enableTwoFactor(event) {
|
||||
event.preventDefault();
|
||||
|
||||
try {
|
||||
const data = await apiRequest('/api/2fa/enable', 'POST', {
|
||||
code: document.getElementById('two-factor-enable-code').value.trim()
|
||||
});
|
||||
|
||||
document.getElementById('two-factor-enable-form').reset();
|
||||
document.getElementById('two-factor-setup-panel').style.display = 'none';
|
||||
setTwoFactorPanels(true);
|
||||
renderRecoveryCodes(data.recovery_codes || []);
|
||||
|
||||
const remaining = document.getElementById('recovery-codes-remaining');
|
||||
if (remaining) remaining.innerText = (data.recovery_codes || []).length;
|
||||
|
||||
showAccountSettingsMessage('2FA enabled. Download your recovery codes now.');
|
||||
loadProfile();
|
||||
} catch (err) {
|
||||
showAccountSettingsMessage(err.message || 'Could not enable 2FA.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function disableTwoFactor(event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (!confirm('Disable 2FA for your account?')) return;
|
||||
|
||||
try {
|
||||
await apiRequest('/api/2fa/disable', 'POST', {
|
||||
password: document.getElementById('two-factor-disable-password').value,
|
||||
code: document.getElementById('two-factor-disable-code').value.trim()
|
||||
});
|
||||
|
||||
document.getElementById('two-factor-disable-form').reset();
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('refresh_token');
|
||||
setTwoFactorPanels(false);
|
||||
renderRecoveryCodes([]);
|
||||
showAccountSettingsMessage('2FA disabled. Redirecting to login because sessions were revoked.');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login';
|
||||
}, 1200);
|
||||
} catch (err) {
|
||||
showAccountSettingsMessage(err.message || 'Could not disable 2FA.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function regenerateRecoveryCodes(event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (!confirm('Generate new recovery codes? Existing unused codes will stop working.')) return;
|
||||
|
||||
try {
|
||||
const data = await apiRequest('/api/2fa/recovery-codes/regenerate', 'POST', {
|
||||
password: document.getElementById('recovery-password').value,
|
||||
code: document.getElementById('recovery-code').value.trim()
|
||||
});
|
||||
|
||||
document.getElementById('recovery-regenerate-form').reset();
|
||||
renderRecoveryCodes(data.recovery_codes || []);
|
||||
|
||||
const remaining = document.getElementById('recovery-codes-remaining');
|
||||
if (remaining) remaining.innerText = (data.recovery_codes || []).length;
|
||||
|
||||
showAccountSettingsMessage('New recovery codes generated. Download them now.');
|
||||
} catch (err) {
|
||||
showAccountSettingsMessage(err.message || 'Could not regenerate recovery codes.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function downloadRecoveryCodes() {
|
||||
if (!latestRecoveryCodes || latestRecoveryCodes.length === 0) {
|
||||
showAccountSettingsMessage('No recovery codes available to download.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const text = [
|
||||
'MiauInv recovery codes',
|
||||
'Save these somewhere safe. Each code can be used once.',
|
||||
'',
|
||||
...latestRecoveryCodes,
|
||||
''
|
||||
].join('\n');
|
||||
|
||||
const blob = new Blob([text], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = 'miauinv-recovery-codes.txt';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user