Fixed new issues with Undefined username in header on profile pic change & TOTP Enabled not checked

This commit is contained in:
Ryan
2025-05-14 06:51:16 -04:00
committed by GitHub
parent 939aa032f0
commit 87368143b5
5 changed files with 210 additions and 125 deletions

View File

@@ -77,6 +77,28 @@
- `#viewSliderContainer` uses `inline-flex` and `align-items: center` so that label, slider, and value text are vertically aligned with the other toolbar elements. - `#viewSliderContainer` uses `inline-flex` and `align-items: center` so that label, slider, and value text are vertically aligned with the other toolbar elements.
- Reset margins/padding on the label and value span within `#viewSliderContainer` to eliminate any vertical misalignment. - Reset margins/padding on the label and value span within `#viewSliderContainer` to eliminate any vertical misalignment.
### 9. Fixed new issues with Undefined username in header on profile pic change & TOTP Enabled not checked
**openUserPanel**
- **Rewritten entirely with DOM APIs** instead of `innerHTML` for any user-supplied text to eliminates “DOM text reinterpreted as HTML” warnings.
- **Default avatar fallback**: now uses `'/assets/default-avatar.png'` whenever `profile_picture` is empty.
- **TOTP checkbox initial state** is now set from the `totp_enabled` value returned by the server.
- **Modal title sync** on reopen now updates the `(username)` correctly (no more “undefined” until refresh).
- **Re-sync on reopen**: background color, avatar, TOTP checkbox and language selector all update when reopen the panel.
**updateAuthenticatedUI**
- **Username fix**: dropdown toggle now always uses `data.username` so the name never becomes `undefined` after uploading a picture.
- **Profile URL update** via `fetchProfilePicture()` always writes into `localStorage` before rebuilding the header, ensuring avatar+name stay in sync instantly.
- **Dropdown rebuild logic** tweaked to update the toggles innerHTML with both avatar and username on every call.
**UserModel::getUser**
- Switched to `explode(':', $line, 4)` to the fourth “profile_picture” field without clobbering the TOTP secret.
- **Strip trailing colons** from the stored URL (`rtrim($parts[3], ':')`) so we never send `…png:` back to the client.
- Returns an array with both `'username'` and `'profile_picture'`, matching what `getCurrentUser.php` needs.
--- ---
## Changes 5/8/2025 ## Changes 5/8/2025

View File

@@ -1,13 +1,13 @@
<?php <?php
require_once __DIR__ . '/../../../config/config.php'; require_once __DIR__ . '/../../../config/config.php';
require_once PROJECT_ROOT . '/src/controllers/UserController.php'; require_once PROJECT_ROOT . '/src/models/UserModel.php';
header('Content-Type: application/json'); header('Content-Type: application/json');
if (empty($_SESSION['authenticated'])) { if (empty($_SESSION['authenticated'])) {
http_response_code(401); http_response_code(401);
echo json_encode(['error'=>'Unauthorized']); echo json_encode(['error'=>'Unauthorized']);
exit; exit;
} }
$user = $_SESSION['username']; $user = $_SESSION['username'];

View File

