AuditLogsForm.tsx207 lines · main
1import { zodResolver } from '@hookform/resolvers/zod'
2import { PermissionAction } from '@supabase/shared-types/out/constants'
3import { useParams } from 'common'
4import { useEffect } from 'react'
5import { useForm } from 'react-hook-form'
6import { toast } from 'sonner'
7import { Button, Card, CardContent, CardFooter, Form, FormControl, FormField, Switch } from 'ui'
8import { Admonition, GenericSkeletonLoader } from 'ui-patterns'
9import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
10import {
11 PageSection,
12 PageSectionContent,
13 PageSectionMeta,
14 PageSectionSummary,
15 PageSectionTitle,
16} from 'ui-patterns/PageSection'
17import * as z from 'zod'
18
19import { AlertError } from '@/components/ui/AlertError'
20import { InlineLink } from '@/components/ui/InlineLink'
21import { useAuthConfigQuery } from '@/data/auth/auth-config-query'
22import { useAuthConfigUpdateMutation } from '@/data/auth/auth-config-update-mutation'
23import { useTablesQuery } from '@/data/tables/tables-query'
24import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
25import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
26
27const schema = z.object({
28 AUDIT_LOG_DISABLE_POSTGRES: z.boolean(),
29})
30
31const AUDIT_LOG_ENTRIES_TABLE = 'audit_log_entries'
32
33export const AuditLogsForm = () => {
34 const { ref: projectRef } = useParams()
35 const { data: project } = useSelectedProjectQuery()
36
37 const { can: canUpdateConfig } = useAsyncCheckPermissions(
38 PermissionAction.UPDATE,
39 'custom_config_gotrue'
40 )
41
42 const { data: tables = [] } = useTablesQuery({
43 projectRef: project?.ref,
44 connectionString: project?.connectionString,
45 includeColumns: false,
46 schema: 'auth',
47 })
48 const auditLogTable = tables.find((x) => x.name === AUDIT_LOG_ENTRIES_TABLE)
49
50 const {
51 data: authConfig,
52 error: authConfigError,
53 isError,
54 isPending: isLoading,
55 } = useAuthConfigQuery({ projectRef })
56
57 const { mutate: updateAuthConfig, isPending: isUpdatingConfig } = useAuthConfigUpdateMutation({
58 onError: (error) => {
59 toast.error(`Failed to update audit logs: ${error?.message}`)
60 },
61 onSuccess: () => {
62 toast.success('Successfully updated audit logs settings')
63 },
64 })
65
66 const form = useForm({
67 resolver: zodResolver(schema as any),
68 defaultValues: { AUDIT_LOG_DISABLE_POSTGRES: false },
69 })
70 const { isDirty } = form.formState
71 const { AUDIT_LOG_DISABLE_POSTGRES: formValueDisablePostgres } = form.watch()
72 const currentlyDisabled = authConfig?.AUDIT_LOG_DISABLE_POSTGRES ?? false
73 const isDisabling = !currentlyDisabled && formValueDisablePostgres
74
75 const onSubmitAuditLogs = (values: any) => {
76 if (!projectRef) return console.error('Project ref is required')
77 updateAuthConfig({ projectRef: projectRef, config: values })
78 }
79
80 useEffect(() => {
81 if (authConfig) {
82 form.reset({ AUDIT_LOG_DISABLE_POSTGRES: authConfig?.AUDIT_LOG_DISABLE_POSTGRES ?? false })
83 }
84 }, [authConfig])
85
86 if (isError) {
87 return (
88 <PageSection>
89 <PageSectionContent>
90 <AlertError
91 error={authConfigError}
92 subject="Failed to retrieve auth configuration for hooks"
93 />
94 </PageSectionContent>
95 </PageSection>
96 )
97 }
98
99 if (isLoading) {
100 return (
101 <PageSection>
102 <PageSectionContent>
103 <GenericSkeletonLoader />
104 </PageSectionContent>
105 </PageSection>
106 )
107 }
108
109 return (
110 <PageSection>
111 <PageSectionMeta>
112 <PageSectionSummary>
113 <PageSectionTitle>Settings</PageSectionTitle>
114 </PageSectionSummary>
115 </PageSectionMeta>
116 <PageSectionContent>
117 <Form {...form}>
118 <form onSubmit={form.handleSubmit(onSubmitAuditLogs)} className="space-y-4">
119 <Card>
120 <CardContent>
121 <FormField
122 control={form.control}
123 name="AUDIT_LOG_DISABLE_POSTGRES"
124 render={({ field }) => (
125 <FormItemLayout
126 layout="flex-row-reverse"
127 label="Write audit logs to the database"
128 description={
129 <p className="text-sm prose text-foreground-lighter max-w-full">
130 When enabled, audit logs are written to the{' '}
131 <InlineLink
132 target="_blank"
133 rel="noopener noreferrer"
134 href={`/project/${projectRef}/editor/${auditLogTable?.id}`}
135 >
136 <code className="text-code-inline">{AUDIT_LOG_ENTRIES_TABLE}</code>
137 </InlineLink>{' '}
138 table.
139 <br />
140 You can disable this to reduce disk usage while still accessing logs
141 through the{' '}
142 <InlineLink
143 href={`/project/${projectRef}/logs/explorer?q=select%0A++cast(timestamp+as+datetime)+as+timestamp%2C%0A++event_message%2C+metadata+%0Afrom+auth_audit_logs+%0Alimit+10%0A`}
144 >
145 Auth logs
146 </InlineLink>
147 .
148 </p>
149 }
150 >
151 <FormControl>
152 <Switch
153 checked={!field.value}
154 onCheckedChange={(value) => field.onChange(!value)}
155 disabled={!canUpdateConfig}
156 />
157 </FormControl>
158 </FormItemLayout>
159 )}
160 />
161 {isDisabling && (
162 <Admonition
163 type="warning"
164 className="mt-4"
165 title="Disabling PostgreSQL storage will not automatically migrate or transfer existing audit log data"
166 description={
167 <p>
168 Future audit logs will only appear in the project’s{' '}
169 <InlineLink
170 href={`/project/${projectRef}/logs/explorer?q=select%0A++cast(timestamp+as+datetime)+as+timestamp%2C%0A++event_message%2C+metadata+%0Afrom+auth_audit_logs+%0Alimit+10%0A`}
171 >
172 auth logs
173 </InlineLink>
174 . You are responsible for backing up, copying, or migrating existing data
175 from the{' '}
176 <code className="text-code-inline break-keep!">
177 {AUDIT_LOG_ENTRIES_TABLE}
178 </code>{' '}
179 table if needed.
180 </p>
181 }
182 />
183 )}
184 </CardContent>
185
186 <CardFooter className="justify-end space-x-2">
187 {isDirty && (
188 <Button type="default" onClick={() => form.reset()}>
189 Cancel
190 </Button>
191 )}
192 <Button
193 type="primary"
194 htmlType="submit"
195 disabled={!canUpdateConfig || isUpdatingConfig || !isDirty}
196 loading={isUpdatingConfig}
197 >
198 Save changes
199 </Button>
200 </CardFooter>
201 </Card>
202 </form>
203 </Form>
204 </PageSectionContent>
205 </PageSection>
206 )
207}