import styled from '@emotion/styled'; import { isNonEmptyString } from '@sniptt/guards'; import { DateTime } from 'luxon'; import { useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useRecoilState } from 'recoil'; import { Button, H2Title, IconRepeat, IconTrash } from 'twenty-ui'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { ApiKeyInput } from '@/settings/developers/components/ApiKeyInput'; import { ApiKeyNameInput } from '@/settings/developers/components/ApiKeyNameInput'; import { apiKeyTokenState } from '@/settings/developers/states/generatedApiKeyTokenState'; import { ApiKey } from '@/settings/developers/types/api-key/ApiKey'; import { computeNewExpirationDate } from '@/settings/developers/utils/computeNewExpirationDate'; import { formatExpiration } from '@/settings/developers/utils/formatExpiration'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { TextInput } from '@/ui/input/components/TextInput'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; import { useGenerateApiKeyTokenMutation } from '~/generated/graphql'; const StyledInfo = styled.span` color: ${({ theme }) => theme.font.color.light}; font-size: ${({ theme }) => theme.font.size.sm}; font-weight: ${({ theme }) => theme.font.weight.regular}; `; const StyledInputContainer = styled.div` align-items: center; display: flex; flex-direction: row; gap: ${({ theme }) => theme.spacing(2)}; width: 100%; `; export const SettingsDevelopersApiKeyDetail = () => { const { enqueueSnackBar } = useSnackBar(); const [isRegenerateKeyModalOpen, setIsRegenerateKeyModalOpen] = useState(false); const [isDeleteApiKeyModalOpen, setIsDeleteApiKeyModalOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); const navigate = useNavigate(); const { apiKeyId = '' } = useParams(); const [apiKeyToken, setApiKeyToken] = useRecoilState(apiKeyTokenState); const [generateOneApiKeyToken] = useGenerateApiKeyTokenMutation(); const { createOneRecord: createOneApiKey } = useCreateOneRecord({ objectNameSingular: CoreObjectNameSingular.ApiKey, }); const { updateOneRecord: updateApiKey } = useUpdateOneRecord({ objectNameSingular: CoreObjectNameSingular.ApiKey, }); const [apiKeyName, setApiKeyName] = useState(''); const { record: apiKeyData, loading } = useFindOneRecord({ objectNameSingular: CoreObjectNameSingular.ApiKey, objectRecordId: apiKeyId, onCompleted: (record) => { setApiKeyName(record.name); }, }); const developerPath = getSettingsPagePath(SettingsPath.Developers); const deleteIntegration = async (redirect = true) => { setIsLoading(true); try { await updateApiKey?.({ idToUpdate: apiKeyId, updateOneRecordInput: { revokedAt: DateTime.now().toString() }, }); if (redirect) { navigate(developerPath); } } catch (err) { enqueueSnackBar(`Error deleting api key: ${err}`, { variant: SnackBarVariant.Error, }); } finally { setIsLoading(false); } }; const createIntegration = async ( name: string, newExpiresAt: string | null, ) => { const newApiKey = await createOneApiKey?.({ name: name, expiresAt: newExpiresAt ?? '', }); if (!newApiKey) { return; } const tokenData = await generateOneApiKeyToken({ variables: { apiKeyId: newApiKey.id, expiresAt: newApiKey?.expiresAt, }, }); return { id: newApiKey.id, token: tokenData.data?.generateApiKeyToken.token, }; }; const regenerateApiKey = async () => { setIsLoading(true); try { if (isNonEmptyString(apiKeyData?.name)) { const newExpiresAt = computeNewExpirationDate( apiKeyData?.expiresAt, apiKeyData?.createdAt, ); const apiKey = await createIntegration(apiKeyData?.name, newExpiresAt); await deleteIntegration(false); if (isNonEmptyString(apiKey?.token)) { setApiKeyToken(apiKey.token); navigate(`/settings/developers/api-keys/${apiKey.id}`); } } } catch (err) { enqueueSnackBar(`Error regenerating api key: ${err}`, { variant: SnackBarVariant.Error, }); } finally { setIsLoading(false); } }; return ( <> {apiKeyData?.name && (
{apiKeyToken ? ( <> {formatExpiration(apiKeyData?.expiresAt || '', true, false)} ) : ( <>
)} Please type "yes" to confirm you want to delete this API Key. Be aware that any script using this key will stop working. } onConfirmClick={deleteIntegration} deleteButtonText="Delete" loading={isLoading} /> If you’ve lost this key, you can regenerate it, but be aware that any script using this key will need to be updated. Please type "yes" to confirm. } onConfirmClick={regenerateApiKey} deleteButtonText="Regenerate key" loading={isLoading} /> ); };