useAdvisorSignals.ts 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. import { useCallback, useEffect, useMemo } from 'react'
  2. import type { AdvisorSignalItem } from './AdvisorPanel.types'
  3. import { useBannedIPsQuery } from '@/data/banned-ips/banned-ips-query'
  4. import type { IPData } from '@/data/banned-ips/banned-ips-query'
  5. import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
  6. const createDismissalStorageKey = (projectRef: string) => `advisor-signal-dismissals:${projectRef}`
  7. const createBannedIPDismissalKey = (ip: string) => `signal:banned-ip:${ip}:v1`
  8. const createBannedIPSignalItems = ({
  9. projectRef,
  10. bannedIPsData,
  11. }: {
  12. projectRef?: string
  13. bannedIPsData?: IPData
  14. }): AdvisorSignalItem[] => {
  15. if (!projectRef) return []
  16. const bannedIPs = bannedIPsData?.banned_ipv4_addresses ?? []
  17. return bannedIPs.map((ip) => ({
  18. id: createBannedIPDismissalKey(ip),
  19. dismissalKey: createBannedIPDismissalKey(ip),
  20. source: 'signal' as const,
  21. type: 'banned-ip' as const,
  22. severity: 'warning' as const,
  23. tab: 'security' as const,
  24. title: 'Banned IP address',
  25. summary: `The IP address \`${ip}\` is temporarily blocked because of suspicious traffic or repeated failed password attempts.`,
  26. description:
  27. 'This IP address is temporarily blocked because of suspicious traffic or repeated failed password attempts. If this block is expected, you can dismiss this signal or remove the ban.',
  28. docsUrl: 'https://supabase.com/docs/reference/cli/supabase-network-bans',
  29. actions: [
  30. {
  31. label: 'Edit network bans',
  32. href: `/project/${projectRef}/database/settings#banned-ips`,
  33. },
  34. ],
  35. sourceData: { type: 'banned-ip' as const, ip },
  36. }))
  37. }
  38. interface UseAdvisorSignalsOptions {
  39. projectRef?: string
  40. enabled?: boolean
  41. }
  42. export const useAdvisorSignals = ({ projectRef, enabled = true }: UseAdvisorSignalsOptions) => {
  43. const { data, isPending, isError } = useBannedIPsQuery({ projectRef }, { enabled })
  44. const storageKey = projectRef
  45. ? createDismissalStorageKey(projectRef)
  46. : 'advisor-signal-dismissals:unknown-project'
  47. const [dismissedKeys, setDismissedKeys] = useLocalStorageQuery<string[]>(storageKey, [])
  48. const dismissedKeySet = useMemo(() => new Set(dismissedKeys), [dismissedKeys])
  49. const dismissSignal = useCallback(
  50. (dismissalKey: string) => {
  51. setDismissedKeys((current) =>
  52. current.includes(dismissalKey) ? current : [...current, dismissalKey]
  53. )
  54. },
  55. [setDismissedKeys]
  56. )
  57. const signalItems = useMemo(
  58. () => createBannedIPSignalItems({ projectRef, bannedIPsData: data }),
  59. [projectRef, data]
  60. )
  61. // Prune stale dismissals when the active signal list changes (e.g. an IP was unbanned).
  62. // Only call the setter when pruning would actually change something — otherwise we
  63. // churn subscribers unnecessarily, which can cause feedback loops when this hook is
  64. // mounted in more than one place (AdvisorSection + AdvisorPanel).
  65. useEffect(() => {
  66. if (!data) return
  67. const hasStaleBannedIPDismissal = dismissedKeys.some(
  68. (key) =>
  69. key.startsWith('signal:banned-ip:') &&
  70. !signalItems.some((item) => item.dismissalKey === key)
  71. )
  72. if (!hasStaleBannedIPDismissal) return
  73. const activeKeys = new Set(signalItems.map((item) => item.dismissalKey))
  74. setDismissedKeys((current) =>
  75. current.filter((key) => (key.startsWith('signal:banned-ip:') ? activeKeys.has(key) : true))
  76. )
  77. }, [data, signalItems, dismissedKeys, setDismissedKeys])
  78. const formattedData = useMemo(
  79. () => signalItems.filter((item) => !dismissedKeySet.has(item.dismissalKey)),
  80. [signalItems, dismissedKeySet]
  81. )
  82. return {
  83. data: formattedData,
  84. dismissSignal,
  85. isPending,
  86. isError,
  87. }
  88. }