Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions src/api/controllers/template.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,15 @@ export class TemplateController {
public async findTemplate(instance: InstanceDto) {
return this.templateService.find(instance);
}

public async editTemplate(
instance: InstanceDto,
data: { templateId: string; category?: string; components?: any; allowCategoryChange?: boolean; ttl?: number },
) {
return this.templateService.edit(instance, data);
}

public async deleteTemplate(instance: InstanceDto, data: { name: string; hsmId?: string }) {
return this.templateService.delete(instance, data);
}
}
13 changes: 13 additions & 0 deletions src/api/dto/template.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,16 @@ export class TemplateDto {
components: any;
webhookUrl?: string;
}

export class TemplateEditDto {
templateId: string;
category?: 'AUTHENTICATION' | 'MARKETING' | 'UTILITY';
allowCategoryChange?: boolean;
ttl?: number;
components?: any;
}

export class TemplateDeleteDto {
name: string;
hsmId?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ import { createId as cuid } from '@paralleldrive/cuid2';
import { Instance, Message } from '@prisma/client';
import { createJid } from '@utils/createJid';
import { fetchLatestWaWebVersion } from '@utils/fetchLatestWaWebVersion';
import {makeProxyAgent, makeProxyAgentUndici} from '@utils/makeProxyAgent';
import { makeProxyAgent, makeProxyAgentUndici } from '@utils/makeProxyAgent';
import { getOnWhatsappCache, saveOnWhatsappCache } from '@utils/onWhatsappCache';
import { status } from '@utils/renderStatus';
import { sendTelemetry } from '@utils/sendTelemetry';
Expand Down
36 changes: 35 additions & 1 deletion src/api/routes/template.router.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { RouterBroker } from '@api/abstract/abstract.router';
import { InstanceDto } from '@api/dto/instance.dto';
import { TemplateDto } from '@api/dto/template.dto';
import { TemplateDeleteDto, TemplateDto, TemplateEditDto } from '@api/dto/template.dto';
import { templateController } from '@api/server.module';
import { ConfigService } from '@config/env.config';
import { createMetaErrorResponse } from '@utils/errorResponse';
import { templateDeleteSchema } from '@validate/templateDelete.schema';
import { templateEditSchema } from '@validate/templateEdit.schema';
import { instanceSchema, templateSchema } from '@validate/validate.schema';
import { RequestHandler, Router } from 'express';

Expand Down Expand Up @@ -35,6 +37,38 @@ export class TemplateRouter extends RouterBroker {
res.status(errorResponse.status).json(errorResponse);
}
})
.post(this.routerPath('edit'), ...guards, async (req, res) => {
try {
const response = await this.dataValidate<TemplateEditDto>({
request: req,
schema: templateEditSchema,
ClassRef: TemplateEditDto,
execute: (instance, data) => templateController.editTemplate(instance, data),
});

res.status(HttpStatus.OK).json(response);
} catch (error) {
console.error('Template edit error:', error);
const errorResponse = createMetaErrorResponse(error, 'template_edit');
res.status(errorResponse.status).json(errorResponse);
}
})
.delete(this.routerPath('delete'), ...guards, async (req, res) => {
try {
const response = await this.dataValidate<TemplateDeleteDto>({
request: req,
schema: templateDeleteSchema,
ClassRef: TemplateDeleteDto,
execute: (instance, data) => templateController.deleteTemplate(instance, data),
});

res.status(HttpStatus.OK).json(response);
} catch (error) {
console.error('Template delete error:', error);
const errorResponse = createMetaErrorResponse(error, 'template_delete');
res.status(errorResponse.status).json(errorResponse);
}
})
.get(this.routerPath('find'), ...guards, async (req, res) => {
try {
const response = await this.dataValidate<InstanceDto>({
Expand Down
105 changes: 105 additions & 0 deletions src/api/services/template.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,77 @@ export class TemplateService {
}
}

public async edit(
instance: InstanceDto,
data: { templateId: string; category?: string; components?: any; allowCategoryChange?: boolean; ttl?: number },
) {
const getInstance = await this.waMonitor.waInstances[instance.instanceName].instance;
if (!getInstance) {
throw new Error('Instance not found');
}

this.businessId = getInstance.businessId;
this.token = getInstance.token;

const payload: Record<string, unknown> = {};
if (typeof data.category === 'string') payload.category = data.category;
if (typeof data.allowCategoryChange === 'boolean') payload.allow_category_change = data.allowCategoryChange;
if (typeof data.ttl === 'number') payload.time_to_live = data.ttl;
if (data.components) payload.components = data.components;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Check for empty array or object in components assignment.

The current logic excludes empty arrays or objects from assignment, which may be valid. Use a check like data.components !== undefined to allow empty values if appropriate.

Suggested change
if (data.components) payload.components = data.components;
if (data.components !== undefined) payload.components = data.components;


const response = await this.requestEditTemplate(data.templateId, payload);

if (!response || response.error) {
if (response && response.error) {
const metaError = new Error(response.error.message || 'WhatsApp API Error');
(metaError as any).template = response.error;
throw metaError;
}
throw new Error('Error to edit template');
}

return response;
}

public async delete(instance: InstanceDto, data: { name: string; hsmId?: string }) {
const getInstance = await this.waMonitor.waInstances[instance.instanceName].instance;
if (!getInstance) {
throw new Error('Instance not found');
}

this.businessId = getInstance.businessId;
this.token = getInstance.token;

const response = await this.requestDeleteTemplate({ name: data.name, hsm_id: data.hsmId });

if (!response || response.error) {
if (response && response.error) {
const metaError = new Error(response.error.message || 'WhatsApp API Error');
(metaError as any).template = response.error;
throw metaError;
}
throw new Error('Error to delete template');
}

try {
// Best-effort local cleanup of stored template metadata
await this.prismaRepository.template.deleteMany({
where: {
OR: [
{ name: data.name, instanceId: getInstance.id },
data.hsmId ? { templateId: data.hsmId, instanceId: getInstance.id } : undefined,
].filter(Boolean) as any,
},
});
Comment on lines +145 to +152
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Avoid using 'as any' in Prisma query filter.

Explicitly define the filter type or refactor the query to maintain type safety and prevent hidden type errors.

Suggested change
await this.prismaRepository.template.deleteMany({
where: {
OR: [
{ name: data.name, instanceId: getInstance.id },
data.hsmId ? { templateId: data.hsmId, instanceId: getInstance.id } : undefined,
].filter(Boolean) as any,
},
});
const orFilter: Array<{ name?: string; templateId?: string; instanceId: string }> = [
{ name: data.name, instanceId: getInstance.id },
];
if (data.hsmId) {
orFilter.push({ templateId: data.hsmId, instanceId: getInstance.id });
}
await this.prismaRepository.template.deleteMany({
where: {
OR: orFilter,
},
});

} catch (err) {
this.logger.warn(
`Failed to cleanup local template records after delete: ${(err as Error)?.message || String(err)}`,
);
}

return response;
}

private async requestTemplate(data: any, method: string) {
try {
let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL;
Expand Down Expand Up @@ -116,4 +187,38 @@ export class TemplateService {
throw new Error(`Connection error: ${e.message}`);
}
}

private async requestEditTemplate(templateId: string, data: any) {
try {
let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL;
const version = this.configService.get<WaBusiness>('WA_BUSINESS').VERSION;
urlServer = `${urlServer}/${version}/${templateId}`;
const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` };
const result = await axios.post(urlServer, data, { headers });
return result.data;
} catch (e) {
this.logger.error(
'WhatsApp API request error: ' + (e.response?.data ? JSON.stringify(e.response?.data) : e.message),
);
if (e.response?.data) return e.response.data;
throw new Error(`Connection error: ${e.message}`);
}
}

private async requestDeleteTemplate(params: { name: string; hsm_id?: string }) {
try {
let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL;
const version = this.configService.get<WaBusiness>('WA_BUSINESS').VERSION;
urlServer = `${urlServer}/${version}/${this.businessId}/message_templates`;
const headers = { Authorization: `Bearer ${this.token}` };
const result = await axios.delete(urlServer, { headers, params });
return result.data;
} catch (e) {
this.logger.error(
'WhatsApp API request error: ' + (e.response?.data ? JSON.stringify(e.response?.data) : e.message),
);
if (e.response?.data) return e.response.data;
throw new Error(`Connection error: ${e.message}`);
}
}
}
35 changes: 17 additions & 18 deletions src/utils/makeProxyAgent.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { HttpsProxyAgent } from 'https-proxy-agent';
import { SocksProxyAgent } from 'socks-proxy-agent';

import { ProxyAgent } from 'undici'
import { ProxyAgent } from 'undici';

type Proxy = {
host: string;
Expand Down Expand Up @@ -46,38 +45,38 @@
}

export function makeProxyAgentUndici(proxy: Proxy | string): ProxyAgent {
let proxyUrl: string
let protocol: string
let proxyUrl: string;
let protocol: string;

if (typeof proxy === 'string') {
const url = new URL(proxy)
protocol = url.protocol.replace(':', '')
proxyUrl = proxy
const url = new URL(proxy);
protocol = url.protocol.replace(':', '');
proxyUrl = proxy;
} else {
const { host, password, port, protocol: proto, username } = proxy
protocol = (proto || 'http').replace(':', '')
const { host, password, port, protocol: proto, username } = proxy;

Check failure

Code scanning / CodeQL

Insecure randomness High

This uses a cryptographically insecure random number generated at
Math.random()
in a security context.

Check failure

Code scanning / CodeQL

Insecure randomness High

This uses a cryptographically insecure random number generated at
Math.random()
in a security context.

Check failure

Code scanning / CodeQL

Insecure randomness High

This uses a cryptographically insecure random number generated at
Math.random()
in a security context.

Check failure

Code scanning / CodeQL

Insecure randomness High

This uses a cryptographically insecure random number generated at
Math.random()
in a security context.

Check failure

Code scanning / CodeQL

Insecure randomness High

This uses a cryptographically insecure random number generated at
Math.random()
in a security context.
protocol = (proto || 'http').replace(':', '');

if (protocol === 'socks') {
protocol = 'socks5'
protocol = 'socks5';
}

const auth = username && password ? `${username}:${password}@` : ''
proxyUrl = `${protocol}://${auth}${host}:${port}`
const auth = username && password ? `${username}:${password}@` : '';
proxyUrl = `${protocol}://${auth}${host}:${port}`;
}

const PROXY_HTTP_PROTOCOL = 'http'
const PROXY_HTTPS_PROTOCOL = 'https'
const PROXY_SOCKS4_PROTOCOL = 'socks4'
const PROXY_SOCKS5_PROTOCOL = 'socks5'
const PROXY_HTTP_PROTOCOL = 'http';
const PROXY_HTTPS_PROTOCOL = 'https';
const PROXY_SOCKS4_PROTOCOL = 'socks4';
const PROXY_SOCKS5_PROTOCOL = 'socks5';

switch (protocol) {
case PROXY_HTTP_PROTOCOL:
case PROXY_HTTPS_PROTOCOL:
case PROXY_SOCKS4_PROTOCOL:
case PROXY_SOCKS5_PROTOCOL:
return new ProxyAgent(proxyUrl)
return new ProxyAgent(proxyUrl);

default:
throw new Error(`Unsupported proxy protocol: ${protocol}`)
throw new Error(`Unsupported proxy protocol: ${protocol}`);
}
}
32 changes: 32 additions & 0 deletions src/validate/templateDelete.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { JSONSchema7 } from 'json-schema';
import { v4 } from 'uuid';

const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => {
const properties: Record<string, unknown> = {};
propertyNames.forEach(
(property) =>
(properties[property] = {
minLength: 1,
description: `The "${property}" cannot be empty`,
}),
);
return {
if: {
propertyNames: {
enum: [...propertyNames],
},
},
then: { properties },
} as JSONSchema7;
};

export const templateDeleteSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
name: { type: 'string' },
hsmId: { type: 'string' },
},
required: ['name'],
...isNotEmpty('name'),
};
35 changes: 35 additions & 0 deletions src/validate/templateEdit.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { JSONSchema7 } from 'json-schema';
import { v4 } from 'uuid';

const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => {
const properties: Record<string, unknown> = {};
propertyNames.forEach(
(property) =>
(properties[property] = {
minLength: 1,
description: `The "${property}" cannot be empty`,
}),
);
return {
if: {
propertyNames: {
enum: [...propertyNames],
},
},
then: { properties },
} as JSONSchema7;
};

export const templateEditSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
templateId: { type: 'string' },
category: { type: 'string', enum: ['AUTHENTICATION', 'MARKETING', 'UTILITY'] },
allowCategoryChange: { type: 'boolean' },
ttl: { type: 'number' },
components: { type: 'array' },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Specify item type for components array in schema.

Defining items: { type: 'object' } for the components array will ensure proper validation and prevent invalid data.

Suggested change
components: { type: 'array' },
components: {
type: 'array',
items: { type: 'object' }
},

},
required: ['templateId'],
...isNotEmpty('templateId'),
};
2 changes: 2 additions & 0 deletions src/validate/validate.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ export * from './message.schema';
export * from './proxy.schema';
export * from './settings.schema';
export * from './template.schema';
export * from './templateDelete.schema';
export * from './templateEdit.schema';
export * from '@api/integrations/chatbot/chatbot.schema';
export * from '@api/integrations/event/event.schema';