SecuritySettings.tsx216 lines · main
1// @ts-nocheck
2import { zodResolver } from '@hookform/resolvers/zod'
3import { PermissionAction } from '@supabase/shared-types/out/constants'
4import { useParams } from 'common'
5import { useEffect } from 'react'
6import { useForm } from 'react-hook-form'
7import { toast } from 'sonner'
8import {
9 Button,
10 Card,
11 CardContent,
12 CardFooter,
13 Form,
14 FormControl,
15 FormField,
16 Switch,
17 Tooltip,
18 TooltipContent,
19 TooltipTrigger,
20} from 'ui'
21import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
22import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
23import { z } from 'zod'
24
25import { ScaffoldContainer, ScaffoldSection } from '@/components/layouts/Scaffold'
26import AlertError from '@/components/ui/AlertError'
27import { InlineLink } from '@/components/ui/InlineLink'
28import NoPermission from '@/components/ui/NoPermission'
29import { UpgradeToPro } from '@/components/ui/UpgradeToPro'
30import { useOrganizationMembersQuery } from '@/data/organizations/organization-members-query'
31import { useOrganizationMfaToggleMutation } from '@/data/organizations/organization-mfa-mutation'
32import { useOrganizationMfaQuery } from '@/data/organizations/organization-mfa-query'
33import { useSendEventMutation } from '@/data/telemetry/send-event-mutation'
34import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
35import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
36import { useProfile } from '@/lib/profile'
37
38const schema = z.object({
39 enforceMfa: z.boolean(),
40})
41
42export const SecuritySettings = () => {
43 const { slug } = useParams()
44 const { profile } = useProfile()
45 const { data: members } = useOrganizationMembersQuery({ slug })
46
47 const { can: canReadMfaConfig, isLoading: isLoadingPermissions } = useAsyncCheckPermissions(
48 PermissionAction.READ,
49 'organizations'
50 )
51 const { can: canUpdateMfaConfig } = useAsyncCheckPermissions(
52 PermissionAction.UPDATE,
53 'organizations'
54 )
55 const { mutate: sendEvent } = useSendEventMutation()
56
57 const { hasAccess: hasAccessToEnforceMfa, isLoading: isLoadingEntitlement } =
58 useCheckEntitlements('security.enforce_mfa')
59
60 const {
61 data: mfaConfig,
62 error: mfaError,
63 isPending: isLoadingMfa,
64 isError: isErrorMfa,
65 isSuccess: isSuccessMfa,
66 } = useOrganizationMfaQuery({ slug }, { enabled: hasAccessToEnforceMfa && canReadMfaConfig })
67
68 const { mutate: toggleMfa, isPending: isUpdatingMfa } = useOrganizationMfaToggleMutation({
69 onError: (error) => {
70 toast.error(`Failed to update MFA enforcement: ${error.message}`)
71 if (mfaConfig !== undefined) form.reset({ enforceMfa: mfaConfig })
72 },
73 onSuccess: (data) => {
74 toast.success('Successfully updated organization MFA settings')
75 sendEvent({
76 action: 'organization_mfa_enforcement_updated',
77 properties: {
78 mfaEnforced: data.enforced,
79 },
80 groups: {
81 organization: slug ?? 'Unknown',
82 },
83 })
84 },
85 })
86
87 const form = useForm<z.infer<typeof schema>>({
88 resolver: zodResolver(schema as any),
89 defaultValues: {
90 enforceMfa: false,
91 },
92 })
93
94 useEffect(() => {
95 if (mfaConfig !== undefined) {
96 form.reset({ enforceMfa: mfaConfig })
97 }
98 }, [mfaConfig, form])
99
100 const hasMFAEnabled =
101 members?.find((member) => member.primary_email == profile?.primary_email)?.mfa_enabled || false
102
103 const onSubmit = (values: { enforceMfa: boolean }) => {
104 if (!slug || !hasAccessToEnforceMfa) return
105 toggleMfa({ slug, setEnforced: values.enforceMfa })
106 }
107
108 return (
109 <ScaffoldContainer size="small" className="px-6 xl:px-10">
110 <ScaffoldSection isFullWidth>
111 {!hasAccessToEnforceMfa && !isLoadingEntitlement ? (
112 <UpgradeToPro
113 source="organizationMfa"
114 primaryText="Organization MFA enforcement is not available on Free Plan"
115 secondaryText="Upgrade to Pro or above to enforce MFA requirements for your organization."
116 featureProposition="enforce MFA requirements"
117 />
118 ) : (
119 <>
120 {isLoadingMfa || isLoadingPermissions || isLoadingEntitlement ? (
121 <Card>
122 <CardContent>
123 <GenericSkeletonLoader />
124 </CardContent>
125 </Card>
126 ) : !canReadMfaConfig ? (
127 <NoPermission resourceText="view organization security settings" />
128 ) : null}
129
130 {(isErrorMfa || mfaError) && hasAccessToEnforceMfa && (
131 <AlertError error={mfaError} subject="Failed to retrieve MFA enforcement status" />
132 )}
133
134 {isSuccessMfa && hasAccessToEnforceMfa && (
135 <Form {...form}>
136 <form onSubmit={form.handleSubmit(onSubmit)}>
137 <Card>
138 <CardContent>
139 <FormField
140 control={form.control}
141 name="enforceMfa"
142 render={({ field }) => (
143 <FormItemLayout
144 layout="flex-row-reverse"
145 label="Require MFA to access organization"
146 description="Team members must have MFA enabled and a valid MFA session to access the organization and any projects."
147 >
148 <FormControl>
149 <Tooltip>
150 <TooltipTrigger asChild>
151 <Switch
152 checked={field.value}
153 onCheckedChange={field.onChange}
154 disabled={
155 !hasAccessToEnforceMfa ||
156 !canUpdateMfaConfig ||
157 !hasMFAEnabled ||
158 isUpdatingMfa
159 }
160 />
161 </TooltipTrigger>
162 {(!canUpdateMfaConfig || !hasMFAEnabled) && (
163 <TooltipContent side="bottom">
164 {!canUpdateMfaConfig ? (
165 "You don't have permission to update MFA settings"
166 ) : (
167 <>
168 <InlineLink href="/account/security">Enable MFA</InlineLink>{' '}
169 on your own account first
170 </>
171 )}
172 </TooltipContent>
173 )}
174 </Tooltip>
175 </FormControl>
176 </FormItemLayout>
177 )}
178 />
179 </CardContent>
180 <CardFooter className="justify-end space-x-2">
181 {form.formState.isDirty && (
182 <Button
183 type="default"
184 disabled={isLoadingMfa || isUpdatingMfa}
185 onClick={() =>
186 form.reset({ enforceMfa: hasAccessToEnforceMfa ? mfaConfig : false })
187 }
188 >
189 Cancel
190 </Button>
191 )}
192 <Button
193 type="primary"
194 htmlType="submit"
195 disabled={
196 !hasAccessToEnforceMfa ||
197 !canUpdateMfaConfig ||
198 isUpdatingMfa ||
199 isLoadingMfa ||
200 !form.formState.isDirty
201 }
202 loading={isUpdatingMfa}
203 >
204 Save changes
205 </Button>
206 </CardFooter>
207 </Card>
208 </form>
209 </Form>
210 )}
211 </>
212 )}
213 </ScaffoldSection>
214 </ScaffoldContainer>
215 )
216}