From 7663067433c8e46f8265c5297f5c75360875621a Mon Sep 17 00:00:00 2001 From: Ana Sofia Marin Alexandre Date: Mon, 21 Oct 2024 12:17:52 +0200 Subject: [PATCH] add jwt support for webhooks graphs --- .../twenty-front/src/generated/graphql.tsx | 10 +++++---- .../modules/auth/states/currentUserState.ts | 1 + .../webhook/hooks/useAnalyticsTinybirdJwt.ts | 8 +++++++ .../developers/webhook/hooks/useGraphData.tsx | 3 +++ .../webhook/utils/fetchGraphDataOrThrow.ts | 6 +++-- .../graphql/fragments/userQueryFragment.ts | 1 + .../analytics/analytics.module.ts | 17 ++++++++++++++ .../analytics/analytics.service.ts | 22 ++++++++++++++++++- .../environment/environment-variables.ts | 16 ++++++++++++-- .../engine/core-modules/user/user.module.ts | 2 ++ .../engine/core-modules/user/user.resolver.ts | 11 ++++++++++ 11 files changed, 88 insertions(+), 9 deletions(-) create mode 100644 packages/twenty-front/src/modules/settings/developers/webhook/hooks/useAnalyticsTinybirdJwt.ts diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 7f053fc6b10f..aab2dbddd2d5 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -1058,6 +1058,7 @@ export type UpdateWorkspaceInput = { export type User = { __typename?: 'User'; + analyticsTinybirdJwt?: Maybe; canImpersonate: Scalars['Boolean']; createdAt: Scalars['DateTime']; defaultAvatarUrl?: Maybe; @@ -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 diff --git a/packages/twenty-front/src/modules/auth/states/currentUserState.ts b/packages/twenty-front/src/modules/auth/states/currentUserState.ts index 2feedc94fea6..352013254120 100644 --- a/packages/twenty-front/src/modules/auth/states/currentUserState.ts +++ b/packages/twenty-front/src/modules/auth/states/currentUserState.ts @@ -7,6 +7,7 @@ export type CurrentUser = Pick< | 'id' | 'email' | 'supportUserHash' + | 'analyticsTinybirdJwt' | 'canImpersonate' | 'onboardingStatus' | 'userVars' diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/hooks/useAnalyticsTinybirdJwt.ts b/packages/twenty-front/src/modules/settings/developers/webhook/hooks/useAnalyticsTinybirdJwt.ts new file mode 100644 index 000000000000..adaa625cd815 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/developers/webhook/hooks/useAnalyticsTinybirdJwt.ts @@ -0,0 +1,8 @@ +import { useRecoilValue } from 'recoil'; + +import { currentUserState } from '@/auth/states/currentUserState'; + +export const useAnalyticsTinybirdJwt = (): string | null | undefined => { + const currentUser = useRecoilValue(currentUserState); + return currentUser?.analyticsTinybirdJwt; +}; diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/hooks/useGraphData.tsx b/packages/twenty-front/src/modules/settings/developers/webhook/hooks/useGraphData.tsx index 62fc6d4ad952..e90d137b8611 100644 --- a/packages/twenty-front/src/modules/settings/developers/webhook/hooks/useGraphData.tsx +++ b/packages/twenty-front/src/modules/settings/developers/webhook/hooks/useGraphData.tsx @@ -1,9 +1,11 @@ +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'; export const useGraphData = (webhookId: string) => { const { enqueueSnackBar } = useSnackBar(); + const supportTinybirdJwt = useAnalyticsTinybirdJwt(); const fetchGraphData = async ( windowLengthGraphOption: '7D' | '1D' | '12H' | '4H', ) => { @@ -11,6 +13,7 @@ export const useGraphData = (webhookId: string) => { return await fetchGraphDataOrThrow({ webhookId, windowLength: windowLengthGraphOption, + tinybirdJwt: supportTinybirdJwt ?? undefined, }); } catch (error) { enqueueSnackBar('Something went wrong while fetching webhook usage', { diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/utils/fetchGraphDataOrThrow.ts b/packages/twenty-front/src/modules/settings/developers/webhook/utils/fetchGraphDataOrThrow.ts index b7123f712579..4fc280598e5a 100644 --- a/packages/twenty-front/src/modules/settings/developers/webhook/utils/fetchGraphDataOrThrow.ts +++ b/packages/twenty-front/src/modules/settings/developers/webhook/utils/fetchGraphDataOrThrow.ts @@ -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, }, }, ); diff --git a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts index 76e5400c34e9..8cdb26be8a17 100644 --- a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts +++ b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts @@ -8,6 +8,7 @@ export const USER_QUERY_FRAGMENT = gql` email canImpersonate supportUserHash + analyticsTinybirdJwt onboardingStatus workspaceMember { ...WorkspaceMemberQueryFragment diff --git a/packages/twenty-server/src/engine/core-modules/analytics/analytics.module.ts b/packages/twenty-server/src/engine/core-modules/analytics/analytics.module.ts index 2b4c8705d62d..6eb0353f9123 100644 --- a/packages/twenty-server/src/engine/core-modules/analytics/analytics.module.ts +++ b/packages/twenty-server/src/engine/core-modules/analytics/analytics.module.ts @@ -1,17 +1,34 @@ +/* eslint-disable no-restricted-imports */ import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; +import { JwtModule as NestJwtModule } from '@nestjs/jwt'; + +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { AnalyticsResolver } from './analytics.resolver'; import { AnalyticsService } from './analytics.service'; const TINYBIRD_BASE_URL = 'https://api.eu-central-1.aws.tinybird.co/v0'; +const InternalTinybirdJwtModule = NestJwtModule.registerAsync({ + useFactory: async (environmentService: EnvironmentService) => { + return { + secret: environmentService.get('TINYBIRD_JWT_TOKEN'), + signOptions: { + expiresIn: environmentService.get('TINYBIRD_TOKEN_EXPIRES_IN'), + }, + }; + }, + inject: [EnvironmentService], +}); + @Module({ providers: [AnalyticsResolver, AnalyticsService], imports: [ HttpModule.register({ baseURL: TINYBIRD_BASE_URL, }), + InternalTinybirdJwtModule, ], exports: [AnalyticsService], }) diff --git a/packages/twenty-server/src/engine/core-modules/analytics/analytics.service.ts b/packages/twenty-server/src/engine/core-modules/analytics/analytics.service.ts index 2b2eeb3d683e..eac672332da5 100644 --- a/packages/twenty-server/src/engine/core-modules/analytics/analytics.service.ts +++ b/packages/twenty-server/src/engine/core-modules/analytics/analytics.service.ts @@ -1,5 +1,6 @@ import { HttpService } from '@nestjs/axios'; import { Injectable, Logger } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; import { AxiosRequestConfig } from 'axios'; @@ -18,6 +19,7 @@ export class AnalyticsService { constructor( private readonly environmentService: EnvironmentService, private readonly httpService: HttpService, + private readonly jwtService: JwtService, ) {} async create( @@ -58,7 +60,7 @@ export class AnalyticsService { const config: AxiosRequestConfig = { headers: { Authorization: - 'Bearer ' + this.environmentService.get('TINYBIRD_TOKEN'), + 'Bearer ' + this.environmentService.get('TINYBIRD_DATASOURCE_TOKEN'), }, }; @@ -86,4 +88,22 @@ export class AnalyticsService { return { success: true }; } + + async generateJWT(workspaceId: string | null | 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.jwtService.sign(payload); + } } diff --git a/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts b/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts index faab8c9d6497..b244fbf73154 100644 --- a/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts +++ b/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts @@ -91,11 +91,23 @@ export class EnvironmentVariables { @CastToBoolean() @IsOptional() @IsBoolean() - ANALYTICS_ENABLED = false; + ANALYTICS_ENABLED = true; @IsString() @ValidateIf((env) => env.ANALYTICS_ENABLED) - TINYBIRD_TOKEN: string; + TINYBIRD_DATASOURCE_TOKEN: string; + + @IsString() + @ValidateIf((env) => env.ANALYTICS_ENABLED) + TINYBIRD_WORKSPACE_UUID: string; + + @IsString() + @ValidateIf((env) => env.ANALYTICS_ENABLED) + TINYBIRD_TOKEN_EXPIRES_IN: string; + + @IsString() + @ValidateIf((env) => env.ANALYTICS_ENABLED) + TINYBIRD_JWT_TOKEN: string; @CastToPositiveNumber() @IsNumber() diff --git a/packages/twenty-server/src/engine/core-modules/user/user.module.ts b/packages/twenty-server/src/engine/core-modules/user/user.module.ts index 7a776cca2508..57ea2140cec2 100644 --- a/packages/twenty-server/src/engine/core-modules/user/user.module.ts +++ b/packages/twenty-server/src/engine/core-modules/user/user.module.ts @@ -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], diff --git a/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts b/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts index d4304622af76..1020ad548271 100644 --- a/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts @@ -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 { + return await this.analyticsservice.generateJWT(workspace?.id); + } + @Mutation(() => String) async uploadProfilePicture( @AuthUser() { id }: User,