Skip to content

[Improvement] Set polling URL, dynamic dropdowns and improve OAuth authorization #8901

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 21 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.compose
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ GAUZY_ZAPIER_CLIENT_ID=XXXXXXXXX
GAUZY_ZAPIER_CLIENT_SECRET=XXXXXXX
GAUZY_ZAPIER_REDIRECT_URL=http://localhost:3000/api/integration/zapier/oauth/callback
GAUZY_ZAPIER_POST_INSTALL_URL="http://localhost:4200/#/pages/integrations/zapier"
# Comma-separated list of domains allowed for OAuth redirects (security feature)
ALLOWED_DOMAINS=gauzy.co,ever.co,staging.gauzy.co,demo.gauzy.co,zapier.com,github.com,upwork.com,hubstaff.com,fiverr.com
MAX_AUTH_CODES=1000

# Github App Install Integration
GAUZY_GITHUB_APP_NAME=
Expand Down
3 changes: 3 additions & 0 deletions .env.demo.compose
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ GAUZY_ZAPIER_CLIENT_ID=XXXXXXXXX
GAUZY_ZAPIER_CLIENT_SECRET=XXXXXXX
GAUZY_ZAPIER_REDIRECT_URL=http://localhost:3000/api/integration/zapier/oauth/callback
GAUZY_ZAPIER_POST_INSTALL_URL="http://localhost:4200/#/pages/integrations/zapier"
# Comma-separated list of domains allowed for OAuth redirects (security feature)
ALLOWED_DOMAINS=gauzy.co,ever.co,staging.gauzy.co,demo.gauzy.co,zapier.com,github.com,upwork.com,hubstaff.com,fiverr.com
MAX_AUTH_CODES=1000

# Github App Install Integration
GAUZY_GITHUB_APP_NAME=
Expand Down
3 changes: 3 additions & 0 deletions .env.docker
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ GAUZY_ZAPIER_CLIENT_ID=XXXXXXXXX
GAUZY_ZAPIER_CLIENT_SECRET=XXXXXXX
GAUZY_ZAPIER_REDIRECT_URL=http://localhost:3000/api/integration/zapier/oauth/callback
GAUZY_ZAPIER_POST_INSTALL_URL="http://localhost:4200/#/pages/integrations/zapier"
# Comma-separated list of domains allowed for OAuth redirects (security feature)
ALLOWED_DOMAINS=gauzy.co,ever.co,staging.gauzy.co,demo.gauzy.co,zapier.com,github.com,upwork.com,hubstaff.com,fiverr.com
MAX_AUTH_CODES=1000

# Github App Install Integration
GAUZY_GITHUB_APP_NAME=
Expand Down
3 changes: 3 additions & 0 deletions .env.local
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,9 @@ GAUZY_ZAPIER_CLIENT_ID=
GAUZY_ZAPIER_CLIENT_SECRET=
GAUZY_ZAPIER_REDIRECT_URL=http://localhost:3000/api/integration/zapier/oauth/callback
GAUZY_ZAPIER_POST_INSTALL_URL="http://localhost:4200/#/pages/integrations/zapier"
# Comma-separated list of domains allowed for OAuth redirects (security feature)
ALLOWED_DOMAINS=gauzy.co,ever.co,staging.gauzy.co,demo.gauzy.co,zapier.com,github.com,upwork.com,hubstaff.com,fiverr.com
MAX_AUTH_CODES=1000

# Third Party Integration Config
INTEGRATED_USER_DEFAULT_PASS=
Expand Down
3 changes: 3 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@ GAUZY_ZAPIER_CLIENT_ID=XXXXXXXXX
GAUZY_ZAPIER_CLIENT_SECRET=XXXXXXX
GAUZY_ZAPIER_REDIRECT_URL=http://localhost:3000/api/integration/zapier/oauth/callback
GAUZY_ZAPIER_POST_INSTALL_URL="http://localhost:4200/#/pages/integrations/zapier"
# Comma-separated list of domains allowed for OAuth redirects (security feature)
ALLOWED_DOMAINS=gauzy.co,ever.co,staging.gauzy.co,demo.gauzy.co,zapier.com,github.com,upwork.com,hubstaff.com,fiverr.com
MAX_AUTH_CODES=1000

