Secure connexion between TinyBird and webhookResponseGraph (#7913)

TLDR:
Secure connexion between tinybird and twenty using jwt when accessing
datasource from tinybird.

Solves:
https://github.com/twentyhq/private-issues/issues/73


In order to test:

1. Set ANALYTICS_ENABLED to true
2. Set TINYBIRD_JWT_TOKEN to the ADMIN token from the workspace
twenty_analytics_playground
3. Set TINYBIRD_JWT_TOKEN to the datasource or your admin token from the
workspace twenty_analytics_playground
4. Create a Webhook in twenty and set wich events it needs to track
5. Run twenty-worker in order to make the webhooks work.
6. Do your tasks in order to populate the data
7. Enter to settings> webhook>your webhook and the statistics section
should be displayed.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Ana Sofia Marin Alexandre 2024-10-21 12:42:52 -03:00 committed by GitHub
parent edf4ae084b
commit 373926b895
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 178 additions and 46 deletions

View File

@ -34,10 +34,10 @@ jobs:
packages/twenty-chrome-extension/**
- name: Install dependencies
if: steps.changed-files.outputs.changed == 'true'
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/yarn-install
- name: Chrome Extension / Run build
if: steps.changed-files.outputs.changed == 'true'
if: steps.changed-files.outputs.any_changed == 'true'
run: npx nx build twenty-chrome-extension
- name: Mark as Valid if No Changes

View File

@ -37,30 +37,33 @@ jobs:
id: changed-files
uses: tj-actions/changed-files@v11
with:
files: 'package.json, packages/twenty-server/**, packages/twenty-emails/**'
files: |
package.json
packages/twenty-server/**
packages/twenty-emails/**
- name: Install dependencies
if: steps.changed-files.outputs.changed == 'true'
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/yarn-install
- name: Server / Restore Task Cache
if: steps.changed-files.outputs.changed == 'true'
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/task-cache
with:
tag: scope:backend
- name: Server / Run lint & typecheck
if: steps.changed-files.outputs.changed == 'true'
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/nx-affected
with:
tag: scope:backend
tasks: lint,typecheck
- name: Server / Build
if: steps.changed-files.outputs.changed == 'true'
if: steps.changed-files.outputs.any_changed == 'true'
run: npx nx build twenty-server
- name: Server / Write .env
if: steps.changed-files.outputs.changed == 'true'
if: steps.changed-files.outputs.any_changed == 'true'
run: npx nx reset:env twenty-server
- name: Worker / Run
if: steps.changed-files.outputs.changed == 'true'
if: steps.changed-files.outputs.any_changed == 'true'
run: npx nx run twenty-server:worker:ci
server-test:
@ -78,18 +81,21 @@ jobs:
id: changed-files
uses: tj-actions/changed-files@v11
with:
files: 'package.json, packages/twenty-server/**, packages/twenty-emails/**'
files: |
package.json
packages/twenty-server/**
packages/twenty-emails/**
- name: Install dependencies
if: steps.changed-files.outputs.changed == 'true'
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/yarn-install
- name: Server / Restore Task Cache
if: steps.changed-files.outputs.changed == 'true'
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/task-cache
with:
tag: scope:backend
- name: Server / Run Tests
if: steps.changed-files.outputs.changed == 'true'
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/nx-affected
with:
tag: scope:backend
@ -122,18 +128,21 @@ jobs:
id: changed-files
uses: tj-actions/changed-files@v11
with:
files: 'package.json, packages/twenty-server/**, packages/twenty-emails/**'
files: |
package.json
packages/twenty-server/**
packages/twenty-emails/**
- name: Install dependencies
if: steps.changed-files.outputs.changed == 'true'
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/yarn-install
- name: Server / Restore Task Cache
if: steps.changed-files.outputs.changed == 'true'
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/task-cache
with:
tag: scope:backend
- name: Server / Run Integration Tests
if: steps.changed-files.outputs.changed == 'true'
if: steps.changed-files.outputs.any_changed == 'true'
uses: ./.github/workflows/actions/nx-affected
with:
tag: scope:backend

View File

@ -45,5 +45,5 @@
"search.exclude": {
"**/.yarn": true,
},
"eslint.debug": true
"eslint.debug": true,
}

