Fixed new issues with Undefined username in header on profile pic change & TOTP Enabled not checked
This commit is contained in:
22
CHANGELOG.md
22
CHANGELOG.md
@@ -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.
|
||||
- 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 toggle’s 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
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<?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');
|
||||
|
||||
if (empty($_SESSION['authenticated'])) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['error'=>'Unauthorized']);
|
||||
exit;
|
||||
http_response_code(401);
|
||||
echo json_encode(['error'=>'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$user = $_SESSION['username'];
|
||||
|
||||
@@ -223,6 +223,9 @@ async function fetchProfilePicture() {
|
||||
}
|
||||
|
||||
export async function updateAuthenticatedUI(data) {
|
||||
// Save latest auth data for later reuse
|
||||
window.__lastAuthData = data;
|
||||
|
||||
// 1) Remove loading overlay safely
|
||||
const loading = document.getElementById('loadingOverlay');
|
||||
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;">`
|
||||
: `<i class="material-icons">account_circle</i>`;
|
||||
|
||||
// fallback username if missing
|
||||
const usernameText = data.username
|
||||
|| localStorage.getItem("username")
|
||||
|| "";
|
||||
|
||||
if (!dd) {
|
||||
dd = document.createElement("div");
|
||||
dd.id = "userDropdown";
|
||||
@@ -314,7 +322,11 @@ export async function updateAuthenticatedUI(data) {
|
||||
toggle.id = "userDropdownToggle";
|
||||
toggle.classList.add("btn","btn-user");
|
||||
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);
|
||||
|
||||
// menu
|
||||
@@ -375,9 +387,13 @@ export async function updateAuthenticatedUI(data) {
|
||||
});
|
||||
|
||||
} else {
|
||||
// update avatar only
|
||||
// update avatar & username only
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,105 +184,128 @@ function normalizePicUrl(raw) {
|
||||
export async function openUserPanel() {
|
||||
// 1) load data
|
||||
const { username = 'User', profile_picture = '', totp_enabled = false } = await fetchCurrentUser();
|
||||
const raw = profile_picture;
|
||||
const picUrl = normalizePicUrl(raw);
|
||||
const raw = profile_picture;
|
||||
const picUrl = normalizePicUrl(raw) || '/assets/default-avatar.png';
|
||||
|
||||
// 2) dark‐mode 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 contentCss = `
|
||||
background: ${isDark ? '#2c2c2c' : '#fff'};
|
||||
color: ${isDark ? '#e0e0e0' : '#000'};
|
||||
padding: 20px;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
border-radius: 8px;
|
||||
overflow-y: auto;
|
||||
max-height: 415px;
|
||||
border: ${isDark ? '1px solid #444' : '1px solid #ccc'};
|
||||
box-sizing: border-box;
|
||||
const contentStyle = `
|
||||
background: ${isDark ? '#2c2c2c' : '#fff'};
|
||||
color: ${isDark ? '#e0e0e0' : '#000'};
|
||||
padding: 20px;
|
||||
max-width: 600px; width:90%;
|
||||
border-radius: 8px;
|
||||
overflow-y: auto; max-height: 415px;
|
||||
border: ${isDark ? '1px solid #444' : '1px solid #ccc'};
|
||||
box-sizing: border-box;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
`;
|
||||
|
||||
/* hide scrollbar in Firefox */
|
||||
scrollbar-width: none;
|
||||
/* hide scrollbar in IE 10+ */
|
||||
-ms-overflow-style: none;
|
||||
`;
|
||||
|
||||
// 3) build or re-use modal
|
||||
// 3) create or reuse modal
|
||||
let modal = document.getElementById('userPanelModal');
|
||||
if (!modal) {
|
||||
// overlay
|
||||
modal = document.createElement('div');
|
||||
modal.id = 'userPanelModal';
|
||||
modal.style.cssText = `
|
||||
position:fixed; top:0; left:0; right:0; bottom:0;
|
||||
background:${overlayBg};
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
z-index:1000;
|
||||
Object.assign(modal.style, {
|
||||
position: 'fixed',
|
||||
top: '0',
|
||||
left: '0',
|
||||
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 = `
|
||||
<div class="modal-content" style="${contentCss}">
|
||||
<span id="closeUserPanel" class="editor-close-btn">×</span>
|
||||
<div style="text-align:center; margin-bottom:20px;">
|
||||
<div style="position:relative; width:80px; height:80px; margin:0 auto;">
|
||||
<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);
|
||||
// title
|
||||
const title = document.createElement('h3');
|
||||
title.style.cssText = 'text-align:center; margin-bottom:20px;';
|
||||
title.textContent = `${t('user_panel')} (${username})`;
|
||||
content.appendChild(title);
|
||||
|
||||
// --- 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')
|
||||
.addEventListener('click', () => modal.style.display = 'none');
|
||||
|
||||
modal.querySelector('#openChangePasswordModalBtn')
|
||||
.addEventListener('click', () => {
|
||||
document.getElementById('changePasswordModal').style.display = 'block';
|
||||
});
|
||||
|
||||
// TOTP
|
||||
const totpCb = modal.querySelector('#userTOTPEnabled');
|
||||
totpCb.addEventListener('change', async function () {
|
||||
// TOTP fieldset
|
||||
const totpFs = document.createElement('fieldset');
|
||||
totpFs.style.marginBottom = '15px';
|
||||
const totpLegend = document.createElement('legend');
|
||||
totpLegend.textContent = t('totp_settings');
|
||||
totpFs.appendChild(totpLegend);
|
||||
const totpLabel = document.createElement('label');
|
||||
totpLabel.style.cursor = 'pointer';
|
||||
const totpCb = document.createElement('input');
|
||||
totpCb.type = 'checkbox';
|
||||
totpCb.id = 'userTOTPEnabled';
|
||||
totpCb.style.verticalAlign = 'middle';
|
||||
totpCb.checked = totp_enabled;
|
||||
totpCb.addEventListener('change', async function() {
|
||||
const resp = await fetch('/api/updateUserPanel.php', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
method: 'POST', credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Type':'application/json',
|
||||
'X-CSRF-Token': window.csrfToken
|
||||
},
|
||||
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'));
|
||||
else if (this.checked) openTOTPModal();
|
||||
});
|
||||
totpLabel.appendChild(totpCb);
|
||||
totpLabel.append(` ${t('enable_totp')}`);
|
||||
totpFs.appendChild(totpLabel);
|
||||
content.appendChild(totpFs);
|
||||
|
||||
// Language
|
||||
const langSel = modal.querySelector('#languageSelector');
|
||||
langSel.addEventListener('change', function () {
|
||||
// language fieldset
|
||||
const langFs = document.createElement('fieldset');
|
||||
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);
|
||||
setLocale(this.value);
|
||||
applyTranslations();
|
||||
});
|
||||
langFs.appendChild(langSel);
|
||||
content.appendChild(langFs);
|
||||
|
||||
// Auto‐upload on file select
|
||||
const fileInput = modal.querySelector('#profilePicInput');
|
||||
fileInput.addEventListener('change', async function () {
|
||||
const file = this.files[0];
|
||||
if (!file) return;
|
||||
|
||||
// wire up image‐input change
|
||||
fileInput.addEventListener('change', async function() {
|
||||
const f = this.files[0];
|
||||
if (!f) return;
|
||||
// preview immediately
|
||||
const img = modal.querySelector('#profilePicPreview');
|
||||
img.src = URL.createObjectURL(file);
|
||||
|
||||
img.src = URL.createObjectURL(f);
|
||||
// upload
|
||||
const fd = new FormData();
|
||||
fd.append('profile_picture', file);
|
||||
fd.append('profile_picture', f);
|
||||
try {
|
||||
const res = await fetch('/api/profile/uploadPicture.php', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
const res = await fetch('/api/profile/uploadPicture.php', {
|
||||
method: 'POST', credentials: 'include',
|
||||
headers: { 'X-CSRF-Token': window.csrfToken },
|
||||
body: fd
|
||||
});
|
||||
const text = await res.text();
|
||||
const js = JSON.parse(text || '{}');
|
||||
const js = JSON.parse(text || '{}');
|
||||
if (!res.ok) {
|
||||
showToast(js.error || t('error_updating_picture'));
|
||||
return;
|
||||
@@ -329,7 +367,6 @@ export async function openUserPanel() {
|
||||
const newUrl = normalizePicUrl(js.url);
|
||||
img.src = newUrl;
|
||||
localStorage.setItem('profilePicUrl', newUrl);
|
||||
// refresh the header immediately
|
||||
updateAuthenticatedUI(window.__lastAuthData || {});
|
||||
showToast(t('profile_picture_updated'));
|
||||
} catch (e) {
|
||||
@@ -338,15 +375,18 @@ export async function openUserPanel() {
|
||||
}
|
||||
});
|
||||
|
||||
// finalize
|
||||
modal.appendChild(content);
|
||||
document.body.appendChild(modal);
|
||||
} else {
|
||||
|
||||
modal.style.background = overlayBg;
|
||||
const contentEl = modal.querySelector('.modal-content');
|
||||
contentEl.style.cssText = contentCss;
|
||||
// re-open: sync current values
|
||||
modal.querySelector('#profilePicPreview').src = picUrl || '/images/default-avatar.png';
|
||||
modal.querySelector('#userTOTPEnabled').checked = totp_enabled;
|
||||
modal.querySelector('#languageSelector').value = localStorage.getItem('language') || 'en';
|
||||
// reuse on reopen
|
||||
Object.assign(modal.style, { background: overlayBg });
|
||||
const content = modal.querySelector('.modal-content');
|
||||
content.style.cssText = contentStyle;
|
||||
modal.querySelector('#profilePicPreview').src = picUrl || '/assets/default-avatar.png';
|
||||
modal.querySelector('#userTOTPEnabled').checked = totp_enabled;
|
||||
modal.querySelector('#languageSelector').value = localStorage.getItem('language') || 'en';
|
||||
modal.querySelector('h3').textContent = `${t('user_panel')} (${username})`;
|
||||
}
|
||||
|
||||
// show
|
||||
|
||||
@@ -678,16 +678,23 @@ class userModel
|
||||
}
|
||||
|
||||
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)
|
||||
$parts = explode(':', $line, 4);
|
||||
// split *all* the fields
|
||||
$parts = explode(':', $line);
|
||||
|
||||
if ($parts[0] !== $username) {
|
||||
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 [
|
||||
'username' => $parts[0],
|
||||
'isAdmin' => $isAdmin,
|
||||
'totp_enabled' => $totpEnabled,
|
||||
'profile_picture' => $pic,
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user