Skip to content

Commit 0546c5c

Browse files
authored
Merge pull request #8901 from ever-co/fix/zapier-plugin-integration
[Improvement] Set polling URL, dynamic dropdowns and improve OAuth authorization
2 parents 01a6fdb + d7ca3da commit 0546c5c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+2041
-561
lines changed

Diff for: .env.compose

+6
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,12 @@ GAUZY_ZAPIER_CLIENT_ID=XXXXXXXXX
149149
GAUZY_ZAPIER_CLIENT_SECRET=XXXXXXX
150150
GAUZY_ZAPIER_REDIRECT_URL=http://localhost:3000/api/integration/zapier/oauth/callback
151151
GAUZY_ZAPIER_POST_INSTALL_URL="http://localhost:4200/#/pages/integrations/zapier"
152+
# Comma-separated list of domains allowed for OAuth redirects (security feature)
153+
GAUZY_ZAPIER_ALLOWED_DOMAINS=gauzy.co,ever.co,staging.gauzy.co,demo.gauzy.co,zapier.com,github.com,upwork.com,hubstaff.com,fiverr.com,api,webapp
154+
# Maximum number of OAuth authorization codes to store in memory
155+
GAUZY_ZAPIER_MAX_AUTH_CODES=1000
156+
# Number of server instances (affects auth code cleanup behavior)
157+
GAUZY_ZAPIER_INSTANCE_COUNT=1
152158

153159
# Github App Install Integration
154160
GAUZY_GITHUB_APP_NAME=

Diff for: .env.demo.compose

+6
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,12 @@ GAUZY_ZAPIER_CLIENT_ID=XXXXXXXXX
151151
GAUZY_ZAPIER_CLIENT_SECRET=XXXXXXX
152152
GAUZY_ZAPIER_REDIRECT_URL=http://localhost:3000/api/integration/zapier/oauth/callback
153153
GAUZY_ZAPIER_POST_INSTALL_URL="http://localhost:4200/#/pages/integrations/zapier"
154+
# Comma-separated list of domains allowed for OAuth redirects (security feature)
155+
GAUZY_ZAPIER_ALLOWED_DOMAINS=gauzy.co,ever.co,staging.gauzy.co,demo.gauzy.co,zapier.com,github.com,upwork.com,hubstaff.com,fiverr.com,api,webapp
156+
# Maximum number of OAuth authorization codes to store in memory
157+
GAUZY_ZAPIER_MAX_AUTH_CODES=1000
158+
# Number of server instances (affects auth code cleanup behavior)
159+
GAUZY_ZAPIER_INSTANCE_COUNT=1
154160

155161
# Github App Install Integration
156162
GAUZY_GITHUB_APP_NAME=

Diff for: .env.docker

+6
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,12 @@ GAUZY_ZAPIER_CLIENT_ID=XXXXXXXXX
148148
GAUZY_ZAPIER_CLIENT_SECRET=XXXXXXX
149149
GAUZY_ZAPIER_REDIRECT_URL=http://localhost:3000/api/integration/zapier/oauth/callback
150150
GAUZY_ZAPIER_POST_INSTALL_URL="http://localhost:4200/#/pages/integrations/zapier"
151+
# Comma-separated list of domains allowed for OAuth redirects (security feature)
152+
GAUZY_ZAPIER_ALLOWED_DOMAINS=gauzy.co,ever.co,staging.gauzy.co,demo.gauzy.co,zapier.com,github.com,upwork.com,hubstaff.com,fiverr.com,api,webapp
153+
# Maximum number of OAuth authorization codes to store in memory
154+
GAUZY_ZAPIER_MAX_AUTH_CODES=1000
155+
# Number of server instances (affects auth code cleanup behavior)
156+
GAUZY_ZAPIER_INSTANCE_COUNT=1
151157

152158
# Github App Install Integration
153159
GAUZY_GITHUB_APP_NAME=

Diff for: .env.local

