Skip to content

Commit

Permalink
Secure connexion between TinyBird and webhookResponseGraph (twentyhq#…
Browse files Browse the repository at this point in the history
…7913)

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

Solves:
twentyhq/private-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 <[email protected]>
  • Loading branch information
anamarn and charlesBochet authored Oct 21, 2024
1 parent edf4ae0 commit 373926b
Show file tree
Hide file tree
Showing 19 changed files with 178 additions and 46 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci-chrome-extension.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 24 additions & 15 deletions .github/workflows/ci-server.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,5 @@
"search.exclude": {
"**/.yarn": true,
},
"eslint.debug": true
"eslint.debug": true,
}
10 changes: 6 additions & 4 deletions packages/twenty-front/src/generated/graphql.tsx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type CurrentUser = Pick<
| 'id'
| 'email'
| 'supportUserHash'
| 'analyticsTinybirdJwt'
| 'canImpersonate'
| 'onboardingStatus'
| 'userVars'
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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 <></>;
};
Original file line number Diff line number Diff line change
@@ -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();
});
});
Original file line number Diff line number Diff line change
@@ -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;
};
Original file line number Diff line number Diff line change
@@ -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', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const USER_QUERY_FRAGMENT = gql`
email
canImpersonate
supportUserHash
analyticsTinybirdJwt
onboardingStatus
workspaceMember {
...WorkspaceMemberQueryFragment
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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,
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -13,13 +10,8 @@ describe('AnalyticsResolver', () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AnalyticsResolver,
AnalyticsService,
{
provide: EnvironmentService,
useValue: {},
},
{
provide: HttpService,
provide: AnalyticsService,
useValue: {},
},
],
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -16,6 +17,10 @@ describe('AnalyticsService', () => {
provide: EnvironmentService,
useValue: {},
},
{
provide: JwtWrapperService,
useValue: {},
},
{
provide: HttpService,
useValue: {},
Expand Down
Loading

0 comments on commit 373926b

Please sign in to comment.