View File

@ -1058,6 +1058,7 @@ export type UpdateWorkspaceInput = {
export type User = {
__typename?: 'User';
analyticsTinybirdJwt?: Maybe<Scalars['String']>;
canImpersonate: Scalars['Boolean'];
createdAt: Scalars['DateTime'];
defaultAvatarUrl?: Maybe<Scalars['String']>;
@ -1520,7 +1521,7 @@ export type ImpersonateMutationVariables = Exact<{
}>;
export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
export type RenewTokenMutationVariables = Exact<{
appToken: Scalars['String'];
@ -1553,7 +1554,7 @@ export type VerifyMutationVariables = Exact<{
}>;
export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
export type CheckUserExistsQueryVariables = Exact<{
email: Scalars['String'];
@ -1607,7 +1608,7 @@ export type SkipSyncEmailOnboardingStepMutationVariables = Exact<{ [key: string]
export type SkipSyncEmailOnboardingStepMutation = { __typename?: 'Mutation', skipSyncEmailOnboardingStep: { __typename?: 'OnboardingStepSuccess', success: boolean } };
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> };
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> };
export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>;
@ -1624,7 +1625,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> } };
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> } };
export type ActivateWorkflowVersionMutationVariables = Exact<{
workflowVersionId: Scalars['String'];
@ -1825,6 +1826,7 @@ export const UserQueryFragmentFragmentDoc = gql`
email
canImpersonate
supportUserHash
analyticsTinybirdJwt
onboardingStatus
workspaceMember {
...WorkspaceMemberQueryFragment

View File

@ -7,6 +7,7 @@ export type CurrentUser = Pick<
| 'id'
| 'email'
| 'supportUserHash'
| 'analyticsTinybirdJwt'
| 'canImpersonate'
| 'onboardingStatus'
| 'userVars'

View File

@ -1,6 +1,6 @@
import { useGraphData } from '@/settings/developers/webhook/hooks/useGraphData';
import { webhookGraphDataState } from '@/settings/developers/webhook/states/webhookGraphDataState';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useSetRecoilState } from 'recoil';
type SettingsDevelopersWebhookUsageGraphEffectProps = {
@ -11,14 +11,18 @@ export const SettingsDevelopersWebhookUsageGraphEffect = ({
webhookId,
}: SettingsDevelopersWebhookUsageGraphEffectProps) => {
const setWebhookGraphData = useSetRecoilState(webhookGraphDataState);
const [isLoaded, setIsLoaded] = useState(false);
const { fetchGraphData } = useGraphData(webhookId);
useEffect(() => {
fetchGraphData('7D').then((graphInput) => {
setWebhookGraphData(graphInput);
});
}, [fetchGraphData, setWebhookGraphData, webhookId]);
if (!isLoaded) {
fetchGraphData('7D').then((graphInput) => {
setWebhookGraphData(graphInput);
});
setIsLoaded(true);
}
}, [fetchGraphData, isLoaded, setWebhookGraphData, webhookId]);
return <></>;
};

View File

@ -0,0 +1,47 @@
import { renderHook } from '@testing-library/react';
import { CurrentUser, currentUserState } from '@/auth/states/currentUserState';
import { useAnalyticsTinybirdJwt } from '@/settings/developers/webhook/hooks/useAnalyticsTinybirdJwt';
import { act } from 'react';
import { useSetRecoilState } from 'recoil';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
const Wrapper = getJestMetadataAndApolloMocksWrapper({
apolloMocks: [],
});
describe('useAnalyticsTinybirdJwt', () => {
it('should return the analytics jwt token', async () => {
const { result } = renderHook(
() => {
const setCurrentUserState = useSetRecoilState(currentUserState);
return {
useAnalyticsTinybirdJwt: useAnalyticsTinybirdJwt(),
setCurrentUserState,
};
},
{ wrapper: Wrapper },
);
act(() => {
result.current.setCurrentUserState({
analyticsTinybirdJwt: 'jwt',
} as CurrentUser);
});
expect(result.current.useAnalyticsTinybirdJwt).toBe('jwt');
act(() => {
result.current.setCurrentUserState(null);
});
expect(result.current.useAnalyticsTinybirdJwt).toBeUndefined();
act(() => {
result.current.setCurrentUserState({} as CurrentUser);
});
expect(result.current.useAnalyticsTinybirdJwt).toBeUndefined();
});
});

View File

@ -0,0 +1,18 @@
import { useRecoilValue } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { isNull } from '@sniptt/guards';
export const useAnalyticsTinybirdJwt = (): string | undefined => {
const currentUser = useRecoilValue(currentUserState);
if (!currentUser) {
return undefined;
}
if (isNull(currentUser.analyticsTinybirdJwt)) {
return undefined;
}
return currentUser.analyticsTinybirdJwt;
};

View File

@ -1,16 +1,24 @@
import { useAnalyticsTinybirdJwt } from '@/settings/developers/webhook/hooks/useAnalyticsTinybirdJwt';
import { fetchGraphDataOrThrow } from '@/settings/developers/webhook/utils/fetchGraphDataOrThrow';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { isUndefined } from '@sniptt/guards';
export const useGraphData = (webhookId: string) => {
const { enqueueSnackBar } = useSnackBar();
const analyticsTinybirdJwt = useAnalyticsTinybirdJwt();
const fetchGraphData = async (
windowLengthGraphOption: '7D' | '1D' | '12H' | '4H',
) => {
try {
if (isUndefined(analyticsTinybirdJwt)) {
throw new Error('No analyticsTinybirdJwt found');
}
return await fetchGraphDataOrThrow({
webhookId,
windowLength: windowLengthGraphOption,
tinybirdJwt: analyticsTinybirdJwt,
});
} catch (error) {
enqueueSnackBar('Something went wrong while fetching webhook usage', {

View File

@ -4,22 +4,24 @@ import { WEBHOOK_GRAPH_API_OPTIONS_MAP } from '@/settings/developers/webhook/con
type fetchGraphDataOrThrowProps = {
webhookId: string;
windowLength: '7D' | '1D' | '12H' | '4H';
tinybirdJwt: string;
};
export const fetchGraphDataOrThrow = async ({
webhookId,
windowLength,
tinybirdJwt,
}: fetchGraphDataOrThrowProps) => {
const queryString = new URLSearchParams({
...WEBHOOK_GRAPH_API_OPTIONS_MAP[windowLength],
webhookIdRequest: webhookId,
}).toString();
const token = 'REPLACE_ME';
const response = await fetch(
`https://api.eu-central-1.aws.tinybird.co/v0/pipes/getWebhooksAnalyticsV2.json?${queryString}`,
{
headers: {
Authorization: 'Bearer ' + token,
Authorization: 'Bearer ' + tinybirdJwt,
},
},
);

