Skip to content

Commit 7c9890f

Browse files
committed
fix(sso): infer GitHub IdP from username when type is missing
A linked IdP carries an explicit `type` only when its idpId still matches an active provider (joinLinkedIdps); a link to a deactivated/omitted provider arrives with type undefined, so IdpIcon fell back to the generic mail glyph even for a plain GitHub handle — confusing UX. Add inferIdpType (handle-shaped username, no @/., → GITHUB) and apply it as a fallback (type ?? inferIdpType(username)) on the linked-account icons in both the SSO linked-accounts list and the account picker. A real backend type always wins; email-shaped logins keep the neutral glyph.
1 parent af04ea9 commit 7c9890f

4 files changed

Lines changed: 45 additions & 16 deletions

File tree

app/modules/auth/idp-detect.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Best-effort IdP type inference from a linked account's username.
2+
//
3+
// Why: a linked IdP only carries an explicit `type` when its `idpId` still matches an ACTIVE
4+
// provider (see joinLinkedIdps). A link to a deactivated provider — or one the backend omits
5+
// from the active list — arrives with `type` undefined, so the row falls back to the generic
6+
// mail glyph even when the username is obviously a GitHub handle. This recovers the icon from
7+
// the only signal left: the shape of the username.
8+
//
9+
// Scope: v1 supports Google + GitHub (see idp-slug TYPE_TO_SLUG). Google logins are email
10+
// addresses; GitHub logins are bare handles (alphanumeric + non-leading/trailing hyphens, no
11+
// '@' or '.'). So only GitHub is inferable here — anything email-shaped returns undefined and
12+
// keeps the caller's generic fallback. Always pair as `link.type ?? inferIdpType(...)` so a real
13+
// type wins.
14+
15+
// GitHub username rules: 1–39 chars, alphanumeric or single internal hyphens, no leading/
16+
// trailing or consecutive hyphens. This intentionally rejects emails (the '.'/'@' fail it).
17+
const GITHUB_HANDLE = /^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$/i;
18+
19+
export function inferIdpType(username?: string | null): string | undefined {
20+
if (!username) return undefined;
21+
return GITHUB_HANDLE.test(username) ? 'GITHUB' : undefined;
22+
}

app/modules/i18n/locales/en.po

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,15 @@ msgstr "A new code has been sent to your email."
3636
msgid "Activate your device"
3737
msgstr "Activate your device"
3838

39-
#: app/routes/accounts.tsx:116
39+
#: app/routes/accounts.tsx:117
4040
msgid "Add an account"
4141
msgstr "Add an account"
4242

4343
#: app/routes/setup/mfa.tsx:160
4444
msgid "Add an extra layer of security to your account by setting up a second factor."
4545
msgstr "Add an extra layer of security to your account by setting up a second factor."
4646

47-
#: app/routes/accounts.tsx:198
47+
#: app/routes/accounts.tsx:201
4848
msgid "Add another account"
4949
msgstr "Add another account"
5050

@@ -112,7 +112,7 @@ msgstr "Authorize device"
112112
msgid "Authorizing as"
113113
msgstr "Authorizing as"
114114

115-
#: app/routes/sso/index.tsx:141
115+
#: app/routes/sso/index.tsx:145
116116
msgid "Available accounts to link"
117117
msgstr "Available accounts to link"
118118

@@ -148,7 +148,7 @@ msgstr "Check your email"
148148
msgid "Choose a new password"
149149
msgstr "Choose a new password"
150150

151-
#: app/routes/accounts.tsx:90
151+
#: app/routes/accounts.tsx:91
152152
msgid "Choose an account"
153153
msgstr "Choose an account"
154154

@@ -177,7 +177,7 @@ msgstr "Confirm new password"
177177
msgid "Confirm password"
178178
msgstr "Confirm password"
179179

180-
#: app/routes/sso/index.tsx:88
180+
#: app/routes/sso/index.tsx:89
181181
msgid "Connected accounts"
182182
msgstr "Connected accounts"
183183

@@ -339,15 +339,15 @@ msgstr "Link expired"
339339
msgid "Link your account"
340340
msgstr "Link your account"
341341

342-
#: app/routes/sso/index.tsx:72
342+
#: app/routes/sso/index.tsx:73
343343
msgid "Linked accounts"
344344
msgstr "Linked accounts"
345345

346346
#: app/routes/setup/authenticator.tsx:131
347347
msgid "Manual setup key"
348348
msgstr "Manual setup key"
349349

350-
#: app/routes/accounts.tsx:161
350+
#: app/routes/accounts.tsx:164
351351
msgid "Needs re-authentication"
352352
msgstr "Needs re-authentication"
353353

@@ -364,7 +364,7 @@ msgstr "No account was found and sign-up is not available."
364364
msgid "No sign-in method is available for this account."
365365
msgstr "No sign-in method is available for this account."
366366

367-
#: app/routes/accounts.tsx:108
367+
#: app/routes/accounts.tsx:109
368368
msgid "No signed-in accounts."
369369
msgstr "No signed-in accounts."
370370

