SecuritySettings.tsx 8.1 KB

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