diff --git a/backend/src/controllers/authController.js b/backend/src/controllers/authController.js index 5f42e91..91d4c10 100644 --- a/backend/src/controllers/authController.js +++ b/backend/src/controllers/authController.js @@ -51,7 +51,7 @@ async function login(req, res, next) { return res.status(400).json({ error: 'email and password are required' }); const user = db.prepare( - 'SELECT id, email, name, role, password_hash, token_version FROM users WHERE email = ?' + 'SELECT id, email, name, role, password_hash, token_version, avatar_url FROM users WHERE email = ?' ).get(email); if (!user || !(await bcrypt.compare(password, user.password_hash))) @@ -60,7 +60,7 @@ async function login(req, res, next) { db.prepare("UPDATE users SET last_login = datetime('now') WHERE id = ?").run(user.id); const token = signToken(user); - res.json({ token, user: { id: user.id, email: user.email, name: user.name, role: user.role } }); + res.json({ token, user: { id: user.id, email: user.email, name: user.name, role: user.role, avatar_url: user.avatar_url } }); } catch (err) { next(err); } } diff --git a/js/api.js b/js/api.js index e96dd6e..b56fed8 100644 --- a/js/api.js +++ b/js/api.js @@ -709,7 +709,12 @@ function lsModal({ title = '', content = '', size = 'md', actions = [], onClose, /* ── renderNavAvatar — paint the sidebar avatar (image or initials) ── Exported via LS.refreshNavAvatar so pages that update avatar_url - (profile preset picker, upload flow) can re-paint without reload. */ + (profile preset picker, upload flow) can re-paint without reload. + + Stale-cache recovery: if the cached user has no `avatar_url` field + (e.g. logged in before login was returning it), fetch /auth/me once + in the background and re-paint when it returns. */ +let _navAvatarFetchInflight = false; function renderNavAvatar(el, user) { if (!el) return; const u = user || getUser(); @@ -717,10 +722,23 @@ function renderNavAvatar(el, user) { if (url) { el.innerHTML = ``; el.style.background = 'transparent'; - } else { - const initials = (u?.name || 'LS').split(' ').slice(0, 2).map(w => w[0]?.toUpperCase() || '').join('') || 'LS'; - el.textContent = initials; - el.style.background = ''; + return; + } + // No url yet — paint initials, then opportunistically refresh from /auth/me + // if the field is absent (not just empty). `null` means "verified absent", + // `undefined` means "we never fetched and might be stale". + const initials = (u?.name || 'LS').split(' ').slice(0, 2).map(w => w[0]?.toUpperCase() || '').join('') || 'LS'; + el.textContent = initials; + el.style.background = ''; + if (u && u.avatar_url === undefined && !_navAvatarFetchInflight && isLoggedIn()) { + _navAvatarFetchInflight = true; + fetchMe().then(fresh => { + if (!fresh) return; + const merged = { ...getUser(), ...fresh, avatar_url: fresh.avatar_url ?? null }; + setUser(merged); + const newEl = document.getElementById('nav-avatar'); + if (newEl) renderNavAvatar(newEl, merged); + }).catch(() => {}).finally(() => { _navAvatarFetchInflight = false; }); } } function refreshNavAvatar() {