@@ -476,7 +476,7 @@ msgstr "Scan the QR code below with your authenticator app, then enter the 6-dig
476476
msgid "Security key"
477477
msgstr "Security key"
478478

479-
#: app/routes/accounts.tsx:91
479+
#: app/routes/accounts.tsx:92
480480
msgid "Select an account to continue or add a new one."
481481
msgstr "Select an account to continue or add a new one."
482482

@@ -492,7 +492,7 @@ msgstr "Send reset link"
492492
msgid "Service temporarily unavailable. Please try again."
493493
msgstr "Service temporarily unavailable. Please try again."
494494

495-
#: app/routes/accounts.tsx:159
495+
#: app/routes/accounts.tsx:162
496496
msgid "Session active"
497497
msgstr "Session active"
498498

@@ -557,7 +557,7 @@ msgstr "Sign in with LDAP"
557557
#: app/routes/logout/index.tsx:47
558558
#: app/routes/logout/index.tsx:53
559559
#: app/routes/signed-in.tsx:97
560-
#: app/routes/sso/index.tsx:179
560+
#: app/routes/sso/index.tsx:183
561561
msgid "Sign out"
562562
msgstr "Sign out"
563563

@@ -663,7 +663,7 @@ msgstr "Too many attempts. Please wait a moment and try again."
663663
msgid "Two-factor verification"
664664
msgstr "Two-factor verification"
665665

666-
#: app/routes/sso/index.tsx:123
666+
#: app/routes/sso/index.tsx:127
667667
msgid "Unlink"
668668
msgstr "Unlink"
669669

@@ -771,7 +771,7 @@ msgid "You are signed in as <0>{loginName}</0>"
771771
msgstr "You are signed in as <0>{loginName}</0>"
772772

773773
#. placeholder {0}: loginName && ( <> Logged in as <span className="font-medium">{loginName}</span>. </> )
774-
#: app/routes/sso/index.tsx:74
774+
#: app/routes/sso/index.tsx:75
775775
msgid "You can link multiple accounts to your Datum account. {0}"
776776
msgstr "You can link multiple accounts to your Datum account. {0}"
777777

@@ -795,7 +795,7 @@ msgstr "You may return to your device."
795795
msgid "You must be signed in to link an external account."
796796
msgstr "You must be signed in to link an external account."
797797

798-
#: app/routes/accounts.tsx:98
798+
#: app/routes/accounts.tsx:99
799799
msgid "You signed in as a different account than the one you were re-authenticating. Both are kept — choose an account to continue."
800800
msgstr "You signed in as a different account than the one you were re-authenticating. Both are kept — choose an account to continue."
801801

app/routes/accounts.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { AuthFormFields } from '@/components/auth-form/auth-form-fields';
33
import { FormError } from '@/components/form-error/form-error';
44
import { IdpIcon } from '@/components/idp-icon/idp-icon';
55
import { useAuthActionError } from '@/hooks/use-auth-action-error';
6+
import { inferIdpType } from '@/modules/auth/idp-detect';
67
import { isAllowedRequestId } from '@/resources/authorize';
78
import {
89
listAccounts,
@@ -138,8 +139,10 @@ export default function AccountPicker() {
138139
<button
139140
type="submit"
140141
className="hover:bg-muted/50 focus-visible:ring-ring flex w-full items-center gap-2 rounded-l-lg p-3 text-left transition-colors focus-visible:ring-2 focus-visible:outline-none">
142+
{/* idpType is absent for deactivated/unknown providers; recover GITHUB from a
143+
handle-shaped loginName so it doesn't fall back to the mail glyph. */}
141144
<IdpIcon
142-
type={account.idpType}
145+
type={account.idpType ?? inferIdpType(account.loginName)}
143146
name={account.displayName ?? account.loginName}
144147
/>
145148
<span className="flex min-w-0 flex-col">

app/routes/sso/index.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { AuthCard } from '@/components/auth-card/auth-card';
22
import { AuthFormFields } from '@/components/auth-form/auth-form-fields';
33
import { IdpIcon } from '@/components/idp-icon/idp-icon';
4+
import { inferIdpType } from '@/modules/auth/idp-detect';
45
import { slugify } from '@/modules/auth/idp-slug';
56
import type { IdProvider } from '@/modules/auth/types';
67
import {
@@ -96,8 +97,11 @@ export default function SsoPage() {
9697
Falls back to the bare IdP user name / id when the provider is no longer
9798
active. Icon is non-interactive — no nested-interactive a11y violation. */}
9899
<span className="flex min-w-0 items-center gap-3">
100+
{/* `type` is only joined in when the IdP is still active; for a deactivated
101+
provider it's undefined and the bare GitHub handle would fall back to the
102+
mail glyph. inferIdpType recovers GITHUB from the username shape. */}
99103
<IdpIcon
100-
type={link.type}
104+
type={link.type ?? inferIdpType(link.idpUserName)}
101105
name={link.name || link.idpUserName || link.idpId}
102106
logoUrl={link.logoUrl}
103107
/>

0 commit comments

Comments
 (0)