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
- Autentica o ator (
JwtAuthenticationGuard) e verifica a permissãoexam:add-audiono escopo do exame informado (AuthorizationGuard, ver visão geral do módulo). - Exige um arquivo no campo multipart
file; sem arquivo, rejeita. - Identifica o formato real do arquivo pela assinatura binária (
AudioContentTypeDetector); um formato não suportado é rejeitado antes de qualquer gravação. - Busca o exame pelo
examIddo path; um exame inexistente ou fora do escopo do ator (oculto por RLS) é tratado como não encontrado. - Monta o nome final do áudio — o campo
audioNamedo corpo, se enviado, ou o nome original do arquivo multipart — e valida esse nome contra um padrão restrito antes de aceitar. - Envia o conteúdo ao storage de objetos, em uma pasta isolada por tenant e unidade.
- 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. - Gera um link de download pré-assinado (válido por 300 segundos) e devolve na resposta.
Endpoints
| Método | Rota | Descrição |
|---|---|---|
| POST | /v1/exams/{examId}/audios | Envia 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
| Rota | Guards | Permissão exigida |
|---|---|---|
POST /exams/{examId}/audios | JwtAuthenticationGuard, AuthorizationGuard | exam: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
| Header | Obrigatório | Descrição |
|---|---|---|
Authorization | Sim | Bearer <access_token> |
Content-Type | Sim | multipart/form-data |
Path parameters
| Nome | Tipo | Obrigatório | Descrição |
|---|---|---|---|
examId | uuid | Sim | Identificador do exame ao qual o áudio será anexado |
Query parameters
Nenhum.
Body
Corpo multipart/form-data:
| Campo | Tipo | Obrigatório | Validação |
|---|---|---|---|
file | binário | Sim | Até 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 |
audioName | string | Nã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 erro | errorCode | Status | Quando ocorre |
|---|---|---|---|
UnauthenticatedError | UNAUTHENTICATED | 401 | token ausente, inválido ou expirado |
ForbiddenAction | FORBIDDEN_ACTION | 403 | ator sem exam:add-audio no escopo do exame |
| (validação de payload) | BAD_REQUEST | 400 | examId 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_REQUEST | 400 | nenhum arquivo enviado no campo file |
— (PayloadTooLargeException, do multer) | HTTP_EXCEPTION | 413 | arquivo maior que 10 MB |
UnsupportedMediaTypeException | HTTP_EXCEPTION | 415 | assinatura binária do arquivo não corresponde a nenhum formato de áudio suportado |
ScopedResourceNotFoundError | RESOURCE_NOT_FOUND | 404 | exame inexistente, ou existente porém fora do escopo do ator (oculto por RLS) |
DiagnosisAttachmentInputInvalidError | ATTACHMENT_INPUT_INVALID | 400 | nome final do áudio (originado do file.originalname, quando audioName não foi enviado) não casa com o padrão permitido |
DiagnosisUnitNotFoundError | UNIT_NOT_FOUND | 404 | a 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
400e404(payload inválido / "Unit not found."); os desfechos413e415, embora reais no código (limite doFileInterceptore doAudioContentTypeDetector), não têm@ApiResponsepróprio no controller — divergência de documentação, não de comportamento.
Regras de negócio
| ID | Regra | Comportamento esperado |
|---|---|---|
| RN-01 | Formato é validado pelo conteúdo, não pela declaração do cliente | a assinatura binária dos primeiros bytes decide o Content-Type canônico gravado no storage |
| RN-02 | Limite de tamanho é 10 MB | constante no código (MAX_AUDIO_UPLOAD_BYTES), aplicado pelo multer antes do controller |
| RN-03 | Nome final do áudio é sempre validado, mesmo vindo do nome original do upload | evita path traversal e caracteres não previstos, independente da origem do nome |
| RN-04 | O objeto salvo usa nome aleatório e extensão literal .audio | o 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-05 | Upload no storage acontece antes do registro no banco | se 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-06 | Toda gravação bem-sucedida audita e reprojeta a worklist | evento diagnosis.exam.audio-added (ação EXAM_AUDIO_ADDED) e scheduleExamProjection(examId), na mesma transação da inserção |
| RN-07 | O link devolvido expira | URL 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 / norma | Exigência | Como a rota atende |
|---|---|---|
| LGPD | minimização e controle de acesso ao dado de voz | link de download é temporário (300s) e pré-assinado, não um link público permanente; acesso à rota exige permissão específica no exame |
| HIPAA | trilha de quem adicionou o quê | evento de auditoria diagnosis.exam.audio-added, com actorUserId, examId e audioId |
| ANVISA (indireto) | integridade do conteúdo aceito | o 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:
| Constante | Uso | Valor |
|---|---|---|
MAX_AUDIO_UPLOAD_BYTES | tamanho máximo do arquivo aceito | 10 MB (10 * 1024 * 1024 bytes) |
PRESIGNED_AUDIO_LINK_EXPIRY_SECONDS | validade do link de download devolvido | 300 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
| Requisito | Definição |
|---|---|
| Idempotência | Não — cada chamada cria um novo registro e um novo objeto no storage, mesmo com o mesmo arquivo |
| Paginação | Não se aplica (rota de criação) |
| Rate limit | Não há @Throttle nesta rota (diferente das rotas sensíveis de Authentication) |
| Cache | Não |
| Auditoria | Sim — evento diagnosis.exam.audio-added |
Relacionado
- 🖥️ Tela:
A confirmar — responsável: time de frontend; data: 24/09/2026.(a pastadocs/portal2/interfaceainda 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