fix(260607-l6l): derive displayName from OIDC claims in me.ts + resolveUserId
BUG 2: Both me.ts and events.ts resolveUserId were passing email (often absent) as displayName to upsertUser, resulting in blank legend names. Also, upsertUser returned existing rows unchanged even when displayName was null and a better value was now available. - me.ts: derive displayName via name → preferred_username → email → "Member <sub-prefix>" fallback, checked defensively. Updated JSDoc. - events.ts resolveUserId: same derivation so write-path upserts don't re-blank a correctly-set displayName. - user.ts: when existing row has null displayName and caller supplies one, issue an UPDATE so already-existing blank rows are corrected on next login. Authelia-side emission of name/preferred_username is an operator concern (claim mappings / userinfo scope in authelia config) — out of scope here. The code now reads whatever claims are present and falls back sensibly.
This commit is contained in:
@@ -49,6 +49,15 @@ export async function upsertUser(
|
||||
.limit(1)
|
||||
|
||||
if (existing[0]) {
|
||||
// If the existing row has no displayName but the caller supplies one, update it now.
|
||||
// This corrects rows created before robust claim derivation was in place (BUG 2 fix).
|
||||
if (!existing[0].displayName && displayName) {
|
||||
await db
|
||||
.update(users)
|
||||
.set({ displayName })
|
||||
.where(eq(users.id, existing[0].id))
|
||||
return { ...existing[0], displayName }
|
||||
}
|
||||
return existing[0]
|
||||
}
|
||||
|
||||
|
||||
@@ -65,9 +65,19 @@ async function resolveUserId(c: any): Promise<number | null> {
|
||||
|
||||
const iss = (auth.iss as string | undefined) ?? ''
|
||||
const sub = auth.sub ?? ''
|
||||
const email = typeof auth.email === 'string' ? auth.email : undefined
|
||||
|
||||
const user = await upsertUser(iss, sub, email)
|
||||
// Derive displayName with same preference order as me.ts (name → preferred_username
|
||||
// → email → sub fallback). Both call sites must agree so a write-path upsert does not
|
||||
// overwrite a correctly-derived name with a worse one.
|
||||
const claimStr = (v: unknown): string | undefined =>
|
||||
typeof v === 'string' && v.trim() !== '' ? v.trim() : undefined
|
||||
const displayName =
|
||||
claimStr(auth.name) ??
|
||||
claimStr(auth.preferred_username) ??
|
||||
claimStr(auth.email) ??
|
||||
`Member ${String(sub).slice(0, 8)}`
|
||||
|
||||
const user = await upsertUser(iss, sub, displayName)
|
||||
return user?.id ?? null
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
* Flow (normal — OIDC active):
|
||||
* 1. getAuth(c) reads iss + sub from the OIDC session JWT cookie
|
||||
* (validated and refreshed by oidcAuthMiddleware — never reaches here unauthenticated)
|
||||
* 2. upsertUser(iss, sub, email) writes the user row on first visit, returns
|
||||
* the existing row on subsequent visits (idempotent, keyed on iss+sub, D-10)
|
||||
* 2. Derives displayName from OIDC claims (name → preferred_username → email → sub fallback)
|
||||
* then calls upsertUser(iss, sub, displayName) — writes on first visit, corrects a
|
||||
* previously blank displayName on subsequent visits (idempotent, keyed on iss+sub, D-10)
|
||||
* 3. Returns { user: { id, displayName, color } }
|
||||
*
|
||||
* Flow (dev bypass — DEV_AUTH_BYPASS=true, non-production):
|
||||
@@ -48,12 +49,28 @@ meRouter.get('/', async (c) => {
|
||||
return c.json({ error: 'Unauthorized' }, 401)
|
||||
}
|
||||
|
||||
// iss and sub are the stable identity fields; email is a display hint only (D-10)
|
||||
// iss and sub are the stable identity fields — identity is always keyed on iss+sub (D-10).
|
||||
const iss = (auth.iss as string | undefined) ?? ''
|
||||
const sub = auth.sub ?? ''
|
||||
const email = typeof auth.email === 'string' ? auth.email : undefined
|
||||
|
||||
const user = await upsertUser(iss, sub, email)
|
||||
// Derive the best available display name from OIDC claims, in preference order:
|
||||
// 1. name — full name set by the IdP (most human-friendly)
|
||||
// 2. preferred_username — often the login handle; still readable
|
||||
// 3. email — readable but reveals contact info; acceptable fallback
|
||||
// 4. sub — always present; not human-friendly but never blank
|
||||
//
|
||||
// Each candidate is tested defensively — Authelia may omit or blank-out any claim.
|
||||
// Whether Authelia emits name/preferred_username is an operator configuration concern
|
||||
// (e.g. userinfo scope, claim mappings in authelia config) — out of scope here.
|
||||
const claimStr = (v: unknown): string | undefined =>
|
||||
typeof v === 'string' && v.trim() !== '' ? v.trim() : undefined
|
||||
const displayName =
|
||||
claimStr(auth.name) ??
|
||||
claimStr(auth.preferred_username) ??
|
||||
claimStr(auth.email) ??
|
||||
`Member ${String(sub).slice(0, 8)}`
|
||||
|
||||
const user = await upsertUser(iss, sub, displayName)
|
||||
|
||||
if (!user) {
|
||||
return c.json({ error: 'Could not resolve user' }, 500)
|
||||
|
||||
Reference in New Issue
Block a user