mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
fix(studio): show custom OAuth providers in Users table (#45658)
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? show provider info in Authentication -> Users page if user is auth'd with custom providers <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Added comprehensive support for custom authentication providers with appropriate type labeling and visual identification * Improved provider display formatting and icon rendering to properly recognize custom authentication options * Enhanced authentication provider interface operations for better custom provider information handling and management <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
1 parent
3ed7c8f522
commit
e90c91e498
1 file changed
+89
-55
@@ -22,6 +22,10 @@ import { BASE_PATH } from '@/lib/constants'
|
||||
const GITHUB_AVATAR_URL = 'https://avatars.githubusercontent.com'
|
||||
const SUPPORTED_CSP_AVATAR_URLS = [GITHUB_AVATAR_URL, 'https://lh3.googleusercontent.com']
|
||||
|
||||
const isCustomProvider = (provider: string) => provider.startsWith('custom:')
|
||||
|
||||
type FormattedUserRow = ReturnType<typeof formatUsersData>[number]
|
||||
|
||||
export const formatUsersData = (users: User[]) => {
|
||||
return users.map((user) => {
|
||||
const provider: string = (user.raw_app_meta_data?.provider as string) ?? ''
|
||||
@@ -39,25 +43,31 @@ export const formatUsersData = (users: User[]) => {
|
||||
providers: user.is_anonymous ? '-' : providers,
|
||||
provider_icons: providers
|
||||
.map((p) => {
|
||||
return p === 'email'
|
||||
? `${BASE_PATH}/img/icons/email-icon2.svg`
|
||||
: p === 'SAML'
|
||||
? `${BASE_PATH}/img/icons/saml-icon.svg`
|
||||
: providerIconMap[p]
|
||||
? `${BASE_PATH}/img/icons/${providerIconMap[p]}.svg`
|
||||
: undefined
|
||||
const isCustom = isCustomProvider(p)
|
||||
|
||||
return isCustom
|
||||
? 'custom'
|
||||
: p === 'email'
|
||||
? `${BASE_PATH}/img/icons/email-icon2.svg`
|
||||
: p === 'SAML'
|
||||
? `${BASE_PATH}/img/icons/saml-icon.svg`
|
||||
: providerIconMap[p]
|
||||
? `${BASE_PATH}/img/icons/${providerIconMap[p]}.svg`
|
||||
: undefined
|
||||
})
|
||||
.filter(Boolean),
|
||||
.filter((icon): icon is string => Boolean(icon)),
|
||||
// I think it's alright to just check via the main provider since email and phone should be mutually exclusive
|
||||
provider_type: user.is_anonymous
|
||||
? 'Anonymous'
|
||||
: provider === 'email'
|
||||
? '-'
|
||||
: socialProviders.includes(provider)
|
||||
? 'Social'
|
||||
: phoneProviders.includes(provider)
|
||||
? 'Phone'
|
||||
: '-',
|
||||
: isCustomProvider(provider)
|
||||
? 'Custom'
|
||||
: socialProviders.includes(provider)
|
||||
? 'Social'
|
||||
: phoneProviders.includes(provider)
|
||||
? 'Phone'
|
||||
: '-',
|
||||
// [Joshen] Note that the images might not load due to CSP issues
|
||||
img: getAvatarUrl(user),
|
||||
name: getDisplayName(user),
|
||||
@@ -168,7 +178,7 @@ export function getDisplayName(user: User, fallback = '-'): string {
|
||||
last_name: cc_last_name,
|
||||
firstName: ccFirstName,
|
||||
first_name: cc_first_name,
|
||||
} = (custom_claims ?? {}) as any
|
||||
} = (custom_claims ?? {}) as Record<string, unknown>
|
||||
|
||||
const last = toPrettyJsonString(
|
||||
familyName ||
|
||||
@@ -253,6 +263,17 @@ export function getAvatarUrl(user: User): string | undefined {
|
||||
}
|
||||
}
|
||||
|
||||
const formatUserProviders = (providers: string[]) => {
|
||||
return providers
|
||||
.map((x) => {
|
||||
const provider = PROVIDERS_SCHEMAS.find(
|
||||
(y) => ('key' in y && y.key === x) || y.title.toLowerCase() === x
|
||||
)
|
||||
return provider?.title ?? (isCustomProvider(x) ? x.split('custom:')[1] : x)
|
||||
})
|
||||
.join(', ')
|
||||
}
|
||||
|
||||
export const formatUserColumns = ({
|
||||
specificFilterColumn,
|
||||
columns,
|
||||
@@ -276,7 +297,7 @@ export const formatUserColumns = ({
|
||||
|
||||
let gridColumns = columns.map((col) => {
|
||||
const savedConfig = config.find((c) => c.id === col.id)
|
||||
const res: Column<any> = {
|
||||
const res: Column<FormattedUserRow> = {
|
||||
key: col.id,
|
||||
name: col.name,
|
||||
resizable: col.resizable ?? true,
|
||||
@@ -304,24 +325,22 @@ export const formatUserColumns = ({
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { isRowSelected, onRowSelectionChange } = useRowSelection()
|
||||
|
||||
const value = row?.[col.id]
|
||||
const value = row?.[col.id as keyof FormattedUserRow]
|
||||
const user = users?.find((u) => u.id === row.id)
|
||||
const formattedValue =
|
||||
value !== null && ['created_at', 'last_sign_in_at'].includes(col.id)
|
||||
? dayjs(value).format('ddd DD MMM YYYY HH:mm:ss [GMT]ZZ')
|
||||
: Array.isArray(value)
|
||||
? col.id === 'providers'
|
||||
? value
|
||||
.map((x) => {
|
||||
const meta = PROVIDERS_SCHEMAS.find(
|
||||
(y) => ('key' in y && y.key === x) || y.title.toLowerCase() === x
|
||||
)
|
||||
return meta?.title
|
||||
})
|
||||
.join(', ')
|
||||
: value.join(', ')
|
||||
: value
|
||||
|
||||
const isConfirmed = !!user?.confirmed_at
|
||||
const isDateBasedValue =
|
||||
value !== null &&
|
||||
['created_at', 'last_sign_in_at'].includes(col.id) &&
|
||||
typeof value === 'string'
|
||||
|
||||
const formattedValue = isDateBasedValue
|
||||
? dayjs(value).format('ddd DD MMM YYYY HH:mm:ss [GMT]ZZ')
|
||||
: Array.isArray(value)
|
||||
? col.id === 'providers'
|
||||
? formatUserProviders(value)
|
||||
: value.join(', ')
|
||||
: value
|
||||
|
||||
if (col.id === 'img') {
|
||||
return (
|
||||
@@ -363,26 +382,38 @@ export const formatUserColumns = ({
|
||||
{col.id === 'providers' &&
|
||||
row.provider_icons.map((icon: string, idx: number) => {
|
||||
const provider = row.providers[idx]
|
||||
return (
|
||||
<div
|
||||
key={`${user?.id}-${provider}-wrapper`}
|
||||
className="min-w-6 min-h-6 rounded-full border flex items-center justify-center bg-surface-75"
|
||||
style={{
|
||||
marginLeft: idx === 0 ? 0 : `-8px`,
|
||||
zIndex: row.provider_icons.length - idx,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
key={`${user?.id}-${provider}`}
|
||||
width={16}
|
||||
src={icon}
|
||||
alt={`${provider} auth icon`}
|
||||
className={cn(
|
||||
(provider === 'github' || provider === 'x') && 'dark:invert'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
const isCustom = isCustomProvider(provider)
|
||||
|
||||
if (isCustom) {
|
||||
return (
|
||||
<div
|
||||
key={`${user?.id}-${provider}-wrapper`}
|
||||
className="min-w-6 min-h-6 rounded-full border flex items-center justify-center bg-surface-75"
|
||||
>
|
||||
{provider.split('custom:')[1][0]}
|
||||
</div>
|
||||
)
|
||||
} else
|
||||
return (
|
||||
<div
|
||||
key={`${user?.id}-${provider}-wrapper`}
|
||||
className="min-w-6 min-h-6 rounded-full border flex items-center justify-center bg-surface-75"
|
||||
style={{
|
||||
marginLeft: idx === 0 ? 0 : `-8px`,
|
||||
zIndex: row.provider_icons.length - idx,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
key={`${user?.id}-${provider}`}
|
||||
width={16}
|
||||
src={icon}
|
||||
alt={`${provider} auth icon`}
|
||||
className={cn(
|
||||
(provider === 'github' || provider === 'x') && 'dark:invert'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{col.id === 'last_sign_in_at' && !isConfirmed ? (
|
||||
<p className="text-foreground-lighter">Waiting for verification</p>
|
||||
@@ -398,8 +429,11 @@ export const formatUserColumns = ({
|
||||
className="gap-x-2"
|
||||
onFocusCapture={(e) => e.stopPropagation()}
|
||||
onSelect={() => {
|
||||
const value = col.id === 'providers' ? row.providers.join(', ') : formattedValue
|
||||
copyToClipboard(value)
|
||||
const value =
|
||||
col.id === 'providers' && Array.isArray(row.providers)
|
||||
? row.providers.join(', ')
|
||||
: formattedValue
|
||||
if (value) copyToClipboard(value)
|
||||
}}
|
||||
>
|
||||
<Copy size={12} />
|
||||
@@ -455,7 +489,7 @@ export const formatUserColumns = ({
|
||||
if (columnOrder.length > 0) {
|
||||
gridColumns = gridColumns
|
||||
.filter((col) => columnOrder.includes(col.key))
|
||||
.sort((a: any, b: any) => {
|
||||
.sort((a, b) => {
|
||||
return columnOrder.indexOf(a.key) - columnOrder.indexOf(b.key)
|
||||
})
|
||||
}
|
||||
@@ -464,5 +498,5 @@ export const formatUserColumns = ({
|
||||
? gridColumns
|
||||
: ([profileImageColumn].concat(
|
||||
gridColumns.filter((col) => visibleColumns.includes(col.key))
|
||||
) as Column<any>[])
|
||||
) as Column<FormattedUserRow>[])
|
||||
}
|
||||
Reference in new issue
Block a user