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:
Maxim Dolgolyov
2026-05-29 15:07:16 +03:00
parent 4423a72635
commit eb19ce3cf9
2 changed files with 25 additions and 7 deletions
+2 -2
View File
@@ -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); }
}
+23 -5
View File
@@ -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() {