import { PermissionAction } from '@supabase/shared-types/out/constants' import { keepPreviousData } from '@tanstack/react-query' import { useDebounce } from '@uidotdev/usehooks' import { useParams } from 'common' import dayjs from 'dayjs' import { ArrowDown, ArrowUp, RefreshCw, User } from 'lucide-react' import Image from 'next/legacy/image' import { useEffect, useMemo, useState } from 'react' import { Alert, AlertDescription, AlertTitle, Button, WarningIcon } from 'ui' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { filterByProjects, filterByUsers, sortAuditLogs } from './AuditLogs.utils' import { LogDetailsPanel } from '@/components/interfaces/AuditLogs/LogDetailsPanel' import { LogsDatePicker } from '@/components/interfaces/Settings/Logs/Logs.DatePickers' import { ScaffoldContainer, ScaffoldSection } from '@/components/layouts/Scaffold' import Table from '@/components/to-be-cleaned/Table' import AlertError from '@/components/ui/AlertError' import { ButtonTooltip } from '@/components/ui/ButtonTooltip' import { FilterPopover } from '@/components/ui/FilterPopover' import NoPermission from '@/components/ui/NoPermission' import { UpgradeToPro } from '@/components/ui/UpgradeToPro' import { useOrganizationRolesV2Query } from '@/data/organization-members/organization-roles-query' import { AuditLog, TIMESTAMP_MICROS_PER_MS, useOrganizationAuditLogsQuery, } from '@/data/organizations/organization-audit-logs-query' import { useOrganizationMembersQuery } from '@/data/organizations/organization-members-query' import { useOrganizationsQuery } from '@/data/organizations/organizations-query' import { useOrgProjectsInfiniteQuery } from '@/data/projects/org-projects-infinite-query' import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' const logsUpgradeError = 'upgrade to Team or Enterprise Plan to access audit logs.' // [Joshen considerations] // - Maybe fix the height of the table to the remaining height of the viewport, so that the search input is always visible // - We'll need pagination as well if the audit logs get too large, but that needs to be implemented on the API side first if possible // - I've hidden time input in the date picker for now cause the time support in the component is a bit iffy, need to investigate // - Maybe a rule to follow from here is just everytime we call dayjs, use UTC(), one TZ to rule them all export const AuditLogs = () => { const { slug } = useParams() const currentTime = dayjs().utc().set('millisecond', 0) const [dateSortDesc, setDateSortDesc] = useState(true) const [dateRange, setDateRange] = useState({ from: currentTime.subtract(1, 'day').toISOString(), to: currentTime.toISOString(), }) const [selectedLog, setSelectedLog] = useState() const [filters, setFilters] = useState<{ users: string[]; projects: string[] }>({ users: [], // gotrue_id[] projects: [], // project_ref[] }) const [search, setSearch] = useState('') const debouncedSearch = useDebounce(search, 500) const { can: canReadAuditLogs, isLoading: isLoadingPermissions } = useAsyncCheckPermissions( PermissionAction.READ, 'notifications' ) const { hasAccess: hasAccessToAuditLogs, isLoading: isLoadingEntitlements } = useCheckEntitlements('security.audit_logs_days') const { data, error, isPending: isLoading, isSuccess, isError, isRefetching, fetchStatus, refetch, } = useOrganizationAuditLogsQuery( { slug, iso_timestamp_start: dateRange.from, iso_timestamp_end: dateRange.to, }, { enabled: canReadAuditLogs, retry: false, refetchOnWindowFocus: (query) => { return !query.state.error?.message.endsWith(logsUpgradeError) }, } ) const isLogsNotAvailableBasedOnPlan = isError && !hasAccessToAuditLogs const isRangeExceededError = isError && error.message.includes('range exceeded') const showFilters = !isLoading && !isLogsNotAvailableBasedOnPlan const { data: projectsData, isLoading: isLoadingProjects, isFetching, isFetchingNextPage, hasNextPage, fetchNextPage, } = useOrgProjectsInfiniteQuery( { slug, search: search.length === 0 ? search : debouncedSearch }, { placeholderData: keepPreviousData, enabled: showFilters } ) const { data: organizations } = useOrganizationsQuery({ enabled: showFilters, }) const { data: members } = useOrganizationMembersQuery({ slug }, { enabled: showFilters }) const { data: rolesData } = useOrganizationRolesV2Query({ slug }, { enabled: showFilters }) const activeMembers = (members ?? []).filter((x) => !x.invited_at) const roles = [...(rolesData?.org_scoped_roles ?? []), ...(rolesData?.project_scoped_roles ?? [])] const projects = useMemo(() => projectsData?.pages.flatMap((page) => page.projects), [projectsData?.pages]) || [] const logs = data?.result ?? [] const sortedLogs = filterByProjects( filterByUsers(sortAuditLogs(logs, dateSortDesc), filters.users), filters.projects ) const shouldShowLoadingState = (isLoading && fetchStatus !== 'idle') || isLoadingPermissions || isLoadingEntitlements // This feature depends on the subscription tier of the user. // The API limits the logs to maximum of 62 days and 5 minutes so when the page is // viewed for more than 5 minutes, the call parameters needs to be updated. This also works with // higher tiers.The user will see a loading shimmer. useEffect(() => { const duration = dayjs(dateRange.from).diff(dayjs(dateRange.to)) const interval = setInterval(() => { const currentTime = dayjs().utc().set('millisecond', 0) setDateRange({ from: currentTime.add(duration).toISOString(), to: currentTime.toISOString(), }) }, 5 * 60000) return () => clearInterval(interval) }, [dateRange.from, dateRange.to]) if (isLogsNotAvailableBasedOnPlan) { return ( ) } return ( <>
{showFilters && (

Filter by

setFilters({ ...filters, users: values })} /> setFilters({ ...filters, projects: values })} search={search} setSearch={setSearch} hasNextPage={hasNextPage} isLoading={isLoadingProjects} isFetching={isFetching} isFetchingNextPage={isFetchingNextPage} fetchNextPage={fetchNextPage} /> setDateRange(value)} helpers={[ { text: 'Last 1 hour', calcFrom: () => dayjs().subtract(1, 'hour').toISOString(), calcTo: () => dayjs().toISOString(), }, { text: 'Last 3 hours', calcFrom: () => dayjs().subtract(3, 'hour').toISOString(), calcTo: () => dayjs().toISOString(), }, { text: 'Last 6 hours', calcFrom: () => dayjs().subtract(6, 'hour').toISOString(), calcTo: () => dayjs().toISOString(), }, { text: 'Last 12 hours', calcFrom: () => dayjs().subtract(12, 'hour').toISOString(), calcTo: () => dayjs().toISOString(), }, { text: 'Last 24 hours', calcFrom: () => dayjs().subtract(1, 'day').toISOString(), calcTo: () => dayjs().toISOString(), }, ]} /> {isSuccess && ( <>

Viewing {sortedLogs.length} logs in total

)}
)} {shouldShowLoadingState ? (
) : !canReadAuditLogs ? ( ) : null} {isError && (isRangeExceededError ? ( Date range too large The selected date range exceeds the maximum allowed period. Please select a smaller time range. ) : ( ))} {isSuccess && ( <> {logs.length === 0 ? (

Your organization does not have any audit logs available yet

) : logs.length > 0 && sortedLogs.length === 0 ? (

No audit logs found based on the filters applied

) : ( User , Action , Target ,

Date

) : ( ) } onClick={() => setDateSortDesc(!dateSortDesc)} tooltip={{ content: { side: 'bottom', text: dateSortDesc ? 'Sort latest first' : 'Sort earliest first', }, }} />
, , ]} body={ sortedLogs?.map((log) => { const user = (members ?? []).find( (member) => member.gotrue_id === log.actor.user_id ) const role = roles.find((role) => user?.role_ids?.[0] === role.id) const project = projects?.find((p) => p.ref === log.project_ref) const organization = organizations?.find( (org) => org.slug === log.organization_slug ) const userIcon = user === undefined ? (

?

) : user?.invited_id || user?.username === user?.primary_email ? (
) : ( ) return ( setSelectedLog(log)} className="cursor-pointer hover:bg-alternative! transition duration-100" >
{userIcon}

{user?.username ?? log.actor.email ?? '-'}

{role && (

{role?.name}

)}

{log.action.status}

{log.action.method}

{log.action.name}

{project || organization ? ( <>

{project ? 'Project: ' : 'Organization: '} {project?.name ?? organization?.name}

{log.project_ref ? `Ref: ${log.project_ref}` : `Slug: ${log.organization_slug}`}

) : (

{log.project_ref ?? log.organization_slug ?? '-'}

)}
{dayjs(log.timestamp / TIMESTAMP_MICROS_PER_MS).format( 'DD MMM YYYY, HH:mm:ss' )}
) }) ?? [] } /> )} )} setSelectedLog(undefined)} /> ) }