@@ -223,6 +223,9 @@ async function fetchProfilePicture() {
} }
export async function updateAuthenticatedUI(data) { export async function updateAuthenticatedUI(data) {
// Save latest auth data for later reuse
window.__lastAuthData = data;
// 1) Remove loading overlay safely // 1) Remove loading overlay safely
const loading = document.getElementById('loadingOverlay'); const loading = document.getElementById('loadingOverlay');
if (loading) loading.remove(); if (loading) loading.remove();
@@ -304,6 +307,11 @@ export async function updateAuthenticatedUI(data) {
? `<img src="${profilePicUrl}" style="width:24px;height:24px;border-radius:50%;vertical-align:middle;">` ? `<img src="${profilePicUrl}" style="width:24px;height:24px;border-radius:50%;vertical-align:middle;">`
: `<i class="material-icons">account_circle</i>`; : `<i class="material-icons">account_circle</i>`;
// fallback username if missing
const usernameText = data.username
|| localStorage.getItem("username")
|| "";
if (!dd) { if (!dd) {
dd = document.createElement("div"); dd = document.createElement("div");
dd.id = "userDropdown"; dd.id = "userDropdown";
@@ -314,7 +322,11 @@ export async function updateAuthenticatedUI(data) {
toggle.id = "userDropdownToggle"; toggle.id = "userDropdownToggle";
toggle.classList.add("btn","btn-user"); toggle.classList.add("btn","btn-user");
toggle.setAttribute("title", t("user_settings")); toggle.setAttribute("title", t("user_settings"));
toggle.innerHTML = `${avatarHTML}<span class="dropdown-username">${data.username}</span><span class="dropdown-caret"></span>`; toggle.innerHTML = `
${avatarHTML}
<span class="dropdown-username">${usernameText}</span>
<span class="dropdown-caret"></span>
`;
dd.append(toggle); dd.append(toggle);
// menu // menu
@@ -375,9 +387,13 @@ export async function updateAuthenticatedUI(data) {
}); });
} else { } else {
// update avatar only // update avatar & username only
const tog = dd.querySelector("#userDropdownToggle"); const tog = dd.querySelector("#userDropdownToggle");
tog.innerHTML = `${avatarHTML}<span class="dropdown-username">${data.username}</span><span class="dropdown-caret"></span>`; tog.innerHTML = `
${avatarHTML}
<span class="dropdown-username">${usernameText}</span>
<span class="dropdown-caret"></span>
`;
dd.style.display = "inline-block"; dd.style.display = "inline-block";
} }
} }

View File

