mirror of
https://github.com/supabase/supabase.git
synced 2026-10-18 23:59:08 +03:00
Start
This commit is contained in:
1 parent
0878275c7d
commit
182f7fd299
5 files changed
+33
-257
No files matched your search
@@ -28,6 +28,7 @@ interface LinterDataGridProps {
|
||||
selectedLint: Lint | null
|
||||
setSelectedLint: (value: Lint | null) => void
|
||||
currentTab: LINTER_LEVELS
|
||||
handleLintClick: (lint: Lint | null) => void
|
||||
}
|
||||
|
||||
const LinterDataGrid = ({
|
||||
@@ -36,6 +37,7 @@ const LinterDataGrid = ({
|
||||
selectedLint,
|
||||
setSelectedLint,
|
||||
currentTab,
|
||||
handleLintClick,
|
||||
}: LinterDataGridProps) => {
|
||||
const gridRef = useRef<DataGridHandle>(null)
|
||||
const { ref } = useParams()
|
||||
@@ -147,7 +149,8 @@ const LinterDataGrid = ({
|
||||
{...props}
|
||||
onClick={() => {
|
||||
if (typeof idx === 'number' && idx >= 0) {
|
||||
setSelectedLint(props.row)
|
||||
// setSelectedLint(props.row)
|
||||
handleLintClick(props.row)
|
||||
gridRef.current?.scrollToCell({ idx: 0, rowIdx: idx })
|
||||
}
|
||||
}}
|
||||
@@ -173,7 +176,7 @@ const LinterDataGrid = ({
|
||||
className="absolute top-3 right-3 px-1"
|
||||
icon={<X size={14} />}
|
||||
onClick={() => {
|
||||
setSelectedLint(null)
|
||||
handleLintClick(null)
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
TableEditor,
|
||||
} from 'icons'
|
||||
import { ICON_SIZE, ICON_STROKE_WIDTH } from './NavigationBar'
|
||||
import { Settings, FileText, List } from 'lucide-react'
|
||||
import { Settings, FileText, List, Lightbulb } from 'lucide-react'
|
||||
|
||||
export const generateToolRoutes = (ref?: string, project?: Project): Route[] => {
|
||||
const isProjectBuilding = project?.status === PROJECT_STATUS.COMING_UP
|
||||
@@ -103,6 +103,16 @@ export const generateOtherRoutes = (ref?: string, project?: Project): Route[] =>
|
||||
const buildingUrl = `/project/${ref}/building`
|
||||
|
||||
return [
|
||||
...(IS_PLATFORM
|
||||
? [
|
||||
{
|
||||
key: 'advisors',
|
||||
label: 'Advisors',
|
||||
icon: <Lightbulb size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
|
||||
link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/advisors/security`),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(IS_PLATFORM
|
||||
? [
|
||||
{
|
||||
|
||||
@@ -39,6 +39,7 @@ index_ as (
|
||||
indisvalid
|
||||
)
|
||||
select
|
||||
md5(fk.schema_name || fk.table_name || fk.fkey_name) AS id,
|
||||
'unindexed_foreign_keys' as name,
|
||||
'Unindexed foreign keys' as title,
|
||||
'INFO' as level,
|
||||
@@ -53,6 +54,7 @@ select
|
||||
) as detail,
|
||||
'https://supabase.com/docs/guides/database/database-linter?lint=0001_unindexed_foreign_keys' as remediation,
|
||||
jsonb_build_object(
|
||||
'id', md5(fk.schema_name || fk.table_name || fk.fkey_name),
|
||||
'schema', fk.schema_name,
|
||||
'name', fk.table_name,
|
||||
'type', 'table',
|
||||
@@ -81,6 +83,7 @@ order by
|
||||
union all
|
||||
(
|
||||
select
|
||||
md5(n.nspname || c.relname) AS id,
|
||||
'auth_users_exposed' as name,
|
||||
'Exposed Auth Users' as title,
|
||||
'ERROR' as level,
|
||||
@@ -198,6 +201,7 @@ with policies as (
|
||||
and pa.polname = pb.policyname
|
||||
)
|
||||
select
|
||||
md5(schema_name || table_name || policy_name) AS id,
|
||||
'auth_rls_initplan' as name,
|
||||
'Auth RLS Initialization Plan' as title,
|
||||
'WARN' as level,
|
||||
@@ -262,6 +266,7 @@ where
|
||||
union all
|
||||
(
|
||||
select
|
||||
md5(pgns.nspname || pgc.relname) AS id,
|
||||
'no_primary_key' as name,
|
||||
'No Primary Key' as title,
|
||||
'INFO' as level,
|
||||
@@ -308,6 +313,7 @@ having
|
||||
union all
|
||||
(
|
||||
select
|
||||
md5(psui.schemaname || psui.relname || psui.indexrelname) AS id,
|
||||
'unused_index' as name,
|
||||
'Unused Index' as title,
|
||||
'INFO' as level,
|
||||
@@ -351,6 +357,7 @@ where
|
||||
union all
|
||||
(
|
||||
select
|
||||
md5(n.nspname || c.relname || r.rolname || act.cmd) AS id,
|
||||
'multiple_permissive_policies' as name,
|
||||
'Multiple Permissive Policies' as title,
|
||||
'WARN' as level,
|
||||
@@ -424,6 +431,7 @@ having
|
||||
union all
|
||||
(
|
||||
select
|
||||
md5(n.nspname || c.relname) AS id,
|
||||
'policy_exists_rls_disabled' as name,
|
||||
'Policy Exists RLS Disabled' as title,
|
||||
'ERROR' as level,
|
||||
@@ -470,6 +478,7 @@ group by
|
||||
union all
|
||||
(
|
||||
select
|
||||
md5(n.nspname || c.relname),
|
||||
'rls_enabled_no_policy' as name,
|
||||
'RLS Enabled No Policy' as title,
|
||||
'INFO' as level,
|
||||
@@ -516,6 +525,7 @@ group by
|
||||
union all
|
||||
(
|
||||
select
|
||||
md5(n.nspname || c.relname || replace(pi.indexdef, pi.indexname, '')) AS id,
|
||||
'duplicate_index' as name,
|
||||
'Duplicate Index' as title,
|
||||
'WARN' as level,
|
||||
@@ -571,6 +581,7 @@ having
|
||||
union all
|
||||
(
|
||||
select
|
||||
md5(n.nspname || c.relname) AS id,
|
||||
'security_definer_view' as name,
|
||||
'Security Definer View' as title,
|
||||
'ERROR' as level,
|
||||
@@ -623,6 +634,7 @@ where
|
||||
union all
|
||||
(
|
||||
select
|
||||
md5(n.nspname || p.proname || md5(p.prosrc)) AS id,
|
||||
'function_search_path_mutable' as name,
|
||||
'Function Search Path Mutable' as title,
|
||||
'WARN' as level,
|
||||
@@ -663,6 +675,7 @@ where
|
||||
union all
|
||||
(
|
||||
select
|
||||
md5(n.nspname || c.relname) AS id,
|
||||
'rls_disabled_in_public' as name,
|
||||
'RLS Disabled in Public' as title,
|
||||
'ERROR' as level,
|
||||
@@ -704,6 +717,7 @@ where
|
||||
union all
|
||||
(
|
||||
select
|
||||
md5('public' || pe.extname) AS id,
|
||||
'extension_in_public' as name,
|
||||
'Extension in Public' as title,
|
||||
'WARN' as level,
|
||||
@@ -756,6 +770,7 @@ with policies as (
|
||||
and pa.polname = pb.policyname
|
||||
)
|
||||
select
|
||||
md5(schema_name || table_name || policy_name) AS id,
|
||||
'rls_references_user_metadata' as name,
|
||||
'RLS references user metadata' as title,
|
||||
'ERROR' as level,
|
||||
@@ -793,6 +808,7 @@ where
|
||||
union all
|
||||
(
|
||||
select
|
||||
md5(n.nspname || c.relname) AS id,
|
||||
'materialized_view_in_api' as name,
|
||||
'Materialized View in API' as title,
|
||||
'WARN' as level,
|
||||
@@ -857,6 +873,7 @@ export const LINT_TYPES = [
|
||||
export type LINT_TYPES = (typeof LINT_TYPES)[number]
|
||||
|
||||
export type Lint = {
|
||||
id: string
|
||||
name: LINT_TYPES
|
||||
level: 'ERROR' | 'WARN' | 'INFO'
|
||||
facing: string
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import { useParams } from 'common'
|
||||
import { lintInfoMap } from 'components/interfaces/Linter/Linter.utils'
|
||||
import { DatabaseLayout } from 'components/layouts'
|
||||
import { FormHeader } from 'components/ui/Forms'
|
||||
import { Lint, useProjectLintsQuery } from 'data/lint/lint-query'
|
||||
import { useSelectedProject } from 'hooks'
|
||||
import { useState } from 'react'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
import { LoadingLine } from 'ui'
|
||||
|
||||
import LintPageTabs from 'components/interfaces/Linter/LintPageTabs'
|
||||
import { LINTER_LEVELS } from 'components/interfaces/Linter/Linter.constants'
|
||||
import LinterDataGrid from 'components/interfaces/Linter/LinterDataGrid'
|
||||
import LinterFilters from 'components/interfaces/Linter/LinterFilters'
|
||||
import LinterPageFooter from 'components/interfaces/Linter/LinterPageFooter'
|
||||
|
||||
const ProjectLints: NextPageWithLayout = () => {
|
||||
const project = useSelectedProject()
|
||||
const { preset } = useParams()
|
||||
|
||||
// need to maintain a list of filters for each tab
|
||||
const [filters, setFilters] = useState<{ level: LINTER_LEVELS; filters: string[] }[]>([
|
||||
{ level: LINTER_LEVELS.ERROR, filters: [] },
|
||||
{ level: LINTER_LEVELS.WARN, filters: [] },
|
||||
{ level: LINTER_LEVELS.INFO, filters: [] },
|
||||
])
|
||||
|
||||
const [currentTab, setCurrentTab] = useState<LINTER_LEVELS>(
|
||||
(preset as LINTER_LEVELS) ?? LINTER_LEVELS.ERROR
|
||||
)
|
||||
const [selectedLint, setSelectedLint] = useState<Lint | null>(null)
|
||||
|
||||
const { data, isLoading, isRefetching, refetch } = useProjectLintsQuery({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
})
|
||||
|
||||
const activeLints = data?.filter((x) => x.categories.includes('PERFORMANCE')) || []
|
||||
|
||||
const currentTabFilters = (filters.find((filter) => filter.level === currentTab)?.filters ||
|
||||
[]) as string[]
|
||||
|
||||
const filteredLints = activeLints
|
||||
.filter((x) => x.level === currentTab)
|
||||
.filter((x) => (currentTabFilters.length > 0 ? currentTabFilters.includes(x.name) : x))
|
||||
|
||||
const filterOptions = lintInfoMap
|
||||
// only show filters for lint types which are present in the results and not ignored
|
||||
.filter((item) =>
|
||||
activeLints.some((lint) => lint.name === item.name && lint.level === currentTab)
|
||||
)
|
||||
.map((type) => ({
|
||||
name: type.title,
|
||||
value: type.name,
|
||||
}))
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<FormHeader
|
||||
className="py-4 px-6 !mb-0"
|
||||
title="Performance Advisor"
|
||||
docsUrl="https://supabase.com/docs/guides/database/database-linter"
|
||||
/>
|
||||
<LintPageTabs
|
||||
activeLints={activeLints}
|
||||
isLoading={isLoading}
|
||||
currentTab={currentTab}
|
||||
setCurrentTab={setCurrentTab}
|
||||
setSelectedLint={setSelectedLint}
|
||||
/>
|
||||
<LinterFilters
|
||||
filterOptions={filterOptions}
|
||||
activeLints={activeLints}
|
||||
currentTab={currentTab}
|
||||
filters={filters}
|
||||
setFilters={setFilters}
|
||||
/>
|
||||
<LoadingLine loading={isRefetching} />
|
||||
<LinterDataGrid
|
||||
filteredLints={filteredLints}
|
||||
currentTab={currentTab}
|
||||
selectedLint={selectedLint}
|
||||
setSelectedLint={setSelectedLint}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<LinterPageFooter isLoading={isLoading} isRefetching={isRefetching} refetch={refetch} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
ProjectLints.getLayout = (page) => <DatabaseLayout title="Linter">{page}</DatabaseLayout>
|
||||
|
||||
export default ProjectLints
|
||||
@@ -1,161 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import LintPageTabs from 'components/interfaces/Linter/LintPageTabs'
|
||||
import { LINTER_LEVELS } from 'components/interfaces/Linter/Linter.constants'
|
||||
import { lintInfoMap } from 'components/interfaces/Linter/Linter.utils'
|
||||
import LinterDataGrid from 'components/interfaces/Linter/LinterDataGrid'
|
||||
import LinterFilters from 'components/interfaces/Linter/LinterFilters'
|
||||
import LinterPageFooter from 'components/interfaces/Linter/LinterPageFooter'
|
||||
import { DatabaseLayout } from 'components/layouts'
|
||||
import { FormHeader } from 'components/ui/Forms'
|
||||
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
|
||||
import { Lint, useProjectLintsQuery } from 'data/lint/lint-query'
|
||||
import { useSelectedProject } from 'hooks'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
import { LoadingLine } from 'ui'
|
||||
|
||||
const ProjectLints: NextPageWithLayout = () => {
|
||||
const project = useSelectedProject()
|
||||
const { preset } = useParams()
|
||||
|
||||
// need to maintain a list of filters for each tab
|
||||
const [filters, setFilters] = useState<{ level: LINTER_LEVELS; filters: string[] }[]>([
|
||||
{ level: LINTER_LEVELS.ERROR, filters: [] },
|
||||
{ level: LINTER_LEVELS.WARN, filters: [] },
|
||||
{ level: LINTER_LEVELS.INFO, filters: [] },
|
||||
])
|
||||
|
||||
const [currentTab, setCurrentTab] = useState<LINTER_LEVELS>(
|
||||
(preset as LINTER_LEVELS) ?? LINTER_LEVELS.ERROR
|
||||
)
|
||||
const [selectedLint, setSelectedLint] = useState<Lint | null>(null)
|
||||
|
||||
const {
|
||||
data,
|
||||
isLoading: areLintsLoading,
|
||||
isRefetching,
|
||||
refetch: refetchLintsQuery,
|
||||
} = useProjectLintsQuery({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
})
|
||||
|
||||
const {
|
||||
data: authConfig,
|
||||
isLoading: isAuthConfigLoading,
|
||||
refetch: refetchAuthConfigQuery,
|
||||
} = useAuthConfigQuery({
|
||||
projectRef: project?.ref,
|
||||
})
|
||||
|
||||
const isLoading = areLintsLoading || isAuthConfigLoading
|
||||
|
||||
const refetch = () => {
|
||||
refetchLintsQuery()
|
||||
refetchAuthConfigQuery()
|
||||
}
|
||||
|
||||
let clientLints: Lint[] = []
|
||||
|
||||
// [Alaister]: checking this client side for speed, but should be moved into the query if possible
|
||||
if (authConfig?.EXTERNAL_EMAIL_ENABLED) {
|
||||
if (authConfig.MAILER_OTP_EXP > 3600) {
|
||||
clientLints.push({
|
||||
name: 'auth_otp_long_expiry',
|
||||
level: 'WARN',
|
||||
facing: 'EXTERNAL',
|
||||
categories: ['SECURITY'],
|
||||
description: 'OTP expiry exceeds recommended threshold',
|
||||
detail:
|
||||
'We have detected that you have enabled the email provider with the OTP expiry set to more than an hour. It is recommended to set this value to less than an hour.',
|
||||
cache_key: 'auth_otp_long_expiry',
|
||||
remediation: 'https://supabase.com/docs/guides/platform/going-into-prod#security',
|
||||
metadata: {
|
||||
type: 'auth',
|
||||
entity: 'Auth',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (authConfig.EXTERNAL_PHONE_ENABLED && authConfig.SMS_OTP_LENGTH < 6) {
|
||||
clientLints.push({
|
||||
name: 'auth_otp_short_length',
|
||||
level: 'WARN',
|
||||
facing: 'EXTERNAL',
|
||||
categories: ['SECURITY'],
|
||||
description: 'OTP length is less than recommended threshold',
|
||||
detail: 'We have detected that you have set the OTP length to less than 6 characters',
|
||||
cache_key: 'auth_otp_short_length',
|
||||
remediation: 'https://supabase.com/docs/guides/platform/going-into-prod#security',
|
||||
metadata: {
|
||||
type: 'auth',
|
||||
entity: 'Auth',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const activeLints = [...(data ?? []), ...clientLints]?.filter((x) =>
|
||||
x.categories.includes('SECURITY')
|
||||
)
|
||||
|
||||
const currentTabFilters = (filters.find((filter) => filter.level === currentTab)?.filters ||
|
||||
[]) as string[]
|
||||
|
||||
const filteredLints = activeLints
|
||||
.filter((x) => x.level === currentTab)
|
||||
.filter((x) => (currentTabFilters.length > 0 ? currentTabFilters.includes(x.name) : x))
|
||||
|
||||
const filterOptions = lintInfoMap
|
||||
// only show filters for lint types which are present in the results and not ignored
|
||||
.filter((item) =>
|
||||
activeLints.some((lint) => lint.name === item.name && lint.level === currentTab)
|
||||
)
|
||||
.map((type) => ({
|
||||
name: type.title,
|
||||
value: type.name,
|
||||
}))
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<FormHeader
|
||||
className="py-4 px-6 !mb-0"
|
||||
title="Security Advisor"
|
||||
docsUrl="https://supabase.com/docs/guides/database/database-linter"
|
||||
/>
|
||||
<LintPageTabs
|
||||
activeLints={activeLints}
|
||||
isLoading={isLoading}
|
||||
currentTab={currentTab}
|
||||
setCurrentTab={setCurrentTab}
|
||||
setSelectedLint={setSelectedLint}
|
||||
/>
|
||||
<LinterFilters
|
||||
filterOptions={filterOptions}
|
||||
activeLints={activeLints}
|
||||
currentTab={currentTab}
|
||||
filters={filters}
|
||||
setFilters={setFilters}
|
||||
/>
|
||||
<LoadingLine loading={isRefetching} />
|
||||
<LinterDataGrid
|
||||
filteredLints={filteredLints}
|
||||
currentTab={currentTab}
|
||||
selectedLint={selectedLint}
|
||||
setSelectedLint={setSelectedLint}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<LinterPageFooter
|
||||
hideDbInspectCTA
|
||||
isLoading={isLoading}
|
||||
isRefetching={isRefetching}
|
||||
refetch={refetch}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
ProjectLints.getLayout = (page) => <DatabaseLayout title="Linter">{page}</DatabaseLayout>
|
||||
|
||||
export default ProjectLints
|
||||
Reference in new issue
Block a user