Skip to main content

Gravar áudio de ditado no exame — API

Recebe um arquivo de áudio (gravado pelo microfone do navegador durante o laudo ou a visualização de imagens) e o anexa a um exame. O arquivo é enviado primeiro ao storage de objetos e só depois registrado no banco; a resposta devolve um link temporário para reprodução imediata.

Funcionamento​

  1. Autentica o ator (JwtAuthenticationGuard) e verifica a permissão exam:add-audio no escopo do exame informado (AuthorizationGuard, ver visão geral do módulo).
  2. Exige um arquivo no campo multipart file; sem arquivo, rejeita.
  3. Identifica o formato real do arquivo pela assinatura binária (AudioContentTypeDetector); um formato não suportado é rejeitado antes de qualquer gravação.
  4. Busca o exame pelo examId do path; um exame inexistente ou fora do escopo do ator (oculto por RLS) é tratado como não encontrado.
  5. Monta o nome final do áudio — o campo audioName do corpo, se enviado, ou o nome original do arquivo multipart — e valida esse nome contra um padrão restrito antes de aceitar.
  6. Envia o conteúdo ao storage de objetos, em uma pasta isolada por tenant e unidade.
  7. Grava a linha em diagnosis_exam_audios, registra um evento de auditoria e agenda a reprojeção da worklist do exame, tudo na mesma transação.
  8. Gera um link de download pré-assinado (válido por 300 segundos) e devolve na resposta.

Endpoints​

MétodoRotaDescrição
POST/v1/exams/{examId}/audiosEnvia um áudio de ditado e o anexa ao exame

Versão: v1

Swagger: POST /exams/{examId}/audios · Rota (Dev): http://localhost:3000/v1/exams/{examId}/audios

Lógica de decisão da rota (validações e permissão → desfechos):

Permissões​

RotaGuardsPermissão exigida
POST /exams/{examId}/audiosJwtAuthenticationGuard, AuthorizationGuardexam:add-audio (PermissionName.EXAM_ADD_AUDIO), escopo resource sobre o exame (enforcement: rls)

Sem token válido → 401 UNAUTHENTICATED. Sem a permissão no escopo resolvido para o exame → 403 FORBIDDEN_ACTION. Um exame que existe mas está fora do escopo do ator não aparece como 403 — a consulta ao exame, já dentro da transação com RLS aplicado, simplesmente não o encontra, e a rota devolve 404.

Headers​

HeaderObrigatórioDescrição
AuthorizationSimBearer <access_token>
Content-TypeSimmultipart/form-data

Path parameters​

NomeTipoObrigatórioDescrição
examIduuidSimIdentificador do exame ao qual o áudio será anexado

Query parameters​

Nenhum.

Body​

Corpo multipart/form-data:

CampoTipoObrigatórioValidação
filebinárioSimAté 10 MB (MAX_AUDIO_UPLOAD_BYTES, constante no código); formato validado pela assinatura binária, não pela extensão nem pelo Content-Type da parte multipart
audioNamestringNão@Matches(/^[\w.\- ]{1,255}$/) no DTO. Se omitido, o serviço usa o nome original do arquivo enviado (file.originalname) — esse nome de fallback não passa pela validação do DTO, mas é revalidado com o mesmo padrão na camada de domínio (UploadedAudioInput), o que bloqueia tentativas de path traversal (/, \) vindas do nome original do upload

Response​

201 — sucesso:

json
{
"data": {
"link": "https://storage.exemplo.com/tenants/.../exam-audios/0199....audio?X-Amz-Expires=300&..."
}
}

link pode ser null se o áudio, por algum motivo, não tiver um caminho persistido — não observado no caminho feliz.

Erros​