@@ -184,105 +184,128 @@ function normalizePicUrl(raw) {
export async function openUserPanel() { export async function openUserPanel() {
// 1) load data // 1) load data
const { username = 'User', profile_picture = '', totp_enabled = false } = await fetchCurrentUser(); const { username = 'User', profile_picture = '', totp_enabled = false } = await fetchCurrentUser();
const raw = profile_picture; const raw = profile_picture;
const picUrl = normalizePicUrl(raw); const picUrl = normalizePicUrl(raw) || '/assets/default-avatar.png';
// 2) darkmode helpers // 2) darkmode helpers
const isDark = document.body.classList.contains('dark-mode'); const isDark = document.body.classList.contains('dark-mode');
const overlayBg = isDark ? 'rgba(0,0,0,0.7)' : 'rgba(0,0,0,0.3)'; const overlayBg = isDark ? 'rgba(0,0,0,0.7)' : 'rgba(0,0,0,0.3)';
const contentCss = ` const contentStyle = `
background: ${isDark ? '#2c2c2c' : '#fff'}; background: ${isDark ? '#2c2c2c' : '#fff'};
color: ${isDark ? '#e0e0e0' : '#000'}; color: ${isDark ? '#e0e0e0' : '#000'};
padding: 20px; padding: 20px;
max-width: 600px; max-width: 600px; width:90%;
width: 90%; border-radius: 8px;
border-radius: 8px; overflow-y: auto; max-height: 415px;
overflow-y: auto; border: ${isDark ? '1px solid #444' : '1px solid #ccc'};
max-height: 415px; box-sizing: border-box;
border: ${isDark ? '1px solid #444' : '1px solid #ccc'}; scrollbar-width: none;
box-sizing: border-box; -ms-overflow-style: none;
`;
/* hide scrollbar in Firefox */ // 3) create or reuse modal
scrollbar-width: none;
/* hide scrollbar in IE 10+ */
-ms-overflow-style: none;
`;
// 3) build or re-use modal
let modal = document.getElementById('userPanelModal'); let modal = document.getElementById('userPanelModal');
if (!modal) { if (!modal) {
// overlay
modal = document.createElement('div'); modal = document.createElement('div');
modal.id = 'userPanelModal'; modal.id = 'userPanelModal';
modal.style.cssText = ` Object.assign(modal.style, {
position:fixed; top:0; left:0; right:0; bottom:0; position: 'fixed',
background:${overlayBg}; top: '0',
display:flex; align-items:center; justify-content:center; left: '0',
z-index:1000; right: '0',
bottom: '0',
background: overlayBg,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: '1000',
});
// content container
const content = document.createElement('div');
content.className = 'modal-content';
content.style.cssText = contentStyle;
// close button
const closeBtn = document.createElement('span');
closeBtn.id = 'closeUserPanel';
closeBtn.className = 'editor-close-btn';
closeBtn.textContent = '×';
closeBtn.addEventListener('click', () => modal.style.display = 'none');
content.appendChild(closeBtn);
// avatar + picker
const avatarWrapper = document.createElement('div');
avatarWrapper.style.cssText = 'text-align:center; margin-bottom:20px;';
const avatarInner = document.createElement('div');
avatarInner.style.cssText = 'position:relative; width:80px; height:80px; margin:0 auto;';
const img = document.createElement('img');
img.id = 'profilePicPreview';
img.src = picUrl;
img.alt = 'Profile Picture';
img.style.cssText = 'width:100%; height:100%; border-radius:50%; object-fit:cover;';
avatarInner.appendChild(img);
const label = document.createElement('label');
label.htmlFor = 'profilePicInput';
label.style.cssText = `
position:absolute; bottom:0; right:0;
width:24px; height:24px;
background:rgba(0,0,0,0.6);
border-radius:50%; display:flex;
align-items:center; justify-content:center;
cursor:pointer;
`; `;
const editIcon = document.createElement('i');
editIcon.className = 'material-icons';
editIcon.style.cssText = 'color:#fff; font-size:16px;';
editIcon.textContent = 'edit';
label.appendChild(editIcon);
avatarInner.appendChild(label);
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.id = 'profilePicInput';
fileInput.accept = 'image/*';
fileInput.style.display = 'none';
avatarInner.appendChild(fileInput);
avatarWrapper.appendChild(avatarInner);
content.appendChild(avatarWrapper);
modal.innerHTML = ` // title
<div class="modal-content" style="${contentCss}"> const title = document.createElement('h3');
<span id="closeUserPanel" class="editor-close-btn">&times;</span> title.style.cssText = 'text-align:center; margin-bottom:20px;';
<div style="text-align:center; margin-bottom:20px;"> title.textContent = `${t('user_panel')} (${username})`;
<div style="position:relative; width:80px; height:80px; margin:0 auto;"> content.appendChild(title);
<img id="profilePicPreview"
src="${picUrl || '/assets/default-avatar.png'}"
style="width:100%; height:100%; border-radius:50%; object-fit:cover;">
<label for="profilePicInput"
style="
position:absolute; bottom:0; right:0;
width:24px; height:24px; background:rgba(0,0,0,0.6);
border-radius:50%; display:flex; align-items:center;
justify-content:center; cursor:pointer;">
<i class="material-icons" style="color:#fff; font-size:16px;">edit</i>
</label>
<input type="file" id="profilePicInput" accept="image/*" style="display:none">
</div>
</div>
<h3 style="text-align:center; margin-bottom:20px;">
${t('user_panel')} (${username})
</h3>
<button id="openChangePasswordModalBtn" class="btn btn-primary" style="margin-bottom:15px;">
${t('change_password')}
</button>
<fieldset style="margin-bottom:15px;">
<legend>${t('totp_settings')}</legend>
<label style="cursor:pointer;">
<input type="checkbox" id="userTOTPEnabled" style="vertical-align:middle;">
${t('enable_totp')}
</label>
</fieldset>
<fieldset style="margin-bottom:15px;">
<legend>${t('language')}</legend>
<select id="languageSelector" class="form-select">
<option value="en">${t('english')}</option>
<option value="es">${t('spanish')}</option>
<option value="fr">${t('french')}</option>
<option value="de">${t('german')}</option>
</select>
</fieldset>
</div>
`;
document.body.appendChild(modal);
// --- wire up handlers --- // change password btn
const pwdBtn = document.createElement('button');
pwdBtn.id = 'openChangePasswordModalBtn';
pwdBtn.className = 'btn btn-primary';
pwdBtn.style.marginBottom = '15px';
pwdBtn.textContent = t('change_password');
pwdBtn.addEventListener('click', () => {
document.getElementById('changePasswordModal').style.display = 'block';
});
content.appendChild(pwdBtn);
modal.querySelector('#closeUserPanel') // TOTP fieldset
.addEventListener('click', () => modal.style.display = 'none'); const totpFs = document.createElement('fieldset');
totpFs.style.marginBottom = '15px';
modal.querySelector('#openChangePasswordModalBtn') const totpLegend = document.createElement('legend');
.addEventListener('click', () => { totpLegend.textContent = t('totp_settings');
document.getElementById('changePasswordModal').style.display = 'block'; totpFs.appendChild(totpLegend);
}); const totpLabel = document.createElement('label');
totpLabel.style.cursor = 'pointer';
// TOTP const totpCb = document.createElement('input');
const totpCb = modal.querySelector('#userTOTPEnabled'); totpCb.type = 'checkbox';
totpCb.addEventListener('change', async function () { totpCb.id = 'userTOTPEnabled';
totpCb.style.verticalAlign = 'middle';
totpCb.checked = totp_enabled;
totpCb.addEventListener('change', async function() {
const resp = await fetch('/api/updateUserPanel.php', { const resp = await fetch('/api/updateUserPanel.php', {
method: 'POST', method: 'POST', credentials: 'include',
credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type':'application/json',
'X-CSRF-Token': window.csrfToken 'X-CSRF-Token': window.csrfToken
}, },
body: JSON.stringify({ totp_enabled: this.checked }) body: JSON.stringify({ totp_enabled: this.checked })
@@ -291,37 +314,52 @@ export async function openUserPanel() {
if (!js.success) showToast(js.error || t('error_updating_totp_setting')); if (!js.success) showToast(js.error || t('error_updating_totp_setting'));
else if (this.checked) openTOTPModal(); else if (this.checked) openTOTPModal();
}); });
totpLabel.appendChild(totpCb);
totpLabel.append(` ${t('enable_totp')}`);
totpFs.appendChild(totpLabel);
content.appendChild(totpFs);
// Language // language fieldset
const langSel = modal.querySelector('#languageSelector'); const langFs = document.createElement('fieldset');
langSel.addEventListener('change', function () { langFs.style.marginBottom = '15px';
const langLegend = document.createElement('legend');
langLegend.textContent = t('language');
langFs.appendChild(langLegend);
const langSel = document.createElement('select');
langSel.id = 'languageSelector';
langSel.className = 'form-select';
['en','es','fr','de'].forEach(code => {
const opt = document.createElement('option');
opt.value = code;
opt.textContent = t(code === 'en'? 'english' : code === 'es'? 'spanish' : code === 'fr'? 'french' : 'german');
langSel.appendChild(opt);
});
langSel.value = localStorage.getItem('language') || 'en';
langSel.addEventListener('change', function() {
localStorage.setItem('language', this.value); localStorage.setItem('language', this.value);
setLocale(this.value); setLocale(this.value);
applyTranslations(); applyTranslations();
}); });
langFs.appendChild(langSel);
content.appendChild(langFs);
// Autoupload on file select // wire up imageinput change
const fileInput = modal.querySelector('#profilePicInput'); fileInput.addEventListener('change', async function() {
fileInput.addEventListener('change', async function () { const f = this.files[0];
const file = this.files[0]; if (!f) return;
if (!file) return;
// preview immediately // preview immediately
const img = modal.querySelector('#profilePicPreview'); img.src = URL.createObjectURL(f);
img.src = URL.createObjectURL(file);
// upload // upload
const fd = new FormData(); const fd = new FormData();
fd.append('profile_picture', file); fd.append('profile_picture', f);
try { try {
const res = await fetch('/api/profile/uploadPicture.php', { const res = await fetch('/api/profile/uploadPicture.php', {
method: 'POST', method: 'POST', credentials: 'include',
credentials: 'include',
headers: { 'X-CSRF-Token': window.csrfToken }, headers: { 'X-CSRF-Token': window.csrfToken },
body: fd body: fd
}); });
const text = await res.text(); const text = await res.text();
const js = JSON.parse(text || '{}'); const js = JSON.parse(text || '{}');
if (!res.ok) { if (!res.ok) {
showToast(js.error || t('error_updating_picture')); showToast(js.error || t('error_updating_picture'));
return; return;
@@ -329,7 +367,6 @@ export async function openUserPanel() {
const newUrl = normalizePicUrl(js.url); const newUrl = normalizePicUrl(js.url);
img.src = newUrl; img.src = newUrl;
localStorage.setItem('profilePicUrl', newUrl); localStorage.setItem('profilePicUrl', newUrl);
// refresh the header immediately
updateAuthenticatedUI(window.__lastAuthData || {}); updateAuthenticatedUI(window.__lastAuthData || {});
showToast(t('profile_picture_updated')); showToast(t('profile_picture_updated'));
} catch (e) { } catch (e) {
@@ -338,15 +375,18 @@ export async function openUserPanel() {
} }
}); });
// finalize
modal.appendChild(content);
document.body.appendChild(modal);
} else { } else {
// reuse on reopen
modal.style.background = overlayBg; Object.assign(modal.style, { background: overlayBg });
const contentEl = modal.querySelector('.modal-content'); const content = modal.querySelector('.modal-content');
contentEl.style.cssText = contentCss; content.style.cssText = contentStyle;
// re-open: sync current values modal.querySelector('#profilePicPreview').src = picUrl || '/assets/default-avatar.png';
modal.querySelector('#profilePicPreview').src = picUrl || '/images/default-avatar.png'; modal.querySelector('#userTOTPEnabled').checked = totp_enabled;
modal.querySelector('#userTOTPEnabled').checked = totp_enabled; modal.querySelector('#languageSelector').value = localStorage.getItem('language') || 'en';
modal.querySelector('#languageSelector').value = localStorage.getItem('language') || 'en'; modal.querySelector('h3').textContent = `${t('user_panel')} (${username})`;
} }
// show // show

View File

@@ -676,22 +676,29 @@ class userModel
if (! file_exists($usersFile)) { if (! file_exists($usersFile)) {
return []; return [];
} }
foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { foreach (file($usersFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
// explode into at most 4 parts: [0]=username, [1]=hash, [2]=isAdmin, [3]=profileUrl (might include a trailing colon) // split *all* the fields
$parts = explode(':', $line, 4); $parts = explode(':', $line);
if ($parts[0] !== $username) { if ($parts[0] !== $username) {
continue; continue;
} }
// strip any trailing colon(s) from the URL field
$pic = isset($parts[3]) ? rtrim($parts[3], ':') : ''; // determine admin & totp
$isAdmin = (isset($parts[2]) && $parts[2] === '1');
$totpEnabled = !empty($parts[3]);
// profile_picture is the 5th field if present
$pic = isset($parts[4]) ? $parts[4] : '';
return [ return [
'username' => $parts[0], 'username' => $parts[0],
'isAdmin' => $isAdmin,
'totp_enabled' => $totpEnabled,
'profile_picture' => $pic, 'profile_picture' => $pic,
]; ];
} }
return []; // user not found return []; // user not found
} }