Files
twenty/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeyDetail.tsx
flox_x fd607789f4 Replace all hardcoded core object name by enum (#3170)
* Replace hardcoded core object name by enum

Signed-off-by: Florian Grabmeier <flo.grabmeier@gmail.com>

* Fix typo

Signed-off-by: Florian Grabmeier <flo.grabmeier@gmail.com>

* Fixed duplicate import

---------

Signed-off-by: Florian Grabmeier <flo.grabmeier@gmail.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
2023-12-29 15:11:30 +01:00

202 lines
6.8 KiB
TypeScript

import { useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import styled from '@emotion/styled';
import { DateTime } from 'luxon';
import { useRecoilState } from 'recoil';
import { useOptimisticEvict } from '@/apollo/optimistic-effect/hooks/useOptimisticEvict';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { ApiKeyInput } from '@/settings/developers/components/ApiKeyInput';
import { useGeneratedApiKeys } from '@/settings/developers/hooks/useGeneratedApiKeys';
import { generatedApiKeyFamilyState } from '@/settings/developers/states/generatedApiKeyFamilyState';
import { ApiKey } from '@/settings/developers/types/ApiKey';
import { computeNewExpirationDate } from '@/settings/developers/utils/compute-new-expiration-date';
import { formatExpiration } from '@/settings/developers/utils/format-expiration';
import { IconRepeat, IconSettings, IconTrash } from '@/ui/display/icon';
import { H2Title } from '@/ui/display/typography/components/H2Title';
import { Button } from '@/ui/input/button/components/Button';
import { TextInput } from '@/ui/input/components/TextInput';
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
import { Section } from '@/ui/layout/section/components/Section';
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
import { useGenerateApiKeyTokenMutation } from '~/generated/graphql';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
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 navigate = useNavigate();
const { apiKeyId = '' } = useParams();
const setGeneratedApi = useGeneratedApiKeys();
const [generatedApiKey] = useRecoilState(
generatedApiKeyFamilyState(apiKeyId),
);
const { performOptimisticEvict } = useOptimisticEvict();
const [generateOneApiKeyToken] = useGenerateApiKeyTokenMutation();
const { createOneRecord: createOneApiKey } = useCreateOneRecord<ApiKey>({
objectNameSingular: CoreObjectNameSingular.ApiKey,
});
const { updateOneRecord: updateApiKey } = useUpdateOneRecord<ApiKey>({
objectNameSingular: CoreObjectNameSingular.ApiKey,
});
const { record: apiKeyData } = useFindOneRecord({
objectNameSingular: CoreObjectNameSingular.ApiKey,
objectRecordId: apiKeyId,
});
const deleteIntegration = async (redirect = true) => {
await updateApiKey?.({
idToUpdate: apiKeyId,
updateOneRecordInput: { revokedAt: DateTime.now().toString() },
});
performOptimisticEvict('ApiKey', 'id', apiKeyId);
if (redirect) {
navigate('/settings/developers/api-keys');
}
};
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 () => {
if (apiKeyData?.name) {
const newExpiresAt = computeNewExpirationDate(
apiKeyData.expiresAt,
apiKeyData.createdAt,
);
const apiKey = await createIntegration(apiKeyData.name, newExpiresAt);
await deleteIntegration(false);
if (apiKey && apiKey.token) {
setGeneratedApi(apiKey.id, apiKey.token);
navigate(`/settings/developers/api-keys/${apiKey.id}`);
}
}
};
useEffect(() => {
if (apiKeyData) {
return () => {
setGeneratedApi(apiKeyId, null);
};
}
});
return (
<>
{apiKeyData?.name && (
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
<SettingsPageContainer>
<SettingsHeaderContainer>
<Breadcrumb
links={[
{ children: 'APIs', href: '/settings/developers/api-keys' },
{ children: apiKeyData.name },
]}
/>
</SettingsHeaderContainer>
<Section>
{generatedApiKey ? (
<>
<H2Title
title="Api Key"
description="Copy this key as it will only be visible this one time"
/>
<ApiKeyInput apiKey={generatedApiKey} />
<StyledInfo>
{formatExpiration(apiKeyData?.expiresAt || '', true, false)}
</StyledInfo>
</>
) : (
<>
<H2Title
title="Api Key"
description="Regenerate an Api key"
/>
<StyledInputContainer>
<Button
title="Regenerate Key"
Icon={IconRepeat}
onClick={regenerateApiKey}
/>
<StyledInfo>
{formatExpiration(
apiKeyData?.expiresAt || '',
true,
false,
)}
</StyledInfo>
</StyledInputContainer>
</>
)}
</Section>
<Section>
<H2Title title="Name" description="Name of your API key" />
<TextInput
placeholder="E.g. backoffice integration"
value={apiKeyData.name}
disabled
fullWidth
/>
</Section>
<Section>
<H2Title
title="Danger zone"
description="Delete this integration"
/>
<Button
accent="danger"
variant="secondary"
title="Disable"
Icon={IconTrash}
onClick={() => deleteIntegration()}
/>
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
)}
</>
);
};