mirror of
https://github.com/lingble/twenty.git
synced 2025-11-01 05:07:56 +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
|
```bash
|
||||||
cp .env.example .env
|
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.
|
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`',
|
'message': 'Icon imports are only allowed for `@/ui/icon`',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'group': ['react-hotkeys-hook'],
|
'group': ['react-hotkeys-web-hook'],
|
||||||
"importNames": ["useHotkeys"],
|
"importNames": ["useHotkeys"],
|
||||||
'message': 'Please use the custom wrapper: `useScopedHotkeys`',
|
'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 { SettingsObjectDetail } from '~/pages/settings/data-model/SettingsObjectDetail';
|
||||||
import { SettingsObjectEdit } from '~/pages/settings/data-model/SettingsObjectEdit';
|
import { SettingsObjectEdit } from '~/pages/settings/data-model/SettingsObjectEdit';
|
||||||
import { SettingsObjects } from '~/pages/settings/data-model/SettingsObjects';
|
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 { SettingsExperience } from '~/pages/settings/SettingsExperience';
|
||||||
import { SettingsProfile } from '~/pages/settings/SettingsProfile';
|
import { SettingsProfile } from '~/pages/settings/SettingsProfile';
|
||||||
import { SettingsWorkspace } from '~/pages/settings/SettingsWorkspace';
|
import { SettingsWorkspace } from '~/pages/settings/SettingsWorkspace';
|
||||||
@@ -31,7 +34,6 @@ import { getPageTitleFromPath } from '~/utils/title-utils';
|
|||||||
import { ObjectTablePage } from './modules/metadata/components/ObjectTablePage';
|
import { ObjectTablePage } from './modules/metadata/components/ObjectTablePage';
|
||||||
import { SettingsObjectNewFieldStep1 } from './pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep1';
|
import { SettingsObjectNewFieldStep1 } from './pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep1';
|
||||||
import { SettingsObjectNewFieldStep2 } from './pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2';
|
import { SettingsObjectNewFieldStep2 } from './pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2';
|
||||||
import { SettingsApis } from './pages/settings/SettingsApis';
|
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
@@ -97,7 +99,25 @@ export const App = () => {
|
|||||||
path={SettingsPath.NewObject}
|
path={SettingsPath.NewObject}
|
||||||
element={<SettingsNewObject />}
|
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
|
<Route
|
||||||
path={SettingsPath.ObjectNewFieldStep1}
|
path={SettingsPath.ObjectNewFieldStep1}
|
||||||
element={<SettingsObjectNewFieldStep1 />}
|
element={<SettingsObjectNewFieldStep1 />}
|
||||||
|
|||||||
@@ -760,6 +760,15 @@ export enum ViewType {
|
|||||||
Table = 'Table'
|
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 = {
|
export type Workspace = {
|
||||||
__typename?: 'Workspace';
|
__typename?: 'Workspace';
|
||||||
Attachment?: Maybe<Array<Attachment>>;
|
Attachment?: Maybe<Array<Attachment>>;
|
||||||
@@ -783,6 +792,7 @@ export type Workspace = {
|
|||||||
viewFilters?: Maybe<Array<ViewFilter>>;
|
viewFilters?: Maybe<Array<ViewFilter>>;
|
||||||
viewSorts?: Maybe<Array<ViewSort>>;
|
viewSorts?: Maybe<Array<ViewSort>>;
|
||||||
views?: Maybe<Array<View>>;
|
views?: Maybe<Array<View>>;
|
||||||
|
webHooks?: Maybe<Array<WebHook>>;
|
||||||
workspaceMember?: Maybe<Array<WorkspaceMember>>;
|
workspaceMember?: Maybe<Array<WorkspaceMember>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -482,6 +482,13 @@ export enum ApiKeyScalarFieldEnum {
|
|||||||
WorkspaceId = 'workspaceId'
|
WorkspaceId = 'workspaceId'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ApiKeyToken = {
|
||||||
|
__typename?: 'ApiKeyToken';
|
||||||
|
expiresAt: Scalars['DateTime'];
|
||||||
|
id: Scalars['String'];
|
||||||
|
token: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
export type ApiKeyUpdateManyWithoutWorkspaceNestedInput = {
|
export type ApiKeyUpdateManyWithoutWorkspaceNestedInput = {
|
||||||
connect?: InputMaybe<Array<ApiKeyWhereUniqueInput>>;
|
connect?: InputMaybe<Array<ApiKeyWhereUniqueInput>>;
|
||||||
disconnect?: InputMaybe<Array<ApiKeyWhereUniqueInput>>;
|
disconnect?: InputMaybe<Array<ApiKeyWhereUniqueInput>>;
|
||||||
@@ -1392,7 +1399,7 @@ export type Mutation = {
|
|||||||
createManyViewFilter: AffectedRows;
|
createManyViewFilter: AffectedRows;
|
||||||
createManyViewSort: AffectedRows;
|
createManyViewSort: AffectedRows;
|
||||||
createOneActivity: Activity;
|
createOneActivity: Activity;
|
||||||
createOneApiKey: AuthToken;
|
createOneApiKey: ApiKeyToken;
|
||||||
createOneComment: Comment;
|
createOneComment: Comment;
|
||||||
createOneCompany: Company;
|
createOneCompany: Company;
|
||||||
createOneField: Field;
|
createOneField: Field;
|
||||||
@@ -1402,6 +1409,7 @@ export type Mutation = {
|
|||||||
createOnePipelineStage: PipelineStage;
|
createOnePipelineStage: PipelineStage;
|
||||||
createOneView: View;
|
createOneView: View;
|
||||||
createOneViewField: ViewField;
|
createOneViewField: ViewField;
|
||||||
|
createOneWebHook: WebHook;
|
||||||
deleteCurrentWorkspace: Workspace;
|
deleteCurrentWorkspace: Workspace;
|
||||||
deleteFavorite: Favorite;
|
deleteFavorite: Favorite;
|
||||||
deleteManyActivities: AffectedRows;
|
deleteManyActivities: AffectedRows;
|
||||||
@@ -1415,6 +1423,7 @@ export type Mutation = {
|
|||||||
deleteOneObject: ObjectDeleteResponse;
|
deleteOneObject: ObjectDeleteResponse;
|
||||||
deleteOnePipelineStage: PipelineStage;
|
deleteOnePipelineStage: PipelineStage;
|
||||||
deleteOneView: View;
|
deleteOneView: View;
|
||||||
|
deleteOneWebHook: WebHook;
|
||||||
deleteUserAccount: User;
|
deleteUserAccount: User;
|
||||||
deleteWorkspaceMember: WorkspaceMember;
|
deleteWorkspaceMember: WorkspaceMember;
|
||||||
impersonate: Verify;
|
impersonate: Verify;
|
||||||
@@ -1559,6 +1568,11 @@ export type MutationCreateOneViewFieldArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationCreateOneWebHookArgs = {
|
||||||
|
data: WebHookCreateInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationDeleteFavoriteArgs = {
|
export type MutationDeleteFavoriteArgs = {
|
||||||
where: FavoriteWhereInput;
|
where: FavoriteWhereInput;
|
||||||
};
|
};
|
||||||
@@ -1609,6 +1623,11 @@ export type MutationDeleteOneViewArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationDeleteOneWebHookArgs = {
|
||||||
|
where: WebHookWhereUniqueInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationDeleteWorkspaceMemberArgs = {
|
export type MutationDeleteWorkspaceMemberArgs = {
|
||||||
where: WorkspaceMemberWhereUniqueInput;
|
where: WorkspaceMemberWhereUniqueInput;
|
||||||
};
|
};
|
||||||
@@ -2523,6 +2542,7 @@ export type Query = {
|
|||||||
findManyViewField: Array<ViewField>;
|
findManyViewField: Array<ViewField>;
|
||||||
findManyViewFilter: Array<ViewFilter>;
|
findManyViewFilter: Array<ViewFilter>;
|
||||||
findManyViewSort: Array<ViewSort>;
|
findManyViewSort: Array<ViewSort>;
|
||||||
|
findManyWebHook: Array<WebHook>;
|
||||||
findManyWorkspaceMember: Array<WorkspaceMember>;
|
findManyWorkspaceMember: Array<WorkspaceMember>;
|
||||||
findUniqueCompany: Company;
|
findUniqueCompany: Company;
|
||||||
findUniquePerson: Person;
|
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 = {
|
export type QueryFindManyWorkspaceMemberArgs = {
|
||||||
cursor?: InputMaybe<WorkspaceMemberWhereUniqueInput>;
|
cursor?: InputMaybe<WorkspaceMemberWhereUniqueInput>;
|
||||||
distinct?: InputMaybe<Array<WorkspaceMemberScalarFieldEnum>>;
|
distinct?: InputMaybe<Array<WorkspaceMemberScalarFieldEnum>>;
|
||||||
@@ -3392,6 +3422,62 @@ export type ViewWhereUniqueInput = {
|
|||||||
id?: InputMaybe<Scalars['String']>;
|
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 = {
|
export type Workspace = {
|
||||||
__typename?: 'Workspace';
|
__typename?: 'Workspace';
|
||||||
Attachment?: Maybe<Array<Attachment>>;
|
Attachment?: Maybe<Array<Attachment>>;
|
||||||
@@ -3415,6 +3501,7 @@ export type Workspace = {
|
|||||||
viewFilters?: Maybe<Array<ViewFilter>>;
|
viewFilters?: Maybe<Array<ViewFilter>>;
|
||||||
viewSorts?: Maybe<Array<ViewSort>>;
|
viewSorts?: Maybe<Array<ViewSort>>;
|
||||||
views?: Maybe<Array<View>>;
|
views?: Maybe<Array<View>>;
|
||||||
|
webHooks?: Maybe<Array<WebHook>>;
|
||||||
workspaceMember?: Maybe<Array<WorkspaceMember>>;
|
workspaceMember?: Maybe<Array<WorkspaceMember>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -3590,6 +3677,7 @@ export type WorkspaceUpdateInput = {
|
|||||||
viewFilters?: InputMaybe<ViewFilterUpdateManyWithoutWorkspaceNestedInput>;
|
viewFilters?: InputMaybe<ViewFilterUpdateManyWithoutWorkspaceNestedInput>;
|
||||||
viewSorts?: InputMaybe<ViewSortUpdateManyWithoutWorkspaceNestedInput>;
|
viewSorts?: InputMaybe<ViewSortUpdateManyWithoutWorkspaceNestedInput>;
|
||||||
views?: InputMaybe<ViewUpdateManyWithoutWorkspaceNestedInput>;
|
views?: InputMaybe<ViewUpdateManyWithoutWorkspaceNestedInput>;
|
||||||
|
webHooks?: InputMaybe<WebHookUpdateManyWithoutWorkspaceNestedInput>;
|
||||||
workspaceMember?: InputMaybe<WorkspaceMemberUpdateManyWithoutWorkspaceNestedInput>;
|
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 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 UserFieldsFragmentFragment = { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null };
|
||||||
|
|
||||||
export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>;
|
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 SearchUserQueryHookResult = ReturnType<typeof useSearchUserQuery>;
|
||||||
export type SearchUserLazyQueryHookResult = ReturnType<typeof useSearchUserLazyQuery>;
|
export type SearchUserLazyQueryHookResult = ReturnType<typeof useSearchUserLazyQuery>;
|
||||||
export type SearchUserQueryResult = Apollo.QueryResult<SearchUserQuery, SearchUserQueryVariables>;
|
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`
|
export const DeleteUserAccountDocument = gql`
|
||||||
mutation DeleteUserAccount {
|
mutation DeleteUserAccount {
|
||||||
deleteUserAccount {
|
deleteUserAccount {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const CompanyBoard = ({
|
|||||||
onEditColumnTitle,
|
onEditColumnTitle,
|
||||||
}: CompanyBoardProps) => {
|
}: CompanyBoardProps) => {
|
||||||
// TODO: we can store objectId and fieldDefinitions in the ViewBarContext
|
// 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 } =
|
const { createView, deleteView, submitCurrentView, updateView } =
|
||||||
useBoardViews({
|
useBoardViews({
|
||||||
objectId: 'company',
|
objectId: 'company',
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const SettingsNavbar = () => {
|
|||||||
end: false,
|
end: false,
|
||||||
});
|
});
|
||||||
const isDevelopersSettingsActive = !!useMatch({
|
const isDevelopersSettingsActive = !!useMatch({
|
||||||
path: useResolvedPath('/settings/api').pathname,
|
path: useResolvedPath('/settings/developers/api-keys').pathname,
|
||||||
end: true,
|
end: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ export const SettingsNavbar = () => {
|
|||||||
{isDevelopersSettingsEnabled && (
|
{isDevelopersSettingsEnabled && (
|
||||||
<NavItem
|
<NavItem
|
||||||
label="Developers"
|
label="Developers"
|
||||||
to="/settings/apis"
|
to="/settings/developers/api-keys"
|
||||||
Icon={IconRobot}
|
Icon={IconRobot}
|
||||||
active={isDevelopersSettingsActive}
|
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};
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const SettingsApisFieldItemTableRow = ({
|
export const SettingsApiKeysFieldItemTableRow = ({
|
||||||
fieldItem,
|
fieldItem,
|
||||||
}: {
|
}: {
|
||||||
fieldItem: ApisFiedlItem;
|
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();
|
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 () => {
|
const debouncedUpdate = debounce(async () => {
|
||||||
if (onFirstNameUpdate) {
|
if (onFirstNameUpdate) {
|
||||||
onFirstNameUpdate(firstName);
|
onFirstNameUpdate(firstName);
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export const NameField = ({
|
|||||||
|
|
||||||
const [updateWorkspace] = useUpdateWorkspaceMutation();
|
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
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
const debouncedUpdate = useCallback(
|
const debouncedUpdate = useCallback(
|
||||||
debounce(async (name: string) => {
|
debounce(async (name: string) => {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export enum AppPath {
|
|||||||
ObjectTablePage = '/objects/:objectNamePlural',
|
ObjectTablePage = '/objects/:objectNamePlural',
|
||||||
|
|
||||||
SettingsCatchAll = `/settings/*`,
|
SettingsCatchAll = `/settings/*`,
|
||||||
|
DevelopersCatchAll = `/developers/*`,
|
||||||
|
|
||||||
// Impersonate
|
// Impersonate
|
||||||
Impersonate = '/impersonate/:userId',
|
Impersonate = '/impersonate/:userId',
|
||||||
|
|||||||
@@ -9,5 +9,7 @@ export enum SettingsPath {
|
|||||||
NewObject = 'objects/new',
|
NewObject = 'objects/new',
|
||||||
WorkspaceMembersPage = 'workspace-members',
|
WorkspaceMembersPage = 'workspace-members',
|
||||||
Workspace = 'workspace',
|
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 = {
|
export type GenericFieldContextType = {
|
||||||
fieldDefinition: FieldDefinition<FieldMetadata>;
|
fieldDefinition: FieldDefinition<FieldMetadata>;
|
||||||
// TODO: add better typing for mutation hook
|
// TODO: add better typing for mutation web-hook
|
||||||
useUpdateEntityMutation: () => [(params: any) => void, any];
|
useUpdateEntityMutation: () => [(params: any) => void, any];
|
||||||
entityId: string;
|
entityId: string;
|
||||||
recoilScopeId: string;
|
recoilScopeId: string;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
|||||||
|
|
||||||
import { SelectHotkeyScope } from '../types/SelectHotkeyScope';
|
import { SelectHotkeyScope } from '../types/SelectHotkeyScope';
|
||||||
|
|
||||||
export type SelectProps<Value extends string> = {
|
export type SelectProps<Value extends string | number> = {
|
||||||
dropdownScopeId: string;
|
dropdownScopeId: string;
|
||||||
onChange: (value: Value) => void;
|
onChange: (value: Value) => void;
|
||||||
options: { value: Value; label: string; Icon?: IconComponent }[];
|
options: { value: Value; label: string; Icon?: IconComponent }[];
|
||||||
@@ -38,7 +38,7 @@ const StyledLabel = styled.div`
|
|||||||
gap: ${({ theme }) => theme.spacing(1)};
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Select = <Value extends string>({
|
export const Select = <Value extends string | number>({
|
||||||
dropdownScopeId,
|
dropdownScopeId,
|
||||||
onChange,
|
onChange,
|
||||||
options,
|
options,
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export type TextInputComponentProps = Omit<
|
|||||||
> & {
|
> & {
|
||||||
className?: string;
|
className?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
|
info?: string;
|
||||||
onChange?: (text: string) => void;
|
onChange?: (text: string) => void;
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
disableHotkeys?: boolean;
|
disableHotkeys?: boolean;
|
||||||
@@ -45,10 +46,16 @@ const StyledLabel = styled.span`
|
|||||||
text-transform: uppercase;
|
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`
|
const StyledInputContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -113,6 +120,7 @@ const TextInputComponent = (
|
|||||||
{
|
{
|
||||||
className,
|
className,
|
||||||
label,
|
label,
|
||||||
|
info,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
onFocus,
|
onFocus,
|
||||||
@@ -204,6 +212,7 @@ const TextInputComponent = (
|
|||||||
)}
|
)}
|
||||||
</StyledTrailingIconContainer>
|
</StyledTrailingIconContainer>
|
||||||
</StyledInputContainer>
|
</StyledInputContainer>
|
||||||
|
{info && <StyledInfo>{info}</StyledInfo>}
|
||||||
{error && <StyledErrorHelper>{error}</StyledErrorHelper>}
|
{error && <StyledErrorHelper>{error}</StyledErrorHelper>}
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
|||||||
|
|
||||||
import { Select, SelectProps } from '../Select';
|
import { Select, SelectProps } from '../Select';
|
||||||
|
|
||||||
type RenderProps = SelectProps<string>;
|
type RenderProps = SelectProps<string | number>;
|
||||||
|
|
||||||
const Render = (args: RenderProps) => {
|
const Render = (args: RenderProps) => {
|
||||||
const [value, setValue] = useState(args.value);
|
const [value, setValue] = useState(args.value);
|
||||||
const handleChange = (value: string) => {
|
const handleChange = (value: string | number) => {
|
||||||
args.onChange?.(value);
|
args.onChange?.(value);
|
||||||
setValue(value);
|
setValue(value);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,3 +38,7 @@ export const Filled: Story = {
|
|||||||
export const Disabled: Story = {
|
export const Disabled: Story = {
|
||||||
args: { disabled: true, value: 'Tim' },
|
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';
|
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.
|
// boardColumnTotalsFamilyState indexed by columnId.
|
||||||
export const boardColumnTotalsFamilySelector = selectorFamily({
|
export const boardColumnTotalsFamilySelector = selectorFamily({
|
||||||
key: 'boardColumnTotalsFamilySelector',
|
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 { getByText } = render(<TestComponentDomMode />);
|
||||||
const inside = getByText('Inside');
|
const inside = getByText('Inside');
|
||||||
const inside2 = getByText('Inside 2');
|
const inside2 = getByText('Inside 2');
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ export const CreateProfile = () => {
|
|||||||
title="Name"
|
title="Name"
|
||||||
description="Your name as it will be displayed on the app"
|
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>
|
<StyledComboInputContainer>
|
||||||
<Controller
|
<Controller
|
||||||
name="firstName"
|
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 styled from '@emotion/styled';
|
||||||
|
|
||||||
import { objectSettingsWidth } from '@/settings/data-model/constants/objectSettings';
|
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 { activeApiKeyItems } from '@/settings/developers/constants/mockObjects';
|
||||||
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';
|
||||||
@@ -34,7 +34,7 @@ const StyledH1Title = styled(H1Title)`
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const SettingsApis = () => {
|
export const SettingsDevelopersApiKeys = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -48,7 +48,7 @@ export const SettingsApis = () => {
|
|||||||
accent="blue"
|
accent="blue"
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate('/');
|
navigate('/settings/developers/api-keys/new');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</StyledHeader>
|
</StyledHeader>
|
||||||
@@ -64,7 +64,7 @@ export const SettingsApis = () => {
|
|||||||
<TableHeader></TableHeader>
|
<TableHeader></TableHeader>
|
||||||
</StyledTableRow>
|
</StyledTableRow>
|
||||||
{activeApiKeyItems.map((fieldItem) => (
|
{activeApiKeyItems.map((fieldItem) => (
|
||||||
<SettingsApisFieldItemTableRow
|
<SettingsApiKeysFieldItemTableRow
|
||||||
key={fieldItem.id}
|
key={fieldItem.id}
|
||||||
fieldItem={fieldItem}
|
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';
|
import { FullHeightStorybookLayout } from '../FullHeightStorybookLayout';
|
||||||
|
|
||||||
export type PageDecoratorArgs = { routePath: string; routeParams: RouteParams };
|
export type PageDecoratorArgs = {
|
||||||
|
routePath: string;
|
||||||
|
routeParams: RouteParams;
|
||||||
|
state?: string;
|
||||||
|
};
|
||||||
|
|
||||||
type RouteParams = {
|
type RouteParams = {
|
||||||
[param: string]: string;
|
[param: string]: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const computeLocation = (routePath: string, routeParams: RouteParams) =>
|
const computeLocation = (
|
||||||
routePath.replace(/:(\w+)/g, (paramName) => routeParams[paramName] ?? '');
|
routePath: string,
|
||||||
|
routeParams: RouteParams,
|
||||||
|
state?: string,
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
pathname: routePath.replace(
|
||||||
|
/:(\w+)/g,
|
||||||
|
(paramName) => routeParams[paramName] ?? '',
|
||||||
|
),
|
||||||
|
state,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const PageDecorator: Decorator<{
|
export const PageDecorator: Decorator<{
|
||||||
routePath: string;
|
routePath: string;
|
||||||
routeParams: RouteParams;
|
routeParams: RouteParams;
|
||||||
|
state?: string;
|
||||||
}> = (Story, { args }) => (
|
}> = (Story, { args }) => (
|
||||||
<UserProvider>
|
<UserProvider>
|
||||||
<ClientConfigProvider>
|
<ClientConfigProvider>
|
||||||
<MemoryRouter
|
<MemoryRouter
|
||||||
initialEntries={[computeLocation(args.routePath, args.routeParams)]}
|
initialEntries={[
|
||||||
|
computeLocation(args.routePath, args.routeParams, args.state),
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<FullHeightStorybookLayout>
|
<FullHeightStorybookLayout>
|
||||||
<HelmetProvider>
|
<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_COMPANY_QUERY } from '@/search/graphql/queries/searchCompanyQuery';
|
||||||
import { SEARCH_PEOPLE_QUERY } from '@/search/graphql/queries/searchPeopleQuery';
|
import { SEARCH_PEOPLE_QUERY } from '@/search/graphql/queries/searchPeopleQuery';
|
||||||
import { SEARCH_USER_QUERY } from '@/search/graphql/queries/searchUserQuery';
|
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_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
|
||||||
import { GET_VIEW_FIELDS } from '@/views/graphql/queries/getViewFields';
|
import { GET_VIEW_FIELDS } from '@/views/graphql/queries/getViewFields';
|
||||||
import { GET_VIEWS } from '@/views/graphql/queries/getViews';
|
import { GET_VIEWS } from '@/views/graphql/queries/getViews';
|
||||||
@@ -30,6 +31,7 @@ import {
|
|||||||
SearchUserQuery,
|
SearchUserQuery,
|
||||||
ViewType,
|
ViewType,
|
||||||
} from '~/generated/graphql';
|
} from '~/generated/graphql';
|
||||||
|
import { mockedApiKeys } from '~/testing/mock-data/api-keys';
|
||||||
|
|
||||||
import { mockedActivities, mockedTasks } from './mock-data/activities';
|
import { mockedActivities, mockedTasks } from './mock-data/activities';
|
||||||
import {
|
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(
|
graphql.mutation(
|
||||||
getOperationName(CREATE_ACTIVITY_WITH_COMMENT) ?? '',
|
getOperationName(CREATE_ACTIVITY_WITH_COMMENT) ?? '',
|
||||||
(req, res, ctx) => {
|
(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 { DateTime } from 'luxon';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
beautifyDateDiff,
|
||||||
beautifyExactDate,
|
beautifyExactDate,
|
||||||
beautifyExactDateTime,
|
beautifyExactDateTime,
|
||||||
beautifyPastDateAbsolute,
|
beautifyPastDateAbsolute,
|
||||||
@@ -237,3 +238,47 @@ describe('hasDatePassed', () => {
|
|||||||
expect(result).toEqual(false);
|
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;
|
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',
|
label: 'Api Key',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
helpText:
|
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}}',
|
connectionLabel: '{{data.currentWorkspace.displayName}}',
|
||||||
|
|||||||
@@ -19,12 +19,12 @@ describe('triggers.company', () => {
|
|||||||
requestDb(
|
requestDb(
|
||||||
z,
|
z,
|
||||||
bundle,
|
bundle,
|
||||||
`query findManyHook {findManyHook(where: {id: {equals: "${result.id}"}}){id operation}}`,
|
`query findManyWebHook {findManyWebHook(where: {id: {equals: "${result.id}"}}){id operation}}`,
|
||||||
),
|
),
|
||||||
bundle,
|
bundle,
|
||||||
);
|
);
|
||||||
expect(checkDbResult.data.findManyHook.length).toEqual(1);
|
expect(checkDbResult.data.findManyWebHook.length).toEqual(1);
|
||||||
expect(checkDbResult.data.findManyHook[0].operation).toEqual(
|
expect(checkDbResult.data.findManyWebHook[0].operation).toEqual(
|
||||||
'createOneCompany',
|
'createOneCompany',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -48,13 +48,13 @@ describe('triggers.company', () => {
|
|||||||
requestDb(
|
requestDb(
|
||||||
z,
|
z,
|
||||||
bundle,
|
bundle,
|
||||||
`query findManyHook {findManyHook(where: {id: {equals: "${result.id}"}}){id}}`,
|
`query findManyWebHook {findManyWebHook(where: {id: {equals: "${result.id}"}}){id}}`,
|
||||||
),
|
),
|
||||||
bundle,
|
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 = {
|
const bundle = {
|
||||||
cleanedRequest: {
|
cleanedRequest: {
|
||||||
id: 'd6ccb1d1-a90b-4822-a992-a0dd946592c9',
|
id: 'd6ccb1d1-a90b-4822-a992-a0dd946592c9',
|
||||||
|
|||||||
@@ -7,22 +7,22 @@ const performSubscribe = async (z: ZObject, bundle: Bundle) => {
|
|||||||
const result = await requestDb(
|
const result = await requestDb(
|
||||||
z,
|
z,
|
||||||
bundle,
|
bundle,
|
||||||
`mutation createOneHook {createOneHook(data:{${handleQueryParams(
|
`mutation createOneWebHook {createOneWebHook(data:{${handleQueryParams(
|
||||||
data,
|
data,
|
||||||
)}}) {id}}`,
|
)}}) {id}}`,
|
||||||
);
|
);
|
||||||
return result.data.createOneHook;
|
return result.data.createOneWebHook;
|
||||||
};
|
};
|
||||||
const performUnsubscribe = async (z: ZObject, bundle: Bundle) => {
|
const performUnsubscribe = async (z: ZObject, bundle: Bundle) => {
|
||||||
const data = { id: bundle.subscribeData?.id };
|
const data = { id: bundle.subscribeData?.id };
|
||||||
const result = await requestDb(
|
const result = await requestDb(
|
||||||
z,
|
z,
|
||||||
bundle,
|
bundle,
|
||||||
`mutation deleteOneHook {deleteOneHook(where:{${handleQueryParams(
|
`mutation deleteOneWebHook {deleteOneWebHook(where:{${handleQueryParams(
|
||||||
data,
|
data,
|
||||||
)}}) {id}}`,
|
)}}) {id}}`,
|
||||||
);
|
);
|
||||||
return result.data.deleteOneHook;
|
return result.data.deleteOneWebHook;
|
||||||
};
|
};
|
||||||
const perform = (z: ZObject, bundle: Bundle) => {
|
const perform = (z: ZObject, bundle: Bundle) => {
|
||||||
return [bundle.cleanedRequest];
|
return [bundle.cleanedRequest];
|
||||||
@@ -55,7 +55,7 @@ export default {
|
|||||||
},
|
},
|
||||||
operation: {
|
operation: {
|
||||||
inputFields: [],
|
inputFields: [],
|
||||||
type: 'hook',
|
type: 'web-hook',
|
||||||
performSubscribe,
|
performSubscribe,
|
||||||
performUnsubscribe,
|
performUnsubscribe,
|
||||||
perform,
|
perform,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
ActivityTarget,
|
ActivityTarget,
|
||||||
Attachment,
|
Attachment,
|
||||||
ApiKey,
|
ApiKey,
|
||||||
Hook,
|
|
||||||
Comment,
|
Comment,
|
||||||
Company,
|
Company,
|
||||||
Favorite,
|
Favorite,
|
||||||
@@ -22,6 +21,7 @@ import {
|
|||||||
ViewField,
|
ViewField,
|
||||||
ViewFilter,
|
ViewFilter,
|
||||||
ViewSort,
|
ViewSort,
|
||||||
|
WebHook,
|
||||||
Workspace,
|
Workspace,
|
||||||
WorkspaceMember,
|
WorkspaceMember,
|
||||||
} from '@prisma/client';
|
} from '@prisma/client';
|
||||||
@@ -36,7 +36,7 @@ type SubjectsAbility = Subjects<{
|
|||||||
Comment: Comment;
|
Comment: Comment;
|
||||||
Company: Company;
|
Company: Company;
|
||||||
Favorite: Favorite;
|
Favorite: Favorite;
|
||||||
Hook: Hook;
|
WebHook: WebHook;
|
||||||
Person: Person;
|
Person: Person;
|
||||||
Pipeline: Pipeline;
|
Pipeline: Pipeline;
|
||||||
PipelineProgress: PipelineProgress;
|
PipelineProgress: PipelineProgress;
|
||||||
@@ -83,10 +83,10 @@ export class AbilityFactory {
|
|||||||
can(AbilityAction.Create, 'ApiKey');
|
can(AbilityAction.Create, 'ApiKey');
|
||||||
can(AbilityAction.Update, 'ApiKey', { workspaceId: workspace.id });
|
can(AbilityAction.Update, 'ApiKey', { workspaceId: workspace.id });
|
||||||
|
|
||||||
// Hook
|
// WebHook
|
||||||
can(AbilityAction.Read, 'Hook', { workspaceId: workspace.id });
|
can(AbilityAction.Read, 'WebHook', { workspaceId: workspace.id });
|
||||||
can(AbilityAction.Create, 'Hook');
|
can(AbilityAction.Create, 'WebHook');
|
||||||
can(AbilityAction.Delete, 'Hook', { workspaceId: workspace.id });
|
can(AbilityAction.Delete, 'WebHook', { workspaceId: workspace.id });
|
||||||
|
|
||||||
// Workspace
|
// Workspace
|
||||||
can(AbilityAction.Read, 'Workspace');
|
can(AbilityAction.Read, 'Workspace');
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ import { Module } from '@nestjs/common';
|
|||||||
|
|
||||||
import { AbilityFactory } from 'src/ability/ability.factory';
|
import { AbilityFactory } from 'src/ability/ability.factory';
|
||||||
import { PrismaService } from 'src/database/prisma.service';
|
import { PrismaService } from 'src/database/prisma.service';
|
||||||
|
import {
|
||||||
|
CreateWebHookAbilityHandler,
|
||||||
|
DeleteWebHookAbilityHandler,
|
||||||
|
ReadWebHookAbilityHandler,
|
||||||
|
} from 'src/ability/handlers/web-hook.ability-handler';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CreateUserAbilityHandler,
|
CreateUserAbilityHandler,
|
||||||
@@ -129,11 +134,6 @@ import {
|
|||||||
ManageApiKeyAbilityHandler,
|
ManageApiKeyAbilityHandler,
|
||||||
ReadApiKeyAbilityHandler,
|
ReadApiKeyAbilityHandler,
|
||||||
} from './handlers/api-key.ability-handler';
|
} from './handlers/api-key.ability-handler';
|
||||||
import {
|
|
||||||
CreateHookAbilityHandler,
|
|
||||||
DeleteHookAbilityHandler,
|
|
||||||
ReadHookAbilityHandler,
|
|
||||||
} from './handlers/hook.ability-handler';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [
|
providers: [
|
||||||
@@ -247,9 +247,9 @@ import {
|
|||||||
CreateApiKeyAbilityHandler,
|
CreateApiKeyAbilityHandler,
|
||||||
UpdateApiKeyAbilityHandler,
|
UpdateApiKeyAbilityHandler,
|
||||||
// Hook
|
// Hook
|
||||||
CreateHookAbilityHandler,
|
CreateWebHookAbilityHandler,
|
||||||
DeleteHookAbilityHandler,
|
DeleteWebHookAbilityHandler,
|
||||||
ReadHookAbilityHandler,
|
ReadWebHookAbilityHandler,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
AbilityFactory,
|
AbilityFactory,
|
||||||
@@ -360,9 +360,9 @@ import {
|
|||||||
CreateApiKeyAbilityHandler,
|
CreateApiKeyAbilityHandler,
|
||||||
UpdateApiKeyAbilityHandler,
|
UpdateApiKeyAbilityHandler,
|
||||||
// Hook
|
// Hook
|
||||||
CreateHookAbilityHandler,
|
CreateWebHookAbilityHandler,
|
||||||
DeleteHookAbilityHandler,
|
DeleteWebHookAbilityHandler,
|
||||||
ReadHookAbilityHandler,
|
ReadWebHookAbilityHandler,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AbilityModule {}
|
export class AbilityModule {}
|
||||||
|
|||||||
@@ -16,14 +16,14 @@ import { AbilityAction } from 'src/ability/ability.action';
|
|||||||
import { assert } from 'src/utils/assert';
|
import { assert } from 'src/utils/assert';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CreateHookAbilityHandler implements IAbilityHandler {
|
export class CreateWebHookAbilityHandler implements IAbilityHandler {
|
||||||
constructor(private readonly prismaService: PrismaService) {}
|
constructor(private readonly prismaService: PrismaService) {}
|
||||||
|
|
||||||
async handle(ability: AppAbility, context: ExecutionContext) {
|
async handle(ability: AppAbility, context: ExecutionContext) {
|
||||||
const gqlContext = GqlExecutionContext.create(context);
|
const gqlContext = GqlExecutionContext.create(context);
|
||||||
const args = gqlContext.getArgs();
|
const args = gqlContext.getArgs();
|
||||||
const allowed = await relationAbilityChecker(
|
const allowed = await relationAbilityChecker(
|
||||||
'Hook',
|
'WebHook',
|
||||||
ability,
|
ability,
|
||||||
this.prismaService.client,
|
this.prismaService.client,
|
||||||
args,
|
args,
|
||||||
@@ -31,27 +31,27 @@ export class CreateHookAbilityHandler implements IAbilityHandler {
|
|||||||
if (!allowed) {
|
if (!allowed) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return ability.can(AbilityAction.Create, 'Hook');
|
return ability.can(AbilityAction.Create, 'WebHook');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DeleteHookAbilityHandler implements IAbilityHandler {
|
export class DeleteWebHookAbilityHandler implements IAbilityHandler {
|
||||||
constructor(private readonly prismaService: PrismaService) {}
|
constructor(private readonly prismaService: PrismaService) {}
|
||||||
async handle(ability: AppAbility, context: ExecutionContext) {
|
async handle(ability: AppAbility, context: ExecutionContext) {
|
||||||
const gqlContext = GqlExecutionContext.create(context);
|
const gqlContext = GqlExecutionContext.create(context);
|
||||||
const args = gqlContext.getArgs();
|
const args = gqlContext.getArgs();
|
||||||
const hook = await this.prismaService.client.hook.findFirst({
|
const hook = await this.prismaService.client.webHook.findFirst({
|
||||||
where: args.where,
|
where: args.where,
|
||||||
});
|
});
|
||||||
assert(hook, '', NotFoundException);
|
assert(hook, '', NotFoundException);
|
||||||
return ability.can(AbilityAction.Delete, subject('Hook', hook));
|
return ability.can(AbilityAction.Delete, subject('WebHook', hook));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ReadHookAbilityHandler implements IAbilityHandler {
|
export class ReadWebHookAbilityHandler implements IAbilityHandler {
|
||||||
async handle(ability: AppAbility) {
|
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 { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
|
||||||
import { UserAbility } from 'src/decorators/user-ability.decorator';
|
import { UserAbility } from 'src/decorators/user-ability.decorator';
|
||||||
import { AppAbility } from 'src/ability/ability.factory';
|
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';
|
import { ApiKeyService } from './api-key.service';
|
||||||
|
|
||||||
@@ -28,13 +28,13 @@ import { ApiKeyService } from './api-key.service';
|
|||||||
export class ApiKeyResolver {
|
export class ApiKeyResolver {
|
||||||
constructor(private readonly apiKeyService: ApiKeyService) {}
|
constructor(private readonly apiKeyService: ApiKeyService) {}
|
||||||
|
|
||||||
@Mutation(() => AuthToken)
|
@Mutation(() => ApiKeyToken)
|
||||||
@UseGuards(AbilityGuard)
|
@UseGuards(AbilityGuard)
|
||||||
@CheckAbilities(CreateApiKeyAbilityHandler)
|
@CheckAbilities(CreateApiKeyAbilityHandler)
|
||||||
async createOneApiKey(
|
async createOneApiKey(
|
||||||
@Args() args: CreateOneApiKeyArgs,
|
@Args() args: CreateOneApiKeyArgs,
|
||||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||||
): Promise<AuthToken> {
|
): Promise<ApiKeyToken> {
|
||||||
return await this.apiKeyService.generateApiKeyToken(
|
return await this.apiKeyService.generateApiKeyToken(
|
||||||
workspaceId,
|
workspaceId,
|
||||||
args.data.name,
|
args.data.name,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { addMilliseconds, addSeconds } from 'date-fns';
|
|||||||
import ms from 'ms';
|
import ms from 'ms';
|
||||||
|
|
||||||
import { PrismaService } from 'src/database/prisma.service';
|
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 { assert } from 'src/utils/assert';
|
||||||
import { EnvironmentService } from 'src/integrations/environment/environment.service';
|
import { EnvironmentService } from 'src/integrations/environment/environment.service';
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ export class ApiKeyService {
|
|||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
name: string,
|
name: string,
|
||||||
expiresAt?: Date | string,
|
expiresAt?: Date | string,
|
||||||
): Promise<AuthToken> {
|
): Promise<ApiKeyToken> {
|
||||||
const secret = this.environmentService.getAccessTokenSecret();
|
const secret = this.environmentService.getAccessTokenSecret();
|
||||||
let expiresIn: string | number;
|
let expiresIn: string | number;
|
||||||
let expirationDate: Date;
|
let expirationDate: Date;
|
||||||
@@ -52,6 +52,7 @@ export class ApiKeyService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
|
id,
|
||||||
token: this.jwtService.sign(jwtPayload, {
|
token: this.jwtService.sign(jwtPayload, {
|
||||||
secret,
|
secret,
|
||||||
expiresIn,
|
expiresIn,
|
||||||
|
|||||||
@@ -9,6 +9,18 @@ export class AuthToken {
|
|||||||
expiresAt: Date;
|
expiresAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class ApiKeyToken {
|
||||||
|
@Field(() => String)
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
token: string;
|
||||||
|
|
||||||
|
@Field(() => Date)
|
||||||
|
expiresAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class AuthTokenPair {
|
export class AuthTokenPair {
|
||||||
@Field(() => AuthToken)
|
@Field(() => AuthToken)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { WebHookModule } from 'src/core/web-hook/web-hook.module';
|
||||||
|
|
||||||
import { UserModule } from './user/user.module';
|
import { UserModule } from './user/user.module';
|
||||||
import { CommentModule } from './comment/comment.module';
|
import { CommentModule } from './comment/comment.module';
|
||||||
import { CompanyModule } from './company/company.module';
|
import { CompanyModule } from './company/company.module';
|
||||||
@@ -15,7 +17,6 @@ import { ActivityModule } from './activity/activity.module';
|
|||||||
import { ViewModule } from './view/view.module';
|
import { ViewModule } from './view/view.module';
|
||||||
import { FavoriteModule } from './favorite/favorite.module';
|
import { FavoriteModule } from './favorite/favorite.module';
|
||||||
import { ApiKeyModule } from './api-key/api-key.module';
|
import { ApiKeyModule } from './api-key/api-key.module';
|
||||||
import { HookModule } from './hook/hook.module';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -34,7 +35,7 @@ import { HookModule } from './hook/hook.module';
|
|||||||
ViewModule,
|
ViewModule,
|
||||||
FavoriteModule,
|
FavoriteModule,
|
||||||
ApiKeyModule,
|
ApiKeyModule,
|
||||||
HookModule,
|
WebHookModule,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
AuthModule,
|
AuthModule,
|
||||||
@@ -48,7 +49,7 @@ import { HookModule } from './hook/hook.module';
|
|||||||
AttachmentModule,
|
AttachmentModule,
|
||||||
FavoriteModule,
|
FavoriteModule,
|
||||||
ApiKeyModule,
|
ApiKeyModule,
|
||||||
HookModule,
|
WebHookModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CoreModule {}
|
export class CoreModule {}
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ import { Module } from '@nestjs/common';
|
|||||||
|
|
||||||
import { PrismaModule } from 'src/database/prisma.module';
|
import { PrismaModule } from 'src/database/prisma.module';
|
||||||
import { AbilityModule } from 'src/ability/ability.module';
|
import { AbilityModule } from 'src/ability/ability.module';
|
||||||
|
import { WebHookResolver } from 'src/core/web-hook/web-hook.resolver';
|
||||||
import { HookResolver } from './hook.resolver';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, AbilityModule],
|
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 { accessibleBy } from '@casl/prisma';
|
||||||
|
|
||||||
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
|
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 { AbilityGuard } from 'src/guards/ability.guard';
|
||||||
import { CheckAbilities } from 'src/decorators/check-abilities.decorator';
|
import { CheckAbilities } from 'src/decorators/check-abilities.decorator';
|
||||||
import {
|
import {
|
||||||
CreateHookAbilityHandler,
|
CreateWebHookAbilityHandler,
|
||||||
DeleteHookAbilityHandler,
|
DeleteWebHookAbilityHandler,
|
||||||
ReadHookAbilityHandler,
|
ReadWebHookAbilityHandler,
|
||||||
} from 'src/ability/handlers/hook.ability-handler';
|
} from 'src/ability/handlers/web-hook.ability-handler';
|
||||||
import { CreateOneHookArgs } from 'src/core/@generated/hook/create-one-hook.args';
|
|
||||||
import { PrismaService } from 'src/database/prisma.service';
|
import { PrismaService } from 'src/database/prisma.service';
|
||||||
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
|
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
|
||||||
import { Workspace } from 'src/core/@generated/workspace/workspace.model';
|
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 { UserAbility } from 'src/decorators/user-ability.decorator';
|
||||||
import { AppAbility } from 'src/ability/ability.factory';
|
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)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Resolver(() => Hook)
|
@Resolver(() => WebHook)
|
||||||
export class HookResolver {
|
export class WebHookResolver {
|
||||||
constructor(private readonly prismaService: PrismaService) {}
|
constructor(private readonly prismaService: PrismaService) {}
|
||||||
@Mutation(() => Hook)
|
@Mutation(() => WebHook)
|
||||||
@UseGuards(AbilityGuard)
|
@UseGuards(AbilityGuard)
|
||||||
@CheckAbilities(CreateHookAbilityHandler)
|
@CheckAbilities(CreateWebHookAbilityHandler)
|
||||||
async createOneHook(
|
async createOneWebHook(
|
||||||
@Args() args: CreateOneHookArgs,
|
@Args() args: CreateOneWebHookArgs,
|
||||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||||
): Promise<Hook> {
|
): Promise<WebHook> {
|
||||||
return this.prismaService.client.hook.create({
|
return this.prismaService.client.webHook.create({
|
||||||
data: {
|
data: {
|
||||||
...args.data,
|
...args.data,
|
||||||
...{ workspace: { connect: { id: workspaceId } } },
|
...{ workspace: { connect: { id: workspaceId } } },
|
||||||
@@ -40,31 +40,31 @@ export class HookResolver {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => Hook, { nullable: false })
|
@Mutation(() => WebHook, { nullable: false })
|
||||||
@UseGuards(AbilityGuard)
|
@UseGuards(AbilityGuard)
|
||||||
@CheckAbilities(DeleteHookAbilityHandler)
|
@CheckAbilities(DeleteWebHookAbilityHandler)
|
||||||
async deleteOneHook(@Args() args: DeleteOneHookArgs): Promise<Hook> {
|
async deleteOneWebHook(@Args() args: DeleteOneWebHookArgs): Promise<WebHook> {
|
||||||
const hookToDelete = this.prismaService.client.hook.findUnique({
|
const hookToDelete = this.prismaService.client.webHook.findUnique({
|
||||||
where: args.where,
|
where: args.where,
|
||||||
});
|
});
|
||||||
if (!hookToDelete) {
|
if (!hookToDelete) {
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
return await this.prismaService.client.hook.delete({
|
return await this.prismaService.client.webHook.delete({
|
||||||
where: args.where,
|
where: args.where,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Query(() => [Hook])
|
@Query(() => [WebHook])
|
||||||
@UseGuards(AbilityGuard)
|
@UseGuards(AbilityGuard)
|
||||||
@CheckAbilities(ReadHookAbilityHandler)
|
@CheckAbilities(ReadWebHookAbilityHandler)
|
||||||
async findManyHook(
|
async findManyWebHook(
|
||||||
@Args() args: FindManyHookArgs,
|
@Args() args: FindManyWebHookArgs,
|
||||||
@UserAbility() ability: AppAbility,
|
@UserAbility() ability: AppAbility,
|
||||||
) {
|
) {
|
||||||
const filterOptions = [accessibleBy(ability).WorkspaceMember];
|
const filterOptions = [accessibleBy(ability).WorkspaceMember];
|
||||||
if (args.where) filterOptions.push(args.where);
|
if (args.where) filterOptions.push(args.where);
|
||||||
return this.prismaService.client.hook.findMany({
|
return this.prismaService.client.webHook.findMany({
|
||||||
...args,
|
...args,
|
||||||
where: { AND: filterOptions },
|
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[]
|
views View[]
|
||||||
viewSorts ViewSort[]
|
viewSorts ViewSort[]
|
||||||
apiKeys ApiKey[]
|
apiKeys ApiKey[]
|
||||||
hooks Hook[]
|
webHooks WebHook[]
|
||||||
|
|
||||||
/// @TypeGraphQL.omit(input: true, output: true)
|
/// @TypeGraphQL.omit(input: true, output: true)
|
||||||
deletedAt DateTime?
|
deletedAt DateTime?
|
||||||
@@ -910,7 +910,7 @@ model ApiKey {
|
|||||||
@@map("api_keys")
|
@@map("api_keys")
|
||||||
}
|
}
|
||||||
|
|
||||||
model Hook {
|
model WebHook {
|
||||||
/// @Validator.IsString()
|
/// @Validator.IsString()
|
||||||
/// @Validator.IsOptional()
|
/// @Validator.IsOptional()
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
@@ -925,5 +925,5 @@ model Hook {
|
|||||||
/// @TypeGraphQL.omit(input: true, output: true)
|
/// @TypeGraphQL.omit(input: true, output: true)
|
||||||
deletedAt DateTime?
|
deletedAt DateTime?
|
||||||
|
|
||||||
@@map("hooks")
|
@@map("web_hooks")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,5 +22,5 @@ export type ModelSelectMap = {
|
|||||||
ViewSort: Prisma.ViewSortSelect;
|
ViewSort: Prisma.ViewSortSelect;
|
||||||
ViewField: Prisma.ViewFieldSelect;
|
ViewField: Prisma.ViewFieldSelect;
|
||||||
ApiKey: Prisma.ApiKeySelect;
|
ApiKey: Prisma.ApiKeySelect;
|
||||||
Hook: Prisma.HookSelect;
|
WebHook: Prisma.WebHookSelect;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user