fix(auth): include avatar_url in login response + lazy refresh stale cache
Login was only returning {id, email, name, role}, so localStorage.ls_user
never had avatar_url for sessions started before today — and the sidebar
fell back to initials forever. Fixes:
• login response now includes avatar_url
• renderNavAvatar detects 'undefined' (cache predates the field) vs
'null' (verified absent) and fires a one-shot /auth/me refresh in
the background, then re-paints. Self-healing for existing sessions
without forcing re-login.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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); }
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = `<img src="/avatars/${escapeHtml(url)}?t=${Date.now()}" alt="" style="width:100%;height:100%;object-fit:cover;border-radius:inherit;display:block">`;
|
||||
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() {
|
||||
|
||||
Reference in New Issue
Block a user