Add VoIP webhook connector for call logging, notifications, and optional click-to-call; align brand to Montact.
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Headers,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
Req,
|
||||
UnauthorizedException,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import type { Request } from 'express';
|
||||
import { JwtAuthGuard } from '../../auth/jwt-auth.guard';
|
||||
import { CurrentUser } from '../../auth/current-user.decorator';
|
||||
import { Roles } from '../../auth/roles.decorator';
|
||||
import { RolesGuard } from '../../auth/roles.guard';
|
||||
import { OrgModuleGuard } from '../../billing/org-module.guard';
|
||||
import { RequireOrgModule } from '../../billing/org-module.decorator';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
import type { VoipWebhookEvent } from '@mento/shared';
|
||||
import { VoipConnectionsService } from './connections.service';
|
||||
import { VoipCallsService } from './calls.service';
|
||||
import {
|
||||
CreateVoipConnectionDto,
|
||||
UpdateVoipConnectionDto,
|
||||
VoipClickToCallDto,
|
||||
} from './dto';
|
||||
|
||||
@Controller('integrations/voip')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard, OrgModuleGuard)
|
||||
@RequireOrgModule('voip')
|
||||
export class VoipController {
|
||||
constructor(
|
||||
private readonly connections: VoipConnectionsService,
|
||||
private readonly calls: VoipCallsService,
|
||||
) {}
|
||||
|
||||
@Get('connections')
|
||||
@Roles('owner', 'admin')
|
||||
list(@CurrentUser() user: { organizationId: string }) {
|
||||
return this.connections.list(user.organizationId);
|
||||
}
|
||||
|
||||
@Post('connections')
|
||||
@Roles('owner', 'admin')
|
||||
create(
|
||||
@CurrentUser() user: { organizationId: string },
|
||||
@Body() dto: CreateVoipConnectionDto,
|
||||
) {
|
||||
return this.connections.create(user.organizationId, dto);
|
||||
}
|
||||
|
||||
@Patch('connections/:id')
|
||||
@Roles('owner', 'admin')
|
||||
update(
|
||||
@CurrentUser() user: { organizationId: string },
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateVoipConnectionDto,
|
||||
) {
|
||||
return this.connections.update(user.organizationId, id, dto);
|
||||
}
|
||||
|
||||
@Delete('connections/:id')
|
||||
@Roles('owner', 'admin')
|
||||
async remove(
|
||||
@CurrentUser() user: { organizationId: string },
|
||||
@Param('id') id: string,
|
||||
) {
|
||||
await this.connections.remove(user.organizationId, id);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
@Get('calls')
|
||||
@Roles('owner', 'admin')
|
||||
recent(
|
||||
@CurrentUser() user: { organizationId: string },
|
||||
@Query('connectionId') connectionId?: string,
|
||||
) {
|
||||
return this.calls.listRecent(user.organizationId, connectionId);
|
||||
}
|
||||
|
||||
@Post('click-to-call')
|
||||
clickToCall(
|
||||
@CurrentUser() user: { organizationId: string; userId: string },
|
||||
@Body() dto: VoipClickToCallDto,
|
||||
) {
|
||||
return this.calls.clickToCall(
|
||||
user.organizationId,
|
||||
user.userId,
|
||||
dto.number,
|
||||
dto.personId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Controller('public/integrations/voip')
|
||||
export class VoipWebhookController {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly connections: VoipConnectionsService,
|
||||
private readonly calls: VoipCallsService,
|
||||
) {}
|
||||
|
||||
@Post(':connectionId/webhook')
|
||||
async webhook(
|
||||
@Param('connectionId') connectionId: string,
|
||||
@Headers('x-voip-signature') signature: string | undefined,
|
||||
@Headers('x-montact-signature') montactSignature: string | undefined,
|
||||
@Req() req: Request & { rawBody?: Buffer },
|
||||
@Body() body: VoipWebhookEvent,
|
||||
) {
|
||||
const row = await this.prisma.voipConnection.findFirst({
|
||||
where: { id: connectionId, enabled: true },
|
||||
});
|
||||
if (!row) throw new UnauthorizedException('اتصال نامعتبر است');
|
||||
|
||||
const secret = await this.connections.decryptWebhookSecret(row);
|
||||
if (secret) {
|
||||
const raw =
|
||||
req.rawBody ?? Buffer.from(JSON.stringify(body ?? {}), 'utf8');
|
||||
const ok = this.connections.verifySignature(
|
||||
raw,
|
||||
signature ?? montactSignature,
|
||||
secret,
|
||||
);
|
||||
if (!ok) throw new UnauthorizedException('امضای وبهوک نامعتبر است');
|
||||
}
|
||||
|
||||
if (!body?.callId) {
|
||||
return { ok: false, error: 'callId required' };
|
||||
}
|
||||
|
||||
const result = await this.calls.ingestWebhook(connectionId, body);
|
||||
return { ok: true, ...result };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user