import { useState, useEffect } from 'react' import { Field, Form, Formik } from 'formik' import { DialogStateReturn } from 'reakit/ts' import { trpc, inferMutationInput } from '~/utils/trpc' import IVInputField from '~/components/IVInputField' import IVButton from '~/components/IVButton' import { notify } from '~/components/NotificationCenter' import useDashboard, { useHasPermission } from '~/components/DashboardContext' import { Link } from 'react-router-dom' import IVSelect from '~/components/IVSelect' import { EXPOSED_ROLES } from '~/utils/permissions' import { userAccessPermissionToString } from '~/utils/text' import IVAPIError from '~/components/IVAPIError' import IVRadio from '~/components/IVRadio' import { TIMEZONE_OPTIONS } from '~/utils/timezones' import { useOrgParams } from '~/utils/organization' import IVDialog, { useDialogState } from '~/components/IVDialog' import EnrollMFAForm from './EnrollMFAForm' import IVTooltip from '~/components/IVTooltip' import MFAInput from '../MFAInput' function UpdateAccountForm() { const mutation = trpc.useMutation('user.edit') const ctx = trpc.useContext() const { me, organization } = useDashboard() const canManageOrganization = useHasPermission('WRITE_ORG_SETTINGS') const [hasPendingEmailConf, setHasPendingEmailConf] = useState(false) return ( ['data']> initialValues={{ firstName: me.firstName ?? '', lastName: me.lastName ?? '', email: me.email, defaultNotificationMethod: me.defaultNotificationMethod ?? 'EMAIL', timeZoneName: me.timeZoneName, }} onSubmit={async data => { if (mutation.isLoading) return mutation.mutate( { id: me.id, data }, { onSuccess(res) { setHasPendingEmailConf(res.requiresEmailConfirmation) notify.success('Your changes were saved.') ctx.refetchQueries(['user.me']) }, } ) }} > {({ values }) => (
{hasPendingEmailConf && (
We sent you an email to confirm your new email address. Please click the link in the email to confirm this change.
)}

Configure where you would like to receive notifications.

Enable by connecting to Slack in your{' '} organization settings. ) : ( <> Contact your organization administrator about connecting your organization to Slack. )) } />
{mutation.isError && (
Sorry, there was a problem editing your account.
)}
)} ) } function UpdatePasswordForm() { const mutation = trpc.useMutation('auth.password.edit') return ( ['data']> initialValues={{ newPassword: '', newPasswordConfirm: '', }} validate={values => { if (values.newPassword !== values.newPasswordConfirm) { return { newPasswordConfirm: 'Passwords do not match', } } if (values.newPassword.length && values.newPassword.length < 6) { return { newPasswordConfirm: 'Password must be at least 6 characters', } } return {} }} onSubmit={async (data, { resetForm }) => { if (mutation.isLoading) return mutation.mutate( { data }, { onSuccess() { notify('Your password was updated.') resetForm() }, } ) }} > {({ errors, touched }) => (

Update password

{mutation.isError && (
Sorry, there was an error while updating your password.
)}
)} ) } function UpdateRoleForm() { const mutation = trpc.useMutation('user.edit-role') const { orgSlug } = useOrgParams() const { me } = useDashboard() if (!import.meta.env.DEV) return null return ( ['data']> initialValues={{ orgSlug, permission: me.userOrganizationAccess.find( access => access.organization.slug === orgSlug )?.permissions[0] || 'ACTION_RUNNER', }} onSubmit={async data => { if (mutation.isLoading) return mutation.mutate( { id: me.id, data }, { onSuccess() { notify.success('Your role was updated. Reloading...') setTimeout(() => window.location.reload(), 1000) }, } ) }} >

Change role

({ label: userAccessPermissionToString(role), value: role, }))} />
) } function AddMFADialog({ dialog, onSubmit, }: { dialog: DialogStateReturn onSubmit: () => void }) { return ( {(dialog.visible || dialog.animating) && ( )} ) } function RemoveMFADialog({ dialog, onSubmit, }: { dialog: DialogStateReturn onSubmit: () => void }) { const challenge = trpc.useMutation(['auth.mfa.challenge']) const removeMfa = trpc.useMutation(['auth.mfa.delete']) const { mutate: startChallenge } = challenge const { visible } = dialog useEffect(() => { if (visible) { startChallenge() } }, [visible, startChallenge]) return (

Enter a code with your current MFA enrollment to disable it.

initialValues={{ code: '', }} initialErrors={{ code: 'Please enter a code', }} onSubmit={async ({ code }) => { if (!challenge.data) return removeMfa.mutate( { code, challengeId: challenge.data, }, { onSuccess() { onSubmit() }, } ) }} validate={({ code }) => { if (!code) { return { code: 'Please enter a code.', } } }} > {({ isValid }) => (
{removeMfa.isError && (
Sorry, that code is invalid. Please try again.
)}
{ dialog.hide() }} />
)}
) } function MFASection() { const { organization } = useDashboard() const hasMfa = trpc.useQuery(['auth.mfa.has']) const addMfaDialog = useDialogState() const removeMfaDialog = useDialogState() const { invalidateQueries } = trpc.useContext() return (

Multi-factor authentication

{hasMfa.data ? ( <>

Multi-factor authentication is currently enabled.

{ const confirmed = window.confirm( 'Are you sure you want to re-enroll in multi-factor authentication? This will remove and invalidate your previous MFA enrollment.' ) if (confirmed) { addMfaDialog.show() } }} /> {organization.requireMfa ? ( ) : ( { removeMfaDialog.show() }} /> )}
) : (

Multi-factor authentication {' '} adds an additional layer of security to your account. We strongly recommending enabling MFA.

{ addMfaDialog.show() }} />
)} { addMfaDialog.hide() hasMfa.refetch() invalidateQueries('auth.session.session') notify.success('Multi-factor authentication is enabled.') }} /> { removeMfaDialog.hide() hasMfa.refetch() invalidateQueries('auth.session.session') notify.success('Multi-factor authentication is disabled.') }} />
) } export default function AccountSettings() { const { integrations } = useDashboard() const hasPassword = trpc.useQuery(['auth.password.has']) return (
{/* These should remain at the bottom of the page to prevent weird flickering */} {integrations?.workos && } {hasPassword.data && }
) }