mirror of
https://github.com/lingble/twenty.git
synced 2025-10-29 20:02:29 +00:00
2060 create a new api key (#2206)
* Add folder for api settings * Init create api key page * Update create api key page * Implement api call to create apiKey * Add create api key mutation * Get id when creating apiKey * Display created Api Key * Add delete api key button * Remove button from InputText * Update stuff * Add test for ApiDetail * Fix type * Use recoil instead of router state * Remane route paths * Remove online return * Move and test date util * Remove useless Component * Rename ApiKeys paths * Rename ApiKeys files * Add input text info testing * Rename hooks to webhooks * Remove console error * Add tests to reach minimum coverage
This commit is contained in:
@@ -37,7 +37,7 @@ From the `packages/twenty-zapier` folder, run:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
Run the application locally, go to [http://localhost:3000/settings/apis](http://localhost:3000/settings/apis), and generate an API key.
|
||||
Run the application locally, go to [http://localhost:3000/settings/developers/api-keys](http://localhost:3000/settings/developers/api-keys), and generate an API key.
|
||||
|
||||
Replace the **YOUR_API_KEY** value in the `.env` file with the API key you just generated.
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ module.exports = {
|
||||
'message': 'Icon imports are only allowed for `@/ui/icon`',
|
||||
},
|
||||
{
|
||||
'group': ['react-hotkeys-hook'],
|
||||
'group': ['react-hotkeys-web-hook'],
|
||||
"importNames": ["useHotkeys"],
|
||||
'message': 'Please use the custom wrapper: `useScopedHotkeys`',
|
||||
},
|
||||
|
||||
@@ -21,6 +21,9 @@ import { SettingsNewObject } from '~/pages/settings/data-model/SettingsNewObject
|
||||
import { SettingsObjectDetail } from '~/pages/settings/data-model/SettingsObjectDetail';
|
||||
import { SettingsObjectEdit } from '~/pages/settings/data-model/SettingsObjectEdit';
|
||||
import { SettingsObjects } from '~/pages/settings/data-model/SettingsObjects';
|
||||
import { SettingsDevelopersApiKeyDetail } from '~/pages/settings/developers/api-keys/SettingsDevelopersApiKeyDetail';
|
||||
import { SettingsDevelopersApiKeys } from '~/pages/settings/developers/api-keys/SettingsDevelopersApiKeys';
|
||||
import { SettingsDevelopersApiKeysNew } from '~/pages/settings/developers/api-keys/SettingsDevelopersApiKeysNew';
|
||||
import { SettingsExperience } from '~/pages/settings/SettingsExperience';
|
||||
import { SettingsProfile } from '~/pages/settings/SettingsProfile';
|
||||
import { SettingsWorkspace } from '~/pages/settings/SettingsWorkspace';
|
||||
@@ -31,7 +34,6 @@ import { getPageTitleFromPath } from '~/utils/title-utils';
|
||||
import { ObjectTablePage } from './modules/metadata/components/ObjectTablePage';
|
||||
import { SettingsObjectNewFieldStep1 } from './pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep1';
|
||||
import { SettingsObjectNewFieldStep2 } from './pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2';
|
||||
import { SettingsApis } from './pages/settings/SettingsApis';
|
||||
|
||||
export const App = () => {
|
||||
const { pathname } = useLocation();
|
||||
@@ -97,7 +99,25 @@ export const App = () => {
|
||||
path={SettingsPath.NewObject}
|
||||
element={<SettingsNewObject />}
|
||||
/>
|
||||
<Route path={SettingsPath.Apis} element={<SettingsApis />} />
|
||||
<Route
|
||||
path={AppPath.DevelopersCatchAll}
|
||||
element={
|
||||
<Routes>
|
||||
<Route
|
||||
path={SettingsPath.Developers}
|
||||
element={<SettingsDevelopersApiKeys />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.DevelopersNewApiKey}
|
||||
element={<SettingsDevelopersApiKeysNew />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.DevelopersApiKeyDetail}
|
||||
element={<SettingsDevelopersApiKeyDetail />}
|
||||
/>
|
||||
</Routes>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.ObjectNewFieldStep1}
|
||||
element={<SettingsObjectNewFieldStep1 />}
|
||||
|
||||
@@ -760,6 +760,15 @@ export enum ViewType {
|
||||
Table = 'Table'
|
||||
}
|
||||
|
||||
export type WebHook = {
|
||||
__typename?: 'WebHook';
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
id: Scalars['ID']['output'];
|
||||
operation: Scalars['String']['output'];
|
||||
targetUrl: Scalars['String']['output'];
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
};
|
||||
|
||||
export type Workspace = {
|
||||
__typename?: 'Workspace';
|
||||
Attachment?: Maybe<Array<Attachment>>;
|
||||
@@ -783,6 +792,7 @@ export type Workspace = {
|
||||
viewFilters?: Maybe<Array<ViewFilter>>;
|
||||
viewSorts?: Maybe<Array<ViewSort>>;
|
||||
views?: Maybe<Array<View>>;
|
||||
webHooks?: Maybe<Array<WebHook>>;
|
||||
workspaceMember?: Maybe<Array<WorkspaceMember>>;
|
||||
};
|
||||
|
||||
|
||||
@@ -482,6 +482,13 @@ export enum ApiKeyScalarFieldEnum {
|
||||
WorkspaceId = 'workspaceId'
|
||||
}
|
||||
|
||||
export type ApiKeyToken = {
|
||||
__typename?: 'ApiKeyToken';
|
||||
expiresAt: Scalars['DateTime'];
|
||||
id: Scalars['String'];
|
||||
token: Scalars['String'];
|
||||
};
|
||||
|
||||
export type ApiKeyUpdateManyWithoutWorkspaceNestedInput = {
|
||||
connect?: InputMaybe<Array<ApiKeyWhereUniqueInput>>;
|
||||
disconnect?: InputMaybe<Array<ApiKeyWhereUniqueInput>>;
|
||||
@@ -1392,7 +1399,7 @@ export type Mutation = {
|
||||
createManyViewFilter: AffectedRows;
|
||||
createManyViewSort: AffectedRows;
|
||||
createOneActivity: Activity;
|
||||
createOneApiKey: AuthToken;
|
||||
createOneApiKey: ApiKeyToken;
|
||||
createOneComment: Comment;
|
||||
createOneCompany: Company;
|
||||
createOneField: Field;
|
||||
@@ -1402,6 +1409,7 @@ export type Mutation = {
|
||||
createOnePipelineStage: PipelineStage;
|
||||
createOneView: View;
|
||||
createOneViewField: ViewField;
|
||||
createOneWebHook: WebHook;
|
||||
deleteCurrentWorkspace: Workspace;
|
||||
deleteFavorite: Favorite;
|
||||
deleteManyActivities: AffectedRows;
|
||||
@@ -1415,6 +1423,7 @@ export type Mutation = {
|
||||
deleteOneObject: ObjectDeleteResponse;
|
||||
deleteOnePipelineStage: PipelineStage;
|
||||
deleteOneView: View;
|
||||
deleteOneWebHook: WebHook;
|
||||
deleteUserAccount: User;
|
||||
deleteWorkspaceMember: WorkspaceMember;
|
||||
impersonate: Verify;
|
||||
@@ -1559,6 +1568,11 @@ export type MutationCreateOneViewFieldArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateOneWebHookArgs = {
|
||||
data: WebHookCreateInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteFavoriteArgs = {
|
||||
where: FavoriteWhereInput;
|
||||
};
|
||||
@@ -1609,6 +1623,11 @@ export type MutationDeleteOneViewArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteOneWebHookArgs = {
|
||||
where: WebHookWhereUniqueInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteWorkspaceMemberArgs = {
|
||||
where: WorkspaceMemberWhereUniqueInput;
|
||||
};
|
||||
@@ -2523,6 +2542,7 @@ export type Query = {
|
||||
findManyViewField: Array<ViewField>;
|
||||
findManyViewFilter: Array<ViewFilter>;
|
||||
findManyViewSort: Array<ViewSort>;
|
||||
findManyWebHook: Array<WebHook>;
|
||||
findManyWorkspaceMember: Array<WorkspaceMember>;
|
||||
findUniqueCompany: Company;
|
||||
findUniquePerson: Person;
|
||||
@@ -2662,6 +2682,16 @@ export type QueryFindManyViewSortArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryFindManyWebHookArgs = {
|
||||
cursor?: InputMaybe<WebHookWhereUniqueInput>;
|
||||
distinct?: InputMaybe<Array<WebHookScalarFieldEnum>>;
|
||||
orderBy?: InputMaybe<Array<WebHookOrderByWithRelationInput>>;
|
||||
skip?: InputMaybe<Scalars['Int']>;
|
||||
take?: InputMaybe<Scalars['Int']>;
|
||||
where?: InputMaybe<WebHookWhereInput>;
|
||||
};
|
||||
|
||||
|
||||
export type QueryFindManyWorkspaceMemberArgs = {
|
||||
cursor?: InputMaybe<WorkspaceMemberWhereUniqueInput>;
|
||||
distinct?: InputMaybe<Array<WorkspaceMemberScalarFieldEnum>>;
|
||||
@@ -3392,6 +3422,62 @@ export type ViewWhereUniqueInput = {
|
||||
id?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type WebHook = {
|
||||
__typename?: 'WebHook';
|
||||
createdAt: Scalars['DateTime'];
|
||||
id: Scalars['ID'];
|
||||
operation: Scalars['String'];
|
||||
targetUrl: Scalars['String'];
|
||||
updatedAt: Scalars['DateTime'];
|
||||
};
|
||||
|
||||
export type WebHookCreateInput = {
|
||||
createdAt?: InputMaybe<Scalars['DateTime']>;
|
||||
id?: InputMaybe<Scalars['String']>;
|
||||
operation: Scalars['String'];
|
||||
targetUrl: Scalars['String'];
|
||||
updatedAt?: InputMaybe<Scalars['DateTime']>;
|
||||
};
|
||||
|
||||
export type WebHookOrderByWithRelationInput = {
|
||||
createdAt?: InputMaybe<SortOrder>;
|
||||
id?: InputMaybe<SortOrder>;
|
||||
operation?: InputMaybe<SortOrder>;
|
||||
targetUrl?: InputMaybe<SortOrder>;
|
||||
updatedAt?: InputMaybe<SortOrder>;
|
||||
};
|
||||
|
||||
export enum WebHookScalarFieldEnum {
|
||||
CreatedAt = 'createdAt',
|
||||
DeletedAt = 'deletedAt',
|
||||
Id = 'id',
|
||||
Operation = 'operation',
|
||||
TargetUrl = 'targetUrl',
|
||||
UpdatedAt = 'updatedAt',
|
||||
WorkspaceId = 'workspaceId'
|
||||
}
|
||||
|
||||
export type WebHookUpdateManyWithoutWorkspaceNestedInput = {
|
||||
connect?: InputMaybe<Array<WebHookWhereUniqueInput>>;
|
||||
disconnect?: InputMaybe<Array<WebHookWhereUniqueInput>>;
|
||||
set?: InputMaybe<Array<WebHookWhereUniqueInput>>;
|
||||
};
|
||||
|
||||
export type WebHookWhereInput = {
|
||||
AND?: InputMaybe<Array<WebHookWhereInput>>;
|
||||
NOT?: InputMaybe<Array<WebHookWhereInput>>;
|
||||
OR?: InputMaybe<Array<WebHookWhereInput>>;
|
||||
createdAt?: InputMaybe<DateTimeFilter>;
|
||||
id?: InputMaybe<StringFilter>;
|
||||
operation?: InputMaybe<StringFilter>;
|
||||
targetUrl?: InputMaybe<StringFilter>;
|
||||
updatedAt?: InputMaybe<DateTimeFilter>;
|
||||
};
|
||||
|
||||
export type WebHookWhereUniqueInput = {
|
||||
id?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type Workspace = {
|
||||
__typename?: 'Workspace';
|
||||
Attachment?: Maybe<Array<Attachment>>;
|
||||
@@ -3415,6 +3501,7 @@ export type Workspace = {
|
||||
viewFilters?: Maybe<Array<ViewFilter>>;
|
||||
viewSorts?: Maybe<Array<ViewSort>>;
|
||||
views?: Maybe<Array<View>>;
|
||||
webHooks?: Maybe<Array<WebHook>>;
|
||||
workspaceMember?: Maybe<Array<WorkspaceMember>>;
|
||||
};
|
||||
|
||||
@@ -3590,6 +3677,7 @@ export type WorkspaceUpdateInput = {
|
||||
viewFilters?: InputMaybe<ViewFilterUpdateManyWithoutWorkspaceNestedInput>;
|
||||
viewSorts?: InputMaybe<ViewSortUpdateManyWithoutWorkspaceNestedInput>;
|
||||
views?: InputMaybe<ViewUpdateManyWithoutWorkspaceNestedInput>;
|
||||
webHooks?: InputMaybe<WebHookUpdateManyWithoutWorkspaceNestedInput>;
|
||||
workspaceMember?: InputMaybe<WorkspaceMemberUpdateManyWithoutWorkspaceNestedInput>;
|
||||
};
|
||||
|
||||
@@ -4118,6 +4206,27 @@ export type SearchUserQueryVariables = Exact<{
|
||||
|
||||
export type SearchUserQuery = { __typename?: 'Query', searchResults: Array<{ __typename?: 'User', avatarUrl?: string | null, id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null }> };
|
||||
|
||||
export type DeleteOneApiKeyMutationVariables = Exact<{
|
||||
apiKeyId: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type DeleteOneApiKeyMutation = { __typename?: 'Mutation', revokeOneApiKey: { __typename?: 'ApiKey', id: string } };
|
||||
|
||||
export type InsertOneApiKeyMutationVariables = Exact<{
|
||||
data: ApiKeyCreateInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type InsertOneApiKeyMutation = { __typename?: 'Mutation', createOneApiKey: { __typename?: 'ApiKeyToken', id: string, token: string, expiresAt: string } };
|
||||
|
||||
export type GetApiKeyQueryVariables = Exact<{
|
||||
apiKeyId: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetApiKeyQuery = { __typename?: 'Query', findManyApiKey: Array<{ __typename?: 'ApiKey', id: string, name: string, expiresAt?: string | null }> };
|
||||
|
||||
export type UserFieldsFragmentFragment = { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null };
|
||||
|
||||
export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>;
|
||||
@@ -6771,6 +6880,111 @@ export function useSearchUserLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions
|
||||
export type SearchUserQueryHookResult = ReturnType<typeof useSearchUserQuery>;
|
||||
export type SearchUserLazyQueryHookResult = ReturnType<typeof useSearchUserLazyQuery>;
|
||||
export type SearchUserQueryResult = Apollo.QueryResult<SearchUserQuery, SearchUserQueryVariables>;
|
||||
export const DeleteOneApiKeyDocument = gql`
|
||||
mutation DeleteOneApiKey($apiKeyId: String!) {
|
||||
revokeOneApiKey(where: {id: $apiKeyId}) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type DeleteOneApiKeyMutationFn = Apollo.MutationFunction<DeleteOneApiKeyMutation, DeleteOneApiKeyMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useDeleteOneApiKeyMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useDeleteOneApiKeyMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useDeleteOneApiKeyMutation` 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 [deleteOneApiKeyMutation, { data, loading, error }] = useDeleteOneApiKeyMutation({
|
||||
* variables: {
|
||||
* apiKeyId: // value for 'apiKeyId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useDeleteOneApiKeyMutation(baseOptions?: Apollo.MutationHookOptions<DeleteOneApiKeyMutation, DeleteOneApiKeyMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<DeleteOneApiKeyMutation, DeleteOneApiKeyMutationVariables>(DeleteOneApiKeyDocument, options);
|
||||
}
|
||||
export type DeleteOneApiKeyMutationHookResult = ReturnType<typeof useDeleteOneApiKeyMutation>;
|
||||
export type DeleteOneApiKeyMutationResult = Apollo.MutationResult<DeleteOneApiKeyMutation>;
|
||||
export type DeleteOneApiKeyMutationOptions = Apollo.BaseMutationOptions<DeleteOneApiKeyMutation, DeleteOneApiKeyMutationVariables>;
|
||||
export const InsertOneApiKeyDocument = gql`
|
||||
mutation InsertOneApiKey($data: ApiKeyCreateInput!) {
|
||||
createOneApiKey(data: $data) {
|
||||
id
|
||||
token
|
||||
expiresAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type InsertOneApiKeyMutationFn = Apollo.MutationFunction<InsertOneApiKeyMutation, InsertOneApiKeyMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useInsertOneApiKeyMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useInsertOneApiKeyMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useInsertOneApiKeyMutation` 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 [insertOneApiKeyMutation, { data, loading, error }] = useInsertOneApiKeyMutation({
|
||||
* variables: {
|
||||
* data: // value for 'data'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useInsertOneApiKeyMutation(baseOptions?: Apollo.MutationHookOptions<InsertOneApiKeyMutation, InsertOneApiKeyMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<InsertOneApiKeyMutation, InsertOneApiKeyMutationVariables>(InsertOneApiKeyDocument, options);
|
||||
}
|
||||
export type InsertOneApiKeyMutationHookResult = ReturnType<typeof useInsertOneApiKeyMutation>;
|
||||
export type InsertOneApiKeyMutationResult = Apollo.MutationResult<InsertOneApiKeyMutation>;
|
||||
export type InsertOneApiKeyMutationOptions = Apollo.BaseMutationOptions<InsertOneApiKeyMutation, InsertOneApiKeyMutationVariables>;
|
||||
export const GetApiKeyDocument = gql`
|
||||
query GetApiKey($apiKeyId: String!) {
|
||||
findManyApiKey(where: {id: {equals: $apiKeyId}}) {
|
||||
id
|
||||
name
|
||||
expiresAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useGetApiKeyQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useGetApiKeyQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useGetApiKeyQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useGetApiKeyQuery({
|
||||
* variables: {
|
||||
* apiKeyId: // value for 'apiKeyId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useGetApiKeyQuery(baseOptions: Apollo.QueryHookOptions<GetApiKeyQuery, GetApiKeyQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<GetApiKeyQuery, GetApiKeyQueryVariables>(GetApiKeyDocument, options);
|
||||
}
|
||||
export function useGetApiKeyLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetApiKeyQuery, GetApiKeyQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<GetApiKeyQuery, GetApiKeyQueryVariables>(GetApiKeyDocument, options);
|
||||
}
|
||||
export type GetApiKeyQueryHookResult = ReturnType<typeof useGetApiKeyQuery>;
|
||||
export type GetApiKeyLazyQueryHookResult = ReturnType<typeof useGetApiKeyLazyQuery>;
|
||||
export type GetApiKeyQueryResult = Apollo.QueryResult<GetApiKeyQuery, GetApiKeyQueryVariables>;
|
||||
export const DeleteUserAccountDocument = gql`
|
||||
mutation DeleteUserAccount {
|
||||
deleteUserAccount {
|
||||
|
||||
@@ -24,7 +24,7 @@ export const CompanyBoard = ({
|
||||
onEditColumnTitle,
|
||||
}: CompanyBoardProps) => {
|
||||
// TODO: we can store objectId and fieldDefinitions in the ViewBarContext
|
||||
// And then use the useBoardViews hook wherever we need it in the board
|
||||
// And then use the useBoardViews web-hook wherever we need it in the board
|
||||
const { createView, deleteView, submitCurrentView, updateView } =
|
||||
useBoardViews({
|
||||
objectId: 'company',
|
||||
|
||||
@@ -41,7 +41,7 @@ export const SettingsNavbar = () => {
|
||||
end: false,
|
||||
});
|
||||
const isDevelopersSettingsActive = !!useMatch({
|
||||
path: useResolvedPath('/settings/api').pathname,
|
||||
path: useResolvedPath('/settings/developers/api-keys').pathname,
|
||||
end: true,
|
||||
});
|
||||
|
||||
@@ -104,7 +104,7 @@ export const SettingsNavbar = () => {
|
||||
{isDevelopersSettingsEnabled && (
|
||||
<NavItem
|
||||
label="Developers"
|
||||
to="/settings/apis"
|
||||
to="/settings/developers/api-keys"
|
||||
Icon={IconRobot}
|
||||
active={isDevelopersSettingsActive}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { IconCopy } from '@/ui/display/icon';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar/hooks/useSnackBar';
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { beautifyDateDiff } from '~/utils/date-utils';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
const StyledLinkContainer = styled.div`
|
||||
flex: 1;
|
||||
margin-right: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
type ApiKeyInputProps = { expiresAt?: string | null; apiKey: string };
|
||||
|
||||
export const ApiKeyInput = ({ expiresAt, apiKey }: ApiKeyInputProps) => {
|
||||
const theme = useTheme();
|
||||
const computeInfo = () => {
|
||||
if (!expiresAt) {
|
||||
return '';
|
||||
}
|
||||
return `This key will expire in ${beautifyDateDiff(expiresAt)}`;
|
||||
};
|
||||
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledLinkContainer>
|
||||
<TextInput info={computeInfo()} value={apiKey} fullWidth />
|
||||
</StyledLinkContainer>
|
||||
<Button
|
||||
Icon={IconCopy}
|
||||
title="Copy"
|
||||
onClick={() => {
|
||||
enqueueSnackBar('Api Key copied to clipboard', {
|
||||
variant: 'success',
|
||||
icon: <IconCopy size={theme.icon.size.md} />,
|
||||
duration: 2000,
|
||||
});
|
||||
navigator.clipboard.writeText(apiKey);
|
||||
}}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@@ -25,7 +25,7 @@ const StyledIconChevronRight = styled(IconChevronRight)`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
`;
|
||||
|
||||
export const SettingsApisFieldItemTableRow = ({
|
||||
export const SettingsApiKeysFieldItemTableRow = ({
|
||||
fieldItem,
|
||||
}: {
|
||||
fieldItem: ApisFiedlItem;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { ApiKeyInput } from '@/settings/developers/components/ApiKeyInput';
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
|
||||
const meta: Meta<typeof ApiKeyInput> = {
|
||||
title: 'Pages/Settings/Developers/ApiKeys/ApiKeyInput',
|
||||
component: ApiKeyInput,
|
||||
decorators: [ComponentDecorator],
|
||||
args: {
|
||||
expiresAt: '2123-11-06T23:59:59.825Z',
|
||||
apiKey:
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0d2VudHktN2VkOWQyMTItMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNjk4MTQyODgyLCJleHAiOjE2OTk0MDE1OTksImp0aSI6ImMyMmFiNjQxLTVhOGYtNGQwMC1iMDkzLTk3MzUwYTM2YzZkOSJ9.JIe2TX5IXrdNl3n-kRFp3jyfNUE7unzXZLAzm2Gxl98',
|
||||
},
|
||||
};
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ApiKeyInput>;
|
||||
|
||||
export const Default: Story = {};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { SettingsApiKeysFieldItemTableRow } from '@/settings/developers/components/SettingsApiKeysFieldItemTableRow';
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
|
||||
const meta: Meta<typeof SettingsApiKeysFieldItemTableRow> = {
|
||||
title: 'Pages/Settings/Developers/ApiKeys/SettingsApiKeysFieldItemTableRow',
|
||||
component: SettingsApiKeysFieldItemTableRow,
|
||||
decorators: [ComponentDecorator],
|
||||
args: {
|
||||
fieldItem: {
|
||||
id: '3f4a42e8-b81f-4f8c-9c20-1602e6b34791',
|
||||
name: 'Zapier Api Key',
|
||||
type: 'internal',
|
||||
expiration: 'In 3 days',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof SettingsApiKeysFieldItemTableRow>;
|
||||
|
||||
export const Default: Story = {};
|
||||
@@ -0,0 +1,11 @@
|
||||
export const ExpirationDates: {
|
||||
value: number;
|
||||
label: string;
|
||||
}[] = [
|
||||
{ label: '15 days', value: 15 },
|
||||
{ label: '30 days', value: 30 },
|
||||
{ label: '90 days', value: 90 },
|
||||
{ label: '1 year', value: 365 },
|
||||
{ label: '2 years', value: 2 * 365 },
|
||||
{ label: 'Never', value: 10 * 365 },
|
||||
];
|
||||
@@ -0,0 +1,9 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const DELETE_ONE_API_KEY = gql`
|
||||
mutation DeleteOneApiKey($apiKeyId: String!) {
|
||||
revokeOneApiKey(where: { id: $apiKeyId }) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,11 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const INSERT_ONE_API_KEY = gql`
|
||||
mutation InsertOneApiKey($data: ApiKeyCreateInput!) {
|
||||
createOneApiKey(data: $data) {
|
||||
id
|
||||
token
|
||||
expiresAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,11 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_API_KEY = gql`
|
||||
query GetApiKey($apiKeyId: String!) {
|
||||
findManyApiKey(where: { id: { equals: $apiKeyId } }) {
|
||||
id
|
||||
name
|
||||
expiresAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const generatedApiKeyState = atom<string | null | undefined>({
|
||||
key: 'generatedApiKeyState',
|
||||
default: null,
|
||||
});
|
||||
@@ -36,7 +36,7 @@ export const NameFields = ({
|
||||
|
||||
const [updateUser] = useUpdateUserMutation();
|
||||
|
||||
// TODO: Enhance this with react-hook-form (https://www.react-hook-form.com)
|
||||
// TODO: Enhance this with react-web-hook-form (https://www.react-hook-form.com)
|
||||
const debouncedUpdate = debounce(async () => {
|
||||
if (onFirstNameUpdate) {
|
||||
onFirstNameUpdate(firstName);
|
||||
|
||||
@@ -34,7 +34,7 @@ export const NameField = ({
|
||||
|
||||
const [updateWorkspace] = useUpdateWorkspaceMutation();
|
||||
|
||||
// TODO: Enhance this with react-hook-form (https://www.react-hook-form.com)
|
||||
// TODO: Enhance this with react-web-hook-form (https://www.react-hook-form.com)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const debouncedUpdate = useCallback(
|
||||
debounce(async (name: string) => {
|
||||
|
||||
@@ -20,6 +20,7 @@ export enum AppPath {
|
||||
ObjectTablePage = '/objects/:objectNamePlural',
|
||||
|
||||
SettingsCatchAll = `/settings/*`,
|
||||
DevelopersCatchAll = `/developers/*`,
|
||||
|
||||
// Impersonate
|
||||
Impersonate = '/impersonate/:userId',
|
||||
|
||||
@@ -9,5 +9,7 @@ export enum SettingsPath {
|
||||
NewObject = 'objects/new',
|
||||
WorkspaceMembersPage = 'workspace-members',
|
||||
Workspace = 'workspace',
|
||||
Apis = 'apis',
|
||||
Developers = 'api-keys',
|
||||
DevelopersNewApiKey = 'api-keys/new',
|
||||
DevelopersApiKeyDetail = 'api-keys/:apiKeyId',
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { FieldMetadata } from '../types/FieldMetadata';
|
||||
|
||||
export type GenericFieldContextType = {
|
||||
fieldDefinition: FieldDefinition<FieldMetadata>;
|
||||
// TODO: add better typing for mutation hook
|
||||
// TODO: add better typing for mutation web-hook
|
||||
useUpdateEntityMutation: () => [(params: any) => void, any];
|
||||
entityId: string;
|
||||
recoilScopeId: string;
|
||||
|
||||
@@ -11,7 +11,7 @@ import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
|
||||
import { SelectHotkeyScope } from '../types/SelectHotkeyScope';
|
||||
|
||||
export type SelectProps<Value extends string> = {
|
||||
export type SelectProps<Value extends string | number> = {
|
||||
dropdownScopeId: string;
|
||||
onChange: (value: Value) => void;
|
||||
options: { value: Value; label: string; Icon?: IconComponent }[];
|
||||
@@ -38,7 +38,7 @@ const StyledLabel = styled.div`
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
export const Select = <Value extends string>({
|
||||
export const Select = <Value extends string | number>({
|
||||
dropdownScopeId,
|
||||
onChange,
|
||||
options,
|
||||
|
||||
@@ -25,6 +25,7 @@ export type TextInputComponentProps = Omit<
|
||||
> & {
|
||||
className?: string;
|
||||
label?: string;
|
||||
info?: string;
|
||||
onChange?: (text: string) => void;
|
||||
fullWidth?: boolean;
|
||||
disableHotkeys?: boolean;
|
||||
@@ -45,10 +46,16 @@ const StyledLabel = styled.span`
|
||||
text-transform: uppercase;
|
||||
`;
|
||||
|
||||
const StyledInfo = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
margin-top: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledInputContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
@@ -113,6 +120,7 @@ const TextInputComponent = (
|
||||
{
|
||||
className,
|
||||
label,
|
||||
info,
|
||||
value,
|
||||
onChange,
|
||||
onFocus,
|
||||
@@ -204,6 +212,7 @@ const TextInputComponent = (
|
||||
)}
|
||||
</StyledTrailingIconContainer>
|
||||
</StyledInputContainer>
|
||||
{info && <StyledInfo>{info}</StyledInfo>}
|
||||
{error && <StyledErrorHelper>{error}</StyledErrorHelper>}
|
||||
</StyledContainer>
|
||||
);
|
||||
|
||||
@@ -6,11 +6,11 @@ import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
|
||||
import { Select, SelectProps } from '../Select';
|
||||
|
||||
type RenderProps = SelectProps<string>;
|
||||
type RenderProps = SelectProps<string | number>;
|
||||
|
||||
const Render = (args: RenderProps) => {
|
||||
const [value, setValue] = useState(args.value);
|
||||
const handleChange = (value: string) => {
|
||||
const handleChange = (value: string | number) => {
|
||||
args.onChange?.(value);
|
||||
setValue(value);
|
||||
};
|
||||
|
||||
@@ -38,3 +38,7 @@ export const Filled: Story = {
|
||||
export const Disabled: Story = {
|
||||
args: { disabled: true, value: 'Tim' },
|
||||
};
|
||||
|
||||
export const WithInfo: Story = {
|
||||
args: { info: 'Some info displayed below the input', value: 'Tim' },
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import { companyProgressesFamilyState } from '@/companies/states/companyProgress
|
||||
|
||||
import { boardCardIdsByColumnIdFamilyState } from '../boardCardIdsByColumnIdFamilyState';
|
||||
|
||||
// TODO: this state should be computed during the synchronization hook and put in a generic
|
||||
// TODO: this state should be computed during the synchronization web-hook and put in a generic
|
||||
// boardColumnTotalsFamilyState indexed by columnId.
|
||||
export const boardColumnTotalsFamilySelector = selectorFamily({
|
||||
key: 'boardColumnTotalsFamilySelector',
|
||||
|
||||
@@ -22,7 +22,7 @@ const TestComponentDomMode = () => {
|
||||
);
|
||||
};
|
||||
|
||||
test('useListenClickOutside hook works in dom mode', async () => {
|
||||
test('useListenClickOutside web-hook works in dom mode', async () => {
|
||||
const { getByText } = render(<TestComponentDomMode />);
|
||||
const inside = getByText('Inside');
|
||||
const inside2 = getByText('Inside 2');
|
||||
|
||||
@@ -136,7 +136,7 @@ export const CreateProfile = () => {
|
||||
title="Name"
|
||||
description="Your name as it will be displayed on the app"
|
||||
/>
|
||||
{/* TODO: When react-hook-form is added to edit page we should create a dedicated component with context */}
|
||||
{/* TODO: When react-web-hook-form is added to edit page we should create a dedicated component with context */}
|
||||
<StyledComboInputContainer>
|
||||
<Controller
|
||||
name="firstName"
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import {
|
||||
PageDecorator,
|
||||
PageDecoratorArgs,
|
||||
} from '~/testing/decorators/PageDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
|
||||
import { SettingsApis } from '../SettingsApis';
|
||||
|
||||
const meta: Meta<PageDecoratorArgs> = {
|
||||
title: 'Pages/Settings/SettingsApi',
|
||||
component: SettingsApis,
|
||||
decorators: [PageDecorator],
|
||||
args: { routePath: '/settings/apis' },
|
||||
parameters: {
|
||||
msw: graphqlMocks,
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export type Story = StoryObj<typeof SettingsApis>;
|
||||
|
||||
export const Default: Story = {};
|
||||
@@ -0,0 +1,76 @@
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { ApiKeyInput } from '@/settings/developers/components/ApiKeyInput';
|
||||
import { generatedApiKeyState } from '@/settings/developers/states/generatedApiKeyState';
|
||||
import { 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 {
|
||||
useDeleteOneApiKeyMutation,
|
||||
useGetApiKeyQuery,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
export const SettingsDevelopersApiKeyDetail = () => {
|
||||
const navigate = useNavigate();
|
||||
const { apiKeyId = '' } = useParams();
|
||||
const [generatedApiKey] = useRecoilState(generatedApiKeyState);
|
||||
const apiKeyQuery = useGetApiKeyQuery({
|
||||
variables: {
|
||||
apiKeyId,
|
||||
},
|
||||
});
|
||||
const [deleteApiKey] = useDeleteOneApiKeyMutation();
|
||||
const deleteIntegration = async () => {
|
||||
await deleteApiKey({ variables: { apiKeyId } });
|
||||
navigate('/settings/developers/api-keys');
|
||||
};
|
||||
const { expiresAt, name } = apiKeyQuery.data?.findManyApiKey[0] || {};
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
||||
<SettingsPageContainer>
|
||||
<SettingsHeaderContainer>
|
||||
<Breadcrumb
|
||||
links={[
|
||||
{ children: 'APIs', href: '/settings/developers/api-keys' },
|
||||
{ children: name || '' },
|
||||
]}
|
||||
/>
|
||||
</SettingsHeaderContainer>
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Api Key"
|
||||
description="Copy this key as it will only be visible this one time"
|
||||
/>
|
||||
<ApiKeyInput expiresAt={expiresAt} apiKey={generatedApiKey || ''} />
|
||||
</Section>
|
||||
<Section>
|
||||
<H2Title title="Name" description="Name of your API key" />
|
||||
<TextInput
|
||||
placeholder="E.g. backoffice integration"
|
||||
value={name || ''}
|
||||
disabled={true}
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -2,7 +2,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { objectSettingsWidth } from '@/settings/data-model/constants/objectSettings';
|
||||
import { SettingsApisFieldItemTableRow } from '@/settings/developers/components/SettingsApisFieldItemTableRow';
|
||||
import { SettingsApiKeysFieldItemTableRow } from '@/settings/developers/components/SettingsApiKeysFieldItemTableRow';
|
||||
import { activeApiKeyItems } from '@/settings/developers/constants/mockObjects';
|
||||
import { IconPlus, IconSettings } from '@/ui/display/icon';
|
||||
import { H1Title } from '@/ui/display/typography/components/H1Title';
|
||||
@@ -34,7 +34,7 @@ const StyledH1Title = styled(H1Title)`
|
||||
margin-bottom: 0;
|
||||
`;
|
||||
|
||||
export const SettingsApis = () => {
|
||||
export const SettingsDevelopersApiKeys = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
@@ -48,7 +48,7 @@ export const SettingsApis = () => {
|
||||
accent="blue"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
navigate('/');
|
||||
navigate('/settings/developers/api-keys/new');
|
||||
}}
|
||||
/>
|
||||
</StyledHeader>
|
||||
@@ -64,7 +64,7 @@ export const SettingsApis = () => {
|
||||
<TableHeader></TableHeader>
|
||||
</StyledTableRow>
|
||||
{activeApiKeyItems.map((fieldItem) => (
|
||||
<SettingsApisFieldItemTableRow
|
||||
<SettingsApiKeysFieldItemTableRow
|
||||
key={fieldItem.id}
|
||||
fieldItem={fieldItem}
|
||||
/>
|
||||
@@ -0,0 +1,100 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { DateTime } from 'luxon';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { ExpirationDates } from '@/settings/developers/constants/expirationDates';
|
||||
import { generatedApiKeyState } from '@/settings/developers/states/generatedApiKeyState';
|
||||
import { IconSettings } from '@/ui/display/icon';
|
||||
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
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 { useInsertOneApiKeyMutation } from '~/generated/graphql';
|
||||
|
||||
export const SettingsDevelopersApiKeysNew = () => {
|
||||
const [insertOneApiKey] = useInsertOneApiKeyMutation();
|
||||
const navigate = useNavigate();
|
||||
const [, setGeneratedApiKey] = useRecoilState(generatedApiKeyState);
|
||||
const [formValues, setFormValues] = useState<{
|
||||
name: string;
|
||||
expirationDate: number;
|
||||
}>({
|
||||
expirationDate: ExpirationDates[0].value,
|
||||
name: '',
|
||||
});
|
||||
const onSave = async () => {
|
||||
const apiKey = await insertOneApiKey({
|
||||
variables: {
|
||||
data: {
|
||||
name: formValues.name,
|
||||
expiresAt: DateTime.now()
|
||||
.plus({ days: formValues.expirationDate })
|
||||
.toISODate(),
|
||||
},
|
||||
},
|
||||
});
|
||||
setGeneratedApiKey(apiKey.data?.createOneApiKey?.token);
|
||||
navigate(
|
||||
`/settings/developers/api-keys/${apiKey.data?.createOneApiKey?.id}`,
|
||||
);
|
||||
};
|
||||
const canSave = !!formValues.name;
|
||||
return (
|
||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
||||
<SettingsPageContainer>
|
||||
<SettingsHeaderContainer>
|
||||
<Breadcrumb
|
||||
links={[
|
||||
{ children: 'APIs', href: '/settings/developers/api-keys' },
|
||||
{ children: 'New' },
|
||||
]}
|
||||
/>
|
||||
<SaveAndCancelButtons
|
||||
isSaveDisabled={!canSave}
|
||||
onCancel={() => {
|
||||
navigate('/settings/developers/api-keys');
|
||||
}}
|
||||
onSave={onSave}
|
||||
/>
|
||||
</SettingsHeaderContainer>
|
||||
<Section>
|
||||
<H2Title title="Name" description="Name of your API key" />
|
||||
<TextInput
|
||||
placeholder="E.g. backoffice integration"
|
||||
value={formValues.name}
|
||||
onChange={(value) => {
|
||||
setFormValues((prevState) => ({
|
||||
...prevState,
|
||||
name: value,
|
||||
}));
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
</Section>
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Expiration Date"
|
||||
description="When the API key will expire."
|
||||
/>
|
||||
<Select
|
||||
dropdownScopeId="object-field-type-select"
|
||||
options={ExpirationDates}
|
||||
value={formValues.expirationDate}
|
||||
onChange={(value) => {
|
||||
setFormValues((prevState) => ({
|
||||
...prevState,
|
||||
expirationDate: value,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</Section>
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { SettingsDevelopersApiKeys } from '~/pages/settings/developers/api-keys/SettingsDevelopersApiKeys';
|
||||
import {
|
||||
PageDecorator,
|
||||
PageDecoratorArgs,
|
||||
} from '~/testing/decorators/PageDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { sleep } from '~/testing/sleep';
|
||||
|
||||
const meta: Meta<PageDecoratorArgs> = {
|
||||
title: 'Pages/Settings/Developers/ApiKeys/SettingsDevelopersApiKeys',
|
||||
component: SettingsDevelopersApiKeys,
|
||||
decorators: [PageDecorator],
|
||||
args: { routePath: '/settings/developers/api-keys' },
|
||||
parameters: {
|
||||
msw: graphqlMocks,
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export type Story = StoryObj<typeof SettingsDevelopersApiKeys>;
|
||||
|
||||
export const Default: Story = {
|
||||
play: async ({}) => {
|
||||
await sleep(100);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { SettingsDevelopersApiKeyDetail } from '~/pages/settings/developers/api-keys/SettingsDevelopersApiKeyDetail';
|
||||
import {
|
||||
PageDecorator,
|
||||
PageDecoratorArgs,
|
||||
} from '~/testing/decorators/PageDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { mockedApiKeyToken } from '~/testing/mock-data/api-keys';
|
||||
import { sleep } from '~/testing/sleep';
|
||||
|
||||
const meta: Meta<PageDecoratorArgs> = {
|
||||
title: 'Pages/Settings/Developers/ApiKeys/SettingsDevelopersApiKeyDetail',
|
||||
component: SettingsDevelopersApiKeyDetail,
|
||||
decorators: [PageDecorator],
|
||||
args: {
|
||||
routePath: '/settings/apis/f7c6d736-8fcd-4e9c-ab99-28f6a9031570',
|
||||
state: mockedApiKeyToken,
|
||||
},
|
||||
parameters: {
|
||||
msw: graphqlMocks,
|
||||
},
|
||||
};
|
||||
export default meta;
|
||||
|
||||
export type Story = StoryObj<typeof SettingsDevelopersApiKeyDetail>;
|
||||
|
||||
export const Default: Story = {
|
||||
play: async ({}) => {
|
||||
await sleep(100);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { SettingsDevelopersApiKeysNew } from '~/pages/settings/developers/api-keys/SettingsDevelopersApiKeysNew';
|
||||
import {
|
||||
PageDecorator,
|
||||
PageDecoratorArgs,
|
||||
} from '~/testing/decorators/PageDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { sleep } from '~/testing/sleep';
|
||||
|
||||
const meta: Meta<PageDecoratorArgs> = {
|
||||
title: 'Pages/Settings/Developers/ApiKeys/SettingsDevelopersApiKeysNew',
|
||||
component: SettingsDevelopersApiKeysNew,
|
||||
decorators: [PageDecorator],
|
||||
args: { routePath: '/settings/developers/api-keys/new' },
|
||||
parameters: {
|
||||
msw: graphqlMocks,
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export type Story = StoryObj<typeof SettingsDevelopersApiKeysNew>;
|
||||
|
||||
export const Default: Story = {
|
||||
play: async ({}) => {
|
||||
await sleep(100);
|
||||
},
|
||||
};
|
||||
@@ -8,23 +8,41 @@ import { UserProvider } from '~/modules/users/components/UserProvider';
|
||||
|
||||
import { FullHeightStorybookLayout } from '../FullHeightStorybookLayout';
|
||||
|
||||
export type PageDecoratorArgs = { routePath: string; routeParams: RouteParams };
|
||||
export type PageDecoratorArgs = {
|
||||
routePath: string;
|
||||
routeParams: RouteParams;
|
||||
state?: string;
|
||||
};
|
||||
|
||||
type RouteParams = {
|
||||
[param: string]: string;
|
||||
};
|
||||
|
||||
const computeLocation = (routePath: string, routeParams: RouteParams) =>
|
||||
routePath.replace(/:(\w+)/g, (paramName) => routeParams[paramName] ?? '');
|
||||
const computeLocation = (
|
||||
routePath: string,
|
||||
routeParams: RouteParams,
|
||||
state?: string,
|
||||
) => {
|
||||
return {
|
||||
pathname: routePath.replace(
|
||||
/:(\w+)/g,
|
||||
(paramName) => routeParams[paramName] ?? '',
|
||||
),
|
||||
state,
|
||||
};
|
||||
};
|
||||
|
||||
export const PageDecorator: Decorator<{
|
||||
routePath: string;
|
||||
routeParams: RouteParams;
|
||||
state?: string;
|
||||
}> = (Story, { args }) => (
|
||||
<UserProvider>
|
||||
<ClientConfigProvider>
|
||||
<MemoryRouter
|
||||
initialEntries={[computeLocation(args.routePath, args.routeParams)]}
|
||||
initialEntries={[
|
||||
computeLocation(args.routePath, args.routeParams, args.state),
|
||||
]}
|
||||
>
|
||||
<FullHeightStorybookLayout>
|
||||
<HelmetProvider>
|
||||
|
||||
@@ -17,6 +17,7 @@ import { SEARCH_ACTIVITY_QUERY } from '@/search/graphql/queries/searchActivityQu
|
||||
import { SEARCH_COMPANY_QUERY } from '@/search/graphql/queries/searchCompanyQuery';
|
||||
import { SEARCH_PEOPLE_QUERY } from '@/search/graphql/queries/searchPeopleQuery';
|
||||
import { SEARCH_USER_QUERY } from '@/search/graphql/queries/searchUserQuery';
|
||||
import { GET_API_KEY } from '@/settings/developers/graphql/queries/getApiKey';
|
||||
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
|
||||
import { GET_VIEW_FIELDS } from '@/views/graphql/queries/getViewFields';
|
||||
import { GET_VIEWS } from '@/views/graphql/queries/getViews';
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
SearchUserQuery,
|
||||
ViewType,
|
||||
} from '~/generated/graphql';
|
||||
import { mockedApiKeys } from '~/testing/mock-data/api-keys';
|
||||
|
||||
import { mockedActivities, mockedTasks } from './mock-data/activities';
|
||||
import {
|
||||
@@ -278,6 +280,13 @@ export const graphqlMocks = [
|
||||
}),
|
||||
);
|
||||
}),
|
||||
graphql.query(getOperationName(GET_API_KEY) ?? '', (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.data({
|
||||
findManyApiKey: mockedApiKeys[0],
|
||||
}),
|
||||
);
|
||||
}),
|
||||
graphql.mutation(
|
||||
getOperationName(CREATE_ACTIVITY_WITH_COMMENT) ?? '',
|
||||
(req, res, ctx) => {
|
||||
|
||||
18
front/src/testing/mock-data/api-keys.ts
Normal file
18
front/src/testing/mock-data/api-keys.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { ApiKey } from '~/generated/graphql';
|
||||
|
||||
type MockedApiKey = Pick<
|
||||
ApiKey,
|
||||
'id' | 'name' | 'createdAt' | 'updatedAt' | 'expiresAt' | '__typename'
|
||||
>;
|
||||
export const mockedApiKeyToken =
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0d2VudHktN2VkOWQyMTItMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNjk4MDkzMDU0LCJleHAiOjE2OTkzMTUxOTksImp0aSI6IjY0Njg3ZWNmLWFhYzktNDNmYi1hY2I4LTE1M2QzNzgwYmIzMSJ9.JkQ3u7aRiqOFQkgHcC-mgCU37096HRSo40A_9X8gEng';
|
||||
export const mockedApiKeys: Array<MockedApiKey> = [
|
||||
{
|
||||
id: 'f7c6d736-8fcd-4e9c-ab99-28f6a9031570',
|
||||
name: 'Zapier Integration',
|
||||
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
||||
updatedAt: '2023-04-26T10:23:42.33625+00:00',
|
||||
expiresAt: '2100-11-06T23:59:59.825Z',
|
||||
__typename: 'ApiKey',
|
||||
},
|
||||
];
|
||||
@@ -2,6 +2,7 @@ import { formatDistanceToNow } from 'date-fns';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import {
|
||||
beautifyDateDiff,
|
||||
beautifyExactDate,
|
||||
beautifyExactDateTime,
|
||||
beautifyPastDateAbsolute,
|
||||
@@ -237,3 +238,47 @@ describe('hasDatePassed', () => {
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('beautifyDateDiff', () => {
|
||||
it('should return the correct date diff', () => {
|
||||
const date = '2023-11-05T00:00:00.000Z';
|
||||
const dateToCompareWith = '2023-11-01T00:00:00.000Z';
|
||||
const result = beautifyDateDiff(date, dateToCompareWith);
|
||||
expect(result).toEqual('4 days');
|
||||
});
|
||||
it('should return the correct date diff for large diff', () => {
|
||||
const date = '2033-11-05T00:00:00.000Z';
|
||||
const dateToCompareWith = '2023-11-01T00:00:00.000Z';
|
||||
const result = beautifyDateDiff(date, dateToCompareWith);
|
||||
expect(result).toEqual('10 years and 4 days');
|
||||
});
|
||||
it('should return the correct date for negative diff', () => {
|
||||
const date = '2013-11-05T00:00:00.000Z';
|
||||
const dateToCompareWith = '2023-11-01T00:00:00.000Z';
|
||||
const result = beautifyDateDiff(date, dateToCompareWith);
|
||||
expect(result).toEqual('-9 years and -361 days');
|
||||
});
|
||||
it('should return the correct date diff for large diff', () => {
|
||||
const date = '2033-11-01T00:00:00.000Z';
|
||||
const dateToCompareWith = '2023-11-01T00:00:00.000Z';
|
||||
const result = beautifyDateDiff(date, dateToCompareWith);
|
||||
expect(result).toEqual('10 years');
|
||||
});
|
||||
it('should return the proper english date diff', () => {
|
||||
const date = '2024-11-02T00:00:00.000Z';
|
||||
const dateToCompareWith = '2023-11-01T00:00:00.000Z';
|
||||
const result = beautifyDateDiff(date, dateToCompareWith);
|
||||
expect(result).toEqual('1 year and 1 day');
|
||||
});
|
||||
it('should round date diff', () => {
|
||||
const date = '2024-11-03T14:04:43.421Z';
|
||||
const dateToCompareWith = '2023-11-01T00:00:00.000Z';
|
||||
const result = beautifyDateDiff(date, dateToCompareWith);
|
||||
expect(result).toEqual('1 year and 2 days');
|
||||
});
|
||||
it('should compare to now', () => {
|
||||
const date = '2200-11-01T00:00:00.000Z';
|
||||
const result = beautifyDateDiff(date);
|
||||
expect(result).toContain('years');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -108,3 +108,17 @@ export const hasDatePassed = (date: Date | string | number) => {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const beautifyDateDiff = (date: string, dateToCompareWith?: string) => {
|
||||
const dateDiff = DateTime.fromISO(date).diff(
|
||||
dateToCompareWith ? DateTime.fromISO(dateToCompareWith) : DateTime.now(),
|
||||
['years', 'days'],
|
||||
);
|
||||
let result = '';
|
||||
if (dateDiff.years) result = result + `${dateDiff.years} year`;
|
||||
if (![0, 1].includes(dateDiff.years)) result = result + 's';
|
||||
if (dateDiff.years && dateDiff.days) result = result + ' and ';
|
||||
if (dateDiff.days) result = result + `${Math.floor(dateDiff.days)} day`;
|
||||
if (![0, 1].includes(dateDiff.days)) result = result + 's';
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ export default {
|
||||
label: 'Api Key',
|
||||
type: 'string',
|
||||
helpText:
|
||||
'Create the api key in [your twenty workspace](https://app.twenty.com/settings/apis)',
|
||||
'Create the api-keys key in [your twenty workspace](https://app.twenty.com/settings/developers/api-keys)',
|
||||
},
|
||||
],
|
||||
connectionLabel: '{{data.currentWorkspace.displayName}}',
|
||||
|
||||
@@ -19,12 +19,12 @@ describe('triggers.company', () => {
|
||||
requestDb(
|
||||
z,
|
||||
bundle,
|
||||
`query findManyHook {findManyHook(where: {id: {equals: "${result.id}"}}){id operation}}`,
|
||||
`query findManyWebHook {findManyWebHook(where: {id: {equals: "${result.id}"}}){id operation}}`,
|
||||
),
|
||||
bundle,
|
||||
);
|
||||
expect(checkDbResult.data.findManyHook.length).toEqual(1);
|
||||
expect(checkDbResult.data.findManyHook[0].operation).toEqual(
|
||||
expect(checkDbResult.data.findManyWebHook.length).toEqual(1);
|
||||
expect(checkDbResult.data.findManyWebHook[0].operation).toEqual(
|
||||
'createOneCompany',
|
||||
);
|
||||
});
|
||||
@@ -48,13 +48,13 @@ describe('triggers.company', () => {
|
||||
requestDb(
|
||||
z,
|
||||
bundle,
|
||||
`query findManyHook {findManyHook(where: {id: {equals: "${result.id}"}}){id}}`,
|
||||
`query findManyWebHook {findManyWebHook(where: {id: {equals: "${result.id}"}}){id}}`,
|
||||
),
|
||||
bundle,
|
||||
);
|
||||
expect(checkDbResult.data.findManyHook.length).toEqual(0);
|
||||
expect(checkDbResult.data.findManyWebHook.length).toEqual(0);
|
||||
});
|
||||
test('should load company from hook', async () => {
|
||||
test('should load company from web-hook', async () => {
|
||||
const bundle = {
|
||||
cleanedRequest: {
|
||||
id: 'd6ccb1d1-a90b-4822-a992-a0dd946592c9',
|
||||
|
||||
@@ -7,22 +7,22 @@ const performSubscribe = async (z: ZObject, bundle: Bundle) => {
|
||||
const result = await requestDb(
|
||||
z,
|
||||
bundle,
|
||||
`mutation createOneHook {createOneHook(data:{${handleQueryParams(
|
||||
`mutation createOneWebHook {createOneWebHook(data:{${handleQueryParams(
|
||||
data,
|
||||
)}}) {id}}`,
|
||||
);
|
||||
return result.data.createOneHook;
|
||||
return result.data.createOneWebHook;
|
||||
};
|
||||
const performUnsubscribe = async (z: ZObject, bundle: Bundle) => {
|
||||
const data = { id: bundle.subscribeData?.id };
|
||||
const result = await requestDb(
|
||||
z,
|
||||
bundle,
|
||||
`mutation deleteOneHook {deleteOneHook(where:{${handleQueryParams(
|
||||
`mutation deleteOneWebHook {deleteOneWebHook(where:{${handleQueryParams(
|
||||
data,
|
||||
)}}) {id}}`,
|
||||
);
|
||||
return result.data.deleteOneHook;
|
||||
return result.data.deleteOneWebHook;
|
||||
};
|
||||
const perform = (z: ZObject, bundle: Bundle) => {
|
||||
return [bundle.cleanedRequest];
|
||||
@@ -55,7 +55,7 @@ export default {
|
||||
},
|
||||
operation: {
|
||||
inputFields: [],
|
||||
type: 'hook',
|
||||
type: 'web-hook',
|
||||
performSubscribe,
|
||||
performUnsubscribe,
|
||||
perform,
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
ActivityTarget,
|
||||
Attachment,
|
||||
ApiKey,
|
||||
Hook,
|
||||
Comment,
|
||||
Company,
|
||||
Favorite,
|
||||
@@ -22,6 +21,7 @@ import {
|
||||
ViewField,
|
||||
ViewFilter,
|
||||
ViewSort,
|
||||
WebHook,
|
||||
Workspace,
|
||||
WorkspaceMember,
|
||||
} from '@prisma/client';
|
||||
@@ -36,7 +36,7 @@ type SubjectsAbility = Subjects<{
|
||||
Comment: Comment;
|
||||
Company: Company;
|
||||
Favorite: Favorite;
|
||||
Hook: Hook;
|
||||
WebHook: WebHook;
|
||||
Person: Person;
|
||||
Pipeline: Pipeline;
|
||||
PipelineProgress: PipelineProgress;
|
||||
@@ -83,10 +83,10 @@ export class AbilityFactory {
|
||||
can(AbilityAction.Create, 'ApiKey');
|
||||
can(AbilityAction.Update, 'ApiKey', { workspaceId: workspace.id });
|
||||
|
||||
// Hook
|
||||
can(AbilityAction.Read, 'Hook', { workspaceId: workspace.id });
|
||||
can(AbilityAction.Create, 'Hook');
|
||||
can(AbilityAction.Delete, 'Hook', { workspaceId: workspace.id });
|
||||
// WebHook
|
||||
can(AbilityAction.Read, 'WebHook', { workspaceId: workspace.id });
|
||||
can(AbilityAction.Create, 'WebHook');
|
||||
can(AbilityAction.Delete, 'WebHook', { workspaceId: workspace.id });
|
||||
|
||||
// Workspace
|
||||
can(AbilityAction.Read, 'Workspace');
|
||||
|
||||
@@ -2,6 +2,11 @@ import { Module } from '@nestjs/common';
|
||||
|
||||
import { AbilityFactory } from 'src/ability/ability.factory';
|
||||
import { PrismaService } from 'src/database/prisma.service';
|
||||
import {
|
||||
CreateWebHookAbilityHandler,
|
||||
DeleteWebHookAbilityHandler,
|
||||
ReadWebHookAbilityHandler,
|
||||
} from 'src/ability/handlers/web-hook.ability-handler';
|
||||
|
||||
import {
|
||||
CreateUserAbilityHandler,
|
||||
@@ -129,11 +134,6 @@ import {
|
||||
ManageApiKeyAbilityHandler,
|
||||
ReadApiKeyAbilityHandler,
|
||||
} from './handlers/api-key.ability-handler';
|
||||
import {
|
||||
CreateHookAbilityHandler,
|
||||
DeleteHookAbilityHandler,
|
||||
ReadHookAbilityHandler,
|
||||
} from './handlers/hook.ability-handler';
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
@@ -247,9 +247,9 @@ import {
|
||||
CreateApiKeyAbilityHandler,
|
||||
UpdateApiKeyAbilityHandler,
|
||||
// Hook
|
||||
CreateHookAbilityHandler,
|
||||
DeleteHookAbilityHandler,
|
||||
ReadHookAbilityHandler,
|
||||
CreateWebHookAbilityHandler,
|
||||
DeleteWebHookAbilityHandler,
|
||||
ReadWebHookAbilityHandler,
|
||||
],
|
||||
exports: [
|
||||
AbilityFactory,
|
||||
@@ -360,9 +360,9 @@ import {
|
||||
CreateApiKeyAbilityHandler,
|
||||
UpdateApiKeyAbilityHandler,
|
||||
// Hook
|
||||
CreateHookAbilityHandler,
|
||||
DeleteHookAbilityHandler,
|
||||
ReadHookAbilityHandler,
|
||||
CreateWebHookAbilityHandler,
|
||||
DeleteWebHookAbilityHandler,
|
||||
ReadWebHookAbilityHandler,
|
||||
],
|
||||
})
|
||||
export class AbilityModule {}
|
||||
|
||||
@@ -16,14 +16,14 @@ import { AbilityAction } from 'src/ability/ability.action';
|
||||
import { assert } from 'src/utils/assert';
|
||||
|
||||
@Injectable()
|
||||
export class CreateHookAbilityHandler implements IAbilityHandler {
|
||||
export class CreateWebHookAbilityHandler implements IAbilityHandler {
|
||||
constructor(private readonly prismaService: PrismaService) {}
|
||||
|
||||
async handle(ability: AppAbility, context: ExecutionContext) {
|
||||
const gqlContext = GqlExecutionContext.create(context);
|
||||
const args = gqlContext.getArgs();
|
||||
const allowed = await relationAbilityChecker(
|
||||
'Hook',
|
||||
'WebHook',
|
||||
ability,
|
||||
this.prismaService.client,
|
||||
args,
|
||||
@@ -31,27 +31,27 @@ export class CreateHookAbilityHandler implements IAbilityHandler {
|
||||
if (!allowed) {
|
||||
return false;
|
||||
}
|
||||
return ability.can(AbilityAction.Create, 'Hook');
|
||||
return ability.can(AbilityAction.Create, 'WebHook');
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class DeleteHookAbilityHandler implements IAbilityHandler {
|
||||
export class DeleteWebHookAbilityHandler implements IAbilityHandler {
|
||||
constructor(private readonly prismaService: PrismaService) {}
|
||||
async handle(ability: AppAbility, context: ExecutionContext) {
|
||||
const gqlContext = GqlExecutionContext.create(context);
|
||||
const args = gqlContext.getArgs();
|
||||
const hook = await this.prismaService.client.hook.findFirst({
|
||||
const hook = await this.prismaService.client.webHook.findFirst({
|
||||
where: args.where,
|
||||
});
|
||||
assert(hook, '', NotFoundException);
|
||||
return ability.can(AbilityAction.Delete, subject('Hook', hook));
|
||||
return ability.can(AbilityAction.Delete, subject('WebHook', hook));
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ReadHookAbilityHandler implements IAbilityHandler {
|
||||
export class ReadWebHookAbilityHandler implements IAbilityHandler {
|
||||
async handle(ability: AppAbility) {
|
||||
return ability.can(AbilityAction.Read, 'Hook');
|
||||
return ability.can(AbilityAction.Read, 'WebHook');
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
|
||||
import { UserAbility } from 'src/decorators/user-ability.decorator';
|
||||
import { AppAbility } from 'src/ability/ability.factory';
|
||||
import { AuthToken } from 'src/core/auth/dto/token.entity';
|
||||
import { ApiKeyToken } from 'src/core/auth/dto/token.entity';
|
||||
|
||||
import { ApiKeyService } from './api-key.service';
|
||||
|
||||
@@ -28,13 +28,13 @@ import { ApiKeyService } from './api-key.service';
|
||||
export class ApiKeyResolver {
|
||||
constructor(private readonly apiKeyService: ApiKeyService) {}
|
||||
|
||||
@Mutation(() => AuthToken)
|
||||
@Mutation(() => ApiKeyToken)
|
||||
@UseGuards(AbilityGuard)
|
||||
@CheckAbilities(CreateApiKeyAbilityHandler)
|
||||
async createOneApiKey(
|
||||
@Args() args: CreateOneApiKeyArgs,
|
||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||
): Promise<AuthToken> {
|
||||
): Promise<ApiKeyToken> {
|
||||
return await this.apiKeyService.generateApiKeyToken(
|
||||
workspaceId,
|
||||
args.data.name,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { addMilliseconds, addSeconds } from 'date-fns';
|
||||
import ms from 'ms';
|
||||
|
||||
import { PrismaService } from 'src/database/prisma.service';
|
||||
import { AuthToken } from 'src/core/auth/dto/token.entity';
|
||||
import { ApiKeyToken } from 'src/core/auth/dto/token.entity';
|
||||
import { assert } from 'src/utils/assert';
|
||||
import { EnvironmentService } from 'src/integrations/environment/environment.service';
|
||||
|
||||
@@ -28,7 +28,7 @@ export class ApiKeyService {
|
||||
workspaceId: string,
|
||||
name: string,
|
||||
expiresAt?: Date | string,
|
||||
): Promise<AuthToken> {
|
||||
): Promise<ApiKeyToken> {
|
||||
const secret = this.environmentService.getAccessTokenSecret();
|
||||
let expiresIn: string | number;
|
||||
let expirationDate: Date;
|
||||
@@ -52,6 +52,7 @@ export class ApiKeyService {
|
||||
},
|
||||
});
|
||||
return {
|
||||
id,
|
||||
token: this.jwtService.sign(jwtPayload, {
|
||||
secret,
|
||||
expiresIn,
|
||||
|
||||
@@ -9,6 +9,18 @@ export class AuthToken {
|
||||
expiresAt: Date;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class ApiKeyToken {
|
||||
@Field(() => String)
|
||||
id: string;
|
||||
|
||||
@Field(() => String)
|
||||
token: string;
|
||||
|
||||
@Field(() => Date)
|
||||
expiresAt: Date;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class AuthTokenPair {
|
||||
@Field(() => AuthToken)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { WebHookModule } from 'src/core/web-hook/web-hook.module';
|
||||
|
||||
import { UserModule } from './user/user.module';
|
||||
import { CommentModule } from './comment/comment.module';
|
||||
import { CompanyModule } from './company/company.module';
|
||||
@@ -15,7 +17,6 @@ import { ActivityModule } from './activity/activity.module';
|
||||
import { ViewModule } from './view/view.module';
|
||||
import { FavoriteModule } from './favorite/favorite.module';
|
||||
import { ApiKeyModule } from './api-key/api-key.module';
|
||||
import { HookModule } from './hook/hook.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -34,7 +35,7 @@ import { HookModule } from './hook/hook.module';
|
||||
ViewModule,
|
||||
FavoriteModule,
|
||||
ApiKeyModule,
|
||||
HookModule,
|
||||
WebHookModule,
|
||||
],
|
||||
exports: [
|
||||
AuthModule,
|
||||
@@ -48,7 +49,7 @@ import { HookModule } from './hook/hook.module';
|
||||
AttachmentModule,
|
||||
FavoriteModule,
|
||||
ApiKeyModule,
|
||||
HookModule,
|
||||
WebHookModule,
|
||||
],
|
||||
})
|
||||
export class CoreModule {}
|
||||
|
||||
@@ -2,11 +2,10 @@ import { Module } from '@nestjs/common';
|
||||
|
||||
import { PrismaModule } from 'src/database/prisma.module';
|
||||
import { AbilityModule } from 'src/ability/ability.module';
|
||||
|
||||
import { HookResolver } from './hook.resolver';
|
||||
import { WebHookResolver } from 'src/core/web-hook/web-hook.resolver';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, AbilityModule],
|
||||
providers: [HookResolver],
|
||||
providers: [WebHookResolver],
|
||||
})
|
||||
export class HookModule {}
|
||||
export class WebHookModule {}
|
||||
@@ -4,35 +4,35 @@ import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
import { accessibleBy } from '@casl/prisma';
|
||||
|
||||
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
|
||||
import { Hook } from 'src/core/@generated/hook/hook.model';
|
||||
import { AbilityGuard } from 'src/guards/ability.guard';
|
||||
import { CheckAbilities } from 'src/decorators/check-abilities.decorator';
|
||||
import {
|
||||
CreateHookAbilityHandler,
|
||||
DeleteHookAbilityHandler,
|
||||
ReadHookAbilityHandler,
|
||||
} from 'src/ability/handlers/hook.ability-handler';
|
||||
import { CreateOneHookArgs } from 'src/core/@generated/hook/create-one-hook.args';
|
||||
CreateWebHookAbilityHandler,
|
||||
DeleteWebHookAbilityHandler,
|
||||
ReadWebHookAbilityHandler,
|
||||
} from 'src/ability/handlers/web-hook.ability-handler';
|
||||
import { PrismaService } from 'src/database/prisma.service';
|
||||
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
|
||||
import { Workspace } from 'src/core/@generated/workspace/workspace.model';
|
||||
import { DeleteOneHookArgs } from 'src/core/@generated/hook/delete-one-hook.args';
|
||||
import { FindManyHookArgs } from 'src/core/@generated/hook/find-many-hook.args';
|
||||
import { UserAbility } from 'src/decorators/user-ability.decorator';
|
||||
import { AppAbility } from 'src/ability/ability.factory';
|
||||
import { CreateOneWebHookArgs } from 'src/core/@generated/web-hook/create-one-web-hook.args';
|
||||
import { DeleteOneWebHookArgs } from 'src/core/@generated/web-hook/delete-one-web-hook.args';
|
||||
import { FindManyWebHookArgs } from 'src/core/@generated/web-hook/find-many-web-hook.args';
|
||||
import { WebHook } from 'src/core/@generated/web-hook/web-hook.model';
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Resolver(() => Hook)
|
||||
export class HookResolver {
|
||||
@Resolver(() => WebHook)
|
||||
export class WebHookResolver {
|
||||
constructor(private readonly prismaService: PrismaService) {}
|
||||
@Mutation(() => Hook)
|
||||
@Mutation(() => WebHook)
|
||||
@UseGuards(AbilityGuard)
|
||||
@CheckAbilities(CreateHookAbilityHandler)
|
||||
async createOneHook(
|
||||
@Args() args: CreateOneHookArgs,
|
||||
@CheckAbilities(CreateWebHookAbilityHandler)
|
||||
async createOneWebHook(
|
||||
@Args() args: CreateOneWebHookArgs,
|
||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||
): Promise<Hook> {
|
||||
return this.prismaService.client.hook.create({
|
||||
): Promise<WebHook> {
|
||||
return this.prismaService.client.webHook.create({
|
||||
data: {
|
||||
...args.data,
|
||||
...{ workspace: { connect: { id: workspaceId } } },
|
||||
@@ -40,31 +40,31 @@ export class HookResolver {
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => Hook, { nullable: false })
|
||||
@Mutation(() => WebHook, { nullable: false })
|
||||
@UseGuards(AbilityGuard)
|
||||
@CheckAbilities(DeleteHookAbilityHandler)
|
||||
async deleteOneHook(@Args() args: DeleteOneHookArgs): Promise<Hook> {
|
||||
const hookToDelete = this.prismaService.client.hook.findUnique({
|
||||
@CheckAbilities(DeleteWebHookAbilityHandler)
|
||||
async deleteOneWebHook(@Args() args: DeleteOneWebHookArgs): Promise<WebHook> {
|
||||
const hookToDelete = this.prismaService.client.webHook.findUnique({
|
||||
where: args.where,
|
||||
});
|
||||
if (!hookToDelete) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
return await this.prismaService.client.hook.delete({
|
||||
return await this.prismaService.client.webHook.delete({
|
||||
where: args.where,
|
||||
});
|
||||
}
|
||||
|
||||
@Query(() => [Hook])
|
||||
@Query(() => [WebHook])
|
||||
@UseGuards(AbilityGuard)
|
||||
@CheckAbilities(ReadHookAbilityHandler)
|
||||
async findManyHook(
|
||||
@Args() args: FindManyHookArgs,
|
||||
@CheckAbilities(ReadWebHookAbilityHandler)
|
||||
async findManyWebHook(
|
||||
@Args() args: FindManyWebHookArgs,
|
||||
@UserAbility() ability: AppAbility,
|
||||
) {
|
||||
const filterOptions = [accessibleBy(ability).WorkspaceMember];
|
||||
if (args.where) filterOptions.push(args.where);
|
||||
return this.prismaService.client.hook.findMany({
|
||||
return this.prismaService.client.webHook.findMany({
|
||||
...args,
|
||||
where: { AND: filterOptions },
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `hooks` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "hooks" DROP CONSTRAINT "hooks_workspaceId_fkey";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "hooks";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "web_hooks" (
|
||||
"id" TEXT NOT NULL,
|
||||
"workspaceId" TEXT NOT NULL,
|
||||
"targetUrl" TEXT NOT NULL,
|
||||
"operation" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"deletedAt" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "web_hooks_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "web_hooks" ADD CONSTRAINT "web_hooks_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "workspaces"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -179,7 +179,7 @@ model Workspace {
|
||||
views View[]
|
||||
viewSorts ViewSort[]
|
||||
apiKeys ApiKey[]
|
||||
hooks Hook[]
|
||||
webHooks WebHook[]
|
||||
|
||||
/// @TypeGraphQL.omit(input: true, output: true)
|
||||
deletedAt DateTime?
|
||||
@@ -910,7 +910,7 @@ model ApiKey {
|
||||
@@map("api_keys")
|
||||
}
|
||||
|
||||
model Hook {
|
||||
model WebHook {
|
||||
/// @Validator.IsString()
|
||||
/// @Validator.IsOptional()
|
||||
id String @id @default(uuid())
|
||||
@@ -925,5 +925,5 @@ model Hook {
|
||||
/// @TypeGraphQL.omit(input: true, output: true)
|
||||
deletedAt DateTime?
|
||||
|
||||
@@map("hooks")
|
||||
@@map("web_hooks")
|
||||
}
|
||||
|
||||
@@ -22,5 +22,5 @@ export type ModelSelectMap = {
|
||||
ViewSort: Prisma.ViewSortSelect;
|
||||
ViewField: Prisma.ViewFieldSelect;
|
||||
ApiKey: Prisma.ApiKeySelect;
|
||||
Hook: Prisma.HookSelect;
|
||||
WebHook: Prisma.WebHookSelect;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user