+6
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,12 @@ GAUZY_ZAPIER_CLIENT_ID=
165165
GAUZY_ZAPIER_CLIENT_SECRET=
166166
GAUZY_ZAPIER_REDIRECT_URL=http://localhost:3000/api/integration/zapier/oauth/callback
167167
GAUZY_ZAPIER_POST_INSTALL_URL="http://localhost:4200/#/pages/integrations/zapier"
168+
# Comma-separated list of domains allowed for OAuth redirects (security feature)
169+
GAUZY_ZAPIER_ALLOWED_DOMAINS=gauzy.co,ever.co,staging.gauzy.co,demo.gauzy.co,zapier.com,github.com,upwork.com,hubstaff.com,fiverr.com,api,webapp
170+
# Maximum number of OAuth authorization codes to store in memory
171+
GAUZY_ZAPIER_MAX_AUTH_CODES=1000
172+
# Number of server instances (affects auth code cleanup behavior)
173+
GAUZY_ZAPIER_INSTANCE_COUNT=1
168174

169175
# Third Party Integration Config
170176
INTEGRATED_USER_DEFAULT_PASS=

Diff for: .env.sample

+6
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,12 @@ GAUZY_ZAPIER_CLIENT_ID=XXXXXXXXX
153153
GAUZY_ZAPIER_CLIENT_SECRET=XXXXXXX
154154
GAUZY_ZAPIER_REDIRECT_URL=http://localhost:3000/api/integration/zapier/oauth/callback
155155
GAUZY_ZAPIER_POST_INSTALL_URL="http://localhost:4200/#/pages/integrations/zapier"
156+
# Comma-separated list of domains allowed for OAuth redirects (security feature)
157+
GAUZY_ZAPIER_ALLOWED_DOMAINS=gauzy.co,ever.co,staging.gauzy.co,demo.gauzy.co,zapier.com,github.com,upwork.com,hubstaff.com,fiverr.com,api,webapp
158+
# Maximum number of OAuth authorization codes to store in memory
159+
GAUZY_ZAPIER_MAX_AUTH_CODES=1000
160+
# Number of server instances (affects auth code cleanup behavior)
161+
GAUZY_ZAPIER_INSTANCE_COUNT=1
156162

157163
FIVERR_CLIENT_ID=XXXXXXX
158164
FIVERR_CLIENT_SECRET=XXXXXXX

Diff for: .scripts/icon-utils/icon-factory.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import * as dotenv from 'dotenv';
2+
dotenv.config();
3+
24
import { PlatformLogoGenerator } from './concrete-generators/platform-logo-generator';
35
import { DesktopIconGenerator } from './concrete-generators/desktop-icon-generator';
46
import { DesktopDefaultIconGenerator } from './concrete-generators/desktop-default-icon-generator';
57
import { NoInternetLogoGenerator } from './concrete-generators/no-internet-logo-generator';
68
import { DesktopEnvironmentManager } from '../electron-desktop-environment/desktop-environment-manager';
79

8-
dotenv.config();
910