View File

@ -8,6 +8,7 @@ export const USER_QUERY_FRAGMENT = gql`
email
canImpersonate
supportUserHash
analyticsTinybirdJwt
onboardingStatus
workspaceMember {
...WorkspaceMemberQueryFragment

View File

@ -1,6 +1,8 @@
import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
import { AnalyticsResolver } from './analytics.resolver';
import { AnalyticsService } from './analytics.service';
@ -9,6 +11,7 @@ const TINYBIRD_BASE_URL = 'https://api.eu-central-1.aws.tinybird.co/v0';
@Module({
providers: [AnalyticsResolver, AnalyticsService],
imports: [
JwtModule,
HttpModule.register({
baseURL: TINYBIRD_BASE_URL,
}),

View File

@ -1,7 +1,4 @@
import { Test, TestingModule } from '@nestjs/testing';
import { HttpService } from '@nestjs/axios';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { AnalyticsResolver } from './analytics.resolver';
import { AnalyticsService } from './analytics.service';
@ -13,13 +10,8 @@ describe('AnalyticsResolver', () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AnalyticsResolver,
AnalyticsService,
{
provide: EnvironmentService,
useValue: {},
},
{
provide: HttpService,
provide: AnalyticsService,
useValue: {},
},
],

View File

@ -1,6 +1,5 @@
import { Args, Mutation, Resolver } from '@nestjs/graphql';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
@ -13,10 +12,7 @@ import { CreateAnalyticsInput } from './dtos/create-analytics.input';
@Resolver(() => Analytics)
export class AnalyticsResolver {
constructor(
private readonly analyticsService: AnalyticsService,
private readonly environmentService: EnvironmentService,
) {}
constructor(private readonly analyticsService: AnalyticsService) {}
@Mutation(() => Analytics)
track(

View File

@ -1,7 +1,8 @@
import { Test, TestingModule } from '@nestjs/testing';
import { HttpService } from '@nestjs/axios';
import { Test, TestingModule } from '@nestjs/testing';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
import { AnalyticsService } from './analytics.service';
@ -16,6 +17,10 @@ describe('AnalyticsService', () => {
provide: EnvironmentService,
useValue: {},
},
{
provide: JwtWrapperService,
useValue: {},
},
{
provide: HttpService,
useValue: {},

View File

@ -4,6 +4,7 @@ import { Injectable, Logger } from '@nestjs/common';
import { AxiosRequestConfig } from 'axios';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
type CreateEventInput = {
action: string;
@ -16,6 +17,7 @@ export class AnalyticsService {
private readonly defaultDatasource = 'event';
constructor(
private readonly jwtWrapperService: JwtWrapperService,
private readonly environmentService: EnvironmentService,
private readonly httpService: HttpService,
) {}
@ -58,7 +60,7 @@ export class AnalyticsService {
const config: AxiosRequestConfig = {
headers: {
Authorization:
'Bearer ' + this.environmentService.get('TINYBIRD_TOKEN'),
'Bearer ' + this.environmentService.get('TINYBIRD_INGEST_TOKEN'),
},
};
@ -86,4 +88,25 @@ export class AnalyticsService {
return { success: true };
}
async generateWorkspaceJwt(workspaceId: string | undefined) {
const pipeId = 't_b49e0fe60f9e438eae81cb31c5260df2'; // refactor this pass as params
//perhaps a constant of name:pipeId??? better typing in this func^
const payload = {
name: 'my_demo_jwt',
workspace_id: this.environmentService.get('TINYBIRD_WORKSPACE_UUID'),
scopes: [
{
type: 'PIPES:READ',
resource: pipeId,
fixed_params: { workspaceId: workspaceId },
},
],
};
return this.jwtWrapperService.sign(payload, {
secret: this.environmentService.get('TINYBIRD_GENERATE_JWT_TOKEN'),
expiresIn: '7d',
});
}
}

View File

@ -95,7 +95,15 @@ export class EnvironmentVariables {
@IsString()
@ValidateIf((env) => env.ANALYTICS_ENABLED)
TINYBIRD_TOKEN: string;
TINYBIRD_INGEST_TOKEN: string;
@IsString()
@ValidateIf((env) => env.ANALYTICS_ENABLED)
TINYBIRD_WORKSPACE_UUID: string;
@IsString()
@ValidateIf((env) => env.ANALYTICS_ENABLED)
TINYBIRD_GENERATE_JWT_TOKEN: string;
@CastToPositiveNumber()
@IsNumber()

View File

@ -7,6 +7,7 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module';
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
@ -37,6 +38,7 @@ import { UserService } from './services/user.service';
OnboardingModule,
TypeOrmModule.forFeature([KeyValuePair], 'core'),
UserVarsModule,
AnalyticsModule,
],
exports: [UserService],
providers: [UserService, UserResolver, TypeORMService],

View File

@ -19,6 +19,7 @@ import { Repository } from 'typeorm';
import { SupportDriver } from 'src/engine/core-modules/environment/interfaces/support.interface';
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
import { AnalyticsService } from 'src/engine/core-modules/analytics/analytics.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
@ -55,6 +56,7 @@ export class UserResolver {
private readonly onboardingService: OnboardingService,
private readonly userVarService: UserVarsService,
private readonly fileService: FileService,
private readonly analyticsService: AnalyticsService,
) {}
@Query(() => User)
@ -154,6 +156,15 @@ export class UserResolver {
return getHMACKey(parent.email, key);
}
@ResolveField(() => String, {
nullable: true,
})
async analyticsTinybirdJwt(
@AuthWorkspace() workspace: Workspace | undefined,
): Promise<string> {
return await this.analyticsService.generateWorkspaceJwt(workspace?.id);
}
@Mutation(() => String)
async uploadProfilePicture(
@AuthUser() { id }: User,