FIVERR_CLIENT_ID=XXXXXXX
FIVERR_CLIENT_SECRET=XXXXXXX
Expand Down
6 changes: 6 additions & 0 deletions packages/common/src/lib/interfaces/IZapierConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,15 @@ export interface IZapierConfig {
/** OAuth client secret provided by Zapier for secure authentication */
readonly clientSecret: string;

/** Allowed domains for Zapier OAuth redirects */
readonly allowedDomains: string[];

/** URI where Zapier will redirect after successful OAuth authorization */
readonly redirectUri: string;

/** Optional URL where users will be directed after installing the Zapier integration */
readonly postInstallUrl?: string;

/** Max authentication code number */
readonly maxAuthCodes?: number;
}
3 changes: 3 additions & 0 deletions packages/config/src/lib/config/zapier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export default registerAs (
// Zapier OAuth Client Secret
clientSecret: process.env.GAUZY_ZAPIER_CLIENT_SECRET || '',

// Zapier allowed domains
allowedDomains: process.env.GAUZY_ALLOWED_DOMAINS ? process.env.GAUZY_ALLOWED_DOMAINS.split(',').map(domain => domain.trim()): [],

// Zapier Redirected URI after successful authentication
redirectUri: process.env.GAUZY_ZAPIER_REDIRECT_URL || '',

Expand Down
2 changes: 2 additions & 0 deletions packages/config/src/lib/environments/environment.prod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ export const environment: IEnvironment = {
zapier: {
clientId: process.env.GAUZY_ZAPIER_CLIENT_ID,
clientSecret: process.env.GAUZY_ZAPIER_CLIENT_SECRET,
allowedDomains: process.env.GAUZY_ALLOWED_DOMAINS ? process.env.GAUZY_ALLOWED_DOMAINS.split(',').map(domain => domain.trim()) : [],
maxAuthCodes: Number.parseInt(process.env.MAX_AUTH_CODES) || 1000,
redirectUri:
process.env.GAUZY_ZAPIER_REDIRECT_URL || `${process.env.API_BASE_URL}/api/integrations/zapier/callback`,
postInstallUrl:
Expand Down
2 changes: 2 additions & 0 deletions packages/config/src/lib/environments/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ export const environment: IEnvironment = {
zapier: {
clientId: process.env.GAUZY_ZAPIER_CLIENT_ID,
clientSecret: process.env.GAUZY_ZAPIER_CLIENT_SECRET,
allowedDomains: process.env.GAUZY_ALLOWED_DOMAINS ? process.env.GAUZY_ALLOWED_DOMAINS.split(',') : [],
maxAuthCodes: Number.parseInt(process.env.MAX_AUTH_CODES) || 1000,
redirectUri:
process.env.GAUZY_ZAPIER_REDIRECT_URL || `${process.env.API_BASE_URL}/api/integrations/zapier/callback`,
postInstallUrl:
Expand Down
9 changes: 9 additions & 0 deletions packages/contracts/src/lib/zapier.model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { IBasePerTenantAndOrganizationEntityModel } from "./base-entity.model";
import { ITimeLog } from "./timesheet.model";

export interface IZapierAccessTokens {
access_token: string;
Expand All @@ -17,6 +18,14 @@ export interface ICreateZapierIntegrationInput extends IBasePerTenantAndOrganiza
client_secret: string;
}

export type ActionType = 'start' | 'stop';

export interface ITimerZapierWebhookData extends IBasePerTenantAndOrganizationEntityModel {
event: string;
action: ActionType,
data: ITimeLog;
}

export interface IZapierEndpoint {
id: string;
name: string;
Expand Down
1 change: 0 additions & 1 deletion packages/core/src/lib/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,6 @@ export class AuthController {
async login(@Body() input: UserLoginDTO): Promise<IAuthResponse | null> {
return await this.commandBus.execute(new AuthLoginCommand(input));
}

/**
* Sign in workspaces by email and password.
*
Expand Down
31 changes: 31 additions & 0 deletions packages/core/src/lib/core/context/request-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,37 @@ export class RequestContext {
}
}

/**
* Retrieves the current organization ID from the request context.
* @returns {string | null} - The current organization ID if available, otherwise null.
*/
static currentOrganizationId(): ID | null {
try {
// Retrieve the current user from the request context
const user: IUser = RequestContext.currentUser();
if(!user) {
return null;
}
// First check if lastOrganizationId exists (most recently used organization)
if (user.lastOrganizationId) {
return user.lastOrganizationId;
}

// If not, check defaultOrganizationId
if (user.defaultOrganizationId) {
return user.defaultOrganizationId;
}

// If none of the above, try to get the first organization from the organizations array
if (user.organizations && user.organizations.length > 0) {
return user.organizations[0].organizationId;
}
return null;
} catch (error) {
// Return null if an error occurs
return null;
}
}
/**
* Retrieves the current user from the request context.
* @param {boolean} throwError - Flag indicating whether to throw an error if user is not found.
Expand Down
4 changes: 4 additions & 0 deletions packages/plugins/integration-zapier/src/lib/handlers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { ZapierTimerStartedHandler } from './zapier-timer-started.handler';
import { ZapierTimerStoppedHandler } from './zapier-timer-stopped.handler';

export const EventHandlers = [ZapierTimerStartedHandler, ZapierTimerStoppedHandler];
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
import { Injectable } from '@nestjs/common';
import { TimerStartedEvent } from '@gauzy/core';
import { ZapierWebhookService } from '../zapier-webhook.service';

@Injectable()
@EventsHandler(TimerStartedEvent)
export class ZapierTimerStartedHandler implements IEventHandler<TimerStartedEvent> {
constructor(private readonly zapierWebhookService: ZapierWebhookService) { }

/**
* Handles the TimerStartedEvent by notifying Zapier webhooks
*
* @param event - The TimerStartedEvent that contains the time log details
* @returns A Promise that resolves once the webhooks are notified
*/
async handle(event: TimerStartedEvent): Promise<void> {
const timeLog = event.timeLog;
if (!timeLog.tenantId || !timeLog.organizationId) {
console.warn('Cannot process timer started event: missing tenantId or organizationId')
}
await this.zapierWebhookService.notifyTimerStatusChanged({
event: 'timer.status.changed',
action: 'start',
data: timeLog,
tenantId: timeLog.tenantId,
organizationId: timeLog.organizationId
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
import { Injectable } from '@nestjs/common';
import { TimerStoppedEvent } from '@gauzy/core';
import { ZapierWebhookService } from '../zapier-webhook.service';

@Injectable()
@EventsHandler(TimerStoppedEvent)
export class ZapierTimerStoppedHandler implements IEventHandler<TimerStoppedEvent> {
constructor(private readonly zapierWebhookService: ZapierWebhookService) {}

/**
* Handles the TimerStoppedEvent by notifying Zapier webhooks
*
* @param event - The TimerStoppedEvent that contains the time log details.
* @returns A Promise that resolves once the webhooks are notified
*/
async handle(event: TimerStoppedEvent): Promise<void> {
const timeLog = event.timeLog;
if (!timeLog.tenantId || !timeLog.organizationId) {
console.warn('Cannot process timer stopped event: missing tenantId or organizationId')
return;
}
await this.zapierWebhookService.notifyTimerStatusChanged({
event: 'timer.status.changed',
action: 'stop',
data: timeLog,
tenantId: timeLog.tenantId,
organizationId: timeLog.organizationId
});
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
import { Logger } from '@nestjs/common';
import * as chalk from 'chalk';
import { ApplicationPluginConfig, CustomEmbeddedFieldConfig, CustomEmbeddedFields } from '@gauzy/common';
import { ApplicationPluginConfig, CustomEmbeddedFieldConfig } from '@gauzy/common';
import { GauzyCorePlugin as Plugin, IOnPluginBootstrap, IOnPluginDestroy } from '@gauzy/plugin';
import { ZapierModule } from './zapier.module';
import { ZapierWebhookSubscription } from './zapier-webhook-subscription.entity';

// Extend the CustomEmbeddedFields interface to include our custom entities
interface ZapierCustomFields extends CustomEmbeddedFields {
IntegrationSetting?: CustomEmbeddedFieldConfig[];
ZapierWebhookSubscription?: CustomEmbeddedFieldConfig[];
}
import { ConfigService } from '@nestjs/config';

@Plugin({
/**
Expand Down Expand Up @@ -78,25 +72,31 @@ interface ZapierCustomFields extends CustomEmbeddedFields {
}
})
export class IntegrationZapierPlugin implements IOnPluginBootstrap, IOnPluginDestroy {
// We enable by default additional logging for each event to avoid cluttering the logs
private logEnabled = true;
private readonly logger = new Logger(IntegrationZapierPlugin.name);

constructor(private readonly _config: ConfigService) { }
/**
* Called when the plugin is being initialized.
*/
onPluginBootstrap(): void | Promise<void> {
if (this.logEnabled) {
console.log(chalk.green(`${IntegrationZapierPlugin.name} is being bootstrapped...`));
this.logger.log(`${IntegrationZapierPlugin.name} is being bootstrapped...`);

// Log Zapier configuration status
const clientId = this._config.get<string>('zapier.clientId');
const clientSecret = this._config.get<string>('zapier.clientSecret');
const apiBaseUrl = this._config.get<string>('baseUrl');
if (!clientId || !clientSecret) {
this.logger.warn('Zapier OAuth credentials not fully configured! Please set GAUZY_ZAPIER_CLIENT_ID and GAUZY_ZAPIER_CLIENT_SECRET')
} else {
this.logger.log('Zapier OAuth credentials configured successfully');
}
this.logger.debug(`Zapier API base URL: ${apiBaseUrl}`);
}

/**
* Called when the plugin is being destroyed.
*/
onPluginDestroy(): void | Promise<void> {
if (this.logEnabled) {
const logger = new Logger(IntegrationZapierPlugin.name);
logger.log(`${IntegrationZapierPlugin.name} is being destroyed...`)
}
this.logger.log(`${IntegrationZapierPlugin.name} is being destroyed...`)
}
}
Loading
Loading