1011
export class IconFactory {
1112
public static async generateDefaultIcons(): Promise<void> {

Diff for: packages/common/src/lib/interfaces/IZapierConfig.ts

+9
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,18 @@ export interface IZapierConfig {
88
/** OAuth client secret provided by Zapier for secure authentication */
99
readonly clientSecret: string;
1010

11+
/** Allowed domains for Zapier OAuth redirects */
12+
readonly allowedDomains: string[];
13+
1114
/** URI where Zapier will redirect after successful OAuth authorization */
1215
readonly redirectUri: string;
1316

1417
/** Optional URL where users will be directed after installing the Zapier integration */
1518
readonly postInstallUrl?: string;
19+
20+
/** Max authentication code number */
21+
readonly maxAuthCodes?: number;
22+
23+
/** Instance count for periodic cleanup */
24+
readonly instanceCount?: boolean | number;
1625
}

Diff for: packages/config/src/lib/config/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ import microsoft from './microsoft';
88
import setting from './setting';
99
import twitter from './twitter';
1010
import jira from './jira';
11+
import zapier from './zapier';
1112

1213
/**
1314
* This array contains individual configuration modules for different social login providers.
1415
*/
15-
export default [app, facebook, github, google, keycloak, linkedin, microsoft, setting, twitter, jira];
16+
export default [app, facebook, github, google, keycloak, linkedin, microsoft, setting, twitter, jira, zapier];

Diff for: packages/config/src/lib/config/zapier.ts

+25-12
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,33 @@ import { IZapierConfig } from '@gauzy/common';
44
/**
55
* Register Zapier OAuth configuration using @nestjs/config
66
*/
7+
export default registerAs(
8+
'zapier',
9+
(): IZapierConfig => ({
10+
// Zapier OAuth Client ID
11+
clientId: process.env.GAUZY_ZAPIER_CLIENT_ID,
712

8-
export default registerAs (
9-
'zapier',
10-
(): IZapierConfig => ({
11-
// Zapier OAuth Client ID
12-
clientId: process.env.GAUZY_ZAPIER_CLIENT_ID || '',
13+
// Zapier OAuth Client Secret
14+
clientSecret: process.env.GAUZY_ZAPIER_CLIENT_SECRET,
1315

14-
// Zapier OAuth Client Secret
15-
clientSecret: process.env.GAUZY_ZAPIER_CLIENT_SECRET || '',
16+
// Zapier max auth codes
17+
maxAuthCodes: Number.parseInt(process.env.GAUZY_ZAPIER_MAX_AUTH_CODES) || 1000,
1618

17-
// Zapier Redirected URI after successful authentication
18-
redirectUri: process.env.GAUZY_ZAPIER_REDIRECT_URL || '',
19+
// Zapier instance count
20+
instanceCount: Number.parseInt(process.env.GAUZY_ZAPIER_INSTANCE_COUNT) || 1,
1921

20-
// Zapier post install URL
21-
postInstallUrl: process.env.GAUZY_ZAPIER_POST_INSTALL_URL || ''
22-
})
22+
// Zapier allowed domains
23+
allowedDomains: (process.env.GAUZY_ZAPIER_ALLOWED_DOMAINS ?? '')
24+
.split(',')
25+
.map((d) => d.trim())
26+
.filter(Boolean),
27+
28+
// Zapier Redirected URI after successful authentication
29+
redirectUri: process.env.GAUZY_ZAPIER_REDIRECT_URL || '',
30+
31+
// Zapier post install URL
32+
postInstallUrl:
33+
process.env.GAUZY_ZAPIER_POST_INSTALL_URL ??
34+
`${process.env.CLIENT_BASE_URL ?? ''}/#/pages/integrations/zapier`
35+
})
2336
);

Diff for: packages/config/src/lib/environments/environment.prod.ts

+6
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,12 @@ export const environment: IEnvironment = {
200200
zapier: {
201201
clientId: process.env.GAUZY_ZAPIER_CLIENT_ID,
202202
clientSecret: process.env.GAUZY_ZAPIER_CLIENT_SECRET,
203+
allowedDomains: (process.env.GAUZY_ZAPIER_ALLOWED_DOMAINS ?? process.env.GAUZY_ALLOWED_DOMAINS ?? '')
204+
.split(',')
205+
.map((d) => d.trim())
206+
.filter(Boolean),
207+
maxAuthCodes: Number.parseInt(process.env.GAUZY_ZAPIER_MAX_AUTH_CODES) || 1000,
208+
instanceCount: process.env.GAUZY_ZAPIER_INSTANCE_COUNT === 'true',
203209
redirectUri:
204210
process.env.GAUZY_ZAPIER_REDIRECT_URL || `${process.env.API_BASE_URL}/api/integrations/zapier/callback`,
205211
postInstallUrl:

Diff for: packages/config/src/lib/environments/environment.ts

+6
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,12 @@ export const environment: IEnvironment = {
200200
zapier: {
201201
clientId: process.env.GAUZY_ZAPIER_CLIENT_ID,
202202
clientSecret: process.env.GAUZY_ZAPIER_CLIENT_SECRET,
203+
allowedDomains: (process.env.GAUZY_ZAPIER_ALLOWED_DOMAINS ?? process.env.GAUZY_ALLOWED_DOMAINS ?? '')
204+
.split(',')
205+
.filter(Boolean)
206+
.map((domain) => domain.trim()),
207+
maxAuthCodes: Number.parseInt(process.env.GAUZY_ZAPIER_MAX_AUTH_CODES) || 1000,
208+
instanceCount: process.env.GAUZY_ZAPIER_INSTANCE_COUNT === 'true',
203209
redirectUri:
204210
process.env.GAUZY_ZAPIER_REDIRECT_URL || `${process.env.API_BASE_URL}/api/integrations/zapier/callback`,
205211
postInstallUrl:

Diff for: packages/config/src/lib/environments/ienvironment.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ import {
1717
IWasabiConfig,
1818
IJiraIntegrationConfig,
1919
IDigitalOceanConfig,
20-
IZapierConfig,
21-
IPosthogConfig
20+
IPosthogConfig,
21+
IZapierConfig
2222
} from '@gauzy/common';
2323
import { FileStorageProviderEnum } from '@gauzy/contracts';
2424

Diff for: packages/contracts/src/index.ts

-1
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,6 @@ export * from './lib/user-organization.model';
150150
export * from './lib/user.model';
151151
export * from './lib/wakatime.model';
152152
export * from './lib/plugin.model';
153-
export * from './lib/zapier.model';
154153

155154
export {
156155
ActorTypeEnum,

Diff for: packages/contracts/src/lib/zapier.model.ts

-29
This file was deleted.

Diff for: packages/core/src/lib/auth/auth.controller.ts

-1
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,6 @@ export class AuthController {
148148
async login(@Body() input: UserLoginDTO): Promise<IAuthResponse | null> {
149149
return await this.commandBus.execute(new AuthLoginCommand(input));
150150
}
151-
152151
/**
153152
* Sign in workspaces by email and password.
154153
*

Diff for: packages/plugins/integration-zapier/src/lib/dto/create-zapier-integration.dto.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ApiProperty } from '@nestjs/swagger';
22
import { IsString, IsNotEmpty, IsIn } from 'class-validator';
3-
import { ICreateZapierIntegrationInput, ZapierGrantType } from '@gauzy/contracts';
43
import { TenantOrganizationBaseDTO } from '@gauzy/core';
4+
import { ICreateZapierIntegrationInput, ZapierGrantType } from '../zapier.types';
55

66
export class CreateZapierIntegrationDto extends TenantOrganizationBaseDTO implements ICreateZapierIntegrationInput {
77
@ApiProperty({ type: () => String })
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { ZapierTimerStartedHandler } from './zapier-timer-started.handler';
2+
import { ZapierTimerStoppedHandler } from './zapier-timer-stopped.handler';
3+
4+
export const EventHandlers = [ZapierTimerStartedHandler, ZapierTimerStoppedHandler];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
2+
import { Injectable } from '@nestjs/common';
3+
import { TimerStartedEvent } from '@gauzy/core';
4+
import { ZapierWebhookService } from '../zapier-webhook.service';
5+
6+
@Injectable()
7+
@EventsHandler(TimerStartedEvent)
8+
export class ZapierTimerStartedHandler implements IEventHandler<TimerStartedEvent> {
9+
constructor(private readonly zapierWebhookService: ZapierWebhookService) { }
10+
11+
/**
12+
* Handles the TimerStartedEvent by notifying Zapier webhooks
13+
*
14+
* @param event - The TimerStartedEvent that contains the time log details
15+
* @returns A Promise that resolves once the webhooks are notified
16+
*/
17+
async handle(event: TimerStartedEvent): Promise<void> {
18+
const timeLog = event.timeLog;
19+
if (!timeLog.tenantId || !timeLog.organizationId) {
20+
console.warn('Cannot process timer started event: missing tenantId or organizationId')
21+
}
22+
await this.zapierWebhookService.notifyTimerStatusChanged({
23+
event: 'timer.status.changed',
24+
action: 'start',
25+
data: timeLog,
26+
tenantId: timeLog.tenantId,
27+
organizationId: timeLog.organizationId
28+
});
29+
}
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
2+
import { Injectable } from '@nestjs/common';
3+
import { TimerStoppedEvent } from '@gauzy/core';
4+
import { ZapierWebhookService } from '../zapier-webhook.service';
5+
6+
@Injectable()
7+
@EventsHandler(TimerStoppedEvent)
8+
export class ZapierTimerStoppedHandler implements IEventHandler<TimerStoppedEvent> {
9+
constructor(private readonly zapierWebhookService: ZapierWebhookService) {}
10+
11+
/**
12+
* Handles the TimerStoppedEvent by notifying Zapier webhooks
13+
*
14+
* @param event - The TimerStoppedEvent that contains the time log details.
15+
* @returns A Promise that resolves once the webhooks are notified
16+
*/
17+
async handle(event: TimerStoppedEvent): Promise<void> {
18+
const timeLog = event.timeLog;
19+
if (!timeLog.tenantId || !timeLog.organizationId) {
20+
console.warn('Cannot process timer stopped event: missing tenantId or organizationId')
21+
return;
22+
}
23+
await this.zapierWebhookService.notifyTimerStatusChanged({
24+
event: 'timer.status.changed',
25+
action: 'stop',
26+
data: timeLog,
27+
tenantId: timeLog.tenantId,
28+
organizationId: timeLog.organizationId
29+
});
30+
}
31+
}

Diff for: packages/plugins/integration-zapier/src/lib/integration-zapier.plugin.ts

+20-18
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,10 @@
11
import { Logger } from '@nestjs/common';
2-
import * as chalk from 'chalk';
3-
import { ApplicationPluginConfig, CustomEmbeddedFieldConfig, CustomEmbeddedFields } from '@gauzy/common';
2+
import { ConfigService } from '@nestjs/config';
3+
import { ApplicationPluginConfig, CustomEmbeddedFieldConfig } from '@gauzy/common';
44
import { GauzyCorePlugin as Plugin, IOnPluginBootstrap, IOnPluginDestroy } from '@gauzy/plugin';
55
import { ZapierModule } from './zapier.module';
66
import { ZapierWebhookSubscription } from './zapier-webhook-subscription.entity';
77

8-
// Extend the CustomEmbeddedFields interface to include our custom entities
9-
interface ZapierCustomFields extends CustomEmbeddedFields {
10-
IntegrationSetting?: CustomEmbeddedFieldConfig[];
11-
ZapierWebhookSubscription?: CustomEmbeddedFieldConfig[];
12-
}
13-
148
@Plugin({
159
/**
1610
* An array of modules that will be imported and registered with the plugin.
@@ -78,25 +72,33 @@ interface ZapierCustomFields extends CustomEmbeddedFields {
7872
}
7973
})
8074
export class IntegrationZapierPlugin implements IOnPluginBootstrap, IOnPluginDestroy {
81-
// We enable by default additional logging for each event to avoid cluttering the logs
82-
private logEnabled = true;
75+
private readonly logger = new Logger(IntegrationZapierPlugin.name);
76+
77+
constructor(private readonly _config: ConfigService) {}
8378

8479
/**
85-
* Called when the plugin is being initialized.
80+
* Lifecycle hook invoked during the plugin's bootstrap phase.
81+
* Validates essential Zapier OAuth configurations and logs the API base URL.
8682
*/
87-
onPluginBootstrap(): void | Promise<void> {
88-
if (this.logEnabled) {
89-
console.log(chalk.green(`${IntegrationZapierPlugin.name} is being bootstrapped...`));
83+
onPluginBootstrap(): void {
84+
this.logger.log(`${IntegrationZapierPlugin.name} is being bootstrapped...`);
85+
86+
const clientId = this._config.get<string>('zapier.clientId');
87+
const clientSecret = this._config.get<string>('zapier.clientSecret');
88+
89+
if (!clientId || !clientSecret) {
90+
this.logger.warn(
91+
'Zapier OAuth credentials are not fully configured. Please set GAUZY_ZAPIER_CLIENT_ID and GAUZY_ZAPIER_CLIENT_SECRET.'
92+
);
93+
} else {
94+
this.logger.log('Zapier OAuth credentials are configured successfully.');
9095
}
9196
}
9297

9398
/**
9499
* Called when the plugin is being destroyed.
95100
*/
96101
onPluginDestroy(): void | Promise<void> {
97-
if (this.logEnabled) {
98-
const logger = new Logger(IntegrationZapierPlugin.name);
99-
logger.log(`${IntegrationZapierPlugin.name} is being destroyed...`)
100-
}
102+
this.logger.log(`${IntegrationZapierPlugin.name} is being destroyed...`);
101103
}
102104
}

0 commit comments

Comments
 (0)