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 }) => (
)}
)
}
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 }) => (
)}
)
}
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)
},
}
)
}}
>
)
}
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 }) => (
)}
)
}
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 && }
)
}