Skip to content

fix(auth): #29 OAuth display-name cascade β€” add user_name + preferred_username tiers#75

Merged
TortoiseWolfe merged 1 commit intomainfrom
fix/29-oauth-display-name-cascade
May 5, 2026
Merged

fix(auth): #29 OAuth display-name cascade β€” add user_name + preferred_username tiers#75
TortoiseWolfe merged 1 commit intomainfrom
fix/29-oauth-display-name-cascade

Conversation

@TortoiseWolfe
Copy link
Copy Markdown
Owner

Summary

GitHub OAuth puts the user's @handle in user_metadata.user_name, not .name. The previous cascade (full_name > name > email_prefix) skipped user_name entirely, so GitHub users with no display name set on GitHub fell through to email prefix even though the @handle was a meaningful identifier already provided by the OAuth flow. OIDC providers using preferred_username had the same issue.

Cascade is now: full_name > name > user_name > preferred_username > email_prefix > "Anonymous User"

What changed

  • src/lib/auth/oauth-utils.ts β€” runtime path. Each tier trims whitespace; whitespace-only fields fall through. Non-string values are skipped without throwing.
  • supabase/migrations/20251006_complete_monolithic_setup.sql PART 9 β€” bootstrap path mirrors JS cascade. NULLIF(TRIM(...), '') handles whitespace-only fields.
  • src/lib/auth/oauth-utils.test.ts β€” 10 new tests (GitHub @handle, OIDC preferred_username, tier precedence, whitespace handling, realistic Google/GitHub fixtures, non-string metadata).

Why root-cause, not workaround

Per user preference: always prefer cleaner long-term solutions. Considered and rejected: detecting "auto-generated email prefix" in populateOAuthProfile and overwriting on match β€” that would leave the underlying cascade gap unfixed.

Bonus discovery: create_user_profile() trigger does NOT set display_name, only (id, created_at, updated_at). So populateOAuthProfile() is the sole authoritative runtime populator. The PART 9 UPDATE handles only the one-time bootstrap for pre-runtime-path users; idempotent via WHERE display_name IS NULL.

Verification

  • pnpm run type-check: clean
  • pnpm run lint: clean
  • pnpm test: 3247/3247 pass (added 10 new tests)
  • UPDATE applied to dev Supabase: 0 rows (no existing OAuth users)
  • Husky pre-push: all green

Test plan

  • CI runs full 3247-test suite
  • Smoke test: sign in via real GitHub OAuth β†’ display_name = handle
  • Smoke test: sign in via real Google OAuth β†’ display_name = full name

Closes #29

πŸ€– Generated with Claude Code

…_username tiers

GitHub OAuth puts the user's @handle in user_metadata.user_name, not
.name. The previous cascade (full_name > name > email_prefix) skipped
user_name entirely, so GitHub users with no display name set on GitHub
fell through to email prefix even though the @handle was a meaningful
identifier already provided by the OAuth flow. OIDC providers using
preferred_username had the same issue.

Cascade is now:
  full_name > name > user_name > preferred_username > email_prefix > "Anonymous User"

Fixed in two places:

1. src/lib/auth/oauth-utils.ts extractOAuthDisplayName() β€” runtime path,
   called by populateOAuthProfile() during the OAuth callback. Each tier
   now trims whitespace; whitespace-only metadata fields fall through
   instead of producing a whitespace-only display name. Non-string values
   (e.g. accidental nulls in metadata) are skipped without throwing.

2. supabase/migrations/20251006_complete_monolithic_setup.sql PART 9
   one-time UPDATE β€” mirrors the JS cascade so the SQL bootstrap path
   produces the same result. NULLIF(TRIM(...), '') handles whitespace-
   only fields the same way.

Note on runtime behavior: create_user_profile() (the on_auth_user_created
trigger at line 357) does NOT set display_name β€” it only inserts
(id, created_at, updated_at). At signup display_name starts NULL and
populateOAuthProfile() is the sole authoritative populator. The PART 9
UPDATE handles the one-time bootstrap for users who existed before that
runtime path landed; idempotent (only fires when display_name IS NULL).

Tests: 10 new cases in oauth-utils.test.ts cover the GitHub @handle
fallthrough, OIDC preferred_username, whitespace handling, non-string
metadata, and realistic Google/GitHub fixture shapes.

Verification:
- pnpm run type-check: clean
- pnpm run lint: clean
- pnpm test: 3247/3247 pass (was 3237 before β€” 10 new tests, all passing)
- UPDATE applied to dev Supabase via Management API: 0 rows affected
  (no existing OAuth users on dev), confirming the WHERE clause is
  idempotent and selective.

Closes #29

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@TortoiseWolfe TortoiseWolfe force-pushed the fix/29-oauth-display-name-cascade branch from ca61ee7 to 5e905f8 Compare May 5, 2026 02:14
@TortoiseWolfe TortoiseWolfe merged commit c768b5b into main May 5, 2026
28 checks passed
@TortoiseWolfe TortoiseWolfe deleted the fix/29-oauth-display-name-cascade branch May 5, 2026 04:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Gap-Audit] 015 OAuth Display Name: populate display_name from provider + cascade fallback + migrate existing users

2 participants