Remove api keys from old world (#2548)

* Use apiKeyV2 for getApiKeys

* Use apiKeyV2 for createApiKey

* Use apiKeyV2 for getApiKey

* Use apiKeyV2 to deleteapikey

* Filter null revokedAt -> not working

* Use apiKeyV2 to regenerate

* Fix default values injected

* Remove useless stuff

* Fix type
This commit is contained in:
martmull
2023-11-16 18:14:04 +01:00
committed by GitHub
parent 31adb24ffd
commit e8a1d0d6d5
9 changed files with 179 additions and 76 deletions

View File

@ -1447,6 +1447,7 @@ export type Mutation = {
deleteUserAccount: User; deleteUserAccount: User;
deleteUserV2: UserV2; deleteUserV2: UserV2;
deleteWorkspaceMember: WorkspaceMember; deleteWorkspaceMember: WorkspaceMember;
generateApiKeyV2Token: ApiKeyToken;
impersonate: Verify; impersonate: Verify;
renewToken: AuthTokens; renewToken: AuthTokens;
revokeOneApiKey: ApiKey; revokeOneApiKey: ApiKey;
@ -1602,6 +1603,11 @@ export type MutationDeleteWorkspaceMemberArgs = {
}; };
export type MutationGenerateApiKeyV2TokenArgs = {
data: ApiKeyCreateInput;
};
export type MutationImpersonateArgs = { export type MutationImpersonateArgs = {
userId: Scalars['String']; userId: Scalars['String'];
}; };
@ -3641,6 +3647,13 @@ export type DeleteOneApiKeyMutationVariables = Exact<{
export type DeleteOneApiKeyMutation = { __typename?: 'Mutation', revokeOneApiKey: { __typename?: 'ApiKey', id: string } }; export type DeleteOneApiKeyMutation = { __typename?: 'Mutation', revokeOneApiKey: { __typename?: 'ApiKey', id: string } };
export type GenerateOneApiKeyTokenMutationVariables = Exact<{
data: ApiKeyCreateInput;
}>;
export type GenerateOneApiKeyTokenMutation = { __typename?: 'Mutation', generateApiKeyV2Token: { __typename?: 'ApiKeyToken', token: string } };
export type InsertOneApiKeyMutationVariables = Exact<{ export type InsertOneApiKeyMutationVariables = Exact<{
data: ApiKeyCreateInput; data: ApiKeyCreateInput;
}>; }>;
@ -5650,6 +5663,39 @@ export function useDeleteOneApiKeyMutation(baseOptions?: Apollo.MutationHookOpti
export type DeleteOneApiKeyMutationHookResult = ReturnType<typeof useDeleteOneApiKeyMutation>; export type DeleteOneApiKeyMutationHookResult = ReturnType<typeof useDeleteOneApiKeyMutation>;
export type DeleteOneApiKeyMutationResult = Apollo.MutationResult<DeleteOneApiKeyMutation>; export type DeleteOneApiKeyMutationResult = Apollo.MutationResult<DeleteOneApiKeyMutation>;
export type DeleteOneApiKeyMutationOptions = Apollo.BaseMutationOptions<DeleteOneApiKeyMutation, DeleteOneApiKeyMutationVariables>; export type DeleteOneApiKeyMutationOptions = Apollo.BaseMutationOptions<DeleteOneApiKeyMutation, DeleteOneApiKeyMutationVariables>;
export const GenerateOneApiKeyTokenDocument = gql`
mutation GenerateOneApiKeyToken($data: ApiKeyCreateInput!) {
generateApiKeyV2Token(data: $data) {
token
}
}
`;
export type GenerateOneApiKeyTokenMutationFn = Apollo.MutationFunction<GenerateOneApiKeyTokenMutation, GenerateOneApiKeyTokenMutationVariables>;
/**
* __useGenerateOneApiKeyTokenMutation__
*
* To run a mutation, you first call `useGenerateOneApiKeyTokenMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useGenerateOneApiKeyTokenMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [generateOneApiKeyTokenMutation, { data, loading, error }] = useGenerateOneApiKeyTokenMutation({
* variables: {
* data: // value for 'data'
* },
* });
*/
export function useGenerateOneApiKeyTokenMutation(baseOptions?: Apollo.MutationHookOptions<GenerateOneApiKeyTokenMutation, GenerateOneApiKeyTokenMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<GenerateOneApiKeyTokenMutation, GenerateOneApiKeyTokenMutationVariables>(GenerateOneApiKeyTokenDocument, options);
}
export type GenerateOneApiKeyTokenMutationHookResult = ReturnType<typeof useGenerateOneApiKeyTokenMutation>;
export type GenerateOneApiKeyTokenMutationResult = Apollo.MutationResult<GenerateOneApiKeyTokenMutation>;
export type GenerateOneApiKeyTokenMutationOptions = Apollo.BaseMutationOptions<GenerateOneApiKeyTokenMutation, GenerateOneApiKeyTokenMutationVariables>;
export const InsertOneApiKeyDocument = gql` export const InsertOneApiKeyDocument = gql`
mutation InsertOneApiKey($data: ApiKeyCreateInput!) { mutation InsertOneApiKey($data: ApiKeyCreateInput!) {
createOneApiKey(data: $data) { createOneApiKey(data: $data) {

View File

@ -1,4 +1,5 @@
import { useMutation } from '@apollo/client'; import { useMutation } from '@apollo/client';
import { v4 } from 'uuid';
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect'; import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
import { useFindOneObjectMetadataItem } from '@/object-metadata/hooks/useFindOneObjectMetadataItem'; import { useFindOneObjectMetadataItem } from '@/object-metadata/hooks/useFindOneObjectMetadataItem';
@ -41,16 +42,7 @@ export const useCreateOneObjectRecord = ({
? async (input: Record<string, any>) => { ? async (input: Record<string, any>) => {
const createdObject = await mutate({ const createdObject = await mutate({
variables: { variables: {
input: { input: { ...input, id: v4() },
...foundObjectMetadataItem.fields.reduce(
(result, field) => ({
...result,
[field.name]: defaultFieldValues[field.type],
}),
{},
),
...input,
},
}, },
}); });
@ -60,6 +52,7 @@ export const useCreateOneObjectRecord = ({
`create${capitalize(foundObjectMetadataItem.nameSingular)}` `create${capitalize(foundObjectMetadataItem.nameSingular)}`
], ],
); );
return createdObject.data;
} }
: undefined; : undefined;

View File

@ -0,0 +1,9 @@
import { gql } from '@apollo/client';
export const GENERATE_ONE_API_KEY_TOKEN = gql`
mutation GenerateOneApiKeyToken($data: ApiKeyCreateInput!) {
generateApiKeyV2Token(data: $data) {
token
}
}
`;

View File

@ -1,5 +1,5 @@
import { ApiFieldItem } from '@/settings/developers/types/ApiFieldItem'; import { ApiFieldItem } from '@/settings/developers/types/ApiFieldItem';
import { GetApiKeysQuery } from '~/generated/graphql'; import { ApiKey } from '~/generated/graphql';
import { beautifyDateDiff } from '~/utils/date-utils'; import { beautifyDateDiff } from '~/utils/date-utils';
export const formatExpiration = ( export const formatExpiration = (
@ -18,9 +18,9 @@ export const formatExpiration = (
}; };
export const formatExpirations = ( export const formatExpirations = (
apiKeysQuery: GetApiKeysQuery, apiKeys: Array<Pick<ApiKey, 'id' | 'name' | 'expiresAt'>>,
): ApiFieldItem[] => { ): ApiFieldItem[] => {
return apiKeysQuery.findManyApiKey.map(({ id, name, expiresAt }) => { return apiKeys.map(({ id, name, expiresAt }) => {
return { return {
id, id,
name, name,

View File

@ -1,9 +1,12 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { DateTime } from 'luxon';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect'; import { useCreateOneObjectRecord } from '@/object-record/hooks/useCreateOneObjectRecord';
import { useFindOneObjectRecord } from '@/object-record/hooks/useFindOneObjectRecord';
import { useUpdateOneObjectRecord } from '@/object-record/hooks/useUpdateOneObjectRecord';
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer'; import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { ApiKeyInput } from '@/settings/developers/components/ApiKeyInput'; import { ApiKeyInput } from '@/settings/developers/components/ApiKeyInput';
@ -18,11 +21,7 @@ import { TextInput } from '@/ui/input/components/TextInput';
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
import { Section } from '@/ui/layout/section/components/Section'; import { Section } from '@/ui/layout/section/components/Section';
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb'; import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
import { import { useGenerateOneApiKeyTokenMutation } from '~/generated/graphql';
useDeleteOneApiKeyMutation,
useGetApiKeyQuery,
useInsertOneApiKeyMutation,
} from '~/generated/graphql';
const StyledInfo = styled.span` const StyledInfo = styled.span`
color: ${({ theme }) => theme.font.color.light}; color: ${({ theme }) => theme.font.color.light};
@ -41,28 +40,29 @@ const StyledInputContainer = styled.div`
export const SettingsDevelopersApiKeyDetail = () => { export const SettingsDevelopersApiKeyDetail = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { apiKeyId = '' } = useParams(); const { apiKeyId = '' } = useParams();
const { triggerOptimisticEffects } = useOptimisticEffect('ApiKeyV2');
const setGeneratedApi = useGeneratedApiKeys(); const setGeneratedApi = useGeneratedApiKeys();
const [generatedApiKey] = useRecoilState( const [generatedApiKey] = useRecoilState(
generatedApiKeyFamilyState(apiKeyId), generatedApiKeyFamilyState(apiKeyId),
); );
const [deleteApiKey] = useDeleteOneApiKeyMutation(); const [generateOneApiKeyToken] = useGenerateOneApiKeyTokenMutation();
const [insertOneApiKey] = useInsertOneApiKeyMutation(); const { createOneObject: createOneApiKey } = useCreateOneObjectRecord({
const apiKeyData = useGetApiKeyQuery({ objectNamePlural: 'apiKeysV2',
variables: { });
apiKeyId, const { updateOneObject: updateApiKey } = useUpdateOneObjectRecord({
}, objectNamePlural: 'apiKeysV2',
}).data?.findManyApiKey[0]; });
const { object: apiKeyData } = useFindOneObjectRecord({
objectNameSingular: 'apiKeyV2',
objectMetadataId: apiKeyId,
});
const deleteIntegration = async (redirect = true) => { const deleteIntegration = async (redirect = true) => {
await deleteApiKey({ await updateApiKey?.({
variables: { apiKeyId }, idToUpdate: apiKeyId,
update: (cache) => input: { revokedAt: DateTime.now().toString() },
cache.evict({
id: cache.identify({ __typename: 'ApiKey', id: apiKeyId }),
}),
}); });
if (redirect) { if (redirect) {
navigate('/settings/developers/api-keys'); navigate('/settings/developers/api-keys');
@ -73,19 +73,23 @@ export const SettingsDevelopersApiKeyDetail = () => {
name: string, name: string,
newExpiresAt: string | null, newExpiresAt: string | null,
) => { ) => {
return await insertOneApiKey({ const newApiKey = await createOneApiKey?.({
name: name,
expiresAt: newExpiresAt,
});
const tokenData = await generateOneApiKeyToken({
variables: { variables: {
data: { data: {
name: name, id: newApiKey.createApiKeyV2.id,
expiresAt: newExpiresAt, expiresAt: newApiKey.createApiKeyV2.expiresAt,
name: newApiKey.createApiKeyV2.name, // TODO update typing to remove useless name param here
}, },
}, },
update: (_cache, { data }) => {
if (data?.createOneApiKey) {
triggerOptimisticEffects('ApiKey', [data?.createOneApiKey]);
}
},
}); });
return {
id: newApiKey.createApiKeyV2.id,
token: tokenData.data?.generateApiKeyV2Token.token,
};
}; };
const regenerateApiKey = async () => { const regenerateApiKey = async () => {
@ -96,14 +100,9 @@ export const SettingsDevelopersApiKeyDetail = () => {
); );
const apiKey = await createIntegration(apiKeyData.name, newExpiresAt); const apiKey = await createIntegration(apiKeyData.name, newExpiresAt);
await deleteIntegration(false); await deleteIntegration(false);
if (apiKey.data?.createOneApiKey) { if (apiKey.token) {
setGeneratedApi( setGeneratedApi(apiKey.id, apiKey.token);
apiKey.data.createOneApiKey.id, navigate(`/settings/developers/api-keys/${apiKey.id}`);
apiKey.data.createOneApiKey.token,
);
navigate(
`/settings/developers/api-keys/${apiKey.data.createOneApiKey.id}`,
);
} }
} }
}; };

View File

@ -1,10 +1,13 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect'; import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
import { useFindManyObjectRecords } from '@/object-record/hooks/useFindManyObjectRecords';
import { objectSettingsWidth } from '@/settings/data-model/constants/objectSettings'; import { objectSettingsWidth } from '@/settings/data-model/constants/objectSettings';
import { SettingsApiKeysFieldItemTableRow } from '@/settings/developers/components/SettingsApiKeysFieldItemTableRow'; import { SettingsApiKeysFieldItemTableRow } from '@/settings/developers/components/SettingsApiKeysFieldItemTableRow';
import { getApiKeysOptimisticEffectDefinition } from '@/settings/developers/optimistic-effect-definitions/getApiKeysOptimisticEffectDefinition'; import { getApiKeysOptimisticEffectDefinition } from '@/settings/developers/optimistic-effect-definitions/getApiKeysOptimisticEffectDefinition';
import { ApiFieldItem } from '@/settings/developers/types/ApiFieldItem';
import { formatExpirations } from '@/settings/developers/utils/format-expiration'; import { formatExpirations } from '@/settings/developers/utils/format-expiration';
import { IconPlus, IconSettings } from '@/ui/display/icon'; import { IconPlus, IconSettings } from '@/ui/display/icon';
import { H1Title } from '@/ui/display/typography/components/H1Title'; import { H1Title } from '@/ui/display/typography/components/H1Title';
@ -14,7 +17,6 @@ import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'
import { Table } from '@/ui/layout/table/components/Table'; import { Table } from '@/ui/layout/table/components/Table';
import { TableHeader } from '@/ui/layout/table/components/TableHeader'; import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow'; import { TableRow } from '@/ui/layout/table/components/TableRow';
import { useGetApiKeysQuery } from '~/generated/graphql';
const StyledContainer = styled.div` const StyledContainer = styled.div`
height: fit-content; height: fit-content;
@ -40,15 +42,26 @@ const StyledH1Title = styled(H1Title)`
export const SettingsDevelopersApiKeys = () => { export const SettingsDevelopersApiKeys = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { registerOptimisticEffect } = useOptimisticEffect('ApiKeyV2'); const { registerOptimisticEffect } = useOptimisticEffect('ApiKeyV2');
const apiKeysQuery = useGetApiKeysQuery({ const [apiKeys, setApiKeys] = useState<Array<ApiFieldItem>>([]);
onCompleted: () => { useFindManyObjectRecords({
objectNamePlural: 'apiKeysV2',
/*filter: { revokedAt: { eq: null } },*/
onCompleted: (data) => {
setApiKeys(
formatExpirations(
data.edges.map((apiKey) => ({
id: apiKey.node.id,
name: apiKey.node.name,
expiresAt: apiKey.node.expiresAt,
})),
),
);
registerOptimisticEffect({ registerOptimisticEffect({
variables: {}, variables: {},
definition: getApiKeysOptimisticEffectDefinition, definition: getApiKeysOptimisticEffectDefinition,
}); });
}, },
}); });
const apiKeys = apiKeysQuery.data ? formatExpirations(apiKeysQuery.data) : [];
return ( return (
<SubMenuTopBarContainer Icon={IconSettings} title="Settings"> <SubMenuTopBarContainer Icon={IconSettings} title="Settings">

View File

@ -2,7 +2,7 @@ import { useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect'; import { useCreateOneObjectRecord } from '@/object-record/hooks/useCreateOneObjectRecord';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer'; import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
@ -15,11 +15,10 @@ import { TextInput } from '@/ui/input/components/TextInput';
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
import { Section } from '@/ui/layout/section/components/Section'; import { Section } from '@/ui/layout/section/components/Section';
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb'; import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
import { useInsertOneApiKeyMutation } from '~/generated/graphql'; import { useGenerateOneApiKeyTokenMutation } from '~/generated/graphql';
export const SettingsDevelopersApiKeysNew = () => { export const SettingsDevelopersApiKeysNew = () => {
const [insertOneApiKey] = useInsertOneApiKeyMutation(); const [generateOneApiKeyToken] = useGenerateOneApiKeyTokenMutation();
const { triggerOptimisticEffects } = useOptimisticEffect('ApiKeyV2');
const navigate = useNavigate(); const navigate = useNavigate();
const setGeneratedApi = useGeneratedApiKeys(); const setGeneratedApi = useGeneratedApiKeys();
const [formValues, setFormValues] = useState<{ const [formValues, setFormValues] = useState<{
@ -29,35 +28,36 @@ export const SettingsDevelopersApiKeysNew = () => {
expirationDate: ExpirationDates[0].value, expirationDate: ExpirationDates[0].value,
name: '', name: '',
}); });
const { createOneObject: createOneApiKey } = useCreateOneObjectRecord({
objectNamePlural: 'apiKeysV2',
});
const onSave = async () => { const onSave = async () => {
const apiKey = await insertOneApiKey({ const expiresAt = formValues.expirationDate
? DateTime.now().plus({ days: formValues.expirationDate }).toString()
: null;
const newApiKey = await createOneApiKey?.({
name: formValues.name,
expiresAt,
});
const tokenData = await generateOneApiKeyToken({
variables: { variables: {
data: { data: {
name: formValues.name, id: newApiKey.createApiKeyV2.id,
expiresAt: formValues.expirationDate expiresAt: newApiKey.createApiKeyV2.expiresAt,
? DateTime.now() name: newApiKey.createApiKeyV2.name, // TODO update typing to remove useless name param here
.plus({ days: formValues.expirationDate })
.toString()
: null,
}, },
}, },
update: (_cache, { data }) => {
if (data?.createOneApiKey) {
triggerOptimisticEffects('ApiKey', [data?.createOneApiKey]);
}
},
}); });
if (apiKey.data?.createOneApiKey) { if (tokenData.data?.generateApiKeyV2Token) {
setGeneratedApi( setGeneratedApi(
apiKey.data.createOneApiKey.id, newApiKey.createApiKeyV2.id,
apiKey.data.createOneApiKey.token, tokenData.data.generateApiKeyV2Token.token,
);
navigate(
`/settings/developers/api-keys/${apiKey.data.createOneApiKey.id}`,
); );
navigate(`/settings/developers/api-keys/${newApiKey.createApiKeyV2.id}`);
} }
}; };
const canSave = !!formValues.name; const canSave = !!formValues.name && createOneApiKey;
return ( return (
<SubMenuTopBarContainer Icon={IconSettings} title="Settings"> <SubMenuTopBarContainer Icon={IconSettings} title="Settings">
<SettingsPageContainer> <SettingsPageContainer>

View File

@ -42,6 +42,21 @@ export class ApiKeyResolver {
); );
} }
@Mutation(() => ApiKeyToken)
@UseGuards(AbilityGuard)
@CheckAbilities(CreateApiKeyAbilityHandler)
async generateApiKeyV2Token(
@Args()
args: CreateOneApiKeyArgs,
@AuthWorkspace() { id: workspaceId }: Workspace,
): Promise<Pick<ApiKeyToken, 'token'> | undefined> {
return await this.apiKeyService.generateApiKeyV2Token(
workspaceId,
args.data.id,
args.data.expiresAt,
);
}
@Mutation(() => ApiKey) @Mutation(() => ApiKey)
@UseGuards(AbilityGuard) @UseGuards(AbilityGuard)
@CheckAbilities(UpdateApiKeyAbilityHandler) @CheckAbilities(UpdateApiKeyAbilityHandler)

View File

@ -21,6 +21,34 @@ export class ApiKeyService {
update = this.prismaService.client.apiKey.update; update = this.prismaService.client.apiKey.update;
delete = this.prismaService.client.apiKey.delete; delete = this.prismaService.client.apiKey.delete;
async generateApiKeyV2Token(
workspaceId: string,
apiKeyId?: string,
expiresAt?: Date | string,
): Promise<Pick<ApiKeyToken, 'token'> | undefined> {
if (!apiKeyId) {
return;
}
const jwtPayload = {
sub: workspaceId,
};
const secret = this.environmentService.getAccessTokenSecret();
let expiresIn: string | number;
if (expiresAt) {
expiresIn = Math.floor(
(new Date(expiresAt).getTime() - new Date().getTime()) / 1000,
);
} else {
expiresIn = this.environmentService.getApiTokenExpiresIn();
}
const token = this.jwtService.sign(jwtPayload, {
secret,
expiresIn,
jwtid: apiKeyId,
});
return { token };
}
async generateApiKeyToken( async generateApiKeyToken(
workspaceId: string, workspaceId: string,
name: string, name: string,