Files
CRM_Montzar/apps/api/src/integrations/voip/voip.controller.ts
T

140 lines
3.9 KiB
TypeScript

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