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 }; } }