Classe de erroerrorCodeStatusQuando ocorre
UnauthenticatedErrorUNAUTHENTICATED401token ausente, inválido ou expirado
ForbiddenActionFORBIDDEN_ACTION403ator sem exam:add-audio no escopo do exame
(validação de payload)BAD_REQUEST400examId não é um uuid válido, ou o campo audioName do corpo não casa com o padrão
BadRequestException (lançada direto no controller)BAD_REQUEST400nenhum arquivo enviado no campo file
— (PayloadTooLargeException, do multer)HTTP_EXCEPTION413arquivo maior que 10 MB
UnsupportedMediaTypeExceptionHTTP_EXCEPTION415assinatura binária do arquivo não corresponde a nenhum formato de áudio suportado
ScopedResourceNotFoundErrorRESOURCE_NOT_FOUND404exame inexistente, ou existente porém fora do escopo do ator (oculto por RLS)
DiagnosisAttachmentInputInvalidErrorATTACHMENT_INPUT_INVALID400nome final do áudio (originado do file.originalname, quando audioName não foi enviado) não casa com o padrão permitido
DiagnosisUnitNotFoundErrorUNIT_NOT_FOUND404a unidade do exame não foi encontrada no diretório de unidades (condição de borda)

O Swagger do controller documenta apenas os exemplos genéricos de 400 e 404 (payload inválido / "Unit not found."); os desfechos 413 e 415, embora reais no código (limite do FileInterceptor e do AudioContentTypeDetector), não têm @ApiResponse próprio no controller — divergência de documentação, não de comportamento.

Regras de negócio​

IDRegraComportamento esperado
RN-01Formato é validado pelo conteúdo, não pela declaração do clientea assinatura binária dos primeiros bytes decide o Content-Type canônico gravado no storage
RN-02Limite de tamanho é 10 MBconstante no código (MAX_AUDIO_UPLOAD_BYTES), aplicado pelo multer antes do controller
RN-03Nome final do áudio é sempre validado, mesmo vindo do nome original do uploadevita path traversal e caracteres não previstos, independente da origem do nome
RN-04O objeto salvo usa nome aleatório e extensão literal .audioo caminho gerado não carrega a extensão real do formato (.wav, .mp3...); o formato real só é conhecido pelo Content-Type gravado no storage no momento do upload
RN-05Upload no storage acontece antes do registro no bancose a escrita da linha falhar, o objeto recém-enviado é removido do storage (melhor esforço; falha na limpeza só gera log de aviso, não é reportada ao cliente)
RN-06Toda gravação bem-sucedida audita e reprojeta a worklistevento diagnosis.exam.audio-added (ação EXAM_AUDIO_ADDED) e scheduleExamProjection(examId), na mesma transação da inserção
RN-07O link devolvido expiraURL pré-assinada, validade de 300 segundos (PRESIGNED_AUDIO_LINK_EXPIRY_SECONDS, constante no código) — não é mais um link público permanente

Compliance​

Órgão / normaExigênciaComo a rota atende
LGPDminimização e controle de acesso ao dado de vozlink de download é temporário (300s) e pré-assinado, não um link público permanente; acesso à rota exige permissão específica no exame
HIPAAtrilha de quem adicionou o quêevento de auditoria diagnosis.exam.audio-added, com actorUserId, examId e audioId
ANVISA (indireto)integridade do conteúdo aceitoo tipo do arquivo é conferido pelo conteúdo real (assinatura binária), não por metadados que o cliente poderia falsificar

Variáveis de ambiente​

Nenhuma variável de ambiente é específica desta rota. Os dois limites relevantes são constantes no código:

ConstanteUsoValor
MAX_AUDIO_UPLOAD_BYTEStamanho máximo do arquivo aceito10 MB (10 * 1024 * 1024 bytes)
PRESIGNED_AUDIO_LINK_EXPIRY_SECONDSvalidade do link de download devolvido300 segundos

Tempo médio de resposta​

A confirmar — responsável: time de Diagnosis; data: 24/09/2026. Não há medição publicada; não foi executado neste levantamento.

Requisitos não funcionais​

RequisitoDefinição
IdempotênciaNão — cada chamada cria um novo registro e um novo objeto no storage, mesmo com o mesmo arquivo
PaginaçãoNão se aplica (rota de criação)
Rate limitNão há @Throttle nesta rota (diferente das rotas sensíveis de Authentication)
CacheNão
AuditoriaSim — evento diagnosis.exam.audio-added

Relacionado​

  • 🖥️ Tela: A confirmar — responsável: time de frontend; data: 24/09/2026. (a pasta docs/portal2/interface ainda não tem página de áudio nesta branch; este levantamento cobriu apenas o backend)
  • 📂 Módulo: Áudio
  • ▶️ Próximo passo: Ouvir e listar áudios