mirror of
https://github.com/supabase/supabase.git
synced 2026-10-09 03:15:06 +03:00
* fix(studio): early fail query when x-connection-encrypted is invalid * fix(studio): uniformize readDatabase and projectDetails connString handling * chore: update api types * chore: add connectionString null option * fix: only enforce x-connection-encrypted on platform * chore: refactor connString check in a single point * chore: fix guard logic * chore: fix pgMetaGuard * chore: fix types
129 lines
4.3 KiB
TypeScript
129 lines
4.3 KiB
TypeScript
import { useInfiniteQuery, UseInfiniteQueryOptions } from '@tanstack/react-query'
|
|
|
|
import type { components } from 'data/api'
|
|
import { executeSql, ExecuteSqlError } from 'data/sql/execute-sql-query'
|
|
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
|
|
import { PROJECT_STATUS } from 'lib/constants'
|
|
import { authKeys } from './keys'
|
|
|
|
export type Filter = 'verified' | 'unverified' | 'anonymous'
|
|
|
|
export type UsersVariables = {
|
|
projectRef?: string
|
|
connectionString?: string | null
|
|
page?: number
|
|
keywords?: string
|
|
filter?: Filter
|
|
providers?: string[]
|
|
sort?: 'created_at' | 'email' | 'phone' | 'last_sign_in_at'
|
|
order?: 'asc' | 'desc'
|
|
}
|
|
|
|
export const USERS_PAGE_LIMIT = 50
|
|
export type User = components['schemas']['UserBody'] & {
|
|
providers: readonly string[]
|
|
}
|
|
|
|
export const getUsersSQL = ({
|
|
page = 0,
|
|
verified,
|
|
keywords,
|
|
providers,
|
|
sort,
|
|
order,
|
|
}: {
|
|
page: number
|
|
verified?: Filter
|
|
keywords?: string
|
|
providers?: string[]
|
|
sort: string
|
|
order: 'asc' | 'desc'
|
|
}) => {
|
|
const offset = page * USERS_PAGE_LIMIT
|
|
const hasValidKeywords = keywords && keywords !== ''
|
|
|
|
const conditions: string[] = []
|
|
const baseQueryUsers = `
|
|
select *, coalesce((select array_agg(distinct i.provider) from auth.identities i where i.user_id = auth.users.id), '{}'::text[]) as providers from auth.users
|
|
`.trim()
|
|
|
|
if (hasValidKeywords) {
|
|
// [Joshen] Escape single quotes properly
|
|
const formattedKeywords = keywords.replaceAll("'", "''")
|
|
conditions.push(
|
|
`id::text like '%${formattedKeywords}%' or email like '%${formattedKeywords}%' or phone like '%${formattedKeywords}%'`
|
|
)
|
|
}
|
|
|
|
if (verified === 'verified') {
|
|
conditions.push(`email_confirmed_at IS NOT NULL or phone_confirmed_at IS NOT NULL`)
|
|
} else if (verified === 'anonymous') {
|
|
conditions.push(`is_anonymous is true`)
|
|
} else if (verified === 'unverified') {
|
|
conditions.push(`email_confirmed_at IS NULL AND phone_confirmed_at IS NULL`)
|
|
}
|
|
|
|
if (providers && providers.length > 0) {
|
|
// [Joshen] This is arguarbly not fully optimized, but at the same time not commonly used
|
|
// JFYI in case we do eventually run into performance issues here when filtering for SAML provider
|
|
if (providers.includes('saml 2.0')) {
|
|
conditions.push(
|
|
`(select jsonb_agg(case when value ~ '^sso' then 'sso' else value end) from jsonb_array_elements_text((raw_app_meta_data ->> 'providers')::jsonb)) ?| array[${providers.map((p) => (p === 'saml 2.0' ? `'sso'` : `'${p}'`)).join(', ')}]`.trim()
|
|
)
|
|
} else {
|
|
conditions.push(
|
|
`(raw_app_meta_data->>'providers')::jsonb ?| array[${providers.map((p) => `'${p}'`).join(', ')}]`
|
|
)
|
|
}
|
|
}
|
|
|
|
const combinedConditions = conditions.map((x) => `(${x})`).join(' and ')
|
|
const sortOn = sort ?? 'created_at'
|
|
const sortOrder = order ?? 'desc'
|
|
|
|
return `${baseQueryUsers}${conditions.length > 0 ? ` where ${combinedConditions}` : ''} order by "${sortOn}" ${sortOrder} nulls last limit ${USERS_PAGE_LIMIT} offset ${offset};`
|
|
}
|
|
|
|
export type UsersData = { result: User[] }
|
|
export type UsersError = ExecuteSqlError
|
|
|
|
export const useUsersInfiniteQuery = <TData = UsersData>(
|
|
{ projectRef, connectionString, keywords, filter, providers, sort, order }: UsersVariables,
|
|
{ enabled = true, ...options }: UseInfiniteQueryOptions<UsersData, UsersError, TData> = {}
|
|
) => {
|
|
const project = useSelectedProject()
|
|
const isActive = project?.status === PROJECT_STATUS.ACTIVE_HEALTHY
|
|
|
|
return useInfiniteQuery<UsersData, UsersError, TData>(
|
|
authKeys.usersInfinite(projectRef, { keywords, filter, providers, sort, order }),
|
|
({ signal, pageParam }) => {
|
|
return executeSql(
|
|
{
|
|
projectRef,
|
|
connectionString,
|
|
sql: getUsersSQL({
|
|
page: pageParam,
|
|
verified: filter,
|
|
keywords,
|
|
providers,
|
|
sort: sort ?? 'created_at',
|
|
order: order ?? 'desc',
|
|
}),
|
|
queryKey: authKeys.usersInfinite(projectRef),
|
|
},
|
|
signal
|
|
)
|
|
},
|
|
{
|
|
enabled: enabled && typeof projectRef !== 'undefined' && isActive,
|
|
getNextPageParam(lastPage, pages) {
|
|
const page = pages.length
|
|
const hasNextPage = lastPage.result.length >= USERS_PAGE_LIMIT
|
|
if (!hasNextPage) return undefined
|
|
return page
|
|
},
|
|
...options,
|
|
}
|
|
)
|
|
}
|