Files
b2bcall/apps/api/src/extensions/extensions.controller.ts
B2BCall Bootstrap 8a41b5d2b2 feat: add trunks and extensions CRUD with realtime PJSIP provisioning
- apps/api/src/telephony: TelephonyModule (conexao AMI da API para
  comandos de controle) + PjsipRealtimeService (unico ponto de escrita nas
  tabelas realtime do schema 'asterisk' via Prisma $executeRaw
  parametrizado)
- apps/api/src/trunks: CRUD completo (tipos IP/AUTH/REGISTRATION), secret
  cifrado em repouso (AES-256-GCM), endpoint de teste de status via
  PJSIPShowContacts/pjsip show registration
- apps/api/src/extensions: CRUD completo, senha SIP gerada
  automaticamente, endpoint de reset, nunca reexibe a senha apos a criacao
- apps/api/src/monitoring: GET /api/monitoring/extensions com prioridade
  de cor (agente.md secao 15) a partir do ExtensionState alimentado por
  apps/asterisk-events
- health check agora reporta status real do Asterisk/AMI via heartbeat do
  asterisk-events no Redis, sem precisar de conexao AMI propria para isso

Testado ponta a ponta contra o Asterisk real: ramal e tronco criados via
API aparecem imediatamente via 'pjsip show endpoint' sem reload (realtime
funcionando), secret nunca retornado pela API, exclusao em cascata
confirmada (ps_endpoints/ps_auths/ps_aors/ps_contacts/ps_registrations).
2026-08-27 12:58:12 -03:00

89 lines
2.3 KiB
TypeScript

import {
Body,
Controller,
Delete,
Get,
Param,
ParseUUIDPipe,
Patch,
Post,
Req,
} from '@nestjs/common';
import type { FastifyRequest } from 'fastify';
import { RequirePermissions } from '../common/decorators/permissions.decorator';
import { CurrentUser } from '../common/decorators/current-user.decorator';
import type { AuthenticatedUser } from '../common/guards/auth.guard';
import { ExtensionsService } from './extensions.service';
import { CreateExtensionDto } from './dto/create-extension.dto';
import { UpdateExtensionDto } from './dto/update-extension.dto';
@Controller('extensions')
export class ExtensionsController {
constructor(private readonly extensionsService: ExtensionsService) {}
@Get()
@RequirePermissions('extensions.view')
list() {
return this.extensionsService.list();
}
@Get(':id')
@RequirePermissions('extensions.view')
findOne(@Param('id', ParseUUIDPipe) id: string) {
return this.extensionsService.findByIdOrThrow(id);
}
@Post()
@RequirePermissions('extensions.create')
create(
@Body() dto: CreateExtensionDto,
@CurrentUser() actor: AuthenticatedUser,
@Req() request: FastifyRequest,
) {
return this.extensionsService.create(dto, actor, {
ip: request.ip,
userAgent: request.headers['user-agent'],
});
}
@Patch(':id')
@RequirePermissions('extensions.update')
update(
@Param('id', ParseUUIDPipe) id: string,
@Body() dto: UpdateExtensionDto,
@CurrentUser() actor: AuthenticatedUser,
@Req() request: FastifyRequest,
) {
return this.extensionsService.update(id, dto, actor, {
ip: request.ip,
userAgent: request.headers['user-agent'],
});
}
@Post(':id/reset-password')
@RequirePermissions('extensions.update')
resetPassword(
@Param('id', ParseUUIDPipe) id: string,
@CurrentUser() actor: AuthenticatedUser,
@Req() request: FastifyRequest,
) {
return this.extensionsService.resetPassword(id, actor, {
ip: request.ip,
userAgent: request.headers['user-agent'],
});
}
@Delete(':id')
@RequirePermissions('extensions.delete')
remove(
@Param('id', ParseUUIDPipe) id: string,
@CurrentUser() actor: AuthenticatedUser,
@Req() request: FastifyRequest,
) {
return this.extensionsService.delete(id, actor, {
ip: request.ip,
userAgent: request.headers['user-agent'],
});
}
}